Vehicle Spawn Pad QoL (#802)

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

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

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

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

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

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

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

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

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

View file

@ -13,6 +13,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import akka.actor.typed.scaladsl.adapter._
import 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
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))
@ -64,8 +65,8 @@ class VehicleSpawnControl3Test extends ActorTest {
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)
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
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)
}
}

View file

@ -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 {
@ -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]
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 None =>
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))
}
}
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,11 +6064,12 @@ 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 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,12 +7879,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* Stop all movement entirely.
* @param vehicle the vehicle
*/
def ConditionalDriverVehicleControl(vehicle: Vehicle): Unit = {
if (serverVehicleControlVelocity.nonEmpty && !serverVehicleControlVelocity.contains(0)) {
TotalDriverVehicleControl(vehicle)
}
}
def TotalDriverVehicleControl(vehicle: Vehicle): Unit = {
if (serverVehicleControlVelocity.nonEmpty) {
serverVehicleControlVelocity = None
sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None))
}
}
/**
* Given a globally unique identifier in the 40100 to 40124 range
@ -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 _ => ;
}

View file

@ -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)
)
}
/**

View file

@ -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
@ -7043,7 +7034,6 @@ object GlobalDefinitions {
val smallTurret = GeometryForm.representByCylinder(radius = 0.48435f, height = 1.23438f) _
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,7 +7086,6 @@ 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
@ -7122,7 +7109,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
spitfire_turret.Geometry = smallTurret
spitfire_cloaked.Name = "spitfire_cloaked"
spitfire_cloaked.Descriptor = "CloakingSpitfires"
spitfire_cloaked.MaxHealth = 100
@ -7145,7 +7131,6 @@ object GlobalDefinitions {
Modifiers = ExplodingRadialDegrade
}
spitfire_cloaked.Geometry = smallTurret
spitfire_aa.Name = "spitfire_aa"
spitfire_aa.Descriptor = "FlakSpitfires"
spitfire_aa.MaxHealth = 100
@ -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"
@ -7234,7 +7215,6 @@ 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
@ -7262,7 +7242,6 @@ 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
@ -7290,7 +7269,6 @@ 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
@ -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

View file

@ -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 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
released = true
backpack = !isAlive
true
} else {
false
}
}
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]

View file

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

View file

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

View file

@ -711,6 +711,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
//uninitialize implants
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}")

View file

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

View file

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

View file

@ -205,7 +205,7 @@ trait DamageableVehicle
}
})
//things positioned around us can get hurt from us
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

View file

@ -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)))
}
}

View file

@ -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"

View file

@ -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.
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,13 +139,10 @@ 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
@ -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
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(orders.length + 1)
Some(s"@SVCP_PositionInQueue^2~^2~")
)
} else {
VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone)
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)
}

View file

@ -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
val
Queue, //optional data is the numeric position in the queue
Blocked, //optional data is a message regarding the blockage
Cancelled = Value
Cancelled //optional data is the message
= Value
}
/**

View file

@ -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
}
}

View file

@ -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 _ => ;

View file

@ -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) =>

View file

@ -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.<br>
* <br>
* 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 _ => ;

View file

@ -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,55 +31,60 @@ 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)
)
} else {
trace("owner lost or vehicle in poor condition; abort order fulfillment")
VehicleSpawnControl.DisposeVehicle(order.vehicle, pad.Zone)
context.parent ! VehicleSpawnControl.ProcessControl.GetNewOrder
}
case Zone.Vehicle.HasSpawned(zone, vehicle) =>
val definition = vehicle.Definition
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 = vehicle.GUID
val vdata = definition.Packet.ConstructorData(vehicle).get
val vguid = v.GUID
val vdata = definition.Packet.ConstructorData(v).get
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.LoadVehicle(Service.defaultPlayerGUID, vehicle, vtype, vguid, vdata)
VehicleAction.LoadVehicle(Service.defaultPlayerGUID, v, vtype, vguid, vdata)
)
railJack ! temp.get
temp = None
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 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")
context.parent ! VehicleSpawnControl.ProcessControl.OrderCancelled
}
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
@ -84,7 +92,3 @@ class VehicleSpawnControlLoadVehicle(pad: VehicleSpawnPad) extends VehicleSpawnC
case _ => ;
}
}
object VehicleSpawnControlLoadVehicle {
private case class WaitOnSpawn(order: VehicleSpawnControl.Order)
}

View file

@ -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.<br>
* <br>
* 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
)
}
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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) }
)
}

View file

@ -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)
}

View file

@ -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 {

View file

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

View file

@ -4,7 +4,7 @@ package net.psforever.objects.zones
import akka.actor.{ActorContext, ActorRef, Props}
import akka.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,42 +1093,100 @@ 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(
def serverSideDamage(
zone: Zone,
obj: PlanetSideGameObject with Vitality,
instigation: Option[DamageResult],
detectionTest: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck
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
source.Definition.innateDamage match {
case Some(damage) =>
serverSideDamage(zone, source, damage, createInteraction, testTargetsFromZone, acquireTargetsFromZone)
case None =>
Nil
}
}
/**
* 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 `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 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(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 = explosion.DamageRadius * explosion.DamageRadius
val radius = damagePropertiesBySource.DamageRadius * damagePropertiesBySource.DamageRadius
//collect all targets that can be damaged
//players
val playerTargets = zone.LivePlayers.filterNot { _.VehicleSeated.nonEmpty }
@ -1145,7 +1204,7 @@ object Zone {
}
}
//amenities
val soiTargets = obj match {
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
@ -1158,111 +1217,45 @@ object Zone {
.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
(
(playerTargets ++ vehicleTargets ++ complexDeployableTargets ++ soiTargets)
.filter { target => target ne source },
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()
.filter { target => target ne source }
)
}
allAffectedTargets
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`
* @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
* 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 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
allAffectedTargets
.foreach { target =>
target.Actor ! Vitality.Damage(
def explosionDamage(
instigation: Option[DamageResult]
)
(
source: PlanetSideGameObject with FactionAffinity with Vitality,
target: PlanetSideGameObject with FactionAffinity with Vitality
): DamageInteraction = {
DamageInteraction(
SourceEntry(target),
EmpReason(obj, effect, target),
sourcePosition
).calculate()
ExplodingEntityReason(source, target.DamageModel, instigation),
target.Position
)
}
allAffectedTargets
}
/**
* 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
}

View file

@ -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 {