diff --git a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala index 142eb0df4..f2f00f6dd 100644 --- a/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala +++ b/server/src/test/scala/actor/objects/VehicleSpawnPadTest.scala @@ -13,6 +13,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import akka.actor.typed.scaladsl.adapter._ import net.psforever.actors.zone.ZoneActor import net.psforever.objects.avatar.Avatar +import net.psforever.objects.serverobject.terminals.Terminal import scala.concurrent.duration._ @@ -29,11 +30,11 @@ class VehicleSpawnControl1Test extends ActorTest { class VehicleSpawnControl2Test extends ActorTest { "VehicleSpawnControl" should { "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") - zone.VehicleEvents = probe.ref //zone events - pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order + zone.VehicleEvents = probe.ref //zone events + pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage]) @@ -55,7 +56,7 @@ class VehicleSpawnControl2Test extends ActorTest { class VehicleSpawnControl3Test extends ActorTest { "VehicleSpawnControl" should { "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 val probe = new TestProbe(system, "zone-events") 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.Spawn() - zone.VehicleEvents = probe.ref //zone events - pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //first order - pad.Actor ! VehicleSpawnPad.VehicleOrder(player2, vehicle) //second order (vehicle shared) + zone.VehicleEvents = probe.ref //zone events + pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //first order + pad.Actor ! VehicleSpawnPad.VehicleOrder(player2, vehicle, terminal) //second order (vehicle shared) assert(probe.receiveOne(1 seconds) match { case VehicleSpawnPad.PeriodicReminder(_, VehicleSpawnPad.Reminders.Queue, _) => true @@ -103,12 +104,12 @@ class VehicleSpawnControl3Test extends ActorTest { class VehicleSpawnControl4Test extends ActorTest { "VehicleSpawnControl" should { "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") zone.VehicleEvents = probe.ref 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) // assert( @@ -125,11 +126,11 @@ class VehicleSpawnControl4Test extends ActorTest { class VehicleSpawnControl5Test extends ActorTest() { "VehicleSpawnControl" should { "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") 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[VehicleServiceMessage]) @@ -152,11 +153,11 @@ class VehicleSpawnControl5Test extends ActorTest() { class VehicleSpawnControl6Test extends ActorTest() { "VehicleSpawnControl" should { "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") 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[VehicleServiceMessage]) @@ -179,12 +180,12 @@ class VehicleSpawnControl6Test extends ActorTest() { class VehicleSpawnControl7Test extends ActorTest { "VehicleSpawnControl" should { "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") player.ExoSuit = ExoSuitType.MAX - zone.VehicleEvents = probe.ref //zone events - pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle) //order + zone.VehicleEvents = probe.ref //zone events + pad.Actor ! VehicleSpawnPad.VehicleOrder(player, vehicle, terminal) //order probe.expectMsgClass(1 minute, classOf[VehicleSpawnPad.ConcealPlayer]) probe.expectMsgClass(1 minute, classOf[VehicleServiceMessage]) @@ -210,7 +211,7 @@ object VehicleSpawnPadControlTest { def SetUpAgents( 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.source.MaxNumberSource import net.psforever.objects.serverobject.structures.Building @@ -218,6 +219,7 @@ object VehicleSpawnPadControlTest { import net.psforever.objects.Tool import net.psforever.types.CharacterSex + val terminal = Terminal(GlobalDefinitions.vehicle_terminal_combined) val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) val weapon = vehicle.WeaponControlledFromSeat(1).get.asInstanceOf[Tool] 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 pad.Position = Vector3(1, 0, 0) vehicle.Position = Vector3(1, 0, 0) - (vehicle, player, pad, zone) + (vehicle, player, pad, terminal, zone) } } diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 154d34a39..626f649aa 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -44,6 +44,7 @@ import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.vehicles._ import net.psforever.objects.vital._ 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.projectile.ProjectileReason 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.properties.PropertyOverrideManager import net.psforever.services.support.SupportActor -import net.psforever.services.teamwork.{ - SquadResponse, - SquadServiceMessage, - SquadServiceResponse, - SquadAction => SquadServiceAction -} +import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} import net.psforever.services.hart.HartTimer import net.psforever.services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} 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 case Recall() => + player.ZoningRequest = Zoning.Method.Recall zoningType = Zoning.Method.Recall zoningChatMessageType = ChatMessageType.CMT_RECALL zoningStatus = Zoning.Status.Request @@ -456,6 +453,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con }) case InstantAction() => + player.ZoningRequest = Zoning.Method.InstantAction zoningType = Zoning.Method.InstantAction zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION zoningStatus = Zoning.Status.Request @@ -482,10 +480,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) 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) { CancelZoningProcessWithDescriptiveReason("cancel") } + player.ZoningRequest = Zoning.Method.Quit zoningType = Zoning.Method.Quit zoningChatMessageType = ChatMessageType.CMT_QUIT zoningStatus = Zoning.Status.Request @@ -1726,6 +1725,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con */ def CancelZoningProcess(): Unit = { zoningTimer.cancel() + player.ZoningRequest = Zoning.Method.None zoningType = Zoning.Method.None zoningStatus = Zoning.Status.None zoningCounter = 0 @@ -1859,6 +1859,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con DropSpecialSlotItem() 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 zoningStatus = Zoning.Status.None @@ -1866,7 +1876,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.GUID(mount) match { case Some(obj: Vehicle) => - TotalDriverVehicleControl(obj) + ConditionalDriverVehicleControl(obj) UnaccessContainer(obj) case _ => ; } @@ -2190,6 +2200,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con specialItemSlotGuid match { case Some(guid: PlanetSideGUID) => specialItemSlotGuid = None + player.Carrying = None continent.GUID(guid) match { case Some(llu: CaptureFlag) => llu.Carrier match { @@ -2398,6 +2409,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some(guid) => if (guid == llu.GUID) { specialItemSlotGuid = None + player.Carrying = None } case _ => ; } @@ -2622,7 +2634,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val player_guid: PlanetSideGUID = tplayer.GUID if (player_guid == player.GUID) { //disembarking self - TotalDriverVehicleControl(obj) + ConditionalDriverVehicleControl(obj) UnaccessContainer(obj) DismountAction(tplayer, obj, seat_num) } else { @@ -2675,7 +2687,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) 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, msg.terminal_guid )(item) @@ -2701,13 +2713,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con lastTerminalOrderFulfillment = true case Terminal.BuyVehicle(vehicle, weapons, trunk) => - continent.map.terminalToSpawnPad.get(msg.terminal_guid.guid) match { - case Some(padGuid) => - tplayer.avatar.purchaseCooldown(vehicle.Definition) match { - case Some(_) => - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) - case None => - val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad] + tplayer.avatar.purchaseCooldown(vehicle.Definition) match { + case Some(_) => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) + case None => + continent.map.terminalToSpawnPad + .find { case (termid, _) => termid == msg.terminal_guid.guid } + .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.Position = pad.Position 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 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)) + 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 @@ -2793,7 +2811,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ObjectDetachMessage( pad_guid, vehicle_guid, - pad_position + Vector3(0, 0, pad.VehicleCreationZOffset), + pad_position + Vector3.z(pad.VehicleCreationZOffset), pad_orientation_z + pad.VehicleCreationZOrientOffset ) ) @@ -2958,6 +2976,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID PlayerActionsToCancel() + serverVehicleControlVelocity = Some(0) CancelAllProximityUnits() if (player.VisibleSlots.contains(player.DrawnSlot)) { player.DrawnSlot = Player.HandsDownSlot @@ -2988,16 +3007,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case VehicleResponse.ServerVehicleOverrideEnd(vehicle, pad) => DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2) - case VehicleResponse.PeriodicReminder(cause, data) => - val msg: String = cause match { - case VehicleSpawnPad.Reminders.Blocked => - s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}" - case VehicleSpawnPad.Reminders.Queue => - s"Your position in the vehicle spawn queue is ${data.getOrElse("dead last")}." - case VehicleSpawnPad.Reminders.Cancelled => - "Your vehicle order has been cancelled." + case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) => + sendResponse(ChatMsg( + ChatMessageType.CMT_OPEN, + true, + "", + s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}", + None + )) + + 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) => //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 _ => ; } case Some(obj: FacilityTurret) => @@ -5101,6 +5128,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (specialItemSlotGuid.isEmpty) { if (obj.Faction == player.Faction) { specialItemSlotGuid = Some(obj.GUID) + player.Carrying = SpecialCarry.CaptureFlag continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) } else { log.warn( @@ -5504,11 +5532,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con projectile.profile.JammerProjectile || projectile.profile.SympatheticExplosion ) { - Zone.causeSpecialEmp( + //can also substitute 'projectile.profile' for 'SpecialEmp.emp' + Zone.serverSideDamage( continent, player, - explosion_pos, - GlobalDefinitions.special_emp.innateDamage.get + SpecialEmp.emp, + SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos), + SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction), + SpecialEmp.findAllBoomers ) } if (profile.ExistsOnRemoteClients && projectile.HasGUID) { @@ -6033,12 +6064,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @see `RegisterVehicle` * @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( new Task() { - private val localVehicle = obj - private val localPad = pad.Actor - private val localPlayer = player + private val localVehicle = obj + private val localPad = pad.Actor + private val localTerminal = terminal + private val localPlayer = player 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 = { - localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle) + localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle, localTerminal) resolver ! Success(this) } }, @@ -7027,10 +7059,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con progressBarUpdate.cancel() progressBarValue = None lastTerminalOrderFulfillment = true - serverVehicleControlVelocity = None accessedContainer match { case Some(v: Vehicle) => val vguid = v.GUID + ConditionalDriverVehicleControl(v) if (v.AccessingTrunk.contains(player.GUID)) { if (player.VehicleSeated.contains(vguid)) { 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 */ def ServerVehicleLockReverse(): Unit = { - serverVehicleControlVelocity = Some(0) + serverVehicleControlVelocity = Some(-1) sendResponse( ServerVehicleOverrideMsg( lock_accelerator = true, @@ -7770,7 +7802,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Set the vehicle to strafe right */ def ServerVehicleLockStrafeRight(): Unit = { - serverVehicleControlVelocity = Some(0) + serverVehicleControlVelocity = Some(-1) sendResponse( ServerVehicleOverrideMsg( lock_accelerator = true, @@ -7791,7 +7823,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * Set the vehicle to strafe left */ def ServerVehicleLockStrafeLeft(): Unit = { - serverVehicleControlVelocity = Some(0) + serverVehicleControlVelocity = Some(-1) sendResponse( ServerVehicleOverrideMsg( lock_accelerator = true, @@ -7812,7 +7844,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param vehicle the vehicle being controlled */ def ServerVehicleLock(vehicle: Vehicle): Unit = { - serverVehicleControlVelocity = Some(0) + serverVehicleControlVelocity = Some(-1) 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. * @param vehicle the vehicle */ - def TotalDriverVehicleControl(vehicle: Vehicle): Unit = { - if (serverVehicleControlVelocity.nonEmpty) { - serverVehicleControlVelocity = None - sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None)) + def ConditionalDriverVehicleControl(vehicle: Vehicle): Unit = { + if (serverVehicleControlVelocity.nonEmpty && !serverVehicleControlVelocity.contains(0)) { + TotalDriverVehicleControl(vehicle) } } + 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 * (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) { continent.GUID(player.avatar.vehicle) match { case Some(vehicle: Vehicle) if vehicle.Actor != Default.Actor => + TotalDriverVehicleControl(vehicle) vehicle.Actor ! Vehicle.Ownership(None) case _ => ; } diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index fc3c00a35..381ffd5fc 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -170,7 +170,12 @@ object ExplosiveDeployableControl { val zone = target.Zone zone.Activity ! Zone.HotSpot.Activity(cause) 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) + ) } /** diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 0899ba585..aa1885c49 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -20,12 +20,7 @@ import net.psforever.objects.serverobject.painbox.PainboxDefinition import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition -import net.psforever.objects.serverobject.structures.{ - AmenityDefinition, - AutoRepairStats, - BuildingDefinition, - WarpGateDefinition -} +import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade} @@ -982,8 +977,6 @@ object GlobalDefinitions { val router_telepad_deployable = SimpleDeployableDefinition(DeployedItem.router_telepad_deployable) - val special_emp = ExplosiveDeployableDefinition(DeployedItem.jammer_mine) - //this is only treated like a deployable val internal_router_telepad_deployable = InternalTelepadDefinition() //objectId: 744 init_deployables() @@ -6788,9 +6781,7 @@ object GlobalDefinitions { dropship.MaxShields = 1000 dropship.CanFly = true dropship.Seats += 0 -> new SeatDefinition() - dropship.Seats += 1 -> new SeatDefinition() { - bailable = true - } + dropship.Seats += 1 -> bailableSeat dropship.Seats += 2 -> bailableSeat dropship.Seats += 3 -> bailableSeat dropship.Seats += 4 -> bailableSeat @@ -7039,11 +7030,10 @@ object GlobalDefinitions { * Initialize `Deployable` globals. */ 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 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) _ - boomer.Name = "boomer" boomer.Descriptor = "Boomers" boomer.MaxHealth = 100 @@ -7066,7 +7056,6 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } boomer.Geometry = mine - he_mine.Name = "he_mine" he_mine.Descriptor = "Mines" he_mine.MaxHealth = 100 @@ -7088,7 +7077,6 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } he_mine.Geometry = mine - jammer_mine.Name = "jammer_mine" jammer_mine.Descriptor = "JammerMines" jammer_mine.MaxHealth = 100 @@ -7098,14 +7086,13 @@ object GlobalDefinitions { jammer_mine.DeployTime = Duration.create(1000, "ms") jammer_mine.DetonateOnJamming = false jammer_mine.Geometry = mine - spitfire_turret.Name = "spitfire_turret" spitfire_turret.Descriptor = "Spitfires" spitfire_turret.MaxHealth = 100 spitfire_turret.Damageable = true spitfire_turret.Repairable = true 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.ReserveAmmunition = false spitfire_turret.DeployCategory = DeployableCategory.SmallTurrets @@ -7122,14 +7109,13 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } spitfire_turret.Geometry = smallTurret - spitfire_cloaked.Name = "spitfire_cloaked" spitfire_cloaked.Descriptor = "CloakingSpitfires" spitfire_cloaked.MaxHealth = 100 spitfire_cloaked.Damageable = true spitfire_cloaked.Repairable = true 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.ReserveAmmunition = false spitfire_cloaked.DeployCategory = DeployableCategory.SmallTurrets @@ -7145,14 +7131,13 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } spitfire_cloaked.Geometry = smallTurret - spitfire_aa.Name = "spitfire_aa" spitfire_aa.Descriptor = "FlakSpitfires" spitfire_aa.MaxHealth = 100 spitfire_aa.Damageable = true spitfire_aa.Repairable = true 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.ReserveAmmunition = false spitfire_aa.DeployCategory = DeployableCategory.SmallTurrets @@ -7168,7 +7153,6 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } spitfire_aa.Geometry = smallTurret - motionalarmsensor.Name = "motionalarmsensor" motionalarmsensor.Descriptor = "MotionSensors" motionalarmsensor.MaxHealth = 100 @@ -7177,7 +7161,6 @@ object GlobalDefinitions { motionalarmsensor.RepairIfDestroyed = false motionalarmsensor.DeployTime = Duration.create(1000, "ms") motionalarmsensor.Geometry = sensor - sensor_shield.Name = "sensor_shield" sensor_shield.Descriptor = "SensorShields" sensor_shield.MaxHealth = 100 @@ -7186,7 +7169,6 @@ object GlobalDefinitions { sensor_shield.RepairIfDestroyed = false sensor_shield.DeployTime = Duration.create(5000, "ms") sensor_shield.Geometry = sensor - tank_traps.Name = "tank_traps" tank_traps.Descriptor = "TankTraps" tank_traps.MaxHealth = 5000 @@ -7205,7 +7187,6 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } tank_traps.Geometry = GeometryForm.representByCylinder(radius = 2.89680997f, height = 3.57812f) - val fieldTurretConverter = new FieldTurretConverter portable_manned_turret.Name = "portable_manned_turret" portable_manned_turret.Descriptor = "FieldTurrets" @@ -7213,11 +7194,11 @@ object GlobalDefinitions { portable_manned_turret.Damageable = true portable_manned_turret.Repairable = true portable_manned_turret.RepairIfDestroyed = false - portable_manned_turret.controlledWeapons += 0 -> 1 - portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap() + portable_manned_turret.controlledWeapons += 0 -> 1 + portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun - portable_manned_turret.MountPoints += 1 -> MountInfo(0) - portable_manned_turret.MountPoints += 2 -> MountInfo(0) + portable_manned_turret.MountPoints += 1 -> MountInfo(0) + portable_manned_turret.MountPoints += 2 -> MountInfo(0) portable_manned_turret.ReserveAmmunition = true portable_manned_turret.FactionLocked = true portable_manned_turret.Packet = fieldTurretConverter @@ -7234,18 +7215,17 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret.Geometry = largeTurret - portable_manned_turret_nc.Name = "portable_manned_turret_nc" portable_manned_turret_nc.Descriptor = "FieldTurrets" portable_manned_turret_nc.MaxHealth = 1000 portable_manned_turret_nc.Damageable = true portable_manned_turret_nc.Repairable = true 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.controlledWeapons += 0 -> 1 - portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) - portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0) + portable_manned_turret_nc.controlledWeapons += 0 -> 1 + portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0) portable_manned_turret_nc.ReserveAmmunition = true portable_manned_turret_nc.FactionLocked = true portable_manned_turret_nc.Packet = fieldTurretConverter @@ -7262,18 +7242,17 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret_nc.Geometry = largeTurret - portable_manned_turret_tr.Name = "portable_manned_turret_tr" portable_manned_turret_tr.Descriptor = "FieldTurrets" portable_manned_turret_tr.MaxHealth = 1000 portable_manned_turret_tr.Damageable = true portable_manned_turret_tr.Repairable = true 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.controlledWeapons += 0 -> 1 - portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) - portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0) + portable_manned_turret_tr.controlledWeapons += 0 -> 1 + portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0) portable_manned_turret_tr.ReserveAmmunition = true portable_manned_turret_tr.FactionLocked = true portable_manned_turret_tr.Packet = fieldTurretConverter @@ -7290,18 +7269,17 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret_tr.Geometry = largeTurret - portable_manned_turret_vs.Name = "portable_manned_turret_vs" portable_manned_turret_vs.Descriptor = "FieldTurrets" portable_manned_turret_vs.MaxHealth = 1000 portable_manned_turret_vs.Damageable = true portable_manned_turret_vs.Repairable = true 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.controlledWeapons += 0 -> 1 - portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) - portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) + portable_manned_turret_vs.controlledWeapons += 0 -> 1 + portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) + portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) portable_manned_turret_vs.ReserveAmmunition = true portable_manned_turret_vs.FactionLocked = true portable_manned_turret_vs.Packet = fieldTurretConverter @@ -7318,7 +7296,6 @@ object GlobalDefinitions { Modifiers = ExplodingRadialDegrade } portable_manned_turret_vs.Geometry = largeTurret - deployable_shield_generator.Name = "deployable_shield_generator" deployable_shield_generator.Descriptor = "ShieldGenerators" deployable_shield_generator.MaxHealth = 1700 @@ -7328,7 +7305,6 @@ object GlobalDefinitions { deployable_shield_generator.DeployTime = Duration.create(6000, "ms") deployable_shield_generator.Model = ComplexDeployableResolutions.calculate deployable_shield_generator.Geometry = GeometryForm.representByCylinder(radius = 0.6562f, height = 2.17188f) - router_telepad_deployable.Name = "router_telepad_deployable" router_telepad_deployable.MaxHealth = 100 router_telepad_deployable.Damageable = true @@ -7338,7 +7314,6 @@ object GlobalDefinitions { router_telepad_deployable.Packet = new TelepadDeployableConverter router_telepad_deployable.Model = SimpleResolutions.calculate router_telepad_deployable.Geometry = GeometryForm.representByRaisedSphere(radius = 1.2344f) - internal_router_telepad_deployable.Name = "router_telepad_deployable" internal_router_telepad_deployable.MaxHealth = 1 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.DeployCategory = DeployableCategory.Telepads 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.Damageable = 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.Damageable = 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.Damageable = 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.Damageable = false diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 4f1f8b33b..3f4a71f53 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever 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.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} 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.interaction.DamageInteraction 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 scala.annotation.tailrec @@ -30,6 +30,7 @@ class Player(var avatar: Avatar) with ZoneAware with AuraContainer { private var backpack: Boolean = false + private var released: Boolean = false private var armor: Int = 0 private var capacitor: Float = 0f @@ -44,12 +45,14 @@ class Player(var avatar: Avatar) private var drawnSlot: Int = Player.HandsDownSlot private var lastDrawnSlot: Int = Player.HandsDownSlot private var backpackAccess: Option[PlanetSideGUID] = None + private var carrying: Option[SpecialCarry] = None - private var facingYawUpper: Float = 0f - private var crouching: Boolean = false - private var jumping: Boolean = false - private var cloaked: Boolean = false - private var afk: Boolean = false + private var facingYawUpper: Float = 0f + private var crouching: Boolean = false + private var jumping: Boolean = false + private var cloaked: Boolean = false + private var afk: Boolean = false + private var zoning: Zoning.Method.Value = Zoning.Method.None private var vehicleSeated: Option[PlanetSideGUID] = None @@ -95,6 +98,7 @@ class Player(var avatar: Avatar) Health = Definition.DefaultHealth Armor = MaxArmor Capacitor = 0 + released = false } isAlive } @@ -108,18 +112,18 @@ class Player(var avatar: Avatar) def Revive: Boolean = { Destroyed = false Health = Definition.DefaultHealth + released = false true } def Release: Boolean = { - if (!isAlive) { - backpack = true - true - } else { - false - } + released = true + backpack = !isAlive + true } + def isReleased: Boolean = released + def Armor: Int = armor def Armor_=(assignArmor: Int): Int = { @@ -498,6 +502,23 @@ class Player(var avatar: Avatar) 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 canEqual(other: Any): Boolean = other.isInstanceOf[Player] diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala new file mode 100644 index 000000000..c0b8a1a24 --- /dev/null +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -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 + ) + } +} \ No newline at end of file diff --git a/src/main/scala/net/psforever/objects/TrapDeployable.scala b/src/main/scala/net/psforever/objects/TrapDeployable.scala index 4dc3a591f..6a9ec3880 100644 --- a/src/main/scala/net/psforever/objects/TrapDeployable.scala +++ b/src/main/scala/net/psforever/objects/TrapDeployable.scala @@ -47,6 +47,6 @@ class TrapDeployableControl(trap: TrapDeployable) extends Actor with DamageableE override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = { super.DestructionAwareness(target, cause) Deployables.AnnounceDestroyDeployable(trap, None) - Zone.causeExplosion(target.Zone, target, Some(cause)) + Zone.serverSideDamage(target.Zone, target, Zone.explosionDamage(Some(cause))) } } diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index f7107c22d..2380f9a26 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -711,6 +711,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //uninitialize implants avatarActor ! AvatarActor.DeinitializeImplants() + //log historical event + target.History(cause) + //log message cause.adversarial match { case Some(a) => damageLog.info(s"DisplayDestroy: ${a.defender} was killed by ${a.attacker}") diff --git a/src/main/scala/net/psforever/objects/avatar/SpecialCarry.scala b/src/main/scala/net/psforever/objects/avatar/SpecialCarry.scala new file mode 100644 index 000000000..c4526a004 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/SpecialCarry.scala @@ -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") +} diff --git a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala index 738e45964..7d7c72f5d 100644 --- a/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala +++ b/src/main/scala/net/psforever/objects/geometry/PrimitiveShape.scala @@ -431,3 +431,73 @@ object Cylinder { */ 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)) + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index 455fa0267..ebbeb7918 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -205,7 +205,7 @@ trait DamageableVehicle } }) //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 Vehicles.BeforeUnloadVehicle(obj, zone) //shields diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala index 6ba61f483..1abd1608f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableWeaponTurret.scala @@ -93,7 +93,7 @@ trait DamageableWeaponTurret EndAllAggravation() DamageableWeaponTurret.DestructionAwareness(obj, cause) DamageableMountable.DestructionAwareness(obj, cause) - Zone.causeExplosion(target.Zone, target, Some(cause)) + Zone.serverSideDamage(target.Zone, target, Zone.explosionDamage(Some(cause))) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 60286181d..677474606 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -122,7 +122,7 @@ class GeneratorControl(gen: Generator) queuedExplosion = Default.Cancellable imminentExplosion = false //hate on everything nearby - Zone.causeExplosion(gen.Zone, gen, gen.LastDamage, explosionFunc) + Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc) gen.ClearHistory() 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 * that the target is not "outside" of the detection radius of the room. * Magic numbers for the room dimensions are employed. - * @see `Zone.causeExplosion` * @see `Zone.distanceCheck` + * @see `Zone.serverSideDamage` * @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; * the `u` prefix indicates a "unit vector" diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala index 8d3dfdb9b..4cde293a3 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnControl.scala @@ -1,12 +1,15 @@ // Copyright (c) 2017 PSForever 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.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.pad.process.{VehicleSpawnControlBase, VehicleSpawnControlConcealPlayer} -import net.psforever.objects.zones.Zone -import net.psforever.objects.{Default, Player, Vehicle} +import net.psforever.objects.zones.{Zone, ZoneAware, Zoning} +import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle} +import net.psforever.types.Vector3 import scala.annotation.tailrec import scala.concurrent.ExecutionContext.Implicits.global @@ -37,8 +40,11 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) /** a reminder sent to future customers */ 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 */ - 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; * used as a guard condition to control order processing rate @@ -46,7 +52,7 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) var trackedOrder: Option[VehicleSpawnControl.Order] = None /** 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 = "" @@ -67,39 +73,60 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) } } + override def postStop() : Unit = { + periodicReminder.cancel() + queueManagement.cancel() + } + def receive: Receive = 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") try { - handleOrderFunc(VehicleSpawnControl.Order(player, vehicle)) + handleOrderFunc(msg) } catch { case _: AssertionError => ; //ehhh case e: Exception => //something unexpected 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 => if (sender() == concealPlayer) { trackedOrder = None //guard off 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. - 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. - During this time, a periodic message about the spawn pad being blocked - will be broadcast to all current customers in the order queue. - */ + 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. + 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 will be broadcast to the order queue. + */ case VehicleSpawnControl.ProcessControl.Reminder => trackedOrder match { case Some(entry) => 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( - VehicleSpawnControl.initialReminderDelay, - VehicleSpawnControl.periodicReminderDelay, + VehicleSpawnControl.periodicReminderTestDelay, + VehicleSpawnControl.periodicReminderTestDelay, self, VehicleSpawnControl.ProcessControl.Reminder ) @@ -112,19 +139,16 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) case VehicleSpawnControl.ProcessControl.Flush => periodicReminder.cancel() - orders.foreach { - CancelOrder - } + orders.foreach { CancelOrder(_, Some("@SVCP_RemovedFromVehicleQueue_Generic")) } orders = Nil trackedOrder match { - case Some(entry) => - CancelOrder(entry) + case Some(entry) => CancelOrder(entry, Some("@SVCP_RemovedFromVehicleQueue_Generic")) case None => ; } trackedOrder = None handleOrderFunc = NewTasking 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 _ => ; } @@ -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. * @param order the order being accepted */ - def NewTasking(order: VehicleSpawnControl.Order): Unit = { + def NewTasking(order: VehicleSpawnPad.VehicleOrder): Unit = { handleOrderFunc = QueuedTasking 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. * @param order the order being accepted */ - def QueuedTasking(order: VehicleSpawnControl.Order): Unit = { - val name = order.driver.Name - if ( - (trackedOrder match { - case Some(tracked) => !tracked.driver.Name.equals(name) - case None => true - }) && orders.forall { !_.driver.Name.equals(name) } - ) { - //not a second order from an existing order's player - orders = orders :+ order - pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( - name, - VehicleSpawnPad.Reminders.Queue, - Some(orders.length + 1) - ) - } else { - VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) + def QueuedTasking(order: VehicleSpawnPad.VehicleOrder): Unit = { + val name = order.player.Name + if (trackedOrder match { + case Some(tracked) => + !tracked.driver.Name.equals(name) + case None => + handleOrderFunc = NewTasking + NewTasking(order) + false + }) { + orders.indexWhere { _.player.Name.equals(name) } match { + case -1 if orders.isEmpty => + //first queued order + orders = List(order) + queueManagementTask() + pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( + 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." * @return the next-available order */ - def SelectFirstOrder(): Option[VehicleSpawnControl.Order] = { + def SelectFirstOrder(): Option[VehicleSpawnPad.VehicleOrder] = { trackedOrder match { case None => - val (completeOrder, remainingOrders): (Option[VehicleSpawnControl.Order], List[VehicleSpawnControl.Order]) = - orders match { + val (completeOrder, remainingOrders): (Option[VehicleSpawnPad.VehicleOrder], List[VehicleSpawnPad.VehicleOrder]) = + orderCredentialsCheck(orders) match { case x :: Nil => + queueManagement.cancel() (Some(x), Nil) case x :: b => (Some(x), b) case Nil => handleOrderFunc = NewTasking + queueManagement.cancel() (None, Nil) } orders = remainingOrders @@ -201,35 +255,93 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) * @param order the order being accepted; * `None`, if no order found or submitted */ - def ProcessOrder(order: Option[VehicleSpawnControl.Order]): Unit = { + def ProcessOrder(order: Option[VehicleSpawnPad.VehicleOrder]): Unit = { periodicReminder.cancel() order match { case Some(_order) => - recursiveOrderReminder(orders.iterator) - trace(s"processing next order - a ${_order.vehicle.Definition.Name} for ${_order.driver.Name}") - trackedOrder = order //guard on - context.system.scheduler.scheduleOnce(2000 milliseconds, concealPlayer, _order) + val size = orders.size + 1 + val driver = _order.player + val name = driver.Name + 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 => ; } } + /** + * 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 * @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 */ - def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnControl.Order]): Unit = { + def BlockedReminder(blockedOrder: VehicleSpawnControl.Order, recipients: Seq[VehicleSpawnPad.VehicleOrder]): Unit = { val user = blockedOrder.vehicle .Seats(0).occupant .orElse(pad.Zone.GUID(blockedOrder.vehicle.Owner)) .orElse(pad.Zone.GUID(blockedOrder.DriverGUID)) - val relevantRecipients = user match { + val relevantRecipients: Iterator[VehicleSpawnPad.VehicleOrder] = user match { case Some(p: Player) if !p.HasGUID => recipients.iterator - case Some(p: Player) if blockedOrder.driver == p => - (blockedOrder +: recipients).iterator - case Some(p: Player) => - (VehicleSpawnControl.Order(p, blockedOrder.vehicle) +: recipients).iterator //who took possession of the vehicle + case Some(_: Player) => + (VehicleSpawnPad.VehicleOrder( + blockedOrder.driver, + blockedOrder.vehicle, + null //permissible + ) +: recipients).iterator //one who took possession of the vehicle case _ => recipients.iterator } @@ -245,24 +357,37 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) /** * Cancel this vehicle order and inform the person who made it, if possible. * @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 = { - val vehicle = entry.vehicle + def CancelOrder(entry: VehicleSpawnControl.Order, msg: Option[String]): Unit = { + 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) { - VehicleSpawnControl.DisposeSpawnedVehicle(entry, pad.Zone) - pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(entry.driver.Name, VehicleSpawnPad.Reminders.Cancelled) + VehicleSpawnControl.DisposeSpawnedVehicle(vehicle, player, pad.Zone) + pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder(player.Name, VehicleSpawnPad.Reminders.Cancelled, msg) } } @tailrec private final def recursiveBlockedReminder( - iter: Iterator[VehicleSpawnControl.Order], + iter: Iterator[VehicleSpawnPad.VehicleOrder], cause: Option[Any] ): Unit = { if (iter.hasNext) { val recipient = iter.next() pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( - recipient.driver.Name, + recipient.player.Name, VehicleSpawnPad.Reminders.Blocked, cause ) @@ -271,30 +396,36 @@ class VehicleSpawnControl(pad: VehicleSpawnPad) } @tailrec private final def recursiveOrderReminder( - iter: Iterator[VehicleSpawnControl.Order], + iter: Iterator[VehicleSpawnPad.VehicleOrder], + size: Int, position: Int = 2 ): Unit = { if (iter.hasNext) { val recipient = iter.next() pad.Zone.VehicleEvents ! VehicleSpawnPad.PeriodicReminder( - recipient.driver.Name, + recipient.player.Name, VehicleSpawnPad.Reminders.Queue, - Some(position) + Some(s"@SVCP_PositionInQueue^$position~^$size~") ) - recursiveOrderReminder(iter, position + 1) + recursiveOrderReminder(iter, size, position + 1) } } } object VehicleSpawnControl { - private final val initialReminderDelay: FiniteDuration = 10000 milliseconds - private final val periodicReminderDelay: FiniteDuration = 10000 milliseconds + private final val periodicReminderTestDelay: FiniteDuration = 10 seconds /** - * An `Enumeration` of non-data control messages for the vehicle spawn process. + * Control messages for the vehicle spawn process. */ - object ProcessControl extends Enumeration { - val Reminder, GetNewOrder, Flush = Value + object ProcessControl { + 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(vehicle.HasGUID, s"when ordering a vehicle, the ${vehicle.Definition.Name} does not have a 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. * 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) */ - def DisposeSpawnedVehicle(entry: VehicleSpawnControl.Order, zone: Zone): Unit = { - DisposeVehicle(entry.vehicle, zone) - zone.VehicleEvents ! VehicleSpawnPad.RevealPlayer(entry.DriverGUID) + def DisposeSpawnedVehicle(vehicle: Vehicle, player: Player, zone: Zone): Unit = { + DisposeVehicle(vehicle, zone) + 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) */ def DisposeVehicle(vehicle: Vehicle, zone: Zone): Unit = { - if (zone.Vehicles.exists(_.GUID == vehicle.GUID)) { //already added to zone - vehicle.Actor ! Vehicle.Deconstruct() + if (zone.Vehicles.contains(vehicle)) { //already added to zone + vehicle.Actor ! Vehicle.Deconstruct(Some(0.seconds)) } else { //just registered to zone zone.tasks ! UnregisterVehicle(vehicle)(zone.GUID) } diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala index 544cf117c..c125da0c1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPad.scala @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.pad import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.types.PlanetSideGUID /** @@ -28,7 +29,7 @@ object VehicleSpawnPad { * @param player the player who submitted the order (the "owner") * @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. @@ -130,9 +131,11 @@ object VehicleSpawnPad { * An `Enumeration` of reasons for sending a periodic reminder to the user. */ object Reminders extends Enumeration { - val Queue, //optional data is the numeric position in the queue - Blocked, //optional data is a message regarding the blockage - Cancelled = Value + val + Queue, //optional data is the numeric position in the queue + Blocked, //optional data is a message regarding the blockage + Cancelled //optional data is the message + = Value } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala index 670e89e27..447af1e08 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/VehicleSpawnPadDefinition.scala @@ -1,7 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.pad +import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.serverobject.structures.AmenityDefinition +import net.psforever.types.Vector3 /** * The definition for any `VehicleSpawnPad`. @@ -39,4 +41,166 @@ class VehicleSpawnPadDefinition(objectId: Int) extends AmenityDefinition(objectI case 947 => Name = "vanu_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 + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala index 2667d785b..b035a7849 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlConcealPlayer.scala @@ -25,19 +25,19 @@ class VehicleSpawnControlConcealPlayer(pad: VehicleSpawnPad) extends VehicleSpaw context.actorOf(Props(classOf[VehicleSpawnControlLoadVehicle], pad), s"${context.parent.path.name}-load") def receive: Receive = { - case order @ VehicleSpawnControl.Order(driver, _) => - //TODO how far can the driver stray from the Terminal before his order is cancelled? - if (driver.Continent == pad.Continent && driver.VehicleSeated.isEmpty && driver.isAlive) { + case order @ VehicleSpawnControl.Order(driver, vehicle) => + if (VehicleSpawnControl.validateOrderCredentials(pad, driver, vehicle).isEmpty) { trace(s"hiding ${driver.Name}") pad.Zone.VehicleEvents ! VehicleSpawnPad.ConcealPlayer(driver.GUID) context.system.scheduler.scheduleOnce(2000 milliseconds, loadVehicle, order) } else { trace(s"integral component lost; abort order fulfillment") - VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) - context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled } - case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => + case msg @ (VehicleSpawnControl.ProcessControl.Reminder | + VehicleSpawnControl.ProcessControl.GetNewOrder | + VehicleSpawnControl.ProcessControl.OrderCancelled) => context.parent ! msg case _ => ; diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala index 047e9c928..dbef9a3c8 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlDriverControl.scala @@ -22,13 +22,10 @@ class VehicleSpawnControlDriverControl(pad: VehicleSpawnPad) extends VehicleSpaw def receive: Receive = { case order @ VehicleSpawnControl.Order(driver, vehicle) => - if (vehicle.Health > 0 && vehicle.PassengerInSeat(driver).contains(0)) { - trace(s"returning control of ${vehicle.Definition.Name} to ${driver.Name}") + trace(s"returning control of ${vehicle.Definition.Name} to its current driver") + if (vehicle.PassengerInSeat(driver).nonEmpty) { 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 case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala index 0010cf22c..516b904b2 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlFinalClearance.scala @@ -1,6 +1,8 @@ // Copyright (c) 2017 PSForever 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.types.{PlanetSideGUID, Vector3} 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. * Each object performs on (or more than one related) actions upon the vehicle order that was submitted.
*
- * There is nothing left to do - * except make certain that the vehicle has moved far enough away from the spawn pad + * There is nothing left to do 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. * 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 @@ -22,10 +23,16 @@ import scala.concurrent.duration._ class VehicleSpawnControlFinalClearance(pad: VehicleSpawnPad) extends VehicleSpawnControlBase(pad) { def LogId = "-clearer" + var temp: Cancellable = Default.Cancellable + + override def postStop() : Unit = { + temp.cancel() + } + def receive: Receive = { - case order @ VehicleSpawnControl.Order(driver, vehicle) => - if (vehicle.PassengerInSeat(driver).isEmpty) { - //ensure the vacant vehicle is above the trench and doors + case order @ VehicleSpawnControl.Order(_, vehicle) => + if (!vehicle.Seats(0).isOccupied) { + //ensure the vacant vehicle is above the trench and the doors vehicle.Position = pad.Position + Vector3.z(pad.Definition.VehicleCreationZOffset) val definition = vehicle.Definition pad.Zone.VehicleEvents ! VehicleServiceMessage( @@ -43,12 +50,20 @@ class VehicleSpawnControlFinalClearance(pad: VehicleSpawnPad) extends VehicleSpa self ! VehicleSpawnControlFinalClearance.Test(order) 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") pad.Zone.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad) 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 { - context.system.scheduler.scheduleOnce(2000 milliseconds, self, test) + temp = context.system.scheduler.scheduleOnce(2000 milliseconds, self, test) } case _ => ; diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala index 1ad844721..d9bdd3d94 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala @@ -1,8 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.pad.process -import akka.actor.{Cancellable, Props} -import net.psforever.objects.{Default, GlobalDefinitions} +import akka.actor.Props +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.zones.Zone import net.psforever.services.Service @@ -11,6 +13,7 @@ import net.psforever.types.Vector3 import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ +import scala.util.Success /** * 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") - var temp: Cancellable = Default.Cancellable + var temp: Option[VehicleSpawnControl.Order] = None - override def postStop() : Unit = { - temp.cancel() - super.postStop() - } + implicit val timeout = Timeout(3.seconds) def receive: Receive = { 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}") vehicle.Position = vehicle.Position - Vector3.z( if (GlobalDefinitions.isFlightVehicle(vehicle.Definition)) 9 else 5 ) //appear below the trench and doors vehicle.Cloaked = vehicle.Definition.CanCloak && driver.Cloaked - pad.Zone.Transport.tell(Zone.Vehicle.Spawn(vehicle), self) - temp = context.system.scheduler.scheduleOnce( - delay = 100 milliseconds, - self, - VehicleSpawnControlLoadVehicle.WaitOnSpawn(order) - ) + + temp = Some(order) + val result = ask(pad.Zone.Transport, Zone.Vehicle.Spawn(vehicle)) + //if too long, or something goes wrong + 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 { trace("owner lost or vehicle in poor condition; abort order fulfillment") - VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone) - context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder + context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled } - 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) => context.parent ! msg case _ => ; } } - -object VehicleSpawnControlLoadVehicle { - private case class WaitOnSpawn(order: VehicleSpawnControl.Order) -} diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala index d589bf0df..0ea6dfb00 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala @@ -2,7 +2,15 @@ package net.psforever.objects.serverobject.pad.process 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.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.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.
*
* 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. - * 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. + * On cue, the trapdoor of the platform will open, and the vehicle will be raised on a railed platform. * @param pad the `VehicleSpawnPad` object being governed */ 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") def receive: Receive = { - case order @ VehicleSpawnControl.Order(_, vehicle) => + case order @ VehicleSpawnControl.Order(driver, vehicle) => 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) context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order) @@ -37,3 +48,46 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont 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 + ) + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index 4235cf554..bf14a8cb3 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -44,7 +44,7 @@ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnCo val vehicle = entry.vehicle //avoid unattended vehicle blocking the pad; user should mount (and does so normally) to reset decon timer 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") pad.Zone.VehicleEvents ! VehicleSpawnPad.StartPlayerSeatedInVehicle(driver.Name, vehicle, pad) } else { @@ -53,7 +53,10 @@ class VehicleSpawnControlSeatDriver(pad: VehicleSpawnPad) extends VehicleSpawnCo context.system.scheduler.scheduleOnce(2500 milliseconds, self, 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") pad.Zone.VehicleEvents ! VehicleSpawnPad.PlayerSeatedInVehicle(entry.driver.Name, entry.vehicle, pad) } else { diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala index 6389c5c58..bbec75af4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala @@ -30,6 +30,7 @@ class VehicleSpawnControlServerVehicleOverride(pad: VehicleSpawnPad) extends Veh val vehicleFailState = vehicle.Health == 0 || vehicle.Position == Vector3.Zero val driverFailState = !driver.isAlive || driver.Continent != pad.Continent || !vehicle.PassengerInSeat(driver).contains(0) + vehicle.MountedIn = None pad.Zone.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad) if (vehicleFailState || driverFailState) { if (vehicleFailState) { diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 62049f764..368fa0622 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -698,7 +698,10 @@ class VehicleControl(vehicle: Vehicle) percentage, body, 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) } ) } diff --git a/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala index 3693b9e16..e3eb107d4 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala @@ -3,7 +3,6 @@ package net.psforever.objects.vital.etc import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.ballistics.SourceEntry -import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.vital.Vitality 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 * that parameterizes information necessary to explain a server-driven electromagnetic pulse occurring. - * @see `VitalityDefinition.explodes` + * @see `SpecialEmp.createEmpInteraction` * @see `VitalityDefinition.innateDamage` - * @see `Zone.causesSpecialEmp` * @param entity the source of the explosive yield * @param damageModel the model to be utilized in these calculations; * typically, but not always, defined by the target @@ -41,7 +39,7 @@ object EmpReason { def apply( owner: PlanetSideGameObject with FactionAffinity, source: DamageWithPosition, - target: PlanetSideServerObject with Vitality + target: PlanetSideGameObject with Vitality ): EmpReason = { EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId) } diff --git a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala index ed1096ab9..1f310c279 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ExplodingEntityReason.scala @@ -4,10 +4,11 @@ package net.psforever.objects.vital.etc import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.ballistics.SourceEntry 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.base.{DamageModifiers, DamageReason, DamageResolution} 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.zones.Zone @@ -18,21 +19,18 @@ import net.psforever.objects.zones.Zone * @see `VitalityDefinition.explodes` * @see `VitalityDefinition.innateDamage` * @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; * typically, but not always, defined by the target * @param instigation what previous event happened, if any, that caused this explosion */ final case class ExplodingEntityReason( - entity: PlanetSideGameObject with Vitality, + entity: SourceEntry, + source: DamageProperties, damageModel: DamageAndResistance, instigation: Option[DamageResult] ) 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 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 */ def adversary: Option[SourceEntry] = instigation match { 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 = definition.ObjectId + override def attribution: Int = entity.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 { diff --git a/src/main/scala/net/psforever/objects/vital/etc/VehicleSpawnReason.scala b/src/main/scala/net/psforever/objects/vital/etc/VehicleSpawnReason.scala new file mode 100644 index 000000000..62f06bc11 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/VehicleSpawnReason.scala @@ -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 + } +} diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 1a3843cf9..3feab1236 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -4,7 +4,7 @@ package net.psforever.objects.zones import akka.actor.{ActorContext, ActorRef, Props} import akka.routing.RandomPool 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.entity.IdentifiableEntity 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.geometry.Geometry3D 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.locks.IFFLock import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.tube.SpawnTube 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.prop.DamageWithPosition import net.psforever.objects.vital.Vitality @@ -1092,177 +1093,169 @@ object Zone { } /** - * Allocates `Damageable` targets within the radius of a server-prepared explosion - * and informs those entities that they have affected by the aforementioned explosion. - * @see `Amenity.Owner` - * @see `ComplexDeployable` - * @see `DamageInteraction` - * @see `DamageResult` - * @see `DamageWithPosition` - * @see `ExplodingEntityReason` - * @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 + * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing + * and informs those entities that they have affected by the aforementioned damage. + * Usually, this is considered an "explosion;" but, the application can be utilized for a variety of unbound damage. + * @param zone the zone in which the damage should occur + * @param source the entity that embodies the damage (information) + * @param createInteraction how the interaction for this damage is to prepared + * @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 causeExplosion( - zone: Zone, - obj: PlanetSideGameObject with Vitality, - instigation: Option[DamageResult], - detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck + def serverSideDamage( + zone: Zone, + source: PlanetSideGameObject with FactionAffinity with Vitality, + createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction, + 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] = { - obj.Definition.innateDamage match { - case Some(explosion) if obj.Definition.explodes => - //useful in this form - 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 + source.Definition.innateDamage match { + case Some(damage) => + serverSideDamage(zone, source, damage, createInteraction, testTargetsFromZone, acquireTargetsFromZone) case None => Nil } } /** - * Allocates `Damageable` targets within the radius of a server-prepared electromagnetic pulse - * and informs those entities that they have affected by the aforementioned pulse. - * Targets within the effect radius within other rooms are affected, unlike with normal damage. - * The only affected target is Boomer deployables. - * @see `Amenity.Owner` - * @see `BoomerDeployable` + * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing + * and informs those entities that they have affected by the aforementioned damage. + * Usually, this is considered an "explosion;" but, the application can be utilized for a variety of unbound damage. * @see `DamageInteraction` * @see `DamageResult` * @see `DamageWithPosition` - * @see `EmpReason` - * @see `Zone.DeployableList` - * @param zone the zone in which the emp should occur - * @param obj the entity that triggered the emp (information) - * @param sourcePosition where the emp physically originates - * @param effect characteristics of the emp produced - * @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 + * @see `Vitality.Damage` + * @see `Vitality.DamageOn` + * @see `VitalityDefinition` + * @see `VitalityDefinition.innateDamage` + * @see `Zone.LocalEvents` + * @param zone the zone in which the damage should occur + * @param source the entity that embodies the damage (information) + * @param createInteraction how the interaction for this damage is to prepared + * @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( - zone: Zone, - obj: PlanetSideServerObject with Vitality, - sourcePosition: Vector3, - effect: DamageWithPosition, - detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck - ): List[PlanetSideServerObject] = { - val proxy: ExplosiveDeployable = { - //construct a proxy unit to represent the pulse - val o = new ExplosiveDeployable(GlobalDefinitions.special_emp) - o.Owner = Some(obj.GUID) - o.OwnerName = obj match { - case p: Player => p.Name - case o: OwnableByPlayer => o.OwnerName.getOrElse("") - 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 + def serverSideDamage( + zone: Zone, + source: PlanetSideGameObject with FactionAffinity with Vitality, + properties: DamageWithPosition, + createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction, + testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean, + acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => (List[PlanetSideServerObject with Vitality], List[PlanetSideGameObject with FactionAffinity with Vitality]) + ): List[PlanetSideServerObject] = { + //collect targets that can be damaged + val (pssos, psgos) = acquireTargetsFromZone(zone, source, properties) + val radius = properties.DamageRadius * properties.DamageRadius + //restrict to targets according to the detection plan + val allAffectedTargets = pssos.filter { target => testTargetsFromZone(source, target, radius) } + //inform remaining targets that they have suffered damage allAffectedTargets - .foreach { target => - target.Actor ! Vitality.Damage( - DamageInteraction( - SourceEntry(target), - EmpReason(obj, effect, target), - sourcePosition - ).calculate() - ) - } + .foreach { target => target.Actor ! Vitality.Damage(createInteraction(source, target).calculate()) } + //important note - these are not returned as targets that were affected + psgos + .filter { target => testTargetsFromZone(source, target, radius) } + .foreach { target => zone.LocalEvents ! Vitality.DamageOn(target, createInteraction(source, target).calculate()) } 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. - * A default function literal mainly used for `causesExplosion`. - * @see `causeExplosion` + * A default function literal mainly used for `serverSideDamage`. * @see `ObjectDefinition.Geometry` - * @param obj1 a game entity, should be the source of the explosion - * @param obj2 a game entity, should be the target of the explosion + * @see `serverSideDamage` + * @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 target entities are near enough to each other; + * @return `true`, if the two entities are near enough to each other; * `false`, otherwise */ 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; * `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 || distanceCheck(g1, g2) <= maxDistance } diff --git a/src/main/scala/net/psforever/objects/zones/Zoning.scala b/src/main/scala/net/psforever/objects/zones/Zoning.scala index 26b064476..16848a0b9 100644 --- a/src/main/scala/net/psforever/objects/zones/Zoning.scala +++ b/src/main/scala/net/psforever/objects/zones/Zoning.scala @@ -7,7 +7,7 @@ object Zoning { object Method extends Enumeration { type Type = Value - val None, InstantAction, Recall, Quit = Value + val None, InstantAction, OutfitRecall, Recall, Quit = Value } object Status extends Enumeration {