diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 8f382ed82..4405357a7 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -60,10 +60,6 @@ add_property maelstrom equiptime 1000 add_property maelstrom holstertime 1000 add_property magcutter equiptime 250 add_property magcutter holstertime 250 -add_property medicalapplicator equiptime 500 -add_property medicalapplicator holstertime 500 -add_property medicalapplicator firemode0_refiretime 500 -add_property medicalapplicator firemode1_refiretime 500 add_property mini_chaingun equiptime 750 add_property mini_chaingun holstertime 750 add_property nano_dispenser equiptime 750 @@ -95,7 +91,7 @@ add_property super_staminakit nodrop false add_property super_staminakit requirement_award0 false add_property suppressor equiptime 600 add_property suppressor holstertime 600 -add_property trek allowed false +add_property trek allowed true add_property trek equiptime 500 add_property trek holstertime 500 add_property vulture requirement_award0 false diff --git a/src/main/resources/zonemaps/map05.json b/src/main/resources/zonemaps/map05.json index a1e3816e4..1f72327d7 100644 --- a/src/main/resources/zonemaps/map05.json +++ b/src/main/resources/zonemaps/map05.json @@ -21625,7 +21625,7 @@ "Owner": 1303, "AbsX": 3577.178, "AbsY": 2712.24927, - "AbsZ": 32.8808441, + "AbsZ": 34.289, "Yaw": 318.0, "GUID": 1843, "MapID": null, @@ -21664,7 +21664,7 @@ "Owner": 1303, "AbsX": 3588.1582, "AbsY": 2700.0542, - "AbsZ": 32.8808441, + "AbsZ": 34.289, "Yaw": 318.0, "GUID": 1846, "MapID": null, diff --git a/src/main/resources/zonemaps/ugd06.json b/src/main/resources/zonemaps/ugd06.json index a87dd2108..33f2c5328 100644 --- a/src/main/resources/zonemaps/ugd06.json +++ b/src/main/resources/zonemaps/ugd06.json @@ -8289,7 +8289,7 @@ "AbsY": 1070.60535, "AbsZ": 173.239014, "Yaw": 338.0, - "GUID": 648, + "GUID": 649, "MapID": null, "IsChildObject": true }, @@ -8302,7 +8302,7 @@ "AbsY": 1132.36707, "AbsZ": 138.999, "Yaw": 124.0, - "GUID": 649, + "GUID": 648, "MapID": null, "IsChildObject": true }, diff --git a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala index 9433192cb..1fd3fa0c6 100644 --- a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala +++ b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala @@ -150,6 +150,11 @@ object MiddlewareActor { packet.isInstanceOf[ChatMsg] } + /** `PropertyOverrideMessage` ptsd from other large packets causing issues when bundled */ + private def propertyOverrideMessageGuard(packet: PlanetSidePacket): Boolean = { + packet.isInstanceOf[PropertyOverrideMessage] + } + /** * A function for blanking tasks related to inbound packet resolution. * Do nothing. @@ -259,7 +264,8 @@ class MiddlewareActor( MiddlewareActor.keepAliveMessageGuard, MiddlewareActor.characterInfoMessageGuard, MiddlewareActor.squadDetailDefinitionMessageGuard, - MiddlewareActor.chatMsgGuard + MiddlewareActor.chatMsgGuard, + MiddlewareActor.propertyOverrideMessageGuard ) private val smpHistoryLength: Int = 100 diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 68ffb98d3..02d8d5ca1 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -2218,12 +2218,12 @@ class AvatarActor( .Holsters() .foreach(holster => holster.Equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + /*case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => //todo fix so player may hold medapp when loading the zone (client crash) val item = SimpleItem(GlobalDefinitions.flail_targeting_laser) holster.Equipment = None holster.Equipment = item - item.GUID = PlanetSideGUID(gen.getAndIncrement) + item.GUID = PlanetSideGUID(gen.getAndIncrement)*/ case Some(tool: Tool) => tool.AmmoSlots.foreach(slot => { slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement) diff --git a/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala index f82492cf2..fe2d4b873 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala @@ -45,6 +45,13 @@ class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: A case GalaxyResponse.MapUpdate(msg) => sendResponse(msg) + import net.psforever.actors.zone.ZoneActor + import net.psforever.zones.Zones + Zones.zones.find(_.Number == msg.continent_id) match { + case Some(zone) => + zone.actor ! ZoneActor.BuildingInfoState(msg) + case None => + } case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) => val faction = player.Faction diff --git a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala index 1b1764743..2f12efd95 100644 --- a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala @@ -2,7 +2,8 @@ package net.psforever.actors.session.normal import akka.actor.ActorContext -import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers} +import net.psforever.actors.session.support.SpawnOperations.ActivityQueuedTask +import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers, SpawnOperations} import net.psforever.objects.ce.Deployable import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.vehicles.MountableWeapons @@ -191,7 +192,17 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) case LocalResponse.SendResponse(msg) => - sendResponse(msg) + msg match { + case m: GenericObjectActionMessage => + // delay building virus alert if player is dead/respawning + if ((m.code == 58 || m.code == 60 || m.code == 61) && !sessionLogic.zoning.spawn.startEnqueueSquadMessages) { + sessionLogic.zoning.spawn.enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.delaySendGenericObjectActionMessage(msg), 1)) + } + else sendResponse(msg) + case _ => + sendResponse(msg) + } case LocalResponse.SetEmpire(objectGuid, empire) => sendResponse(SetEmpireMessage(objectGuid, empire)) diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index deda57ab3..3cc25a788 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -12,7 +12,7 @@ import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.zone.ZoneActor import net.psforever.objects.LivePlayerList import net.psforever.objects.sourcing.PlayerSource -import net.psforever.objects.zones.ZoneInfo +import net.psforever.objects.zones.{Zone, ZoneInfo} import net.psforever.packet.game.SetChatFilterMessage import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -20,7 +20,7 @@ import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServic import net.psforever.types.ChatMessageType.CMT_QUIT import org.log4s.Logger -import java.util.concurrent.{Executors, TimeUnit} +import java.util.concurrent.{Executors, ScheduledFuture, TimeUnit} import scala.annotation.unused import scala.collection.{Seq, mutable} import scala.concurrent.ExecutionContext.Implicits.global @@ -368,7 +368,10 @@ class ChatOperations( case (Some(buildings), Some(faction), Some(_)) => //TODO implement timer //schedule processing of buildings with a delay - processBuildingsWithDelay(buildings, faction, 1000) //delay of 1000ms between each building operation + processBuildingsWithDelay(buildings, faction, 100) { zone => + zone.actor ! ZoneActor.ZoneMapUpdate() + zone.actor ! ZoneActor.AssignLockedBy(zone, notifyPlayers=true) + } true case _ => false @@ -379,30 +382,36 @@ class ChatOperations( buildings: Seq[Building], faction: PlanetSideEmpire.Value, delayMillis: Long - ): Unit = { - val buildingIterator = buildings.iterator - scheduler.scheduleAtFixedRate( + )(onComplete: Zone => Unit): Unit = { + import net.psforever.objects.serverobject.structures.StructureType + val buildingsToProcess = buildings.filter(b => b.CaptureTerminal.isDefined && b.Faction != faction) + val iterator = buildingsToProcess.iterator + val zone = buildings.head.Zone + var handle: ScheduledFuture[_] = null + handle = scheduler.scheduleAtFixedRate( () => { - if (buildingIterator.hasNext) { - val building = buildingIterator.next() + if (iterator.hasNext) { + val building = iterator.next() val terminal = building.CaptureTerminal.get - val zone = building.Zone - val zoneActor = zone.actor - val buildingActor = building.Actor - //clear any previous hack - if (building.CaptureTerminalIsHacked) { - zone.LocalEvents ! LocalServiceMessage( - zone.id, - LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody) - ) + if (building.BuildingType == StructureType.Tower) { + building.Actor ! BuildingActor.SetFaction(faction) + building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false)) + building.Actor ! BuildingActor.MapUpdate() } - //push any updates this might cause - zoneActor ! ZoneActor.ZoneMapUpdate() - //convert faction affiliation - buildingActor ! BuildingActor.SetFaction(faction) - buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) - //push for map updates again - zoneActor ! ZoneActor.ZoneMapUpdate() + else { + if (building.CaptureTerminalIsHacked) { + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody) + ) + } + building.Actor ! BuildingActor.SetFaction(faction) + building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false)) + } + } + else { + handle.cancel(false) + onComplete(zone) } }, 0, diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index 539e50935..b7dc548eb 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -1258,7 +1258,12 @@ class GeneralOperations( def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { equipment match { case Some(item) => - sendUseGeneralEntityMessage(terminal, item) + if (terminal.Definition == GlobalDefinitions.main_terminal) { + sendUseMainTerminalMessage(terminal, item, msg.unk2) + } + else { + sendUseGeneralEntityMessage(terminal, item) + } case None if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => @@ -1484,6 +1489,14 @@ class GeneralOperations( obj.Actor ! CommonMessages.Use(player, Some(equipment)) } + def sendUseMainTerminalMessage(obj: PlanetSideServerObject, equipment: Equipment, virus: Long): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + if (player.Faction == obj.Faction) + obj.Actor ! CommonMessages.RemoveVirus(player, Some(equipment)) + else + obj.Actor ! CommonMessages.UploadVirus(player, Some(equipment), virus) + } + def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") equipment match { diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala index 905d2ca01..7f5004529 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -7,7 +7,8 @@ import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf import net.psforever.objects.{PlanetSideGameObject, Tool, Vehicle} import net.psforever.objects.vehicles.{CargoBehavior, MountableWeapons} import net.psforever.objects.vital.InGameHistory -import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage} +import net.psforever.packet.game.{DismountVehicleCargoMsg, GenericObjectActionMessage, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage} +import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{BailType, PlanetSideGUID, Vector3} // @@ -197,8 +198,23 @@ class SessionMountHandlers( * @param seatNum the mount out of which which the player is disembarking */ def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - DismountAction(tplayer, obj, seatNum) tplayer.WhichSide = OutsideOf + if (tplayer.BailProtection) { + tplayer.ContributionFrom(obj) + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.SendResponse(Service.defaultPlayerGUID, PlanetsideAttributeMessage(obj.GUID, 81, 1)) + ) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.SendResponse(Service.defaultPlayerGUID, ObjectDetachMessage(obj.GUID, tplayer.GUID, tplayer.Position, obj.Orientation)) + ) + } + else { + sendResponse(GenericObjectActionMessage(obj.GUID, 24)) + DismountAction(tplayer, obj, seatNum) + } //until vehicles maintain synchronized momentum without a driver obj match { case v: Vehicle 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 11f039ee9..bbbb3a982 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -54,7 +54,7 @@ import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.vehicles._ import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning} import net.psforever.objects._ -import net.psforever.packet.game.{AvatarAwardMessage, AvatarSearchCriteriaMessage, AvatarStatisticsMessage, AwardCompletion, BindPlayerMessage, BindStatus, CargoMountPointStatusMessage, ChangeShortcutBankMessage, ChatChannel, CreateShortcutMessage, DroppodFreefallingMessage, LoadMapMessage, ObjectCreateDetailedMessage, ObjectDeleteMessage, PlanetsideStringAttributeMessage, PlayerStateShiftMessage, SetChatFilterMessage, SetCurrentAvatarMessage, ShiftState} +import net.psforever.packet.game.{AvatarAwardMessage, AvatarSearchCriteriaMessage, AvatarStatisticsMessage, AwardCompletion, BindPlayerMessage, BindStatus, CargoMountPointStatusMessage, ChangeShortcutBankMessage, ChatChannel, CreateShortcutMessage, DroppodFreefallingMessage, LoadMapMessage, ObjectCreateDetailedMessage, ObjectDeleteMessage, PlayerStateShiftMessage, SetChatFilterMessage, SetCurrentAvatarMessage, ShiftState} import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, ChatMsg, ContinentalLockUpdateMessage, DeadState, DensityLevelUpdateMessage, DeployRequestMessage, DeployableInfo, DeployableObjectsInfoMessage, DeploymentAction, DisconnectMessage, DroppodError, DroppodLaunchResponseMessage, FriendsResponse, GenericObjectActionMessage, GenericObjectStateMsg, HotSpotUpdateMessage, ObjectAttachMessage, ObjectCreateMessage, PlanetsideAttributeEnum, PlanetsideAttributeMessage, PropertyOverrideMessage, ReplicationStreamMessage, SetEmpireMessage, TimeOfDayMessage, TriggerEffectMessage, ZoneForcedCavernConnectionsMessage, ZoneInfoMessage, ZoneLockInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo} import net.psforever.packet.game.{BeginZoningMessage, DroppodLaunchRequestMessage, ReleaseAvatarRequestMessage, SpawnRequestMessage, WarpgateRequest} import net.psforever.packet.game.DeathStatistic @@ -193,6 +193,10 @@ object SpawnOperations { def sendEventMessage(msg: ChatMsg)(sessionLogic: SessionData): Unit = { sessionLogic.sendResponse(msg) } + + def delaySendGenericObjectActionMessage(msg: PlanetSideGamePacket)(sessionLogic: SessionData): Unit = { + sessionLogic.sendResponse(msg) + } } class ZoningOperations( @@ -338,6 +342,13 @@ class ZoningOperations( sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 19, 1)) } } + //adjust for health module benefit so overhead health bar accounts for added health + live.filter { tplayer => + tplayer.MaxHealth == 120 + } + .foreach { targetPlayer => + sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 1, 120)) + } //load corpses in zone continent.Corpses.foreach { spawn.DepictPlayerAsCorpse @@ -559,27 +570,19 @@ class ZoningOperations( val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR) val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC) val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS) - zone.Buildings.foreach({ case (_, building) => initBuilding(continentNumber, building.MapId, building) }) sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) - //TODO should actually not claim that the sanctuary or VR zones are locked by their respective empire - if (continentNumber == 11) - sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NC)) - else if (continentNumber == 12) - sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.TR)) - else if (continentNumber == 13) - sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.VS)) - else + if (continentNumber == 11 || continentNumber == 12 || continentNumber == 13) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NEUTRAL)) + else + sendResponse(ContinentalLockUpdateMessage(continentNumber, zone.lockedBy)) //CaptureFlagUpdateMessage() //VanuModuleUpdateMessage() //ModuleLimitsMessage() - val isCavern = continent.map.cavern - sendResponse(ZoneInfoMessage(continentNumber, empire_status=true, if (isCavern) { - Int.MaxValue.toLong - } else { - 0L - })) + val isCavern = zone.map.cavern + if (!isCavern) { + sendResponse(ZoneInfoMessage(continentNumber, empire_status = true, 0L)) + } sendResponse(ZoneLockInfoMessage(continentNumber, lock_status=false, unk=true)) sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0)) sendResponse( @@ -701,6 +704,7 @@ class ZoningOperations( log.warn( s"SpawnPointResponse: ${player.Name}'s zoning was not in order at the time a response was received; attempting to guess what ${player.Sex.pronounSubject} wants to do" ) + player.protectedWhileZoning = true } val previousZoningType = ztype CancelZoningProcess() @@ -1117,6 +1121,27 @@ class ZoningOperations( PlanetsideAttributeEnum.ControlConsoleHackUpdate, HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false) ) + case GlobalDefinitions.vanu_control_console => + sessionLogic.general.sendPlanetsideAttributeMessage( + amenity.GUID, + PlanetsideAttributeEnum.ControlConsoleHackUpdate, + HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false) + ) + case GlobalDefinitions.main_terminal => + val virus = amenity.asInstanceOf[Terminal].Owner.asInstanceOf[Building].virusId + val hackStateMap: Map[Long, HackState7] = Map( + 0L -> HackState7.UnlockDoors, + 1L -> HackState7.DisableLatticeBenefits, + 2L -> HackState7.NTUDrain, + 3L -> HackState7.DisableRadar, + 4L -> HackState7.AccessEquipmentTerms + ) + val hackState = hackStateMap.getOrElse(virus, HackState7.Unk8) + sessionLogic.general.hackObject(amenity.GUID, unk1 = 1114636288L, hackState) + if (virus != 8 && !sessionLogic.zoning.spawn.startEnqueueSquadMessages) { + sessionLogic.zoning.spawn.enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.delaySendGenericObjectActionMessage(GenericObjectActionMessage(amenityId, 58)), 1)) + } case _ => sessionLogic.general.hackObject(amenity.GUID, unk1 = 1114636288L, HackState7.Unk8) //generic hackable object } @@ -1403,6 +1428,7 @@ class ZoningOperations( s"LoadZoneTransferPassengerMessages: ${player.Name} expected a manifest for zone transfer; got nothing" ) } + vehicle.protectedWhileZoning = false } /** Before changing zones, perform the following task (which can be a nesting of subtasks). */ @@ -1967,6 +1993,7 @@ class ZoningOperations( deadState = DeadState.RespawnTime val tplayer = new Player(avatar) session = session.copy(player = tplayer) + tplayer.protectedWhileZoning = true //actual zone is undefined; going to our sanctuary RandomSanctuarySpawnPosition(tplayer) DefinitionUtil.applyDefaultLoadout(tplayer) @@ -1979,6 +2006,7 @@ class ZoningOperations( deadState = DeadState.RespawnTime session = session.copy(player = new Player(avatar)) player.Zone = inZone + player.protectedWhileZoning = true optionalSavedData match { case Some(results) => val health = results.health @@ -2092,9 +2120,10 @@ class ZoningOperations( log.info(s"RestoreInfo: player $name is alive") deadState = DeadState.Alive session = session.copy(player = p, avatar = a) + p.protectedWhileZoning = true sessionLogic.persist() setupAvatarFunc = AvatarRejoin - dropMedicalApplicators(p) + //dropMedicalApplicators(p) avatarActor ! AvatarActor.ReplaceAvatar(a) avatarLoginResponse(a) @@ -2104,7 +2133,7 @@ class ZoningOperations( deadState = DeadState.Dead session = session.copy(player = p, avatar = a) sessionLogic.persist() - dropMedicalApplicators(p) + //dropMedicalApplicators(p) HandleReleaseAvatar(p, inZone) avatarActor ! AvatarActor.ReplaceAvatar(a) avatarLoginResponse(a) @@ -2548,9 +2577,6 @@ class ZoningOperations( sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } } - if (player.outfit_id == 0) { - SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) - } /*make weather happen sendResponse(WeatherMessage(List(),List( StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), @@ -2644,6 +2670,7 @@ class ZoningOperations( def AvatarRejoin(): Unit = { sessionLogic.vehicles.GetKnownVehicleAndSeat() match { case (Some(vehicle: Vehicle), Some(seat: Int)) => + vehicle.protectedWhileZoning = true //vehicle and driver/passenger val vguid = vehicle.GUID sendResponse(OCM.apply(vehicle)) @@ -2674,7 +2701,6 @@ class ZoningOperations( log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") } setupAvatarFunc = AvatarCreate - SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) /*make weather happen sendResponse(WeatherMessage(List(),List( StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), @@ -2688,6 +2714,8 @@ class ZoningOperations( StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215), StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215), StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215))))*/ + player.Zone.ApplyHomeLockBenefitsOnLogin(player) + SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) } @@ -2922,13 +2950,40 @@ class ZoningOperations( 0 seconds } else { //for other zones ... - //biolabs have/grant benefits - val cryoBenefit: Float = toSpawnPoint.Owner match { - case b: Building if b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) => 0.5f - case _ => 1f + val spawnTimeBenefit: Float = toSpawnPoint.Owner match { + case b: Building => FasterRespawnBenefits(b) + case _ => 1f } //TODO cumulative death penalty - (toSpawnPoint.Definition.Delay.toFloat * cryoBenefit).seconds + (toSpawnPoint.Definition.Delay.toFloat * spawnTimeBenefit).seconds + } + } + + /** + * Multiple benefits can be given to an empire based on global ownership of certain zones or facility types that + * are linked to the facility being spawned at. + * @return float to potentially lower the respawn time if benefits are available + */ + def FasterRespawnBenefits(building: Building): Float = { + //Searhus lock benefit also gives biolab faster respawn + val searhusBenefit = Zones.zones.find(_.Number == 9).exists(_.benefitRecipient == player.Faction) + building match { + case b: Building + if (b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) && b.virusId != 1 && + b.hasCavernLockBenefit) || + (b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked && + searhusBenefit && b.hasCavernLockBenefit) => + 0.3f + case b: Building + if !b.CaptureTerminalIsHacked && b.hasCavernLockBenefit && b.virusId != 1 => + 0.5f + case b: Building + if (b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) && b.virusId != 1) || + (b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked && + searhusBenefit) => + 0.5f + case _ => + 1f } } @@ -3204,7 +3259,11 @@ class ZoningOperations( buildingType == StructureType.Bunker } .foreach { case (_, building) => - sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0 /*building.BuildingType == StructureType.Facility*/)) + if (building.hasCavernLockBenefit) { + sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 1)) + } + else + sendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0)) } statisticsPacketFunc() if (tplayer.ExoSuit == ExoSuitType.MAX) { @@ -3329,6 +3388,20 @@ class ZoningOperations( } }) } + nextSpawnPoint.map(_.Owner) match { + case Some(b: Building) if b.hasCavernLockBenefit => + tplayer.MaxHealth = 120 + tplayer.Health = 120 + tplayer.Zone.AvatarEvents ! AvatarServiceMessage( + tplayer.Zone.id, + AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 0, 120) + ) + tplayer.Zone.AvatarEvents ! AvatarServiceMessage( + tplayer.Zone.id, + AvatarAction.PlanetsideAttributeToAll(tplayer.GUID, 1, 120) + ) + case _ => () + } doorsThatShouldBeOpenInRange(pos, range = 100f) setAvatar = true player.allowInteraction = true @@ -3484,6 +3557,7 @@ class ZoningOperations( deadState = DeadState.Release //we may be alive or dead, may or may not be a corpse sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true)) DrawCurrentAmsSpawnPoint() + player.protectedWhileZoning = true } /** @@ -3663,6 +3737,9 @@ class ZoningOperations( //originally the client sent a death statistic update in between each change of statistic categories, about 30 times sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard)))) statisticsPacketFunc = respawnAvatarStatisticsFields + player.protectedWhileZoning = false + player.Zone.ApplyHomeLockBenefitsOnLogin(player) + SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic) } /** @@ -3693,6 +3770,7 @@ class ZoningOperations( } //originally the client sent a death statistic update in between each change of statistic categories, about 30 times sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard)))) + player.protectedWhileZoning = false } /** diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 334921248..0f385dae4 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -8,6 +8,8 @@ import net.psforever.actors.commands.NtuCommand import net.psforever.actors.zone.building._ import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.zones.Zone +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.ContinentalLockUpdateMessage import net.psforever.persistence import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -76,6 +78,9 @@ object BuildingActor { final case class DensityLevelUpdate(building: Building) extends Command + final case class ContinentalLock(zone: Zone) extends Command + + final case class HomeLockBenefits(msg: PlanetSideGamePacket) extends Command /** * Set a facility affiliated to one faction to be affiliated to a different faction. * @param details building and event system references @@ -162,7 +167,6 @@ object BuildingActor { val building = details.building val zone = building.Zone building.Faction = faction - zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction)) } } @@ -252,6 +256,14 @@ class BuildingActor( case DensityLevelUpdate(building) => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building))) Behaviors.same + + case ContinentalLock(zone) => + details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(ContinentalLockUpdateMessage(zone.Number, zone.lockedBy))) + Behaviors.same + + case HomeLockBenefits(msg) => + details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(msg)) + Behaviors.same } } } diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index c5747619e..28a3c2015 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.structures.{StructureType, WarpGate} import net.psforever.objects.zones.Zone import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup} import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle} -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} +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 @@ -18,8 +18,10 @@ import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.vital.{InGameActivity, InGameHistory} import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator} +import net.psforever.packet.game.{BuildingInfoUpdateMessage, PlanetsideAttributeMessage} import net.psforever.util.Database._ import net.psforever.persistence +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import scala.collection.mutable import scala.util.{Failure, Success} @@ -78,6 +80,10 @@ object ZoneActor { final case class RewardThisDeath(entity: PlanetSideGameObject with FactionAffinity with InGameHistory) extends Command final case class RewardOurSupporters(target: SourceEntry, history: Iterable[InGameActivity], kill: Kill, bep: Long) extends Command + + final case class AssignLockedBy(zone: Zone, notifyPlayers: Boolean) extends Command + + final case class BuildingInfoState(msg: BuildingInfoUpdateMessage) extends Command } class ZoneActor( @@ -115,6 +121,7 @@ class ZoneActor( // TODO this happens during testing, need a way to not always persist during tests } } + AssignLockedBy(zone, notifyPlayers=false) case Failure(e) => log.error(e.getMessage) } @@ -183,14 +190,75 @@ class ZoneActor( case ZoneMapUpdate() => zone.Buildings .filter(building => - building._2.BuildingType == StructureType.Facility || building._2.BuildingType == StructureType.Tower) + building._2.BuildingType == StructureType.Facility) .values .foreach(_.Actor ! BuildingActor.MapUpdate()) Behaviors.same + + case AssignLockedBy(zone, notifyPlayers) => + AssignLockedBy(zone, notifyPlayers) + Behaviors.same + + case BuildingInfoState(msg) => + UpdateBuildingState(msg) + Behaviors.same } .receiveSignal { case (_, PostStop) => Behaviors.same } } + + def AssignLockedBy(zone: Zone, notifyPlayers: Boolean): Unit = { + val buildings = zone.Buildings.values + val facilities = if (zone.id.startsWith("c")) { + buildings.filter(b => + b.Name.startsWith("N") || b.Name.startsWith("S")).toSeq + } + else { + buildings.filter(_.BuildingType == StructureType.Facility).toSeq + } + val factions = facilities.map(_.Faction).toSet + zone.lockedBy = + if (factions.size == 1) factions.head + else PlanetSideEmpire.NEUTRAL + zone.benefitRecipient = + if (facilities.nonEmpty && facilities.forall(_.Faction == facilities.head.Faction)) + facilities.head.Faction + else + zone.benefitRecipient + if (facilities.nonEmpty && notifyPlayers) { zone.NotifyContinentalLockBenefits(zone, facilities.head) } + } + + def UpdateBuildingState(msg: BuildingInfoUpdateMessage): Unit = { + val buildingOpt = zone.Buildings.collectFirst { + case (_, b) if b.MapId == msg.building_map_id => b + } + buildingOpt.foreach { building => + if (msg.generator_state == PlanetSideGeneratorState.Normal && building.hasCavernLockBenefit) { + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 1)) + ) + } + msg.is_hacked match { + case true if building.BuildingType == StructureType.Facility && !zone.map.cavern => + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0)) + ) + case false if building.hasCavernLockBenefit => + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 1)) + ) + case false if building.BuildingType == StructureType.Facility && !zone.map.cavern && !building.hasCavernLockBenefit => + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0)) + ) + case _ => + } + } + } } diff --git a/src/main/scala/net/psforever/actors/zone/building/CavernFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/CavernFacilityLogic.scala index 30f41b25a..ebfb38a80 100644 --- a/src/main/scala/net/psforever/actors/zone/building/CavernFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/CavernFacilityLogic.scala @@ -4,8 +4,8 @@ package net.psforever.actors.zone.building import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import net.psforever.actors.commands.NtuCommand -import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails} -import net.psforever.objects.serverobject.structures.{Amenity, Building} +import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor} +import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -84,7 +84,16 @@ case object CavernFacilityLogic ): Behavior[Command] = { BuildingActor.setFactionTo(details, faction, log) val building = details.building - building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) } + val gates: Iterable[Building] = building.Zone.Buildings.values.filter(_.BuildingType == StructureType.WarpGate) + gates.foreach { g => + val neighbors = g.Neighbours.getOrElse(Nil) + neighbors.collect { + case otherWg: Building => otherWg + } + .filter(_.Zone != g.Zone) + .foreach { otherGate => otherGate.Zone.actor ! ZoneActor.ZoneMapUpdate() + } + } Behaviors.same } 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 546a4c4a8..4dd21dcb7 100644 --- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala @@ -10,6 +10,7 @@ import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.objects.sourcing.PlayerSource +import net.psforever.packet.game.PlanetsideAttributeMessage import net.psforever.services.{InterstellarClusterService, Service} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} @@ -158,6 +159,7 @@ case object MajorFacilityLogic * @return the next behavior for this control agency messaging system */ def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = { + import net.psforever.objects.GlobalDefinitions entity match { case gen: Generator => if (generatorStateChange(details, gen, data)) { @@ -176,12 +178,24 @@ case object MajorFacilityLogic case _ => log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.") } - // When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state - building.HackableAmenities.foreach(amenity => { - if (amenity.HackedBy.isDefined) { - building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity)) - } - }) + // When a CC is hacked (or resecured) clear hacks on amenities based on currently installed virus + val hackedAmenities = building.HackableAmenities.filter(_.HackedBy.isDefined) + val amenitiesToClear = building.virusId match { + case 0 => + hackedAmenities.filterNot(a => a.Definition == GlobalDefinitions.lock_external || a.Definition == GlobalDefinitions.main_terminal) + case 4 => + hackedAmenities.filterNot(a => a.Definition == GlobalDefinitions.order_terminal || a.Definition == GlobalDefinitions.main_terminal) + case 8 => + hackedAmenities + case _ => + hackedAmenities + } + amenitiesToClear.foreach { amenity => + building.Zone.LocalEvents ! LocalServiceMessage( + amenity.Zone.id, + LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity) + ) + } // No map update needed - will be sent by `HackCaptureActor` when required case _ => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) @@ -231,6 +245,8 @@ case object MajorFacilityLogic } setFactionTo(details, PlanetSideEmpire.NEUTRAL) details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply = false + details.building.Zone.lockedBy = PlanetSideEmpire.NEUTRAL + details.building.Zone.NotifyContinentalLockBenefits(details.building.Zone, details.building) Behaviors.same } @@ -287,6 +303,12 @@ case object MajorFacilityLogic building.PlayersInSOI.foreach { player => events ! AvatarServiceMessage(player.Name, msg) } + if (building.hasCavernLockBenefit) { + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0)) + ) + } false case Some(GeneratorControl.Event.Destroyed) => true diff --git a/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala b/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala index 194ce913c..b3eb01c1f 100644 --- a/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala +++ b/src/main/scala/net/psforever/actors/zone/building/WarpGateLogic.scala @@ -122,9 +122,6 @@ case object WarpGateLogic } updateBroadcastCapabilitiesOfWarpGate(details, wg, setBroadcastTo) updateBroadcastCapabilitiesOfWarpGate(details, otherWg, setBroadcastTo) - if (wg.Zone.map.cavern && !otherWg.Zone.map.cavern) { - otherWg.Zone.actor ! ZoneActor.ZoneMapUpdate() - } case (Some(_), Some(wg : WarpGate), Some(otherWg : WarpGate), None) => handleWarpGateDeadendPair(details, otherWg, wg) diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 96aae6eb6..c5b198fb1 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -1125,6 +1125,8 @@ object GlobalDefinitions { val medical_terminal = new MedicalTerminalDefinition(529) + val medical_terminal_healing_module = new MedicalTerminalDefinition(530) + val portable_med_terminal = new MedicalTerminalDefinition(689) val pad_landing_frame = new MedicalTerminalDefinition(618) @@ -1239,6 +1241,8 @@ object GlobalDefinitions { val vanu_control_console = new CaptureTerminalDefinition(930) // Cavern CC + val main_terminal = new MainTerminalDefinition(473) + val llm_socket = new CaptureFlagSocketDefinition() val capture_flag = new CaptureFlagDefinition() diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 8297e711c..c0023ebb6 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -91,6 +91,7 @@ class Player(var avatar: Avatar) var outfit_window_open: Boolean = false var outfit_list_open: Boolean = false var maxAutoRunEnabled: Boolean = false + var protectedWhileZoning: Boolean = false /** From PlanetsideAttributeMessage */ var PlanetsideAttribute: Array[Long] = Array.ofDim(120) diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index 6fb9036eb..3658294d5 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -114,6 +114,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) private var cloaked: Boolean = false private var flying: Option[Int] = None private var capacitor: Int = 0 + var protectedWhileZoning: Boolean = false /** * Permissions control who gets to access different parts of the vehicle; diff --git a/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala b/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala index a527095ea..7ce23c412 100644 --- a/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala +++ b/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala @@ -72,9 +72,9 @@ object FirstTimeEvents { ) val Other: Set[String] = Set( - "used_nchev_scattercannon", - "used_nchev_falcon", - "used_nchev_sparrow", + "used_nc_hev_scattercannon", + "used_nc_hev_falcon", + "used_nc_hev_sparrow", "used_energy_gun_nc", "visited_portable_manned_turret_nc" ) diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index dea237e9c..972106aa9 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -407,10 +407,28 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm terminalUsedAction case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => + import net.psforever.objects.serverobject.structures.Building log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}") val originalSuit = player.ExoSuit val originalSubtype = Loadout.DetermineSubtype(player) - val dropPred = ContainableBehavior.DropPredicate(player) + val terminalOpt: Option[Terminal] = + player.Zone.GUID(msg.terminal_guid).collect { + case t: Terminal => t + } + val hasCavernEquipmentBenefit: Boolean = + terminalOpt.exists { terminal => + terminal.Owner match { + case fac: Building => + fac.hasCavernLockBenefit && player.Faction == fac.Faction + case _ => + false + } + } + val dropPred = + if (hasCavernEquipmentBenefit) + ContainableBehavior.DropPredicateEquipmentBenefit(player) + else + ContainableBehavior.DropPredicate(player) //determine player's next exo-suit val (nextSuit, nextSubtype) = { lazy val fallbackSuit = if (Players.CertificationToUseExoSuit(player, originalSuit, originalSubtype)) { @@ -790,7 +808,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm target: Target, applyDamageTo: Output ): Unit = { - if (player.isAlive && !player.spectator) { + if (player.isAlive && !player.spectator && !player.protectedWhileZoning) { val originalHealth = player.Health val originalArmor = player.Armor val originalStamina = player.avatar.stamina diff --git a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index b363fcee4..fa0a41787 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -5,6 +5,7 @@ import net.psforever.objects.equipment.{Equipment, EquipmentSlot} import net.psforever.objects.{PlanetSideGameObject, Vehicle} import net.psforever.packet.game.objectcreate._ import net.psforever.types.{DriveState, PlanetSideGUID, VehicleFormat} +import net.psforever.zones.Zones import scala.util.{Failure, Success, Try} @@ -14,6 +15,8 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { override def ConstructorData(obj: Vehicle): Try[VehicleData] = { val health = StatConverter.Health(obj.Health, obj.MaxHealth) + val boosted = if (Zones.zones.find(_.Number == 3).exists(_.benefitRecipient == obj.Faction)) true + else false if (health > 0) { //active Success( VehicleData( @@ -32,7 +35,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { case None => PlanetSideGUID(0) } ), - unk3 = false, + boostMaxHealth = boosted, health, unk4 = false, no_mount_points = false, @@ -59,7 +62,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { v5 = None, guid = PlanetSideGUID(0) ), - unk3 = false, + boostMaxHealth = boosted, health = 0, unk4 = false, no_mount_points = true, diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala index dae476f0f..867db90a7 100644 --- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala +++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala @@ -29,13 +29,23 @@ object EffectTarget { //noinspection ScalaUnusedSymbol def Valid(target: PlanetSideGameObject): Boolean = true - def Medical(target: PlanetSideGameObject): Boolean = + def Medical(target: PlanetSideGameObject): Boolean = { target match { case p: Player => p.Health > 0 && (p.Health < p.MaxHealth || p.Armor < p.MaxArmor) case _ => false } + } + + def HealthModule(target: PlanetSideGameObject): Boolean = { + target match { + case p: Player => + p.Health > 0 && p.Health < 120 + case _ => + false + } + } def HealthCrystal(target: PlanetSideGameObject): Boolean = target match { diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index d60c9d5ed..ff136ba94 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -370,6 +370,15 @@ object GlobalDefinitionsMiscellaneous { medical_terminal.RepairIfDestroyed = true medical_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.711f, height = 1.75f) + medical_terminal_healing_module.Name = "medical_terminal_healing_module" + medical_terminal_healing_module.Interval = 2000 + medical_terminal_healing_module.HealAmount = 1 + medical_terminal_healing_module.ArmorAmount = 0 + medical_terminal_healing_module.UseRadius = 300 + medical_terminal_healing_module.TargetValidation += EffectTarget.Category.Player -> EffectTarget.Validation.HealthModule + medical_terminal_healing_module.Damageable = false + medical_terminal_healing_module.Repairable = false + adv_med_terminal.Name = "adv_med_terminal" adv_med_terminal.Interval = 500 adv_med_terminal.HealAmount = 8 @@ -720,6 +729,10 @@ object GlobalDefinitionsMiscellaneous { vanu_control_console.Repairable = false vanu_control_console.FacilityHackTime = 10.minutes + main_terminal.Name = "main_terminal" + main_terminal.Damageable = false + main_terminal.Repairable = false + lodestar_repair_terminal.Name = "lodestar_repair_terminal" lodestar_repair_terminal.Interval = 1000 lodestar_repair_terminal.HealAmount = 60 diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala index 2267824f7..737c114d2 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala @@ -1782,6 +1782,8 @@ object GlobalDefinitionsProjectile { spitfire_aa_ammo_projectile.ProjectileDamageTypeSecondary = DamageType.Splash spitfire_aa_ammo_projectile.InitialVelocity = 100 spitfire_aa_ammo_projectile.Lifespan = 5f + spitfire_aa_ammo_projectile.DamageToArmorFirst = true + spitfire_aa_ammo_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(spitfire_aa_ammo_projectile) spitfire_aa_ammo_projectile.Modifiers = List( CerberusTurretWrongTarget, @@ -1797,6 +1799,7 @@ object GlobalDefinitionsProjectile { spitfire_ammo_projectile.DegradeMultiplier = 0.5f spitfire_ammo_projectile.InitialVelocity = 100 spitfire_ammo_projectile.Lifespan = .5f + spitfire_ammo_projectile.DamageToArmorFirst = true spitfire_ammo_projectile.DamageToBattleframeOnly = true ProjectileDefinition.CalculateDerivedFields(spitfire_ammo_projectile) diff --git a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala index ef1d4679b..df35f955a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala +++ b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -11,7 +11,9 @@ object CommonMessages { final case class Hack(player: Player, obj: PlanetSideServerObject with Hackable, data: Option[Any] = None) final case class ClearHack() final case class EntityHackState(obj: PlanetSideGameObject with Hackable, hackState: Boolean) - + final case class UploadVirus(player: Player, data: Option[Any] = None, virus: Long) + final case class RemoveVirus(player: Player, data: Option[Any]) + /** * The message that progresses some form of user-driven activity with a certain eventual outcome * and potential feedback per cycle. diff --git a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala index d3680de99..77bf4c960 100644 --- a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala @@ -681,6 +681,19 @@ object ContainableBehavior { entry.obj.isInstanceOf[BoomerTrigger] || (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) } + + /** + * Same as above except the terminal used is from a facility that has cavern equipment benefit + * so allow cavern equipment to be kept + */ + def DropPredicateEquipmentBenefit(tplayer: Player): InventoryItem => Boolean = + entry => { + val objDef = entry.obj.Definition + val faction = GlobalDefinitions.isFactionEquipment(objDef) + objDef == GlobalDefinitions.router_telepad || + entry.obj.isInstanceOf[BoomerTrigger] || + (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) + } } object Containable { diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala index 2519ddc4e..e49cd2d8b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableVehicle.scala @@ -75,7 +75,7 @@ trait DamageableVehicle val shields = obj.Shields val damageToHealth = originalHealth - health val damageToShields = originalShields - shields - if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) { + if (WillAffectTarget(target, damageToHealth + damageToShields, cause) && !obj.protectedWhileZoning) { target.LogActivity(cause) DamageLog( target, diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala index 74d9e4744..7eb82fe0e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala @@ -1,6 +1,7 @@ // Copyright (c) 2020-2024 PSForever package net.psforever.objects.serverobject.environment +import net.psforever.objects.serverobject.interior.Sidedness import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle} import net.psforever.objects.vital.Vitality import net.psforever.types.Vector3 @@ -22,8 +23,8 @@ object EnvironmentAttribute { (obj.Definition.DrownAtMaxDepth || obj.Definition.DisableAtMaxDepth) && canInteractWithPlayersAndVehicles(obj) && (obj match { - case p: Player => p.VehicleSeated.isEmpty - case v: Vehicle => v.MountedIn.isEmpty + case p: Player => p.VehicleSeated.isEmpty && p.WhichSide == Sidedness.OutsideOf + case v: Vehicle => v.MountedIn.isEmpty && v.WhichSide == Sidedness.OutsideOf case _ => false }) } diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala index de99cf44e..2d089817b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala @@ -152,7 +152,7 @@ object Watery { val oldTimeRemaining: Long = math.max(0, completionTime - System.currentTimeMillis()) val oldTimeRatio: Float = oldTimeRemaining / oldDuration.toFloat val percentage: Float = oldTimeRatio * 100 - val recoveryTime: Long = newDuration * (1f - oldTimeRatio).toLong + val recoveryTime: Long = (newDuration * (1f - oldTimeRatio)).toLong (true, recoveryTime, percentage) case Some(OxygenState.Recovery) => //interrupted while recovering, calculate the progress and keep recovering 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 5c6aca70c..5933beb0d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala +++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala @@ -1,11 +1,13 @@ // Copyright (c) 2020 PSForever 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.terminals.Terminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal -import net.psforever.objects.{Player, Vehicle} +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7} +import net.psforever.packet.game.{GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -140,6 +142,141 @@ object GenericHackables { } } + def FinishVirusAction(target: PlanetSideServerObject with Hackable, user: Player, hackValue: Int, hackClearValue: Int, virus: Long)(): Unit = { + import akka.pattern.ask + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + val tplayer = user + ask(target.Actor, CommonMessages.Hack(tplayer, target))(timeout = 2 second) + .mapTo[CommonMessages.EntityHackState] + .onComplete { + case Success(_) => + val building = target.asInstanceOf[Terminal].Owner.asInstanceOf[Building] + val zone = target.Zone + val zoneId = zone.id + val pguid = tplayer.GUID + if (tplayer.Faction == target.Faction) { + //clear virus + val currVirus = building.virusId + building.virusId = 8 + building.virusInstalledBy = None + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction + .ClearTemporaryHack(pguid, target) + ) + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(GenericObjectActionMessage(target.GUID, 60)) + ) + currVirus match { + case 0L => + building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.lock_external).foreach { iff => + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.ClearTemporaryHack(PlanetSideGUID(0), iff) + ) + } + case 4L => + building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.order_terminal).foreach { term => + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.ClearTemporaryHack(PlanetSideGUID(0), term) + ) + } + case _ => () + } + building.Actor ! BuildingActor.MapUpdate() + } + else { + //install virus + val currVirus = building.virusId + //clear previous virus unlocks to prevent virus stacking + currVirus match { + case 0L => + if (virus != 0) { + building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.lock_external).foreach { iff => + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.ClearTemporaryHack(PlanetSideGUID(0), iff) + ) + } + } + case 4L => + if (virus != 4) { + building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.order_terminal).foreach { term => + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.ClearTemporaryHack(PlanetSideGUID(0), term) + ) + } + } + case _ => () + } + val virusLength: Map[Long, Int] = Map( + 0L -> 3600, + 1L -> 900, + 2L -> 3600, + 3L -> 900, + 4L -> 120 + ) + val installedVirusDuration = virusLength(virus) + val hackStateMap: Map[Long, HackState7] = Map( + 0L -> HackState7.UnlockDoors, + 1L -> HackState7.DisableLatticeBenefits, + 2L -> HackState7.NTUDrain, + 3L -> HackState7.DisableRadar, + 4L -> HackState7.AccessEquipmentTerms + ) + val hackState = hackStateMap.getOrElse(virus, HackState7.Unk8) + building.virusId = virus + building.virusInstalledBy = Some(tplayer.Faction.id) + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.TriggerSound(pguid, TriggeredSound.TREKSuccessful, tplayer.Position, 30, 0.49803925f) + ) + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction + .HackTemporarily(pguid, zone, target, installedVirusDuration, hackClearValue, installedVirusDuration, unk2=hackState) + ) + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(GenericObjectActionMessage(target.GUID, 61)) + ) + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.SendResponse(GenericObjectActionMessage(target.GUID, 58)) + ) + //amenities if applicable + virus match { + case 0L => + building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.lock_external).foreach{ iff => + var setHacked = iff.asInstanceOf[PlanetSideServerObject with Hackable] + setHacked.HackedBy = tplayer + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.HackTemporarily(pguid, zone, iff, hackValue, hackClearValue, installedVirusDuration) + ) + } + case 4L => + building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.order_terminal).foreach{ term => + var setHacked = term.asInstanceOf[PlanetSideServerObject with Hackable] + setHacked.HackedBy = tplayer + zone.LocalEvents ! LocalServiceMessage( + zoneId, + LocalAction.HackTemporarily(pguid, zone, term, hackValue, hackClearValue, installedVirusDuration) + ) + } + case _ => () + } + building.Actor ! BuildingActor.MapUpdate() + } + case Failure(_) => + log.warn(s"Virus action failed on target: ${target.Definition.Name}@${target.GUID.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 diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala index 8e4d07bc1..7bbb1123c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala +++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlLoadVehicle.scala @@ -10,6 +10,7 @@ import net.psforever.objects.zones.Zone import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.Vector3 +import net.psforever.zones.Zones import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -44,6 +45,12 @@ class VehicleSpawnControlLoadVehicle(pad: VehicleSpawnPad) extends VehicleSpawnC ) //appear below the trench and doors vehicle.WhichSide = pad.WhichSide vehicle.Cloaked = vehicle.Definition.CanCloak && driver.Cloaked + // increase MaxHealth by 10% if driver has Cyssor empire armor benefit + if (Zones.zones.find(_.Number == 3).exists(_.benefitRecipient == driver.Faction)) { + val boosted = Math.round(vehicle.MaxHealth * 1.1).toInt + vehicle.MaxHealth = boosted + vehicle.Health = boosted + } temp = Some(order) val result = ask(pad.Zone.Transport, Zone.Vehicle.Spawn(vehicle)) diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala index 1d2f7c73b..b880045d4 100644 --- a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala +++ b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala @@ -270,20 +270,19 @@ trait AmenityAutoRepair autoRepairTimer.cancel() autoRepairQueueTask = Some(System.currentTimeMillis() + delay) val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate //doubled intentionally - autoRepairTimer = if(AutoRepairObject.Owner == Building.NoBuilding) { - //without an owner, auto-repair freely - context.system.scheduler.scheduleOnce( - delay milliseconds, - self, - NtuCommand.Grant(null, modifiedDrain) - ) - } else { - //ask politely - context.system.scheduler.scheduleOnce( - delay milliseconds, - AutoRepairObject.Owner.Actor, - BuildingActor.Ntu(NtuCommand.Request(modifiedDrain, ntuGrantActorRef)) - ) - } + AutoRepairObject.Owner match { + case Building.NoBuilding => + autoRepairTimer = context.system.scheduler.scheduleOnce( + delay.milliseconds, + self, + NtuCommand.Grant(null, modifiedDrain)) + case b: Building => + val doubledDrain = if (b.virusId == 2) modifiedDrain * 2 else modifiedDrain + autoRepairTimer = context.system.scheduler.scheduleOnce( + delay.milliseconds, + b.Actor, + BuildingActor.Ntu(NtuCommand.Request(doubledDrain, ntuGrantActorRef))) + case _ => () + } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala b/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala index 3efb93c97..232a3c8c5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala @@ -58,7 +58,7 @@ trait RepairableEntity extends Repairable { */ protected def CanPerformRepairs(target: Repairable.Target, player: Player, item: Tool): Boolean = { val definition = target.Definition - definition.Repairable && target.Health < definition.MaxHealth && (definition.RepairIfDestroyed || !target.Destroyed) && + definition.Repairable && target.Health < target.MaxHealth && (definition.RepairIfDestroyed || !target.Destroyed) && (target.Faction == player.Faction || target.Faction == PlanetSideEmpire.NEUTRAL) && item.Magazine > 0 && player.isAlive && Vector3.Distance(target.Position, player.Position) < definition.RepairDistance } 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 adcf381b1..a7863ab56 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.Zone import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.{BuildingInfoUpdateMessage, DensityLevelUpdateMessage} +import net.psforever.packet.game.{Additional3, BuildingInfoUpdateMessage, DensityLevelUpdateMessage} import net.psforever.types._ import scalax.collection.{Graph, GraphEdge} import akka.actor.typed.scaladsl.adapter._ @@ -34,6 +34,9 @@ class Building( 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 + var hasCavernLockBenefit: Boolean = false super.Zone_=(zone) super.GUID_=(PlanetSideGUID(building_guid)) //set Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later @@ -204,13 +207,22 @@ class Building( } val cavernBenefit: Set[CavernBenefit] = if ( generatorState != PlanetSideGeneratorState.Destroyed && - faction != PlanetSideEmpire.NEUTRAL && - connectedCavern().nonEmpty + faction != PlanetSideEmpire.NEUTRAL && !CaptureTerminalIsHacked && + connectedCavern().exists(_.Zone.lockedBy == faction) ) { - Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule) + hasCavernLockBenefit = true + Set(CavernBenefit.VehicleModule, CavernBenefit.EquipmentModule, CavernBenefit.ShieldModule, + CavernBenefit.SpeedModule, CavernBenefit.HealthModule, CavernBenefit.PainModule) } else { + hasCavernLockBenefit = false Set(CavernBenefit.None) } + val (installedVirus, installedByFac) = if (virusId == 8) { + (8, None) + } + else { + (virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3)))) + } BuildingInfoUpdateMessage( Zone.Number, @@ -230,8 +242,8 @@ class Building( unk4 = Nil, unk5 = 0, unk6 = false, - unk7 = 8, // unk7 != 8 will cause malformed packet - unk7x = None, + installedVirus, + installedByFac, boostSpawnPain, boostGeneratorPain ) diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala index 90c750b00..a4962f5a9 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala @@ -45,6 +45,7 @@ trait FacilityHackParticipation extends ParticipationLogic { .filterNot { p => playerContribution.exists { case (u, _) => p.CharId == u } } + informOfInstalledVirus(newParticipants) playerContribution = vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++ newParticipants.map { p => (p.CharId, (p, 1, curr)) } ++ @@ -96,6 +97,27 @@ trait FacilityHackParticipation extends ParticipationLogic { }) :+ newEntry } } + + /** + * send packet that makes building lights green in case a virus was installed before this player got there + * @param list new players to the SOI + */ + protected def informOfInstalledVirus(list : List[Player]): Unit = { + if (building.virusId != 8) { + import net.psforever.objects.serverobject.terminals.Terminal + import net.psforever.objects.GlobalDefinitions + import net.psforever.services.Service + import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} + val mainTerm = building.Amenities.filter(x => x.isInstanceOf[Terminal] && x.Definition == GlobalDefinitions.main_terminal).head.GUID + val msg1 = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, mainTerm, 61) + val msg2 = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, mainTerm, 58) + val events = building.Zone.AvatarEvents + list.foreach { p => + events ! AvatarServiceMessage(p.Name, msg1) + events ! AvatarServiceMessage(p.Name, msg2) + } + } + } } object FacilityHackParticipation { diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/MainTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/MainTerminalDefinition.scala new file mode 100644 index 000000000..270d6bfaf --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/MainTerminalDefinition.scala @@ -0,0 +1,16 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.serverobject.terminals + +import akka.actor.ActorContext +import net.psforever.objects.{Default, Player} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.structures.Amenity + +/** + * The definition for any `Terminal` that is of a type "main_terminal". + * Main terminal objects are used to upload or remove a virus from a major facility + * @param objectId the object's identifier number + */ +class MainTerminalDefinition(objectId: Int) extends TerminalDefinition(objectId) { + def Request(player: Player, msg: Any): Terminal.Exchange = Terminal.NoDeal() +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala index 4e81bac5c..b26f1d617 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala @@ -247,6 +247,9 @@ object ProximityTerminalControl { target: PlanetSideGameObject ): Boolean = { (terminal.Definition, target) match { + case (_: MedicalTerminalDefinition, p: Player) + if terminal.Definition == + GlobalDefinitions.medical_terminal_healing_module => HealthModule(terminal, p) case (_: MedicalTerminalDefinition, p: Player) => HealthAndArmorTerminal(terminal, p) case (_: WeaponRechargeTerminalDefinition, p: Player) => WeaponRechargeTerminal(terminal, p) case (_: MedicalTerminalDefinition, v: Vehicle) => VehicleRepairTerminal(terminal, v) @@ -269,6 +272,16 @@ object ProximityTerminalControl { fullHeal && fullRepair } + /** + * Activated by a facility having a linked cavern lock or health module installed. Friendly players + * within the SOI receive constant healing as requested by the client + */ + def HealthModule(unit: Terminal with ProximityUnit, target: Player): Boolean = { + val medDef = unit.Definition.asInstanceOf[MedicalTerminalDefinition] + val fullHeal = HealthModuleAction(unit, target, medDef.HealAmount, PlayerHealthCallback) + fullHeal + } + /** * When driving a vehicle close to a rearm/repair silo, * restore the vehicle's health points. @@ -318,6 +331,35 @@ object ProximityTerminalControl { } } + /** + * Heals players and increases their health/max health up to 120 if they enter the SOI this benefit is active in. + */ + def HealthModuleAction( + terminal: Terminal, + target: PlanetSideGameObject with Vitality with ZoneAware, + healAmount: Int, + updateFunc: PlanetSideGameObject with Vitality with ZoneAware => Unit + ): Boolean = { + val maxHealthCap = 120 + val zone = target.Zone + val oldMax = target.MaxHealth + val newMax = math.min(oldMax + healAmount, maxHealthCap) + + if (oldMax < maxHealthCap) { + target.MaxHealth = newMax + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, + AvatarAction.PlanetsideAttributeToAll(target.GUID, 1, newMax) + ) + } + if (target.Health < newMax) { + target.Health = math.min(target.Health + healAmount, newMax) + target.LogActivity(HealFromTerminal(AmenitySource(terminal), 1)) + updateFunc(target) + } + target.Health == newMax + } + def PlayerHealthCallback(target: PlanetSideGameObject with Vitality with ZoneAware): Unit = { val zone = target.Zone zone.AvatarEvents ! AvatarServiceMessage( diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index 94c340fde..b3a80776e 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -2,7 +2,7 @@ package net.psforever.objects.serverobject.terminals import akka.actor.ActorRef -import net.psforever.objects.{GlobalDefinitions, SimpleItem} +import net.psforever.objects.{GlobalDefinitions, SimpleItem, Tool} import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable.Target @@ -57,7 +57,28 @@ class TerminalControl(term: Terminal) ) case _ => () } - + case CommonMessages.UploadVirus(player, Some(item: Tool), virus) + if item.Definition == GlobalDefinitions.trek => + term.Owner match { + case _: Building => + sender() ! CommonMessages.Progress( + 1.66f, + GenericHackables.FinishVirusAction(term, player, hackValue = -1, hackClearValue = -1, virus), + GenericHackables.HackingTickAction(HackState1.Unk1, player, term, item.GUID) + ) + case _ => () + } + case CommonMessages.RemoveVirus(player, Some(item: SimpleItem)) + if item.Definition == GlobalDefinitions.remote_electronics_kit => + term.Owner match { + case _: Building => + sender() ! CommonMessages.Progress( + 1.66f, + GenericHackables.FinishVirusAction(term, player, hackValue = -1, hackClearValue = -1, virus=8L), + GenericHackables.HackingTickAction(HackState1.Unk1, player, term, item.GUID) + ) + case _ => () + } case _ => () } diff --git a/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala b/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala index 40c749d1a..2efb24ae7 100644 --- a/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala +++ b/src/main/scala/net/psforever/objects/vital/collision/CollisionReason.scala @@ -99,5 +99,6 @@ object CollisionReason { * Damage is considered `Direct`, however, which defines some resistance. */ val noDamage = new DamageProperties { CausesDamageType = DamageType.Direct + DamageToArmorFirst = true } } diff --git a/src/main/scala/net/psforever/objects/vital/etc/PainboxReason.scala b/src/main/scala/net/psforever/objects/vital/etc/PainboxReason.scala index 764697bd8..78753916c 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/PainboxReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/PainboxReason.scala @@ -1,7 +1,9 @@ // Copyright (c) 2020 PSForever package net.psforever.objects.vital.etc +import net.psforever.objects.Vehicle import net.psforever.objects.serverobject.painbox.Painbox +import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} import net.psforever.objects.vital.base.{DamageReason, DamageResolution} @@ -13,7 +15,18 @@ final case class PainboxReason(entity: Painbox) extends DamageReason { private val definition = entity.Definition assert(definition.innateDamage.nonEmpty, s"causal entity '${definition.Name}' does not emit pain field") - def source: DamageWithPosition = definition.innateDamage.get + def source: DamageWithPosition = { + val base = definition.innateDamage.get + entity.Owner match { + case b: Building if b.hasCavernLockBenefit => + new DamageWithPosition { + Damage0 = 5 + DamageRadius = 0 + DamageToHealthOnly = true + } + case _ => base + } + } def resolution: DamageResolution.Value = DamageResolution.Resolved diff --git a/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala b/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala index 5772b39d5..3e12bada4 100644 --- a/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala +++ b/src/main/scala/net/psforever/objects/vital/prop/DamageProperties.scala @@ -21,7 +21,11 @@ trait DamageProperties private var damageTypeSecondary: DamageType.Value = DamageType.None /** against Infantry targets, damage does not apply to armor damage */ private var damageToHealthOnly: Boolean = false - /** against Vehicle targets, damage does not apply to vehicle shield */ + /** against Infantry targets, damage does not apply to armor damage */ + private var damageToArmorFirst: Boolean = false + /** against Infantry targets, damage applies to armor before it does health; + * regardless of other resistance conditions, non-zero armor is reduced before health; + * should not have priority over the flag for infantry health only */ private var damageToVehicleOnly: Boolean = false /** against battleframe targets, damage does not apply to battleframe robotics shield; * this is equivalent to the property "bfr_permeate_shield" */ @@ -84,6 +88,13 @@ trait DamageProperties DamageToHealthOnly } + def DamageToArmorFirst : Boolean = damageToArmorFirst + + def DamageToArmorFirst_=(armorFirst: Boolean) : Boolean = { + damageToArmorFirst = armorFirst + DamageToArmorFirst + } + def DamageToVehicleOnly : Boolean = damageToVehicleOnly def DamageToVehicleOnly_=(vehicleOnly: Boolean) : Boolean = { diff --git a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index cc6884073..44774f2c8 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -8,13 +8,15 @@ import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vehicles.VehicleSubsystemEntry import net.psforever.objects.vital.base.DamageResolution -import net.psforever.objects.vital.{DamagingActivity, Vitality, InGameHistory} +import net.psforever.objects.vital.{DamagingActivity, InGameHistory, Vitality} import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.resistance.ResistanceSelection import net.psforever.types.{ExoSuitType, ImplantType} +import scala.annotation.unused + /** * The base for the combining step of all projectile-induced damage calculation function literals. */ @@ -37,13 +39,16 @@ object ResolutionCalculations { type Output = PlanetSideGameObject with FactionAffinity => DamageResult type Form = (DamageCalculations.Selector, ResistanceSelection.Format, DamageInteraction) => Output - def NoDamage(data: DamageInteraction)(a: Int, b: Int): Int = 0 + + def NoDamage(@unused data: DamageInteraction)(@unused a: Int, @unused b: Int): Int = 0 def InfantryDamage(data: DamageInteraction): (Int, Int) => (Int, Int) = { data.target match { case target: PlayerSource => - if(data.cause.source.DamageToHealthOnly) { + if (data.cause.source.DamageToHealthOnly) { DamageToHealthOnly(target.health) + } else if (data.cause.source.DamageToArmorFirst) { + InfantryArmorDamageFirst(target.health, target.armor) } else { InfantryDamageAfterResist(target.health, target.armor) } @@ -84,6 +89,25 @@ object ResolutionCalculations { } } + def InfantryArmorDamageFirst(currentHP: Int, currentArmor: Int)(damages: Int, resistance: Int): (Int, Int) = { + if (damages > 0 && currentHP > 0) { + if (currentArmor <= 0) { + (damages, 0) //no armor; health damage + } else if (damages > resistance) { + val resistedDam = damages - resistance + if (resistedDam <= currentArmor) { + (0, resistedDam) //armor damage + } else { + (resistedDam, currentArmor) //deplete armor; health damage + } + } else { + (0, damages) //too weak; armor damage (less than resistance) + } + } else { + (0, 0) //no damage + } + } + def MaxDamage(data: DamageInteraction): (Int, Int) => (Int, Int) = { data.target match { case target: PlayerSource => @@ -131,7 +155,7 @@ object ResolutionCalculations { } } - def NoApplication(damageValue: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = { + def NoApplication(@unused damageValue: Int, data: DamageInteraction)(target: PlanetSideGameObject with FactionAffinity): DamageResult = { val sameTarget = SourceEntry(target) DamageResult(sameTarget, sameTarget, data) } diff --git a/src/main/scala/net/psforever/objects/zones/MapInfo.scala b/src/main/scala/net/psforever/objects/zones/MapInfo.scala index 3875a2287..29a31bb76 100644 --- a/src/main/scala/net/psforever/objects/zones/MapInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/MapInfo.scala @@ -51,34 +51,16 @@ case object MapInfo extends StringEnum[MapInfo] { scale = MapScale.Dim8192, hotSpotSpan = 80, environment = { - //exclude parts of voltan and naum due to their generator rooms being below sealevel - val northVoltan = 3562.4844f - val southVoltan = 3401.6875f - val eastVoltan = 4556.703f - val westVoltan = 4411.6875f - val northNaum = 3575.8047f - val southNaum = 3539.5234f - val eastNaum = 5490.6875f - val westNaum = 5427.078f - List( - Pool(EnvironmentAttribute.Water, 11, 8192, westVoltan, 0, 0), //west of voltan - Pool(EnvironmentAttribute.Water, 11, 8192, westNaum, 0, eastVoltan), //between voltan and naum - Pool(EnvironmentAttribute.Water, 11, 8192, 8192, 0, eastNaum), //east of naum - Pool(EnvironmentAttribute.Water, 11, 8192, eastVoltan, northVoltan, westVoltan), //north of voltan - Pool(EnvironmentAttribute.Water, 11, southVoltan, eastVoltan, 0, westVoltan), //south of voltan - Pool(EnvironmentAttribute.Water, 11, 8192, eastNaum, northNaum, westNaum), //north of naum - Pool(EnvironmentAttribute.Water, 11, southNaum, eastNaum, 0, westNaum) //south of naum - //TODO voltan Killplane - //TODO naum Killplane - ) ++ MapEnvironment.zoneMapEdgeKillPlane( - MapScale.Dim8192, - (400, 400, 200, 400), - List( - (450, 450, 250, 450, 3), - (500, 500, 300, 500, 2), - (600, 600, 400, 600, 1) + List(SeaLevel(EnvironmentAttribute.Water, 11)) ++ + MapEnvironment.zoneMapEdgeKillPlane( + MapScale.Dim8192, + (400, 400, 200, 400), + List( + (450, 450, 250, 450, 3), + (500, 500, 300, 500, 2), + (600, 600, 400, 600, 1) + ) ) - ) } ) @@ -185,8 +167,7 @@ case object MapInfo extends StringEnum[MapInfo] { hotSpotSpan = 80, environment = List( SeaLevel(EnvironmentAttribute.Water, 10.03125f), - Pool(EnvironmentAttribute.Water, 213.03125f, 3116.7266f, 4724.414f, 2685.8281f, 4363.461f), //east side of southwest of tootega - Pool(EnvironmentAttribute.Water, 213.03125f, 2994.2969f, 4363.461f, 2685.8281f, 4187.4375f), //west side of southwest of tootega + Pool(EnvironmentAttribute.Water, 213.03125f, 3116.7266f, 4724.414f, 2685.8281f, 4187.4375f) //southwest of tootega ) ++ MapEnvironment.zoneMapEdgeKillPlane( MapScale.Dim8192, (400, 400, 400, 400), @@ -265,15 +246,16 @@ case object MapInfo extends StringEnum[MapInfo] { checksum = 230810349L, scale = MapScale.Dim8192, hotSpotSpan = 80, - environment = List(SeaLevel(EnvironmentAttribute.Water, 28)) ++ MapEnvironment.zoneMapEdgeKillPlane( - MapScale.Dim8192, - (200, 200, 200, 200), - List( - (250, 250, 250, 250, 3), - (300, 300, 300, 300, 2), - (400, 400, 400, 400, 1) + environment = List(SeaLevel(EnvironmentAttribute.Water, 28)) ++ + MapEnvironment.zoneMapEdgeKillPlane( + MapScale.Dim8192, + (200, 200, 200, 200), + List( + (250, 250, 250, 250, 3), + (300, 300, 300, 300, 2), + (400, 400, 400, 400, 1) + ) ) - ) ) case object Map11 @@ -370,13 +352,13 @@ case object MapInfo extends StringEnum[MapInfo] { scale = MapScale.Dim2560, hotSpotSpan = 80, environment = List( + SeaLevel(EnvironmentAttribute.Death, 10), Pool(EnvironmentAttribute.Water, 194.89062f, 1763.4141f, 1415.125f, 1333.9531f, 1280.4609f), //east, northern pool Pool(EnvironmentAttribute.Water, 192.40625f, 1717.5703f, 1219.3359f, 1572.8828f, 1036.1328f), //bottom, northern pool Pool(EnvironmentAttribute.Water, 192.32812f, 1966.1562f, 1252.7344f, 1889.8047f, 1148.5312f), //top, northern pool Pool(EnvironmentAttribute.Water, 191.65625f, 1869.1484f, 1195.6406f, 1743.8125f, 1050.7344f), //middle, northern pool Pool(EnvironmentAttribute.Water, 183.98438f, 914.33594f, 1369.5f, 626.03906f, 666.3047f), //upper southern pools - Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f), //lowest southern pool - SeaLevel(EnvironmentAttribute.Death, 10) + Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f) //lowest southern pool ) ) @@ -449,14 +431,8 @@ case object MapInfo extends StringEnum[MapInfo] { checksum = 3654267088L, scale = MapScale.Dim4096, hotSpotSpan = 80, - environment = List( - Pool(EnvironmentAttribute.Water, 3.5f, 2867f, 1228f, 1128f, 0f), //west - Pool(EnvironmentAttribute.Water, 3.5f, 4096f, 4096f, 2867f, 0f), //north - Pool(EnvironmentAttribute.Water, 3.5f, 2867f, 4096f, 1227f, 2900f), //east - Pool(EnvironmentAttribute.Water, 3.5f, 1227f, 4096f, 0f, 2000f), //southeast - Pool(EnvironmentAttribute.Water, 3.5f, 1128f, 2000f, 0f, 0f), //southwest - Pool(EnvironmentAttribute.Death, 0.5f, 2867f, 2900f, 1128f, 1228f), //central, kill - ) ++ MapEnvironment.dim4096MapEdgeKillPlanes + environment = List(SeaLevel(EnvironmentAttribute.Water, 3.5f)) ++ + MapEnvironment.dim4096MapEdgeKillPlanes ) case object Map99 diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 4deac1700..1caf87d72 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -32,9 +32,9 @@ import scalax.collection.GraphEdge._ import scala.util.Try import akka.actor.typed import net.psforever.actors.session.AvatarActor -import net.psforever.actors.zone.ZoneActor +import net.psforever.actors.zone.{BuildingActor, ZoneActor} import net.psforever.actors.zone.building.WarpGateLogic -import net.psforever.objects.avatar.Avatar +import net.psforever.objects.avatar.{Avatar, PlayerControl} import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.geometry.d3.VolumetricGeometry import net.psforever.objects.guid.pool.NumberPool @@ -56,6 +56,8 @@ import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.blockmap.{BlockMap, SectorPopulation} +import net.psforever.packet.game.EmpireBenefitsMessage.{ZoneBenefit, ZoneLock, ZoneLockBenefit, ZoneLockZone} +import net.psforever.packet.game.{EmpireBenefitsMessage, PropertyOverrideMessage} import net.psforever.services.Service import net.psforever.zones.Zones @@ -194,6 +196,16 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { */ private var zoneInitialized: Promise[Boolean] = Promise[Boolean]() + /** + * For ContinentalLockUpdateMessage + */ + var lockedBy: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL + + /** + * Used with lockedBy, but persists until another empire locks the cont + */ + var benefitRecipient: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL + /** * When the zone has completed initializing, this will be the future. * @see `init(ActorContext)` @@ -639,6 +651,142 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { } output.toList } + + def NotifyContinentalLockBenefits(zone: Zone, building: Building): Unit = { + building.Actor ! BuildingActor.ContinentalLock(zone) + ApplyHomeLockBenefits(building) + } + + def ApplyHomeLockBenefits(building: Building): Unit = { + val homeSets: Map[PlanetSideEmpire.Value, Set[Int]] = Map( + PlanetSideEmpire.TR -> Set(1, 2), + PlanetSideEmpire.VS -> Set(5, 6), + PlanetSideEmpire.NC -> Set(7, 10) + ) + val homePerks: Map[PlanetSideEmpire.Value, String] = Map( + PlanetSideEmpire.TR -> "battlewagon 15mmbullet prowler 105mmbullet threemanheavybuggy heavy_grenade_mortar apc_tr", + PlanetSideEmpire.VS -> "magrider pulse_battery heavy_rail_beam_battery twomanhoverbuggy flux_cannon_thresher_battery aurora fluxpod_ammo apc_vs", + PlanetSideEmpire.NC -> "thunderer gauss_cannon_ammo twomanheavybuggy firebird_missile vanguard 150mmbullet apc_nc" + ) + + def isLockedBy(homeSet: Set[Int], empire: PlanetSideEmpire.Value): Boolean = + Zones.zones.filter(z => homeSet.contains(z.Number)).forall(_.benefitRecipient == empire) + + val perks: Map[PlanetSideEmpire.Value, String] = + PlanetSideEmpire.values.map { empire => + val empirePerks = homeSets.collect { + case (owner, zoneSet) if owner != empire && isLockedBy(zoneSet, empire) => + homePerks(owner) + } + empire -> empirePerks.mkString(" ") + }.toMap + + if (perks.values.forall(_.isEmpty)) {/*do nothing*/} + else { + val overrideMsg = PropertyOverrideMessage(List(PropertyOverrideMessage.GamePropertyScope(0, List(PropertyOverrideMessage.GamePropertyTarget(343, + List(PropertyOverrideMessage.GameProperty("purchase_exempt_vs", perks(PlanetSideEmpire.VS)), + PropertyOverrideMessage.GameProperty("purchase_exempt_tr", perks(PlanetSideEmpire.TR)), + PropertyOverrideMessage.GameProperty("purchase_exempt_nc", perks(PlanetSideEmpire.NC)))))))) + building.Actor ! BuildingActor.HomeLockBenefits(overrideMsg) + } + val benefitMsg = BuildEmpireBenefits() + if (benefitMsg.zoneLocks.nonEmpty) { + building.Actor ! BuildingActor.HomeLockBenefits(benefitMsg) + } + } + + def ApplyHomeLockBenefitsOnLogin(player: Player): Unit = { + val homeSets: Map[PlanetSideEmpire.Value, Set[Int]] = Map( + PlanetSideEmpire.TR -> Set(1, 2), + PlanetSideEmpire.VS -> Set(5, 6), + PlanetSideEmpire.NC -> Set(7, 10) + ) + val homePerks: Map[PlanetSideEmpire.Value, String] = Map( + PlanetSideEmpire.TR -> "battlewagon 15mmbullet prowler 105mmbullet threemanheavybuggy heavy_grenade_mortar apc_tr", + PlanetSideEmpire.VS -> "magrider pulse_battery heavy_rail_beam_battery twomanhoverbuggy flux_cannon_thresher_battery aurora fluxpod_ammo apc_vs", + PlanetSideEmpire.NC -> "thunderer gauss_cannon_ammo twomanheavybuggy firebird_missile vanguard 150mmbullet apc_nc" + ) + + def isLockedBy(homeSet: Set[Int], empire: PlanetSideEmpire.Value): Boolean = + Zones.zones.filter(z => homeSet.contains(z.Number)).forall(_.benefitRecipient == empire) + + val perks: Map[PlanetSideEmpire.Value, String] = + PlanetSideEmpire.values.map { empire => + val empirePerks = homeSets.collect { + case (owner, zoneSet) if owner != empire && isLockedBy(zoneSet, empire) => + homePerks(owner) + } + empire -> empirePerks.mkString(" ") + }.toMap + + if (perks.values.forall(_.isEmpty)) {/*do nothing*/} + else { + val overrideMsg = PropertyOverrideMessage(List(PropertyOverrideMessage.GamePropertyScope(0, List(PropertyOverrideMessage.GamePropertyTarget(343, + List(PropertyOverrideMessage.GameProperty("purchase_exempt_vs", perks(PlanetSideEmpire.VS)), + PropertyOverrideMessage.GameProperty("purchase_exempt_tr", perks(PlanetSideEmpire.TR)), + PropertyOverrideMessage.GameProperty("purchase_exempt_nc", perks(PlanetSideEmpire.NC)))))))) + PlayerControl.sendResponse(player.Zone, player.Name, overrideMsg) + } + + val benefitMsg = BuildEmpireBenefits() + if (benefitMsg.zoneLocks.nonEmpty) { + PlayerControl.sendResponse(player.Zone, player.Name, benefitMsg) + } + } + + def BuildEmpireBenefits(): EmpireBenefitsMessage = { + val locks = scala.collection.mutable.ArrayBuffer[ZoneLock]() + val benefits = scala.collection.mutable.ArrayBuffer[ZoneBenefit]() + val homeSets: Map[PlanetSideEmpire.Value, Set[Int]] = Map( + PlanetSideEmpire.TR -> Set(1, 2), + PlanetSideEmpire.VS -> Set(5, 6), + PlanetSideEmpire.NC -> Set(7, 10) + ) + val benefitOfZones: Map[Int, Int] = Map( + 3 -> 6, // Cyssor gives benefit 6 (+10% armor bonus to vehicles) + 4 -> 1, // Ishundar gives benefit 1 (vehicle shields) + 9 -> 3 // Searhus gives benefit 3 (faster respawn) + ) + val homePerkBenefits: Map[PlanetSideEmpire.Value, Int] = Map( + PlanetSideEmpire.TR -> 7, + PlanetSideEmpire.NC -> 8, + PlanetSideEmpire.VS -> 9 + ) + def isLockedBy(homeSet: Set[Int], empire: PlanetSideEmpire.Value): Boolean = + Zones.zones.filter(z => homeSet.contains(z.Number)).forall(_.benefitRecipient == empire) + + // home zone perks + homeSets.foreach { case (owner, set) => + PlanetSideEmpire.values.filterNot(_ == PlanetSideEmpire.NEUTRAL).foreach { empire => + if (owner != empire && isLockedBy(set, empire)) { + locks += ZoneLock(empire, s"lock-${owner.toString.toLowerCase}-homes") + benefits += ZoneBenefit(empire, ZoneLockBenefit(homePerkBenefits(owner))) + } + } + } + + benefitOfZones.foreach { case (zoneNum, benefitId) => + Zones.zones.find(_.Number == zoneNum).foreach { z => + z.benefitRecipient match { + case PlanetSideEmpire.NEUTRAL => + //nothing + case empire => + locks += ZoneLock(empire, s"lock-z$zoneNum") + benefits += ZoneBenefit(empire, ZoneLockBenefit(benefitId)) + } + } + } + // all four islands together give benefit 4 (vehicle repair) + val islandZones: Set[Int] = Set(29, 30, 31, 32) + val islandBenefit: Int = 4 + PlanetSideEmpire.values.filterNot(_ == PlanetSideEmpire.NEUTRAL).foreach { empire => + if (isLockedBy(islandZones, empire)) { + locks += ZoneLock(empire, ZoneLockZone.i1_i2_i3_i4) + benefits += ZoneBenefit(empire, ZoneLockBenefit(islandBenefit)) + } + } + EmpireBenefitsMessage(locks.toVector, benefits.toVector) + } } /** diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 0253d2b80..54c4e3352 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -557,7 +557,7 @@ object GamePacketOpcode extends Enumeration { case 0xd4 => game.GenericObjectActionAtPositionMessage.decode case 0xd5 => game.PropertyOverrideMessage.decode case 0xd6 => game.WarpgateLinkOverrideMessage.decode - case 0xd7 => noDecoder(EmpireBenefitsMessage) + case 0xd7 => game.EmpireBenefitsMessage.decode // 0xd8 case 0xd8 => noDecoder(ForceEmpireMessage) case 0xd9 => game.BroadcastWarpgateUpdateMessage.decode diff --git a/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala b/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala index c59bed321..d8b6d86fa 100644 --- a/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala @@ -24,11 +24,12 @@ final case class Additional1(unk1: String, unk2: Int, unk3: Long) final case class Additional2(unk1: Int, unk2: Long) /** - * na - * @param unk1 na - * @param unk2 na + * Used for building information window on map. Tells the empire who installed the virus which one + * and tells the defending faction generic "Infected" + * @param inform_defenders na + * @param installed_by_id na */ -final case class Additional3(unk1: Boolean, unk2: Int) +final case class Additional3(inform_defenders: Boolean, installed_by_id: Int) /** * Update the state of map asset for a client's specific building's state. @@ -73,9 +74,14 @@ final case class Additional3(unk1: Boolean, unk2: Int) * @param unk4 na * @param unk5 na * @param unk6 na - * @param unk7 na; - * value != 8 causes the next field to be defined - * @param unk7x na + * @param virus_id id of virus installed. value != 8 causes the next field to be defined. + * 0 - unlock all doors + * 1 - disable linked benefits + * 2 - double ntu drain + * 3 - disable enemy radar + * 4 - access equipment terminals + * 8 - no virus installed - if 8, virus_installed_by is None + * @param virus_installed_by if virus_id = 8, None, else this has bool and id of the empire that installed the virus * @param boost_spawn_pain if the building has spawn tubes, the (boosted) strength of its enemy pain field * @param boost_generator_pain if the building has a generator, the (boosted) strength of its enemy pain field */ @@ -97,8 +103,8 @@ final case class BuildingInfoUpdateMessage( unk4: List[Additional2], unk5: Long, unk6: Boolean, - unk7: Int, - unk7x: Option[Additional3], + virus_id: Int, + virus_installed_by: Option[Additional3], boost_spawn_pain: Boolean, boost_generator_pain: Boolean ) extends PlanetSideGamePacket { @@ -129,8 +135,8 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] * A `Codec` for a set of additional fields. */ private val additional3_codec: Codec[Additional3] = ( - ("unk1" | bool) :: - ("unk2" | uint2L) + ("inform_defenders" | bool) :: + ("installed_by_id" | uint2L) ).as[Additional3] /** @@ -190,8 +196,8 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage] ("unk4" | listOfN(uint4L, additional2_codec)) :: ("unk5" | uint32L) :: ("unk6" | bool) :: - (("unk7" | uint4L) >>:~ { unk7 => - conditional(unk7 != 8, codec = "unk7x" | additional3_codec) :: + (("virus_id" | uint4L) >>:~ { virus_id => + conditional(virus_id != 8, codec = "virus_installed_by" | additional3_codec) :: ("boost_spawn_pain" | bool) :: ("boost_generator_pain" | bool) }) diff --git a/src/main/scala/net/psforever/packet/game/EmpireBenefitsMessage.scala b/src/main/scala/net/psforever/packet/game/EmpireBenefitsMessage.scala new file mode 100644 index 000000000..24a62a6d5 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/EmpireBenefitsMessage.scala @@ -0,0 +1,110 @@ +// Copyright (c) 2025 PSForever +package net.psforever.packet.game + +import net.psforever.packet.game.EmpireBenefitsMessage.{ZoneBenefit, ZoneLock} +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideEmpire +import scodec.Codec +import scodec.codecs._ + +import scala.language.implicitConversions + +/** + * EmpireBenefitsMessage + * + * zoneLocks gives the client information about which empire locks what continent. + * This produces a chat message. + * zoneBenefits tells the client what empire has which benefits enabled. + * This has to match zoneLocks to work properly. + */ +final case class EmpireBenefitsMessage( + zoneLocks: Vector[ZoneLock], + zoneBenefits: Vector[ZoneBenefit] + ) extends PlanetSideGamePacket { + type Packet = EmpireBenefitsMessage + def opcode = GamePacketOpcode.EmpireBenefitsMessage + def encode = EmpireBenefitsMessage.encode(this) +} + +object EmpireBenefitsMessage extends Marshallable[EmpireBenefitsMessage] { + + /** + * ZoneLockZone + * + * Available Types of Zones + * + * These zones can be used to notify the client of a lock. + */ + object ZoneLockZone extends Enumeration { + type Type = String + + val i1: ZoneLockZone.Value = Value("lock-i1") // Extinction Continental Lock + val i2: ZoneLockZone.Value = Value("lock-i2") // Ascension Continental Lock + val i3: ZoneLockZone.Value = Value("lock-i3") // Desolation Continental Lock + val i4: ZoneLockZone.Value = Value("lock-i4") // Nexus Continental Lock + val i1_i2_i3_i4: ZoneLockZone.Value = Value("lock-i1-i2-i3-i4") // Oshur Cluster Lock + val z3: ZoneLockZone.Value = Value("lock-z3") // Cyssor Continental Lock + val z4: ZoneLockZone.Value = Value("lock-z4") // Ishundar Continental Lock + val z9: ZoneLockZone.Value = Value("lock-z9") // Searhus Continental Lock + val tr_homes: ZoneLockZone.Value = Value("lock-tr-homes") // TR Home Continent Lock + val nc_homes: ZoneLockZone.Value = Value("lock-nc-homes") // NC Home Continent Lock + val vs_homes: ZoneLockZone.Value = Value("lock-vs-homes") // VS Home Continent Lock + + implicit def valueToType(v: ZoneLockZone.Value): Type = v.toString + implicit val codec: Codec[Type] = PacketHelpers.encodedStringAligned(6) + } + + /** + * ZoneLockBenefit + * + * Available Types of Benefits + * + * Benefits 0, 2 and 5 are unknown. Benefits for i1 to i4 are unknown and mapped incorrectly here. + */ + object ZoneLockBenefit extends Enumeration { + type Type = Value + + val i1: ZoneLockBenefit.Value = Value(-1) // Extinction Continental Lock + val i2: ZoneLockBenefit.Value = Value(-2) // Ascension Continental Lock + val i3: ZoneLockBenefit.Value = Value(-3) // Desolation Continental Lock + val i4: ZoneLockBenefit.Value = Value(-4) // Nexus Continental Lock + + // val unk0: ZoneLockBenefit.Value = Value(0) + val z4: ZoneLockBenefit.Value = Value(1) // Ishundar Continental Lock + // val unk2: ZoneLockBenefit.Value = Value(2) + val z9: ZoneLockBenefit.Value = Value(3) // Searhus Continental Lock + val i1_i2_i3_i4: ZoneLockBenefit.Value = Value(4) // Oshur Cluster Lock + // val unk5: ZoneLockBenefit.Value = Value(5) + val z3: ZoneLockBenefit.Value = Value(6) // Cyssor Continental Lock + val tr_homes: ZoneLockBenefit.Value = Value(7) // TR Home Continent Lock + val nc_homes: ZoneLockBenefit.Value = Value(8) // NC Home Continent Lock + val vs_homes: ZoneLockBenefit.Value = Value(9) // VS Home Continent Lock + + implicit val codec: Codec[Type] = PacketHelpers.createEnumerationCodec(this, uint16L) + } + + final case class ZoneLock( + empire: PlanetSideEmpire.Type, + zone: ZoneLockZone.Type, + ) + + final case class ZoneBenefit( + empire: PlanetSideEmpire.Type, + value: ZoneLockBenefit.Type + ) + + private implicit val zoneLockCodec: Codec[ZoneLock] = ( + ("empire" | PlanetSideEmpire.codec) :: + ("zone" | ZoneLockZone.codec) + ).as[ZoneLock] + + private implicit val zoneBenefitCodec: Codec[ZoneBenefit] = ( + ("empire" | PlanetSideEmpire.codec) :: + ("benefit" | ZoneLockBenefit.codec) + ).as[ZoneBenefit] + + implicit val codec: Codec[EmpireBenefitsMessage] = ( + ("zoneLocks" | vectorOfN(uint32L.xmap(_.toInt, _.toLong), zoneLockCodec)) :: + ("zoneBenefits" | vectorOfN(uint32L.xmap(_.toInt, _.toLong), zoneBenefitCodec)) + ).as[EmpireBenefitsMessage] +} diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala index 8d4b7e4cb..a7bee165e 100644 --- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala @@ -50,6 +50,9 @@ import shapeless.{::, HNil} * 53 - Put down an FDU
* 56 - Sets vehicle or player to be black ops
* 57 - Reverts player from black ops
+ * 58 - Virus installed, changes lighting in facility to green + * 60 - Virus purged + * 61 - Virus recently installed. Counts down from 2 minutes until a new virus can be uploaded *
* What are these values?
* 90? - for observed driven BFR's, model pitches up slightly and stops idle animation
diff --git a/src/main/scala/net/psforever/packet/game/HackMessage.scala b/src/main/scala/net/psforever/packet/game/HackMessage.scala index 1df1afb5d..b5d0c7e7b 100644 --- a/src/main/scala/net/psforever/packet/game/HackMessage.scala +++ b/src/main/scala/net/psforever/packet/game/HackMessage.scala @@ -28,11 +28,11 @@ sealed abstract class HackState7(val value: Int) extends IntEnumEntry object HackState7 extends IntEnum[HackState7] { val values: IndexedSeq[HackState7] = findValues - case object Unk0 extends HackState7(value = 0) - case object Unk1 extends HackState7(value = 1) - case object Unk2 extends HackState7(value = 2) - case object Unk3 extends HackState7(value = 3) - case object Unk4 extends HackState7(value = 4) + case object UnlockDoors extends HackState7(value = 0) + case object DisableLatticeBenefits extends HackState7(value = 1) + case object NTUDrain extends HackState7(value = 2) + case object DisableRadar extends HackState7(value = 3) + case object AccessEquipmentTerms extends HackState7(value = 4) case object Unk5 extends HackState7(value = 5) case object Unk6 extends HackState7(value = 6) case object Unk7 extends HackState7(value = 7) diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index 87f94fb5f..5a6a64231 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -201,7 +201,7 @@ import scodec.codecs._ * `68 - Vehicle shield health`
* `79 - ???`
* `80 - Damage vehicle (unknown value)`
- * `81 - ???`
+ * `81 - Player bailed from vehicle, causes bail animation `
* `113 - Vehicle capacitor - e.g. Leviathan EMP charge` * @param guid the object * @param attribute_type the field diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala index 67d102c91..f6524b324 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala @@ -44,7 +44,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl * -jammered - vehicles will not be jammered by setting this field
* -player_guid the vehicle's (official) owner; * a living player in the game world on the same continent as the vehicle who may mount the driver mount - * @param unk3 na + * @param boostMaxHealth vehicle gets 10% more armor from vehicle armor benefit given by Cyssor empire lock * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) * @param unk4 na * @param no_mount_points do not display entry points for the seats @@ -65,7 +65,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl final case class VehicleData( pos: PlacementData, data: CommonFieldData, - unk3: Boolean, + boostMaxHealth: Boolean, health: Int, unk4: Boolean, no_mount_points: Boolean, @@ -106,7 +106,7 @@ object VehicleData extends Marshallable[VehicleData] { cloak: Boolean, inventory: Option[InventoryData] ): VehicleData = { - VehicleData(pos, basic, unk3 = false, health, unk4 = false, no_mount_points = false, driveState, unk5 = false, unk6 = false, cloak = cloak, None, inventory)( + VehicleData(pos, basic, boostMaxHealth = false, health, unk4 = false, no_mount_points = false, driveState, unk5 = false, unk6 = false, cloak = cloak, None, inventory)( VehicleFormat.Normal ) } @@ -128,7 +128,7 @@ object VehicleData extends Marshallable[VehicleData] { format: UtilityVehicleData, inventory: Option[InventoryData] ): VehicleData = { - VehicleData(pos, basic, unk3 = false, health, unk4 = false, no_mount_points = false, driveState, unk5 = false, unk6 = false, cloak = cloak, Some(format), inventory)( + VehicleData(pos, basic, boostMaxHealth = false, health, unk4 = false, no_mount_points = false, driveState, unk5 = false, unk6 = false, cloak = cloak, Some(format), inventory)( VehicleFormat.Utility ) } @@ -150,7 +150,7 @@ object VehicleData extends Marshallable[VehicleData] { format: VariantVehicleData, inventory: Option[InventoryData] ): VehicleData = { - VehicleData(pos, basic, unk3 = false, health, unk4 = false, no_mount_points = false, driveState, unk5 = false, unk6 = false, cloak = cloak, Some(format), inventory)( + VehicleData(pos, basic, boostMaxHealth = false, health, unk4 = false, no_mount_points = false, driveState, unk5 = false, unk6 = false, cloak = cloak, Some(format), inventory)( VehicleFormat.Variant ) } diff --git a/src/main/scala/net/psforever/services/CavernRotationService.scala b/src/main/scala/net/psforever/services/CavernRotationService.scala index 9fc208b21..824a817b1 100644 --- a/src/main/scala/net/psforever/services/CavernRotationService.scala +++ b/src/main/scala/net/psforever/services/CavernRotationService.scala @@ -555,15 +555,37 @@ class CavernRotationService( def sendCavernRotationUpdates(sendToSession: ActorRef): Unit = { val curr = System.currentTimeMillis() val (lockedZones, unlockedZones) = managedZones.partition(_.locked) - //borrow GalaxyService response structure, but send to the specific endpoint - lockedZones.foreach { monitor => + //borrow GalaxyService response structure, but send to the specific endpoint math.max(0, monitor.start + monitor.duration - curr) + unlockedZones.foreach { monitor => + sendToSession ! GalaxyServiceResponse("", GalaxyResponse.UnlockedZoneUpdate(monitor.zone)) + } + val sortedLocked = lockedZones.sortBy(z => z.start) + sortedLocked.take(2).foreach { monitor => sendToSession ! GalaxyServiceResponse( "", GalaxyResponse.LockedZoneUpdate(monitor.zone, math.max(0, monitor.start + monitor.duration - curr)) ) } - unlockedZones.foreach { monitor => - sendToSession ! GalaxyServiceResponse("", GalaxyResponse.UnlockedZoneUpdate(monitor.zone)) + sortedLocked.takeRight(2).foreach { monitor => + sendToSession ! GalaxyServiceResponse( + "", + GalaxyResponse.LockedZoneUpdate(monitor.zone, 0L) + ) + } + } + + def sendCavernRotationUpdatesToAll(galaxyService: ActorRef): Unit = { + val curr = System.currentTimeMillis() + val (lockedZones, unlockedZones) = managedZones.partition(_.locked) + unlockedZones.foreach { z => + galaxyService ! GalaxyServiceMessage(GalaxyAction.UnlockedZoneUpdate(z.zone)) + } + val sortedLocked = lockedZones.sortBy(z => z.start) + sortedLocked.take(2).foreach { z => + galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(z.zone, z.start + z.duration - curr)) + } + sortedLocked.takeRight(2).foreach { z => + galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(z.zone, 0L)) } } @@ -595,9 +617,9 @@ class CavernRotationService( //zone transition immediately lockTimer.cancel() unlockTimer.cancel() + retimeZonesUponForcedRotation(galaxyService) zoneRotationFunc(galaxyService) lockTimerToDisplayWarning(timeBetweenRotationsHours.hours - firstClosingWarningAtMinutes.minutes) - retimeZonesUponForcedRotation(galaxyService) } else { //instead of transitioning immediately, jump to the 5 minute rotation warning for the benefit of players lockTimer.cancel() //won't need to retime until zone change @@ -651,7 +673,6 @@ class CavernRotationService( galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse( ChatMsg(ChatMessageType.UNK_229, s"@cavern_switched^@${lockingZone.id}~^@${unlockingZone.id}") )) - galaxyService ! GalaxyServiceMessage(GalaxyAction.UnlockedZoneUpdate(unlockingZone)) //change warp gate statuses to reflect zone lock state CavernRotationService.disableLatticeLinksAndWarpGateAccessibility( ((prevToLock until managedZones.size) ++ (0 until prevToLock)) @@ -664,7 +685,7 @@ class CavernRotationService( .map(managedZones(_).zone) ) } - galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(locking.zone, fullHoursBetweenRotationsAsMillis)) + sendCavernRotationUpdatesToAll(galaxyService) } /** @@ -689,9 +710,6 @@ class CavernRotationService( val zone = managedZones(monitorIndex) val newStart = startingInThePast + (index * timeBetweenRotationsHours).hours.toMillis zone.start = newStart - if (zone.locked) { - galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(zone.zone, newStart + fullDurationAsMillis - curr)) - } } //println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString("")) } @@ -715,10 +733,8 @@ class CavernRotationService( val advanceByTimeAsMillis = advanceTimeBy.toMillis managedZones.foreach { zone => zone.start = zone.start - advanceByTimeAsMillis - if (zone.locked) { - galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(zone.zone, zone.start + zone.duration - curr)) - } } + sendCavernRotationUpdatesToAll(galaxyService) //println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString("")) } diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index 4c6203ea2..ffe816566 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -259,14 +259,18 @@ class HackCaptureActor extends Actor { ) } // Push map update to clients - owner.Zone.actor ! ZoneActor.ZoneMapUpdate() + if (owner.BuildingType == StructureType.Tower) + owner.Actor ! BuildingActor.MapUpdate() + else + owner.Zone.actor ! ZoneActor.ZoneMapUpdate() } private def HackCompleted(terminal: CaptureTerminal with Hackable, hackedByFaction: PlanetSideEmpire.Value): Unit = { val building = terminal.Owner.asInstanceOf[Building] if (building.NtuLevel > 0) { + building.virusId = 8 + building.virusInstalledBy = None log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction") - building.Actor! BuildingActor.SetFaction(hackedByFaction) //dispatch to players aligned with the capturing faction within the SOI val events = building.Zone.LocalEvents val msg = LocalAction.SendGenericActionMessage(Service.defaultPlayerGUID, GenericAction.FacilityCaptureFanfare) @@ -275,19 +279,34 @@ class HackCaptureActor extends Actor { .collect { case p if p.Faction == hackedByFaction => events ! LocalServiceMessage(p.Name, msg) } - val zoneBases = building.Zone.Buildings.filter(base => - base._2.BuildingType == StructureType.Facility) - val ownedBases = building.Zone.Buildings.filter(base => - base._2.BuildingType == StructureType.Facility && base._2.Faction == hackedByFaction - && base._2.GUID != building.GUID) - val zoneTowers = building.Zone.Buildings.filter(tower => - tower._2.BuildingType == StructureType.Tower && tower._2.Faction != hackedByFaction) - // All major facilities in zone are now owned by the hacking faction. Capture all towers in the zone - // Base that was just hacked is not counted (hence the size - 1) because it wasn't always in ownedBases (async?) - if (zoneBases.size - 1 == ownedBases.size && zoneTowers.nonEmpty) - { - processBuildingsWithDelay(zoneTowers.values.toSeq, hackedByFaction, 1000) + val buildings = building.Zone.Buildings.values + val hackedBaseId = building.GUID + val facilities = if (building.Zone.id.startsWith("c")) { + buildings.filter(b => + b.Name.startsWith("N") || b.Name.startsWith("S")).toSeq } + else { + buildings.filter(_.BuildingType == StructureType.Facility).toSeq + } + val ownedFacilities = facilities.filter(b => + b.Faction == hackedByFaction && b.GUID != hackedBaseId + ) + val towersToCapture = buildings.filter(b => + b.BuildingType == StructureType.Tower && b.Faction != hackedByFaction + ).toSeq + if (ownedFacilities.size == facilities.size - 1) { + building.Zone.lockedBy = hackedByFaction + building.Zone.benefitRecipient = hackedByFaction + building.Zone.NotifyContinentalLockBenefits(building.Zone, building) + if (towersToCapture.nonEmpty) { + processBuildingsWithDelay(towersToCapture, hackedByFaction, 100) + } + } + else if (building.Zone.lockedBy != PlanetSideEmpire.NEUTRAL) { + building.Zone.lockedBy = PlanetSideEmpire.NEUTRAL + building.Zone.NotifyContinentalLockBenefits(building.Zone, building) + } + building.Actor ! BuildingActor.SetFaction(hackedByFaction) } else { log.info("Base hack completed, but base was out of NTU.") } @@ -317,23 +336,10 @@ class HackCaptureActor extends Actor { if (buildingIterator.hasNext) { val building = buildingIterator.next() val terminal = building.CaptureTerminal.get - val zone = building.Zone - val zoneActor = zone.actor val buildingActor = building.Actor - //clear any previous hack - if (building.CaptureTerminalIsHacked) { - zone.LocalEvents ! LocalServiceMessage( - zone.id, - LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody) - ) - } - //push any updates this might cause - zoneActor ! ZoneActor.ZoneMapUpdate() - //convert faction affiliation buildingActor ! BuildingActor.SetFaction(faction) buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) - //push for map updates again - zoneActor ! ZoneActor.ZoneMapUpdate() + buildingActor ! BuildingActor.MapUpdate() } }, 0, diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala index 6cb3e5bc6..c93ae2696 100644 --- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala @@ -3,7 +3,7 @@ package net.psforever.services.local.support import java.util.concurrent.TimeUnit import akka.actor.{Actor, Cancellable} -import net.psforever.objects.Default +import net.psforever.objects.{Default, GlobalDefinitions} import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.zones.Zone @@ -30,8 +30,12 @@ class HackClearActor() extends Actor { def receive: Receive = { case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) => val durationMillis = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS) - hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationMillis) - + val newEntry = HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationMillis) + // Remove any existing entry for this GUID + zone in case of virus adding an entry for same target + hackedObjects = hackedObjects.filterNot(e => e.target.GUID == target.GUID && e.zone.id == zone.id) + hackedObjects = newEntry :: hackedObjects + // Sort so they are removed in the correct order + hackedObjects = hackedObjects.sortBy(e => e.time + e.duration) // Restart the timer, in case this is the first object in the hacked objects list RestartTimer() @@ -49,6 +53,9 @@ class HackClearActor() extends Actor { entry.unk1, entry.unk2 ) //call up to the main event system + if (entry.target.Definition == GlobalDefinitions.main_terminal) { + ClearVirusFromBuilding(entry.target) + } }) RestartTimer() @@ -93,6 +100,29 @@ class HackClearActor() extends Actor { } } + /** + * When the hack timer expires on a main_terminal, clear the virus from the building and + * inform the players in the area + * @param target main_terminal object + */ + private def ClearVirusFromBuilding(target: PlanetSideServerObject): Unit = { + import net.psforever.objects.serverobject.structures.Building + import net.psforever.objects.serverobject.terminals.Terminal + import net.psforever.actors.zone.BuildingActor + import net.psforever.services.Service + import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} + + val building = target.asInstanceOf[Terminal].Owner.asInstanceOf[Building] + building.virusId = 8 + building.virusInstalledBy = None + val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.GUID, 60) + val events = building.Zone.AvatarEvents + building.PlayersInSOI.foreach { player => + events ! AvatarServiceMessage(player.Name, msg) + } + building.Actor ! BuildingActor.MapUpdate() + } + /** * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered. * Separate the original `List` into two: diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index f1463dfda..4ac2da6a6 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -156,7 +156,7 @@ object Zones { "vanu_vehicle_station" ) private val basicTerminalTypes = - Seq("order_terminal", "spawn_terminal", "cert_terminal", "order_terminal", "vanu_equipment_term") + Seq("order_terminal", "spawn_terminal", "cert_terminal", "order_terminal", "vanu_equipment_term", "main_terminal") private val spawnPadTerminalTypes = Seq( "ground_vehicle_terminal", "air_vehicle_terminal", @@ -345,6 +345,15 @@ object Zones { ), owningBuildingGuid = buildingGuid ) + //health module slowly heals friendly players in the soi + zoneMap.addLocalObject( + buildingGuid + 2, + ProximityTerminal.Constructor( + structure.position, + GlobalDefinitions.medical_terminal_healing_module + ), + owningBuildingGuid = buildingGuid + ) } } val filteredZoneEntities = @@ -557,7 +566,7 @@ object Zones { case "adv_med_terminal" | "repair_silo" | "pad_landing_frame" | "pad_landing_tower_frame" | "medical_terminal" | "crystals_health_a" | "crystals_health_b" | "crystals_repair_a" | "crystals_repair_b" | "crystals_vehicle_a" | - "crystals_vehicle_b" | "crystals_energy_a" | "crystals_energy_b" => + "crystals_vehicle_b" | "crystals_energy_a" | "crystals_energy_b" | "medical_terminal_healing_module" => zoneMap.addLocalObject( obj.guid, ProximityTerminal diff --git a/src/test/scala/game/BuildingInfoUpdateMessageTest.scala b/src/test/scala/game/BuildingInfoUpdateMessageTest.scala index 848139214..7731c947a 100644 --- a/src/test/scala/game/BuildingInfoUpdateMessageTest.scala +++ b/src/test/scala/game/BuildingInfoUpdateMessageTest.scala @@ -30,8 +30,8 @@ class BuildingInfoUpdateMessageTest extends Specification { unk4, unk5, unk6, - unk7, - unk7x, + virus_id, + virus_installed_by, boost_spawn_pain, boost_generator_pain ) => @@ -53,8 +53,8 @@ class BuildingInfoUpdateMessageTest extends Specification { unk4.isEmpty mustEqual true unk5 mustEqual 0 unk6 mustEqual false - unk7 mustEqual 8 - unk7x.isEmpty mustEqual true + virus_id mustEqual 8 + virus_installed_by.isEmpty mustEqual true boost_spawn_pain mustEqual false boost_generator_pain mustEqual false case _ => diff --git a/src/test/scala/game/EmpireBenefitsMessageTest.scala b/src/test/scala/game/EmpireBenefitsMessageTest.scala new file mode 100644 index 000000000..9f86281d6 --- /dev/null +++ b/src/test/scala/game/EmpireBenefitsMessageTest.scala @@ -0,0 +1,182 @@ +// Copyright (c) 2025 PSForever +package game + +import net.psforever.packet._ +import net.psforever.packet.game.EmpireBenefitsMessage +import net.psforever.packet.game.EmpireBenefitsMessage.{ZoneBenefit, ZoneLock, ZoneLockBenefit, ZoneLockZone} +import net.psforever.types.PlanetSideEmpire +import org.specs2.mutable._ +import scodec.bits._ + +class EmpireBenefitsMessageTest extends Specification { + + val sample1: ByteVector = ByteVector.fromValidHex( + "d7" + // header + "04000000" + // count uint32L + "21c06c6f636b2d7a3321c06c6f636b2d7a3464006c6f636b2d69312d69322d69332d6934a1c06c6f636b2d7a39" + + "04000000" + // count uint32L + "004000600410020300" + ) + + val sample2: ByteVector = ByteVector.fromValidHex( + "d7" + + "05000000 23406c6f636b2d76732d686f6d657321c06c6f636b2d7a3321c06c6f636b2d7a3461c06c6f636b2d7a3964006c6f636b2d69312d69322d69332d6934" + + "05000000 004000600024010300410000" + ) + + val sample3: ByteVector = ByteVector.fromValidHex( + "d7" + + "05000000 21c06c6f636b2d7a3321c06c6f636b2d7a3423406c6f636b2d6e632d686f6d657361c06c6f636b2d7a39a4006c6f636b2d69312d69322d69332d6934" + + "05000000 004000600020010300810000" + ) + + val sample4: ByteVector = ByteVector.fromValidHex( + "d7" + + "06000000 a3406c6f636b2d6e632d686f6d6573a3406c6f636b2d74722d686f6d6573a1c06c6f636b2d7a33a1c06c6f636b2d7a34a1c06c6f636b2d7a39a4006c6f636b2d69312d69322d69332d6934" + + "06000000 80402030081002060081c0208000" + ) + + private val sample1_expectedLocks = Vector( + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.z3), + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.z4), + ZoneLock(PlanetSideEmpire.NC, ZoneLockZone.i1_i2_i3_i4), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.z9) + ) + + private val sample1_expectedBenefits = Vector( + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.z4), + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.z3), + ZoneBenefit(PlanetSideEmpire.NC, ZoneLockBenefit.i1_i2_i3_i4), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.z9) + ) + + private val sample2_expectedLocks = Vector( + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.vs_homes), + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.z3), + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.z4), + ZoneLock(PlanetSideEmpire.NC, ZoneLockZone.z9), + ZoneLock(PlanetSideEmpire.NC, ZoneLockZone.i1_i2_i3_i4) + ) + + private val sample2_expectedBenefits = Vector( + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.z4), + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.z3), + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.vs_homes), + ZoneBenefit(PlanetSideEmpire.NC, ZoneLockBenefit.z9), + ZoneBenefit(PlanetSideEmpire.NC, ZoneLockBenefit.i1_i2_i3_i4) + ) + + private val sample3_expectedLocks = Vector( + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.z3), + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.z4), + ZoneLock(PlanetSideEmpire.TR, ZoneLockZone.nc_homes), + ZoneLock(PlanetSideEmpire.NC, ZoneLockZone.z9), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.i1_i2_i3_i4) + ) + + private val sample3_expectedBenefits = Vector( + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.z4), + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.z3), + ZoneBenefit(PlanetSideEmpire.TR, ZoneLockBenefit.nc_homes), + ZoneBenefit(PlanetSideEmpire.NC, ZoneLockBenefit.z9), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.i1_i2_i3_i4) + ) + + private val sample4_expectedLocks = Vector( + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.nc_homes), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.tr_homes), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.z3), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.z4), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.z9), + ZoneLock(PlanetSideEmpire.VS, ZoneLockZone.i1_i2_i3_i4) + ) + + private val sample4_expectedBenefits = Vector( + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.z4), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.z9), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.i1_i2_i3_i4), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.z3), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.tr_homes), + ZoneBenefit(PlanetSideEmpire.VS, ZoneLockBenefit.nc_homes) + ) + + "decode sample1" in { + PacketCoding.decodePacket(sample1).require match { + case EmpireBenefitsMessage(a, b) => + a mustEqual sample1_expectedLocks + b mustEqual sample1_expectedBenefits + case _ => + ko + } + } + + "encode sample1" in { + val msg = EmpireBenefitsMessage( + zoneLocks = sample1_expectedLocks, + zoneBenefits = sample1_expectedBenefits + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual sample1 + } + + "decode sample2" in { + PacketCoding.decodePacket(sample2).require match { + case EmpireBenefitsMessage(a, b) => + a mustEqual sample2_expectedLocks + b mustEqual sample2_expectedBenefits + case _ => + ko + } + } + + "encode sample2" in { + val msg = EmpireBenefitsMessage( + zoneLocks = sample2_expectedLocks, + zoneBenefits = sample2_expectedBenefits + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual sample2 + } + + "decode sample3" in { + PacketCoding.decodePacket(sample3).require match { + case EmpireBenefitsMessage(a, b) => + a mustEqual sample3_expectedLocks + b mustEqual sample3_expectedBenefits + case _ => + ko + } + } + + "encode sample3" in { + val msg = EmpireBenefitsMessage( + zoneLocks = sample3_expectedLocks, + zoneBenefits = sample3_expectedBenefits + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual sample3 + } + + "decode sample4" in { + PacketCoding.decodePacket(sample4).require match { + case EmpireBenefitsMessage(a, b) => + a mustEqual sample4_expectedLocks + b mustEqual sample4_expectedBenefits + case _ => + ko + } + } + + "encode sample4" in { + val msg = EmpireBenefitsMessage( + zoneLocks = sample4_expectedLocks, + zoneBenefits = sample4_expectedBenefits + ) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual sample4 + } +} diff --git a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index f384ea651..08df24820 100644 --- a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -39,7 +39,7 @@ class MountedVehiclesTest extends Specification { vdata.no_mount_points mustEqual false vdata.driveState mustEqual DriveState.Mobile vdata.cloak mustEqual false - vdata.unk3 mustEqual false + vdata.boostMaxHealth mustEqual false vdata.unk4 mustEqual false vdata.unk5 mustEqual false vdata.unk6 mustEqual false diff --git a/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala index 57dedbdb0..d1bdaa164 100644 --- a/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala +++ b/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala @@ -36,7 +36,7 @@ class UtilityVehiclesTest extends Specification { ant.driveState mustEqual DriveState.Mobile ant.health mustEqual 255 ant.cloak mustEqual false - ant.unk3 mustEqual false + ant.boostMaxHealth mustEqual false ant.unk4 mustEqual false ant.unk5 mustEqual false ant.unk6 mustEqual false @@ -67,7 +67,7 @@ class UtilityVehiclesTest extends Specification { ams.vehicle_format_data mustEqual Some(UtilityVehicleData(60)) ams.health mustEqual 236 ams.cloak mustEqual true - ams.unk3 mustEqual false + ams.boostMaxHealth mustEqual false ams.unk4 mustEqual false ams.unk5 mustEqual false ams.unk6 mustEqual true @@ -120,7 +120,7 @@ class UtilityVehiclesTest extends Specification { case _ => ko } - ams.unk3 mustEqual false + ams.boostMaxHealth mustEqual false ams.health mustEqual 255 ams.unk4 mustEqual false ams.no_mount_points mustEqual false @@ -320,7 +320,7 @@ class UtilityVehiclesTest extends Specification { Some(Vector3(27.3375f, -0.78749996f, 0.1125f)) ), CommonFieldData(PlanetSideEmpire.TR, false, false, false, None, false, Some(false), None, PlanetSideGUID(3087)), - unk3 = false, + boostMaxHealth = false, health = 255, unk4 = false, no_mount_points = false,