diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala index 86e42e942..ae8399757 100644 --- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -9,7 +9,6 @@ import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, V import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions -import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech @@ -105,7 +104,8 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) - if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 => + if seatNumber == 0 && + obj.Definition.MaxCapacitor > 0 => log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID @@ -134,13 +134,9 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) - if obj.Definition.MaxCapacitor > 0 => - log.info(s"${player.Name} mounts ${ - obj.SeatPermissionGroup(seatNumber) match { - case Some(seatType) => s"a $seatType seat (#$seatNumber)" - case None => "a seat" - } - } of the ${obj.Definition.Name}") + if obj.Definition.MaxCapacitor > 0 && + obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) => + log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() @@ -149,17 +145,26 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor)) sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc tplayer.Actor ! ResetAllEnvironmentInteractions ops.MountingAction(tplayer, obj, seatNumber) - case Mountable.CanMount(obj: Vehicle, seatNumber, _) => - log.info(s"${player.Name} mounts the ${ - obj.SeatPermissionGroup(seatNumber) match { - case Some(seatType) => s"a $seatType seat (#$seatNumber)" - case None => "a seat" - } - } of the ${obj.Definition.Name}") + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if obj.Definition.MaxCapacitor > 0 => + log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor)) + sessionLogic.general.accessContainer(obj) + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) => + log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() @@ -167,10 +172,21 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc tplayer.Actor ! ResetAllEnvironmentInteractions ops.MountingAction(tplayer, obj, seatNumber) + case Mountable.CanMount(obj: Vehicle, seatNumber, _) => + log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sessionLogic.general.accessContainer(obj) + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc + case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) if obj.Definition == GlobalDefinitions.vanu_sentry_turret => log.info(s"${player.Name} mounts the ${obj.Definition.Name}") diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala index 77d3a99fa..9d7d436fd 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -5,11 +5,12 @@ import akka.actor.{ActorContext, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.{Vehicle, Vehicles} +import net.psforever.objects.{PlanetSideGameObject, Vehicle, Vehicles} import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.interaction.InteractsWithZone import net.psforever.packet.game.{ChatMsg, ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{ChatMessageType, DriveState, Vector3} @@ -94,7 +95,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ) ) sessionLogic.squad.updateSquad() - obj.zoneInteractions() + VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(obj, player) case (None, _) => //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle @@ -168,7 +169,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } - obj.zoneInteractions() + VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(obj, player) } else { obj.Velocity = None obj.Flying = None @@ -216,9 +217,16 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) case _ => (None, None) }) match { - case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => + case (None, _) | (_, None) => //error - we do not recognize being mounted or controlling anything, but what can we do about it? () - case _ => + case (Some(_: Vehicle), Some(0)) => //no (see: VSM or FVSM for valid cases) + () + case (Some(entity: PlanetSideGameObject with InteractsWithZone), Some(_)) => //yes + sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(entity, player) + case _ => //yes sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala index 44769b870..3e8405940 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -467,6 +467,11 @@ class SessionData( persist() if (player.HasGUID) { turnCounterFunc(player.GUID) + continent + .GUID(player.VehicleSeated) + .foreach { + VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(_, player) + } } else { turnCounterFunc(PlanetSideGUID(0)) } diff --git a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala index a9f4dc2e8..a1fdd87ae 100644 --- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala @@ -7,6 +7,7 @@ import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.zones.Zone import net.psforever.objects._ +import net.psforever.objects.zones.interaction.InteractsWithZone import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _} import net.psforever.types.DriveState @@ -195,3 +196,56 @@ class VehicleOperations( sendResponse(pkt) } } + +object VehicleOperations { + def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject, passenger: Player): Unit = { + obj match { + case obj: Vehicle => + updateVehicleZoneInteractionFromEarliestSeat(obj, passenger) + case obj: Mountable with InteractsWithZone => + updateEntityZoneInteractionFromEarliestSeat(obj, passenger, obj) + case _ => () + } + } + + private def updateVehicleZoneInteractionFromEarliestSeat(obj: Vehicle, passenger: Player): Unit = { + //vehicle being ferried; check if the ferry has occupants that might have speaking rights before us + var targetVehicle = obj + val carrierSeatVacancy: Boolean = obj match { + case v if v.MountedIn.nonEmpty => + obj.Zone.GUID(v.MountedIn) match { + case Some(carrier: Vehicle) => + targetVehicle = carrier + !carrier.Seats.values.exists(_.isOccupied) + case _ => + true + } + case _ => true + } + if (carrierSeatVacancy) { + updateEntityZoneInteractionFromEarliestSeat(obj, passenger, targetVehicle) + } + } + + private def updateEntityZoneInteractionFromEarliestSeat( + obj: Mountable with InteractsWithZone, + passenger: Player, + updateTarget: InteractsWithZone + ): Unit = { + val inSeatNumberOpt = obj.PassengerInSeat(passenger) + if (inSeatNumberOpt.contains(0)) { + //we're responsible as the primary operator + updateTarget.zoneInteractions() + } else if (!obj.Seat(seatNumber = 0).exists(_.isOccupied)) { + //there is no primary operator; are we responsible? + //determine if we are the player in the seat closest to the "front" + val noPlayersInEarlierSeats = inSeatNumberOpt + .exists { seatIndex => + !(1 until seatIndex).exists { i => obj.Seat(i).exists(_.isOccupied) } + } + if (noPlayersInEarlierSeats) { + updateTarget.zoneInteractions() + } + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index d67de9261..60d27e123 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -1930,7 +1930,7 @@ class ZoningOperations( /** Upstream message counter
* Checks for server acknowledgement of the following messages in the following conditions:
* `PlayerStateMessageUpstream` (infantry)
- * `VehicleStateMessage` (driver mount only)
+ * `VehicleStateMessage` and `FrameVehicleStateMessage` (driver mount)
* `ChildObjectStateMessage` (any gunner mount that is not the driver)
* `KeepAliveMessage` (any passenger mount that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 5d385f072..f21f74707 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -10,7 +10,7 @@ import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.mount.{InteractWithForceDomeProtectionSeatedInEntity, InteractWithRadiationCloudsSeatedInEntity} +import net.psforever.objects.serverobject.mount.interaction.{InteractWithForceDomeProtectionSeatedInEntity, InteractWithRadiationCloudsSeatedInEntity} import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret} import net.psforever.objects.serverobject.turret.{TurretControl, TurretDefinition, WeaponTurret} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index 122618c39..60ef15996 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -16,7 +16,7 @@ import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness} import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.vehicles._ -import net.psforever.objects.vehicles.interaction.{InteractWithForceDomeProtectionSeatedInVehicle, TriggerOnVehicleRule, WithLava, WithWater} +import net.psforever.objects.vehicles.interaction.{InteractWithForceDomeProtectionSeatedInVehicle, InteractWithRadiationCloudsSeatedInVehicle, TriggerOnVehicleRule, WithLava, WithWater} import net.psforever.objects.vital.resistance.StandardResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala index 66b92688d..c2365a224 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala @@ -4,7 +4,7 @@ package net.psforever.objects.avatar.interaction import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.dome.{ForceDomeControl, ForceDomePhysics} import net.psforever.objects.zones.blockmap.SectorPopulation -import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType} +import net.psforever.objects.zones.interaction.{InteractsWithZone, ZoneInteraction, ZoneInteractionType} case object ForceZoneProtection extends ZoneInteractionType diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala index b6da16d55..976ea1b2c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -32,10 +32,10 @@ trait Damageable { * cite the `originalTakesDamage` protocol during inheritance overrides */ val takesDamage: Receive = { case Damageable.MakeVulnerable => - isVulnerable = false + isVulnerable = true case Damageable.MakeInvulnerable => - isVulnerable = true + isVulnerable = false case Vitality.Damage(damage_func) => val obj = DamageableObject @@ -47,14 +47,14 @@ trait Damageable { /** a duplicate of the core implementation for the default mixin hook, for use in overriding */ final val originalTakesDamage: Receive = { case Damageable.MakeVulnerable => - isVulnerable = false + isVulnerable = true case Damageable.MakeInvulnerable => - isVulnerable = true + isVulnerable = false case Vitality.Damage(damage_func) => val obj = DamageableObject - if (obj.CanDamage) { + if (isVulnerable && obj.CanDamage) { PerformDamage(obj, damage_func) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala index c7c598d8d..b52f4afd6 100644 --- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala +++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala @@ -15,6 +15,7 @@ import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import scala.annotation.unused import scala.util.{Failure, Success} object GenericHackables { @@ -49,6 +50,8 @@ object GenericHackables { } } + private def DontStopHackAttempt(@unused target: PlanetSideServerObject, @unused hacker: Player): Boolean = false + /** * Evaluate the progress of the user applying a tool to modify some server object. * This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily. @@ -62,6 +65,7 @@ object GenericHackables { * @param target the object being affected * @param tool_guid the tool being used to affest the object * @param progress the current progress value + * @param additionalCancellationTests context-specific tests for hack continuation * @return `true`, if the next cycle of progress should occur; * `false`, otherwise */ @@ -70,7 +74,7 @@ object GenericHackables { hacker: Player, target: PlanetSideServerObject, tool_guid: PlanetSideGUID, - additionalCancellationTests: (PlanetSideServerObject, Player) => Boolean = ForceDomeProtectsFromHacking + additionalCancellationTests: (PlanetSideServerObject, Player) => Boolean )( progress: Float ): Boolean = { @@ -93,6 +97,30 @@ object GenericHackables { ) progressState != HackState.Cancelled } + /** + * Evaluate the progress of the user applying a tool to modify some server object. + * This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily. + * The act of transforming allied units of one kind into allied units of another kind (facility turret upgrades) + * is also governed by this action per tick of progress. + * @param progressType 1 - remote electronics kit hack (various ...); + * 2 - nano dispenser (upgrade canister) turret upgrade + * @param hacker the player performing the action + * @param target the object being affected + * @param tool_guid the tool being used to affest the object + * @param progress the current progress value + * @return `true`, if the next cycle of progress should occur; + * `false`, otherwise + */ + def HackingTickAction( + progressType: HackState1, + hacker: Player, + target: PlanetSideServerObject, + tool_guid: PlanetSideGUID + )( + progress: Float + ): Boolean = { + HackingTickAction(progressType, hacker, target, tool_guid, DontStopHackAttempt)(progress) + } /** * The force dome prevents hacking if its protection has been declared over a capitol. diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithForceDomeProtectionSeatedInEntity.scala similarity index 84% rename from src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala rename to src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithForceDomeProtectionSeatedInEntity.scala index 2dddc5ba0..272c31c43 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithForceDomeProtectionSeatedInEntity.scala @@ -1,10 +1,11 @@ // Copyright (c) 2025 PSForever -package net.psforever.objects.serverobject.mount +package net.psforever.objects.serverobject.mount.interaction import net.psforever.objects.avatar.interaction.InteractWithForceDomeProtection import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.dome.ForceDomePhysics -import net.psforever.objects.zones.InteractsWithZone +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.zones.interaction.InteractsWithZone class InteractWithForceDomeProtectionSeatedInEntity extends InteractWithForceDomeProtection { diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithRadiationCloudsSeatedInEntity.scala similarity index 94% rename from src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala rename to src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithRadiationCloudsSeatedInEntity.scala index 6e404519c..29a0b2f97 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithRadiationCloudsSeatedInEntity.scala @@ -1,7 +1,8 @@ // Copyright (c) 2024 PSForever -package net.psforever.objects.serverobject.mount +package net.psforever.objects.serverobject.mount.interaction import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} +import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.base.DamageResolution diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/RadiationInMountableInteraction.scala similarity index 69% rename from src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala rename to src/main/scala/net/psforever/objects/serverobject/mount/interaction/RadiationInMountableInteraction.scala index 5d6f32ca1..98c6b0336 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/RadiationInMountableInteraction.scala @@ -1,4 +1,4 @@ -package net.psforever.objects.serverobject.mount +package net.psforever.objects.serverobject.mount.interaction import net.psforever.objects.zones.interaction.ZoneInteractionType diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala index d63b8070a..ed0f38e8e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala @@ -2,7 +2,8 @@ package net.psforever.objects.serverobject.terminals.implant import net.psforever.objects.serverobject.hackable.Hackable -import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable, Seat} +import net.psforever.objects.serverobject.mount.interaction.InteractWithRadiationCloudsSeatedInEntity +import net.psforever.objects.serverobject.mount.{Mountable, Seat} import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware import net.psforever.objects.vital.resistance.StandardResistanceProfile diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala index 699fdccee..118fca04e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala @@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.equipment.JammableUnit import net.psforever.objects.serverobject.interior.Sidedness -import net.psforever.objects.serverobject.mount.InteractWithRadiationCloudsSeatedInEntity +import net.psforever.objects.serverobject.mount.interaction.InteractWithRadiationCloudsSeatedInEntity import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware import net.psforever.objects.serverobject.turret.auto.AutomatedTurret diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala index dec138367..d90baa787 100644 --- a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala +++ b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala @@ -19,12 +19,12 @@ trait CargoBehavior { val zone = obj.Zone zone.GUID(isMounting) match { case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) - case _ => ; + case _ => () } isMounting = None zone.GUID(isDismounting) match { case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID) - case _ => ; + case _ => () } isDismounting = None startCargoDismountingNoCleanup(bailed = false) @@ -38,14 +38,10 @@ trait CargoBehavior { startCargoDismounting(bailed) case CargoBehavior.EndCargoMounting(carrier_guid) => - if (isMounting.contains(carrier_guid)) { - isMounting = None - } + endCargoMounting(carrier_guid) case CargoBehavior.EndCargoDismounting(carrier_guid) => - if (isDismounting.contains(carrier_guid)) { - isDismounting = None - } + endCargoDismounting(carrier_guid) } def startCargoMounting(carrier_guid: PlanetSideGUID, mountPoint: Int): Unit = { @@ -84,6 +80,18 @@ trait CargoBehavior { } .nonEmpty } + + def endCargoMounting(carrierGuid: PlanetSideGUID): Unit = { + if (isMounting.contains(carrierGuid)) { + isMounting = None + } + } + + def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = { + if (isDismounting.contains(carrierGuid)) { + isDismounting = None + } + } } object CargoBehavior { diff --git a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala index a8028d017..5892d974f 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala @@ -14,7 +14,16 @@ import net.psforever.objects.vital.interaction.DamageResult class CargoCarrierControl(vehicle: Vehicle) extends VehicleControl(vehicle) with CarrierBehavior { - def CarrierObject = vehicle + def CarrierObject: Vehicle = vehicle + + override def TestToStartSelfReporting(): Boolean = { + super.TestToStartSelfReporting() && + !CarrierObject + .CargoHolds + .values + .flatMap(_.occupants) + .exists(_.Seats.values.exists(_.isOccupied)) + } override def postStop() : Unit = { super.postStop() diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 4fd8a9ce1..d7b745a86 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -20,7 +20,7 @@ import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.environment.interaction.{InteractWithEnvironment, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.hackable.GenericHackables -import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction} +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.RepairableVehicle import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.Terminal @@ -31,6 +31,7 @@ import net.psforever.objects.vehicles.interaction.WithWater import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.zones._ +import net.psforever.objects.zones.interaction.IndependentZoneInteraction import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent @@ -63,7 +64,8 @@ class VehicleControl(vehicle: Vehicle) with AggravatedBehavior with RespondsToZoneEnvironment with CargoBehavior - with AffectedByAutomaticTurretFire { + with AffectedByAutomaticTurretFire + with IndependentZoneInteraction { //make control actors belonging to utilities when making control actor belonging to vehicle vehicle.Utilities.foreach { case (_, util) => util.Setup } @@ -77,6 +79,7 @@ class VehicleControl(vehicle: Vehicle) def InteractiveObject: Vehicle = vehicle def CargoObject: Vehicle = vehicle def AffectedObject: Vehicle = vehicle + def ZoneInteractionObject: Vehicle = vehicle /** cheap flag for whether the vehicle is decaying */ var decaying : Boolean = false @@ -84,8 +87,6 @@ class VehicleControl(vehicle: Vehicle) var decayTimer : Cancellable = Default.Cancellable /** becoming waterlogged, or drying out? */ var submergedCondition : Option[OxygenState] = None - /** ... */ - var passengerRadiationCloudTimer: Cancellable = Default.Cancellable def receive : Receive = Enabled @@ -94,7 +95,7 @@ class VehicleControl(vehicle: Vehicle) damageableVehiclePostStop() decaying = false decayTimer.cancel() - passengerRadiationCloudTimer.cancel() + StopInteractionSelfReporting() vehicle.Utilities.values.foreach { util => context.stop(util().Actor) util().Actor = Default.Actor @@ -113,6 +114,7 @@ class VehicleControl(vehicle: Vehicle) .orElse(environmentBehavior) .orElse(cargoBehavior) .orElse(takeAutomatedDamage) + .orElse(zoneInteractionBehavior) .orElse { case Vehicle.Ownership(None) => LoseOwnership() @@ -288,11 +290,6 @@ class VehicleControl(vehicle: Vehicle) final def Enabled: Receive = commonEnabledBehavior .orElse { - case VehicleControl.RadiationTick if !passengerRadiationCloudTimer.isCancelled => - vehicle - .interaction() - .find(_.Type == RadiationInMountableInteraction) - .foreach(_.interaction(vehicle.getInteractionSector, vehicle)) case _ => () } @@ -373,7 +370,7 @@ class VehicleControl(vehicle: Vehicle) decaying = false decayTimer.cancel() } - passengerRadiationCloudTimer.cancel() + TryStopInteractionSelfReporting() updateZoneInteractionProgressUI(user) case Some(seatNumber) => //literally any other seat @@ -381,9 +378,7 @@ class VehicleControl(vehicle: Vehicle) user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number)) decaying = false decayTimer.cancel() - if (!vehicle.Seats(0).isOccupied && passengerRadiationCloudTimer.isCancelled) { - StartRadiationSelfReporting() - } + StopInteractionSelfReporting() updateZoneInteractionProgressUI(user) case None => () @@ -400,15 +395,14 @@ class VehicleControl(vehicle: Vehicle) def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = { val obj = MountableObject - val allSeatsUnoccupied = !obj.Seats.values.exists(_.isOccupied) // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount if (!obj.Seats(0).isOccupied) { obj.Velocity = Some(Vector3.Zero) } - if (allSeatsUnoccupied) { - passengerRadiationCloudTimer.cancel() - } else if (seatBeingDismounted == 0) { - StartRadiationSelfReporting() + val allSeatsUnoccupied = !vehicle.Seats.values.exists(_.isOccupied) + val otherTests = TestToStartSelfReporting() + if (allSeatsUnoccupied && otherTests) { + StartInteractionSelfReporting() } if (!obj.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number)) @@ -432,14 +426,18 @@ class VehicleControl(vehicle: Vehicle) } } - private def StartRadiationSelfReporting(): Unit = { - passengerRadiationCloudTimer.cancel() - passengerRadiationCloudTimer = context.system.scheduler.scheduleWithFixedDelay( - 250.milliseconds, - 250.milliseconds, - self, - VehicleControl.RadiationTick - ) + def TestToStartSelfReporting(): Boolean = { + vehicle.MountedIn.isEmpty + } + + def PerformSelfReportRunCheck(): Unit = { + val noOccupancy = !vehicle.Seats.values.exists(_.isOccupied) + val otherTests = TestToStartSelfReporting() + if (noOccupancy && otherTests) { + StartInteractionSelfReporting() + } else { + StopInteractionSelfReporting() + } } def PrepareForDisabled(kickPassengers: Boolean) : Unit = { @@ -767,9 +765,31 @@ class VehicleControl(vehicle: Vehicle) } override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { - passengerRadiationCloudTimer.cancel() + StopInteractionSelfReportingNoReset() super.DestructionAwareness(target, cause) } + + override def endCargoMounting(carrierGuid: PlanetSideGUID): Unit = { + super.endCargoMounting(carrierGuid) + StopInteractionSelfReporting() + vehicle.Zone.GUID(carrierGuid) match { + case Some(v: Vehicle) => v.Actor ! IndependentZoneInteraction.SelfReportRunCheck + case _ => () + } + } + + override def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = { + super.endCargoDismounting(carrierGuid) + val allSeatsUnoccupied = !vehicle.Seats.values.exists(_.isOccupied) + val otherTests = TestToStartSelfReporting() + if (allSeatsUnoccupied && otherTests) { + StartInteractionSelfReporting() + } + vehicle.Zone.GUID(carrierGuid) match { + case Some(v: Vehicle) => v.Actor ! IndependentZoneInteraction.SelfReportRunCheck + case _ => () + } + } } object VehicleControl { @@ -779,7 +799,5 @@ object VehicleControl { private case class Deletion() - private case object RadiationTick - final case class AssignOwnership(player: Option[Player]) } diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala index 0a988f818..de9671711 100644 --- a/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala @@ -4,8 +4,8 @@ package net.psforever.objects.vehicles.interaction import net.psforever.objects.Vehicle import net.psforever.objects.avatar.interaction.{ForceZoneProtection, InteractWithForceDomeProtection} import net.psforever.objects.serverobject.dome.ForceDomePhysics -import net.psforever.objects.serverobject.mount.InteractWithForceDomeProtectionSeatedInEntity -import net.psforever.objects.zones.InteractsWithZone +import net.psforever.objects.serverobject.mount.interaction.InteractWithForceDomeProtectionSeatedInEntity +import net.psforever.objects.zones.interaction.InteractsWithZone class InteractWithForceDomeProtectionSeatedInVehicle extends InteractWithForceDomeProtectionSeatedInEntity { diff --git a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithRadiationCloudsSeatedInVehicle.scala similarity index 89% rename from src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala rename to src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithRadiationCloudsSeatedInVehicle.scala index 2a95e783d..51efe167f 100644 --- a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithRadiationCloudsSeatedInVehicle.scala @@ -1,8 +1,8 @@ // Copyright (c) 2021 PSForever -package net.psforever.objects.vehicles +package net.psforever.objects.vehicles.interaction import net.psforever.objects.Vehicle -import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction} +import net.psforever.objects.serverobject.mount.interaction.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction} import net.psforever.objects.zones.blockmap.SectorPopulation import net.psforever.objects.zones.interaction.InteractsWithZone diff --git a/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala b/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala new file mode 100644 index 000000000..ec84d39cc --- /dev/null +++ b/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala @@ -0,0 +1,91 @@ +// Copyright (c) 2026 PSForever +package net.psforever.objects.zones.interaction + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.Default + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +trait IndependentZoneInteraction { + _: Actor => + /** ... */ + private var zoneInteractionIntervalDefault: FiniteDuration = 250.milliseconds + /** ... */ + private var zoneInteractionTimer: Cancellable = Default.Cancellable + + def ZoneInteractionObject: InteractsWithZone + + val zoneInteractionBehavior: Receive = { + case IndependentZoneInteraction.InteractionTick => + PerformZoneInteractionSelfReporting() + + case IndependentZoneInteraction.SelfReportRunCheck => + PerformSelfReportRunCheck() + } + + def ZoneInteractionInterval: FiniteDuration = zoneInteractionIntervalDefault + + def ZoneInteractionInterval_=(interval: FiniteDuration): FiniteDuration = { + zoneInteractionIntervalDefault = interval + ZoneInteractionInterval + } + + def TestToStartSelfReporting(): Boolean + + def PerformZoneInteractionSelfReporting(): Unit = { + if (!zoneInteractionTimer.isCancelled) { + ZoneInteractionObject.zoneInteractions() + } + } + + def PerformSelfReportRunCheck(): Unit + + final def StartInteractionSelfReporting(): Unit = { + org.log4s.getLogger("ZIT").info("starting timer") + zoneInteractionTimer.cancel() + zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay( + 0.seconds, + zoneInteractionIntervalDefault, + self, + IndependentZoneInteraction.InteractionTick + ) + } + + final def StartInteractionSelfReporting(initialDelay: FiniteDuration): Unit = { + org.log4s.getLogger("ZIT").info("starting timer") + zoneInteractionTimer.cancel() + zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay( + initialDelay, + zoneInteractionIntervalDefault, + self, + IndependentZoneInteraction.InteractionTick + ) + } + + final def TryStopInteractionSelfReporting(): Boolean = { + if (!zoneInteractionTimer.isCancelled) { + ZoneInteractionObject.resetInteractions() + zoneInteractionTimer.cancel() + } else { + false + } + } + + final def StopInteractionSelfReporting(): Boolean = { + ZoneInteractionObject.resetInteractions() + zoneInteractionTimer.cancel() + } + + final def StopInteractionSelfReportingNoReset(): Boolean = { + zoneInteractionTimer.cancel() + } + + final def ZoneInteractionSelfReportingIsRunning: Boolean = !zoneInteractionTimer.isCancelled +} + +object IndependentZoneInteraction { + private case object InteractionTick + + final case object SelfReportRunCheck +}