From 1d57cca1d3587949abc413b2036ef8697d462af8 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sun, 7 Dec 2025 23:39:43 -0500 Subject: [PATCH 01/13] adding entity to represent force dome, and wiring force dome to capitol facility; touching the force dome while it is active causes death on both server and client --- .../actors/session/csr/GeneralLogic.scala | 24 ++++++- .../actors/session/normal/GeneralLogic.scala | 20 +++++- .../psforever/objects/GlobalDefinitions.scala | 13 ++++ .../dome/ForceDomeDefinition.scala | 9 +++ .../serverobject/dome/ForceDomePhysics.scala | 40 ++++++++++++ .../objects/vital/etc/ForceDomeExposure.scala | 62 +++++++++++++++++++ .../objects/vital/etc/SuicideReason.scala | 2 +- .../scala/net/psforever/zones/Zones.scala | 37 +++++++---- 8 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala create mode 100644 src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index f2b59f251..76e8f2761 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -14,6 +14,7 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.{CommonMessages, ServerObject} import net.psforever.objects.serverobject.containable.Containable +import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.llu.CaptureFlag @@ -26,11 +27,14 @@ import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.FacilityTurret +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vehicles.Utility import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.etc.ForceDomeExposure +import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Initial, Unk1} +import net.psforever.packet.game.OutfitEventAction.{Initial, OutfitInfo, OutfitRankNames, Unk1} import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMemberEvent, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.RemoverActor import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -538,7 +542,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { player.BailProtection = false - val GenericCollisionMsg(ctype, p, _, _, pv, _, _, _, _, _, _, _) = pkt + val GenericCollisionMsg(ctype, p, _, _, pv, t, _, _, _, _, _, _) = pkt if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { if (ops.heightTrend) { ops.heightHistory = ops.heightLast @@ -555,8 +559,22 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex v.BailProtection = false case (CollisionIs.OfAircraft, Some(v: Vehicle)) if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => () + case (CollisionIs.BetweenThings, Some(field: ForceDomePhysics)) /*if field.Energized*/ => + val target = sessionLogic + .vehicles + .findLocalVehicle + .getOrElse(player) + target.Actor ! Vitality.Damage( + DamageInteraction( + PlayerSource(player), + ForceDomeExposure(SourceEntry(field)), + player.Position + ).calculate() + ) + target.BailProtection = false + player.BailProtection = false case (CollisionIs.BetweenThings, _) => - log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") + log.warn(s"GenericCollision: CollisionIs.BetweenThings detected - no handling case for obj id:${t.guid}") case _ => () } } diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 449980c49..1db6ea648 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -16,6 +16,7 @@ import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable +import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf @@ -29,11 +30,11 @@ import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.FacilityTurret -import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vehicles.Utility import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} -import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.vital.etc.{ForceDomeExposure, SuicideReason} import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket @@ -636,6 +637,21 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) + case (CollisionIs.BetweenThings, Some(field: ForceDomePhysics)) /*if field.Energized*/ => + val target = sessionLogic + .vehicles + .findLocalVehicle + .getOrElse(player) + target.Actor ! Vitality.Damage( + DamageInteraction( + PlayerSource(player), + ForceDomeExposure(SourceEntry(field)), + player.Position + ).calculate() + ) + target.BailProtection = false + player.BailProtection = false + (None, None, false, Vector3.Zero) case (CollisionIs.BetweenThings, _) => log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") (None, None, false, Vector3.Zero) diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index c5b198fb1..573220471 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -8,6 +8,7 @@ import net.psforever.objects.definition.converter._ import net.psforever.objects.equipment._ import net.psforever.objects.global.{GlobalDefinitionsAmmo, GlobalDefinitionsBuilding, GlobalDefinitionsDeployable, GlobalDefinitionsExoSuit, GlobalDefinitionsImplant, GlobalDefinitionsKit, GlobalDefinitionsMiscellaneous, GlobalDefinitionsProjectile, GlobalDefinitionsTool, GlobalDefinitionsVehicle} import net.psforever.objects.locker.LockerContainerDefinition +import net.psforever.objects.serverobject.dome.ForceDomeDefinition import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.serverobject.generator.GeneratorDefinition import net.psforever.objects.serverobject.locks.IFFLockDefinition @@ -1286,6 +1287,18 @@ object GlobalDefinitions { val zipline = new GenericTeleportationDefinition(1047) + val force_dome_generator = new ForceDomeDefinition(322) + + val force_dome_amp_physics = new ForceDomeDefinition(313) + + val force_dome_comm_physics = new ForceDomeDefinition(316) + + val force_dome_cryo_physics = new ForceDomeDefinition(319) + + val force_dome_dsp_physics = new ForceDomeDefinition(321) + + val force_dome_tech_physics = new ForceDomeDefinition(323) + /* Buildings */ diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala new file mode 100644 index 000000000..b362682e6 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -0,0 +1,9 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.serverobject.dome + +import net.psforever.objects.serverobject.structures.AmenityDefinition + +class ForceDomeDefinition(objectId: Int) + extends AmenityDefinition(objectId) { + Name = "force_dome" +} diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala new file mode 100644 index 000000000..3a4c0dc7b --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala @@ -0,0 +1,40 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.serverobject.dome + +import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.types.Vector3 + +class ForceDomePhysics(private val cfddef: ForceDomeDefinition) + extends Amenity { + private var energized: Boolean = false + + def Energized: Boolean = energized + + def Energized_=(state: Boolean): Boolean = { + energized = state + Energized + } + + def Definition: ForceDomeDefinition = cfddef +} + +object ForceDomePhysics { + import akka.actor.ActorContext + + /** + * Instantiate and configure a `CapitolForceDome` object. + * @param pos positon of the object in the zone's coordinate system + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `CapitolForceDome` object + */ + def Constructor(pos: Vector3)(id: Int, context: ActorContext): ForceDomePhysics = { + //import akka.actor.Props + import net.psforever.objects.GlobalDefinitions + + val obj = new ForceDomePhysics(GlobalDefinitions.force_dome_generator) + obj.Position = pos + //obj.Actor = context.actorOf(Props(classOf[null], obj), s"${GlobalDefinitions.door.Name}_$id") + obj + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala new file mode 100644 index 000000000..d09ddd880 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.vital.etc + +import net.psforever.objects.sourcing.{AmenitySource, SourceEntry} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} +import net.psforever.objects.vital.base.{DamageReason, DamageResolution} +import net.psforever.objects.vital.damage.DamageCalculations +import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} + +/** + * A wrapper for a "damage source" in damage calculations that indicates a harmful interaction from a capitol force dome. + * @param field the target of the field in question + */ +final case class ForceDomeExposure(field: SourceEntry) + extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Collision + + def same(test: DamageReason): Boolean = test match { + case eer: ForceDomeExposure => eer.field eq field + case _ => false + } + + /** + * Want to blame the capitol facility that is being protected. + */ + override def attribution: Int = field match { + case a: AmenitySource => a.installation.Definition.ObjectId + case _ => field.Definition.ObjectId + } + + /** + * A direct connection to the damage information, numbers and properties. + */ + override def source: DamageProperties = ForceDomeExposure.damageProperties + + /** + * The functionality that is necessary for interaction of a vital game object with the rest of the hostile game world. + */ + override def damageModel: DamageAndResistance = ForceDomeExposure.drm + + /** + * The person to be blamed for this. + */ + override def adversary: Option[SourceEntry] = None +} + +object ForceDomeExposure { + final val drm = new DamageResistanceModel { + DamageUsing = DamageCalculations.AgainstExoSuit + ResistUsing = NoResistanceSelection + Model = SimpleResolutions.calculate + } + + final val damageProperties = new DamageProperties { + Damage0 = 99999 + DamageToHealthOnly = true + DamageToVehicleOnly = true + DamageToBattleframeOnly = true + } +} + diff --git a/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala b/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala index cf7f1f085..c80ce9c64 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala @@ -29,7 +29,7 @@ final case class SuicideReason() eventually, they stop logging in. Anyway, this has nothing to do with that. - Most playes probably just want to jump to the next base over. + Most players probably just want to jump to the next base over. */ def source: DamageProperties = SuicideReason.damageProperties diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 4ac2da6a6..702924f74 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -11,6 +11,7 @@ import io.circe.parser._ import net.psforever.objects.{GlobalDefinitions, LocalLockerItem, LocalProjectile} import net.psforever.objects.definition.BasicDefinition import net.psforever.objects.guid.selector.{NumberSelector, RandomSelector, SpecificSelector} +import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.doors.{Door, DoorDefinition, SpawnTubeDoor} import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition} @@ -100,17 +101,9 @@ object Zones { "PathPoints" )(ZipLinePath.apply) - // monolith, hst, warpgate are ignored for now as the scala code isn't ready to handle them. // BFR terminals/doors are ignored as top level elements as sanctuaries have them with no associated building. (repair_silo also has this problem, but currently is ignored in the AmenityExtrator project) // Force domes have GUIDs but are currently classed as separate entities. The dome is controlled by sending GOAM 44 / 48 / 52 to the building GUID - private val ignoredEntities = Seq( - "monolith", - "force_dome_dsp_physics", - "force_dome_comm_physics", - "force_dome_cryo_physics", - "force_dome_tech_physics", - "force_dome_amp_physics" - ) + private val ignoredEntities = Seq("monolith") private val towerTypes = Seq("tower_a", "tower_b", "tower_c") private val facilityTypes = Seq("amp_station", "cryo_facility", "comm_station", "comm_station_dsp", "tech_plant") @@ -127,6 +120,13 @@ object Zones { "vt_spawn", "vt_vehicle" ) + private val forceDomeTypes = Seq( + "force_dome_dsp_physics", + "force_dome_comm_physics", + "force_dome_cryo_physics", + "force_dome_tech_physics", + "force_dome_amp_physics" + ) private val cavernBuildingTypes = Seq( "ceiling_bldg_a", "ceiling_bldg_b", @@ -380,11 +380,27 @@ object Zones { createObjects( zoneMap, - zoneObjects.filterNot { _.objectType.startsWith("bfr_") }, + zoneObjects.filterNot { obj => obj.objectType.startsWith("bfr_") || forceDomeTypes.contains(obj.objectType) }, ownerGuid = 0, None, turretWeaponGuid ) + //force dome physics object are not owned + //for our benefit, we can attach them as amenities to the zone's capitol facility + zoneObjects + .find { obj => forceDomeTypes.contains(obj.objectType) } + .foreach { forceDome => + structures + .find { structure => Building.Capitols.contains(structure.objectName) } + .foreach { capitol => + zoneMap + .addLocalObject( + forceDome.guid, + ForceDomePhysics.Constructor(forceDome.position), + owningBuildingGuid = capitol.guid + ) + } + } lattice.asObject.get(mapid).foreach { obj => obj.asArray.get.foreach { entry => @@ -710,7 +726,6 @@ object Zones { case _ => () } - } } From 8fedd2e7242da1c2ae1c853b17bc19a24b635f5c Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 15 Dec 2025 15:14:27 -0500 Subject: [PATCH 02/13] force dome is now an amenity owned by its encompassing capitol facility and has its own control agency that is aware of the faction affinity, hack state, etc. conditions that would cause it to close over or open up --- .../session/support/ZoningOperations.scala | 2 +- .../net/psforever/actors/zone/ZoneActor.scala | 2 - .../zone/building/MajorFacilityLogic.scala | 106 +----- .../serverobject/dome/ForceDomeControl.scala | 313 ++++++++++++++++++ .../serverobject/dome/ForceDomePhysics.scala | 16 +- .../serverobject/structures/Building.scala | 15 +- .../net/psforever/objects/zones/Zone.scala | 8 + .../scala/net/psforever/zones/Zones.scala | 16 +- 8 files changed, 354 insertions(+), 124 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala 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 bbbb3a982..d67de9261 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -1060,7 +1060,7 @@ class ZoningOperations( case _ => () } // capitol force dome state - if (building.IsCapitol && building.ForceDomeActive) { + if (building.IsCapitol && building.ForceDome.exists(_.Energized)) { sendResponse(GenericObjectActionMessage(guid, 13)) } // amenities diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 28a3c2015..391b0a7c8 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -10,7 +10,6 @@ import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup} import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3} import akka.actor.typed.scaladsl.adapter._ -import net.psforever.actors.zone.building.MajorFacilityLogic import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior @@ -110,7 +109,6 @@ class ZoneActor( //warp gates are controlled by game logic and are better off not restored via the database case Some(b) => if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) { - b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false) b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b)) b.CaptureTerminal.collect { terminal => val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true) diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala index 4dd21dcb7..2ee6b7797 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -15,7 +15,7 @@ import net.psforever.services.{InterstellarClusterService, Service} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState} +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} /** * A package class that conveys the important information for handling facility updates. @@ -56,99 +56,6 @@ case object MajorFacilityLogic MajorFacilityWrapper(building, context, details.galaxyService, details.interstellarCluster) } - /** - * Evaluate the conditions of the building - * and determine if its capitol force dome state should be updated - * to reflect the actual conditions of the base or its surrounding bases. - * If this building is considered a subcapitol facility to the zone's actual capitol facility, - * and has the capitol force dome has a dependency upon it, - * pass a message onto that facility that it should check its own state alignment. - * @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building - */ - private def alignForceDomeStatus(details: BuildingWrapper, mapUpdateOnChange: Boolean = true): Behavior[Command] = { - val building = details.building - checkForceDomeStatus(building) match { - case Some(updatedStatus) if updatedStatus != building.ForceDomeActive => - updateForceDomeStatus(details, updatedStatus, mapUpdateOnChange) - case _ => ; - } - Behaviors.same - } - - /** - * Dispatch a message to update the state of the clients with the server state of the capitol force dome. - * @param updatedStatus the new capitol force dome status - * @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building - */ - private def updateForceDomeStatus( - details: BuildingWrapper, - updatedStatus: Boolean, - mapUpdateOnChange: Boolean - ): Unit = { - val building = details.building - val zone = building.Zone - building.ForceDomeActive = updatedStatus - zone.LocalEvents ! LocalServiceMessage( - zone.id, - LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus) - ) - if (mapUpdateOnChange) { - details.context.self ! BuildingActor.MapUpdate() - } - } - - /** - * The natural conditions of a facility that is not eligible for its capitol force dome to be expanded. - * The only test not employed is whether or not the target building is a capitol. - * Ommission of this condition makes this test capable of evaluating subcapitol eligibility - * for capitol force dome expansion. - * @param building the target building - * @return `true`, if the conditions for capitol force dome are not met; - * `false`, otherwise - */ - private def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = { - building.Faction == PlanetSideEmpire.NEUTRAL || - building.NtuLevel == 0 || - (building.Generator match { - case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed - case _ => false - }) - } - - /** - * If this building is a capitol major facility, - * use the faction affinity, the generator status, and the resource silo's capacitance level - * to determine if the capitol force dome should be active. - * @param building the building being evaluated - * @return the condition of the capitol force dome; - * `None`, if the facility is not a capitol building; - * `Some(true|false)` to indicate the state of the force dome - */ - def checkForceDomeStatus(building: Building): Option[Boolean] = { - if (building.IsCapitol) { - val originalStatus = building.ForceDomeActive - val faction = building.Faction - val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) { - false - } else { - val ownedSubCapitols = building.Neighbours(faction) match { - case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) } - case None => 0 - } - if (originalStatus && ownedSubCapitols <= 1) { - false - } else if (!originalStatus && ownedSubCapitols > 1) { - true - } else { - originalStatus - } - } - Some(updatedStatus) - } else { - None - } - } - /** * The power structure of major facilities has to be statused on the continental map * via the state of its nanite-to-energy generator, and @@ -267,7 +174,7 @@ case object MajorFacilityLogic } /** - * The generator is an extrememly important amenity of a major facility + * The generator is an extremely important amenity of a major facility * that is given its own status indicators that are apparent from the continental map * and warning messages that are displayed to everyone who might have an interest in the that particular generator. * @param details package class that conveys the important information @@ -314,7 +221,6 @@ case object MajorFacilityLogic true case Some(GeneratorControl.Event.Offline) => powerLost(details) - alignForceDomeStatus(details, mapUpdateOnChange = false) val zone = building.Zone val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2) building.PlayersInSOI.foreach { player => @@ -326,7 +232,6 @@ case object MajorFacilityLogic case Some(GeneratorControl.Event.Online) => // Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal. powerRestored(details) - alignForceDomeStatus(details, mapUpdateOnChange = false) val events = zone.AvatarEvents val guid = building.GUID val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0) @@ -348,16 +253,17 @@ case object MajorFacilityLogic ): Behavior[Command] = { if (details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply) { BuildingActor.setFactionTo(details, faction, log) - alignForceDomeStatus(details, mapUpdateOnChange = false) val building = details.building - building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) } + val alertMsg = BuildingActor.AlertToFactionChange(building) + building.Neighbours.getOrElse(Nil).foreach { _.Actor ! alertMsg } + building.Amenities.foreach { _.Actor ! alertMsg } } Behaviors.same } def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = { - alignForceDomeStatus(details) val bldg = details.building + bldg.Amenities.foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) } //todo map update? //the presence of the flag means that we are involved in an ongoing llu hack (bldg.GetFlag, bldg.CaptureTerminal) match { case (Some(flag), Some(terminal)) if (flag.Target eq building) && flag.Faction != building.Faction => diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala new file mode 100644 index 000000000..dc101b229 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -0,0 +1,313 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.serverobject.dome + +import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl} +import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} +import net.psforever.objects.serverobject.turret.FacilityTurret +import net.psforever.packet.game.ChatMsg +import net.psforever.services.Service +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGeneratorState, Vector3} + +object ForceDomeControl { + trait Command + + final case object CustomExpand extends Command + + final case object CustomCollapse extends Command + + final case object NormalBehavior extends Command + + /** + * Dispatch a message to update the state of the clients with the server state of the capitol force dome. + * @param dome force dome + * @param activationState new force dome status + */ + def ChangeDomeEnergizedState(dome: ForceDomePhysics, activationState: Boolean): Unit = { + dome.Energized = activationState + val owner = dome.Owner + val zone = owner.Zone + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, owner.GUID, activationState) + ) + } + + /** + * If this building is a capitol major facility, + * use the faction affinity, the generator status, and the resource silo's capacitance level + * to determine if the capitol force dome should be active. + * @param building building being evaluated + * @param dome force dome + * @return the condition of the capitol force dome; + * `None`, if the facility is not a capitol building; + * `Some(true|false)` to indicate the state of the force dome + */ + def CheckForceDomeStatus(building: Building, dome: ForceDomePhysics): Option[Boolean] = { + if (building.IsCapitol) { + Some( + if (ForceDomeControl.InvalidBuildingCapitolForceDomeConditions(building)) { + false + } else { + building + .Neighbours(building.Faction) + .map(_.count(b => !ForceDomeControl.InvalidBuildingCapitolForceDomeConditions(b))) + .exists(_ > 1) + } + ) + } else { + None + } + } + + /** + * The natural conditions of a facility that is not eligible for its capitol force dome to be expanded. + * The only test not employed is whether or not the target building is a capitol. + * Omission of this condition makes this test capable of evaluating subcapitol eligibility + * for capitol force dome expansion. + * @param building target building + * @return `true`, if the conditions for capitol force dome are not met; + * `false`, otherwise + */ + def InvalidBuildingCapitolForceDomeConditions(building: Building): Boolean = { + building.Faction == PlanetSideEmpire.NEUTRAL || + building.NtuLevel == 0 || + building.Generator.exists(_.Condition == PlanetSideGeneratorState.Destroyed) + } + + /** + * na + * @param dome force dome + * @return na + */ + def GeneralFacilityPerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = { + val generatorTowerCenter = dome.Position.xy + val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) + val pointsOfForceDomePerimeter = turretPoints.map { p => + val segmentFromTowerToTurret = p - generatorTowerCenter + Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset + } + pointsOfForceDomePerimeter + .flatMap { point => + pointsOfForceDomePerimeter + .sortBy(p => Vector3.DistanceSquared(p, point)) + .slice(1, 3) + .map { otherPoint => + if (point.y > otherPoint.y || point.x < otherPoint.x) { + (point, otherPoint) + } else { + (otherPoint, point) + } + } + } + .distinct + } + + def TechPlantFacilityPerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = { + val generatorTowerCenter = dome.Position.xy + val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) + val organizedByClosestToGarage = dome + .Owner + .Amenities + .find(_.Definition.Name.equals("gr_door_garage_ext")) + .map { garage => + val doorPosition = garage.Position.xy + turretPoints.sortBy(point => Vector3.DistanceSquared(doorPosition, point)) + } + .getOrElse(List[Vector3]()) + + //val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) + val pointsOfForceDomePerimeter = turretPoints.map { p => + val segmentFromTowerToTurret = p - generatorTowerCenter + Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset + } + Nil + } + + /** + * na + * @param building target building + * @param state na + */ + def CustomDomeStateEnforcedMessage( + building: Building, + state: Boolean + ): Unit = { + val events = building.Zone.LocalEvents + val message = LocalAction.SendResponse(ChatMsg( + ChatMessageType.UNK_227, + s"Capitol force dome state change was suppressed. ${building.Name} will remain ${if (state) "enveloped" else "exposed"}." + )) + building.PlayersInSOI.foreach { player => + events ! LocalServiceMessage(player.Name, message) + } + } + + /** + * na + * @param building target building + */ + def NormalDomeStateMessage(building: Building): Unit = { + val events = building.Zone.LocalEvents + val message = LocalAction.SendResponse(ChatMsg( + ChatMessageType.UNK_227, + "Expected capitol force dome state change will resume." + )) + building.PlayersInSOI.foreach { player => + events ! LocalServiceMessage(player.Name, message) + } + } +} + +/** + * An `Actor` that handles messages being dispatched to a specific capitol facility's force dome. + * @param dome the `ForceDomePhysics` object being governed + */ +class ForceDomeControl(dome: ForceDomePhysics) + extends PoweredAmenityControl + with CaptureTerminalAwareBehavior + with FactionAffinityBehavior.Check { + def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome + def FactionObject: FactionAffinity = dome + + private var perimeterSegments: List[(Vector3, Vector3)] = Nil + + private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building] + + private var customState: Option[Boolean] = None + + def commonBehavior: Receive = checkBehavior + .orElse { + case Service.Startup() => + setupPerimeter() + + case ForceDomeControl.CustomExpand + if !dome.Energized && (customState.isEmpty || customState.contains(false)) => + customState = Some(true) + ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true) + ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = true) + + case ForceDomeControl.CustomExpand + if customState.isEmpty => + customState = Some(true) + ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true) + + case ForceDomeControl.CustomCollapse + if dome.Energized && (customState.isEmpty || customState.contains(true)) => + customState = Some(false) + ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = false) + ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false) + + case ForceDomeControl.CustomCollapse + if customState.isEmpty => + customState = Some(false) + ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = false) + + case ForceDomeControl.NormalBehavior + if customState.nonEmpty => + customState = None + ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) + alignForceDomeStatusAndUpdate(domeOwnerAsABuilding) + } + + def poweredStateLogic: Receive = { + commonBehavior + .orElse(captureTerminalAwareBehaviour) + .orElse { + case BuildingActor.AlertToFactionChange(_) => + blockedByCustomStateOr(alignForceDomeStatusAndUpdate, domeOwnerAsABuilding) + + case _ => () + } + } + + def unpoweredStateLogic: Receive = { + commonBehavior + .orElse { + case _ => () + } + } + + def powerTurnOffCallback() : Unit = { + if (dome.Energized && customState.isEmpty) { + ForceDomeControl.ChangeDomeEnergizedState(dome,activationState = false) + } + } + + def powerTurnOnCallback() : Unit = { + blockedByCustomStateOr(alignForceDomeStatus, domeOwnerAsABuilding) + } + + override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = { + super.captureTerminalIsResecured(terminal) + blockedByCustomStateOr(alignForceDomeStatus, domeOwnerAsABuilding) + } + + override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = { + super.captureTerminalIsHacked(terminal) + if (dome.Energized && customState.isEmpty) { + ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false) + } + } + + private def setupPerimeter(): Unit = { + //todo tech plants have an indent + if (perimeterSegments.isEmpty) { + perimeterSegments = ForceDomeControl.GeneralFacilityPerimeter(dome) + } + } + + /** + * na + * @param func function to run if not blocked + * @param building facility to operate upon (parameter to `func`) + * @return next behavior for an actor state + */ + private def blockedByCustomStateOr(func: Building => Unit, building: Building): Unit = { + customState match { + case None => + func(building) + case Some(state) => + ForceDomeControl.CustomDomeStateEnforcedMessage(building, state) + } + } + + /** + * Evaluate the conditions of the building + * and determine if its capitol force dome state should be updated + * to reflect the actual conditions of the base or its surrounding bases. + * If this building is considered a subcapitol facility to the zone's actual capitol facility, + * and has the capitol force dome has a dependency upon it, + * pass a message onto that facility that it should check its own state alignment. + * @param building the building being evaluated + */ + private def alignForceDomeStatusAndUpdate(building: Building): Unit = { + ForceDomeControl.CheckForceDomeStatus(building, dome).foreach { + updatedStatus => + if (updatedStatus != dome.Energized) { + ForceDomeControl.ChangeDomeEnergizedState(dome, updatedStatus) + dome.Owner.Actor ! BuildingActor.MapUpdate() + } + } + } + + /** + * Evaluate the conditions of the building + * and determine if its capitol force dome state should be updated + * to reflect the actual conditions of the base or its surrounding bases. + * If this building is considered a subcapitol facility to the zone's actual capitol facility, + * and has the capitol force dome has a dependency upon it, + * pass a message onto that facility that it should check its own state alignment. + * @param building the building being evaluated + */ + private def alignForceDomeStatus(building: Building): Unit = { + ForceDomeControl.CheckForceDomeStatus(building, dome).foreach { + updatedStatus => + if (updatedStatus != dome.Energized) { + ForceDomeControl.ChangeDomeEnergizedState(dome, updatedStatus) + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala index 3a4c0dc7b..88d62963c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala @@ -2,10 +2,12 @@ package net.psforever.objects.serverobject.dome import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware import net.psforever.types.Vector3 class ForceDomePhysics(private val cfddef: ForceDomeDefinition) - extends Amenity { + extends Amenity + with CaptureTerminalAware { private var energized: Boolean = false def Energized: Boolean = energized @@ -23,18 +25,18 @@ object ForceDomePhysics { /** * Instantiate and configure a `CapitolForceDome` object. - * @param pos positon of the object in the zone's coordinate system + * @param pos position of the object in the zone's coordinate system + * @param fddef specific type of force dome * @param id the unique id that will be assigned to this entity * @param context a context to allow the object to properly set up `ActorSystem` functionality * @return the `CapitolForceDome` object */ - def Constructor(pos: Vector3)(id: Int, context: ActorContext): ForceDomePhysics = { - //import akka.actor.Props - import net.psforever.objects.GlobalDefinitions + def Constructor(pos: Vector3, fddef: ForceDomeDefinition)(id: Int, context: ActorContext): ForceDomePhysics = { + import akka.actor.Props - val obj = new ForceDomePhysics(GlobalDefinitions.force_dome_generator) + val obj = new ForceDomePhysics(fddef) obj.Position = pos - //obj.Actor = context.actorOf(Props(classOf[null], obj), s"${GlobalDefinitions.door.Name}_$id") + obj.Actor = context.actorOf(Props(classOf[ForceDomeControl], obj), name = s"${fddef.Name}_$id") obj } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index a7863ab56..e1ef6f2c7 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -15,6 +15,7 @@ import net.psforever.packet.game.{Additional3, BuildingInfoUpdateMessage, Densit import net.psforever.types._ import scalax.collection.{Graph, GraphEdge} import akka.actor.typed.scaladsl.adapter._ +import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal @@ -32,7 +33,6 @@ class Building( private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var playersInSOI: List[Player] = List.empty - private var forceDomeActive: Boolean = false private var participationFunc: ParticipationLogic = NoParticipation var virusId: Long = 8 // 8 default = no virus var virusInstalledBy: Option[Int] = None // faction id @@ -59,11 +59,6 @@ class Building( case None => false } } - def ForceDomeActive: Boolean = forceDomeActive - def ForceDomeActive_=(activated: Boolean): Boolean = { - forceDomeActive = activated - forceDomeActive - } def Faction: PlanetSideEmpire.Value = faction @@ -108,6 +103,13 @@ class Building( } } + def ForceDome: Option[ForceDomePhysics] = { + Amenities.find(_.isInstanceOf[ForceDomePhysics]) match { + case Some(out: ForceDomePhysics) => Some(out) + case _ => None + } + } + def NtuSource: Option[NtuContainer] = { Amenities.find(_.isInstanceOf[NtuContainer]) match { case Some(o: NtuContainer) => Some(o) @@ -223,6 +225,7 @@ class Building( else { (virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3)))) } + val forceDomeActive = ForceDome.exists(_.Energized) BuildingInfoUpdateMessage( Zone.Number, diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 1caf87d72..e48338c59 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -40,6 +40,7 @@ import net.psforever.objects.geometry.d3.VolumetricGeometry import net.psforever.objects.guid.pool.NumberPool import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.dome.{ForceDomeDefinition, ForceDomePhysics} import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.environment.EnvironmentAttribute import net.psforever.objects.serverobject.interior.{InteriorAware, Sidedness} @@ -1638,6 +1639,13 @@ object Zone { case painbox: Painbox => painbox.Actor ! Service.Startup() } + //capitol facilities have force domes + buildings.values + .flatMap(_.Amenities.filter(_.Definition.isInstanceOf[ForceDomeDefinition])) + .collect { + case obj: ForceDomePhysics => + obj.Actor ! Service.Startup() + } //the orbital_buildings in sanctuary zones have to establish their shuttle routes map.shuttleBays .map { diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 702924f74..3682a9ad5 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -11,7 +11,7 @@ import io.circe.parser._ import net.psforever.objects.{GlobalDefinitions, LocalLockerItem, LocalProjectile} import net.psforever.objects.definition.BasicDefinition import net.psforever.objects.guid.selector.{NumberSelector, RandomSelector, SpecificSelector} -import net.psforever.objects.serverobject.dome.ForceDomePhysics +import net.psforever.objects.serverobject.dome.{ForceDomeDefinition, ForceDomePhysics} import net.psforever.objects.serverobject.doors.{Door, DoorDefinition, SpawnTubeDoor} import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition} @@ -385,7 +385,7 @@ object Zones { None, turretWeaponGuid ) - //force dome physics object are not owned + //force dome physics objects have no owner //for our benefit, we can attach them as amenities to the zone's capitol facility zoneObjects .find { obj => forceDomeTypes.contains(obj.objectType) } @@ -393,12 +393,12 @@ object Zones { structures .find { structure => Building.Capitols.contains(structure.objectName) } .foreach { capitol => - zoneMap - .addLocalObject( - forceDome.guid, - ForceDomePhysics.Constructor(forceDome.position), - owningBuildingGuid = capitol.guid - ) + val definition = DefinitionUtil.fromString(forceDome.objectType).asInstanceOf[ForceDomeDefinition] + zoneMap.addLocalObject( + forceDome.guid, + ForceDomePhysics.Constructor(forceDome.position, definition), + owningBuildingGuid = capitol.guid + ) } } From 6a960ed5acc8179d62eedd12937ae57e0faf57b8 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 16 Dec 2025 20:37:29 -0500 Subject: [PATCH 03/13] force dome messages owner about change in state, triggering the NTU silo to give away repairs for free; activating the force dome kills or destroys all enemies within its radius --- .../zone/building/MajorFacilityLogic.scala | 34 +++ .../objects/ExplosiveDeployable.scala | 3 +- .../GlobalDefinitionsMiscellaneous.scala | 15 ++ .../serverobject/dome/ForceDomeControl.scala | 243 ++++++++++++------ .../generator/GeneratorControl.scala | 2 +- .../process/VehicleSpawnControlRailJack.scala | 3 +- .../resourcesilo/ResourceSiloControl.scala | 13 +- .../net/psforever/objects/zones/Zone.scala | 47 +++- 8 files changed, 262 insertions(+), 98 deletions(-) diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala index 2ee6b7797..00325776c 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -6,7 +6,9 @@ import akka.actor.typed.{ActorRef, Behavior} import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import net.psforever.actors.commands.NtuCommand import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor} +import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} +import net.psforever.objects.serverobject.resourcesilo.ResourceSiloControl import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.objects.sourcing.PlayerSource @@ -104,6 +106,10 @@ case object MajorFacilityLogic ) } // No map update needed - will be sent by `HackCaptureActor` when required + case dome: ForceDomePhysics => + // The force dome being expanded modifies the NTU drain rate + val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome)) + details.building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier)) case _ => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) } @@ -345,4 +351,32 @@ case object MajorFacilityLogic } Behaviors.same } + + private def calculateNtuDrainMultiplierFrom( + building: Building, + domeOpt: Option[ForceDomePhysics] = None, + mainTerminalOpt: Option[Any] = None + ): Float = { + val domeParam = domeOpt.orElse { + building.Amenities.find(_.isInstanceOf[ForceDomePhysics]) match { + case Some(d: ForceDomePhysics) => Some(d) + case _ => None + } + } + val mainTerminalParam = mainTerminalOpt.orElse(None) //todo main terminal and viruses + getNtuDrainMultiplierFromAmenities(domeParam, mainTerminalParam) + } + + private def getNtuDrainMultiplierFromAmenities( + dome: Option[ForceDomePhysics], + mainTerminal: Option[Any] + ): Float = { + // The force dome being expanded means all repairs are essentially for free + dome + .map { case d if d.Energized => 0f } + .orElse { + mainTerminal.flatMap { _ => Some(2f) } //todo main terminal and viruses + } + .getOrElse(1f) + } } diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index b32bf2e4d..ceea45622 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -187,7 +187,8 @@ object ExplosiveDeployableControl { zone, target, Zone.explosionDamage(Some(cause)), - ExplosiveDeployableControl.detectionForExplosiveSource(target) + ExplosiveDeployableControl.detectionForExplosiveSource(target), + Zone.findAllTargets ) } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index ff136ba94..9714631bd 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -956,5 +956,20 @@ object GlobalDefinitionsMiscellaneous { zipline.Name = "zipline" zipline.interference = InterferenceRange(deployables = 5.5f) + + force_dome_amp_physics.Name = "force_dome_amp_physics" + force_dome_amp_physics.UseRadius = 142.26f + + force_dome_comm_physics.Name = "force_dome_comm_physics" + force_dome_comm_physics.UseRadius = 121.8149f + + force_dome_cryo_physics.Name = "force_dome_cryo_physics" + force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f + + force_dome_dsp_physics.Name = "force_dome_dsp_physics" + force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f + + force_dome_tech_physics.Name = "force_dome_tech_physics" + force_dome_tech_physics.UseRadius = 150.1284f } } diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index dc101b229..12c438043 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -2,10 +2,18 @@ package net.psforever.objects.serverobject.dome import net.psforever.actors.zone.BuildingActor +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.objects.serverobject.turret.FacilityTurret +import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.etc.ForceDomeExposure +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.prop.DamageWithPosition +import net.psforever.objects.zones.Zone import net.psforever.packet.game.ChatMsg import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -29,6 +37,7 @@ object ForceDomeControl { dome.Energized = activationState val owner = dome.Owner val zone = owner.Zone + owner.Actor ! BuildingActor.AmenityStateChange(dome) zone.LocalEvents ! LocalServiceMessage( zone.id, LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, owner.GUID, activationState) @@ -48,12 +57,12 @@ object ForceDomeControl { def CheckForceDomeStatus(building: Building, dome: ForceDomePhysics): Option[Boolean] = { if (building.IsCapitol) { Some( - if (ForceDomeControl.InvalidBuildingCapitolForceDomeConditions(building)) { + if (InvalidBuildingCapitolForceDomeConditions(building)) { false } else { building .Neighbours(building.Faction) - .map(_.count(b => !ForceDomeControl.InvalidBuildingCapitolForceDomeConditions(b))) + .map(_.count(b => !InvalidBuildingCapitolForceDomeConditions(b))) .exists(_ > 1) } ) @@ -105,24 +114,25 @@ object ForceDomeControl { .distinct } - def TechPlantFacilityPerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = { - val generatorTowerCenter = dome.Position.xy - val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) - val organizedByClosestToGarage = dome - .Owner - .Amenities - .find(_.Definition.Name.equals("gr_door_garage_ext")) - .map { garage => - val doorPosition = garage.Position.xy - turretPoints.sortBy(point => Vector3.DistanceSquared(doorPosition, point)) - } - .getOrElse(List[Vector3]()) - - //val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) - val pointsOfForceDomePerimeter = turretPoints.map { p => - val segmentFromTowerToTurret = p - generatorTowerCenter - Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset - } + import scala.annotation.unused + def TechPlantFacilityPerimeter(@unused dome: ForceDomePhysics): List[(Vector3, Vector3)] = { +// val generatorTowerCenter = dome.Position.xy +// val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) +// val organizedByClosestToGarage = dome +// .Owner +// .Amenities +// .find(_.Definition.Name.equals("gr_door_garage_ext")) +// .map { garage => +// val doorPosition = garage.Position.xy +// turretPoints.sortBy(point => Vector3.DistanceSquared(doorPosition, point)) +// } +// .getOrElse(List[Vector3]()) +// +// //val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) +// val pointsOfForceDomePerimeter = turretPoints.map { p => +// val segmentFromTowerToTurret = p - generatorTowerCenter +// Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset +// } Nil } @@ -147,7 +157,7 @@ object ForceDomeControl { /** * na - * @param building target building + * @param building facility */ def NormalDomeStateMessage(building: Building): Unit = { val events = building.Zone.LocalEvents @@ -159,6 +169,116 @@ object ForceDomeControl { events ! LocalServiceMessage(player.Name, message) } } + + /** + * Evaluate the conditions of the building + * and determine if its capitol force dome state should be updated + * to reflect the actual conditions of the base or its surrounding bases. + * If this building is considered a subcapitol facility to the zone's actual capitol facility, + * and has the capitol force dome has a dependency upon it, + * pass a message onto that facility that it should check its own state alignment. + * @param building facility with `dome` + */ + def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Unit = { + CheckForceDomeStatus(building, dome).foreach { + case true => + if (!dome.Energized) { + ChangeDomeEnergizedState(dome, activationState = true) + ForceDomeKills(dome) + dome.Owner.Actor ! BuildingActor.MapUpdate() + } + case false => + if (dome.Energized) { + ChangeDomeEnergizedState(dome, activationState = false) + dome.Owner.Actor ! BuildingActor.MapUpdate() + } + } + } + + /** + * Evaluate the conditions of the building + * and determine if its capitol force dome state should be updated + * to reflect the actual conditions of the base or its surrounding bases. + * If this building is considered a subcapitol facility to the zone's actual capitol facility, + * and has the capitol force dome has a dependency upon it, + * pass a message onto that facility that it should check its own state alignment. + * @param building facility with `dome` + */ + private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Unit = { + CheckForceDomeStatus(building, dome).foreach { + case true => + if (!dome.Energized) { + ChangeDomeEnergizedState(dome, activationState = true) + ForceDomeKills(dome) + } + case false => + if (dome.Energized) { + ChangeDomeEnergizedState(dome, activationState = false) + } + } + } + + /** + * Being too close to the force dome can destroy targets if they do not match the faction alignment of the dome. + * This is the usual fate of opponents upon it being expanded (energeized). + * @see `Zone.serverSideDamage` + * @param dome force dome + * @return a list of affected entities + */ + def ForceDomeKills(dome: ForceDomePhysics): List[PlanetSideServerObject] = { + Zone.serverSideDamage( + dome.Zone, + dome, + contactWithForceDome, + Zone.distanceCheck, + forceDomeTargets(dome.Definition.UseRadius, dome.Faction) + ) + } + + /** + * na + * @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 + */ + private def contactWithForceDome( + source: PlanetSideGameObject with FactionAffinity with Vitality, + target: PlanetSideGameObject with FactionAffinity with Vitality + ): DamageInteraction = { + DamageInteraction( + SourceEntry(target), + ForceDomeExposure(SourceEntry(source)), + target.Position + ) + } + + /** + * na + * @see `DamageWithPosition` + * @see `Zone.blockMap.sector` + * @param zone the zone in which the explosion should occur + * @param source a game entity that is treated as the origin and is excluded from results + * @param damagePropertiesBySource information about the effect/damage + * @return a list of affected entities + */ + private def forceDomeTargets( + radius: Float, + targetFaction: PlanetSideEmpire.Value + ) + ( + zone: Zone, + source: PlanetSideGameObject with Vitality, + damagePropertiesBySource: DamageWithPosition + ): List[PlanetSideServerObject with Vitality] = { + val sector = zone.blockMap.sector(source.Position.xy, radius) + val playerTargets = sector.livePlayerList.filterNot { _.VehicleSeated.nonEmpty } + //vehicles + val vehicleTargets = sector.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty } + //deployables + val deployableTargets = sector.deployableList.filterNot { _.Destroyed } + //altogether ... + (playerTargets ++ vehicleTargets ++ deployableTargets).filterNot(_.Faction == targetFaction) + } } /** @@ -172,17 +292,12 @@ class ForceDomeControl(dome: ForceDomePhysics) def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome def FactionObject: FactionAffinity = dome - private var perimeterSegments: List[(Vector3, Vector3)] = Nil - private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building] private var customState: Option[Boolean] = None def commonBehavior: Receive = checkBehavior .orElse { - case Service.Startup() => - setupPerimeter() - case ForceDomeControl.CustomExpand if !dome.Energized && (customState.isEmpty || customState.contains(false)) => customState = Some(true) @@ -209,7 +324,7 @@ class ForceDomeControl(dome: ForceDomePhysics) if customState.nonEmpty => customState = None ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) - alignForceDomeStatusAndUpdate(domeOwnerAsABuilding) + ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome) } def poweredStateLogic: Receive = { @@ -217,7 +332,7 @@ class ForceDomeControl(dome: ForceDomePhysics) .orElse(captureTerminalAwareBehaviour) .orElse { case BuildingActor.AlertToFactionChange(_) => - blockedByCustomStateOr(alignForceDomeStatusAndUpdate, domeOwnerAsABuilding) + blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatusAndUpdate) case _ => () } @@ -231,83 +346,53 @@ class ForceDomeControl(dome: ForceDomePhysics) } def powerTurnOffCallback() : Unit = { - if (dome.Energized && customState.isEmpty) { - ForceDomeControl.ChangeDomeEnergizedState(dome,activationState = false) - } + deenergizeUnlessSuppressedDueToCustomState() } def powerTurnOnCallback() : Unit = { - blockedByCustomStateOr(alignForceDomeStatus, domeOwnerAsABuilding) + blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatus) } override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = { super.captureTerminalIsResecured(terminal) - blockedByCustomStateOr(alignForceDomeStatus, domeOwnerAsABuilding) + blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatus) } override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = { super.captureTerminalIsHacked(terminal) - if (dome.Energized && customState.isEmpty) { - ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false) - } + deenergizeUnlessSuppressedDueToCustomState() } - private def setupPerimeter(): Unit = { - //todo tech plants have an indent - if (perimeterSegments.isEmpty) { - perimeterSegments = ForceDomeControl.GeneralFacilityPerimeter(dome) + private def deenergizeUnlessSuppressedDueToCustomState(): Unit = { + if (dome.Energized) { + if (customState.isEmpty) { + ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false) + } else { + ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true) + } } } + /** + * na + * @param func function to run if not blocked + * @return next behavior for an actor state + */ + private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Unit): Unit = { + blockedByCustomStateOr(func, domeOwnerAsABuilding, dome) + } /** * na * @param func function to run if not blocked * @param building facility to operate upon (parameter to `func`) * @return next behavior for an actor state */ - private def blockedByCustomStateOr(func: Building => Unit, building: Building): Unit = { + private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Unit, building: Building, dome: ForceDomePhysics): Unit = { customState match { case None => - func(building) + func(building, dome) case Some(state) => ForceDomeControl.CustomDomeStateEnforcedMessage(building, state) } } - - /** - * Evaluate the conditions of the building - * and determine if its capitol force dome state should be updated - * to reflect the actual conditions of the base or its surrounding bases. - * If this building is considered a subcapitol facility to the zone's actual capitol facility, - * and has the capitol force dome has a dependency upon it, - * pass a message onto that facility that it should check its own state alignment. - * @param building the building being evaluated - */ - private def alignForceDomeStatusAndUpdate(building: Building): Unit = { - ForceDomeControl.CheckForceDomeStatus(building, dome).foreach { - updatedStatus => - if (updatedStatus != dome.Energized) { - ForceDomeControl.ChangeDomeEnergizedState(dome, updatedStatus) - dome.Owner.Actor ! BuildingActor.MapUpdate() - } - } - } - - /** - * Evaluate the conditions of the building - * and determine if its capitol force dome state should be updated - * to reflect the actual conditions of the base or its surrounding bases. - * If this building is considered a subcapitol facility to the zone's actual capitol facility, - * and has the capitol force dome has a dependency upon it, - * pass a message onto that facility that it should check its own state alignment. - * @param building the building being evaluated - */ - private def alignForceDomeStatus(building: Building): Unit = { - ForceDomeControl.CheckForceDomeStatus(building, dome).foreach { - updatedStatus => - if (updatedStatus != dome.Energized) { - ForceDomeControl.ChangeDomeEnergizedState(dome, updatedStatus) - } - } - } } diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 07e2b9d9f..a7b0c78d5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -122,7 +122,7 @@ class GeneratorControl(gen: Generator) queuedExplosion = Default.Cancellable imminentExplosion = false //hate on everything nearby - Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc) + Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc, Zone.findAllTargets) case GeneratorControl.Restored() => gen.ClearHistory() diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala index 0f87181bc..c7740c7da 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala @@ -37,7 +37,8 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont pad.Zone, pad, VehicleSpawnControlRailJack.prepareSpawnExplosion(pad, SourceEntry(driver), SourceEntry(vehicle)), - pad.Definition.killBox(pad, vehicle.Definition.CanFly) + pad.Definition.killBox(pad, vehicle.Definition.CanFly), + Zone.findAllTargets ) pad.Zone.VehicleEvents ! VehicleSpawnPad.AttachToRails(vehicle, pad) context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order) diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala index ab4d3499c..996b4d876 100644 --- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -18,6 +18,10 @@ import net.psforever.util.Config import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ +object ResourceSiloControl { + final case class DrainMultiplier(multiplier: Float) +} + /** * An `Actor` that handles messages being dispatched to a specific `ResourceSilo` entity. * @@ -30,7 +34,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) def FactionObject: FactionAffinity = resourceSilo private[this] val log = org.log4s.getLogger - var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation + private var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation + /** the higher the multiplier, the greater the drain */ + private var drainMultiplier: Float = 1.0f def receive: Receive = { case Service.Startup() => @@ -53,6 +59,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) checkBehavior .orElse(storageBehavior) .orElse { + case ResourceSiloControl.DrainMultiplier(multiplier) => + drainMultiplier = multiplier + case CommonMessages.Use(_, Some(vehicle: Vehicle)) if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) => val siloFaction = resourceSilo.Faction @@ -171,7 +180,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo) */ def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = { val originalAmount = resourceSilo.NtuCapacitor - UpdateChargeLevel(-min) + UpdateChargeLevel(-min * drainMultiplier) sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor) } diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index e48338c59..b47182a6f 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -40,7 +40,6 @@ import net.psforever.objects.geometry.d3.VolumetricGeometry import net.psforever.objects.guid.pool.NumberPool import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.dome.{ForceDomeDefinition, ForceDomePhysics} import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.environment.EnvironmentAttribute import net.psforever.objects.serverobject.interior.{InteriorAware, Sidedness} @@ -1639,13 +1638,6 @@ object Zone { case painbox: Painbox => painbox.Actor ! Service.Startup() } - //capitol facilities have force domes - buildings.values - .flatMap(_.Amenities.filter(_.Definition.isInstanceOf[ForceDomeDefinition])) - .collect { - case obj: ForceDomePhysics => - obj.Actor ! Service.Startup() - } //the orbital_buildings in sanctuary zones have to establish their shuttle routes map.shuttleBays .map { @@ -1817,6 +1809,29 @@ object Zone { /* explosions */ + /** + * 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 + * @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, + createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction + ): List[PlanetSideServerObject] = { + source.Definition.innateDamage match { + case Some(damage) => + serverSideDamage(zone, source, damage, createInteraction, distanceCheck, findAllTargets) + 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. @@ -1824,8 +1839,10 @@ object Zone { * @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 + * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage; + * filters targets from the existing selection + * @param acquireTargetsFromZone the main target-collecting algorithm; + * collects targets from sector information * @return a list of affected entities; * only mostly complete due to the exclusion of objects whose damage resolution is different than usual */ @@ -1833,8 +1850,8 @@ object Zone { zone: Zone, source: PlanetSideGameObject with FactionAffinity with Vitality, createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction, - testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck, - acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality] = findAllTargets + testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean, + acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality] ): List[PlanetSideServerObject] = { source.Definition.innateDamage match { case Some(damage) => @@ -1859,8 +1876,10 @@ object Zone { * @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 + * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage; + * filters targets from the existing selection + * @param acquireTargetsFromZone the main target-collecting algorithm; + * collects targets from sector information * @return a list of affected entities; * only mostly complete due to the exclusion of objects whose damage resolution is different than usual */ From 4b3f8ea6c054e96892c0da174707c6d3a77610e6 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 18 Dec 2025 19:29:06 -0500 Subject: [PATCH 04/13] the force dome exhibits a perimeter in which enemies will be destroyed when it energizes; the facility generator will become undestroyable when the force dome is energized --- .../zone/building/MajorFacilityLogic.scala | 11 +- .../GlobalDefinitionsMiscellaneous.scala | 40 +++ .../serverobject/damage/Damageable.scala | 32 ++- .../serverobject/dome/ForceDomeControl.scala | 250 +++++++++++------- .../dome/ForceDomeDefinition.scala | 12 + 5 files changed, 245 insertions(+), 100 deletions(-) diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala index 00325776c..24825d7f4 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -6,6 +6,7 @@ import akka.actor.typed.{ActorRef, Behavior} import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import net.psforever.actors.commands.NtuCommand import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor} +import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl} import net.psforever.objects.serverobject.resourcesilo.ResourceSiloControl @@ -107,9 +108,12 @@ case object MajorFacilityLogic } // No map update needed - will be sent by `HackCaptureActor` when required case dome: ForceDomePhysics => + val building = details.building // The force dome being expanded modifies the NTU drain rate val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome)) - details.building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier)) + building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier)) + // The force dome being expanded marks the generator as being invulnerable; it can be damaged otherwise + building.Generator.foreach { _.Actor ! Damageable.Vulnerability(dome.Energized) } case _ => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) } @@ -373,7 +377,10 @@ case object MajorFacilityLogic ): Float = { // The force dome being expanded means all repairs are essentially for free dome - .map { case d if d.Energized => 0f } + .flatMap { + case d if d.Energized => Some(0f) + case _ => None + } .orElse { mainTerminal.flatMap { _ => Some(2f) } //todo main terminal and viruses } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index 9714631bd..6a5e90721 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -959,17 +959,57 @@ object GlobalDefinitionsMiscellaneous { force_dome_amp_physics.Name = "force_dome_amp_physics" force_dome_amp_physics.UseRadius = 142.26f + force_dome_amp_physics.PerimeterOffsets = List( + Vector3(83.05469f, 114.875f, 0f), + Vector3(-90.328125f, 114.875f, 0f), + Vector3(-90.328125f, -106.90625f, 0f), + Vector3(83.05469f, -106.90625f, 0f) + ) force_dome_comm_physics.Name = "force_dome_comm_physics" force_dome_comm_physics.UseRadius = 121.8149f + force_dome_comm_physics.PerimeterOffsets = List( + Vector3(35.1875f, -89.859375f, 0f), + Vector3(80.96875f, -43.773438f, 0f), + Vector3(80.96875f, 91.08594f, 0f), + Vector3(-37.296875f, 91.08594f, 0f), + Vector3(-83.640625f, 45.601562f, 0f), + Vector3(-83.640625f, -89.859375f, 0f) + ) force_dome_cryo_physics.Name = "force_dome_cryo_physics" force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f + force_dome_cryo_physics.PerimeterOffsets = List( + Vector3(72.75476f, 39.902725f, 0), + Vector3(24.505968f, 88.03482f, 0), + Vector3(-74.73426f, 88.03482f, 0), + Vector3(-74.73426f, -103.47f, 0), + Vector3(72.75476f, -103.47f, 0) + ) force_dome_dsp_physics.Name = "force_dome_dsp_physics" force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f + force_dome_dsp_physics.PerimeterOffsets = List( + Vector3(35.03125f, -93.25f, 0f), + Vector3(-83.1875f, -93.25f, 0f), + Vector3(-83.1875f, 114.515625f, 0f), + Vector3(-12.109375f, 188.26562f, 0f), + Vector3(130.44531f, 188.26562f, 0f), + Vector3(130.44531f, -93.28125f, 0f) + ) force_dome_tech_physics.Name = "force_dome_tech_physics" force_dome_tech_physics.UseRadius = 150.1284f + force_dome_tech_physics.PerimeterOffsets = List( //todo double-check eisa, esamir + Vector3(130.14636f, -95.20665f, 0f), + Vector3(130.14636f, 34.441734f, 0f), + Vector3(103.98575f, 52.58408f, 0f), + Vector3(16.405174f, 54.746464f, 0f), + Vector3(14.256668f, 107.01521f, 0f), + Vector3(-92.08687f, 107.01521f, 0f), + Vector3(-92.08687f, -96.176155f, 0f), + Vector3(-73.64424f, -114.65837f, 0f), + Vector3(102.12191f, -114.65837f, 0f) + ) } } 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 7f1863ffb..b6da16d55 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -17,7 +17,6 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations * All of these should be affected by the damage where applicable. */ trait Damageable { - /** * Contextual access to the object being the target of this damage. * Needs declaration in lowest implementing code. @@ -25,19 +24,34 @@ trait Damageable { */ def DamageableObject: Damageable.Target + /** a local `canDamage` flag */ + private var isVulnerable: Boolean = true + /** the official mixin hook; * `orElse` onto the "control" `Actor` `receive`; or, * cite the `originalTakesDamage` protocol during inheritance overrides */ val takesDamage: Receive = { + case Damageable.MakeVulnerable => + isVulnerable = false + + case Damageable.MakeInvulnerable => + isVulnerable = true + case Vitality.Damage(damage_func) => val obj = DamageableObject - if (obj.CanDamage) { + if (isVulnerable && obj.CanDamage) { PerformDamage(obj, damage_func) } } /** a duplicate of the core implementation for the default mixin hook, for use in overriding */ final val originalTakesDamage: Receive = { + case Damageable.MakeVulnerable => + isVulnerable = false + + case Damageable.MakeInvulnerable => + isVulnerable = true + case Vitality.Damage(damage_func) => val obj = DamageableObject if (obj.CanDamage) { @@ -67,6 +81,20 @@ object Damageable { */ final val LogChannel: String = "DamageResolution" + trait PersonalVulnerability + + final case object MakeVulnerable extends PersonalVulnerability + + final case object MakeInvulnerable extends PersonalVulnerability + + def Vulnerability(state: Boolean): PersonalVulnerability = { + if (state) { + MakeInvulnerable + } else { + MakeVulnerable + } + } + /** * Does the possibility exist that the designated target can be affected by this projectile's damage? * @see `Hackable` diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index 12c438043..4ee8e7261 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -7,7 +7,6 @@ import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} -import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.etc.ForceDomeExposure @@ -19,6 +18,8 @@ import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGeneratorState, Vector3} +import scala.annotation.unused + object ForceDomeControl { trait Command @@ -30,7 +31,7 @@ object ForceDomeControl { /** * Dispatch a message to update the state of the clients with the server state of the capitol force dome. - * @param dome force dome + * @param dome force dome * @param activationState new force dome status */ def ChangeDomeEnergizedState(dome: ForceDomePhysics, activationState: Boolean): Unit = { @@ -49,7 +50,7 @@ object ForceDomeControl { * use the faction affinity, the generator status, and the resource silo's capacitance level * to determine if the capitol force dome should be active. * @param building building being evaluated - * @param dome force dome + * @param dome force dome * @return the condition of the capitol force dome; * `None`, if the facility is not a capitol building; * `Some(true|false)` to indicate the state of the force dome @@ -78,7 +79,7 @@ object ForceDomeControl { * for capitol force dome expansion. * @param building target building * @return `true`, if the conditions for capitol force dome are not met; - * `false`, otherwise + * `false`, otherwise */ def InvalidBuildingCapitolForceDomeConditions(building: Building): Boolean = { building.Faction == PlanetSideEmpire.NEUTRAL || @@ -87,59 +88,30 @@ object ForceDomeControl { } /** - * na + * Apply a fixed point and a rotation value to a series of vertex offsets, + * then daisy-chain the resulting vertices in such a way that + * it creates a perimeter around the (building) owner of the capitol force dome. + * The resulting capitol force dome barrier is a blocky pyramoid shape. * @param dome force dome - * @return na + * @return perimeter of the force dome barrier */ - def GeneralFacilityPerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = { - val generatorTowerCenter = dome.Position.xy - val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) - val pointsOfForceDomePerimeter = turretPoints.map { p => - val segmentFromTowerToTurret = p - generatorTowerCenter - Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset + def SetupForceDomePerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = { + val center = dome.Position.xy + val rotation = math.toRadians(dome.Owner.Orientation.z).toFloat + val perimeterOffsets = dome.Definition.PerimeterOffsets + val perimeterPoints = perimeterOffsets.map { + center + Vector3.PlanarRotateAroundPoint(_, Vector3(0, 0, 1), rotation) } - pointsOfForceDomePerimeter - .flatMap { point => - pointsOfForceDomePerimeter - .sortBy(p => Vector3.DistanceSquared(p, point)) - .slice(1, 3) - .map { otherPoint => - if (point.y > otherPoint.y || point.x < otherPoint.x) { - (point, otherPoint) - } else { - (otherPoint, point) - } - } - } - .distinct - } - - import scala.annotation.unused - def TechPlantFacilityPerimeter(@unused dome: ForceDomePhysics): List[(Vector3, Vector3)] = { -// val generatorTowerCenter = dome.Position.xy -// val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) -// val organizedByClosestToGarage = dome -// .Owner -// .Amenities -// .find(_.Definition.Name.equals("gr_door_garage_ext")) -// .map { garage => -// val doorPosition = garage.Position.xy -// turretPoints.sortBy(point => Vector3.DistanceSquared(doorPosition, point)) -// } -// .getOrElse(List[Vector3]()) -// -// //val turretPoints = dome.Owner.Amenities.filter(_.isInstanceOf[FacilityTurret]).map(_.Position.xy) -// val pointsOfForceDomePerimeter = turretPoints.map { p => -// val segmentFromTowerToTurret = p - generatorTowerCenter -// Vector3.Unit(segmentFromTowerToTurret) * (Vector3.Magnitude(segmentFromTowerToTurret) + 20) //todo get correct distance offset -// } - Nil + ((0 until perimeterPoints.size - 1).map { index => + (perimeterPoints(index), perimeterPoints(index + 1)) + } :+ (perimeterPoints.last, perimeterPoints.head)).toList } /** - * na + * The capitol force dome should have changed states but it will not! + * Make certain everyone knows! * @param building target building - * @param state na + * @param state whether the force dome is energized or not */ def CustomDomeStateEnforcedMessage( building: Building, @@ -156,7 +128,8 @@ object ForceDomeControl { } /** - * na + * The capitol force dome will start changing states normally. + * Make certain everyone knows. * @param building facility */ def NormalDomeStateMessage(building: Building): Unit = { @@ -179,19 +152,19 @@ object ForceDomeControl { * pass a message onto that facility that it should check its own state alignment. * @param building facility with `dome` */ - def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Unit = { - CheckForceDomeStatus(building, dome).foreach { - case true => - if (!dome.Energized) { - ChangeDomeEnergizedState(dome, activationState = true) - ForceDomeKills(dome) - dome.Owner.Actor ! BuildingActor.MapUpdate() - } - case false => - if (dome.Energized) { - ChangeDomeEnergizedState(dome, activationState = false) - dome.Owner.Actor ! BuildingActor.MapUpdate() - } + def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Boolean = { + val energizedState = dome.Energized + CheckForceDomeStatus(building, dome).exists { + case true if !energizedState => + dome.Owner.Actor ! BuildingActor.MapUpdate() + ChangeDomeEnergizedState(dome, activationState = true) + true + case false if energizedState => + ChangeDomeEnergizedState(dome, activationState = false) + dome.Owner.Actor ! BuildingActor.MapUpdate() + false + case _ => + energizedState } } @@ -204,44 +177,44 @@ object ForceDomeControl { * pass a message onto that facility that it should check its own state alignment. * @param building facility with `dome` */ - private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Unit = { - CheckForceDomeStatus(building, dome).foreach { - case true => - if (!dome.Energized) { - ChangeDomeEnergizedState(dome, activationState = true) - ForceDomeKills(dome) - } - case false => - if (dome.Energized) { - ChangeDomeEnergizedState(dome, activationState = false) - } + private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Boolean = { + val energizedState = dome.Energized + CheckForceDomeStatus(building, dome).exists { + case true if !energizedState => + ChangeDomeEnergizedState(dome, activationState = true) + true + case false if energizedState => + ChangeDomeEnergizedState(dome, activationState = false) + false + case _ => + energizedState } } /** * Being too close to the force dome can destroy targets if they do not match the faction alignment of the dome. - * This is the usual fate of opponents upon it being expanded (energeized). + * This is the usual fate of opponents upon it being expanded (energized). * @see `Zone.serverSideDamage` * @param dome force dome * @return a list of affected entities */ - def ForceDomeKills(dome: ForceDomePhysics): List[PlanetSideServerObject] = { + def ForceDomeKills(dome: ForceDomePhysics, perimeter: List[(Vector3, Vector3)]): List[PlanetSideServerObject] = { Zone.serverSideDamage( dome.Zone, dome, - contactWithForceDome, - Zone.distanceCheck, + makesContactWithForceDome, + targetUnderForceDome(perimeter), forceDomeTargets(dome.Definition.UseRadius, dome.Faction) ) } /** - * na + * Prepare damage information related to being caugt underneath the capitol force dome when it expands. * @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 */ - private def contactWithForceDome( + private def makesContactWithForceDome( source: PlanetSideGameObject with FactionAffinity with Vitality, target: PlanetSideGameObject with FactionAffinity with Vitality ): DamageInteraction = { @@ -254,6 +227,84 @@ object ForceDomeControl { /** * na + * @see `Zone.distanceCheck` + * @param segments ground-level perimeter of the force dome is defined by these segments (as vertex pairs) + * @param obj1 a game entity, should be the force dome + * @param obj2 a game entity, should be a damageable target of the force dome's wrath + * @param maxDistance ot applicable + * @return `true`, if target is detected within the force dome kill region + * `false`, otherwise + */ + private def targetUnderForceDome( + segments: List[(Vector3, Vector3)] + ) + ( + obj1: PlanetSideGameObject, + obj2: PlanetSideGameObject, + @unused maxDistance: Float + ): Boolean = { + val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position + val Vector3(targetX, targetY, targetZ) = obj2.Position - centerPos //deltas of segment of target to dome + val checkForIntersection = segments.exists { case (point1, point2) => + //want targets within the perimeter; if there's an intersection, target is outside of the perimeter + segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y) + } + !checkForIntersection && (targetZ < centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat)) + } + + /** + * A function to assist line segment intersection tests. + * The important frame of reference is checking whether a hypothetical segment between a point and a target + * intersects with an established line segment between two other points. + * For our purposes, the resulting line segments will never be collinear, so there is no reason to test that. + * @param pointX x-coordinate used to create a test segment + * @param pointY y-coordinate used to create a test segment + * @param targetX x-coordinate of an important point for a test segment + * @param targetY y-coordinate of an important point for a test segment + * @param segmentPoint1x x-coordinate of one point from a segment + * @param segmentPoint1y y-coordinate of one point from a segment + * @param segmentPoint2x x-coordinate of a different point from a segment + * @param segmentPoint2y y-coordinate of a different point from a segment + * @return `true`, if the points form into two segments that intersect; + * `false`, otherwise + */ + private def segmentIntersectionTestPerSegment( + pointX: Float, + pointY: Float, + targetX: Float, + targetY: Float, + segmentPoint1x: Float, + segmentPoint1y: Float, + segmentPoint2x: Float, + segmentPoint2y: Float + ): Boolean = { + //based on Franklin Antonio's "Faster Line Segment Intersection" topic "in Graphics Gems III" book (http://www.graphicsgems.org/) + //compare, java.awt.geom.Line2D.linesIntersect + val bx = segmentPoint1x - segmentPoint2x //delta-x of segment + val by = segmentPoint1y - segmentPoint2y //delta-y of segment + val cx = pointX - segmentPoint1x //delta-x of hypotenuse of triangle formed by center, segment endpoint, and intersection point + val cy = pointY - segmentPoint1y //delta-y of hypotenuse of triangle formed by center, segment endpoint, and intersection point + val alphaNumerator = by * cx - bx * cy + val commonDenominator = targetY * bx - targetX * by + val betaNumerator = targetX * cy - targetY * cx + if ( + commonDenominator > 0 && + (alphaNumerator < 0 || alphaNumerator > commonDenominator || betaNumerator < 0 || betaNumerator > commonDenominator) + ) { + false + } else if ( + commonDenominator < 0 && + (alphaNumerator > 0 || alphaNumerator < commonDenominator || betaNumerator > 0 || betaNumerator < commonDenominator) + ) { + false + } else { + //a collinear line test could go here, but we don't need it + true + } + } + + /** + * Collect all enemy players, vehicles, and combat engineering deployables in a sector. * @see `DamageWithPosition` * @see `Zone.blockMap.sector` * @param zone the zone in which the explosion should occur @@ -292,8 +343,13 @@ class ForceDomeControl(dome: ForceDomePhysics) def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome def FactionObject: FactionAffinity = dome + /** a capitol force dome's owner should always be a facility, preferably the capitol facility of the continent; + * to save time, casted this entity and cache it for repeated use once; + * force dome is not immediately owned (by correct facility) so delay determination */ private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building] - + /** ground-level perimeter of the force dome is defined by these segments (as vertex pairs) */ + private val perimeterSegments: List[(Vector3, Vector3)] = ForceDomeControl.SetupForceDomePerimeter(dome) + /** force the dome into a certain state regardless of what conditions would normally transition it into that state */ private var customState: Option[Boolean] = None def commonBehavior: Receive = checkBehavior @@ -325,6 +381,7 @@ class ForceDomeControl(dome: ForceDomePhysics) customState = None ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome) + ForceDomeControl.ForceDomeKills(dome, perimeterSegments) } def poweredStateLogic: Receive = { @@ -363,6 +420,10 @@ class ForceDomeControl(dome: ForceDomePhysics) deenergizeUnlessSuppressedDueToCustomState() } + /** + * Power down the force dome if it was previously being powered and + * as long as a custom state of being energized is not being enforced. + */ private def deenergizeUnlessSuppressedDueToCustomState(): Unit = { if (dome.Energized) { if (customState.isEmpty) { @@ -374,25 +435,22 @@ class ForceDomeControl(dome: ForceDomePhysics) } /** - * na + * Yield to a custom value enforcing a certain force dome state - energized or powered down. + * If the custom state is not declared, run the function and analyze any change in the force dome's natural state. * @param func function to run if not blocked - * @return next behavior for an actor state + * @return current energized state of the dome */ - private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Unit): Unit = { - blockedByCustomStateOr(func, domeOwnerAsABuilding, dome) - } - /** - * na - * @param func function to run if not blocked - * @param building facility to operate upon (parameter to `func`) - * @return next behavior for an actor state - */ - private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Unit, building: Building, dome: ForceDomePhysics): Unit = { + private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = { customState match { case None => - func(building, dome) + val newState = func(domeOwnerAsABuilding, dome) + if (!dome.Energized && newState) { + ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + } + newState case Some(state) => - ForceDomeControl.CustomDomeStateEnforcedMessage(building, state) + ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state) + state } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala index b362682e6..e5858502c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -2,8 +2,20 @@ package net.psforever.objects.serverobject.dome import net.psforever.objects.serverobject.structures.AmenityDefinition +import net.psforever.types.Vector3 class ForceDomeDefinition(objectId: Int) extends AmenityDefinition(objectId) { Name = "force_dome" + /** offsets that define the perimeter of the pyramidal force "dome" barrier; + * these points are the closest to where the dome interacts with the ground at a corner; + * should be sequential, either clockwise or counterclockwise */ + private var perimeter: List[Vector3] = List() + + def PerimeterOffsets: List[Vector3] = perimeter + + def PerimeterOffsets_=(points: List[Vector3]): List[Vector3] = { + perimeter = points + PerimeterOffsets + } } From dd0f5fc928fc41b9def3ec31dcfd7d4e55dcef67 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 22 Dec 2025 20:58:15 -0500 Subject: [PATCH 05/13] force domes should be on the zone blockmap; correct issue with force dome death; interaction that sets players found under the force dome to be invulnerable works --- .../scala/net/psforever/objects/Player.scala | 3 +- .../InteractWithForceDomeProtection.scala | 89 +++++++++++++++++++ .../serverobject/dome/ForceDomeControl.scala | 57 ++++++++---- .../dome/ForceDomeDefinition.scala | 23 +++++ .../serverobject/dome/ForceDomePhysics.scala | 9 ++ .../objects/vital/etc/ForceDomeExposure.scala | 4 +- 6 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index c0023ebb6..4cb25871e 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.avatar.interaction.{TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater} +import net.psforever.objects.avatar.interaction.{InteractWithForceDomeProtection, TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} @@ -51,6 +51,7 @@ class Player(var avatar: Avatar) interaction(new InteractWithMines(range = 10, TriggerOnPlayerRule)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) + interaction(new InteractWithForceDomeProtection()) private var backpack: Boolean = false private var released: Boolean = false diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala new file mode 100644 index 000000000..fc4062791 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala @@ -0,0 +1,89 @@ +// Copyright (c) 2025 PSForever +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} + +case object ForceZoneProtection extends ZoneInteractionType + +/** + * Entities under the capitol force dome that have not died in its initial activation + * do not take further damage until removed from under the dome or until the dome is deactivated. + */ +class InteractWithForceDomeProtection + extends ZoneInteraction { + def Type: ZoneInteractionType = ForceZoneProtection + + def range: Float = 10f + + /** increment to n, reevaluate the dome protecting the target, reset counter to 0 */ + private var protectSkipCounter: Int = 0 + /** dome currently protecting the target */ + private var protectedBy: Option[ForceDomePhysics] = None + + /** + * na + * @see `ForceDomeControl.TargetUnderForceDome` + * @param sector the portion of the block map being tested + * @param target the fixed element in this test + */ + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { + if (protectSkipCounter < 4) { + protectSkipCounter += 1 + } else { + protectSkipCounter = 0 + protectedBy match { + case Some(dome) + if dome.Perimeter.isEmpty || + target.Zone != dome.Zone || + !ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) => + resetInteraction(target) + case Some(_) => + () //no action + case None => + searchForInteractionCause(sector, target) + } + } + } + + /** + * Look the through the list of amenities in this sector for capitol force domes, + * determine which force domes are energized (activated, expanded, enveloping, etc.), + * and find the first active dome under which the target `entity` is positioned. + * The target `entity` is considered protected and can not be damaged until further notice. + * @see `Damageable.MakeInvulnerable` + * @see `ForceDomeControl.TargetUnderForceDome` + * @param sector – the portion of the block map being tested + * @param target – the fixed element in this test + * @return whichever force dome entity is detected to encircle this target `entity`, if any + */ + private def searchForInteractionCause(sector: SectorPopulation, target: InteractsWithZone): Option[ForceDomePhysics] = { + sector + .amenityList + .flatMap { + case dome: ForceDomePhysics if dome.Perimeter.nonEmpty => Some(dome) + case _ => None + } + .find { dome => + ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) + } + .map { dome => + protectedBy = Some(dome) + target.Actor ! Damageable.MakeInvulnerable + dome + } + } + + /** + * na + * @see `Damageable.MakeVulnerable` + * @param target the fixed element in this test + */ + def resetInteraction(target: InteractsWithZone): Unit = { + protectSkipCounter = 0 + protectedBy = None + target.Actor ! Damageable.MakeVulnerable + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index 4ee8e7261..8b66a5a1c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -29,6 +29,12 @@ object ForceDomeControl { final case object NormalBehavior extends Command + final case object ApplyProtection extends Command + + final case object RemoveProtection extends Command + + final case object Purge extends Command + /** * Dispatch a message to update the state of the clients with the server state of the capitol force dome. * @param dome force dome @@ -202,8 +208,9 @@ object ForceDomeControl { Zone.serverSideDamage( dome.Zone, dome, + ForceDomeExposure.damageProperties, makesContactWithForceDome, - targetUnderForceDome(perimeter), + TargetUnderForceDome(perimeter), forceDomeTargets(dome.Definition.UseRadius, dome.Faction) ) } @@ -235,14 +242,14 @@ object ForceDomeControl { * @return `true`, if target is detected within the force dome kill region * `false`, otherwise */ - private def targetUnderForceDome( - segments: List[(Vector3, Vector3)] - ) - ( - obj1: PlanetSideGameObject, - obj2: PlanetSideGameObject, - @unused maxDistance: Float - ): Boolean = { + def TargetUnderForceDome( + segments: List[(Vector3, Vector3)] + ) + ( + obj1: PlanetSideGameObject, + obj2: PlanetSideGameObject, + @unused maxDistance: Float + ): Boolean = { val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position val Vector3(targetX, targetY, targetZ) = obj2.Position - centerPos //deltas of segment of target to dome val checkForIntersection = segments.exists { case (point1, point2) => @@ -343,12 +350,12 @@ class ForceDomeControl(dome: ForceDomePhysics) def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome def FactionObject: FactionAffinity = dome - /** a capitol force dome's owner should always be a facility, preferably the capitol facility of the continent; - * to save time, casted this entity and cache it for repeated use once; - * force dome is not immediately owned (by correct facility) so delay determination */ + /** a capitol force dome's owner should always be a facility; + * to save time, cast this entity and cache it for repeated use once; + * force dome is not immediately owned by its correct facility so delay determination */ private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building] /** ground-level perimeter of the force dome is defined by these segments (as vertex pairs) */ - private val perimeterSegments: List[(Vector3, Vector3)] = ForceDomeControl.SetupForceDomePerimeter(dome) + private lazy val perimeterSegments: List[(Vector3, Vector3)] = ForceDomeControl.SetupForceDomePerimeter(dome) /** force the dome into a certain state regardless of what conditions would normally transition it into that state */ private var customState: Option[Boolean] = None @@ -382,6 +389,16 @@ class ForceDomeControl(dome: ForceDomePhysics) ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome) ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + + case ForceDomeControl.ApplyProtection + if dome.Energized => + dome.Perimeter = perimeterSegments + + case ForceDomeControl.RemoveProtection => + dome.Perimeter = List.empty + + case ForceDomeControl.Purge => + ForceDomeControl.ForceDomeKills(dome, perimeterSegments) } def poweredStateLogic: Receive = { @@ -437,15 +454,25 @@ class ForceDomeControl(dome: ForceDomePhysics) /** * Yield to a custom value enforcing a certain force dome state - energized or powered down. * If the custom state is not declared, run the function and analyze any change in the force dome's natural state. + * Apply changes to region represented as "bound" by the perimeter as indicated by a state change. * @param func function to run if not blocked * @return current energized state of the dome */ private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = { + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + customState match { case None => + val oldState = dome.Energized val newState = func(domeOwnerAsABuilding, dome) - if (!dome.Energized && newState) { - ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + if (!oldState && newState) { + //dome activating + context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.Purge) + context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection) + } else if (oldState && !newState) { + //dome de-activating + dome.Zone.blockMap.removeFrom(dome) } newState case Some(state) => diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala index e5858502c..e6df1ecdb 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -1,12 +1,16 @@ // Copyright (c) 2025 PSForever package net.psforever.objects.serverobject.dome +import net.psforever.objects.geometry.d3.{Sphere, VolumetricGeometry} import net.psforever.objects.serverobject.structures.AmenityDefinition +import net.psforever.objects.sourcing.SourceEntry import net.psforever.types.Vector3 class ForceDomeDefinition(objectId: Int) extends AmenityDefinition(objectId) { Name = "force_dome" + Geometry = ForceDomeDefinition.representBy + /** offsets that define the perimeter of the pyramidal force "dome" barrier; * these points are the closest to where the dome interacts with the ground at a corner; * should be sequential, either clockwise or counterclockwise */ @@ -19,3 +23,22 @@ class ForceDomeDefinition(objectId: Int) PerimeterOffsets } } + +object ForceDomeDefinition { + /** + * na + * @param o na + * @return na + */ + def representBy(o: Any): VolumetricGeometry = { + import net.psforever.objects.geometry.GeometryForm.invalidPoint + o match { + case fdp: ForceDomePhysics => + Sphere(fdp.Position, fdp.Definition.UseRadius) + case s: SourceEntry => + Sphere(s.Position, s.Definition.UseRadius) + case _ => + Sphere(invalidPoint, 1f) + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala index 88d62963c..2b2641537 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala @@ -10,6 +10,8 @@ class ForceDomePhysics(private val cfddef: ForceDomeDefinition) with CaptureTerminalAware { private var energized: Boolean = false + private var perimeter: List[(Vector3, Vector3)] = List() + def Energized: Boolean = energized def Energized_=(state: Boolean): Boolean = { @@ -17,6 +19,13 @@ class ForceDomePhysics(private val cfddef: ForceDomeDefinition) Energized } + def Perimeter: List[(Vector3, Vector3)] = perimeter + + def Perimeter_=(list: List[(Vector3, Vector3)]): List[(Vector3, Vector3)] = { + perimeter = list + Perimeter + } + def Definition: ForceDomeDefinition = cfddef } diff --git a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala index d09ddd880..557a7e061 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala @@ -5,7 +5,7 @@ import net.psforever.objects.sourcing.{AmenitySource, SourceEntry} import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} import net.psforever.objects.vital.base.{DamageReason, DamageResolution} import net.psforever.objects.vital.damage.DamageCalculations -import net.psforever.objects.vital.prop.DamageProperties +import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition} import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} /** @@ -52,7 +52,7 @@ object ForceDomeExposure { Model = SimpleResolutions.calculate } - final val damageProperties = new DamageProperties { + final val damageProperties = new DamageWithPosition { Damage0 = 99999 DamageToHealthOnly = true DamageToVehicleOnly = true From 94bd315354b2c40221c80ee6b9ce76b112cfb412 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 23 Dec 2025 14:51:25 -0500 Subject: [PATCH 06/13] wrote variable and method documentation; prepared interaction handlers for force domes for mountable (vehicle) targets --- .../scala/net/psforever/objects/Player.scala | 2 +- .../InteractWithForceDomeProtection.scala | 17 +++++-- .../serverobject/dome/ForceDomeControl.scala | 22 ++++++--- .../dome/ForceDomeDefinition.scala | 11 ++--- .../serverobject/dome/ForceDomePhysics.scala | 16 ++++-- ...ithForceDomeProtectionSeatedInEntity.scala | 36 ++++++++++++++ ...thForceDomeProtectionSeatedInVehicle.scala | 49 +++++++++++++++++++ .../objects/vital/etc/ForceDomeExposure.scala | 10 +--- .../scala/net/psforever/zones/Zones.scala | 6 +-- 9 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 4cb25871e..3681936fa 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -40,6 +40,7 @@ class Player(var avatar: Avatar) with InteriorAwareFromInteraction with AuraContainer with MountableEntity { + interaction(new InteractWithForceDomeProtection()) interaction(environment.interaction.InteractWithEnvironment(Seq( new WithEntrance(), new WithWater(avatar.name), @@ -51,7 +52,6 @@ class Player(var avatar: Avatar) interaction(new InteractWithMines(range = 10, TriggerOnPlayerRule)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) - interaction(new InteractWithForceDomeProtection()) private var backpack: Boolean = false private var released: Boolean = false 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 fc4062791..145c8514e 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala @@ -24,7 +24,9 @@ class InteractWithForceDomeProtection private var protectedBy: Option[ForceDomePhysics] = None /** - * na + * If the target is protected, do conditions allow it to remain protected? + * If the target was vulnerable, can it be protected? + * Five second pause between evaluations (0-3, wait; 4, test). * @see `ForceDomeControl.TargetUnderForceDome` * @param sector the portion of the block map being tested * @param target the fixed element in this test @@ -70,19 +72,24 @@ class InteractWithForceDomeProtection ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) } .map { dome => - protectedBy = Some(dome) - target.Actor ! Damageable.MakeInvulnerable + applyProtection(target, dome) dome } } + protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { + protectedBy = Some(dome) + target.Actor ! Damageable.MakeInvulnerable + } + /** - * na + * No longer invulnerable (if ever). + * Set the counter to force a reevaluation of the vulnerability state next turn. * @see `Damageable.MakeVulnerable` * @param target the fixed element in this test */ def resetInteraction(target: InteractsWithZone): Unit = { - protectSkipCounter = 0 + protectSkipCounter = 5 protectedBy = None target.Actor ! Damageable.MakeVulnerable } diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index 8b66a5a1c..28b665866 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -157,6 +157,8 @@ object ForceDomeControl { * and has the capitol force dome has a dependency upon it, * pass a message onto that facility that it should check its own state alignment. * @param building facility with `dome` + * @param dome force dome + * @return current state of the capitol force dome */ def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Boolean = { val energizedState = dome.Energized @@ -182,6 +184,8 @@ object ForceDomeControl { * and has the capitol force dome has a dependency upon it, * pass a message onto that facility that it should check its own state alignment. * @param building facility with `dome` + * @param dome force dome + * @return current state of the capitol force dome */ private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Boolean = { val energizedState = dome.Energized @@ -202,7 +206,8 @@ object ForceDomeControl { * This is the usual fate of opponents upon it being expanded (energized). * @see `Zone.serverSideDamage` * @param dome force dome - * @return a list of affected entities + * @param perimeter ground-level perimeter of the force dome is defined by these segments (as vertex pairs) + * @return list of affected entities */ def ForceDomeKills(dome: ForceDomePhysics, perimeter: List[(Vector3, Vector3)]): List[PlanetSideServerObject] = { Zone.serverSideDamage( @@ -222,9 +227,9 @@ object ForceDomeControl { * @return a `DamageInteraction` object */ private def makesContactWithForceDome( - source: PlanetSideGameObject with FactionAffinity with Vitality, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): DamageInteraction = { + source: PlanetSideGameObject with FactionAffinity with Vitality, + target: PlanetSideGameObject with FactionAffinity with Vitality + ): DamageInteraction = { DamageInteraction( SourceEntry(target), ForceDomeExposure(SourceEntry(source)), @@ -233,7 +238,11 @@ object ForceDomeControl { } /** - * na + * To be considered within a force dome, a target entity must satisfy two orientations + * where the second condition is one of two qualifications: + * 1. within an angular perimeter boundary, and + * 2a. below the base coordinate of the force dome or + * 2b. within a region above the base of the force dome represented by a literal "dome" (half of a sphere). * @see `Zone.distanceCheck` * @param segments ground-level perimeter of the force dome is defined by these segments (as vertex pairs) * @param obj1 a game entity, should be the force dome @@ -470,9 +479,6 @@ class ForceDomeControl(dome: ForceDomePhysics) //dome activating context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.Purge) context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection) - } else if (oldState && !newState) { - //dome de-activating - dome.Zone.blockMap.removeFrom(dome) } newState case Some(state) => diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala index e6df1ecdb..5f4d50450 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -26,19 +26,16 @@ class ForceDomeDefinition(objectId: Int) object ForceDomeDefinition { /** - * na - * @param o na - * @return na + * Transform a capitol force dome into a bounded geometric representation. + * @param o any entity from which to produce a geometric representation + * @return geometric representation */ def representBy(o: Any): VolumetricGeometry = { - import net.psforever.objects.geometry.GeometryForm.invalidPoint o match { case fdp: ForceDomePhysics => Sphere(fdp.Position, fdp.Definition.UseRadius) - case s: SourceEntry => - Sphere(s.Position, s.Definition.UseRadius) case _ => - Sphere(invalidPoint, 1f) + net.psforever.objects.geometry.GeometryForm.invalidPoint } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala index 2b2641537..1cd60c2a0 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala @@ -8,10 +8,20 @@ import net.psforever.types.Vector3 class ForceDomePhysics(private val cfddef: ForceDomeDefinition) extends Amenity with CaptureTerminalAware { + /** whether the dome is active or not */ private var energized: Boolean = false - + /** defined perimeter of this force dome on the floor; + * the walls created by this perimeter are angled inwards towards the facility, but that's not a consideration */ private var perimeter: List[(Vector3, Vector3)] = List() + override def Position: Vector3 = Owner.Position + + override def Position_=(vec: Vector3): Vector3 = Owner.Position + + override def Orientation: Vector3 = Owner.Orientation + + override def Orientation_=(vec: Vector3): Vector3 = Owner.Orientation + def Energized: Boolean = energized def Energized_=(state: Boolean): Boolean = { @@ -34,17 +44,15 @@ object ForceDomePhysics { /** * Instantiate and configure a `CapitolForceDome` object. - * @param pos position of the object in the zone's coordinate system * @param fddef specific type of force dome * @param id the unique id that will be assigned to this entity * @param context a context to allow the object to properly set up `ActorSystem` functionality * @return the `CapitolForceDome` object */ - def Constructor(pos: Vector3, fddef: ForceDomeDefinition)(id: Int, context: ActorContext): ForceDomePhysics = { + def Constructor(fddef: ForceDomeDefinition)(id: Int, context: ActorContext): ForceDomePhysics = { import akka.actor.Props val obj = new ForceDomePhysics(fddef) - obj.Position = pos obj.Actor = context.actorOf(Props(classOf[ForceDomeControl], obj), name = s"${fddef.Name}_$id") obj } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala new file mode 100644 index 000000000..fdbf6d081 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.serverobject.mount + +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 + +class InteractWithForceDomeProtectionSeatedInEntity +extends InteractWithForceDomeProtection { + override def range: Float = 30f + + override protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { + super.applyProtection(target, dome) + target + .asInstanceOf[Mountable] + .Seats + .values + .flatMap(_.occupants) + .foreach { occupant => + occupant.Actor ! Damageable.MakeInvulnerable + } + } + + override def resetInteraction(target: InteractsWithZone): Unit = { + super.resetInteraction(target) + target + .asInstanceOf[Mountable] + .Seats + .values + .flatMap(_.occupants) + .foreach { occupant => + occupant.Actor ! Damageable.MakeVulnerable + } + } +} diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala new file mode 100644 index 000000000..181ca2cfe --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala @@ -0,0 +1,49 @@ +// Copyright (c) 2025 PSForever +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 + +class InteractWithForceDomeProtectionSeatedInVehicle + extends InteractWithForceDomeProtectionSeatedInEntity { + override protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { + super.applyProtection(target, dome) + target + .asInstanceOf[Vehicle] + .CargoHolds + .values + .flatMap(_.occupants) + .foreach { vehicle => + vehicle + .interaction() + .find(_.Type == ForceZoneProtection) + .foreach { + case interaction: InteractWithForceDomeProtection => + interaction.applyProtection(vehicle, dome) + case _ => () + } + } + } + + override def resetInteraction(target: InteractsWithZone): Unit = { + super.resetInteraction(target) + target + .asInstanceOf[Vehicle] + .CargoHolds + .values + .flatMap(_.occupants) + .foreach { vehicle => + vehicle + .interaction() + .find(_.Type == ForceZoneProtection) + .foreach { + case interaction: InteractWithForceDomeProtection => + interaction.resetInteraction(vehicle) + case _ => () + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala index 557a7e061..d2aa5ce4d 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala @@ -22,25 +22,19 @@ final case class ForceDomeExposure(field: SourceEntry) } /** - * Want to blame the capitol facility that is being protected. + * Blame the capitol facility that is being protected. */ override def attribution: Int = field match { case a: AmenitySource => a.installation.Definition.ObjectId case _ => field.Definition.ObjectId } - /** - * A direct connection to the damage information, numbers and properties. - */ override def source: DamageProperties = ForceDomeExposure.damageProperties - /** - * The functionality that is necessary for interaction of a vital game object with the rest of the hostile game world. - */ override def damageModel: DamageAndResistance = ForceDomeExposure.drm /** - * The person to be blamed for this. + * No one person will be blamed for this. */ override def adversary: Option[SourceEntry] = None } diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 3682a9ad5..c94862623 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -392,12 +392,12 @@ object Zones { .foreach { forceDome => structures .find { structure => Building.Capitols.contains(structure.objectName) } - .foreach { capitol => + .foreach { structure => val definition = DefinitionUtil.fromString(forceDome.objectType).asInstanceOf[ForceDomeDefinition] zoneMap.addLocalObject( forceDome.guid, - ForceDomePhysics.Constructor(forceDome.position, definition), - owningBuildingGuid = capitol.guid + ForceDomePhysics.Constructor(definition), + owningBuildingGuid = structure.guid ) } } From 73f352490c03db10839591ca0deb38a3127a6be0 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 3 Jan 2026 10:30:52 -0500 Subject: [PATCH 07/13] force dome provides damage protection to certain amenities, e.g., the generator, the turrets, and any implant machines (cryo); force dome will also suspend hacking attempts under it's envelope, but counter-hacking (resecure) should still be possible; operated turret deployables gain protection while manned; turrets no longer share knowledge of each other's upgrade cycles --- .../session/csr/MountHandlerLogic.scala | 2 +- .../session/normal/MountHandlerLogic.scala | 2 +- .../zone/building/MajorFacilityLogic.scala | 11 +++- .../psforever/objects/TurretDeployable.scala | 3 +- .../scala/net/psforever/objects/Vehicle.scala | 3 +- .../InteractWithForceDomeProtection.scala | 2 +- .../GlobalDefinitionsMiscellaneous.scala | 7 ++- .../serverobject/dome/ForceDomeControl.scala | 12 ++-- .../dome/ForceDomeDefinition.scala | 14 ++++- .../hackable/GenericHackables.scala | 57 ++++++++++++------- ...ithForceDomeProtectionSeatedInEntity.scala | 2 +- .../capture/CaptureTerminalControl.scala | 2 +- .../terminals/capture/CaptureTerminals.scala | 52 ++++++++++++++++- .../serverobject/turret/FacilityTurret.scala | 22 +++++++ .../turret/FacilityTurretControl.scala | 2 +- .../serverobject/turret/WeaponTurrets.scala | 3 +- ...thForceDomeProtectionSeatedInVehicle.scala | 2 +- 17 files changed, 154 insertions(+), 44 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala index 4e9480d54..1965825ab 100644 --- a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala @@ -166,7 +166,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) - if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L => + if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L => obj.setMiddleOfUpgrade(false) sessionLogic.zoning.CancelZoningProcess() sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) 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 f7230008f..86e42e942 100644 --- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -181,7 +181,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) - if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L => + if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L => log.info(s"${player.Name} mounts the ${obj.Definition.Name}") obj.setMiddleOfUpgrade(false) sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala index 24825d7f4..c90579c55 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -109,11 +109,16 @@ case object MajorFacilityLogic // No map update needed - will be sent by `HackCaptureActor` when required case dome: ForceDomePhysics => val building = details.building - // The force dome being expanded modifies the NTU drain rate + // The protection of the force dome modifies the NTU drain rate val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome)) building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier)) - // The force dome being expanded marks the generator as being invulnerable; it can be damaged otherwise - building.Generator.foreach { _.Actor ! Damageable.Vulnerability(dome.Energized) } + // The protection of the force dome marks the generator (and some other amenities) as being invulnerable + val msg = Damageable.Vulnerability(dome.Perimeter.nonEmpty) + val applicable = dome.Definition.ApplyProtectionTo + building + .Amenities + .filter(amenity => applicable.contains(amenity.Definition)) + .foreach { _.Actor ! msg } case _ => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) } diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 17dd8dc3b..5d385f072 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.InteractWithRadiationCloudsSeatedInEntity +import net.psforever.objects.serverobject.mount.{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} @@ -36,6 +36,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition) HackDuration = Array(0, 20, 10, 5) if (tdef.Seats.nonEmpty) { + interaction(new InteractWithForceDomeProtectionSeatedInEntity) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f)) } diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index 3658294d5..122618c39 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.{TriggerOnVehicleRule, WithLava, WithWater} +import net.psforever.objects.vehicles.interaction.{InteractWithForceDomeProtectionSeatedInVehicle, TriggerOnVehicleRule, WithLava, WithWater} import net.psforever.objects.vital.resistance.StandardResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel @@ -94,6 +94,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) with AuraContainer with MountableEntity with InteriorAwareFromInteraction { + interaction(new InteractWithForceDomeProtectionSeatedInVehicle) interaction(environment.interaction.InteractWithEnvironment(Seq( new WithEntrance(), new WithWater(), 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 145c8514e..66b92688d 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala @@ -77,7 +77,7 @@ class InteractWithForceDomeProtection } } - protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { + def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { protectedBy = Some(dome) target.Actor ! Damageable.MakeInvulnerable } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index 6a5e90721..b85b9e872 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -965,6 +965,7 @@ object GlobalDefinitionsMiscellaneous { Vector3(-90.328125f, -106.90625f, 0f), Vector3(83.05469f, -106.90625f, 0f) ) + force_dome_amp_physics.ApplyProtectionTo = List(generator, manned_turret) force_dome_comm_physics.Name = "force_dome_comm_physics" force_dome_comm_physics.UseRadius = 121.8149f @@ -976,6 +977,7 @@ object GlobalDefinitionsMiscellaneous { Vector3(-83.640625f, 45.601562f, 0f), Vector3(-83.640625f, -89.859375f, 0f) ) + force_dome_comm_physics.ApplyProtectionTo = List(generator, manned_turret) force_dome_cryo_physics.Name = "force_dome_cryo_physics" force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f @@ -986,6 +988,7 @@ object GlobalDefinitionsMiscellaneous { Vector3(-74.73426f, -103.47f, 0), Vector3(72.75476f, -103.47f, 0) ) + force_dome_cryo_physics.ApplyProtectionTo = List(generator, implant_terminal_mech, manned_turret) force_dome_dsp_physics.Name = "force_dome_dsp_physics" force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f @@ -997,10 +1000,11 @@ object GlobalDefinitionsMiscellaneous { Vector3(130.44531f, 188.26562f, 0f), Vector3(130.44531f, -93.28125f, 0f) ) + force_dome_dsp_physics.ApplyProtectionTo = List(generator, manned_turret) force_dome_tech_physics.Name = "force_dome_tech_physics" force_dome_tech_physics.UseRadius = 150.1284f - force_dome_tech_physics.PerimeterOffsets = List( //todo double-check eisa, esamir + force_dome_tech_physics.PerimeterOffsets = List( //todo double-check, e.g., eisa, esamir Vector3(130.14636f, -95.20665f, 0f), Vector3(130.14636f, 34.441734f, 0f), Vector3(103.98575f, 52.58408f, 0f), @@ -1011,5 +1015,6 @@ object GlobalDefinitionsMiscellaneous { Vector3(-73.64424f, -114.65837f, 0f), Vector3(102.12191f, -114.65837f, 0f) ) + force_dome_tech_physics.ApplyProtectionTo = List(generator, manned_turret) } } diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index 28b665866..53bec2242 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -164,8 +164,8 @@ object ForceDomeControl { val energizedState = dome.Energized CheckForceDomeStatus(building, dome).exists { case true if !energizedState => - dome.Owner.Actor ! BuildingActor.MapUpdate() ChangeDomeEnergizedState(dome, activationState = true) + dome.Owner.Actor ! BuildingActor.MapUpdate() true case false if energizedState => ChangeDomeEnergizedState(dome, activationState = false) @@ -247,7 +247,7 @@ object ForceDomeControl { * @param segments ground-level perimeter of the force dome is defined by these segments (as vertex pairs) * @param obj1 a game entity, should be the force dome * @param obj2 a game entity, should be a damageable target of the force dome's wrath - * @param maxDistance ot applicable + * @param maxDistance not applicable * @return `true`, if target is detected within the force dome kill region * `false`, otherwise */ @@ -260,12 +260,12 @@ object ForceDomeControl { @unused maxDistance: Float ): Boolean = { val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position - val Vector3(targetX, targetY, targetZ) = obj2.Position - centerPos //deltas of segment of target to dome - val checkForIntersection = segments.exists { case (point1, point2) => + val Vector3(targetX, targetY, _) = obj2.Position.xy - centerPos.xy //deltas of segment of target to dome + lazy val checkForIntersection = segments.exists { case (point1, point2) => //want targets within the perimeter; if there's an intersection, target is outside of the perimeter segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y) } - !checkForIntersection && (targetZ < centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat)) + segments.nonEmpty && !checkForIntersection && (obj2.Position.z <= centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat)) } /** @@ -402,9 +402,11 @@ class ForceDomeControl(dome: ForceDomePhysics) case ForceDomeControl.ApplyProtection if dome.Energized => dome.Perimeter = perimeterSegments + dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome) case ForceDomeControl.RemoveProtection => dome.Perimeter = List.empty + dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome) case ForceDomeControl.Purge => ForceDomeControl.ForceDomeKills(dome, perimeterSegments) diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala index 5f4d50450..efca36455 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala @@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.dome import net.psforever.objects.geometry.d3.{Sphere, VolumetricGeometry} import net.psforever.objects.serverobject.structures.AmenityDefinition -import net.psforever.objects.sourcing.SourceEntry import net.psforever.types.Vector3 class ForceDomeDefinition(objectId: Int) @@ -22,6 +21,19 @@ class ForceDomeDefinition(objectId: Int) perimeter = points PerimeterOffsets } + + private var protects: List[AmenityDefinition] = List() + + def ApplyProtectionTo: List[AmenityDefinition] = protects + + def ApplyProtectionTo_=(protect: AmenityDefinition): List[AmenityDefinition] = { + ApplyProtectionTo_=(List(protect)) + } + + def ApplyProtectionTo_=(protect: List[AmenityDefinition]): List[AmenityDefinition] = { + protects = protect + ApplyProtectionTo + } } object ForceDomeDefinition { 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 5933beb0d..c7c598d8d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala +++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala @@ -2,11 +2,13 @@ package net.psforever.objects.serverobject.hackable import net.psforever.actors.zone.BuildingActor -import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} +import net.psforever.objects.serverobject.dome.ForceDomeControl +import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.packet.game.{GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import net.psforever.services.Service @@ -17,23 +19,7 @@ import scala.util.{Failure, Success} object GenericHackables { private val log = org.log4s.getLogger("HackableBehavior") - private var turretUpgradeTime: Long = System.currentTimeMillis() - private var turretUpgradeTimeSet: Boolean = false - def updateTurretUpgradeTime(): Long = { - turretUpgradeTime = System.currentTimeMillis() - turretUpgradeTimeSet = true - turretUpgradeTime - } - - // Used for checking the time without updating it - def getTurretUpgradeTime: Long = { - if (!turretUpgradeTimeSet) { - turretUpgradeTime = System.currentTimeMillis() - turretUpgradeTimeSet = true - } - turretUpgradeTime - } /** * na * @@ -79,7 +65,13 @@ object GenericHackables { * @return `true`, if the next cycle of progress should occur; * `false`, otherwise */ - def HackingTickAction(progressType: HackState1, hacker: Player, target: PlanetSideServerObject, tool_guid: PlanetSideGUID)( + def HackingTickAction( + progressType: HackState1, + hacker: Player, + target: PlanetSideServerObject, + tool_guid: PlanetSideGUID, + additionalCancellationTests: (PlanetSideServerObject, Player) => Boolean = ForceDomeProtectsFromHacking + )( progress: Float ): Boolean = { //hack state for progress bar visibility @@ -87,9 +79,7 @@ object GenericHackables { (HackState.Start, 0) } else if (progress >= 100L) { (HackState.Finished, 100) - } else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID) { - (HackState.Cancelled, 0) - } else if (target.isInstanceOf[CaptureTerminal] && EndHackProgress(target, hacker)) { + } else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID || additionalCancellationTests(target, hacker)) { (HackState.Cancelled, 0) } else { (HackState.Ongoing, progress.toInt) @@ -104,6 +94,31 @@ object GenericHackables { progressState != HackState.Cancelled } + /** + * The force dome prevents hacking if its protection has been declared over a capitol. + * Under normal circumstances, the dome will be visible in the sky at his point, + * blocking enemy encounter within its boundaries, + * so anything that can be hacked is on that boundary perimeter, + * or an alternate method of entry (Router) has been compromised. + * @see `ForceDomeControl.TargetUnderForceDome` + * @see `Sector` + * @param target the `Hackable` object that has been hacked + * @param hacker the player performing the action + * @return `true`, if the target is within boundary of a working force dome and thus protected; + * `false`, otherwise + */ + def ForceDomeProtectsFromHacking(target: PlanetSideServerObject, hacker: Player): Boolean = { + //explicitly allow friendly hacking which is typically clearing a hack + target.Faction != hacker.Faction && + (target match { + case obj: Amenity => obj.Owner.asInstanceOf[Building].ForceDome.toList + case obj: BlockMapEntity => target.Zone.blockMap.sector(obj).buildingList.flatMap(_.ForceDome) + case _ => List() + }) + .filter(_.Perimeter.nonEmpty) + .exists(dome => ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f)) + } + /** * The process of hacking an object is completed. * Pass the message onto the hackable object and onto the local events system. diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala index fdbf6d081..2dddc5ba0 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithForceDomeProtectionSeatedInEntity.scala @@ -10,7 +10,7 @@ class InteractWithForceDomeProtectionSeatedInEntity extends InteractWithForceDomeProtection { override def range: Float = 30f - override protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { + override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { super.applyProtection(target, dome) target .asInstanceOf[Mountable] diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala index f5dc91a72..fef28d51f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala @@ -29,7 +29,7 @@ class CaptureTerminalControl(terminal: CaptureTerminal) sender() ! CommonMessages.Progress( GenericHackables.GetHackSpeed(player, terminal), CaptureTerminals.FinishHackingCaptureConsole(terminal, player, unk = -1), - GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID) + GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID, CaptureTerminals.EndHackProgress) ) } diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala index 399d45d84..9f07bce14 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala @@ -1,13 +1,18 @@ +// Copyright (c) 2021 PSForever package net.psforever.objects.serverobject.terminals.capture import net.psforever.objects.Player -import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.hackable.GenericHackables +import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.sourcing.PlayerSource import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.types.PlanetSideEmpire +import scala.concurrent.duration._ import scala.util.{Failure, Success} -object CaptureTerminals {import scala.concurrent.duration._ +object CaptureTerminals { private val log = org.log4s.getLogger("CaptureTerminals") /** @@ -55,4 +60,47 @@ object CaptureTerminals {import scala.concurrent.duration._ log.warn(s"Hack message failed on target guid: ${target.GUID}") } } + + /** + * Check if the state of connected facilities has changed since the hack progress began. It accounts for a friendly facility + * on the other side of a warpgate as well in case there are no friendly facilities in the same zone + * @param target the `Hackable` object that has been hacked + * @param hacker the player performing the action + * @return `true`, if the hack should be ended; `false`, otherwise + */ + def EndHackProgress(target: PlanetSideServerObject, hacker: Player): Boolean = { + val building = target.asInstanceOf[CaptureTerminal].Owner.asInstanceOf[Building] + val hackerFaction = hacker.Faction + if (GenericHackables.ForceDomeProtectsFromHacking(target, hacker)) { + true + } else if (building.Faction == PlanetSideEmpire.NEUTRAL || + building.BuildingType == StructureType.Tower || + building.Faction == hackerFaction) { + false + } else { + val stopHackingCount = building.Neighbours match { + case Some(neighbors) => + neighbors.count { + case wg: WarpGate if wg.Faction == hackerFaction => + true + case wg: WarpGate => + val friendlyBaseOpt = for { + otherWg <- wg.Neighbours.flatMap(_.find(_.isInstanceOf[WarpGate])) + friendly <- otherWg.Neighbours.flatMap(_.collectFirst { case b: Building if !b.isInstanceOf[WarpGate] => b }) + } yield friendly + friendlyBaseOpt.exists { fb => + fb.Faction == hackerFaction && + !fb.CaptureTerminalIsHacked && + fb.NtuLevel > 0 + } + case b => + b.Faction == hackerFaction && + !b.CaptureTerminalIsHacked && + b.NtuLevel > 0 + } + case None => 0 + } + stopHackingCount == 0 + } + } } 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 28d975950..699fdccee 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala @@ -23,6 +23,28 @@ class FacilityTurret(tDef: FacilityTurretDefinition) WeaponTurret.LoadDefinition(turret = this) WhichSide = Sidedness.OutsideOf + private var turretUpgradeTime: Long = System.currentTimeMillis() + private var turretUpgradeTimeSet: Boolean = false + + def UpdateTurretUpgradeTime(): Long = { + turretUpgradeTime = System.currentTimeMillis() + turretUpgradeTimeSet = true + turretUpgradeTime + } + + // Used for checking the time without updating it + def CheckTurretUpgradeTime: Long = { + if (!turretUpgradeTimeSet) { + turretUpgradeTime = System.currentTimeMillis() + turretUpgradeTimeSet = true + } + turretUpgradeTime + } + + def FinishedTurretUpgradeReset(): Unit = { + turretUpgradeTimeSet = false + } + def TurretOwner: SourceEntry = { Seats .headOption diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 12be65269..01613cd27 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -100,7 +100,7 @@ class FacilityTurretControl(turret: FacilityTurret) seatNumber: Int, player: Player): Boolean = { super.mountTest(obj, seatNumber, player) && - (!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L) + (!TurretObject.isUpgrading || System.currentTimeMillis() - TurretObject.CheckTurretUpgradeTime >= 1500L) } override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = { diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala index 9dd64c311..03b8925ae 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala @@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.avatar.Certification import net.psforever.objects.ce.Deployable -import net.psforever.objects.serverobject.hackable.GenericHackables.updateTurretUpgradeTime import net.psforever.objects.{Player, Tool, TurretDeployable} import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, InventoryStateMessage} import net.psforever.services.Service @@ -83,7 +82,7 @@ object WeaponTurrets { } else if (turret.Destroyed) { (HackState.Cancelled, 0) } else { - updateTurretUpgradeTime() + turret.UpdateTurretUpgradeTime() (HackState.Ongoing, progress.toInt) } turret.Zone.AvatarEvents ! AvatarServiceMessage( 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 181ca2cfe..0a988f818 100644 --- a/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala @@ -9,7 +9,7 @@ import net.psforever.objects.zones.InteractsWithZone class InteractWithForceDomeProtectionSeatedInVehicle extends InteractWithForceDomeProtectionSeatedInEntity { - override protected def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { + override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = { super.applyProtection(target, dome) target .asInstanceOf[Vehicle] From ba266d0a3e7887bcd3c18287412fb24e3bbc1367 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sun, 4 Jan 2026 14:05:12 -0500 Subject: [PATCH 08/13] csr-level commands for force dome manipulation --- .../actors/session/csr/ChatLogic.scala | 37 +++++++++++++++++++ .../serverobject/dome/ForceDomeControl.scala | 5 ++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala index 55e947a8a..eee8cb565 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -8,6 +8,7 @@ import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, Sess import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Session, TurretDeployable} import net.psforever.objects.ce.{Deployable, DeployableCategory} import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.dome.ForceDomeControl import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.Building @@ -227,6 +228,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "sayspectator" => customCommandSpeakAsSpectator(params, message) case "setempire" => customCommandSetEmpire(params) case "weaponlock" => customCommandZoneWeaponUnlock(session, params) + case "forcedome" => customForceDomeCommand(session, params) case _ => // command was not handled sendResponse( @@ -509,6 +511,41 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext true } + private def customForceDomeCommand(session: Session, contents: Seq[String]): Boolean = { + //locate force dome + var postUsageMessage: Boolean = false + val locatedForceDomesInZone = session.zone.Buildings.values.flatMap(_.ForceDome) + if (locatedForceDomesInZone.nonEmpty) { + contents + .headOption + .map(_.toLowerCase()) + .collect { + case "on" | "o" | "" => + locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.CustomExpand) + case "off" | "of" => + locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.CustomCollapse) + case "protect" => + locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.ApplyProtection) + case "normal" => + locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.NormalBehavior) + case "purge" => + locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.Purge) + case "help" | "usage" => + postUsageMessage = true + case token => + sendResponse(ChatMsg(ChatMessageType.UNK_227, s"unknown command - $token")) + postUsageMessage = true + } + if (postUsageMessage) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "!forcedome [o[n]|of[f]|protect|normal|purge]")) + } + } else { + //no force domes in zone + sendResponse(ChatMsg(ChatMessageType.UNK_227, "no capitol force dome(s) detected in zone")) + } + true + } + private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = { stateOpt match { case None => diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index 53bec2242..ffbb7c745 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -396,8 +396,9 @@ class ForceDomeControl(dome: ForceDomePhysics) if customState.nonEmpty => customState = None ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding) - ForceDomeControl.AlignForceDomeStatusAndUpdate(domeOwnerAsABuilding, dome) - ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + if (!blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatusAndUpdate)) { + ForceDomeControl.ForceDomeKills(dome, perimeterSegments) + } case ForceDomeControl.ApplyProtection if dome.Energized => From 52dbe6a6495f84761ee8c339322d114401cc2fa9 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Fri, 9 Jan 2026 23:28:54 -0500 Subject: [PATCH 09/13] extending the force dome protection over a variety of entities in a different manner, with a focus on how to perform state reset (dismounting and dome protect end); completely refactored and reworked the self-reported zone interaction timer for vehicles; separated passenger seat mounting from gunner seat mounting --- .../session/normal/MountHandlerLogic.scala | 52 +++++++---- .../actors/session/normal/VehicleLogic.scala | 18 +++- .../actors/session/support/SessionData.scala | 5 + .../session/support/VehicleOperations.scala | 54 +++++++++++ .../session/support/ZoningOperations.scala | 2 +- .../psforever/objects/TurretDeployable.scala | 2 +- .../scala/net/psforever/objects/Vehicle.scala | 2 +- .../InteractWithForceDomeProtection.scala | 2 +- .../serverobject/damage/Damageable.scala | 10 +- .../hackable/GenericHackables.scala | 30 +++++- ...ithForceDomeProtectionSeatedInEntity.scala | 5 +- ...actWithRadiationCloudsSeatedInEntity.scala | 3 +- .../RadiationInMountableInteraction.scala | 2 +- .../implant/ImplantTerminalMech.scala | 3 +- .../serverobject/turret/FacilityTurret.scala | 2 +- .../objects/vehicles/CargoBehavior.scala | 24 +++-- .../control/CargoCarrierControl.scala | 11 ++- .../vehicles/control/VehicleControl.scala | 78 ++++++++++------ ...thForceDomeProtectionSeatedInVehicle.scala | 4 +- ...ctWithRadiationCloudsSeatedInVehicle.scala | 4 +- .../IndependentZoneInteraction.scala | 91 +++++++++++++++++++ 21 files changed, 322 insertions(+), 82 deletions(-) rename src/main/scala/net/psforever/objects/serverobject/mount/{ => interaction}/InteractWithForceDomeProtectionSeatedInEntity.scala (84%) rename src/main/scala/net/psforever/objects/serverobject/mount/{ => interaction}/InteractWithRadiationCloudsSeatedInEntity.scala (94%) rename src/main/scala/net/psforever/objects/serverobject/mount/{ => interaction}/RadiationInMountableInteraction.scala (69%) rename src/main/scala/net/psforever/objects/vehicles/{ => interaction}/InteractWithRadiationCloudsSeatedInVehicle.scala (89%) create mode 100644 src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala 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 +} From 26b70dbcd95a579f9bfdc138c766849c0ef1f4c0 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 12 Jan 2026 19:54:29 -0500 Subject: [PATCH 10/13] adjusted self-reporting zone interaction logic; allowed for status of mounting into turret entities; made routine mounting behavior callbacks --- .../objects/FieldTurretDeployable.scala | 21 +- .../mount/MountableBehavior.scala | 13 +- .../turret/FacilityTurretControl.scala | 1 - .../turret/MountableTurretControl.scala | 23 +- .../objects/sourcing/MountableEntry.scala | 6 + .../objects/sourcing/TurretSource.scala | 2 +- .../objects/sourcing/VehicleSource.scala | 2 +- .../objects/vehicles/control/BfrControl.scala | 33 +- .../control/DeployingVehicleControl.scala | 8 +- .../vehicles/control/VehicleControl.scala | 282 ++++++++---------- .../objects/vital/InGameHistory.scala | 57 ++-- .../objects/zones/exp/KillContributions.scala | 56 ++-- .../psforever/objects/zones/exp/Support.scala | 16 +- .../IndependentZoneInteraction.scala | 10 +- 14 files changed, 289 insertions(+), 241 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala diff --git a/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala b/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala index 1745f6476..be51a5cba 100644 --- a/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala @@ -9,12 +9,14 @@ import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.turret.{MountableTurret, WeaponTurrets} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.sourcing.SourceEntry -import net.psforever.objects.vital.{InGameActivity, ShieldCharge} +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, TurretSource} +import net.psforever.objects.vital.{DismountingActivity, InGameActivity, MountingActivity, ShieldCharge} import net.psforever.packet.game.HackState1 import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.PlanetSideGUID +import scala.annotation.unused + /** definition */ class FieldTurretDeployableDefinition(private val objectId: Int) @@ -70,6 +72,21 @@ class FieldTurretControl(turret: TurretDeployable) player: Player ): Boolean = MountableTurret.MountTest(TurretObject, player) + override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = { + super.mountActionResponse(user, mountPoint, seatNumber) + if (turret.PassengerInSeat(user).contains(0)) { + val vsrc = TurretSource(turret) + user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), turret.Zone.Number)) + } + } + + override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = { + super.dismountActionResponse(user, seatBeingDismounted) + if (!turret.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated + user.LogActivity(DismountingActivity(TurretSource(turret), PlayerSource(user), turret.Zone.Number)) + } + } + //make certain vehicles don't charge shields too quickly private def canChargeShields: Boolean = { val func: InGameActivity => Boolean = WithShields.LastShieldChargeOrDamage(System.currentTimeMillis(), turret.Definition) diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala index aca66da06..2bef37bf1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala @@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.types.BailType +import scala.annotation.unused import scala.collection.mutable trait MountableBehavior { @@ -45,13 +46,17 @@ trait MountableBehavior { case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) => user.VehicleSeated = obj.GUID usedMountPoint.put(user.Name, mount_point) - obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(user) + mountActionResponse(user, mount_point, seatNum) sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point)) case _ => sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point)) } } + def mountActionResponse(user: Player, @unused mountPoint: Int, @unused seatIndex: Int): Unit = { + MountableObject.Zone.actor ! ZoneActor.RemoveFromBlockMap(user) + } + protected def mountTest( obj: PlanetSideServerObject with Mountable, seatNumber: Int, @@ -87,7 +92,7 @@ trait MountableBehavior { val obj = MountableObject if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user, bail_type)) { user.VehicleSeated = None - obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position) + dismountActionResponse(user, seat_number) sender() ! Mountable.MountMessages( user, Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number)) @@ -98,6 +103,10 @@ trait MountableBehavior { } } + def dismountActionResponse(user: Player, @unused seatIndex: Int): Unit = { + MountableObject.Zone.actor ! ZoneActor.AddToBlockMap(user, MountableObject.Position) + } + protected def dismountTest( obj: Mountable with WorldEntity, seatNumber: Int, diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 01613cd27..24f1bdea4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -5,7 +5,6 @@ import net.psforever.objects.{GlobalDefinitions, Player, Tool} import net.psforever.objects.equipment.Ammo import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.Damageable -import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.repair.AmenityAutoRepair import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala index f347f6bc2..bc4f7bd16 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala @@ -4,6 +4,10 @@ package net.psforever.objects.serverobject.turret import net.psforever.objects.Player import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} +import net.psforever.objects.sourcing.{PlayerSource, TurretSource} +import net.psforever.objects.vital.{DismountingActivity, MountingActivity} + +import scala.annotation.unused trait MountableTurretControl extends TurretControl @@ -11,9 +15,22 @@ trait MountableTurretControl override def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable /** commonBehavior does not implement mountingBehavior; please do so when implementing */ - override def commonBehavior: Receive = - super.commonBehavior - .orElse(dismountBehavior) + override def commonBehavior: Receive = super.commonBehavior.orElse(dismountBehavior) + + override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = { + super.mountActionResponse(user, mountPoint, seatNumber) + if (TurretObject.PassengerInSeat(user).contains(0)) { + val vsrc = TurretSource(TurretObject) + user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), TurretObject.Zone.Number)) + } + } + + override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = { + super.dismountActionResponse(user, seatBeingDismounted) + if (!TurretObject.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated + user.LogActivity(DismountingActivity(TurretSource(TurretObject), PlayerSource(user), TurretObject.Zone.Number)) + } + } override protected def mountTest( obj: PlanetSideServerObject with Mountable, diff --git a/src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala b/src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala new file mode 100644 index 000000000..ae0c2f41e --- /dev/null +++ b/src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala @@ -0,0 +1,6 @@ +// Copyright (c) 2026 PSForever +package net.psforever.objects.sourcing + +trait MountableEntry { + def occupants: List[SourceEntry] +} diff --git a/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala b/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala index 1261cb281..0e2880e4f 100644 --- a/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala +++ b/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala @@ -18,7 +18,7 @@ final case class TurretSource( Orientation: Vector3, occupants: List[SourceEntry], unique: SourceUniqueness - ) extends SourceWithHealthEntry with SourceWithShieldsEntry { + ) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry { def Name: String = SourceEntry.NameFormat(Definition.Descriptor) def Health: Int = health def Shields: Int = shields diff --git a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala index 25f60b710..91617bd0f 100644 --- a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala +++ b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala @@ -33,7 +33,7 @@ final case class VehicleSource( occupants: List[SourceEntry], Modifiers: ResistanceProfile, unique: UniqueVehicle - ) extends SourceWithHealthEntry with SourceWithShieldsEntry { + ) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry { def Name: String = SourceEntry.NameFormat(Definition.Name) def Health: Int = health def Shields: Int = shields diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala index 9dbc3436d..bd1aeaa1d 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala @@ -20,6 +20,7 @@ import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types._ +import scala.annotation.unused import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -170,8 +171,21 @@ class BfrControl(vehicle: Vehicle) specialArmWeaponEquipManagement(item, slot, handiness) } - override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = { - super.dismountCleanup(seatBeingDismounted, player) + override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = { + super.mountActionResponse(user, mountPoint, seatNumber) + if (vehicle.Seats.values.exists(_.isOccupied)) { + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { + case Some(subsys) + if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) => + //if the shield is damaged, it does not turn on until the damaged is cleared + vehicleSubsystemMessages(subsys.changedMessages(vehicle)) + case _ => () + } + } + } + + override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = { + super.dismountActionResponse(user, seatBeingDismounted) if (!vehicle.Seats.values.exists(_.isOccupied)) { vehicle .Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { @@ -196,19 +210,6 @@ class BfrControl(vehicle: Vehicle) } } - override def mountCleanup(mount_point: Int, user: Player): Unit = { - super.mountCleanup(mount_point, user) - if (vehicle.Seats.values.exists(_.isOccupied)) { - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { - case Some(subsys) - if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) => - //if the shield is damaged, it does not turn on until the damaged is cleared - vehicleSubsystemMessages(subsys.changedMessages(vehicle)) - case _ => () - } - } - } - override def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = { if (msg.transaction_type == TransactionType.Loadout) { !vehicle.Jammed @@ -455,7 +456,7 @@ class BfrControl(vehicle: Vehicle) } } - def specialArmWeaponEquipManagement(item: Equipment, slot: Int, handiness: equipment.Hand): Unit = { + def specialArmWeaponEquipManagement(item: Equipment, slot: Int, @unused handiness: equipment.Hand): Unit = { if (item.Size == EquipmentSize.BFRArmWeapon && vehicle.VisibleSlots.contains(slot)) { val weapons = vehicle.Weapons //budget logic: the arm weapons are "next to each other" index-wise diff --git a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala index 85d567390..7f913cff4 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala @@ -5,7 +5,6 @@ import akka.actor.ActorRef import net.psforever.objects._ import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} -import net.psforever.objects.serverobject.mount.Mountable import net.psforever.types._ /** @@ -36,13 +35,10 @@ class DeployingVehicleControl(vehicle: Vehicle) */ override def commonDisabledBehavior : Receive = super.commonDisabledBehavior + .orElse(dismountBehavior) .orElse { - case msg : Deployment.TryUndeploy => + case msg: Deployment.TryUndeploy => deployBehavior.apply(msg) - - case msg @ Mountable.TryDismount(player, seat_num, _) => - dismountBehavior.apply(msg) - dismountCleanup(seat_num, player) } /** 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 d7b745a86..fb3bc6459 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -29,7 +29,7 @@ import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vehicles._ 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.vital.{DamagingActivity, DismountingActivity, InGameActivity, MountingActivity, ShieldCharge, SpawningActivity} import net.psforever.objects.zones._ import net.psforever.objects.zones.interaction.IndependentZoneInteraction import net.psforever.packet.PlanetSideGamePacket @@ -40,6 +40,7 @@ import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import scala.annotation.unused import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.util.Random @@ -104,7 +105,80 @@ class VehicleControl(vehicle: Vehicle) endAllCargoOperations() } + private val mountingFailureReasons: Receive = { + case Mountable.TryMount(user, mountPoint) + if vehicle.DeploymentState == DriveState.AutoPilot => + sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint)) + + case Mountable.TryMount(user, mountPoint) + if vehicle.Zone.blockMap.sector(vehicle).buildingList.exists { + case wg: WarpGate => + Vector3.DistanceSquared(vehicle.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2) + case _ => false + } && user.Carrying.contains(SpecialCarry.CaptureFlag) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint)) + } + + private val disountingFailureReasons: Receive = { + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.DeploymentState == DriveState.AutoPilot => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + // Issue 1133. Todo: There may be a better way to address the issue? + case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) && + (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true + case _ => false + }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) && + (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true + case _ => false + }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed + && GlobalDefinitions.isFlightVehicle(vehicle.Definition) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true + case _ if Random.nextInt(10) == 1 => false + case _ => true }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed + && GlobalDefinitions.isFlightVehicle(vehicle.Definition) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true + case _ if Random.nextInt(5) == 1 => false + case _ => true }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed + && GlobalDefinitions.isFlightVehicle(vehicle.Definition) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true + case _ if Random.nextInt(4) == 1 => false + case _ => true }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.isMoving(test = 1f) && bailType == BailType.Normal => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + } + def commonEnabledBehavior: Receive = checkBehavior + .orElse(mountingFailureReasons) + .orElse(mountBehavior) + .orElse(disountingFailureReasons) + .orElse(dismountBehavior) .orElse(attributeBehavior) .orElse(jammableBehavior) .orElse(takesDamage) @@ -122,79 +196,6 @@ class VehicleControl(vehicle: Vehicle) case Vehicle.Ownership(Some(player)) => GainOwnership(player) - case Mountable.TryMount(user, mountPoint) - if vehicle.DeploymentState == DriveState.AutoPilot => - sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint)) - - case Mountable.TryMount(user, mountPoint) - if vehicle.Zone.blockMap.sector(vehicle).buildingList.exists { - case wg: WarpGate => - Vector3.DistanceSquared(vehicle.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2) - case _ => false - } && user.Carrying.contains(SpecialCarry.CaptureFlag) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint)) - - case msg @ Mountable.TryMount(player, mount_point) => - mountBehavior.apply(msg) - mountCleanup(mount_point, player) - - // Issue 1133. Todo: There may be a better way to address the issue? - case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) && - (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true - case _ => false - }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) && - (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true - case _ => false - }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case Mountable.TryDismount(user, seat_num, bailType) - if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed - && GlobalDefinitions.isFlightVehicle(vehicle.Definition) - && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) - && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true - case _ if Random.nextInt(10) == 1 => false - case _ => true }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case Mountable.TryDismount(user, seat_num, bailType) - if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed - && GlobalDefinitions.isFlightVehicle(vehicle.Definition) - && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) - && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true - case _ if Random.nextInt(5) == 1 => false - case _ => true }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case Mountable.TryDismount(user, seat_num, bailType) - if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed - && GlobalDefinitions.isFlightVehicle(vehicle.Definition) - && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) - && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { - case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true - case _ if Random.nextInt(4) == 1 => false - case _ => true }) => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case Mountable.TryDismount(user, seat_num, bailType) - if vehicle.DeploymentState == DriveState.AutoPilot => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case Mountable.TryDismount(user, seat_num, bailType) - if vehicle.isMoving(test = 1f) && bailType == BailType.Normal => - sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) - - case msg @ Mountable.TryDismount(player, seat_num, _) => - dismountBehavior.apply(msg) - dismountCleanup(seat_num, player) - case CommonMessages.ChargeShields(amount, motivator) => chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }) @@ -215,7 +216,6 @@ class VehicleControl(vehicle: Vehicle) events ! VehicleServiceMessage(toChannel, VehicleAction.SendResponse(guid0, pkt)) } - case FactionAffinity.ConvertFactionAffinity(faction) => val originalAffinity = vehicle.Faction if (originalAffinity != (vehicle.Faction = faction)) { @@ -253,7 +253,7 @@ class VehicleControl(vehicle: Vehicle) AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result = true) ) - case _ => ; + case _ => () } } else { zone.AvatarEvents ! AvatarServiceMessage( @@ -294,11 +294,8 @@ class VehicleControl(vehicle: Vehicle) } def commonDisabledBehavior: Receive = checkBehavior + .orElse(dismountBehavior) .orElse { - case msg @ Mountable.TryDismount(user, seat_num, _) => - dismountBehavior.apply(msg) - dismountCleanup(seat_num, user) - case Vehicle.Deconstruct(time) => time match { case Some(delay) if vehicle.Definition.undergoesDecay => @@ -317,7 +314,7 @@ class VehicleControl(vehicle: Vehicle) final def Disabled: Receive = commonDisabledBehavior .orElse { - case _ => ; + case _ => () } def commonDeleteBehavior: Receive = checkBehavior @@ -333,7 +330,7 @@ class VehicleControl(vehicle: Vehicle) final def ReadyToDelete: Receive = commonDeleteBehavior .orElse { - case _ => ; + case _ => () } override protected def mountTest( @@ -351,37 +348,32 @@ class VehicleControl(vehicle: Vehicle) super.mountTest(obj, seatNumber, user) } - def mountCleanup(mount_point: Int, user: Player): Unit = { - vehicle.PassengerInSeat(user) match { - case Some(0) => //driver seat - val vsrc = VehicleSource(vehicle) - user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), vehicle.Zone.Number)) - //if the driver mount, change ownership if that is permissible for this vehicle - if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) { - //whatever vehicle was previously owned - vehicle.Zone.GUID(user.avatar.vehicle) match { - case Some(v: Vehicle) => - v.Actor ! Vehicle.Ownership(None) - case _ => - user.avatar.vehicle = None - } - GainOwnership(user) //gain new ownership - } else { - decaying = false - decayTimer.cancel() + override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = { + super.mountActionResponse(user, mountPoint, seatNumber) + val vsrc = VehicleSource(vehicle) + user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number)) + if (seatNumber == 0) { + //if the driver mount, change ownership if that is permissible for this vehicle + if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) { + //whatever vehicle was previously owned + vehicle.Zone.GUID(user.avatar.vehicle) match { + case Some(v: Vehicle) => + v.Actor ! Vehicle.Ownership(None) + case _ => + user.avatar.vehicle = None } - TryStopInteractionSelfReporting() - updateZoneInteractionProgressUI(user) - - case Some(seatNumber) => //literally any other seat - val vsrc = VehicleSource(vehicle) - user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number)) + GainOwnership(user) //gain new ownership + } else { decaying = false decayTimer.cancel() - StopInteractionSelfReporting() - updateZoneInteractionProgressUI(user) - - case None => () + } + TryStopInteractionSelfReporting() + updateZoneInteractionProgressUI(user) + } else { + decaying = false + decayTimer.cancel() + StopInteractionSelfReporting() + updateZoneInteractionProgressUI(user) } } @@ -393,51 +385,37 @@ class VehicleControl(vehicle: Vehicle) vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user) } - def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = { + override def dismountActionResponse(user: Player, @unused seatBeingDismounted: Int): Unit = { + super.dismountActionResponse(user, seatBeingDismounted) + user.LogActivity(DismountingActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number)) val obj = MountableObject - // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount - if (!obj.Seats(0).isOccupied) { + if (seatBeingDismounted == 0) { obj.Velocity = Some(Vector3.Zero) } - val allSeatsUnoccupied = !vehicle.Seats.values.exists(_.isOccupied) - val otherTests = TestToStartSelfReporting() - if (allSeatsUnoccupied && otherTests) { + if (TestToStartSelfReporting()) { StartInteractionSelfReporting() } - if (!obj.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated - user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number)) - //we were only owning the vehicle while we sat in its driver seat - val canBeOwned = obj.Definition.CanBeOwned - if (canBeOwned.contains(false) && seatBeingDismounted == 0) { - LoseOwnership() - } - //are we already decaying? are we unowned? is no one seated anywhere? - if (!decaying && - obj.Definition.undergoesDecay && - obj.OwnerGuid.isEmpty && - allSeatsUnoccupied) { - decaying = true - decayTimer = context.system.scheduler.scheduleOnce( - MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), - self, - VehicleControl.PrepareForDeletion() - ) - } + //we were only owning the vehicle while we sat in its driver seat + val canBeOwned = obj.Definition.CanBeOwned + if (canBeOwned.contains(false) && seatBeingDismounted == 0) { + LoseOwnership() + } + //are we already decaying? are we unowned? is no one seated anywhere? + if (!decaying && + obj.Definition.undergoesDecay && + obj.OwnerGuid.isEmpty && + !vehicle.Seats.values.exists(_.isOccupied)) { + decaying = true + decayTimer = context.system.scheduler.scheduleOnce( + MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), + self, + VehicleControl.PrepareForDeletion() + ) } } 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() - } + vehicle.MountedIn.isEmpty && !vehicle.Seats.values.exists(_.isOccupied) } def PrepareForDisabled(kickPassengers: Boolean) : Unit = { @@ -566,7 +544,7 @@ class VehicleControl(vehicle: Vehicle) VehicleAction.InventoryState2(Service.defaultPlayerGUID, box.GUID, iguid, box.Capacity) ) } - case _ => ; + case _ => () } } @@ -710,7 +688,7 @@ class VehicleControl(vehicle: Vehicle) VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, vguid) ) } - case _ => ; // No player seated + case _ => () // No player seated } } vehicle.CargoHolds.foreach { @@ -722,11 +700,11 @@ class VehicleControl(vehicle: Vehicle) // Instruct client to start bail dismount procedure self ! DismountVehicleCargoMsg(dguid, cargo.GUID, bailed = true, requestedByPassenger = false, kicked = false) } - case None => ; // No vehicle in cargo + case None => () // No vehicle in cargo } } } - case None => ; + case None => () } } else { log.warn( @@ -780,9 +758,7 @@ class VehicleControl(vehicle: Vehicle) override def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = { super.endCargoDismounting(carrierGuid) - val allSeatsUnoccupied = !vehicle.Seats.values.exists(_.isOccupied) - val otherTests = TestToStartSelfReporting() - if (allSeatsUnoccupied && otherTests) { + if (TestToStartSelfReporting()) { StartInteractionSelfReporting() } vehicle.Zone.GUID(carrierGuid) match { diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index 70f8b4193..6da32a780 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -4,7 +4,7 @@ package net.psforever.objects.vital import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource} +import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource} import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} @@ -79,44 +79,48 @@ final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amoun final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) extends GeneralActivity +trait TerminalUse { + def terminal: AmenitySource +} + final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) - extends GeneralActivity + extends GeneralActivity with TerminalUse final case class TelepadUseActivity(router: VehicleSource, telepad: DeployableSource, player: PlayerSource) extends GeneralActivity -sealed trait VehicleMountChange extends GeneralActivity { - def vehicle: VehicleSource +sealed trait MountChange extends GeneralActivity { + def mount: SourceEntry with MountableEntry def zoneNumber: Int } -sealed trait VehiclePassengerMountChange extends VehicleMountChange { +sealed trait PassengerMountChange extends MountChange { def player: PlayerSource } -sealed trait VehicleCargoMountChange extends VehicleMountChange { +sealed trait CargoMountChange extends MountChange { def cargo: VehicleSource } -final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int) - extends VehiclePassengerMountChange +final case class MountingActivity(mount: SourceEntry with MountableEntry, player: PlayerSource, zoneNumber: Int) + extends PassengerMountChange -final case class VehicleDismountActivity( - vehicle: VehicleSource, - player: PlayerSource, - zoneNumber: Int, - pairedEvent: Option[VehicleMountActivity] = None - ) extends VehiclePassengerMountChange +final case class DismountingActivity( + mount: SourceEntry with MountableEntry, + player: PlayerSource, + zoneNumber: Int, + pairedEvent: Option[MountingActivity] = None + ) extends PassengerMountChange -final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int) - extends VehicleCargoMountChange +final case class VehicleCargoMountActivity(mount: VehicleSource, cargo: VehicleSource, zoneNumber: Int) + extends CargoMountChange final case class VehicleCargoDismountActivity( - vehicle: VehicleSource, + mount: VehicleSource, cargo: VehicleSource, zoneNumber: Int, pairedEvent: Option[VehicleCargoMountActivity] = None - ) extends VehicleCargoMountChange + ) extends CargoMountChange final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity]) extends GeneralActivity { @@ -165,8 +169,8 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int) final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int) extends HealingActivity with SupportActivityCausedByAnother -final case class HealFromTerminal(term: AmenitySource, amount: Int) - extends HealingActivity +final case class HealFromTerminal(terminal: AmenitySource, amount: Int) + extends HealingActivity with TerminalUse final case class HealFromImplant(implant: ImplantType, amount: Int) extends HealingActivity @@ -180,7 +184,8 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int) final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int) extends RepairingActivity with SupportActivityCausedByAnother -final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity +final case class RepairFromTerminal(terminal: AmenitySource, amount: Int) + extends RepairingActivity with TerminalUse final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int) extends RepairingActivity @@ -251,24 +256,24 @@ trait InGameHistory { */ def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = { action match { - case Some(act: VehicleDismountActivity) if act.pairedEvent.isEmpty => + case Some(act: DismountingActivity) if act.pairedEvent.isEmpty => history - .findLast(_.isInstanceOf[VehicleMountActivity]) + .findLast(_.isInstanceOf[MountingActivity]) .collect { - case event: VehicleMountActivity if event.vehicle.unique == act.vehicle.unique => + case event: MountingActivity if event.mount.unique == act.mount.unique => history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act) } .orElse { history = history :+ act None } - case Some(act: VehicleDismountActivity) => + case Some(act: DismountingActivity) => history = history :+ act case Some(act: VehicleCargoDismountActivity) => history .findLast(_.isInstanceOf[VehicleCargoMountActivity]) .collect { - case event: VehicleCargoMountActivity if event.vehicle.unique == act.vehicle.unique => + case event: VehicleCargoMountActivity if event.mount.unique == act.mount.unique => history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act) } .orElse { diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala index 5a58f8b82..441d28f8c 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala @@ -4,8 +4,8 @@ package net.psforever.objects.zones.exp import akka.actor.ActorRef import net.psforever.objects.GlobalDefinitions import net.psforever.objects.avatar.scoring.{Kill, SupportActivity} -import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource} -import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity} +import net.psforever.objects.sourcing.{BuildingSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, UniquePlayer, VehicleSource} +import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, DismountingActivity, MountingActivity} import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -388,13 +388,15 @@ object KillContributions { the player should not get credit from being the vehicle owner in matters of transportation there are considerations of time and distance traveled before the kill as well */ - case out: VehicleDismountActivity - if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) + case out: DismountingActivity + if out.mount.isInstanceOf[VehicleSource] && + !ownershipFromMount(out.mount).contains(out.player.unique) && + out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) } .collect { - case (in: VehicleMountActivity, out: VehicleDismountActivity) - if in.vehicle.unique == out.vehicle.unique && - out.vehicle.Faction == out.player.Faction && + case (in: MountingActivity, out: DismountingActivity) + if in.mount.unique == out.mount.unique && + out.mount.Faction == out.player.Faction && /* considerations of time and distance transported before the kill */ @@ -407,7 +409,7 @@ object KillContributions { } } || { val sameZone = in.zoneNumber == out.zoneNumber - val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) + val distanceTransported = Vector3.DistanceSquared(in.mount.Position.xy, out.mount.Position.xy) val distanceMoved = { val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero) Vector3.DistanceSquared(killLocation, out.player.Position.xy) @@ -423,9 +425,9 @@ object KillContributions { } //apply dismountActivity - .groupBy { _.vehicle } - .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty => - val promotedOwner = PlayerSource(mount.owner.get, mount.Position) + .groupBy { _.mount } + .collect { case (mount, dismountsFromVehicle) if ownershipFromMount(mount).nonEmpty => + val promotedOwner = PlayerSource(ownershipFromMount(mount).get, mount.Position) val size = dismountsFromVehicle.size val time = dismountsFromVehicle.maxBy(_.time).time List((HotDropKillAssist(mount.Definition.ObjectId, 0), "hotdrop", promotedOwner)) @@ -457,6 +459,24 @@ object KillContributions { } } + /** + * Determine the owner of the entity based on information about the entity. + * @param mount mountable entity which can be owned + * @return the optional unique referential signature for the owner + */ + private def ownershipFromMount(mount: SourceEntry with MountableEntry): Option[UniquePlayer] = { + mount match { + case v: VehicleSource => + v.owner + case t: TurretSource => t.occupants.headOption.flatMap { + case p: PlayerSource => Some(p.unique) + case _ => None + } + case _ => + None + } + } + /** * Gather and reward specific in-game equipment use activity.
* na @@ -486,14 +506,14 @@ object KillContributions { val dismountActivity = history .collect { case out: VehicleCargoDismountActivity - if out.vehicle.owner.nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) + if ownershipFromMount(out.mount).nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) } .collect { case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity) - if in.vehicle.unique == out.vehicle.unique && - out.vehicle.Faction == out.cargo.Faction && - (in.vehicle.Definition == GlobalDefinitions.router || { - val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) + if in.mount.unique == out.mount.unique && + out.mount.Faction == out.cargo.Faction && + (in.mount.Definition == GlobalDefinitions.router || { + val distanceTransported = Vector3.DistanceSquared(in.mount.Position.xy, out.mount.Position.xy) val distanceMoved = { val killLocation = kill.info.adversarial .collect { adversarial => adversarial.attacker.Position.xy } @@ -513,7 +533,7 @@ object KillContributions { val promotedOwner = PlayerSource(mount.owner.get, mount.Position) val mountId = mount.Definition.ObjectId dismountsFromVehicle - .groupBy(_.vehicle) + .groupBy(_.mount) .map { case (vehicle, events) => val size = events.size val time = events.maxBy(_.time).time @@ -673,8 +693,6 @@ object KillContributions { ): Unit = { history .collect { - case h: HealFromTerminal => (h.term, h) - case r: RepairFromTerminal => (r.term, r) case t: TerminalUsedActivity => (t.terminal, t) } .groupBy(_._1.unique) diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index 1ad7ead6e..d9017e600 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -2,7 +2,7 @@ package net.psforever.objects.zones.exp import net.psforever.objects.sourcing.PlayerSource -import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleDismountActivity, VehicleMountActivity, VehicleMountChange, VitalityDefinition} +import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, DismountingActivity, MountingActivity, MountChange, VitalityDefinition} import net.psforever.types.{ExoSuitType, PlanetSideEmpire} import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel} @@ -82,7 +82,7 @@ object Support { val wornTime: mutable.HashMap[Int, Long] = mutable.HashMap[Int, Long]() var currentSuit: Int = initialExosuit.id var lastActTime: Long = history.head.time - var lastMountAct: Option[VehicleMountChange] = None + var lastMountAct: Option[MountChange] = None //collect history events that encompass changes to exo-suits and to mounting conditions history.collect { case suitChange: ExoSuitChange => @@ -93,7 +93,7 @@ object Support { ) currentSuit = suitChange.exosuit.id lastActTime = suitChange.time - case mount: VehicleMountActivity => + case mount: MountingActivity => updateEquippedEntry( currentSuit, mount.time - lastActTime, @@ -101,18 +101,18 @@ object Support { ) lastActTime = mount.time lastMountAct = Some(mount) - case dismount: VehicleDismountActivity + case dismount: DismountingActivity if dismount.pairedEvent.isEmpty => updateEquippedEntry( - dismount.vehicle.Definition.ObjectId, + dismount.mount.Definition.ObjectId, dismount.time - lastActTime, wornTime ) lastActTime = dismount.time lastMountAct = None - case dismount: VehicleDismountActivity => + case dismount: DismountingActivity => updateEquippedEntry( - dismount.vehicle.Definition.ObjectId, + dismount.mount.Definition.ObjectId, dismount.time - dismount.pairedEvent.get.time, wornTime ) @@ -125,7 +125,7 @@ object Support { .collect { mount => //dying in a vehicle is a reason to care about the last mount activity updateEquippedEntry( - mount.vehicle.Definition.ObjectId, + mount.mount.Definition.ObjectId, lastTime - mount.time, wornTime ) diff --git a/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala b/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala index ec84d39cc..9450ece1e 100644 --- a/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala +++ b/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala @@ -39,10 +39,15 @@ trait IndependentZoneInteraction { } } - def PerformSelfReportRunCheck(): Unit + def PerformSelfReportRunCheck(): Unit = { + if (TestToStartSelfReporting()) { + StartInteractionSelfReporting() + } else { + StopInteractionSelfReporting() + } + } final def StartInteractionSelfReporting(): Unit = { - org.log4s.getLogger("ZIT").info("starting timer") zoneInteractionTimer.cancel() zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay( 0.seconds, @@ -53,7 +58,6 @@ trait IndependentZoneInteraction { } final def StartInteractionSelfReporting(initialDelay: FiniteDuration): Unit = { - org.log4s.getLogger("ZIT").info("starting timer") zoneInteractionTimer.cancel() zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay( initialDelay, From 398b98514a59a50e331e858b7deeb4953596256f Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Wed, 14 Jan 2026 02:05:01 -0500 Subject: [PATCH 11/13] streamline pass on the vehicle operation and aaccountability --- .../CustomerServiceRepresentativeMode.scala | 25 ++--- .../actors/session/csr/VehicleLogic.scala | 96 +++++++++--------- .../actors/session/normal/VehicleLogic.scala | 97 ++++++++----------- .../actors/session/support/SessionData.scala | 6 +- .../session/support/VehicleOperations.scala | 35 ++++--- .../serverobject/mount/Mountable.scala | 11 +++ 6 files changed, 133 insertions(+), 137 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala index a6b17186b..f0ce6a974 100644 --- a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala @@ -8,7 +8,6 @@ import net.psforever.objects.serverobject.ServerObject import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.Zone -import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.packet.game.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage} import net.psforever.packet.game.objectcreate.RibbonBars import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -110,20 +109,22 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { private def keepAlivePersistanceCSR(): Unit = { val player = data.player - data.keepAlivePersistence() - topOffHealthOfPlayer(player) player.allowInteraction = false topOffHealthOfPlayer(player) - data.continent.GUID(data.player.VehicleSeated) - .collect { - case obj: PlanetSideGameObject with Vitality with BlockMapEntity => + data.zoning.spawn.interimUngunnedVehicle = None + data.keepAlivePersistence() + if (player.HasGUID) { + data.zoning.spawn.tryQueuedActivity() + data.turnCounterFunc(player.GUID) + data.continent + .GUID(player.VehicleSeated) + .collect { case obj: PlanetSideGameObject with Vitality => topOffHealth(obj) - data.updateBlockMap(obj, obj.Position) - obj - } - .getOrElse { - data.updateBlockMap(player, player.Position) - } + } + data.squad.updateSquad() + } else { + data.turnCounterFunc(PlanetSideGUID(0)) + } } private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala index 4307dce9f..bc02a8f55 100644 --- a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala @@ -11,6 +11,7 @@ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.interaction.InteractsWithZone import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -30,6 +31,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex /* packets */ def handleVehicleState(pkt: VehicleStateMessage): Unit = { + player.allowInteraction = false val VehicleStateMessage( vehicle_guid, unk1, @@ -46,23 +48,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) - sessionLogic.general.fallHeightTracker(pos.z) - if (obj.MountedIn.isEmpty) { - sessionLogic.updateBlockMap(obj, pos) - } topOffHealthOfPlayer() topOffHealth(obj) - player.Position = pos //convenient - if (obj.WeaponControlledFromSeat(0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient + val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { + case Some(v: Vehicle) => + (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) + case _ => + (pos, ang, vel, true) } - obj.Position = pos - obj.Orientation = ang - if (obj.MountedIn.isEmpty) { + if (notMountedState) { + sessionLogic.updateBlockMap(obj, position) if (obj.DeploymentState != DriveState.Deployed) { - obj.Velocity = vel + obj.Velocity = velocity } else { obj.Velocity = Some(Vector3.Zero) } @@ -74,20 +74,20 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } + player.Position = position //convenient + obj.Position = position + obj.Orientation = angle + // continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.VehicleState( player.GUID, vehicle_guid, unk1, - obj.Position, - ang, - obj.Velocity, - if (obj.isFlying) { - is_flying - } else { - None - }, + position, + angle, + velocity, + obj.Flying, unk6, unk7, wheels, @@ -96,8 +96,6 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ) ) sessionLogic.squad.updateSquad() - player.allowInteraction = false - obj.zoneInteractions() 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 @@ -113,6 +111,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex } def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { + player.allowInteraction = false val FrameVehicleStateMessage( vehicle_guid, unk1, @@ -132,34 +131,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) topOffHealthOfPlayer() topOffHealth(obj) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { case Some(v: Vehicle) => - sessionLogic.updateBlockMap(obj, pos) (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) case _ => (pos, ang, vel, true) } - player.Position = position //convenient - if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient - } - obj.Position = position - obj.Orientation = angle - obj.Velocity = velocity - // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) { - // //dev stuff goes here - // } - // else - // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) { - // //dev stuff goes here - // } - obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile if (notMountedState) { + sessionLogic.updateBlockMap(obj, position) if (obj.DeploymentState != DriveState.Kneeling) { + obj.Velocity = velocity if (is_airborne) { val flight = if (ascending_flight) flight_time else -flight_time obj.Flying = Some(flight) @@ -172,12 +158,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } - player.allowInteraction = false - obj.zoneInteractions() } else { obj.Velocity = None obj.Flying = None } + player.Position = position //convenient + obj.Position = position + obj.Orientation = angle + obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.FrameVehicleState( @@ -214,34 +202,40 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex } def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { + player.allowInteraction = false val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt val (o, tools) = sessionLogic.shooting.FindContainedWeapon - //is COSM our primary upstream packet? (o match { - case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) + case Some(mount: Mountable) => (mount, 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 (Some(obj: PlanetSideGameObject with Vitality), _) => + case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases + () + case (Some(entity: PlanetSideGameObject with Mountable with InteractsWithZone), Some(_)) => //COSM is our primary upstream packet + sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) topOffHealthOfPlayer() - topOffHealth(obj) - case _ => + topOffHealth(entity) + sessionLogic.squad.updateSquad() + case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details + sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) } - //the majority of the following check retrieves information to determine if we are in control of the child - tools.find { _.GUID == object_guid } match { + //in the following condition we are in control of the child + tools.find(_.GUID == object_guid) match { case None => - //todo: old warning; this state is problematic, but can trigger in otherwise valid instances + //old warning; this state is problematic, but can trigger in otherwise valid instances //log.warn( // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" //) - case Some(_) => - //TODO set tool orientation? - player.Orientation = Vector3(0f, pitch, yaw) + case Some(tool) => + val angle = Vector3(0f, pitch, yaw) + tool.Orientation = angle + player.Orientation = angle continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) 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 9d7d436fd..883fbfe94 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -49,18 +49,16 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) sessionLogic.general.fallHeightTracker(pos.z) - if (obj.MountedIn.isEmpty) { + val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { + case Some(v: Vehicle) => + (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) + case _ => + (pos, ang, vel, true) + } + if (notMountedState) { sessionLogic.updateBlockMap(obj, pos) - } - player.Position = pos //convenient - if (obj.WeaponControlledFromSeat(0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient - } - obj.Position = pos - obj.Orientation = ang - if (obj.MountedIn.isEmpty) { if (obj.DeploymentState != DriveState.Deployed) { - obj.Velocity = vel + obj.Velocity = velocity } else { obj.Velocity = Some(Vector3.Zero) } @@ -68,10 +66,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Flying = is_flying //usually Some(7) } obj.Cloaked = obj.Definition.CanCloak && is_cloaked + obj.zoneInteractions() } else { obj.Velocity = None obj.Flying = None } + player.Position = position //convenient + obj.Position = position + obj.Orientation = angle // continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -79,14 +81,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex player.GUID, vehicle_guid, unk1, - obj.Position, - ang, - obj.Velocity, - if (obj.isFlying) { - is_flying - } else { - None - }, + position, + angle, + velocity, + obj.Flying, unk6, unk7, wheels, @@ -95,10 +93,9 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ) ) sessionLogic.squad.updateSquad() - 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 + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //placing a "not driving" warning here may trigger as we are disembarking the vehicle case (_, Some(index)) => log.error( s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" @@ -133,30 +130,17 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) + sessionLogic.general.fallHeightTracker(pos.z) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { case Some(v: Vehicle) => - sessionLogic.updateBlockMap(obj, pos) (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) case _ => (pos, ang, vel, true) } - player.Position = position //convenient - if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient - } - obj.Position = position - obj.Orientation = angle - obj.Velocity = velocity - // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) { - // //dev stuff goes here - // } - // else - // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) { - // //dev stuff goes here - // } - obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile if (notMountedState) { + sessionLogic.updateBlockMap(obj, position) if (obj.DeploymentState != DriveState.Kneeling) { + obj.Velocity = velocity if (is_airborne) { val flight = if (ascending_flight) flight_time else -flight_time obj.Flying = Some(flight) @@ -169,11 +153,15 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } - VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(obj, player) + obj.zoneInteractions() } else { obj.Velocity = None obj.Flying = None } + player.Position = position //convenient + obj.Position = position + obj.Orientation = angle + obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.FrameVehicleState( @@ -196,8 +184,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ) sessionLogic.squad.updateSquad() 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 + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //placing a "not driving" warning here may trigger as we are disembarking the vehicle case (_, Some(index)) => log.error( s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" @@ -212,35 +200,36 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt val (o, tools) = sessionLogic.shooting.FindContainedWeapon - //is COSM our primary upstream packet? (o match { - case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) + case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player)) case _ => (None, None) }) match { case (None, _) | (_, None) => //error - we do not recognize being mounted or controlling anything, but what can we do about it? () - case (Some(_: Vehicle), Some(0)) => //no (see: VSM or FVSM for valid cases) + case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases () - case (Some(entity: PlanetSideGameObject with InteractsWithZone), Some(_)) => //yes - sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? + case (Some(entity: PlanetSideGameObject with Mountable with InteractsWithZone), Some(seatNumber)) => //COSM is our primary upstream packet + sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) - VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(entity, player) - case _ => //yes - sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? + VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(entity, seatNumber) + sessionLogic.squad.updateSquad() + case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details + sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) } - //the majority of the following check retrieves information to determine if we are in control of the child - tools.find { _.GUID == object_guid } match { + //in the following condition we are in control of the child + tools.find(_.GUID == object_guid) match { case None => - //todo: old warning; this state is problematic, but can trigger in otherwise valid instances + //old warning; this state is problematic, but can trigger in otherwise valid instances //log.warn( // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" //) - case Some(_) => - //TODO set tool orientation? - player.Orientation = Vector3(0f, pitch, yaw) + case Some(tool) => + val angle = Vector3(0f, pitch, yaw) + tool.Orientation = angle + player.Orientation = angle continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) 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 3e8405940..7e672cba6 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -466,12 +466,14 @@ class SessionData( zoning.spawn.interimUngunnedVehicle = None persist() if (player.HasGUID) { + zoning.spawn.tryQueuedActivity(player.Velocity) turnCounterFunc(player.GUID) continent .GUID(player.VehicleSeated) - .foreach { - VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(_, player) + .collect { case v: PlanetSideGameObject with Mountable => + VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(v, player) } + squad.updateSquad() } 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 a1fdd87ae..22a743c5e 100644 --- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala @@ -198,17 +198,23 @@ class VehicleOperations( } object VehicleOperations { - def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject, passenger: Player): Unit = { + def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject with Mountable, passenger: Player): Unit = { + obj.PassengerInSeat(passenger).foreach { seatNumber => + updateMountableZoneInteractionFromEarliestSeat(obj, seatNumber) + } + } + + def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject with Mountable, seatNumber: Int): Unit = { obj match { case obj: Vehicle => - updateVehicleZoneInteractionFromEarliestSeat(obj, passenger) + updateVehicleZoneInteractionFromEarliestSeat(obj, seatNumber) case obj: Mountable with InteractsWithZone => - updateEntityZoneInteractionFromEarliestSeat(obj, passenger, obj) + updateEntityZoneInteractionFromEarliestSeat(obj, seatNumber, obj) case _ => () } } - private def updateVehicleZoneInteractionFromEarliestSeat(obj: Vehicle, passenger: Player): Unit = { + private def updateVehicleZoneInteractionFromEarliestSeat(obj: Vehicle, seatNumber: Int): 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 { @@ -223,29 +229,22 @@ object VehicleOperations { case _ => true } if (carrierSeatVacancy) { - updateEntityZoneInteractionFromEarliestSeat(obj, passenger, targetVehicle) + updateEntityZoneInteractionFromEarliestSeat(obj, seatNumber, targetVehicle) } } private def updateEntityZoneInteractionFromEarliestSeat( obj: Mountable with InteractsWithZone, - passenger: Player, + seatNumber: Int, updateTarget: InteractsWithZone ): Unit = { - val inSeatNumberOpt = obj.PassengerInSeat(passenger) - if (inSeatNumberOpt.contains(0)) { + if (seatNumber == 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() - } + } else if(!obj.Seat(seatNumber = 0).exists(_.isOccupied) && obj.OccupiedSeats().headOption.contains(seatNumber)) { + //there is no primary operator + //we are responsible as the player in the seat closest to the "front" + updateTarget.zoneInteractions() } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala index c797371ac..16167151d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala @@ -34,6 +34,17 @@ trait Mountable { } } + /** + * All the seats that have occupants by their seat number. + * @return list of the numbers of all occupied seats + */ + def OccupiedSeats(): List[Int] = { + seats + .collect { case (index, seat) if seat.isOccupied => index } + .toList + .sorted + } + /** * Retrieve a mapping of each mount from its mount point index. * @return the mapping of mount point to mount From c7368d47a4915c4f1d93769fba3d2d193d89263e Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 19 Jan 2026 14:37:21 -0500 Subject: [PATCH 12/13] expanded scope of 'csr override message for Capitol force dome state; spelling and guard condition changes to dismount blocking cases --- .../serverobject/dome/ForceDomeControl.scala | 16 ++++++++-------- .../vehicles/control/VehicleControl.scala | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index ffbb7c745..fa9a8553a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -123,14 +123,12 @@ object ForceDomeControl { building: Building, state: Boolean ): Unit = { - val events = building.Zone.LocalEvents + val zone = building.Zone val message = LocalAction.SendResponse(ChatMsg( - ChatMessageType.UNK_227, - s"Capitol force dome state change was suppressed. ${building.Name} will remain ${if (state) "enveloped" else "exposed"}." + ChatMessageType.UNK_229, + s"The Capitol force dome at ${building.Name} will remain ${if (state) "activated" else "deactivated"}." )) - building.PlayersInSOI.foreach { player => - events ! LocalServiceMessage(player.Name, message) - } + zone.LocalEvents ! LocalServiceMessage(zone.id, message) } /** @@ -473,7 +471,6 @@ class ForceDomeControl(dome: ForceDomePhysics) private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = { import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global - customState match { case None => val oldState = dome.Energized @@ -484,9 +481,12 @@ class ForceDomeControl(dome: ForceDomePhysics) context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection) } newState - case Some(state) => + case Some(state) + if !ForceDomeControl.CheckForceDomeStatus(domeOwnerAsABuilding, dome).contains(state) => ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state) state + case Some(state) => + state } } } 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 fb3bc6459..d110fdc98 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -119,7 +119,7 @@ class VehicleControl(vehicle: Vehicle) sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint)) } - private val disountingFailureReasons: Receive = { + private val dismountingFailureReasons: Receive = { case Mountable.TryDismount(user, seat_num, bailType) if vehicle.DeploymentState == DriveState.AutoPilot => sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) @@ -142,7 +142,7 @@ class VehicleControl(vehicle: Vehicle) case Mountable.TryDismount(user, seat_num, bailType) if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed && GlobalDefinitions.isFlightVehicle(vehicle.Definition) - && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner)) && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true case _ if Random.nextInt(10) == 1 => false @@ -152,7 +152,7 @@ class VehicleControl(vehicle: Vehicle) case Mountable.TryDismount(user, seat_num, bailType) if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed && GlobalDefinitions.isFlightVehicle(vehicle.Definition) - && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner)) && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true case _ if Random.nextInt(5) == 1 => false @@ -162,7 +162,7 @@ class VehicleControl(vehicle: Vehicle) case Mountable.TryDismount(user, seat_num, bailType) if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed && GlobalDefinitions.isFlightVehicle(vehicle.Definition) - && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner)) && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true case _ if Random.nextInt(4) == 1 => false @@ -177,7 +177,7 @@ class VehicleControl(vehicle: Vehicle) def commonEnabledBehavior: Receive = checkBehavior .orElse(mountingFailureReasons) .orElse(mountBehavior) - .orElse(disountingFailureReasons) + .orElse(dismountingFailureReasons) .orElse(dismountBehavior) .orElse(attributeBehavior) .orElse(jammableBehavior) From f5d7fed1cf8953369d053c25dd5fabee245bac35 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sun, 25 Jan 2026 19:17:37 -0500 Subject: [PATCH 13/13] corrected death by force dome contact, for all players; corrected reset of force dome protection field condition --- .../CustomerServiceRepresentativeMode.scala | 116 ++++++++++-------- .../actors/session/csr/GeneralLogic.scala | 67 ++++------ .../actors/session/csr/VehicleLogic.scala | 72 ++--------- .../actors/session/normal/GeneralLogic.scala | 34 ++--- .../session/spectator/GeneralLogic.scala | 34 ++++- .../InteractWithForceDomeProtection.scala | 4 +- .../equipment/ArmorSiphonBehavior.scala | 2 +- .../serverobject/damage/Damageable.scala | 23 ++-- .../serverobject/dome/ForceDomeControl.scala | 2 + .../auto/AffectedByAutomaticTurretFire.scala | 2 +- 10 files changed, 171 insertions(+), 185 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala index f0ce6a974..0e454f71b 100644 --- a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala @@ -110,7 +110,7 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { private def keepAlivePersistanceCSR(): Unit = { val player = data.player player.allowInteraction = false - topOffHealthOfPlayer(player) + CustomerServiceRepresentativeMode.topOffHealthOfPlayer(data, player) data.zoning.spawn.interimUngunnedVehicle = None data.keepAlivePersistence() if (player.HasGUID) { @@ -119,63 +119,13 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { data.continent .GUID(player.VehicleSeated) .collect { case obj: PlanetSideGameObject with Vitality => - topOffHealth(obj) + CustomerServiceRepresentativeMode.topOffHealth(data, obj) } data.squad.updateSquad() } else { data.turnCounterFunc(PlanetSideGUID(0)) } } - - private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = { - obj match { - case p: Player => topOffHealthOfPlayer(p) - case v: Vehicle => topOffHealthOfVehicle(v) - case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o) - case _ => () - } - } - - private def topOffHealthOfPlayer(player: Player): Unit = { - //driver below half health, full heal - val maxHealthOfPlayer = player.MaxHealth.toLong - if (player.Health < maxHealthOfPlayer * 0.5f) { - player.Health = maxHealthOfPlayer.toInt - player.LogActivity(player.ClearHistory().head) - data.sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer)) - data.continent.AvatarEvents ! AvatarServiceMessage(data.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer)) - } - } - - private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = { - topOffHealthOfGeneric(vehicle) - //vehicle shields below half, full shields - val maxShieldsOfVehicle = vehicle.MaxShields.toLong - val shieldsUi = vehicle.Definition.shieldUiAttribute - if (vehicle.Shields < maxShieldsOfVehicle) { - val guid = vehicle.GUID - vehicle.Shields = maxShieldsOfVehicle.toInt - data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle)) - data.continent.VehicleEvents ! VehicleServiceMessage( - data.continent.id, - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle) - ) - } - } - - private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = { - //below half health, full heal - val guid = obj.GUID - val maxHealthOf = obj.MaxHealth.toLong - if (obj.Health < maxHealthOf) { - obj.Health = maxHealthOf.toInt - data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf)) - data.continent.VehicleEvents ! VehicleServiceMessage( - data.continent.id, - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf) - ) - } - } } case object CustomerServiceRepresentativeMode extends PlayerMode { @@ -202,4 +152,66 @@ case object CustomerServiceRepresentativeMode extends PlayerMode { None )) } + + def topOffHealth(data: SessionData, obj: PlanetSideGameObject with Vitality): Unit = { + obj match { + case p: Player => topOffHealthOfPlayer(data, p) + case v: Vehicle => topOffHealthOfVehicle(data, v) + case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(data, o) + case _ => () + } + } + + def topOffHealthOfPlayer(data: SessionData, player: Player): Unit = { + //below half health, full heal + val maxHealthOfPlayer = player.MaxHealth.toLong + val guid = player.GUID + val zoneid = data.zoning.zoneChannel + if (player.Health < maxHealthOfPlayer * 0.5f) { + if (player.Health == 0) { + player.Revive + } + player.Health = maxHealthOfPlayer.toInt + player.LogActivity(player.ClearHistory().head) + data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOfPlayer)) + data.continent.AvatarEvents ! AvatarServiceMessage(zoneid, AvatarAction.PlanetsideAttribute(guid, 0, maxHealthOfPlayer)) + } + //below half armor, full armor + val maxArmor = player.MaxArmor.toLong + if (player.Armor < maxArmor) { + player.Armor = maxArmor.toInt + data.sendResponse(PlanetsideAttributeMessage(guid, 4, maxArmor)) + data.continent.AvatarEvents ! AvatarServiceMessage(zoneid, AvatarAction.PlanetsideAttribute(guid, 4, maxArmor)) + } + } + + def topOffHealthOfVehicle(data: SessionData, vehicle: Vehicle): Unit = { + topOffHealthOfGeneric(data, vehicle) + //vehicle shields below half, full shields + val maxShieldsOfVehicle = vehicle.MaxShields.toLong + val shieldsUi = vehicle.Definition.shieldUiAttribute + if (vehicle.Shields < maxShieldsOfVehicle) { + val guid = vehicle.GUID + vehicle.Shields = maxShieldsOfVehicle.toInt + data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle)) + data.continent.VehicleEvents ! VehicleServiceMessage( + data.continent.id, + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle) + ) + } + } + + def topOffHealthOfGeneric(data: SessionData, obj: PlanetSideGameObject with Vitality): Unit = { + //below half health, full heal + val guid = obj.GUID + val maxHealthOf = obj.MaxHealth.toLong + if (obj.Health < maxHealthOf) { + obj.Health = maxHealthOf.toInt + data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf)) + data.continent.VehicleEvents ! VehicleServiceMessage( + data.continent.id, + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf) + ) + } + } } diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index 76e8f2761..31ca715e4 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -27,11 +27,8 @@ import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.FacilityTurret -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vehicles.Utility import net.psforever.objects.vital.Vitality -import net.psforever.objects.vital.etc.ForceDomeExposure -import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.OutfitEventAction.{Initial, OutfitInfo, OutfitRankNames, Unk1} @@ -40,8 +37,8 @@ import net.psforever.services.RemoverActor import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} -import scodec.bits.ByteVector +import scala.concurrent.duration._ import scala.util.Success object GeneralLogic { @@ -81,28 +78,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) sessionLogic.updateBlockMap(player, pos) - //below half health, full heal - val maxHealth = player.MaxHealth.toLong - if (player.Health < maxHealth) { - player.Health = maxHealth.toInt - player.LogActivity(player.ClearHistory().head) - sendResponse(PlanetsideAttributeMessage(avatarGuid, 0, maxHealth)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 0, maxHealth)) - } - //below half stamina, full stamina - val avatar = player.avatar - val maxStamina = avatar.maxStamina - if (avatar.stamina < maxStamina) { - avatarActor ! AvatarActor.RestoreStamina(maxStamina) - sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong)) - } - //below half armor, full armor - val maxArmor = player.MaxArmor.toLong - if (player.Armor < maxArmor) { - player.Armor = maxArmor.toInt - sendResponse(PlanetsideAttributeMessage(avatarGuid, 4, maxArmor)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 4, maxArmor)) - } + topOffHealthOfPlayer() //expected val isMoving = WorldEntity.isMoving(vel) val isMovingPlus = isMoving || isJumping || jumpThrust @@ -559,20 +535,19 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex v.BailProtection = false case (CollisionIs.OfAircraft, Some(v: Vehicle)) if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => () - case (CollisionIs.BetweenThings, Some(field: ForceDomePhysics)) /*if field.Energized*/ => - val target = sessionLogic - .vehicles - .findLocalVehicle - .getOrElse(player) - target.Actor ! Vitality.Damage( - DamageInteraction( - PlayerSource(player), - ForceDomeExposure(SourceEntry(field)), - player.Position - ).calculate() - ) - target.BailProtection = false - player.BailProtection = false + case (CollisionIs.BetweenThings, Some(v: Vehicle)) => + v.Actor ! Vehicle.Deconstruct(Some(1 millisecond)) + continent.GUID(t) match { + case Some(_: ForceDomePhysics) => + player.Actor ! Player.Die() + case _ => () + } + case (CollisionIs.BetweenThings, Some(_: Player)) => + continent.GUID(t) match { + case Some(_: ForceDomePhysics) => + player.Actor ! Player.Die() + case _ => () + } case (CollisionIs.BetweenThings, _) => log.warn(s"GenericCollision: CollisionIs.BetweenThings detected - no handling case for obj id:${t.guid}") case _ => () @@ -823,4 +798,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.CapacitorState = CapacitorStateType.Idle } } + + def topOffHealthOfPlayer(): Unit = { + //below half health, full heal + CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player) + //below half stamina, full stamina + val avatar = player.avatar + val maxStamina = avatar.maxStamina + if (avatar.stamina < maxStamina) { + avatarActor ! AvatarActor.RestoreStamina(maxStamina) + sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong)) + } + } } diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala index bc02a8f55..6870f3c12 100644 --- a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala @@ -5,17 +5,15 @@ 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.{PlanetSideGameObject, Player, 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.vital.Vitality import net.psforever.objects.zones.Zone import net.psforever.objects.zones.interaction.InteractsWithZone -import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage} -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{DriveState, PlanetSideGUID, Vector3} +import net.psforever.types.{DriveState, Vector3} object VehicleLogic { def apply(ops: VehicleOperations): VehicleLogic = { @@ -51,8 +49,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) - topOffHealthOfPlayer() - topOffHealth(obj) + CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player) + CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { case Some(v: Vehicle) => (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) @@ -134,8 +132,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) - topOffHealthOfPlayer() - topOffHealth(obj) + CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player) + CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { case Some(v: Vehicle) => (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) @@ -217,8 +215,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) - topOffHealthOfPlayer() - topOffHealth(entity) + CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player) + CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, entity) sessionLogic.squad.updateSquad() case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity) @@ -336,56 +334,4 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ) } } - - private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = { - obj match { - case _: Player => topOffHealthOfPlayer() - case v: Vehicle => topOffHealthOfVehicle(v) - case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o) - case _ => () - } - } - - private def topOffHealthOfPlayer(): Unit = { - //driver below half health, full heal - val maxHealthOfPlayer = player.MaxHealth.toLong - if (player.Health < maxHealthOfPlayer * 0.5f) { - player.Health = maxHealthOfPlayer.toInt - player.LogActivity(player.ClearHistory().head) - sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer)) - continent.AvatarEvents ! AvatarServiceMessage(sessionLogic.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer)) - } - } - - private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = { - topOffHealthOfPlayer() - topOffHealthOfGeneric(vehicle) - //vehicle shields below half, full shields - val maxShieldsOfVehicle = vehicle.MaxShields.toLong - val shieldsUi = vehicle.Definition.shieldUiAttribute - if (vehicle.Shields < maxShieldsOfVehicle) { - val guid = vehicle.GUID - vehicle.Shields = maxShieldsOfVehicle.toInt - sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle) - ) - } - } - - private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = { - topOffHealthOfPlayer() - //vehicle below half health, full heal - val guid = obj.GUID - val maxHealthOf = obj.MaxHealth.toLong - if (obj.Health < maxHealthOf) { - obj.Health = maxHealthOf.toInt - sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf) - ) - } - } } diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 1db6ea648..823d09923 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -16,6 +16,7 @@ import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable +import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator @@ -23,6 +24,7 @@ import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker +import net.psforever.objects.serverobject.mount.MountableEntity import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal @@ -637,25 +639,15 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) - case (CollisionIs.BetweenThings, Some(field: ForceDomePhysics)) /*if field.Energized*/ => - val target = sessionLogic - .vehicles - .findLocalVehicle - .getOrElse(player) - target.Actor ! Vitality.Damage( - DamageInteraction( - PlayerSource(player), - ForceDomeExposure(SourceEntry(field)), - player.Position - ).calculate() - ) + case (CollisionIs.BetweenThings, out @ Some(target: PlanetSideServerObject with MountableEntity)) => target.BailProtection = false player.BailProtection = false + (out, sessionLogic.validObject(t, decorator = "GenericCollision/Surface"), false, pv) + case (_, Some(obj)) => + log.error(s"GenericCollision: $ctype detected: no handling case for ${obj.Definition.Name}") (None, None, false, Vector3.Zero) - case (CollisionIs.BetweenThings, _) => - log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") - (None, None, false, Vector3.Zero) - case _ => + case (_, None) => + log.error(s"GenericCollision: $ctype detected: no entity detected as 'Primary'") (None, None, false, Vector3.Zero) } val curr = System.currentTimeMillis() @@ -677,6 +669,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } } + case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, Some(field: ForceDomePhysics)) => + us.Actor ! Damageable.MakeVulnerable + us.Actor ! Vitality.Damage( + DamageInteraction( + PlayerSource(player), + ForceDomeExposure(SourceEntry(field)), + player.Position + ).calculate() + ) + case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) => collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala index 8c332585a..0987bfd1c 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -9,15 +9,18 @@ import net.psforever.objects.avatar.{Avatar, Implant} import net.psforever.objects.ballistics.Projectile import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.serverobject.containable.Containable +import net.psforever.objects.serverobject.dome.ForceDomePhysics import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.vehicles.Utility import net.psforever.objects.zones.ZoneProjectile import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.AccountPersistenceService import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.{ExoSuitType, Vector3} +import scala.concurrent.duration.DurationInt + object GeneralLogic { def apply(ops: GeneralOperations): GeneralLogic = { new GeneralLogic(ops, ops.context) @@ -283,7 +286,34 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } } - def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { /* intentionally blank */ } + def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { + player.BailProtection = false + val GenericCollisionMsg(ctype, p, _, _, pv, t, _, _, _, _, _, _) = pkt + if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { + if (ops.heightTrend) { + ops.heightHistory = ops.heightLast + } + else { + ops.heightLast = ops.heightHistory + } + } + (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { + case (CollisionIs.BetweenThings, Some(v: Vehicle)) => + v.Actor ! Vehicle.Deconstruct(Some(1 millisecond)) + continent.GUID(t) match { + case Some(_: ForceDomePhysics) => + player.Actor ! Player.Die() + case _ => () + } + case (CollisionIs.BetweenThings, Some(_: Player)) => + continent.GUID(t) match { + case Some(_: ForceDomePhysics) => + player.Actor ! Player.Die() + case _ => () + } + case _ => () + } + } def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* intentionally blank */ } 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 c2365a224..3b6e30605 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala @@ -40,7 +40,7 @@ class InteractWithForceDomeProtection case Some(dome) if dome.Perimeter.isEmpty || target.Zone != dome.Zone || - !ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) => + !ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f) => resetInteraction(target) case Some(_) => () //no action @@ -69,7 +69,7 @@ class InteractWithForceDomeProtection case _ => None } .find { dome => - ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(target, dome, maxDistance = 0f) + ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f) } .map { dome => applyProtection(target, dome) diff --git a/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala b/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala index 9c153e42b..5547f36fd 100644 --- a/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala +++ b/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala @@ -43,7 +43,7 @@ object ArmorSiphonBehavior { val after = item.Discharge() if (before > after) { v.Actor ! ArmorSiphonBehavior.Recharge(iguid) - PerformDamage( + PerformDamageIfVulnerable( obj, DamageInteraction( VehicleSource(obj), 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 976ea1b2c..65d94d0d4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala @@ -38,10 +38,7 @@ trait Damageable { isVulnerable = false case Vitality.Damage(damage_func) => - val obj = DamageableObject - if (isVulnerable && obj.CanDamage) { - PerformDamage(obj, damage_func) - } + PerformDamageIfVulnerable(DamageableObject, damage_func) } /** a duplicate of the core implementation for the default mixin hook, for use in overriding */ @@ -53,10 +50,20 @@ trait Damageable { isVulnerable = false case Vitality.Damage(damage_func) => - val obj = DamageableObject - if (isVulnerable && obj.CanDamage) { - PerformDamage(obj, damage_func) - } + PerformDamageIfVulnerable(DamageableObject, damage_func) + } + + /** + * Assess if the target is vulnerable to damage. + * If so, attempt damage calculations. + * @see `ResolutionCalculations.Output` + * @param obj the entity to be damaged + * @param applyDamageTo the function that applies the damage to the target in a target-tailored fashion + */ + def PerformDamageIfVulnerable(obj: Damageable.Target, applyDamageTo: ResolutionCalculations.Output): Unit = { + if (isVulnerable && obj.CanDamage) { + PerformDamage(obj, applyDamageTo) + } } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala index fa9a8553a..e959dd836 100644 --- a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala @@ -479,6 +479,8 @@ class ForceDomeControl(dome: ForceDomePhysics) //dome activating context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.Purge) context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection) + } else if (oldState && !newState) { + context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.RemoveProtection) } newState case Some(state) diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala index 7485c3a47..d2b4ecab2 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala @@ -60,7 +60,7 @@ trait AffectedByAutomaticTurretFire extends Damageable { ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel), correctedTargetPosition ) - PerformDamage(target, resolvedProjectile.calculate()) + PerformDamageIfVulnerable(target, resolvedProjectile.calculate()) } } }