Vehicle Spawn Pad QoL (#802)

* preparation for redefining the checks involved in spawning a vehicle from a spawn pad

* pass the terminal from which a vehicle order was issued to be used in validation tests; implemented validation tests to ensure delayed orders remain valid until being executed; various messages about orders that have failed validation, as well as order changes and queue changes

* local zoning field on player for use in statusing and message composition

* expiration of a vehicle order for a given reason; linking of messages for expiration of vehicle order queue; death on an active vehicle pad (during rail operation)

* players that die to spawning vehicles can blame that vehicle's future driver; the calculations for server-side damage are heavily modified

* definitions for vehicle spawn pad kill box, used during vehicle generation to eliminate targets on the spawn pad, per kind of vehicle spawn pad

* reusing common order validation test for some stages of the vehicle order fulfillment process

* adjusts when the vehicle thinks it is mounted to the spawn pad; vehicle wreckage should be cleaned up quicker

* cancelling the active order
This commit is contained in:
Fate-JH 2021-05-05 10:13:57 -04:00 committed by GitHub
parent 42e4db8972
commit 7fca0a5582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1254 additions and 466 deletions

View file

@ -13,6 +13,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.serverobject.terminals.Terminal
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -29,11 +30,11 @@ class VehicleSpawnControl1Test extends ActorTest {
class VehicleSpawnControl2Test extends ActorTest { class VehicleSpawnControl2Test extends ActorTest {
"VehicleSpawnControl" should { "VehicleSpawnControl" should {
"complete a vehicle order" in { "complete a vehicle order" in {
val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) val (vehicle, player, pad, terminal, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR)
val probe = new TestProbe(system, "zone-events") val probe = new TestProbe(system, "zone-events")
zone.VehicleEvents = probe.ref //zone events zone.VehicleEvents = probe.ref //zone events
pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer])
probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage]) probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage])
@ -55,7 +56,7 @@ class VehicleSpawnControl2Test extends ActorTest {
class VehicleSpawnControl3Test extends ActorTest { class VehicleSpawnControl3Test extends ActorTest {
"VehicleSpawnControl" should { "VehicleSpawnControl" should {
"block the second vehicle order until the first is completed" in { "block the second vehicle order until the first is completed" in {
val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) val (vehicle, player, pad, terminal, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR)
//we can recycle the vehicle and the player for each order //we can recycle the vehicle and the player for each order
val probe = new TestProbe(system, "zone-events") val probe = new TestProbe(system, "zone-events")
val player2 = Player(Avatar(0, "test2", player.Faction, CharacterSex.Male, 0, CharacterVoice.Mute)) val player2 = Player(Avatar(0, "test2", player.Faction, CharacterSex.Male, 0, CharacterVoice.Mute))
@ -63,9 +64,9 @@ class VehicleSpawnControl3Test extends ActorTest {
player2.Continent = zone.id player2.Continent = zone.id
player2.Spawn() player2.Spawn()
zone.VehicleEvents = probe.ref //zone events zone.VehicleEvents = probe.ref //zone events
pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //first order pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //first order
pad.Actor ! VehicleSpawnPad.VehicleOrder(player2, vehicle) //second order (vehicle shared) pad.Actor ! VehicleSpawnPad.VehicleOrder(player2, vehicle, terminal) //second order (vehicle shared)
assert(probe.receiveOne(1 seconds) match { assert(probe.receiveOne(1 seconds) match {
case VehicleSpawnPad.PeriodicReminder(_, VehicleSpawnPad.Reminders.Queue, _) => true case VehicleSpawnPad.PeriodicReminder(_, VehicleSpawnPad.Reminders.Queue, _) => true
@ -103,12 +104,12 @@ class VehicleSpawnControl3Test extends ActorTest {
class VehicleSpawnControl4Test extends ActorTest { class VehicleSpawnControl4Test extends ActorTest {
"VehicleSpawnControl" should { "VehicleSpawnControl" should {
"clean up the vehicle if the driver-to-be is on the wrong continent" in { "clean up the vehicle if the driver-to-be is on the wrong continent" in {
val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) val (vehicle, player, pad, terminal, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR)
val probe = new TestProbe(system, "zone-events") val probe = new TestProbe(system, "zone-events")
zone.VehicleEvents = probe.ref zone.VehicleEvents = probe.ref
player.Continent = "problem" //problem player.Continent = "problem" //problem
pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order
val msg = probe.receiveOne(1 minute) val msg = probe.receiveOne(1 minute)
// assert( // assert(
@ -125,11 +126,11 @@ class VehicleSpawnControl4Test extends ActorTest {
class VehicleSpawnControl5Test extends ActorTest() { class VehicleSpawnControl5Test extends ActorTest() {
"VehicleSpawnControl" should { "VehicleSpawnControl" should {
"abandon a destroyed vehicle on the spawn pad (blocking)" in { "abandon a destroyed vehicle on the spawn pad (blocking)" in {
val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) val (vehicle, player, pad, terminal, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR)
val probe = new TestProbe(system, "zone-events") val probe = new TestProbe(system, "zone-events")
zone.VehicleEvents = probe.ref zone.VehicleEvents = probe.ref
pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer])
probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage]) probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage])
@ -152,11 +153,11 @@ class VehicleSpawnControl5Test extends ActorTest() {
class VehicleSpawnControl6Test extends ActorTest() { class VehicleSpawnControl6Test extends ActorTest() {
"VehicleSpawnControl" should { "VehicleSpawnControl" should {
"abandon a vehicle on the spawn pad if driver is unfit to drive (blocking)" in { "abandon a vehicle on the spawn pad if driver is unfit to drive (blocking)" in {
val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) val (vehicle, player, pad, terminal, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR)
val probe = new TestProbe(system, "zone-events") val probe = new TestProbe(system, "zone-events")
zone.VehicleEvents = probe.ref zone.VehicleEvents = probe.ref
pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer])
probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage]) probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage])
@ -179,12 +180,12 @@ class VehicleSpawnControl6Test extends ActorTest() {
class VehicleSpawnControl7Test extends ActorTest { class VehicleSpawnControl7Test extends ActorTest {
"VehicleSpawnControl" should { "VehicleSpawnControl" should {
"abandon a vehicle on the spawn pad if driver is unfit to drive (blocking)" in { "abandon a vehicle on the spawn pad if driver is unfit to drive (blocking)" in {
val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) val (vehicle, player, pad, terminal, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR)
val probe = new TestProbe(system, "zone-events") val probe = new TestProbe(system, "zone-events")
player.ExoSuit = ExoSuitType.MAX player.ExoSuit = ExoSuitType.MAX
zone.VehicleEvents = probe.ref //zone events zone.VehicleEvents = probe.ref //zone events
pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order
probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer])
probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage]) probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage])
@ -210,7 +211,7 @@ object VehicleSpawnPadControlTest {
def SetUpAgents( def SetUpAgents(
faction: PlanetSideEmpire.Value faction: PlanetSideEmpire.Value
)(implicit system: ActorSystem): (Vehicle, Player, VehicleSpawnPad, Zone) = { )(implicit system: ActorSystem): (Vehicle, Player, VehicleSpawnPad, Terminal, Zone) = {
import net.psforever.objects.guid.NumberPoolHub import net.psforever.objects.guid.NumberPoolHub
import net.psforever.objects.guid.source.MaxNumberSource import net.psforever.objects.guid.source.MaxNumberSource
import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.serverobject.structures.Building
@ -218,6 +219,7 @@ object VehicleSpawnPadControlTest {
import net.psforever.objects.Tool import net.psforever.objects.Tool
import net.psforever.types.CharacterSex import net.psforever.types.CharacterSex
val terminal = Terminal(GlobalDefinitions.vehicle_terminal_combined)
val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy)
val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool] val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool]
val guid: NumberPoolHub = new NumberPoolHub(MaxNumberSource(5)) val guid: NumberPoolHub = new NumberPoolHub(MaxNumberSource(5))
@ -252,6 +254,6 @@ object VehicleSpawnPadControlTest {
//note: pad and vehicle are both at Vector3(1,0,0) so they count as blocking //note: pad and vehicle are both at Vector3(1,0,0) so they count as blocking
pad.Position = Vector3(1, 0, 0) pad.Position = Vector3(1, 0, 0)
vehicle.Position = Vector3(1, 0, 0) vehicle.Position = Vector3(1, 0, 0)
(vehicle, player, pad, zone) (vehicle, player, pad, terminal, zone)
} }
} }

View file

@ -44,6 +44,7 @@ import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vital._ import net.psforever.objects.vital._
import net.psforever.objects.vital.base._ import net.psforever.objects.vital.base._
import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning} import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
@ -60,12 +61,7 @@ import net.psforever.services.local.support.{CaptureFlagManager, HackCaptureActo
import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
import net.psforever.services.properties.PropertyOverrideManager import net.psforever.services.properties.PropertyOverrideManager
import net.psforever.services.support.SupportActor import net.psforever.services.support.SupportActor
import net.psforever.services.teamwork.{ import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction}
SquadResponse,
SquadServiceMessage,
SquadServiceResponse,
SquadAction => SquadServiceAction
}
import net.psforever.services.hart.HartTimer import net.psforever.services.hart.HartTimer
import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import net.psforever.services.{RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS} import net.psforever.services.{RemoverActor, Service, ServiceManager, InterstellarClusterService => ICS}
@ -443,6 +439,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
session.player.spectator = spectator session.player.spectator = spectator
case Recall() => case Recall() =>
player.ZoningRequest = Zoning.Method.Recall
zoningType = Zoning.Method.Recall zoningType = Zoning.Method.Recall
zoningChatMessageType = ChatMessageType.CMT_RECALL zoningChatMessageType = ChatMessageType.CMT_RECALL
zoningStatus = Zoning.Status.Request zoningStatus = Zoning.Status.Request
@ -456,6 +453,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}) })
case InstantAction() => case InstantAction() =>
player.ZoningRequest = Zoning.Method.InstantAction
zoningType = Zoning.Method.InstantAction zoningType = Zoning.Method.InstantAction
zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION
zoningStatus = Zoning.Status.Request zoningStatus = Zoning.Status.Request
@ -482,10 +480,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
case Quit() => case Quit() =>
//priority to quitting is given to quit over other zoning methods //priority is given to quit over other zoning methods
if (session.zoningType == Zoning.Method.InstantAction || session.zoningType == Zoning.Method.Recall) { if (session.zoningType == Zoning.Method.InstantAction || session.zoningType == Zoning.Method.Recall) {
CancelZoningProcessWithDescriptiveReason("cancel") CancelZoningProcessWithDescriptiveReason("cancel")
} }
player.ZoningRequest = Zoning.Method.Quit
zoningType = Zoning.Method.Quit zoningType = Zoning.Method.Quit
zoningChatMessageType = ChatMessageType.CMT_QUIT zoningChatMessageType = ChatMessageType.CMT_QUIT
zoningStatus = Zoning.Status.Request zoningStatus = Zoning.Status.Request
@ -1726,6 +1725,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
*/ */
def CancelZoningProcess(): Unit = { def CancelZoningProcess(): Unit = {
zoningTimer.cancel() zoningTimer.cancel()
player.ZoningRequest = Zoning.Method.None
zoningType = Zoning.Method.None zoningType = Zoning.Method.None
zoningStatus = Zoning.Status.None zoningStatus = Zoning.Status.None
zoningCounter = 0 zoningCounter = 0
@ -1859,6 +1859,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
DropSpecialSlotItem() DropSpecialSlotItem()
ToggleMaxSpecialState(enable = false) ToggleMaxSpecialState(enable = false)
if (player.LastDamage match {
case Some(damage) => damage.interaction.cause match {
case cause: ExplodingEntityReason => cause.entity.isInstanceOf[VehicleSpawnPad]
case _ => false
}
case None => false
}) {
//also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
sendResponse(ChatMsg(ChatMessageType.UNK_227, false, "", "@SVCP_Killed_OnPadOnCreate", None))
}
keepAliveFunc = NormalKeepAlive keepAliveFunc = NormalKeepAlive
zoningStatus = Zoning.Status.None zoningStatus = Zoning.Status.None
@ -1866,7 +1876,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
continent.GUID(mount) match { continent.GUID(mount) match {
case Some(obj: Vehicle) => case Some(obj: Vehicle) =>
TotalDriverVehicleControl(obj) ConditionalDriverVehicleControl(obj)
UnaccessContainer(obj) UnaccessContainer(obj)
case _ => ; case _ => ;
} }
@ -2190,6 +2200,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
specialItemSlotGuid match { specialItemSlotGuid match {
case Some(guid: PlanetSideGUID) => case Some(guid: PlanetSideGUID) =>
specialItemSlotGuid = None specialItemSlotGuid = None
player.Carrying = None
continent.GUID(guid) match { continent.GUID(guid) match {
case Some(llu: CaptureFlag) => case Some(llu: CaptureFlag) =>
llu.Carrier match { llu.Carrier match {
@ -2398,6 +2409,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case Some(guid) => case Some(guid) =>
if (guid == llu.GUID) { if (guid == llu.GUID) {
specialItemSlotGuid = None specialItemSlotGuid = None
player.Carrying = None
} }
case _ => ; case _ => ;
} }
@ -2622,7 +2634,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
val player_guid: PlanetSideGUID = tplayer.GUID val player_guid: PlanetSideGUID = tplayer.GUID
if (player_guid == player.GUID) { if (player_guid == player.GUID) {
//disembarking self //disembarking self
TotalDriverVehicleControl(obj) ConditionalDriverVehicleControl(obj)
UnaccessContainer(obj) UnaccessContainer(obj)
DismountAction(tplayer, obj, seat_num) DismountAction(tplayer, obj, seat_num)
} else { } else {
@ -2675,7 +2687,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case None => case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
continent.tasks ! BuyNewEquipmentPutInInventory( continent.tasks ! BuyNewEquipmentPutInInventory(
continent.GUID(tplayer.VehicleSeated) match { case Some(v: Vehicle) => v; case _ => player }, continent.GUID(tplayer.VehicleSeated) match { case Some(v : Vehicle) => v; case _ => player },
tplayer, tplayer,
msg.terminal_guid msg.terminal_guid
)(item) )(item)
@ -2701,13 +2713,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
lastTerminalOrderFulfillment = true lastTerminalOrderFulfillment = true
case Terminal.BuyVehicle(vehicle, weapons, trunk) => case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
continent.map.terminalToSpawnPad.get(msg.terminal_guid.guid) match { tplayer.avatar.purchaseCooldown(vehicle.Definition) match {
case Some(padGuid) => case Some(_) =>
tplayer.avatar.purchaseCooldown(vehicle.Definition) match { sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
case Some(_) => case None =>
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) continent.map.terminalToSpawnPad
case None => .find { case (termid, _) => termid == msg.terminal_guid.guid }
val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad] .collect {
case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b))
case _ => (None, None)
}
.get match {
case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
vehicle.Faction = tplayer.Faction vehicle.Faction = tplayer.Faction
vehicle.Position = pad.Position vehicle.Position = pad.Position
vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset) vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
@ -2732,13 +2749,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
entry.obj.Faction = tplayer.Faction entry.obj.Faction = tplayer.Faction
vTrunk.InsertQuickly(entry.start, entry.obj) vTrunk.InsertQuickly(entry.start, entry.obj)
}) })
continent.tasks ! RegisterVehicleFromSpawnPad(vehicle, pad) continent.tasks ! RegisterVehicleFromSpawnPad(vehicle, pad, term)
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true)) sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
case _ =>
log.error(
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
)
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
} }
case None =>
log.error(
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
)
} }
lastTerminalOrderFulfillment = true lastTerminalOrderFulfillment = true
@ -2793,7 +2811,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ObjectDetachMessage( ObjectDetachMessage(
pad_guid, pad_guid,
vehicle_guid, vehicle_guid,
pad_position + Vector3(0, 0, pad.VehicleCreationZOffset), pad_position + Vector3.z(pad.VehicleCreationZOffset),
pad_orientation_z + pad.VehicleCreationZOrientOffset pad_orientation_z + pad.VehicleCreationZOrientOffset
) )
) )
@ -2958,6 +2976,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, pad) => case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, pad) =>
val vehicle_guid = vehicle.GUID val vehicle_guid = vehicle.GUID
PlayerActionsToCancel() PlayerActionsToCancel()
serverVehicleControlVelocity = Some(0)
CancelAllProximityUnits() CancelAllProximityUnits()
if (player.VisibleSlots.contains(player.DrawnSlot)) { if (player.VisibleSlots.contains(player.DrawnSlot)) {
player.DrawnSlot = Player.HandsDownSlot player.DrawnSlot = Player.HandsDownSlot
@ -2988,16 +3007,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case VehicleResponse.ServerVehicleOverrideEnd(vehicle, pad) => case VehicleResponse.ServerVehicleOverrideEnd(vehicle, pad) =>
DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2) DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2)
case VehicleResponse.PeriodicReminder(cause, data) => case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
val msg: String = cause match { sendResponse(ChatMsg(
case VehicleSpawnPad.Reminders.Blocked => ChatMessageType.CMT_OPEN,
s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}" true,
case VehicleSpawnPad.Reminders.Queue => "",
s"Your position in the vehicle spawn queue is ${data.getOrElse("dead last")}." s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
case VehicleSpawnPad.Reminders.Cancelled => None
"Your vehicle order has been cancelled." ))
case VehicleResponse.PeriodicReminder(_, data) =>
val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match {
case Some(msg: String)
if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg)
case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg)
case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.")
} }
sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None)) sendResponse(ChatMsg(isType, flag, "", msg, None))
case VehicleResponse.ChangeLoadout(target, old_weapons, added_weapons, old_inventory, new_inventory) => case VehicleResponse.ChangeLoadout(target, old_weapons, added_weapons, old_inventory, new_inventory) =>
//TODO when vehicle weapons can be changed without visual glitches, rewrite this //TODO when vehicle weapons can be changed without visual glitches, rewrite this
@ -4869,6 +4895,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} }
case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU")
} }
case _ => ;
} }
case Some(obj: FacilityTurret) => case Some(obj: FacilityTurret) =>
@ -5101,6 +5128,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (specialItemSlotGuid.isEmpty) { if (specialItemSlotGuid.isEmpty) {
if (obj.Faction == player.Faction) { if (obj.Faction == player.Faction) {
specialItemSlotGuid = Some(obj.GUID) specialItemSlotGuid = Some(obj.GUID)
player.Carrying = SpecialCarry.CaptureFlag
continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player)
} else { } else {
log.warn( log.warn(
@ -5504,11 +5532,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
projectile.profile.JammerProjectile || projectile.profile.JammerProjectile ||
projectile.profile.SympatheticExplosion projectile.profile.SympatheticExplosion
) { ) {
Zone.causeSpecialEmp( //can also substitute 'projectile.profile' for 'SpecialEmp.emp'
Zone.serverSideDamage(
continent, continent,
player, player,
explosion_pos, SpecialEmp.emp,
GlobalDefinitions.special_emp.innateDamage.get SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
SpecialEmp.findAllBoomers
) )
} }
if (profile.ExistsOnRemoteClients && projectile.HasGUID) { if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
@ -6033,12 +6064,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @see `RegisterVehicle` * @see `RegisterVehicle`
* @return a `TaskResolver.GiveTask` message * @return a `TaskResolver.GiveTask` message
*/ */
def RegisterVehicleFromSpawnPad(obj: Vehicle, pad: VehicleSpawnPad): TaskResolver.GiveTask = { def RegisterVehicleFromSpawnPad(obj: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskResolver.GiveTask = {
TaskResolver.GiveTask( TaskResolver.GiveTask(
new Task() { new Task() {
private val localVehicle = obj private val localVehicle = obj
private val localPad = pad.Actor private val localPad = pad.Actor
private val localPlayer = player private val localTerminal = terminal
private val localPlayer = player
override def Description: String = s"register a ${localVehicle.Definition.Name} for spawn pad" override def Description: String = s"register a ${localVehicle.Definition.Name} for spawn pad"
@ -6051,7 +6083,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} }
def Execute(resolver: ActorRef): Unit = { def Execute(resolver: ActorRef): Unit = {
localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle) localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle, localTerminal)
resolver ! Success(this) resolver ! Success(this)
} }
}, },
@ -7027,10 +7059,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
progressBarUpdate.cancel() progressBarUpdate.cancel()
progressBarValue = None progressBarValue = None
lastTerminalOrderFulfillment = true lastTerminalOrderFulfillment = true
serverVehicleControlVelocity = None
accessedContainer match { accessedContainer match {
case Some(v: Vehicle) => case Some(v: Vehicle) =>
val vguid = v.GUID val vguid = v.GUID
ConditionalDriverVehicleControl(v)
if (v.AccessingTrunk.contains(player.GUID)) { if (v.AccessingTrunk.contains(player.GUID)) {
if (player.VehicleSeated.contains(vguid)) { if (player.VehicleSeated.contains(vguid)) {
v.AccessingTrunk = None //player is seated; just stop accessing trunk v.AccessingTrunk = None //player is seated; just stop accessing trunk
@ -7749,7 +7781,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* Set the vehicle to move in reverse * Set the vehicle to move in reverse
*/ */
def ServerVehicleLockReverse(): Unit = { def ServerVehicleLockReverse(): Unit = {
serverVehicleControlVelocity = Some(0) serverVehicleControlVelocity = Some(-1)
sendResponse( sendResponse(
ServerVehicleOverrideMsg( ServerVehicleOverrideMsg(
lock_accelerator = true, lock_accelerator = true,
@ -7770,7 +7802,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* Set the vehicle to strafe right * Set the vehicle to strafe right
*/ */
def ServerVehicleLockStrafeRight(): Unit = { def ServerVehicleLockStrafeRight(): Unit = {
serverVehicleControlVelocity = Some(0) serverVehicleControlVelocity = Some(-1)
sendResponse( sendResponse(
ServerVehicleOverrideMsg( ServerVehicleOverrideMsg(
lock_accelerator = true, lock_accelerator = true,
@ -7791,7 +7823,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* Set the vehicle to strafe left * Set the vehicle to strafe left
*/ */
def ServerVehicleLockStrafeLeft(): Unit = { def ServerVehicleLockStrafeLeft(): Unit = {
serverVehicleControlVelocity = Some(0) serverVehicleControlVelocity = Some(-1)
sendResponse( sendResponse(
ServerVehicleOverrideMsg( ServerVehicleOverrideMsg(
lock_accelerator = true, lock_accelerator = true,
@ -7812,7 +7844,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @param vehicle the vehicle being controlled * @param vehicle the vehicle being controlled
*/ */
def ServerVehicleLock(vehicle: Vehicle): Unit = { def ServerVehicleLock(vehicle: Vehicle): Unit = {
serverVehicleControlVelocity = Some(0) serverVehicleControlVelocity = Some(-1)
sendResponse(ServerVehicleOverrideMsg(true, true, false, false, 0, 1, 0, Some(0))) sendResponse(ServerVehicleOverrideMsg(true, true, false, false, 0, 1, 0, Some(0)))
} }
@ -7847,13 +7879,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* Stop all movement entirely. * Stop all movement entirely.
* @param vehicle the vehicle * @param vehicle the vehicle
*/ */
def TotalDriverVehicleControl(vehicle: Vehicle): Unit = { def ConditionalDriverVehicleControl(vehicle: Vehicle): Unit = {
if (serverVehicleControlVelocity.nonEmpty) { if (serverVehicleControlVelocity.nonEmpty && !serverVehicleControlVelocity.contains(0)) {
serverVehicleControlVelocity = None TotalDriverVehicleControl(vehicle)
sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None))
} }
} }
def TotalDriverVehicleControl(vehicle: Vehicle): Unit = {
serverVehicleControlVelocity = None
sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None))
}
/** /**
* Given a globally unique identifier in the 40100 to 40124 range * Given a globally unique identifier in the 40100 to 40124 range
* (with an optional 25 as buffer), * (with an optional 25 as buffer),
@ -8883,6 +8919,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (player.avatar.vehicle.nonEmpty && player.VehicleSeated != player.avatar.vehicle) { if (player.avatar.vehicle.nonEmpty && player.VehicleSeated != player.avatar.vehicle) {
continent.GUID(player.avatar.vehicle) match { continent.GUID(player.avatar.vehicle) match {
case Some(vehicle: Vehicle) if vehicle.Actor != Default.Actor => case Some(vehicle: Vehicle) if vehicle.Actor != Default.Actor =>
TotalDriverVehicleControl(vehicle)
vehicle.Actor ! Vehicle.Ownership(None) vehicle.Actor ! Vehicle.Ownership(None)
case _ => ; case _ => ;
} }

View file

@ -170,7 +170,12 @@ object ExplosiveDeployableControl {
val zone = target.Zone val zone = target.Zone
zone.Activity ! Zone.HotSpot.Activity(cause) zone.Activity ! Zone.HotSpot.Activity(cause)
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target)) zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.Detonate(target.GUID, target))
Zone.causeExplosion(zone, target, Some(cause), ExplosiveDeployableControl.detectionForExplosiveSource(target)) Zone.serverSideDamage(
zone,
target,
Zone.explosionDamage(Some(cause)),
ExplosiveDeployableControl.detectionForExplosiveSource(target)
)
} }
/** /**

View file

@ -20,12 +20,7 @@ import net.psforever.objects.serverobject.painbox.PainboxDefinition
import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals._
import net.psforever.objects.serverobject.tube.SpawnTubeDefinition import net.psforever.objects.serverobject.tube.SpawnTubeDefinition
import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition
import net.psforever.objects.serverobject.structures.{ import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition}
AmenityDefinition,
AutoRepairStats,
BuildingDefinition,
WarpGateDefinition
}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition
import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition}
import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade} import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade}
@ -982,8 +977,6 @@ object GlobalDefinitions {
val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable) val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable)
val special_emp = ExplosiveDeployableDefinition(DeployedItem.jammer_mine)
//this is only treated like a deployable //this is only treated like a deployable
val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744 val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744
init_deployables() init_deployables()
@ -6788,9 +6781,7 @@ object GlobalDefinitions {
dropship.MaxShields = 1000 dropship.MaxShields = 1000
dropship.CanFly = true dropship.CanFly = true
dropship.Seats += 0 -> new SeatDefinition() dropship.Seats += 0 -> new SeatDefinition()
dropship.Seats += 1 -> new SeatDefinition() { dropship.Seats += 1 -> bailableSeat
bailable = true
}
dropship.Seats += 2 -> bailableSeat dropship.Seats += 2 -> bailableSeat
dropship.Seats += 3 -> bailableSeat dropship.Seats += 3 -> bailableSeat
dropship.Seats += 4 -> bailableSeat dropship.Seats += 4 -> bailableSeat
@ -7039,11 +7030,10 @@ object GlobalDefinitions {
* Initialize `Deployable` globals. * Initialize `Deployable` globals.
*/ */
private def init_deployables(): Unit = { private def init_deployables(): Unit = {
val mine = GeometryForm.representByCylinder(radius = 0.1914f, height = 0.0957f) _ val mine = GeometryForm.representByCylinder(radius = 0.1914f, height = 0.0957f) _
val smallTurret = GeometryForm.representByCylinder(radius = 0.48435f, height = 1.23438f) _ val smallTurret = GeometryForm.representByCylinder(radius = 0.48435f, height = 1.23438f) _
val sensor = GeometryForm.representByCylinder(radius = 0.1914f, height = 1.21875f) _ val sensor = GeometryForm.representByCylinder(radius = 0.1914f, height = 1.21875f) _
val largeTurret = GeometryForm.representByCylinder(radius = 0.8437f, height = 2.29687f) _ val largeTurret = GeometryForm.representByCylinder(radius = 0.8437f, height = 2.29687f) _
boomer.Name = "boomer" boomer.Name = "boomer"
boomer.Descriptor = "Boomers" boomer.Descriptor = "Boomers"
boomer.MaxHealth = 100 boomer.MaxHealth = 100
@ -7066,7 +7056,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
boomer.Geometry = mine boomer.Geometry = mine
he_mine.Name = "he_mine" he_mine.Name = "he_mine"
he_mine.Descriptor = "Mines" he_mine.Descriptor = "Mines"
he_mine.MaxHealth = 100 he_mine.MaxHealth = 100
@ -7088,7 +7077,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
he_mine.Geometry = mine he_mine.Geometry = mine
jammer_mine.Name = "jammer_mine" jammer_mine.Name = "jammer_mine"
jammer_mine.Descriptor = "JammerMines" jammer_mine.Descriptor = "JammerMines"
jammer_mine.MaxHealth = 100 jammer_mine.MaxHealth = 100
@ -7098,14 +7086,13 @@ object GlobalDefinitions {
jammer_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.DeployTime = Duration.create(1000, "ms")
jammer_mine.DetonateOnJamming = false jammer_mine.DetonateOnJamming = false
jammer_mine.Geometry = mine jammer_mine.Geometry = mine
spitfire_turret.Name = "spitfire_turret" spitfire_turret.Name = "spitfire_turret"
spitfire_turret.Descriptor = "Spitfires" spitfire_turret.Descriptor = "Spitfires"
spitfire_turret.MaxHealth = 100 spitfire_turret.MaxHealth = 100
spitfire_turret.Damageable = true spitfire_turret.Damageable = true
spitfire_turret.Repairable = true spitfire_turret.Repairable = true
spitfire_turret.RepairIfDestroyed = false spitfire_turret.RepairIfDestroyed = false
spitfire_turret.WeaponPaths += 1 -> new mutable.HashMap() spitfire_turret.WeaponPaths += 1 -> new mutable.HashMap()
spitfire_turret.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon spitfire_turret.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon
spitfire_turret.ReserveAmmunition = false spitfire_turret.ReserveAmmunition = false
spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets
@ -7122,14 +7109,13 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
spitfire_turret.Geometry = smallTurret spitfire_turret.Geometry = smallTurret
spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Name = "spitfire_cloaked"
spitfire_cloaked.Descriptor = "CloakingSpitfires" spitfire_cloaked.Descriptor = "CloakingSpitfires"
spitfire_cloaked.MaxHealth = 100 spitfire_cloaked.MaxHealth = 100
spitfire_cloaked.Damageable = true spitfire_cloaked.Damageable = true
spitfire_cloaked.Repairable = true spitfire_cloaked.Repairable = true
spitfire_cloaked.RepairIfDestroyed = false spitfire_cloaked.RepairIfDestroyed = false
spitfire_cloaked.WeaponPaths += 1 -> new mutable.HashMap() spitfire_cloaked.WeaponPaths += 1 -> new mutable.HashMap()
spitfire_cloaked.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon spitfire_cloaked.WeaponPaths(1) += TurretUpgrade.None -> spitfire_weapon
spitfire_cloaked.ReserveAmmunition = false spitfire_cloaked.ReserveAmmunition = false
spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets
@ -7145,14 +7131,13 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
spitfire_cloaked.Geometry = smallTurret spitfire_cloaked.Geometry = smallTurret
spitfire_aa.Name = "spitfire_aa" spitfire_aa.Name = "spitfire_aa"
spitfire_aa.Descriptor = "FlakSpitfires" spitfire_aa.Descriptor = "FlakSpitfires"
spitfire_aa.MaxHealth = 100 spitfire_aa.MaxHealth = 100
spitfire_aa.Damageable = true spitfire_aa.Damageable = true
spitfire_aa.Repairable = true spitfire_aa.Repairable = true
spitfire_aa.RepairIfDestroyed = false spitfire_aa.RepairIfDestroyed = false
spitfire_aa.WeaponPaths += 1 -> new mutable.HashMap() spitfire_aa.WeaponPaths += 1 -> new mutable.HashMap()
spitfire_aa.WeaponPaths(1) += TurretUpgrade.None -> spitfire_aa_weapon spitfire_aa.WeaponPaths(1) += TurretUpgrade.None -> spitfire_aa_weapon
spitfire_aa.ReserveAmmunition = false spitfire_aa.ReserveAmmunition = false
spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets
@ -7168,7 +7153,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
spitfire_aa.Geometry = smallTurret spitfire_aa.Geometry = smallTurret
motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Name = "motionalarmsensor"
motionalarmsensor.Descriptor = "MotionSensors" motionalarmsensor.Descriptor = "MotionSensors"
motionalarmsensor.MaxHealth = 100 motionalarmsensor.MaxHealth = 100
@ -7177,7 +7161,6 @@ object GlobalDefinitions {
motionalarmsensor.RepairIfDestroyed = false motionalarmsensor.RepairIfDestroyed = false
motionalarmsensor.DeployTime = Duration.create(1000, "ms") motionalarmsensor.DeployTime = Duration.create(1000, "ms")
motionalarmsensor.Geometry = sensor motionalarmsensor.Geometry = sensor
sensor_shield.Name = "sensor_shield" sensor_shield.Name = "sensor_shield"
sensor_shield.Descriptor = "SensorShields" sensor_shield.Descriptor = "SensorShields"
sensor_shield.MaxHealth = 100 sensor_shield.MaxHealth = 100
@ -7186,7 +7169,6 @@ object GlobalDefinitions {
sensor_shield.RepairIfDestroyed = false sensor_shield.RepairIfDestroyed = false
sensor_shield.DeployTime = Duration.create(5000, "ms") sensor_shield.DeployTime = Duration.create(5000, "ms")
sensor_shield.Geometry = sensor sensor_shield.Geometry = sensor
tank_traps.Name = "tank_traps" tank_traps.Name = "tank_traps"
tank_traps.Descriptor = "TankTraps" tank_traps.Descriptor = "TankTraps"
tank_traps.MaxHealth = 5000 tank_traps.MaxHealth = 5000
@ -7205,7 +7187,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f) tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f)
val fieldTurretConverter = new FieldTurretConverter val fieldTurretConverter = new FieldTurretConverter
portable_manned_turret.Name = "portable_manned_turret" portable_manned_turret.Name = "portable_manned_turret"
portable_manned_turret.Descriptor = "FieldTurrets" portable_manned_turret.Descriptor = "FieldTurrets"
@ -7213,11 +7194,11 @@ object GlobalDefinitions {
portable_manned_turret.Damageable = true portable_manned_turret.Damageable = true
portable_manned_turret.Repairable = true portable_manned_turret.Repairable = true
portable_manned_turret.RepairIfDestroyed = false portable_manned_turret.RepairIfDestroyed = false
portable_manned_turret.controlledWeapons += 0 -> 1 portable_manned_turret.controlledWeapons += 0 -> 1
portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun
portable_manned_turret.MountPoints += 1 -> MountInfo(0) portable_manned_turret.MountPoints += 1 -> MountInfo(0)
portable_manned_turret.MountPoints += 2 -> MountInfo(0) portable_manned_turret.MountPoints += 2 -> MountInfo(0)
portable_manned_turret.ReserveAmmunition = true portable_manned_turret.ReserveAmmunition = true
portable_manned_turret.FactionLocked = true portable_manned_turret.FactionLocked = true
portable_manned_turret.Packet = fieldTurretConverter portable_manned_turret.Packet = fieldTurretConverter
@ -7234,18 +7215,17 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
portable_manned_turret.Geometry = largeTurret portable_manned_turret.Geometry = largeTurret
portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Name = "portable_manned_turret_nc"
portable_manned_turret_nc.Descriptor = "FieldTurrets" portable_manned_turret_nc.Descriptor = "FieldTurrets"
portable_manned_turret_nc.MaxHealth = 1000 portable_manned_turret_nc.MaxHealth = 1000
portable_manned_turret_nc.Damageable = true portable_manned_turret_nc.Damageable = true
portable_manned_turret_nc.Repairable = true portable_manned_turret_nc.Repairable = true
portable_manned_turret_nc.RepairIfDestroyed = false portable_manned_turret_nc.RepairIfDestroyed = false
portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc
portable_manned_turret_nc.controlledWeapons += 0 -> 1 portable_manned_turret_nc.controlledWeapons += 0 -> 1
portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0) portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0)
portable_manned_turret_nc.ReserveAmmunition = true portable_manned_turret_nc.ReserveAmmunition = true
portable_manned_turret_nc.FactionLocked = true portable_manned_turret_nc.FactionLocked = true
portable_manned_turret_nc.Packet = fieldTurretConverter portable_manned_turret_nc.Packet = fieldTurretConverter
@ -7262,18 +7242,17 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
portable_manned_turret_nc.Geometry = largeTurret portable_manned_turret_nc.Geometry = largeTurret
portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Name = "portable_manned_turret_tr"
portable_manned_turret_tr.Descriptor = "FieldTurrets" portable_manned_turret_tr.Descriptor = "FieldTurrets"
portable_manned_turret_tr.MaxHealth = 1000 portable_manned_turret_tr.MaxHealth = 1000
portable_manned_turret_tr.Damageable = true portable_manned_turret_tr.Damageable = true
portable_manned_turret_tr.Repairable = true portable_manned_turret_tr.Repairable = true
portable_manned_turret_tr.RepairIfDestroyed = false portable_manned_turret_tr.RepairIfDestroyed = false
portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr
portable_manned_turret_tr.controlledWeapons += 0 -> 1 portable_manned_turret_tr.controlledWeapons += 0 -> 1
portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0) portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0)
portable_manned_turret_tr.ReserveAmmunition = true portable_manned_turret_tr.ReserveAmmunition = true
portable_manned_turret_tr.FactionLocked = true portable_manned_turret_tr.FactionLocked = true
portable_manned_turret_tr.Packet = fieldTurretConverter portable_manned_turret_tr.Packet = fieldTurretConverter
@ -7290,18 +7269,17 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
portable_manned_turret_tr.Geometry = largeTurret portable_manned_turret_tr.Geometry = largeTurret
portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Name = "portable_manned_turret_vs"
portable_manned_turret_vs.Descriptor = "FieldTurrets" portable_manned_turret_vs.Descriptor = "FieldTurrets"
portable_manned_turret_vs.MaxHealth = 1000 portable_manned_turret_vs.MaxHealth = 1000
portable_manned_turret_vs.Damageable = true portable_manned_turret_vs.Damageable = true
portable_manned_turret_vs.Repairable = true portable_manned_turret_vs.Repairable = true
portable_manned_turret_vs.RepairIfDestroyed = false portable_manned_turret_vs.RepairIfDestroyed = false
portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs
portable_manned_turret_vs.controlledWeapons += 0 -> 1 portable_manned_turret_vs.controlledWeapons += 0 -> 1
portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0)
portable_manned_turret_vs.ReserveAmmunition = true portable_manned_turret_vs.ReserveAmmunition = true
portable_manned_turret_vs.FactionLocked = true portable_manned_turret_vs.FactionLocked = true
portable_manned_turret_vs.Packet = fieldTurretConverter portable_manned_turret_vs.Packet = fieldTurretConverter
@ -7318,7 +7296,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade Modifiers = ExplodingRadialDegrade
} }
portable_manned_turret_vs.Geometry = largeTurret portable_manned_turret_vs.Geometry = largeTurret
deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Name = "deployable_shield_generator"
deployable_shield_generator.Descriptor = "ShieldGenerators" deployable_shield_generator.Descriptor = "ShieldGenerators"
deployable_shield_generator.MaxHealth = 1700 deployable_shield_generator.MaxHealth = 1700
@ -7328,7 +7305,6 @@ object GlobalDefinitions {
deployable_shield_generator.DeployTime = Duration.create(6000, "ms") deployable_shield_generator.DeployTime = Duration.create(6000, "ms")
deployable_shield_generator.Model = ComplexDeployableResolutions.calculate deployable_shield_generator.Model = ComplexDeployableResolutions.calculate
deployable_shield_generator.Geometry = GeometryForm.representByCylinder(radius = 0.6562f, height = 2.17188f) deployable_shield_generator.Geometry = GeometryForm.representByCylinder(radius = 0.6562f, height = 2.17188f)
router_telepad_deployable.Name = "router_telepad_deployable" router_telepad_deployable.Name = "router_telepad_deployable"
router_telepad_deployable.MaxHealth = 100 router_telepad_deployable.MaxHealth = 100
router_telepad_deployable.Damageable = true router_telepad_deployable.Damageable = true
@ -7338,7 +7314,6 @@ object GlobalDefinitions {
router_telepad_deployable.Packet = new TelepadDeployableConverter router_telepad_deployable.Packet = new TelepadDeployableConverter
router_telepad_deployable.Model = SimpleResolutions.calculate router_telepad_deployable.Model = SimpleResolutions.calculate
router_telepad_deployable.Geometry = GeometryForm.representByRaisedSphere(radius = 1.2344f) router_telepad_deployable.Geometry = GeometryForm.representByRaisedSphere(radius = 1.2344f)
internal_router_telepad_deployable.Name = "router_telepad_deployable" internal_router_telepad_deployable.Name = "router_telepad_deployable"
internal_router_telepad_deployable.MaxHealth = 1 internal_router_telepad_deployable.MaxHealth = 1
internal_router_telepad_deployable.Damageable = false internal_router_telepad_deployable.Damageable = false
@ -7346,22 +7321,6 @@ object GlobalDefinitions {
internal_router_telepad_deployable.DeployTime = Duration.create(1, "ms") internal_router_telepad_deployable.DeployTime = Duration.create(1, "ms")
internal_router_telepad_deployable.DeployCategory = DeployableCategory.Telepads internal_router_telepad_deployable.DeployCategory = DeployableCategory.Telepads
internal_router_telepad_deployable.Packet = new InternalTelepadDeployableConverter internal_router_telepad_deployable.Packet = new InternalTelepadDeployableConverter
special_emp.Name = "emp"
special_emp.MaxHealth = 1
special_emp.Damageable = false
special_emp.Repairable = false
special_emp.DeployCategory = DeployableCategory.Mines
special_emp.explodes = true
special_emp.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 0
DamageAtEdge = 1.0f
DamageRadius = 5f
AdditionalEffect = true
Modifiers = MaxDistanceCutoff
}
} }
/** /**
@ -7713,14 +7672,54 @@ object GlobalDefinitions {
mb_pad_creation.Name = "mb_pad_creation" mb_pad_creation.Name = "mb_pad_creation"
mb_pad_creation.Damageable = false mb_pad_creation.Damageable = false
mb_pad_creation.Repairable = false mb_pad_creation.Repairable = false
mb_pad_creation.killBox = VehicleSpawnPadDefinition.prepareKillBox(
forwardLimit = 14,
backLimit = 10,
sideLimit = 7.5f,
aboveLimit = 5 //double to 10 when spawning a flying vehicle
)
mb_pad_creation.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 99999
DamageRadiusMin = 14
DamageRadius = 14.5f
DamageAtEdge = 0.00002f
//damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m
}
dropship_pad_doors.Name = "dropship_pad_doors" dropship_pad_doors.Name = "dropship_pad_doors"
dropship_pad_doors.Damageable = false dropship_pad_doors.Damageable = false
dropship_pad_doors.Repairable = false dropship_pad_doors.Repairable = false
dropship_pad_doors.killBox = VehicleSpawnPadDefinition.prepareKillBox(
forwardLimit = 14,
backLimit = 14,
sideLimit = 13.5f,
aboveLimit = 5 //doubles to 10
)
dropship_pad_doors.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 99999
DamageRadiusMin = 14
DamageRadius = 14.5f
DamageAtEdge = 0.00002f
//damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m
}
vanu_vehicle_creation_pad.Name = "vanu_vehicle_creation_pad" vanu_vehicle_creation_pad.Name = "vanu_vehicle_creation_pad"
vanu_vehicle_creation_pad.Damageable = false vanu_vehicle_creation_pad.Damageable = false
vanu_vehicle_creation_pad.Repairable = false vanu_vehicle_creation_pad.Repairable = false
vanu_vehicle_creation_pad.killBox = VehicleSpawnPadDefinition.prepareVanuKillBox(
radius = 8.5f,
aboveLimit = 5
)
vanu_vehicle_creation_pad.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 99999
DamageRadiusMin = 14
DamageRadius = 14.5f
DamageAtEdge = 0.00002f
//damage is 99999 at 14m, dropping rapidly to ~1 at 14.5m
}
mb_locker.Name = "mb_locker" mb_locker.Name = "mb_locker"
mb_locker.Damageable = false mb_locker.Damageable = false

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
@ -13,7 +13,7 @@ import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.ZoneAware import net.psforever.objects.zones.{ZoneAware, Zoning}
import net.psforever.types.{PlanetSideGUID, _} import net.psforever.types.{PlanetSideGUID, _}
import scala.annotation.tailrec import scala.annotation.tailrec
@ -30,6 +30,7 @@ class Player(var avatar: Avatar)
with ZoneAware with ZoneAware
with AuraContainer { with AuraContainer {
private var backpack: Boolean = false private var backpack: Boolean = false
private var released: Boolean = false
private var armor: Int = 0 private var armor: Int = 0
private var capacitor: Float = 0f private var capacitor: Float = 0f
@ -44,12 +45,14 @@ class Player(var avatar: Avatar)
private var drawnSlot: Int = Player.HandsDownSlot private var drawnSlot: Int = Player.HandsDownSlot
private var lastDrawnSlot: Int = Player.HandsDownSlot private var lastDrawnSlot: Int = Player.HandsDownSlot
private var backpackAccess: Option[PlanetSideGUID] = None private var backpackAccess: Option[PlanetSideGUID] = None
private var carrying: Option[SpecialCarry] = None
private var facingYawUpper: Float = 0f private var facingYawUpper: Float = 0f
private var crouching: Boolean = false private var crouching: Boolean = false
private var jumping: Boolean = false private var jumping: Boolean = false
private var cloaked: Boolean = false private var cloaked: Boolean = false
private var afk: Boolean = false private var afk: Boolean = false
private var zoning: Zoning.Method.Value = Zoning.Method.None
private var vehicleSeated: Option[PlanetSideGUID] = None private var vehicleSeated: Option[PlanetSideGUID] = None
@ -95,6 +98,7 @@ class Player(var avatar: Avatar)
Health = Definition.DefaultHealth Health = Definition.DefaultHealth
Armor = MaxArmor Armor = MaxArmor
Capacitor = 0 Capacitor = 0
released = false
} }
isAlive isAlive
} }
@ -108,18 +112,18 @@ class Player(var avatar: Avatar)
def Revive: Boolean = { def Revive: Boolean = {
Destroyed = false Destroyed = false
Health = Definition.DefaultHealth Health = Definition.DefaultHealth
released = false
true true
} }
def Release: Boolean = { def Release: Boolean = {
if (!isAlive) { released = true
backpack = true backpack = !isAlive
true true
} else {
false
}
} }
def isReleased: Boolean = released
def Armor: Int = armor def Armor: Int = armor
def Armor_=(assignArmor: Int): Int = { def Armor_=(assignArmor: Int): Int = {
@ -498,6 +502,23 @@ class Player(var avatar: Avatar)
VehicleSeated VehicleSeated
} }
def Carrying: Option[SpecialCarry] = carrying
def Carrying_=(item: SpecialCarry): Option[SpecialCarry] = {
Carrying
}
def Carrying_=(item: Option[SpecialCarry]): Option[SpecialCarry] = {
Carrying
}
def ZoningRequest: Zoning.Method.Value = zoning
def ZoningRequest_=(request: Zoning.Method.Value): Zoning.Method.Value = {
zoning = request
ZoningRequest
}
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel] def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
def canEqual(other: Any): Boolean = other.isInstanceOf[Player] def canEqual(other: Any): Boolean = other.isInstanceOf[Player]

View file

@ -0,0 +1,144 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.etc.EmpReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.projectile.MaxDistanceCutoff
import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.zones.Zone
import net.psforever.types.{PlanetSideEmpire, Vector3}
/**
* Information and functions useful for the construction of a server-side electromagnetic pulse
* (not intigated by any specific thing the client does).
*/
object SpecialEmp {
/** A defaulted emp definition.
* Any projectile definition can be used. */
final val emp = new DamageWithPosition {
CausesDamageType = DamageType.Splash
SympatheticExplosion = true
Damage0 = 0
DamageAtEdge = 1.0f
DamageRadius = 5f
AdditionalEffect = true
Modifiers = MaxDistanceCutoff
}
/** The definition for a proxy object that represents a physical component of the source of the electromagnetic pulse. */
private val proxy_definition = new ObjectDefinition(objectId = 420) with VitalityDefinition {
Name = "emp"
MaxHealth = 1
Damageable = false
Repairable = false
explodes = true
innateDamage = emp
}
/**
* The damage interaction for an electromagnetic pulse effect.
* @param empEffect information about the effect
* @param position where the effect occurs
* @param source a game object that represents the source of the EMP
* @param target a game object that is affected by the EMP
* @return a `DamageInteraction` object
*/
def createEmpInteraction(
empEffect: DamageWithPosition,
position: Vector3
)
(
source: PlanetSideGameObject with FactionAffinity with Vitality,
target: PlanetSideGameObject with FactionAffinity with Vitality
): DamageInteraction = {
DamageInteraction(
SourceEntry(target),
EmpReason(source, empEffect, target),
position
)
}
/**
* The "within affected distance" test for special electromagnetic pulses
* is not necessarily centered around a game object as the source of that EMP.
* A proxy entity is generated to perform the measurements and
* is given token information that connects it with another object for the proper attribution.
* @see `OwnableByPlayer`
* @see `PlanetSideServerObject`
* @see `SpecialEmp.distanceCheck`
* @param owner the formal entity to which the EMP is attributed
* @param position the coordinate location of the EMP
* @param faction the affinity of the EMP
* @return a function that determines if two game entities are near enough to each other
*/
def prepareDistanceCheck(
owner: PlanetSideGameObject,
position: Vector3,
faction: PlanetSideEmpire.Value
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
distanceCheck(new PlanetSideServerObject with OwnableByPlayer {
Owner = Some(owner.GUID)
OwnerName = owner match {
case p: Player => p.Name
case o: OwnableByPlayer => o.OwnerName.getOrElse("")
case _ => ""
}
Position = position
def Faction = faction
def Definition = proxy_definition
})
}
/**
* The "within affected distance" test for special electromagnetic pulses
* is not necessarily centered around a game object as the source of that EMP.
* A proxy entity is provided to perform the measurements and
* is given token information that connects it with another object for the proper attribution.
* @see `Zone.distanceCheck`
* @param obj1 a game entity, should be the source of the damage
* @param obj2 a game entity, should be the target of the damage
* @param maxDistance the square of the maximum distance permissible between game entities
* before they are no longer considered "near"
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
def distanceCheck(
proxy: PlanetSideGameObject
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
maxDistance: Float
): Boolean = {
Zone.distanceCheck(proxy, obj2, maxDistance)
}
/**
* A sort of `SpecialEmp` that only affects deployed boomer explosives
* must find affected deployed boomer explosive entities.
* @see `BoomerDeployable`
* @param zone the zone in which to search
* @param obj a game entity that is excluded from results
* @param properties information about the effect/damage
* @return two lists of objects with different characteristics;
* the first list is `PlanetSideServerObject` entities with `Vitality`;
* since only boomer explosives are returned, this second list can be ignored
*/
def findAllBoomers(
zone: Zone,
obj: PlanetSideGameObject with FactionAffinity with Vitality,
properties: DamageWithPosition
): (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) = {
(
zone.DeployableList
.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) => o },
Nil
)
}
}

View file

@ -47,6 +47,6 @@ class TrapDeployableControl(trap: TrapDeployable) extends Actor with DamageableE
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = { override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause) super.DestructionAwareness(target, cause)
Deployables.AnnounceDestroyDeployable(trap, None) Deployables.AnnounceDestroyDeployable(trap, None)
Zone.causeExplosion(target.Zone, target, Some(cause)) Zone.serverSideDamage(target.Zone, target, Zone.explosionDamage(Some(cause)))
} }
} }

View file

@ -711,6 +711,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//uninitialize implants //uninitialize implants
avatarActor ! AvatarActor.DeinitializeImplants() avatarActor ! AvatarActor.DeinitializeImplants()
//log historical event
target.History(cause)
//log message
cause.adversarial match { cause.adversarial match {
case Some(a) => case Some(a) =>
damageLog.info(s"DisplayDestroy: ${a.defender} was killed by ${a.attacker}") damageLog.info(s"DisplayDestroy: ${a.defender} was killed by ${a.attacker}")

View file

@ -0,0 +1,22 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.avatar
import enumeratum.values.{StringEnum, StringEnumEntry}
sealed abstract class SpecialCarry(override val value: String) extends StringEnumEntry
/**
* Things that the player can carry that are not stored in the inventory or in holsters.
*/
object SpecialCarry extends StringEnum[SpecialCarry] {
val values = findValues
/** The lattice logic unit (LLU). Not actually a flag. */
case object CaptureFlag extends SpecialCarry(value = "CaptureFlag")
/** Special enhancement modules generated in cavern facilities to be installed into above ground facilities. */
case object VanuModule extends SpecialCarry(value = "VanuModule")
/** Mysterious MacGuffins tied to the Bending. */
case object MonolithUnit extends SpecialCarry(value = "MonolithUnit")
/** Pyon~~ */
case object RabbitBall extends SpecialCarry(value = "RabbitBall")
}

View file

@ -431,3 +431,73 @@ object Cylinder {
*/ */
def apply(p: Vector3, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), v, radius, height) def apply(p: Vector3, v: Vector3, radius: Float, height: Float): Cylinder = Cylinder(Point3D(p), v, radius, height)
} }
/**
* Untested geometry.
* @param p na
* @param relativeForward na
* @param relativeUp na
* @param length na
* @param width na
* @param height na
*/
final case class Cuboid(
p: Point3D,
relativeForward: Vector3,
relativeUp: Vector3,
length: Float,
width: Float,
height: Float,
) extends Geometry3D {
def center: Point3D = Point3D(p.asVector3 + relativeUp * height * 0.5f)
override def pointOnOutside(v: Vector3): Point3D = {
import net.psforever.types.Vector3.{CrossProduct, DotProduct, neg}
val height2 = height * 0.5f
val relativeSide = CrossProduct(relativeForward, relativeUp)
//val forwardVector = relativeForward * length
//val sideVector = relativeSide * width
//val upVector = relativeUp * height2
val closestVector: Vector3 = Seq(
relativeForward, relativeSide, relativeUp,
neg(relativeForward), neg(relativeSide), neg(relativeUp)
).maxBy { dir => DotProduct(dir, v) }
def dz(): Float = {
if (Geometry.closeToInsignificance(v.z) != 0) {
closestVector.z / v.z
} else {
0f
}
}
def dy(): Float = {
if (Geometry.closeToInsignificance(v.y) != 0) {
val fyfactor = closestVector.y / v.y
if (v.z * fyfactor <= height2) {
fyfactor
} else {
dz()
}
} else {
dz()
}
}
val scaleFactor: Float = {
if (Geometry.closeToInsignificance(v.x) != 0) {
val fxfactor = closestVector.x / v.x
if (v.y * fxfactor <= length) {
if (v.z * fxfactor <= height2) {
fxfactor
} else {
dy()
}
} else {
dy()
}
} else {
dy()
}
}
Point3D(center.asVector3 + (v * scaleFactor))
}
}

View file

@ -205,7 +205,7 @@ trait DamageableVehicle
} }
}) })
//things positioned around us can get hurt from us //things positioned around us can get hurt from us
Zone.causeExplosion(obj.Zone, obj, Some(cause)) Zone.serverSideDamage(obj.Zone, target, Zone.explosionDamage(Some(cause)))
//special considerations for certain vehicles //special considerations for certain vehicles
Vehicles.BeforeUnloadVehicle(obj, zone) Vehicles.BeforeUnloadVehicle(obj, zone)
//shields //shields

View file

@ -93,7 +93,7 @@ trait DamageableWeaponTurret
EndAllAggravation() EndAllAggravation()
DamageableWeaponTurret.DestructionAwareness(obj, cause) DamageableWeaponTurret.DestructionAwareness(obj, cause)
DamageableMountable.DestructionAwareness(obj, cause) DamageableMountable.DestructionAwareness(obj, cause)
Zone.causeExplosion(target.Zone, target, Some(cause)) Zone.serverSideDamage(target.Zone, target, Zone.explosionDamage(Some(cause)))
} }
} }

View file

@ -122,7 +122,7 @@ class GeneratorControl(gen: Generator)
queuedExplosion = Default.Cancellable queuedExplosion = Default.Cancellable
imminentExplosion = false imminentExplosion = false
//hate on everything nearby //hate on everything nearby
Zone.causeExplosion(gen.Zone, gen, gen.LastDamage, explosionFunc) Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc)
gen.ClearHistory() gen.ClearHistory()
case GeneratorControl.Restored() => case GeneratorControl.Restored() =>
@ -338,8 +338,8 @@ object GeneratorControl {
* As a consequence, different measurements must be performed to determine that the target is "within" and * As a consequence, different measurements must be performed to determine that the target is "within" and
* that the target is not "outside" of the detection radius of the room. * that the target is not "outside" of the detection radius of the room.
* Magic numbers for the room dimensions are employed. * Magic numbers for the room dimensions are employed.
* @see `Zone.causeExplosion`
* @see `Zone.distanceCheck` * @see `Zone.distanceCheck`
* @see `Zone.serverSideDamage`
* @param g1ctrXY the center of the generator on the xy-axis * @param g1ctrXY the center of the generator on the xy-axis
* @param ufront a `Vector3` entity that points to the "front" direction of the generator; * @param ufront a `Vector3` entity that points to the "front" direction of the generator;
* the `u` prefix indicates a "unit vector" * the `u` prefix indicates a "unit vector"

View file

@ -1,12 +1,15 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.pad package net.psforever.objects.serverobject.pad
import akka.actor.{ActorContext, Cancellable, Props} import akka.actor.{Cancellable, Props}
import net.psforever.objects.avatar.SpecialCarry
import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.guid.GUIDTask.UnregisterVehicle import net.psforever.objects.guid.GUIDTask.UnregisterVehicle
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer} import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.{Zone, ZoneAware, Zoning}
import net.psforever.objects.{Default, Player, Vehicle} import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.Vector3
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -37,8 +40,11 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
/** a reminder sent to future customers */ /** a reminder sent to future customers */
var periodicReminder: Cancellable = Default.Cancellable var periodicReminder: Cancellable = Default.Cancellable
/** repeatedly test whether queued orders are valid */
var queueManagement: Cancellable = Default.Cancellable
/** a list of vehicle orders that have been submitted for this spawn pad */ /** a list of vehicle orders that have been submitted for this spawn pad */
var orders: List[VehicleSpawnControl.Order] = List.empty[VehicleSpawnControl.Order] var orders: List[VehicleSpawnPad.VehicleOrder] = List.empty[VehicleSpawnPad.VehicleOrder]
/** the current vehicle order being acted upon; /** the current vehicle order being acted upon;
* used as a guard condition to control order processing rate * used as a guard condition to control order processing rate
@ -46,7 +52,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
var trackedOrder: Option[VehicleSpawnControl.Order] = None var trackedOrder: Option[VehicleSpawnControl.Order] = None
/** how to process either the first order or every subsequent order */ /** how to process either the first order or every subsequent order */
var handleOrderFunc: VehicleSpawnControl.Order => Unit = NewTasking var handleOrderFunc: VehicleSpawnPad.VehicleOrder => Unit = NewTasking
def LogId = "" def LogId = ""
@ -67,39 +73,60 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
} }
} }
override def postStop() : Unit = {
periodicReminder.cancel()
queueManagement.cancel()
}
def receive: Receive = def receive: Receive =
checkBehavior.orElse { checkBehavior.orElse {
case VehicleSpawnPad.VehicleOrder(player, vehicle) => case msg @ VehicleSpawnPad.VehicleOrder(player, vehicle, _) =>
trace(s"order from ${player.Name} for a ${vehicle.Definition.Name} received") trace(s"order from ${player.Name} for a ${vehicle.Definition.Name} received")
try { try {
handleOrderFunc(VehicleSpawnControl.Order(player, vehicle)) handleOrderFunc(msg)
} catch { } catch {
case _: AssertionError => ; //ehhh case _: AssertionError => ; //ehhh
case e: Exception => //something unexpected case e: Exception => //something unexpected
e.printStackTrace() e.printStackTrace()
} }
case VehicleSpawnControl.ProcessControl.OrderCancelled =>
trackedOrder match {
case Some(entry)
if sender() == concealPlayer =>
CancelOrder(
entry,
VehicleSpawnControl.validateOrderCredentials(pad, entry.driver, entry.vehicle)
.orElse(Some("@SVCP_RemovedFromVehicleQueue_Generic"))
)
case _ => ;
}
trackedOrder = None //guard off
SelectOrder()
case VehicleSpawnControl.ProcessControl.GetNewOrder => case VehicleSpawnControl.ProcessControl.GetNewOrder =>
if (sender() == concealPlayer) { if (sender() == concealPlayer) {
trackedOrder = None //guard off trackedOrder = None //guard off
SelectOrder() SelectOrder()
} }
case VehicleSpawnControl.ProcessControl.QueueManagement =>
queueManagementTask()
/* /*
When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action. When the vehicle is spawned and added to the pad, it will "occupy" the pad and block it from further action.
Normally, the player who wanted to spawn the vehicle will be automatically put into the driver mount. Normally, the player who wanted to spawn the vehicle will be automatically put into the driver mount.
If this is blocked, the vehicle will idle on the pad and must be moved far enough away from the point of origin. If this is blocked, the vehicle will idle on the pad and must be moved far enough away from the point of origin.
During this time, a periodic message about the spawn pad being blocked During this time, a periodic message about the spawn pad being blocked will be broadcast to the order queue.
will be broadcast to all current customers in the order queue. */
*/
case VehicleSpawnControl.ProcessControl.Reminder => case VehicleSpawnControl.ProcessControl.Reminder =>
trackedOrder match { trackedOrder match {
case Some(entry) => case Some(entry) =>
if (periodicReminder.isCancelled) { if (periodicReminder.isCancelled) {
trace(s"the pad has become blocked by ${entry.vehicle.Definition.Name}") trace(s"the pad has become blocked by a ${entry.vehicle.Definition.Name} in its current order")
periodicReminder = context.system.scheduler.scheduleWithFixedDelay( periodicReminder = context.system.scheduler.scheduleWithFixedDelay(
VehicleSpawnControl.initialReminderDelay, VehicleSpawnControl.periodicReminderTestDelay,
VehicleSpawnControl.periodicReminderDelay, VehicleSpawnControl.periodicReminderTestDelay,
self, self,
VehicleSpawnControl.ProcessControl.Reminder VehicleSpawnControl.ProcessControl.Reminder
) )
@ -112,19 +139,16 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
case VehicleSpawnControl.ProcessControl.Flush => case VehicleSpawnControl.ProcessControl.Flush =>
periodicReminder.cancel() periodicReminder.cancel()
orders.foreach { orders.foreach { CancelOrder(_, Some("@SVCP_RemovedFromVehicleQueue_Generic")) }
CancelOrder
}
orders = Nil orders = Nil
trackedOrder match { trackedOrder match {
case Some(entry) => case Some(entry) => CancelOrder(entry, Some("@SVCP_RemovedFromVehicleQueue_Generic"))
CancelOrder(entry)
case None => ; case None => ;
} }
trackedOrder = None trackedOrder = None
handleOrderFunc = NewTasking handleOrderFunc = NewTasking
pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) //cautious animation reset pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) //cautious animation reset
concealPlayer ! akka.actor.Kill //should cause the actor to restart, which will abort any trapped messages concealPlayer ! akka.actor.Kill //should cause the actor to restart, which will abort any trapped messages
case _ => ; case _ => ;
} }
@ -134,7 +158,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* All orders accepted in the meantime will be queued and a note about priority will be issued. * All orders accepted in the meantime will be queued and a note about priority will be issued.
* @param order the order being accepted * @param order the order being accepted
*/ */
def NewTasking(order: VehicleSpawnControl.Order): Unit = { def NewTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
handleOrderFunc = QueuedTasking handleOrderFunc = QueuedTasking
ProcessOrder(Some(order)) ProcessOrder(Some(order))
} }
@ -144,23 +168,51 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* all orders accepted in the meantime will be queued and a note about priority will be issued. * all orders accepted in the meantime will be queued and a note about priority will be issued.
* @param order the order being accepted * @param order the order being accepted
*/ */
def QueuedTasking(order: VehicleSpawnControl.Order): Unit = { def QueuedTasking(order: VehicleSpawnPad.VehicleOrder): Unit = {
val name = order.driver.Name val name = order.player.Name
if ( if (trackedOrder match {
(trackedOrder match { case Some(tracked) =>
case Some(tracked) => !tracked.driver.Name.equals(name) !tracked.driver.Name.equals(name)
case None => true case None =>
}) && orders.forall { !_.driver.Name.equals(name) } handleOrderFunc = NewTasking
) { NewTasking(order)
//not a second order from an existing order's player false
orders = orders :+ order }) {
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( orders.indexWhere { _.player.Name.equals(name) } match {
name, case -1 if orders.isEmpty =>
VehicleSpawnPad.Reminders.Queue, //first queued order
Some(orders.length + 1) orders = List(order)
) queueManagementTask()
} else { pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(
VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) name,
VehicleSpawnPad.Reminders.Queue,
Some(s"@SVCP_PositionInQueue^2~^2~")
)
case -1 =>
//new order
orders = orders :+ order
val size = orders.size + 1
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(
name,
VehicleSpawnPad.Reminders.Queue,
Some(s"@SVCP_PositionInQueue^$size~^$size~")
)
case n if orders(n).vehicle.Definition ne order.vehicle.Definition =>
//replace existing order with new order
val zone = pad.Zone
val originalOrder = orders(n)
val originalVehicle = originalOrder.vehicle.Definition.Name
orders = (orders.take(n) :+ order) ++ orders.drop(n+1)
VehicleSpawnControl.DisposeVehicle(originalOrder.vehicle, zone)
zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(
name,
VehicleSpawnPad.Reminders.Queue,
Some(s"@SVCP_ReplacedVehicleWithVehicle^@$originalVehicle~^@${order.vehicle.Definition.Name}~")
)
case _ =>
//order is the duplicate of an existing order; do nothing to the queue
CancelOrder(order, None)
}
} }
} }
@ -174,17 +226,19 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* If the queue has been exhausted, set functionality to prepare to accept the next order as a "first order." * If the queue has been exhausted, set functionality to prepare to accept the next order as a "first order."
* @return the next-available order * @return the next-available order
*/ */
def SelectFirstOrder(): Option[VehicleSpawnControl.Order] = { def SelectFirstOrder(): Option[VehicleSpawnPad.VehicleOrder] = {
trackedOrder match { trackedOrder match {
case None => case None =>
val (completeOrder, remainingOrders): (Option[VehicleSpawnControl.Order], List[VehicleSpawnControl.Order]) = val (completeOrder, remainingOrders): (Option[VehicleSpawnPad.VehicleOrder], List[VehicleSpawnPad.VehicleOrder]) =
orders match { orderCredentialsCheck(orders) match {
case x :: Nil => case x :: Nil =>
queueManagement.cancel()
(Some(x), Nil) (Some(x), Nil)
case x :: b => case x :: b =>
(Some(x), b) (Some(x), b)
case Nil => case Nil =>
handleOrderFunc = NewTasking handleOrderFunc = NewTasking
queueManagement.cancel()
(None, Nil) (None, Nil)
} }
orders = remainingOrders orders = remainingOrders
@ -201,35 +255,93 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
* @param order the order being accepted; * @param order the order being accepted;
* `None`, if no order found or submitted * `None`, if no order found or submitted
*/ */
def ProcessOrder(order: Option[VehicleSpawnControl.Order]): Unit = { def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = {
periodicReminder.cancel() periodicReminder.cancel()
order match { order match {
case Some(_order) => case Some(_order) =>
recursiveOrderReminder(orders.iterator) val size = orders.size + 1
trace(s"processing next order - a ${_order.vehicle.Definition.Name} for ${_order.driver.Name}") val driver = _order.player
trackedOrder = order //guard on val name = driver.Name
context.system.scheduler.scheduleOnce(2000 milliseconds, concealPlayer, _order) val vehicle = _order.vehicle
val newOrder = VehicleSpawnControl.Order(driver, vehicle)
recursiveOrderReminder(orders.iterator, size)
trace(s"processing next order - a ${vehicle.Definition.Name} for $name")
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(
name,
VehicleSpawnPad.Reminders.Queue,
Some(s"@SVCP_PositionInQueue^1~^$size~")
)
trackedOrder = Some(newOrder) //guard on
context.system.scheduler.scheduleOnce(2000 milliseconds, concealPlayer, newOrder)
case None => ; case None => ;
} }
} }
/**
* One-stop shop to test queued vehicle spawn pad orders for valid credentials and
* either start a periodic examination of those credentials until the queue has been emptied or
* cancel a running periodic examination if the queue is already empty.
*/
def queueManagementTask(): Unit = {
if (orders.nonEmpty) {
orders = orderCredentialsCheck(orders).toList
if (queueManagement.isCancelled) {
queueManagement = context.system.scheduler.scheduleWithFixedDelay(
1.second,
1.second,
self,
VehicleSpawnControl.ProcessControl.QueueManagement
)
}
} else {
queueManagement.cancel()
}
}
/**
* For all orders, ensure that that order's details match acceptable specifications
* and partition all orders that should be cancelled for one reason or another.
* Generate informative error messages for the failing orders, cancel those partitioned orders,
* and only return all orders that are still valid.
* @param recipients the original list of orders
* @return the list of still-acceptable orders
*/
def orderCredentialsCheck(recipients: Iterable[VehicleSpawnPad.VehicleOrder]): Iterable[VehicleSpawnPad.VehicleOrder] = {
recipients
.map { order =>
(order, VehicleSpawnControl.validateOrderCredentials(order.terminal, order.player, order.vehicle))
}
.foldRight(List.empty[VehicleSpawnPad.VehicleOrder]) {
case (f, list) =>
f match {
case (order, msg @ Some(_)) =>
CancelOrder(order, msg)
list
case (order, None) =>
list :+ order
}
}
}
/** /**
* na * na
* @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating * @param blockedOrder the previous order whose vehicle is blocking the spawn pad from operating
* @param recipients all of the other customers who will be receiving the message * @param recipients all of the other customers who will be receiving the message
*/ */
def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnControl.Order]): Unit = { def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = {
val user = blockedOrder.vehicle val user = blockedOrder.vehicle
.Seats(0).occupant .Seats(0).occupant
.orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner)) .orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner))
.orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) .orElse(pad.Zone.GUID(blockedOrder.DriverGUID))
val relevantRecipients = user match { val relevantRecipients: Iterator[VehicleSpawnPad.VehicleOrder] = user match {
case Some(p: Player) if !p.HasGUID => case Some(p: Player) if !p.HasGUID =>
recipients.iterator recipients.iterator
case Some(p: Player) if blockedOrder.driver == p => case Some(_: Player) =>
(blockedOrder +: recipients).iterator (VehicleSpawnPad.VehicleOrder(
case Some(p: Player) => blockedOrder.driver,
(VehicleSpawnControl.Order(p, blockedOrder.vehicle) +: recipients).iterator //who took possession of the vehicle blockedOrder.vehicle,
null //permissible
) +: recipients).iterator //one who took possession of the vehicle
case _ => case _ =>
recipients.iterator recipients.iterator
} }
@ -245,24 +357,37 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
/** /**
* Cancel this vehicle order and inform the person who made it, if possible. * Cancel this vehicle order and inform the person who made it, if possible.
* @param entry the order being cancelled * @param entry the order being cancelled
* @param context an `ActorContext` object for which to create the `TaskResolver` object
*/ */
def CancelOrder(entry: VehicleSpawnControl.Order)(implicit context: ActorContext): Unit = { def CancelOrder(entry: VehicleSpawnControl.Order, msg: Option[String]): Unit = {
val vehicle = entry.vehicle CancelOrder(entry.vehicle, entry.driver, msg)
}
/**
* Cancel this vehicle order and inform the person who made it, if possible.
* @param entry the order being cancelled
*/
def CancelOrder(entry: VehicleSpawnPad.VehicleOrder, msg: Option[String]): Unit = {
CancelOrder(entry.vehicle, entry.player, msg)
}
/**
* Cancel this vehicle order and inform the person who made it, if possible.
* @param vehicle the vehicle from the order being cancelled
* @param player the player who would driver the vehicle from the order being cancelled
*/
def CancelOrder(vehicle: Vehicle, player: Player, msg: Option[String]): Unit = {
if (vehicle.Seats.values.count(_.isOccupied) == 0) { if (vehicle.Seats.values.count(_.isOccupied) == 0) {
VehicleSpawnControl.DisposeSpawnedVehicle(entry, pad.Zone) VehicleSpawnControl.DisposeSpawnedVehicle(vehicle, player, pad.Zone)
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(entry.driver.Name, VehicleSpawnPad.Reminders.Cancelled) pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(player.Name, VehicleSpawnPad.Reminders.Cancelled, msg)
} }
} }
@tailrec private final def recursiveBlockedReminder( @tailrec private final def recursiveBlockedReminder(
iter: Iterator[VehicleSpawnControl.Order], iter: Iterator[VehicleSpawnPad.VehicleOrder],
cause: Option[Any] cause: Option[Any]
): Unit = { ): Unit = {
if (iter.hasNext) { if (iter.hasNext) {
val recipient = iter.next() val recipient = iter.next()
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(
recipient.driver.Name, recipient.player.Name,
VehicleSpawnPad.Reminders.Blocked, VehicleSpawnPad.Reminders.Blocked,
cause cause
) )
@ -271,30 +396,36 @@ class VehicleSpawnControl(pad: VehicleSpawnPad)
} }
@tailrec private final def recursiveOrderReminder( @tailrec private final def recursiveOrderReminder(
iter: Iterator[VehicleSpawnControl.Order], iter: Iterator[VehicleSpawnPad.VehicleOrder],
size: Int,
position: Int = 2 position: Int = 2
): Unit = { ): Unit = {
if (iter.hasNext) { if (iter.hasNext) {
val recipient = iter.next() val recipient = iter.next()
pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(
recipient.driver.Name, recipient.player.Name,
VehicleSpawnPad.Reminders.Queue, VehicleSpawnPad.Reminders.Queue,
Some(position) Some(s"@SVCP_PositionInQueue^$position~^$size~")
) )
recursiveOrderReminder(iter, position + 1) recursiveOrderReminder(iter, size, position + 1)
} }
} }
} }
object VehicleSpawnControl { object VehicleSpawnControl {
private final val initialReminderDelay: FiniteDuration = 10000 milliseconds private final val periodicReminderTestDelay: FiniteDuration = 10 seconds
private final val periodicReminderDelay: FiniteDuration = 10000 milliseconds
/** /**
* An `Enumeration` of non-data control messages for the vehicle spawn process. * Control messages for the vehicle spawn process.
*/ */
object ProcessControl extends Enumeration { object ProcessControl {
val Reminder, GetNewOrder, Flush = Value sealed trait ProcessControl
case object Flush extends ProcessControl
case object OrderCancelled extends ProcessControl
case object GetNewOrder extends ProcessControl
case object Reminder extends ProcessControl
case object QueueManagement extends ProcessControl
} }
/** /**
@ -306,17 +437,74 @@ object VehicleSpawnControl {
assert(driver.HasGUID, s"when ordering a vehicle, the prospective driver ${driver.Name} does not have a GUID") assert(driver.HasGUID, s"when ordering a vehicle, the prospective driver ${driver.Name} does not have a GUID")
assert(vehicle.HasGUID, s"when ordering a vehicle, the ${vehicle.Definition.Name} does not have a GUID") assert(vehicle.HasGUID, s"when ordering a vehicle, the ${vehicle.Definition.Name} does not have a GUID")
val DriverGUID = driver.GUID val DriverGUID = driver.GUID
val time = System.currentTimeMillis()
}
/**
* Assess the applicable details of an order that is being processed (is usually enqueued)
* and determine whether it is is still valid based on the current situation of those details.
* @param inZoneThing some physical aspect of this system through which the order will be processed;
* either the vehicle spawn pad or the vehicle spawn terminal are useful;
* this entity and the player are subject to a distance check
* @param player the player who would be the driver of the vehicle filed in the order
* @param vehicle the vehicle filed in the order
* @param tooFarDistance the distance check;
* defaults to 1225 (35m squared) relative to the anticipation of a `Terminal` entity
* @return whether or not a cancellation message is associated with these entry details,
* explaining why the order should be cancelled
*/
def validateOrderCredentials(
inZoneThing: PlanetSideGameObject with WorldEntity with ZoneAware,
player: Player,
vehicle: Vehicle,
tooFarDistance: Float = 1225
): Option[String] = {
if (!player.HasGUID || player.Zone != inZoneThing.Zone || !vehicle.HasGUID || vehicle.Destroyed) {
Some("@SVCP_RemovedFromVehicleQueue_Generic")
} else if (!player.isAlive || player.isReleased) {
Some("@SVCP_RemovedFromVehicleQueue_Destroyed")
} else if (vehicle.PassengerInSeat(player).isEmpty) {
//once seated, these are not a concern anymore
if (inZoneThing.Destroyed) {
Some("@SVCP_RemovedFromQueue_TerminalDestroyed")
} else if (Vector3.DistanceSquared(inZoneThing.Position, player.Position) > tooFarDistance) {
Some("@SVCP_RemovedFromVehicleQueue_MovedTooFar")
} else if (player.VehicleSeated.nonEmpty) {
Some("@SVCP_RemovedFromVehicleQueue_ParentChanged")
} else if (!vehicle.Seats(0).definition.restriction.test(player)) {
Some("@SVCP_RemovedFromVehicleQueue_ArmorChanged")
} else if (player.Carrying.contains(SpecialCarry.CaptureFlag)) {
Some("@SVCP_RemovedFromVehicleQueue_CaptureFlag")
} else if (player.Carrying.contains(SpecialCarry.VanuModule)) {
Some("@SVCP_RemovedFromVehicleQueue_VanuModule")
} else if (player.Carrying.contains(SpecialCarry.MonolithUnit)) {
Some("@SVCP_RemovedFromVehicleQueue_MonolithUnit")
} else if ( player.ZoningRequest == Zoning.Method.Quit) {
Some("@SVCP_RemovedFromVehicleQueue_Quit")
} else if ( player.ZoningRequest == Zoning.Method.InstantAction) {
Some("@SVCP_RemovedFromVehicleQueue_InstantAction")
} else if ( player.ZoningRequest == Zoning.Method.Recall) {
Some("@SVCP_RemovedFromVehicleQueue_Recall")
} else if ( player.ZoningRequest == Zoning.Method.OutfitRecall) {
Some("@SVCP_RemovedFromVehicleQueue_OutfitRecall")
} else {
None
}
} else {
None
}
} }
/** /**
* Properly clean up a vehicle that has been registered and spawned into the game world. * Properly clean up a vehicle that has been registered and spawned into the game world.
* Call this downstream of "`ConcealPlayer`". * Call this downstream of "`ConcealPlayer`".
* @param entry the order being cancelled * @param vehicle the vehicle being disposed
* @param player the player who would own the vehicle being disposed
* @param zone the zone in which the vehicle is registered (should be located) * @param zone the zone in which the vehicle is registered (should be located)
*/ */
def DisposeSpawnedVehicle(entry: VehicleSpawnControl.Order, zone: Zone): Unit = { def DisposeSpawnedVehicle(vehicle: Vehicle, player: Player, zone: Zone): Unit = {
DisposeVehicle(entry.vehicle, zone) DisposeVehicle(vehicle, zone)
zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(entry.DriverGUID) zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(player.GUID)
} }
/** /**
@ -325,8 +513,8 @@ object VehicleSpawnControl {
* @param zone the zone in which the vehicle is registered (should be located) * @param zone the zone in which the vehicle is registered (should be located)
*/ */
def DisposeVehicle(vehicle: Vehicle, zone: Zone): Unit = { def DisposeVehicle(vehicle: Vehicle, zone: Zone): Unit = {
if (zone.Vehicles.exists(_.GUID == vehicle.GUID)) { //already added to zone if (zone.Vehicles.contains(vehicle)) { //already added to zone
vehicle.Actor ! Vehicle.Deconstruct() vehicle.Actor ! Vehicle.Deconstruct(Some(0.seconds))
} else { //just registered to zone } else { //just registered to zone
zone.tasks ! UnregisterVehicle(vehicle)(zone.GUID) zone.tasks ! UnregisterVehicle(vehicle)(zone.GUID)
} }

View file

@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.pad
import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.{Player, Vehicle}
import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.types.PlanetSideGUID import net.psforever.types.PlanetSideGUID
/** /**
@ -28,7 +29,7 @@ object VehicleSpawnPad {
* @param player the player who submitted the order (the "owner") * @param player the player who submitted the order (the "owner")
* @param vehicle the vehicle produced from the order * @param vehicle the vehicle produced from the order
*/ */
final case class VehicleOrder(player: Player, vehicle: Vehicle) final case class VehicleOrder(player: Player, vehicle: Vehicle, terminal: Terminal)
/** /**
* Message to indicate that a certain player should be made transparent. * Message to indicate that a certain player should be made transparent.
@ -130,9 +131,11 @@ object VehicleSpawnPad {
* An `Enumeration` of reasons for sending a periodic reminder to the user. * An `Enumeration` of reasons for sending a periodic reminder to the user.
*/ */
object Reminders extends Enumeration { object Reminders extends Enumeration {
val Queue, //optional data is the numeric position in the queue val
Blocked, //optional data is a message regarding the blockage Queue, //optional data is the numeric position in the queue
Cancelled = Value Blocked, //optional data is a message regarding the blockage
Cancelled //optional data is the message
= Value
} }
/** /**

View file

@ -1,7 +1,9 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.pad package net.psforever.objects.serverobject.pad
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.serverobject.structures.AmenityDefinition import net.psforever.objects.serverobject.structures.AmenityDefinition
import net.psforever.types.Vector3
/** /**
* The definition for any `VehicleSpawnPad`. * The definition for any `VehicleSpawnPad`.
@ -39,4 +41,166 @@ class VehicleSpawnPadDefinition(objectId: Int) extends AmenityDefinition(objectI
case 947 => Name = "vanu_vehicle_creation_pad" case 947 => Name = "vanu_vehicle_creation_pad"
case _ => throw new IllegalArgumentException("Not a valid object id with the type vehicle_creation_pad") case _ => throw new IllegalArgumentException("Not a valid object id with the type vehicle_creation_pad")
} }
/** The region surrounding a vehicle spawn pad that is cleared of damageable targets prior to a vehicle being spawned.
* I mean to say that, if it can die, that target will die.
* @see `net.psforever.objects.serverobject.pad.process.VehicleSpawnControlRailJack` */
var killBox: (VehicleSpawnPad, Boolean)=>(PlanetSideGameObject, PlanetSideGameObject, Float)=> Boolean =
VehicleSpawnPadDefinition.prepareKillBox(forwardLimit = 0, backLimit = 0, sideLimit = 0, aboveLimit = 0)
}
object VehicleSpawnPadDefinition {
/**
* A function that sets up the region around a vehicle spawn pad
* to be cleared of damageable targets upon spawning of a vehicle.
* All measurements are provided in terms of distance from the center of the pad.
* These generic pads are rectangular in bounds and the kill box is cuboid in shape.
* @param forwardLimit how far in front of the spawn pad is to be cleared
* @param backLimit how far behind the spawn pad to be cleared;
* "back" is a squared direction usually in that direction of the corresponding terminal
* @param sideLimit how far to either side of the spawn pad is to be cleared
* @param aboveLimit how far above the spawn pad is to be cleared
* @param pad the vehicle spawn pad in question
* @param flightVehicle whether the current vehicle being ordered is a flying craft
* @return a function that describes a region around the vehicle spawn pad
*/
def prepareKillBox(
forwardLimit: Float,
backLimit: Float,
sideLimit: Float,
aboveLimit: Float
)
(
pad: VehicleSpawnPad,
flightVehicle: Boolean
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
val forward = Vector3(0,1,0).Rz(pad.Orientation.z + pad.Definition.VehicleCreationZOrientOffset)
val side = Vector3.CrossProduct(forward, Vector3(0,0,1))
vehicleSpawnKillBox(
forward,
side,
pad.Position,
if (flightVehicle) backLimit else forwardLimit,
backLimit,
sideLimit,
if (flightVehicle) aboveLimit * 2 else aboveLimit,
)
}
/**
* A function that finalizes the detection for the region around a vehicle spawn pad
* to be cleared of damageable targets upon spawning of a vehicle.
* All measurements are provided in terms of distance from the center of the pad.
* These generic pads are rectangular in bounds and the kill box is cuboid in shape.
* @param forward a direction in a "forwards" direction relative to the orientation of the spawn pad
* @param side a direction in a "side-wards" direction relative to the orientation of the spawn pad
* @param origin the center of the spawn pad
* @param forwardLimit how far in front of the spawn pad is to be cleared
* @param backLimit how far behind the spawn pad to be cleared
* @param sideLimit how far to either side of the spawn pad is to be cleared
* @param aboveLimit how far above the spawn pad is to be cleared
* @param obj1 a game entity, should be the source
* @param obj2 a game entity, should be the target
* @param maxDistance the square of the maximum distance permissible between game entities
* before they are no longer considered "near"
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
protected def vehicleSpawnKillBox(
forward: Vector3,
side: Vector3,
origin: Vector3,
forwardLimit: Float,
backLimit: Float,
sideLimit: Float,
aboveLimit: Float
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
maxDistance: Float
): Boolean = {
val dir: Vector3 = {
val g2 = obj2.Definition.Geometry(obj2)
val cdir = Vector3.Unit(origin - g2.center.asVector3)
val point = g2.pointOnOutside(cdir).asVector3
point - origin
}
val originZ = origin.z
val obj2Z = obj2.Position.z
originZ - 1 <= obj2Z && originZ + aboveLimit > obj2Z &&
{
val calculatedForwardDistance = Vector3.ScalarProjection(dir, forward)
if (calculatedForwardDistance >= 0) {
calculatedForwardDistance < forwardLimit
}
else {
-calculatedForwardDistance < backLimit
}
} &&
math.abs(Vector3.ScalarProjection(dir, side)) < sideLimit
}
/**
* A function that sets up the region around a vehicle spawn pad
* to be cleared of damageable targets upon spawning of a vehicle.
* All measurements are provided in terms of distance from the center of the pad.
* These pads are only found in the cavern zones and are cylindrical in shape.
* @param radius the distance from the middle of the spawn pad
* @param aboveLimit how far above the spawn pad is to be cleared
* @param pad he vehicle spawn pad in question
* @param flightVehicle whether the current vehicle being ordered is a flying craft
* @return a function that describes a region around the vehicle spawn pad
*/
def prepareVanuKillBox(
radius: Float,
aboveLimit: Float
)
(
pad: VehicleSpawnPad,
flightVehicle: Boolean
): (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = {
if (flightVehicle) {
vanuKillBox(pad.Position, radius, aboveLimit * 2)
} else {
vanuKillBox(pad.Position, radius * 1.2f, aboveLimit)
}
}
/**
* A function that finalizes the detection for the region around a vehicle spawn pad
* to be cleared of damageable targets upon spawning of a vehicle.
* All measurements are provided in terms of distance from the center of the pad.
* These pads are only found in the cavern zones and are cylindrical in shape.
* @param origin the center of the spawn pad
* @param radius the distance from the middle of the spawn pad
* @param aboveLimit how far above the spawn pad is to be cleared
* @param obj1 a game entity, should be the source
* @param obj2 a game entity, should be the target
* @param maxDistance the square of the maximum distance permissible between game entities
* before they are no longer considered "near"
* @return `true`, if the two entities are near enough to each other;
* `false`, otherwise
*/
def vanuKillBox(
origin: Vector3,
radius: Float,
aboveLimit: Float
)
(
obj1: PlanetSideGameObject,
obj2: PlanetSideGameObject,
maxDistance: Float
): Boolean = {
val dir: Vector3 = {
val g2 = obj2.Definition.Geometry(obj2)
val cdir = Vector3.Unit(origin - g2.center.asVector3)
val point = g2.pointOnOutside(cdir).asVector3
point - origin
}
val originZ = origin.z
val obj2Z = obj2.Position.z
originZ - 1 <= obj2Z && originZ + aboveLimit > obj2Z &&
Vector3.MagnitudeSquared(dir.xy) < radius * radius
}
} }

View file

@ -25,19 +25,19 @@ class VehicleSpawnControlConcealPlayer(pad: VehicleSpawnPad) extends VehicleSpaw
context.actorOf(Props(classOf[VehicleSpawnControlLoadVehicle], pad), s"${context.parent.path.name}-load") context.actorOf(Props(classOf[VehicleSpawnControlLoadVehicle], pad), s"${context.parent.path.name}-load")
def receive: Receive = { def receive: Receive = {
case order @ VehicleSpawnControl.Order(driver, _) => case order @ VehicleSpawnControl.Order(driver, vehicle) =>
//TODO how far can the driver stray from the Terminal before his order is cancelled? if (VehicleSpawnControl.validateOrderCredentials(pad, driver, vehicle).isEmpty) {
if (driver.Continent == pad.Continent && driver.VehicleSeated.isEmpty && driver.isAlive) {
trace(s"hiding ${driver.Name}") trace(s"hiding ${driver.Name}")
pad.Zone.VehicleEvents ! VehicleSpawnPad.ConcealPlayer(driver.GUID) pad.Zone.VehicleEvents ! VehicleSpawnPad.ConcealPlayer(driver.GUID)
context.system.scheduler.scheduleOnce(2000 milliseconds, loadVehicle, order) context.system.scheduler.scheduleOnce(2000 milliseconds, loadVehicle, order)
} else { } else {
trace(s"integral component lost; abort order fulfillment") trace(s"integral component lost; abort order fulfillment")
VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
} }
case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => case msg @ (VehicleSpawnControl.ProcessControl.Reminder |
VehicleSpawnControl.ProcessControl.GetNewOrder |
VehicleSpawnControl.ProcessControl.OrderCancelled) =>
context.parent ! msg context.parent ! msg
case _ => ; case _ => ;

View file

@ -22,13 +22,10 @@ class VehicleSpawnControlDriverControl(pad: VehicleSpawnPad) extends VehicleSpaw
def receive: Receive = { def receive: Receive = {
case order @ VehicleSpawnControl.Order(driver, vehicle) => case order @ VehicleSpawnControl.Order(driver, vehicle) =>
if (vehicle.Health > 0 && vehicle.PassengerInSeat(driver).contains(0)) { trace(s"returning control of ${vehicle.Definition.Name} to its current driver")
trace(s"returning control of ${vehicle.Definition.Name} to ${driver.Name}") if (vehicle.PassengerInSeat(driver).nonEmpty) {
pad.Zone.VehicleEvents ! VehicleSpawnPad.ServerVehicleOverrideEnd(driver.Name, vehicle, pad) pad.Zone.VehicleEvents ! VehicleSpawnPad.ServerVehicleOverrideEnd(driver.Name, vehicle, pad)
} else {
trace(s"${driver.Name} is not seated in ${vehicle.Definition.Name}; vehicle controls might have been locked")
} }
vehicle.MountedIn = None
finalClear ! order finalClear ! order
case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) =>

View file

@ -1,6 +1,8 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.pad.process package net.psforever.objects.serverobject.pad.process
import akka.actor.Cancellable
import net.psforever.objects.Default
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.types.{PlanetSideGUID, Vector3} import net.psforever.types.{PlanetSideGUID, Vector3}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
@ -13,8 +15,7 @@ import scala.concurrent.duration._
* The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other. * The basic `VehicleSpawnControl` is the root of a simple tree of "spawn control" objects that chain to each other.
* Each object performs on (or more than one related) actions upon the vehicle order that was submitted.<br> * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.<br>
* <br> * <br>
* There is nothing left to do * There is nothing left to do except make certain that the vehicle has moved far enough away from the spawn pad
* except make certain that the vehicle has moved far enough away from the spawn pad
* to not block the next order that may be queued. * to not block the next order that may be queued.
* A long call is made to the root of this `Actor` object chain to start work on any subsequent vehicle order. * A long call is made to the root of this `Actor` object chain to start work on any subsequent vehicle order.
* @param pad the `VehicleSpawnPad` object being governed * @param pad the `VehicleSpawnPad` object being governed
@ -22,10 +23,16 @@ import scala.concurrent.duration._
class VehicleSpawnControlFinalClearance(pad: VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { class VehicleSpawnControlFinalClearance(pad: VehicleSpawnPad) extends VehicleSpawnControlBase(pad) {
def LogId = "-clearer" def LogId = "-clearer"
var temp: Cancellable = Default.Cancellable
override def postStop() : Unit = {
temp.cancel()
}
def receive: Receive = { def receive: Receive = {
case order @ VehicleSpawnControl.Order(driver, vehicle) => case order @ VehicleSpawnControl.Order(_, vehicle) =>
if (vehicle.PassengerInSeat(driver).isEmpty) { if (!vehicle.Seats(0).isOccupied) {
//ensure the vacant vehicle is above the trench and doors //ensure the vacant vehicle is above the trench and the doors
vehicle.Position = pad.Position + Vector3.z(pad.Definition.VehicleCreationZOffset) vehicle.Position = pad.Position + Vector3.z(pad.Definition.VehicleCreationZOffset)
val definition = vehicle.Definition val definition = vehicle.Definition
pad.Zone.VehicleEvents ! VehicleServiceMessage( pad.Zone.VehicleEvents ! VehicleServiceMessage(
@ -43,12 +50,20 @@ class VehicleSpawnControlFinalClearance(pad: VehicleSpawnPad) extends VehicleSpa
self ! VehicleSpawnControlFinalClearance.Test(order) self ! VehicleSpawnControlFinalClearance.Test(order)
case test @ VehicleSpawnControlFinalClearance.Test(entry) => case test @ VehicleSpawnControlFinalClearance.Test(entry) =>
if (Vector3.DistanceSquared(entry.vehicle.Position, pad.Position) > 100.0f) { //10m away from pad //the vehicle has an initial decay of 30s in which time it needs to be mounted
//once mounted, it will complain to the current driver that it is blocking the spawn pad
//no time limit exists for that state
val vehicle = entry.vehicle
if (Vector3.DistanceSquared(vehicle.Position, pad.Position) > 144) { //12m away from pad
trace("pad cleared") trace("pad cleared")
pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad)
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
} else if (vehicle.Destroyed) {
trace("clearing the pad of vehicle wreckage")
VehicleSpawnControl.DisposeVehicle(vehicle, pad.Zone)
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
} else { } else {
context.system.scheduler.scheduleOnce(2000 milliseconds, self, test) temp = context.system.scheduler.scheduleOnce(2000 milliseconds, self, test)
} }
case _ => ; case _ => ;

View file

@ -1,8 +1,10 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.pad.process package net.psforever.objects.serverobject.pad.process
import akka.actor.{Cancellable, Props} import akka.actor.Props
import net.psforever.objects.{Default, GlobalDefinitions} import akka.pattern.ask
import akka.util.Timeout
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.services.Service import net.psforever.services.Service
@ -11,6 +13,7 @@ import net.psforever.types.Vector3
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Success
/** /**
* An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`. * An `Actor` that handles vehicle spawning orders for a `VehicleSpawnPad`.
@ -28,63 +31,64 @@ class VehicleSpawnControlLoadVehicle(pad: VehicleSpawnPad) extends VehicleSpawnC
val railJack = context.actorOf(Props(classOf[VehicleSpawnControlRailJack], pad), s"${context.parent.path.name}-rails") val railJack = context.actorOf(Props(classOf[VehicleSpawnControlRailJack], pad), s"${context.parent.path.name}-rails")
var temp: Cancellable = Default.Cancellable var temp: Option[VehicleSpawnControl.Order] = None
override def postStop() : Unit = { implicit val timeout = Timeout(3.seconds)
temp.cancel()
super.postStop()
}
def receive: Receive = { def receive: Receive = {
case order @ VehicleSpawnControl.Order(driver, vehicle) => case order @ VehicleSpawnControl.Order(driver, vehicle) =>
if (driver.Continent == pad.Continent && vehicle.Health > 0 && driver.isAlive) { if (VehicleSpawnControl.validateOrderCredentials(pad, driver, vehicle).isEmpty) {
trace(s"loading the ${vehicle.Definition.Name}") trace(s"loading the ${vehicle.Definition.Name}")
vehicle.Position = vehicle.Position - Vector3.z( vehicle.Position = vehicle.Position - Vector3.z(
if (GlobalDefinitions.isFlightVehicle(vehicle.Definition)) 9 else 5 if (GlobalDefinitions.isFlightVehicle(vehicle.Definition)) 9 else 5
) //appear below the trench and doors ) //appear below the trench and doors
vehicle.Cloaked = vehicle.Definition.CanCloak && driver.Cloaked vehicle.Cloaked = vehicle.Definition.CanCloak && driver.Cloaked
pad.Zone.Transport.tell(Zone.Vehicle.Spawn(vehicle), self)
temp = context.system.scheduler.scheduleOnce( temp = Some(order)
delay = 100 milliseconds, val result = ask(pad.Zone.Transport, Zone.Vehicle.Spawn(vehicle))
self, //if too long, or something goes wrong
VehicleSpawnControlLoadVehicle.WaitOnSpawn(order) result.recover {
) case _ =>
temp = None
context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled
}
//resolution
result.onComplete {
case Success(Zone.Vehicle.HasSpawned(zone, v))
if (temp match { case Some(_order) => _order.vehicle eq v; case _ => false }) =>
val definition = v.Definition
val vtype = definition.ObjectId
val vguid = v.GUID
val vdata = definition.Packet.ConstructorData(v).get
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.LoadVehicle(Service.defaultPlayerGUID, v, vtype, vguid, vdata)
)
railJack ! temp.get
temp = None
case Success(Zone.Vehicle.CanNotSpawn(_, _, reason)) =>
trace(s"vehicle can not spawn - $reason; abort order fulfillment")
temp = None
context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled
case _ =>
temp match {
case Some(_) =>
trace(s"abort order fulfillment")
context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled
case None => ; //should we have gotten this message?
}
temp = None
}
} else { } else {
trace("owner lost or vehicle in poor condition; abort order fulfillment") trace("owner lost or vehicle in poor condition; abort order fulfillment")
VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
} }
case Zone.Vehicle.HasSpawned(zone, vehicle) =>
val definition = vehicle.Definition
val vtype = definition.ObjectId
val vguid = vehicle.GUID
val vdata = definition.Packet.ConstructorData(vehicle).get
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.LoadVehicle(Service.defaultPlayerGUID, vehicle, vtype, vguid, vdata)
)
case VehicleSpawnControlLoadVehicle.WaitOnSpawn(order) =>
if (pad.Zone.Vehicles.contains(order.vehicle)) {
railJack ! order
} else {
VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone)
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
}
case Zone.Vehicle.CanNotSpawn(_, _, reason) =>
trace(s"vehicle $reason; abort order fulfillment")
temp.cancel()
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) =>
context.parent ! msg context.parent ! msg
case _ => ; case _ => ;
} }
} }
object VehicleSpawnControlLoadVehicle {
private case class WaitOnSpawn(order: VehicleSpawnControl.Order)
}

View file

@ -2,7 +2,15 @@
package net.psforever.objects.serverobject.pad.process package net.psforever.objects.serverobject.pad.process
import akka.actor.Props import akka.actor.Props
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.etc.{ExplodingEntityReason, VehicleSpawnReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.zones.Zone
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -13,10 +21,7 @@ import scala.concurrent.duration._
* Each object performs on (or more than one related) actions upon the vehicle order that was submitted.<br> * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.<br>
* <br> * <br>
* When the vehicle is added into the environment, it is attached to the spawn pad platform. * When the vehicle is added into the environment, it is attached to the spawn pad platform.
* On cue, the trapdoor of the platform will open, and the vehicle will be raised up into plain sight on a group of rails. * On cue, the trapdoor of the platform will open, and the vehicle will be raised on a railed platform.
* These actions are actually integrated into previous stages and into later stages of the process.
* The primary objective to be completed is a specific place to start a frequent message to the other customers.
* It has failure cases should the driver be in an incorrect state.
* @param pad the `VehicleSpawnPad` object being governed * @param pad the `VehicleSpawnPad` object being governed
*/ */
class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnControlBase(pad) {
@ -26,8 +31,14 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont
context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-mount") context.actorOf(Props(classOf[VehicleSpawnControlSeatDriver], pad), s"${context.parent.path.name}-mount")
def receive: Receive = { def receive: Receive = {
case order @ VehicleSpawnControl.Order(_, vehicle) => case order @ VehicleSpawnControl.Order(driver, vehicle) =>
vehicle.MountedIn = pad.GUID vehicle.MountedIn = pad.GUID
Zone.serverSideDamage(
pad.Zone,
pad,
VehicleSpawnControlRailJack.prepareSpawnExplosion(pad, SourceEntry(driver), SourceEntry(vehicle)),
pad.Definition.killBox(pad, vehicle.Definition.CanFly)
)
pad.Zone.VehicleEvents ! VehicleSpawnPad.AttachToRails(vehicle, pad) pad.Zone.VehicleEvents ! VehicleSpawnPad.AttachToRails(vehicle, pad)
context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order) context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order)
@ -37,3 +48,46 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont
case _ => ; case _ => ;
} }
} }
object VehicleSpawnControlRailJack {
def prepareSpawnExplosion(
pad: VehicleSpawnPad,
driver: SourceEntry,
vehicle: SourceEntry
):
(
PlanetSideGameObject with FactionAffinity with Vitality,
PlanetSideGameObject with FactionAffinity with Vitality
) => DamageInteraction = {
vehicleSpawnExplosion(
vehicle,
pad.Definition.innateDamage.get,
Some(DamageInteraction(
SourceEntry(pad),
VehicleSpawnReason(driver, vehicle),
pad.Position
).calculate()(pad))
)
}
def vehicleSpawnExplosion(
vehicle: SourceEntry,
properties: DamageProperties,
cause: Option[DamageResult]
)
(
source: PlanetSideGameObject with FactionAffinity with Vitality,
target: PlanetSideGameObject with FactionAffinity with Vitality
): DamageInteraction = {
DamageInteraction(
SourceEntry(target),
ExplodingEntityReason(
vehicle,
properties,
target.DamageModel,
cause
),
target.Position
)
}
}

View file

@ -44,7 +44,7 @@ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnCo
val vehicle = entry.vehicle val vehicle = entry.vehicle
//avoid unattended vehicle blocking the pad; user should mount (and does so normally) to reset decon timer //avoid unattended vehicle blocking the pad; user should mount (and does so normally) to reset decon timer
vehicle.Actor ! Vehicle.Deconstruct(Some(30 seconds)) vehicle.Actor ! Vehicle.Deconstruct(Some(30 seconds))
if (vehicle.Health > 0 && driver.isAlive && driver.Continent == pad.Continent && driver.VehicleSeated.isEmpty) { if (VehicleSpawnControl.validateOrderCredentials(pad, driver, vehicle).isEmpty) {
trace("driver to be made seated in vehicle") trace("driver to be made seated in vehicle")
pad.Zone.VehicleEvents ! VehicleSpawnPad.StartPlayerSeatedInVehicle(driver.Name, vehicle, pad) pad.Zone.VehicleEvents ! VehicleSpawnPad.StartPlayerSeatedInVehicle(driver.Name, vehicle, pad)
} else { } else {
@ -53,7 +53,10 @@ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnCo
context.system.scheduler.scheduleOnce(2500 milliseconds, self, VehicleSpawnControlSeatDriver.DriverInSeat(entry)) context.system.scheduler.scheduleOnce(2500 milliseconds, self, VehicleSpawnControlSeatDriver.DriverInSeat(entry))
case VehicleSpawnControlSeatDriver.DriverInSeat(entry) => case VehicleSpawnControlSeatDriver.DriverInSeat(entry) =>
if (entry.vehicle.Health > 0 && entry.driver.isAlive && entry.vehicle.PassengerInSeat(entry.driver).contains(0)) { val driver = entry.driver
val vehicle = entry.vehicle
if (VehicleSpawnControl.validateOrderCredentials(pad, driver, vehicle).isEmpty &&
entry.vehicle.PassengerInSeat(entry.driver).contains(0)) {
trace(s"driver ${entry.driver.Name} has taken the wheel") trace(s"driver ${entry.driver.Name} has taken the wheel")
pad.Zone.VehicleEvents ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.driver.Name, entry.vehicle, pad) pad.Zone.VehicleEvents ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.driver.Name, entry.vehicle, pad)
} else { } else {

View file

@ -30,6 +30,7 @@ class VehicleSpawnControlServerVehicleOverride(pad: VehicleSpawnPad) extends Veh
val vehicleFailState = vehicle.Health == 0 || vehicle.Position == Vector3.Zero val vehicleFailState = vehicle.Health == 0 || vehicle.Position == Vector3.Zero
val driverFailState = val driverFailState =
!driver.isAlive || driver.Continent != pad.Continent || !vehicle.PassengerInSeat(driver).contains(0) !driver.isAlive || driver.Continent != pad.Continent || !vehicle.PassengerInSeat(driver).contains(0)
vehicle.MountedIn = None
pad.Zone.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad) pad.Zone.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad)
if (vehicleFailState || driverFailState) { if (vehicleFailState || driverFailState) {
if (vehicleFailState) { if (vehicleFailState) {

View file

@ -698,7 +698,10 @@ class VehicleControl(vehicle: Vehicle)
percentage, percentage,
body, body,
vehicle.Seats.values vehicle.Seats.values
.flatMap { case seat if seat.isOccupied => seat.occupants } .flatMap {
case seat if seat.isOccupied => seat.occupants
case _ => Nil
}
.filter { p => p.isAlive && (p.Zone eq vehicle.Zone) } .filter { p => p.isAlive && (p.Zone eq vehicle.Zone) }
) )
} }

View file

@ -3,7 +3,6 @@ package net.psforever.objects.vital.etc
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageReason, DamageResolution} import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
@ -13,9 +12,8 @@ import net.psforever.objects.vital.resolution.DamageAndResistance
/** /**
* A wrapper for a "damage source" in damage calculations * A wrapper for a "damage source" in damage calculations
* that parameterizes information necessary to explain a server-driven electromagnetic pulse occurring. * that parameterizes information necessary to explain a server-driven electromagnetic pulse occurring.
* @see `VitalityDefinition.explodes` * @see `SpecialEmp.createEmpInteraction`
* @see `VitalityDefinition.innateDamage` * @see `VitalityDefinition.innateDamage`
* @see `Zone.causesSpecialEmp`
* @param entity the source of the explosive yield * @param entity the source of the explosive yield
* @param damageModel the model to be utilized in these calculations; * @param damageModel the model to be utilized in these calculations;
* typically, but not always, defined by the target * typically, but not always, defined by the target
@ -41,7 +39,7 @@ object EmpReason {
def apply( def apply(
owner: PlanetSideGameObject with FactionAffinity, owner: PlanetSideGameObject with FactionAffinity,
source: DamageWithPosition, source: DamageWithPosition,
target: PlanetSideServerObject with Vitality target: PlanetSideGameObject with Vitality
): EmpReason = { ): EmpReason = {
EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId) EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId)
} }

View file

@ -4,10 +4,11 @@ package net.psforever.objects.vital.etc
import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.ballistics.SourceEntry import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.ObjectDefinition
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.{Vitality, VitalityDefinition} import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution} import net.psforever.objects.vital.base.{DamageModifiers, DamageReason, DamageResolution}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition}
import net.psforever.objects.vital.resolution.DamageAndResistance import net.psforever.objects.vital.resolution.DamageAndResistance
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
@ -18,21 +19,18 @@ import net.psforever.objects.zones.Zone
* @see `VitalityDefinition.explodes` * @see `VitalityDefinition.explodes`
* @see `VitalityDefinition.innateDamage` * @see `VitalityDefinition.innateDamage`
* @see `Zone.causesExplosion` * @see `Zone.causesExplosion`
* @param entity the source of the explosive yield * @param entity what is accredited as the source of the explosive yield
* @param source information about the explosive yield
* @param damageModel the model to be utilized in these calculations; * @param damageModel the model to be utilized in these calculations;
* typically, but not always, defined by the target * typically, but not always, defined by the target
* @param instigation what previous event happened, if any, that caused this explosion * @param instigation what previous event happened, if any, that caused this explosion
*/ */
final case class ExplodingEntityReason( final case class ExplodingEntityReason(
entity: PlanetSideGameObject with Vitality, entity: SourceEntry,
source: DamageProperties,
damageModel: DamageAndResistance, damageModel: DamageAndResistance,
instigation: Option[DamageResult] instigation: Option[DamageResult]
) extends DamageReason { ) extends DamageReason {
private val definition = entity.Definition.asInstanceOf[ObjectDefinition with VitalityDefinition]
assert(definition.explodes && definition.innateDamage.nonEmpty, "causal entity does not explode")
def source: DamageWithPosition = definition.innateDamage.get
def resolution: DamageResolution.Value = DamageResolution.Explosion def resolution: DamageResolution.Value = DamageResolution.Explosion
def same(test: DamageReason): Boolean = test match { def same(test: DamageReason): Boolean = test match {
@ -43,11 +41,29 @@ final case class ExplodingEntityReason(
/** lay the blame on that which caused this explosion to occur */ /** lay the blame on that which caused this explosion to occur */
def adversary: Option[SourceEntry] = instigation match { def adversary: Option[SourceEntry] = instigation match {
case Some(prior) => prior.interaction.cause.adversary case Some(prior) => prior.interaction.cause.adversary
case None => None case None => Some(entity)
} }
/** the entity that exploded is the source of the damage */ override def attribution: Int = entity.Definition.ObjectId
override def attribution: Int = definition.ObjectId }
object ExplodingEntityReason {
/**
* An overloaded constructor for a wrapper for a "damage source" in damage calculations.
* @param entity the source of the explosive yield
* @param damageModel the model to be utilized in these calculations
* @param instigation what previous event happened, if any, that caused this explosion
* @return an `ExplodingEntityReason` entity
*/
def apply(
entity: PlanetSideGameObject with FactionAffinity with Vitality,
damageModel: DamageAndResistance,
instigation: Option[DamageResult]
): ExplodingEntityReason = {
val definition = entity.Definition.asInstanceOf[ObjectDefinition with VitalityDefinition]
assert(definition.explodes && definition.innateDamage.nonEmpty, "causal entity does not explode")
ExplodingEntityReason(SourceEntry(entity), definition.innateDamage.get, damageModel, instigation)
}
} }
object ExplodingDamageModifiers { object ExplodingDamageModifiers {

View file

@ -0,0 +1,46 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.ballistics.SourceEntry
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.damage.DamageCalculations.AgainstNothing
import net.psforever.objects.vital.prop.DamageProperties
import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
/**
* The instigating cause of dying on an operational vehicle spawn pad.
* @param driver the driver whose vehicle was being created
* @param vehicle the vehicle being created
*/
final case class VehicleSpawnReason(driver: SourceEntry, vehicle: SourceEntry)
extends DamageReason {
def resolution: DamageResolution.Value = DamageResolution.Resolved
def same(test: DamageReason): Boolean = test match {
case cause: VehicleSpawnReason =>
driver.Name.equals(cause.driver.Name) &&
(vehicle.Definition eq cause.vehicle.Definition)
case _ =>
false
}
def source: DamageProperties = VehicleSpawnReason.source
def damageModel: DamageAndResistance = VehicleSpawnReason.drm
override def adversary : Option[SourceEntry] = Some(driver)
override def attribution : Int = vehicle.Definition.ObjectId
}
object VehicleSpawnReason {
private val source = new DamageProperties { /*intentional blank*/ }
/** basic damage, no resisting, quick and simple */
private val drm = new DamageResistanceModel {
DamageUsing = AgainstNothing
ResistUsing = NoResistanceSelection
Model = SimpleResolutions.calculate
}
}

View file

@ -4,7 +4,7 @@ package net.psforever.objects.zones
import akka.actor.{ActorContext, ActorRef, Props} import akka.actor.{ActorContext, ActorRef, Props}
import akka.routing.RandomPool import akka.routing.RandomPool
import net.psforever.objects.ballistics.{Projectile, SourceEntry} import net.psforever.objects.ballistics.{Projectile, SourceEntry}
import net.psforever.objects._ import net.psforever.objects.{PlanetSideGameObject, _}
import net.psforever.objects.ce.{ComplexDeployable, Deployable, SimpleDeployable} import net.psforever.objects.ce.{ComplexDeployable, Deployable, SimpleDeployable}
import net.psforever.objects.entity.IdentifiableEntity import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
@ -40,13 +40,14 @@ import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.Avatar
import net.psforever.objects.geometry.Geometry3D import net.psforever.objects.geometry.Geometry3D
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.vehicles.UtilityType import net.psforever.objects.vehicles.UtilityType
import net.psforever.objects.vital.etc.{EmpReason, ExplodingEntityReason} import net.psforever.objects.vital.etc.ExplodingEntityReason
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.prop.DamageWithPosition
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
@ -1092,177 +1093,169 @@ object Zone {
} }
/** /**
* Allocates `Damageable` targets within the radius of a server-prepared explosion * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
* and informs those entities that they have affected by the aforementioned explosion. * and informs those entities that they have affected by the aforementioned damage.
* @see `Amenity.Owner` * Usually, this is considered an "explosion;" but, the application can be utilized for a variety of unbound damage.
* @see `ComplexDeployable` * @param zone the zone in which the damage should occur
* @see `DamageInteraction` * @param source the entity that embodies the damage (information)
* @see `DamageResult` * @param createInteraction how the interaction for this damage is to prepared
* @see `DamageWithPosition` * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage
* @see `ExplodingEntityReason` * @param acquireTargetsFromZone the main target-collecting algorithm
* @see `SimpleDeployable`
* @see `VitalityDefinition`
* @see `VitalityDefinition.innateDamage`
* @see `Zone.Buildings`
* @see `Zone.DeployableList`
* @see `Zone.LivePlayers`
* @see `Zone.LocalEvents`
* @see `Zone.Vehicles`
* @param zone the zone in which the explosion should occur
* @param obj the entity that embodies the explosion (information)
* @param instigation whatever prior action triggered the entity to explode, if anything
* @param detectionTest a custom test to determine if any given target is affected;
* defaults to an internal test for simple radial proximity
* @return a list of affected entities; * @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual * only mostly complete due to the exclusion of objects whose damage resolution is different than usual
*/ */
def causeExplosion( def serverSideDamage(
zone: Zone, zone: Zone,
obj: PlanetSideGameObject with Vitality, source: PlanetSideGameObject with FactionAffinity with Vitality,
instigation: Option[DamageResult], createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck,
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) = findAllTargets
): List[PlanetSideServerObject] = { ): List[PlanetSideServerObject] = {
obj.Definition.innateDamage match { source.Definition.innateDamage match {
case Some(explosion) if obj.Definition.explodes => case Some(damage) =>
//useful in this form serverSideDamage(zone, source, damage, createInteraction, testTargetsFromZone, acquireTargetsFromZone)
val sourcePosition = obj.Position
val sourcePositionXY = sourcePosition.xy
val radius = explosion.DamageRadius * explosion.DamageRadius
//collect all targets that can be damaged
//players
val playerTargets = zone.LivePlayers.filterNot { _.VehicleSeated.nonEmpty }
//vehicles
val vehicleTargets = zone.Vehicles.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
//deployables
val (simpleDeployableTargets, complexDeployableTargets) =
zone.DeployableList
.filterNot { _.Destroyed }
.foldRight((List.empty[SimpleDeployable], List.empty[ComplexDeployable])) { case (f, (simp, comp)) =>
f match {
case o: SimpleDeployable => (simp :+ o, comp)
case o: ComplexDeployable => (simp, comp :+ o)
case _ => (simp, comp)
}
}
//amenities
val soiTargets = obj match {
case o: Amenity =>
//fortunately, even where soi overlap, amenities in different buildings are never that close to each other
o.Owner.Amenities
case _ =>
zone.Buildings.values
.filter { b =>
val soiRadius = b.Definition.SOIRadius * b.Definition.SOIRadius
Vector3.DistanceSquared(sourcePositionXY, b.Position.xy) < soiRadius || soiRadius <= radius
}
.flatMap { _.Amenities }
.filter { _.Definition.Damageable }
}
//restrict to targets according to the detection plan
val allAffectedTargets = (playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets)
.filter { target =>
(target ne obj) && detectionTest(obj, target, radius)
}
//inform remaining targets that they have suffered an explosion
allAffectedTargets
.foreach { target =>
target.Actor ! Vitality.Damage(
DamageInteraction(
SourceEntry(target),
ExplodingEntityReason(obj, target.DamageModel, instigation),
target.Position
).calculate()
)
}
//important note - these are not returned as targets that were affected
simpleDeployableTargets
.filter { target =>
(target ne obj) && detectionTest(obj, target, radius)
}
.foreach { target =>
zone.LocalEvents ! Vitality.DamageOn(
target,
DamageInteraction(
SourceEntry(target),
ExplodingEntityReason(obj, target.DamageModel, instigation),
target.Position
).calculate()
)
}
allAffectedTargets
case None => case None =>
Nil Nil
} }
} }
/** /**
* Allocates `Damageable` targets within the radius of a server-prepared electromagnetic pulse * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
* and informs those entities that they have affected by the aforementioned pulse. * and informs those entities that they have affected by the aforementioned damage.
* Targets within the effect radius within other rooms are affected, unlike with normal damage. * Usually, this is considered an "explosion;" but, the application can be utilized for a variety of unbound damage.
* The only affected target is Boomer deployables.
* @see `Amenity.Owner`
* @see `BoomerDeployable`
* @see `DamageInteraction` * @see `DamageInteraction`
* @see `DamageResult` * @see `DamageResult`
* @see `DamageWithPosition` * @see `DamageWithPosition`
* @see `EmpReason` * @see `Vitality.Damage`
* @see `Zone.DeployableList` * @see `Vitality.DamageOn`
* @param zone the zone in which the emp should occur * @see `VitalityDefinition`
* @param obj the entity that triggered the emp (information) * @see `VitalityDefinition.innateDamage`
* @param sourcePosition where the emp physically originates * @see `Zone.LocalEvents`
* @param effect characteristics of the emp produced * @param zone the zone in which the damage should occur
* @param detectionTest a custom test to determine if any given target is affected; * @param source the entity that embodies the damage (information)
* defaults to an internal test for simple radial proximity * @param createInteraction how the interaction for this damage is to prepared
* @return a list of affected entities * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage
* @param acquireTargetsFromZone the main target-collecting algorithm
* @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
*/ */
def causeSpecialEmp( def serverSideDamage(
zone: Zone, zone: Zone,
obj: PlanetSideServerObject with Vitality, source: PlanetSideGameObject with FactionAffinity with Vitality,
sourcePosition: Vector3, properties: DamageWithPosition,
effect: DamageWithPosition, createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean,
): List[PlanetSideServerObject] = { acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality])
val proxy: ExplosiveDeployable = { ): List[PlanetSideServerObject] = {
//construct a proxy unit to represent the pulse //collect targets that can be damaged
val o = new ExplosiveDeployable(GlobalDefinitions.special_emp) val (pssos, psgos) = acquireTargetsFromZone(zone, source, properties)
o.Owner = Some(obj.GUID) val radius = properties.DamageRadius * properties.DamageRadius
o.OwnerName = obj match { //restrict to targets according to the detection plan
case p: Player => p.Name val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) }
case o: OwnableByPlayer => o.OwnerName.getOrElse("") //inform remaining targets that they have suffered damage
case _ => ""
}
o.Position = sourcePosition
o.Faction = obj.Faction
o
}
val radius = effect.DamageRadius * effect.DamageRadius
//only boomers can be affected (that's why it's special)
val allAffectedTargets = zone.DeployableList
.collect { case o: BoomerDeployable if !o.Destroyed && (o ne obj) && detectionTest(proxy, o, radius) => o }
//inform targets that they have suffered the effects of the emp
allAffectedTargets allAffectedTargets
.foreach { target => .foreach { target => target.Actor ! Vitality.Damage(createInteraction(source, target).calculate()) }
target.Actor ! Vitality.Damage( //important note - these are not returned as targets that were affected
DamageInteraction( psgos
SourceEntry(target), .filter { target => testTargetsFromZone(source, target, radius) }
EmpReason(obj, effect, target), .foreach { target => zone.LocalEvents ! Vitality.DamageOn(target, createInteraction(source, target).calculate()) }
sourcePosition
).calculate()
)
}
allAffectedTargets allAffectedTargets
} }
/**
* na
* @see `Amenity.Owner`
* @see `ComplexDeployable`
* @see `DamageWithPosition`
* @see `SimpleDeployable`
* @see `Zone.Buildings`
* @see `Zone.DeployableList`
* @see `Zone.LivePlayers`
* @see `Zone.Vehicles`
* @param zone the zone in which to search
* @param source a game entity that is treated as the origin and is excluded from results
* @param damagePropertiesBySource information about the effect/damage
* @return two lists of objects with different characteristics;
* the first list is `PlanetSideServerObject` entities with `Vitality`;
* the second list is `PlanetSideGameObject` entities with both `Vitality` and `FactionAffinity`
*/
def findAllTargets(
zone: Zone,
source: PlanetSideGameObject with Vitality,
damagePropertiesBySource: DamageWithPosition
): (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) = {
val sourcePosition = source.Position
val sourcePositionXY = sourcePosition.xy
val radius = damagePropertiesBySource.DamageRadius * damagePropertiesBySource.DamageRadius
//collect all targets that can be damaged
//players
val playerTargets = zone.LivePlayers.filterNot { _.VehicleSeated.nonEmpty }
//vehicles
val vehicleTargets = zone.Vehicles.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
//deployables
val (simpleDeployableTargets, complexDeployableTargets) =
zone.DeployableList
.filterNot { _.Destroyed }
.foldRight((List.empty[SimpleDeployable], List.empty[ComplexDeployable])) { case (f, (simp, comp)) =>
f match {
case o: SimpleDeployable => (simp :+ o, comp)
case o: ComplexDeployable => (simp, comp :+ o)
case _ => (simp, comp)
}
}
//amenities
val soiTargets = source match {
case o: Amenity =>
//fortunately, even where soi overlap, amenities in different buildings are never that close to each other
o.Owner.Amenities
case _ =>
zone.Buildings.values
.filter { b =>
val soiRadius = b.Definition.SOIRadius * b.Definition.SOIRadius
Vector3.DistanceSquared(sourcePositionXY, b.Position.xy) < soiRadius || soiRadius <= radius
}
.flatMap { _.Amenities }
.filter { _.Definition.Damageable }
}
(
(playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets)
.filter { target => target ne source },
simpleDeployableTargets
.filter { target => target ne source }
)
}
/**
* na
* @param instigation what previous event happened, if any, that caused this explosion
* @param source a game object that represents the source of the explosion
* @param target a game object that is affected by the explosion
* @return a `DamageInteraction` object
*/
def explosionDamage(
instigation: Option[DamageResult]
)
(
source: PlanetSideGameObject with FactionAffinity with Vitality,
target: PlanetSideGameObject with FactionAffinity with Vitality
): DamageInteraction = {
DamageInteraction(
SourceEntry(target),
ExplodingEntityReason(source, target.DamageModel, instigation),
target.Position
)
}
/** /**
* Two game entities are considered "near" each other if they are within a certain distance of one another. * Two game entities are considered "near" each other if they are within a certain distance of one another.
* A default function literal mainly used for `causesExplosion`. * A default function literal mainly used for `serverSideDamage`.
* @see `causeExplosion`
* @see `ObjectDefinition.Geometry` * @see `ObjectDefinition.Geometry`
* @param obj1 a game entity, should be the source of the explosion * @see `serverSideDamage`
* @param obj2 a game entity, should be the target of the explosion * @param obj1 a game entity, should be the source of the damage
* @param obj2 a game entity, should be the target of the damage
* @param maxDistance the square of the maximum distance permissible between game entities * @param maxDistance the square of the maximum distance permissible between game entities
* before they are no longer considered "near" * before they are no longer considered "near"
* @return `true`, if the target entities are near enough to each other; * @return `true`, if the two entities are near enough to each other;
* `false`, otherwise * `false`, otherwise
*/ */
def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { def distanceCheck(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = {
@ -1278,7 +1271,7 @@ object Zone {
* @return `true`, if the target entities are near enough to each other; * @return `true`, if the target entities are near enough to each other;
* `false`, otherwise * `false`, otherwise
*/ */
def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = { private def distanceCheck(g1: Geometry3D, g2: Geometry3D, maxDistance: Float): Boolean = {
Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance || Vector3.DistanceSquared(g1.center.asVector3, g2.center.asVector3) <= maxDistance ||
distanceCheck(g1, g2) <= maxDistance distanceCheck(g1, g2) <= maxDistance
} }

View file

@ -7,7 +7,7 @@ object Zoning {
object Method extends Enumeration { object Method extends Enumeration {
type Type = Value type Type = Value
val None, InstantAction, Recall, Quit = Value val None, InstantAction, OutfitRecall, Recall, Quit = Value
} }
object Status extends Enumeration { object Status extends Enumeration {