Merge branch 'master' into rad-poisoning

This commit is contained in:
Fate-JH 2026-01-05 23:07:05 -05:00 committed by GitHub
commit 0ae5a46934
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1362 additions and 258 deletions

View file

@ -60,10 +60,6 @@ add_property maelstrom equiptime 1000
add_property maelstrom holstertime 1000 add_property maelstrom holstertime 1000
add_property magcutter equiptime 250 add_property magcutter equiptime 250
add_property magcutter holstertime 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 equiptime 750
add_property mini_chaingun holstertime 750 add_property mini_chaingun holstertime 750
add_property nano_dispenser equiptime 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 super_staminakit requirement_award0 false
add_property suppressor equiptime 600 add_property suppressor equiptime 600
add_property suppressor holstertime 600 add_property suppressor holstertime 600
add_property trek allowed false add_property trek allowed true
add_property trek equiptime 500 add_property trek equiptime 500
add_property trek holstertime 500 add_property trek holstertime 500
add_property vulture requirement_award0 false add_property vulture requirement_award0 false

View file

@ -21625,7 +21625,7 @@
"Owner": 1303, "Owner": 1303,
"AbsX": 3577.178, "AbsX": 3577.178,
"AbsY": 2712.24927, "AbsY": 2712.24927,
"AbsZ": 32.8808441, "AbsZ": 34.289,
"Yaw": 318.0, "Yaw": 318.0,
"GUID": 1843, "GUID": 1843,
"MapID": null, "MapID": null,
@ -21664,7 +21664,7 @@
"Owner": 1303, "Owner": 1303,
"AbsX": 3588.1582, "AbsX": 3588.1582,
"AbsY": 2700.0542, "AbsY": 2700.0542,
"AbsZ": 32.8808441, "AbsZ": 34.289,
"Yaw": 318.0, "Yaw": 318.0,
"GUID": 1846, "GUID": 1846,
"MapID": null, "MapID": null,

View file

@ -8289,7 +8289,7 @@
"AbsY": 1070.60535, "AbsY": 1070.60535,
"AbsZ": 173.239014, "AbsZ": 173.239014,
"Yaw": 338.0, "Yaw": 338.0,
"GUID": 648, "GUID": 649,
"MapID": null, "MapID": null,
"IsChildObject": true "IsChildObject": true
}, },
@ -8302,7 +8302,7 @@
"AbsY": 1132.36707, "AbsY": 1132.36707,
"AbsZ": 138.999, "AbsZ": 138.999,
"Yaw": 124.0, "Yaw": 124.0,
"GUID": 649, "GUID": 648,
"MapID": null, "MapID": null,
"IsChildObject": true "IsChildObject": true
}, },

View file

@ -150,6 +150,11 @@ object MiddlewareActor {
packet.isInstanceOf[ChatMsg] 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. * A function for blanking tasks related to inbound packet resolution.
* Do nothing. * Do nothing.
@ -259,7 +264,8 @@ class MiddlewareActor(
MiddlewareActor.keepAliveMessageGuard, MiddlewareActor.keepAliveMessageGuard,
MiddlewareActor.characterInfoMessageGuard, MiddlewareActor.characterInfoMessageGuard,
MiddlewareActor.squadDetailDefinitionMessageGuard, MiddlewareActor.squadDetailDefinitionMessageGuard,
MiddlewareActor.chatMsgGuard MiddlewareActor.chatMsgGuard,
MiddlewareActor.propertyOverrideMessageGuard
) )
private val smpHistoryLength: Int = 100 private val smpHistoryLength: Int = 100

View file

@ -2218,12 +2218,12 @@ class AvatarActor(
.Holsters() .Holsters()
.foreach(holster => .foreach(holster =>
holster.Equipment match { 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) //todo fix so player may hold medapp when loading the zone (client crash)
val item = SimpleItem(GlobalDefinitions.flail_targeting_laser) val item = SimpleItem(GlobalDefinitions.flail_targeting_laser)
holster.Equipment = None holster.Equipment = None
holster.Equipment = item holster.Equipment = item
item.GUID = PlanetSideGUID(gen.getAndIncrement) item.GUID = PlanetSideGUID(gen.getAndIncrement)*/
case Some(tool: Tool) => case Some(tool: Tool) =>
tool.AmmoSlots.foreach(slot => { tool.AmmoSlots.foreach(slot => {
slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement) slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement)

View file

@ -45,6 +45,13 @@ class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: A
case GalaxyResponse.MapUpdate(msg) => case GalaxyResponse.MapUpdate(msg) =>
sendResponse(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) => case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
val faction = player.Faction val faction = player.Faction

View file

@ -2,7 +2,8 @@
package net.psforever.actors.session.normal package net.psforever.actors.session.normal
import akka.actor.ActorContext 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.ce.Deployable
import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.vehicles.MountableWeapons 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) sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid)
case LocalResponse.SendResponse(msg) => 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) => case LocalResponse.SetEmpire(objectGuid, empire) =>
sendResponse(SetEmpireMessage(objectGuid, empire)) sendResponse(SetEmpireMessage(objectGuid, empire))

View file

@ -12,7 +12,7 @@ import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.LivePlayerList import net.psforever.objects.LivePlayerList
import net.psforever.objects.sourcing.PlayerSource 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.packet.game.SetChatFilterMessage
import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} 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 net.psforever.types.ChatMessageType.CMT_QUIT
import org.log4s.Logger import org.log4s.Logger
import java.util.concurrent.{Executors, TimeUnit} import java.util.concurrent.{Executors, ScheduledFuture, TimeUnit}
import scala.annotation.unused import scala.annotation.unused
import scala.collection.{Seq, mutable} import scala.collection.{Seq, mutable}
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@ -368,7 +368,10 @@ class ChatOperations(
case (Some(buildings), Some(faction), Some(_)) => case (Some(buildings), Some(faction), Some(_)) =>
//TODO implement timer //TODO implement timer
//schedule processing of buildings with a delay //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 true
case _ => case _ =>
false false
@ -379,30 +382,36 @@ class ChatOperations(
buildings: Seq[Building], buildings: Seq[Building],
faction: PlanetSideEmpire.Value, faction: PlanetSideEmpire.Value,
delayMillis: Long delayMillis: Long
): Unit = { )(onComplete: Zone => Unit): Unit = {
val buildingIterator = buildings.iterator import net.psforever.objects.serverobject.structures.StructureType
scheduler.scheduleAtFixedRate( 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) { if (iterator.hasNext) {
val building = buildingIterator.next() val building = iterator.next()
val terminal = building.CaptureTerminal.get val terminal = building.CaptureTerminal.get
val zone = building.Zone if (building.BuildingType == StructureType.Tower) {
val zoneActor = zone.actor building.Actor ! BuildingActor.SetFaction(faction)
val buildingActor = building.Actor building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false))
//clear any previous hack building.Actor ! BuildingActor.MapUpdate()
if (building.CaptureTerminalIsHacked) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
)
} }
//push any updates this might cause else {
zoneActor ! ZoneActor.ZoneMapUpdate() if (building.CaptureTerminalIsHacked) {
//convert faction affiliation zone.LocalEvents ! LocalServiceMessage(
buildingActor ! BuildingActor.SetFaction(faction) zone.id,
buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
//push for map updates again )
zoneActor ! ZoneActor.ZoneMapUpdate() }
building.Actor ! BuildingActor.SetFaction(faction)
building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false))
}
}
else {
handle.cancel(false)
onComplete(zone)
} }
}, },
0, 0,

View file

@ -1258,7 +1258,12 @@ class GeneralOperations(
def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match { equipment match {
case Some(item) => case Some(item) =>
sendUseGeneralEntityMessage(terminal, item) if (terminal.Definition == GlobalDefinitions.main_terminal) {
sendUseMainTerminalMessage(terminal, item, msg.unk2)
}
else {
sendUseGeneralEntityMessage(terminal, item)
}
case None case None
if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction ||
terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL =>
@ -1484,6 +1489,14 @@ class GeneralOperations(
obj.Actor ! CommonMessages.Use(player, Some(equipment)) 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 = { def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
equipment match { equipment match {

View file

@ -7,7 +7,8 @@ import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
import net.psforever.objects.{PlanetSideGameObject, Tool, Vehicle} import net.psforever.objects.{PlanetSideGameObject, Tool, Vehicle}
import net.psforever.objects.vehicles.{CargoBehavior, MountableWeapons} import net.psforever.objects.vehicles.{CargoBehavior, MountableWeapons}
import net.psforever.objects.vital.InGameHistory 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.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{BailType, PlanetSideGUID, Vector3} 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 * @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 = { def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
DismountAction(tplayer, obj, seatNum)
tplayer.WhichSide = OutsideOf 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 //until vehicles maintain synchronized momentum without a driver
obj match { obj match {
case v: Vehicle case v: Vehicle

View file

@ -54,7 +54,7 @@ import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning} import net.psforever.objects.zones.{Zone, ZoneHotSpotProjector, Zoning}
import net.psforever.objects._ 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.{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.{BeginZoningMessage, DroppodLaunchRequestMessage, ReleaseAvatarRequestMessage, SpawnRequestMessage, WarpgateRequest}
import net.psforever.packet.game.DeathStatistic import net.psforever.packet.game.DeathStatistic
@ -193,6 +193,10 @@ object SpawnOperations {
def sendEventMessage(msg: ChatMsg)(sessionLogic: SessionData): Unit = { def sendEventMessage(msg: ChatMsg)(sessionLogic: SessionData): Unit = {
sessionLogic.sendResponse(msg) sessionLogic.sendResponse(msg)
} }
def delaySendGenericObjectActionMessage(msg: PlanetSideGamePacket)(sessionLogic: SessionData): Unit = {
sessionLogic.sendResponse(msg)
}
} }
class ZoningOperations( class ZoningOperations(
@ -338,6 +342,13 @@ class ZoningOperations(
sendResponse(PlanetsideAttributeMessage(targetPlayer.GUID, 19, 1)) 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 //load corpses in zone
continent.Corpses.foreach { continent.Corpses.foreach {
spawn.DepictPlayerAsCorpse spawn.DepictPlayerAsCorpse
@ -559,27 +570,19 @@ class ZoningOperations(
val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR) val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC) val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS) val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
zone.Buildings.foreach({ case (_, building) => initBuilding(continentNumber, building.MapId, building) }) zone.Buildings.foreach({ case (_, building) => initBuilding(continentNumber, building.MapId, building) })
sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) 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 || continentNumber == 12 || continentNumber == 13)
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
sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NEUTRAL)) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NEUTRAL))
else
sendResponse(ContinentalLockUpdateMessage(continentNumber, zone.lockedBy))
//CaptureFlagUpdateMessage() //CaptureFlagUpdateMessage()
//VanuModuleUpdateMessage() //VanuModuleUpdateMessage()
//ModuleLimitsMessage() //ModuleLimitsMessage()
val isCavern = continent.map.cavern val isCavern = zone.map.cavern
sendResponse(ZoneInfoMessage(continentNumber, empire_status=true, if (isCavern) { if (!isCavern) {
Int.MaxValue.toLong sendResponse(ZoneInfoMessage(continentNumber, empire_status = true, 0L))
} else { }
0L
}))
sendResponse(ZoneLockInfoMessage(continentNumber, lock_status=false, unk=true)) sendResponse(ZoneLockInfoMessage(continentNumber, lock_status=false, unk=true))
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0)) sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
sendResponse( sendResponse(
@ -701,6 +704,7 @@ class ZoningOperations(
log.warn( 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" 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 val previousZoningType = ztype
CancelZoningProcess() CancelZoningProcess()
@ -1117,6 +1121,27 @@ class ZoningOperations(
PlanetsideAttributeEnum.ControlConsoleHackUpdate, PlanetsideAttributeEnum.ControlConsoleHackUpdate,
HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false) 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 _ => case _ =>
sessionLogic.general.hackObject(amenity.GUID, unk1 = 1114636288L, HackState7.Unk8) //generic hackable object 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" 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). */ /** Before changing zones, perform the following task (which can be a nesting of subtasks). */
@ -1967,6 +1993,7 @@ class ZoningOperations(
deadState = DeadState.RespawnTime deadState = DeadState.RespawnTime
val tplayer = new Player(avatar) val tplayer = new Player(avatar)
session = session.copy(player = tplayer) session = session.copy(player = tplayer)
tplayer.protectedWhileZoning = true
//actual zone is undefined; going to our sanctuary //actual zone is undefined; going to our sanctuary
RandomSanctuarySpawnPosition(tplayer) RandomSanctuarySpawnPosition(tplayer)
DefinitionUtil.applyDefaultLoadout(tplayer) DefinitionUtil.applyDefaultLoadout(tplayer)
@ -1979,6 +2006,7 @@ class ZoningOperations(
deadState = DeadState.RespawnTime deadState = DeadState.RespawnTime
session = session.copy(player = new Player(avatar)) session = session.copy(player = new Player(avatar))
player.Zone = inZone player.Zone = inZone
player.protectedWhileZoning = true
optionalSavedData match { optionalSavedData match {
case Some(results) => case Some(results) =>
val health = results.health val health = results.health
@ -2092,9 +2120,10 @@ class ZoningOperations(
log.info(s"RestoreInfo: player $name is alive") log.info(s"RestoreInfo: player $name is alive")
deadState = DeadState.Alive deadState = DeadState.Alive
session = session.copy(player = p, avatar = a) session = session.copy(player = p, avatar = a)
p.protectedWhileZoning = true
sessionLogic.persist() sessionLogic.persist()
setupAvatarFunc = AvatarRejoin setupAvatarFunc = AvatarRejoin
dropMedicalApplicators(p) //dropMedicalApplicators(p)
avatarActor ! AvatarActor.ReplaceAvatar(a) avatarActor ! AvatarActor.ReplaceAvatar(a)
avatarLoginResponse(a) avatarLoginResponse(a)
@ -2104,7 +2133,7 @@ class ZoningOperations(
deadState = DeadState.Dead deadState = DeadState.Dead
session = session.copy(player = p, avatar = a) session = session.copy(player = p, avatar = a)
sessionLogic.persist() sessionLogic.persist()
dropMedicalApplicators(p) //dropMedicalApplicators(p)
HandleReleaseAvatar(p, inZone) HandleReleaseAvatar(p, inZone)
avatarActor ! AvatarActor.ReplaceAvatar(a) avatarActor ! AvatarActor.ReplaceAvatar(a)
avatarLoginResponse(a) avatarLoginResponse(a)
@ -2548,9 +2577,6 @@ class ZoningOperations(
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
} }
} }
if (player.outfit_id == 0) {
SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic)
}
/*make weather happen /*make weather happen
sendResponse(WeatherMessage(List(),List( sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217),
@ -2644,6 +2670,7 @@ class ZoningOperations(
def AvatarRejoin(): Unit = { def AvatarRejoin(): Unit = {
sessionLogic.vehicles.GetKnownVehicleAndSeat() match { sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
case (Some(vehicle: Vehicle), Some(seat: Int)) => case (Some(vehicle: Vehicle), Some(seat: Int)) =>
vehicle.protectedWhileZoning = true
//vehicle and driver/passenger //vehicle and driver/passenger
val vguid = vehicle.GUID val vguid = vehicle.GUID
sendResponse(OCM.apply(vehicle)) sendResponse(OCM.apply(vehicle))
@ -2674,7 +2701,6 @@ class ZoningOperations(
log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data")
} }
setupAvatarFunc = AvatarCreate setupAvatarFunc = AvatarCreate
SessionOutfitHandlers.HandleLoginOutfitCheck(player, sessionLogic)
/*make weather happen /*make weather happen
sendResponse(WeatherMessage(List(),List( sendResponse(WeatherMessage(List(),List(
StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), 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.9f, 0.9f, 0.0f), 243, 215),
StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215), StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215),
StormInfo(Vector3(0.95f, 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 //begin looking for conditions to set the avatar
context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200))
} }
@ -2922,13 +2950,40 @@ class ZoningOperations(
0 seconds 0 seconds
} else { } else {
//for other zones ... //for other zones ...
//biolabs have/grant benefits val spawnTimeBenefit: Float = toSpawnPoint.Owner match {
val cryoBenefit: Float = toSpawnPoint.Owner match { case b: Building => FasterRespawnBenefits(b)
case b: Building if b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) => 0.5f case _ => 1f
case _ => 1f
} }
//TODO cumulative death penalty //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 buildingType == StructureType.Bunker
} }
.foreach { case (_, building) => .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() statisticsPacketFunc()
if (tplayer.ExoSuit == ExoSuitType.MAX) { 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) doorsThatShouldBeOpenInRange(pos, range = 100f)
setAvatar = true setAvatar = true
player.allowInteraction = 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 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)) sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
DrawCurrentAmsSpawnPoint() 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 //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)))) sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard))))
statisticsPacketFunc = respawnAvatarStatisticsFields 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 //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)))) sendResponse(AvatarStatisticsMessage(DeathStatistic(ScoreCard.deathCount(avatar.scorecard))))
player.protectedWhileZoning = false
} }
/** /**

View file

@ -8,6 +8,8 @@ import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.building._ import net.psforever.actors.zone.building._
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.ContinentalLockUpdateMessage
import net.psforever.persistence import net.psforever.persistence
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -76,6 +78,9 @@ object BuildingActor {
final case class DensityLevelUpdate(building: Building) extends Command 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. * Set a facility affiliated to one faction to be affiliated to a different faction.
* @param details building and event system references * @param details building and event system references
@ -162,7 +167,6 @@ object BuildingActor {
val building = details.building val building = details.building
val zone = building.Zone val zone = building.Zone
building.Faction = faction building.Faction = faction
zone.actor ! ZoneActor.ZoneMapUpdate() // Update entire lattice to show lattice benefits
zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction)) zone.LocalEvents ! LocalServiceMessage(zone.id, LocalAction.SetEmpire(building.GUID, faction))
} }
} }
@ -252,6 +256,14 @@ class BuildingActor(
case DensityLevelUpdate(building) => case DensityLevelUpdate(building) =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building))) details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building)))
Behaviors.same 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
} }
} }
} }

View file

@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.structures.{StructureType, WarpGate}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup} import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle} 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 akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.avatar.scoring.Kill 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.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory} import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator} import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
import net.psforever.packet.game.{BuildingInfoUpdateMessage, PlanetsideAttributeMessage}
import net.psforever.util.Database._ import net.psforever.util.Database._
import net.psforever.persistence import net.psforever.persistence
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.collection.mutable import scala.collection.mutable
import scala.util.{Failure, Success} 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 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 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( class ZoneActor(
@ -115,6 +121,7 @@ class ZoneActor(
// TODO this happens during testing, need a way to not always persist during tests // 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) case Failure(e) => log.error(e.getMessage)
} }
@ -183,14 +190,75 @@ class ZoneActor(
case ZoneMapUpdate() => case ZoneMapUpdate() =>
zone.Buildings zone.Buildings
.filter(building => .filter(building =>
building._2.BuildingType == StructureType.Facility || building._2.BuildingType == StructureType.Tower) building._2.BuildingType == StructureType.Facility)
.values .values
.foreach(_.Actor ! BuildingActor.MapUpdate()) .foreach(_.Actor ! BuildingActor.MapUpdate())
Behaviors.same Behaviors.same
case AssignLockedBy(zone, notifyPlayers) =>
AssignLockedBy(zone, notifyPlayers)
Behaviors.same
case BuildingInfoState(msg) =>
UpdateBuildingState(msg)
Behaviors.same
} }
.receiveSignal { .receiveSignal {
case (_, PostStop) => case (_, PostStop) =>
Behaviors.same 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 _ =>
}
}
}
} }

View file

@ -4,8 +4,8 @@ package net.psforever.actors.zone.building
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails} import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor}
import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
@ -84,7 +84,16 @@ case object CavernFacilityLogic
): Behavior[Command] = { ): Behavior[Command] = {
BuildingActor.setFactionTo(details, faction, log) BuildingActor.setFactionTo(details, faction, log)
val building = details.building 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 Behaviors.same
} }

View file

@ -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.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.sourcing.PlayerSource
import net.psforever.packet.game.PlanetsideAttributeMessage
import net.psforever.services.{InterstellarClusterService, Service} import net.psforever.services.{InterstellarClusterService, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
@ -158,6 +159,7 @@ case object MajorFacilityLogic
* @return the next behavior for this control agency messaging system * @return the next behavior for this control agency messaging system
*/ */
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = { def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
import net.psforever.objects.GlobalDefinitions
entity match { entity match {
case gen: Generator => case gen: Generator =>
if (generatorStateChange(details, gen, data)) { if (generatorStateChange(details, gen, data)) {
@ -176,12 +178,24 @@ case object MajorFacilityLogic
case _ => case _ =>
log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.") 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 // When a CC is hacked (or resecured) clear hacks on amenities based on currently installed virus
building.HackableAmenities.foreach(amenity => { val hackedAmenities = building.HackableAmenities.filter(_.HackedBy.isDefined)
if (amenity.HackedBy.isDefined) { val amenitiesToClear = building.virusId match {
building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity)) 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 // No map update needed - will be sent by `HackCaptureActor` when required
case _ => case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
@ -231,6 +245,8 @@ case object MajorFacilityLogic
} }
setFactionTo(details, PlanetSideEmpire.NEUTRAL) setFactionTo(details, PlanetSideEmpire.NEUTRAL)
details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply = false details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply = false
details.building.Zone.lockedBy = PlanetSideEmpire.NEUTRAL
details.building.Zone.NotifyContinentalLockBenefits(details.building.Zone, details.building)
Behaviors.same Behaviors.same
} }
@ -287,6 +303,12 @@ case object MajorFacilityLogic
building.PlayersInSOI.foreach { player => building.PlayersInSOI.foreach { player =>
events ! AvatarServiceMessage(player.Name, msg) events ! AvatarServiceMessage(player.Name, msg)
} }
if (building.hasCavernLockBenefit) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.SendResponse(PlanetsideAttributeMessage(building.GUID, 67, 0))
)
}
false false
case Some(GeneratorControl.Event.Destroyed) => case Some(GeneratorControl.Event.Destroyed) =>
true true

View file

@ -122,9 +122,6 @@ case object WarpGateLogic
} }
updateBroadcastCapabilitiesOfWarpGate(details, wg, setBroadcastTo) updateBroadcastCapabilitiesOfWarpGate(details, wg, setBroadcastTo)
updateBroadcastCapabilitiesOfWarpGate(details, otherWg, 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) => case (Some(_), Some(wg : WarpGate), Some(otherWg : WarpGate), None) =>
handleWarpGateDeadendPair(details, otherWg, wg) handleWarpGateDeadendPair(details, otherWg, wg)

View file

@ -1125,6 +1125,8 @@ object GlobalDefinitions {
val medical_terminal = new MedicalTerminalDefinition(529) val medical_terminal = new MedicalTerminalDefinition(529)
val medical_terminal_healing_module = new MedicalTerminalDefinition(530)
val portable_med_terminal = new MedicalTerminalDefinition(689) val portable_med_terminal = new MedicalTerminalDefinition(689)
val pad_landing_frame = new MedicalTerminalDefinition(618) val pad_landing_frame = new MedicalTerminalDefinition(618)
@ -1239,6 +1241,8 @@ object GlobalDefinitions {
val vanu_control_console = new CaptureTerminalDefinition(930) // Cavern CC val vanu_control_console = new CaptureTerminalDefinition(930) // Cavern CC
val main_terminal = new MainTerminalDefinition(473)
val llm_socket = new CaptureFlagSocketDefinition() val llm_socket = new CaptureFlagSocketDefinition()
val capture_flag = new CaptureFlagDefinition() val capture_flag = new CaptureFlagDefinition()

View file

@ -91,6 +91,7 @@ class Player(var avatar: Avatar)
var outfit_window_open: Boolean = false var outfit_window_open: Boolean = false
var outfit_list_open: Boolean = false var outfit_list_open: Boolean = false
var maxAutoRunEnabled: Boolean = false var maxAutoRunEnabled: Boolean = false
var protectedWhileZoning: Boolean = false
/** From PlanetsideAttributeMessage */ /** From PlanetsideAttributeMessage */
var PlanetsideAttribute: Array[Long] = Array.ofDim(120) var PlanetsideAttribute: Array[Long] = Array.ofDim(120)

View file

@ -114,6 +114,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
private var cloaked: Boolean = false private var cloaked: Boolean = false
private var flying: Option[Int] = None private var flying: Option[Int] = None
private var capacitor: Int = 0 private var capacitor: Int = 0
var protectedWhileZoning: Boolean = false
/** /**
* Permissions control who gets to access different parts of the vehicle; * Permissions control who gets to access different parts of the vehicle;

View file

@ -72,9 +72,9 @@ object FirstTimeEvents {
) )
val Other: Set[String] = Set( val Other: Set[String] = Set(
"used_nchev_scattercannon", "used_nc_hev_scattercannon",
"used_nchev_falcon", "used_nc_hev_falcon",
"used_nchev_sparrow", "used_nc_hev_sparrow",
"used_energy_gun_nc", "used_energy_gun_nc",
"visited_portable_manned_turret_nc" "visited_portable_manned_turret_nc"
) )

View file

@ -407,10 +407,28 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
terminalUsedAction terminalUsedAction
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) => 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}") log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
val originalSuit = player.ExoSuit val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player) 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 //determine player's next exo-suit
val (nextSuit, nextSubtype) = { val (nextSuit, nextSubtype) = {
lazy val fallbackSuit = if (Players.CertificationToUseExoSuit(player, originalSuit, originalSubtype)) { lazy val fallbackSuit = if (Players.CertificationToUseExoSuit(player, originalSuit, originalSubtype)) {
@ -790,7 +808,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
target: Target, target: Target,
applyDamageTo: Output applyDamageTo: Output
): Unit = { ): Unit = {
if (player.isAlive && !player.spectator) { if (player.isAlive && !player.spectator && !player.protectedWhileZoning) {
val originalHealth = player.Health val originalHealth = player.Health
val originalArmor = player.Armor val originalArmor = player.Armor
val originalStamina = player.avatar.stamina val originalStamina = player.avatar.stamina

View file

@ -5,6 +5,7 @@ import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.{PlanetSideGameObject, Vehicle} import net.psforever.objects.{PlanetSideGameObject, Vehicle}
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate._
import net.psforever.types.{DriveState, PlanetSideGUID, VehicleFormat} import net.psforever.types.{DriveState, PlanetSideGUID, VehicleFormat}
import net.psforever.zones.Zones
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -14,6 +15,8 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
override def ConstructorData(obj: Vehicle): Try[VehicleData] = { override def ConstructorData(obj: Vehicle): Try[VehicleData] = {
val health = StatConverter.Health(obj.Health, obj.MaxHealth) 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 if (health > 0) { //active
Success( Success(
VehicleData( VehicleData(
@ -32,7 +35,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
case None => PlanetSideGUID(0) case None => PlanetSideGUID(0)
} }
), ),
unk3 = false, boostMaxHealth = boosted,
health, health,
unk4 = false, unk4 = false,
no_mount_points = false, no_mount_points = false,
@ -59,7 +62,7 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() {
v5 = None, v5 = None,
guid = PlanetSideGUID(0) guid = PlanetSideGUID(0)
), ),
unk3 = false, boostMaxHealth = boosted,
health = 0, health = 0,
unk4 = false, unk4 = false,
no_mount_points = true, no_mount_points = true,

View file

@ -29,13 +29,23 @@ object EffectTarget {
//noinspection ScalaUnusedSymbol //noinspection ScalaUnusedSymbol
def Valid(target: PlanetSideGameObject): Boolean = true def Valid(target: PlanetSideGameObject): Boolean = true
def Medical(target: PlanetSideGameObject): Boolean = def Medical(target: PlanetSideGameObject): Boolean = {
target match { target match {
case p: Player => case p: Player =>
p.Health > 0 && (p.Health < p.MaxHealth || p.Armor < p.MaxArmor) p.Health > 0 && (p.Health < p.MaxHealth || p.Armor < p.MaxArmor)
case _ => case _ =>
false false
} }
}
def HealthModule(target: PlanetSideGameObject): Boolean = {
target match {
case p: Player =>
p.Health > 0 && p.Health < 120
case _ =>
false
}
}
def HealthCrystal(target: PlanetSideGameObject): Boolean = def HealthCrystal(target: PlanetSideGameObject): Boolean =
target match { target match {

View file

@ -370,6 +370,15 @@ object GlobalDefinitionsMiscellaneous {
medical_terminal.RepairIfDestroyed = true medical_terminal.RepairIfDestroyed = true
medical_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.711f, height = 1.75f) 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.Name = "adv_med_terminal"
adv_med_terminal.Interval = 500 adv_med_terminal.Interval = 500
adv_med_terminal.HealAmount = 8 adv_med_terminal.HealAmount = 8
@ -720,6 +729,10 @@ object GlobalDefinitionsMiscellaneous {
vanu_control_console.Repairable = false vanu_control_console.Repairable = false
vanu_control_console.FacilityHackTime = 10.minutes 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.Name = "lodestar_repair_terminal"
lodestar_repair_terminal.Interval = 1000 lodestar_repair_terminal.Interval = 1000
lodestar_repair_terminal.HealAmount = 60 lodestar_repair_terminal.HealAmount = 60

View file

@ -1782,6 +1782,8 @@ object GlobalDefinitionsProjectile {
spitfire_aa_ammo_projectile.ProjectileDamageTypeSecondary = DamageType.Splash spitfire_aa_ammo_projectile.ProjectileDamageTypeSecondary = DamageType.Splash
spitfire_aa_ammo_projectile.InitialVelocity = 100 spitfire_aa_ammo_projectile.InitialVelocity = 100
spitfire_aa_ammo_projectile.Lifespan = 5f spitfire_aa_ammo_projectile.Lifespan = 5f
spitfire_aa_ammo_projectile.DamageToArmorFirst = true
spitfire_aa_ammo_projectile.DamageToBattleframeOnly = true
ProjectileDefinition.CalculateDerivedFields(spitfire_aa_ammo_projectile) ProjectileDefinition.CalculateDerivedFields(spitfire_aa_ammo_projectile)
spitfire_aa_ammo_projectile.Modifiers = List( spitfire_aa_ammo_projectile.Modifiers = List(
CerberusTurretWrongTarget, CerberusTurretWrongTarget,
@ -1797,6 +1799,7 @@ object GlobalDefinitionsProjectile {
spitfire_ammo_projectile.DegradeMultiplier = 0.5f spitfire_ammo_projectile.DegradeMultiplier = 0.5f
spitfire_ammo_projectile.InitialVelocity = 100 spitfire_ammo_projectile.InitialVelocity = 100
spitfire_ammo_projectile.Lifespan = .5f spitfire_ammo_projectile.Lifespan = .5f
spitfire_ammo_projectile.DamageToArmorFirst = true
spitfire_ammo_projectile.DamageToBattleframeOnly = true spitfire_ammo_projectile.DamageToBattleframeOnly = true
ProjectileDefinition.CalculateDerivedFields(spitfire_ammo_projectile) ProjectileDefinition.CalculateDerivedFields(spitfire_ammo_projectile)

View file

@ -11,7 +11,9 @@ object CommonMessages {
final case class Hack(player: Player, obj: PlanetSideServerObject with Hackable, data: Option[Any] = None) final case class Hack(player: Player, obj: PlanetSideServerObject with Hackable, data: Option[Any] = None)
final case class ClearHack() final case class ClearHack()
final case class EntityHackState(obj: PlanetSideGameObject with Hackable, hackState: Boolean) 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 * The message that progresses some form of user-driven activity with a certain eventual outcome
* and potential feedback per cycle. * and potential feedback per cycle.

View file

@ -681,6 +681,19 @@ object ContainableBehavior {
entry.obj.isInstanceOf[BoomerTrigger] || entry.obj.isInstanceOf[BoomerTrigger] ||
(faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) (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 { object Containable {

View file

@ -75,7 +75,7 @@ trait DamageableVehicle
val shields = obj.Shields val shields = obj.Shields
val damageToHealth = originalHealth - health val damageToHealth = originalHealth - health
val damageToShields = originalShields - shields val damageToShields = originalShields - shields
if (WillAffectTarget(target, damageToHealth + damageToShields, cause)) { if (WillAffectTarget(target, damageToHealth + damageToShields, cause) && !obj.protectedWhileZoning) {
target.LogActivity(cause) target.LogActivity(cause)
DamageLog( DamageLog(
target, target,

View file

@ -1,6 +1,7 @@
// Copyright (c) 2020-2024 PSForever // Copyright (c) 2020-2024 PSForever
package net.psforever.objects.serverobject.environment package net.psforever.objects.serverobject.environment
import net.psforever.objects.serverobject.interior.Sidedness
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle} import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle}
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.types.Vector3 import net.psforever.types.Vector3
@ -22,8 +23,8 @@ object EnvironmentAttribute {
(obj.Definition.DrownAtMaxDepth || obj.Definition.DisableAtMaxDepth) && (obj.Definition.DrownAtMaxDepth || obj.Definition.DisableAtMaxDepth) &&
canInteractWithPlayersAndVehicles(obj) && canInteractWithPlayersAndVehicles(obj) &&
(obj match { (obj match {
case p: Player => p.VehicleSeated.isEmpty case p: Player => p.VehicleSeated.isEmpty && p.WhichSide == Sidedness.OutsideOf
case v: Vehicle => v.MountedIn.isEmpty case v: Vehicle => v.MountedIn.isEmpty && v.WhichSide == Sidedness.OutsideOf
case _ => false case _ => false
}) })
} }

View file

@ -152,7 +152,7 @@ object Watery {
val oldTimeRemaining: Long = math.max(0, completionTime - System.currentTimeMillis()) val oldTimeRemaining: Long = math.max(0, completionTime - System.currentTimeMillis())
val oldTimeRatio: Float = oldTimeRemaining / oldDuration.toFloat val oldTimeRatio: Float = oldTimeRemaining / oldDuration.toFloat
val percentage: Float = oldTimeRatio * 100 val percentage: Float = oldTimeRatio * 100
val recoveryTime: Long = newDuration * (1f - oldTimeRatio).toLong val recoveryTime: Long = (newDuration * (1f - oldTimeRatio)).toLong
(true, recoveryTime, percentage) (true, recoveryTime, percentage)
case Some(OxygenState.Recovery) => case Some(OxygenState.Recovery) =>
//interrupted while recovering, calculate the progress and keep recovering //interrupted while recovering, calculate the progress and keep recovering

View file

@ -1,11 +1,13 @@
// Copyright (c) 2020 PSForever // Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.hackable 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.structures.{Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal 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.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.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} 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 * 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 * on the other side of a warpgate as well in case there are no friendly facilities in the same zone

View file

@ -10,6 +10,7 @@ import net.psforever.objects.zones.Zone
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.Vector3 import net.psforever.types.Vector3
import net.psforever.zones.Zones
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -44,6 +45,12 @@ class VehicleSpawnControlLoadVehicle(pad: VehicleSpawnPad) extends VehicleSpawnC
) //appear below the trench and doors ) //appear below the trench and doors
vehicle.WhichSide = pad.WhichSide vehicle.WhichSide = pad.WhichSide
vehicle.Cloaked = vehicle.Definition.CanCloak && driver.Cloaked 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) temp = Some(order)
val result = ask(pad.Zone.Transport, Zone.Vehicle.Spawn(vehicle)) val result = ask(pad.Zone.Transport, Zone.Vehicle.Spawn(vehicle))

View file

@ -270,20 +270,19 @@ trait AmenityAutoRepair
autoRepairTimer.cancel() autoRepairTimer.cancel()
autoRepairQueueTask = Some(System.currentTimeMillis() + delay) autoRepairQueueTask = Some(System.currentTimeMillis() + delay)
val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate //doubled intentionally val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate //doubled intentionally
autoRepairTimer = if(AutoRepairObject.Owner == Building.NoBuilding) { AutoRepairObject.Owner match {
//without an owner, auto-repair freely case Building.NoBuilding =>
context.system.scheduler.scheduleOnce( autoRepairTimer = context.system.scheduler.scheduleOnce(
delay milliseconds, delay.milliseconds,
self, self,
NtuCommand.Grant(null, modifiedDrain) NtuCommand.Grant(null, modifiedDrain))
) case b: Building =>
} else { val doubledDrain = if (b.virusId == 2) modifiedDrain * 2 else modifiedDrain
//ask politely autoRepairTimer = context.system.scheduler.scheduleOnce(
context.system.scheduler.scheduleOnce( delay.milliseconds,
delay milliseconds, b.Actor,
AutoRepairObject.Owner.Actor, BuildingActor.Ntu(NtuCommand.Request(doubledDrain, ntuGrantActorRef)))
BuildingActor.Ntu(NtuCommand.Request(modifiedDrain, ntuGrantActorRef)) case _ => ()
) }
}
} }
} }

View file

@ -58,7 +58,7 @@ trait RepairableEntity extends Repairable {
*/ */
protected def CanPerformRepairs(target: Repairable.Target, player: Player, item: Tool): Boolean = { protected def CanPerformRepairs(target: Repairable.Target, player: Player, item: Tool): Boolean = {
val definition = target.Definition 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 && (target.Faction == player.Faction || target.Faction == PlanetSideEmpire.NEUTRAL) && item.Magazine > 0 &&
player.isAlive && Vector3.Distance(target.Position, player.Position) < definition.RepairDistance player.isAlive && Vector3.Distance(target.Position, player.Position) < definition.RepairDistance
} }

View file

@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity 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 net.psforever.types._
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
@ -34,6 +34,9 @@ class Building(
private var playersInSOI: List[Player] = List.empty private var playersInSOI: List[Player] = List.empty
private var forceDomeActive: Boolean = false private var forceDomeActive: Boolean = false
private var participationFunc: ParticipationLogic = NoParticipation 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.Zone_=(zone)
super.GUID_=(PlanetSideGUID(building_guid)) //set super.GUID_=(PlanetSideGUID(building_guid)) //set
Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later 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 ( val cavernBenefit: Set[CavernBenefit] = if (
generatorState != PlanetSideGeneratorState.Destroyed && generatorState != PlanetSideGeneratorState.Destroyed &&
faction != PlanetSideEmpire.NEUTRAL && faction != PlanetSideEmpire.NEUTRAL && !CaptureTerminalIsHacked &&
connectedCavern().nonEmpty 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 { } else {
hasCavernLockBenefit = false
Set(CavernBenefit.None) Set(CavernBenefit.None)
} }
val (installedVirus, installedByFac) = if (virusId == 8) {
(8, None)
}
else {
(virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3))))
}
BuildingInfoUpdateMessage( BuildingInfoUpdateMessage(
Zone.Number, Zone.Number,
@ -230,8 +242,8 @@ class Building(
unk4 = Nil, unk4 = Nil,
unk5 = 0, unk5 = 0,
unk6 = false, unk6 = false,
unk7 = 8, // unk7 != 8 will cause malformed packet installedVirus,
unk7x = None, installedByFac,
boostSpawnPain, boostSpawnPain,
boostGeneratorPain boostGeneratorPain
) )

View file

@ -45,6 +45,7 @@ trait FacilityHackParticipation extends ParticipationLogic {
.filterNot { p => .filterNot { p =>
playerContribution.exists { case (u, _) => p.CharId == u } playerContribution.exists { case (u, _) => p.CharId == u }
} }
informOfInstalledVirus(newParticipants)
playerContribution = playerContribution =
vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++ vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++
newParticipants.map { p => (p.CharId, (p, 1, curr)) } ++ newParticipants.map { p => (p.CharId, (p, 1, curr)) } ++
@ -96,6 +97,27 @@ trait FacilityHackParticipation extends ParticipationLogic {
}) :+ newEntry }) :+ 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 { object FacilityHackParticipation {

View file

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

View file

@ -247,6 +247,9 @@ object ProximityTerminalControl {
target: PlanetSideGameObject target: PlanetSideGameObject
): Boolean = { ): Boolean = {
(terminal.Definition, target) match { (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 (_: MedicalTerminalDefinition, p: Player) => HealthAndArmorTerminal(terminal, p)
case (_: WeaponRechargeTerminalDefinition, p: Player) => WeaponRechargeTerminal(terminal, p) case (_: WeaponRechargeTerminalDefinition, p: Player) => WeaponRechargeTerminal(terminal, p)
case (_: MedicalTerminalDefinition, v: Vehicle) => VehicleRepairTerminal(terminal, v) case (_: MedicalTerminalDefinition, v: Vehicle) => VehicleRepairTerminal(terminal, v)
@ -269,6 +272,16 @@ object ProximityTerminalControl {
fullHeal && fullRepair 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, * When driving a vehicle close to a rearm/repair silo,
* restore the vehicle's health points. * 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 = { def PlayerHealthCallback(target: PlanetSideGameObject with Vitality with ZoneAware): Unit = {
val zone = target.Zone val zone = target.Zone
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(

View file

@ -2,7 +2,7 @@
package net.psforever.objects.serverobject.terminals package net.psforever.objects.serverobject.terminals
import akka.actor.ActorRef 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.CommonMessages
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
@ -57,7 +57,28 @@ class TerminalControl(term: Terminal)
) )
case _ => () 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 _ => () case _ => ()
} }

View file

@ -99,5 +99,6 @@ object CollisionReason {
* Damage is considered `Direct`, however, which defines some resistance. */ * Damage is considered `Direct`, however, which defines some resistance. */
val noDamage = new DamageProperties { val noDamage = new DamageProperties {
CausesDamageType = DamageType.Direct CausesDamageType = DamageType.Direct
DamageToArmorFirst = true
} }
} }

View file

@ -1,7 +1,9 @@
// Copyright (c) 2020 PSForever // Copyright (c) 2020 PSForever
package net.psforever.objects.vital.etc package net.psforever.objects.vital.etc
import net.psforever.objects.Vehicle
import net.psforever.objects.serverobject.painbox.Painbox import net.psforever.objects.serverobject.painbox.Painbox
import net.psforever.objects.serverobject.structures.Building
import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
import net.psforever.objects.vital.base.{DamageReason, DamageResolution} 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 private val definition = entity.Definition
assert(definition.innateDamage.nonEmpty, s"causal entity '${definition.Name}' does not emit pain field") 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 def resolution: DamageResolution.Value = DamageResolution.Resolved

View file

@ -21,7 +21,11 @@ trait DamageProperties
private var damageTypeSecondary: DamageType.Value = DamageType.None private var damageTypeSecondary: DamageType.Value = DamageType.None
/** against Infantry targets, damage does not apply to armor damage */ /** against Infantry targets, damage does not apply to armor damage */
private var damageToHealthOnly: Boolean = false 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 private var damageToVehicleOnly: Boolean = false
/** against battleframe targets, damage does not apply to battleframe robotics shield; /** against battleframe targets, damage does not apply to battleframe robotics shield;
* this is equivalent to the property "bfr_permeate_shield" */ * this is equivalent to the property "bfr_permeate_shield" */
@ -84,6 +88,13 @@ trait DamageProperties
DamageToHealthOnly DamageToHealthOnly
} }
def DamageToArmorFirst : Boolean = damageToArmorFirst
def DamageToArmorFirst_=(armorFirst: Boolean) : Boolean = {
damageToArmorFirst = armorFirst
DamageToArmorFirst
}
def DamageToVehicleOnly : Boolean = damageToVehicleOnly def DamageToVehicleOnly : Boolean = damageToVehicleOnly
def DamageToVehicleOnly_=(vehicleOnly: Boolean) : Boolean = { def DamageToVehicleOnly_=(vehicleOnly: Boolean) : Boolean = {

View file

@ -8,13 +8,15 @@ import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vehicles.VehicleSubsystemEntry import net.psforever.objects.vehicles.VehicleSubsystemEntry
import net.psforever.objects.vital.base.DamageResolution 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.damage.DamageCalculations
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.resistance.ResistanceSelection import net.psforever.objects.vital.resistance.ResistanceSelection
import net.psforever.types.{ExoSuitType, ImplantType} import net.psforever.types.{ExoSuitType, ImplantType}
import scala.annotation.unused
/** /**
* The base for the combining step of all projectile-induced damage calculation function literals. * 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 Output = PlanetSideGameObject with FactionAffinity => DamageResult
type Form = (DamageCalculations.Selector, ResistanceSelection.Format, DamageInteraction) => Output 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) = { def InfantryDamage(data: DamageInteraction): (Int, Int) => (Int, Int) = {
data.target match { data.target match {
case target: PlayerSource => case target: PlayerSource =>
if(data.cause.source.DamageToHealthOnly) { if (data.cause.source.DamageToHealthOnly) {
DamageToHealthOnly(target.health) DamageToHealthOnly(target.health)
} else if (data.cause.source.DamageToArmorFirst) {
InfantryArmorDamageFirst(target.health, target.armor)
} else { } else {
InfantryDamageAfterResist(target.health, target.armor) 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) = { def MaxDamage(data: DamageInteraction): (Int, Int) => (Int, Int) = {
data.target match { data.target match {
case target: PlayerSource => 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) val sameTarget = SourceEntry(target)
DamageResult(sameTarget, sameTarget, data) DamageResult(sameTarget, sameTarget, data)
} }

View file

@ -51,34 +51,16 @@ case object MapInfo extends StringEnum[MapInfo] {
scale = MapScale.Dim8192, scale = MapScale.Dim8192,
hotSpotSpan = 80, hotSpotSpan = 80,
environment = { environment = {
//exclude parts of voltan and naum due to their generator rooms being below sealevel List(SeaLevel(EnvironmentAttribute.Water, 11)) ++
val northVoltan = 3562.4844f MapEnvironment.zoneMapEdgeKillPlane(
val southVoltan = 3401.6875f MapScale.Dim8192,
val eastVoltan = 4556.703f (400, 400, 200, 400),
val westVoltan = 4411.6875f List(
val northNaum = 3575.8047f (450, 450, 250, 450, 3),
val southNaum = 3539.5234f (500, 500, 300, 500, 2),
val eastNaum = 5490.6875f (600, 600, 400, 600, 1)
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)
) )
)
} }
) )
@ -185,8 +167,7 @@ case object MapInfo extends StringEnum[MapInfo] {
hotSpotSpan = 80, hotSpotSpan = 80,
environment = List( environment = List(
SeaLevel(EnvironmentAttribute.Water, 10.03125f), 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, 3116.7266f, 4724.414f, 2685.8281f, 4187.4375f) //southwest of tootega
Pool(EnvironmentAttribute.Water, 213.03125f, 2994.2969f, 4363.461f, 2685.8281f, 4187.4375f), //west side of southwest of tootega
) ++ MapEnvironment.zoneMapEdgeKillPlane( ) ++ MapEnvironment.zoneMapEdgeKillPlane(
MapScale.Dim8192, MapScale.Dim8192,
(400, 400, 400, 400), (400, 400, 400, 400),
@ -265,15 +246,16 @@ case object MapInfo extends StringEnum[MapInfo] {
checksum = 230810349L, checksum = 230810349L,
scale = MapScale.Dim8192, scale = MapScale.Dim8192,
hotSpotSpan = 80, hotSpotSpan = 80,
environment = List(SeaLevel(EnvironmentAttribute.Water, 28)) ++ MapEnvironment.zoneMapEdgeKillPlane( environment = List(SeaLevel(EnvironmentAttribute.Water, 28)) ++
MapScale.Dim8192, MapEnvironment.zoneMapEdgeKillPlane(
(200, 200, 200, 200), MapScale.Dim8192,
List( (200, 200, 200, 200),
(250, 250, 250, 250, 3), List(
(300, 300, 300, 300, 2), (250, 250, 250, 250, 3),
(400, 400, 400, 400, 1) (300, 300, 300, 300, 2),
(400, 400, 400, 400, 1)
)
) )
)
) )
case object Map11 case object Map11
@ -370,13 +352,13 @@ case object MapInfo extends StringEnum[MapInfo] {
scale = MapScale.Dim2560, scale = MapScale.Dim2560,
hotSpotSpan = 80, hotSpotSpan = 80,
environment = List( 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, 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.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, 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, 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, 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 Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f) //lowest southern pool
SeaLevel(EnvironmentAttribute.Death, 10)
) )
) )
@ -449,14 +431,8 @@ case object MapInfo extends StringEnum[MapInfo] {
checksum = 3654267088L, checksum = 3654267088L,
scale = MapScale.Dim4096, scale = MapScale.Dim4096,
hotSpotSpan = 80, hotSpotSpan = 80,
environment = List( environment = List(SeaLevel(EnvironmentAttribute.Water, 3.5f)) ++
Pool(EnvironmentAttribute.Water, 3.5f, 2867f, 1228f, 1128f, 0f), //west MapEnvironment.dim4096MapEdgeKillPlanes
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
) )
case object Map99 case object Map99

View file

@ -32,9 +32,9 @@ import scalax.collection.GraphEdge._
import scala.util.Try import scala.util.Try
import akka.actor.typed import akka.actor.typed
import net.psforever.actors.session.AvatarActor 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.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.definition.ObjectDefinition
import net.psforever.objects.geometry.d3.VolumetricGeometry import net.psforever.objects.geometry.d3.VolumetricGeometry
import net.psforever.objects.guid.pool.NumberPool 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.prop.DamageWithPosition
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.blockmap.{BlockMap, SectorPopulation} 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.services.Service
import net.psforever.zones.Zones 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]() 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. * When the zone has completed initializing, this will be the future.
* @see `init(ActorContext)` * @see `init(ActorContext)`
@ -639,6 +651,142 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
} }
output.toList 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)
}
} }
/** /**

View file

@ -557,7 +557,7 @@ object GamePacketOpcode extends Enumeration {
case 0xd4 => game.GenericObjectActionAtPositionMessage.decode case 0xd4 => game.GenericObjectActionAtPositionMessage.decode
case 0xd5 => game.PropertyOverrideMessage.decode case 0xd5 => game.PropertyOverrideMessage.decode
case 0xd6 => game.WarpgateLinkOverrideMessage.decode case 0xd6 => game.WarpgateLinkOverrideMessage.decode
case 0xd7 => noDecoder(EmpireBenefitsMessage) case 0xd7 => game.EmpireBenefitsMessage.decode
// 0xd8 // 0xd8
case 0xd8 => noDecoder(ForceEmpireMessage) case 0xd8 => noDecoder(ForceEmpireMessage)
case 0xd9 => game.BroadcastWarpgateUpdateMessage.decode case 0xd9 => game.BroadcastWarpgateUpdateMessage.decode

View file

@ -24,11 +24,12 @@ final case class Additional1(unk1: String, unk2: Int, unk3: Long)
final case class Additional2(unk1: Int, unk2: Long) final case class Additional2(unk1: Int, unk2: Long)
/** /**
* na * Used for building information window on map. Tells the empire who installed the virus which one
* @param unk1 na * and tells the defending faction generic "Infected"
* @param unk2 na * @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. * 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 unk4 na
* @param unk5 na * @param unk5 na
* @param unk6 na * @param unk6 na
* @param unk7 na; * @param virus_id id of virus installed. value != 8 causes the next field to be defined.
* value != 8 causes the next field to be defined * 0 - unlock all doors
* @param unk7x na * 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_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 * @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], unk4: List[Additional2],
unk5: Long, unk5: Long,
unk6: Boolean, unk6: Boolean,
unk7: Int, virus_id: Int,
unk7x: Option[Additional3], virus_installed_by: Option[Additional3],
boost_spawn_pain: Boolean, boost_spawn_pain: Boolean,
boost_generator_pain: Boolean boost_generator_pain: Boolean
) extends PlanetSideGamePacket { ) extends PlanetSideGamePacket {
@ -129,8 +135,8 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
* A `Codec` for a set of additional fields. * A `Codec` for a set of additional fields.
*/ */
private val additional3_codec: Codec[Additional3] = ( private val additional3_codec: Codec[Additional3] = (
("unk1" | bool) :: ("inform_defenders" | bool) ::
("unk2" | uint2L) ("installed_by_id" | uint2L)
).as[Additional3] ).as[Additional3]
/** /**
@ -190,8 +196,8 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
("unk4" | listOfN(uint4L, additional2_codec)) :: ("unk4" | listOfN(uint4L, additional2_codec)) ::
("unk5" | uint32L) :: ("unk5" | uint32L) ::
("unk6" | bool) :: ("unk6" | bool) ::
(("unk7" | uint4L) >>:~ { unk7 => (("virus_id" | uint4L) >>:~ { virus_id =>
conditional(unk7 != 8, codec = "unk7x" | additional3_codec) :: conditional(virus_id != 8, codec = "virus_installed_by" | additional3_codec) ::
("boost_spawn_pain" | bool) :: ("boost_spawn_pain" | bool) ::
("boost_generator_pain" | bool) ("boost_generator_pain" | bool)
}) })

View file

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

View file

@ -50,6 +50,9 @@ import shapeless.{::, HNil}
* 53 - Put down an FDU<br/> * 53 - Put down an FDU<br/>
* 56 - Sets vehicle or player to be black ops<br/> * 56 - Sets vehicle or player to be black ops<br/>
* 57 - Reverts player from black ops<br/> * 57 - Reverts player from black ops<br/>
* 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
* <br> * <br>
* What are these values?<br> * What are these values?<br>
* 90? - for observed driven BFR's, model pitches up slightly and stops idle animation<br> * 90? - for observed driven BFR's, model pitches up slightly and stops idle animation<br>

View file

@ -28,11 +28,11 @@ sealed abstract class HackState7(val value: Int) extends IntEnumEntry
object HackState7 extends IntEnum[HackState7] { object HackState7 extends IntEnum[HackState7] {
val values: IndexedSeq[HackState7] = findValues val values: IndexedSeq[HackState7] = findValues
case object Unk0 extends HackState7(value = 0) case object UnlockDoors extends HackState7(value = 0)
case object Unk1 extends HackState7(value = 1) case object DisableLatticeBenefits extends HackState7(value = 1)
case object Unk2 extends HackState7(value = 2) case object NTUDrain extends HackState7(value = 2)
case object Unk3 extends HackState7(value = 3) case object DisableRadar extends HackState7(value = 3)
case object Unk4 extends HackState7(value = 4) case object AccessEquipmentTerms extends HackState7(value = 4)
case object Unk5 extends HackState7(value = 5) case object Unk5 extends HackState7(value = 5)
case object Unk6 extends HackState7(value = 6) case object Unk6 extends HackState7(value = 6)
case object Unk7 extends HackState7(value = 7) case object Unk7 extends HackState7(value = 7)

View file

@ -201,7 +201,7 @@ import scodec.codecs._
* `68 - Vehicle shield health`<br> * `68 - Vehicle shield health`<br>
* `79 - ???`<br> * `79 - ???`<br>
* `80 - Damage vehicle (unknown value)`<br> * `80 - Damage vehicle (unknown value)`<br>
* `81 - ???`<br> * `81 - Player bailed from vehicle, causes bail animation `<br>
* `113 - Vehicle capacitor - e.g. Leviathan EMP charge` * `113 - Vehicle capacitor - e.g. Leviathan EMP charge`
* @param guid the object * @param guid the object
* @param attribute_type the field * @param attribute_type the field

View file

@ -44,7 +44,7 @@ final case class VariantVehicleData(unk: Int) extends SpecificVehicleData(Vehicl
* -jammered - vehicles will not be jammered by setting this field<br> * -jammered - vehicles will not be jammered by setting this field<br>
* -player_guid the vehicle's (official) owner; * -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 * 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 health the amount of health the vehicle has, as a percentage of a filled bar (255)
* @param unk4 na * @param unk4 na
* @param no_mount_points do not display entry points for the seats * @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( final case class VehicleData(
pos: PlacementData, pos: PlacementData,
data: CommonFieldData, data: CommonFieldData,
unk3: Boolean, boostMaxHealth: Boolean,
health: Int, health: Int,
unk4: Boolean, unk4: Boolean,
no_mount_points: Boolean, no_mount_points: Boolean,
@ -106,7 +106,7 @@ object VehicleData extends Marshallable[VehicleData] {
cloak: Boolean, cloak: Boolean,
inventory: Option[InventoryData] inventory: Option[InventoryData]
): VehicleData = { ): 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 VehicleFormat.Normal
) )
} }
@ -128,7 +128,7 @@ object VehicleData extends Marshallable[VehicleData] {
format: UtilityVehicleData, format: UtilityVehicleData,
inventory: Option[InventoryData] inventory: Option[InventoryData]
): VehicleData = { ): 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 VehicleFormat.Utility
) )
} }
@ -150,7 +150,7 @@ object VehicleData extends Marshallable[VehicleData] {
format: VariantVehicleData, format: VariantVehicleData,
inventory: Option[InventoryData] inventory: Option[InventoryData]
): VehicleData = { ): 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 VehicleFormat.Variant
) )
} }

View file

@ -555,15 +555,37 @@ class CavernRotationService(
def sendCavernRotationUpdates(sendToSession: ActorRef): Unit = { def sendCavernRotationUpdates(sendToSession: ActorRef): Unit = {
val curr = System.currentTimeMillis() val curr = System.currentTimeMillis()
val (lockedZones, unlockedZones) = managedZones.partition(_.locked) val (lockedZones, unlockedZones) = managedZones.partition(_.locked)
//borrow GalaxyService response structure, but send to the specific endpoint //borrow GalaxyService response structure, but send to the specific endpoint math.max(0, monitor.start + monitor.duration - curr)
lockedZones.foreach { monitor => unlockedZones.foreach { monitor =>
sendToSession ! GalaxyServiceResponse("", GalaxyResponse.UnlockedZoneUpdate(monitor.zone))
}
val sortedLocked = lockedZones.sortBy(z => z.start)
sortedLocked.take(2).foreach { monitor =>
sendToSession ! GalaxyServiceResponse( sendToSession ! GalaxyServiceResponse(
"", "",
GalaxyResponse.LockedZoneUpdate(monitor.zone, math.max(0, monitor.start + monitor.duration - curr)) GalaxyResponse.LockedZoneUpdate(monitor.zone, math.max(0, monitor.start + monitor.duration - curr))
) )
} }
unlockedZones.foreach { monitor => sortedLocked.takeRight(2).foreach { monitor =>
sendToSession ! GalaxyServiceResponse("", GalaxyResponse.UnlockedZoneUpdate(monitor.zone)) 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 //zone transition immediately
lockTimer.cancel() lockTimer.cancel()
unlockTimer.cancel() unlockTimer.cancel()
retimeZonesUponForcedRotation(galaxyService)
zoneRotationFunc(galaxyService) zoneRotationFunc(galaxyService)
lockTimerToDisplayWarning(timeBetweenRotationsHours.hours - firstClosingWarningAtMinutes.minutes) lockTimerToDisplayWarning(timeBetweenRotationsHours.hours - firstClosingWarningAtMinutes.minutes)
retimeZonesUponForcedRotation(galaxyService)
} else { } else {
//instead of transitioning immediately, jump to the 5 minute rotation warning for the benefit of players //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 lockTimer.cancel() //won't need to retime until zone change
@ -651,7 +673,6 @@ class CavernRotationService(
galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse( galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(
ChatMsg(ChatMessageType.UNK_229, s"@cavern_switched^@${lockingZone.id}~^@${unlockingZone.id}") 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 //change warp gate statuses to reflect zone lock state
CavernRotationService.disableLatticeLinksAndWarpGateAccessibility( CavernRotationService.disableLatticeLinksAndWarpGateAccessibility(
((prevToLock until managedZones.size) ++ (0 until prevToLock)) ((prevToLock until managedZones.size) ++ (0 until prevToLock))
@ -664,7 +685,7 @@ class CavernRotationService(
.map(managedZones(_).zone) .map(managedZones(_).zone)
) )
} }
galaxyService ! GalaxyServiceMessage(GalaxyAction.LockedZoneUpdate(locking.zone, fullHoursBetweenRotationsAsMillis)) sendCavernRotationUpdatesToAll(galaxyService)
} }
/** /**
@ -689,9 +710,6 @@ class CavernRotationService(
val zone = managedZones(monitorIndex) val zone = managedZones(monitorIndex)
val newStart = startingInThePast + (index * timeBetweenRotationsHours).hours.toMillis val newStart = startingInThePast + (index * timeBetweenRotationsHours).hours.toMillis
zone.start = newStart 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("")) //println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString(""))
} }
@ -715,10 +733,8 @@ class CavernRotationService(
val advanceByTimeAsMillis = advanceTimeBy.toMillis val advanceByTimeAsMillis = advanceTimeBy.toMillis
managedZones.foreach { zone => managedZones.foreach { zone =>
zone.start = zone.start - advanceByTimeAsMillis 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("")) //println(managedZones.flatMap { z => s"[${z.start + z.duration - curr}]"}.mkString(""))
} }

View file

@ -259,14 +259,18 @@ class HackCaptureActor extends Actor {
) )
} }
// Push map update to clients // 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 = { private def HackCompleted(terminal: CaptureTerminal with Hackable, hackedByFaction: PlanetSideEmpire.Value): Unit = {
val building = terminal.Owner.asInstanceOf[Building] val building = terminal.Owner.asInstanceOf[Building]
if (building.NtuLevel > 0) { if (building.NtuLevel > 0) {
building.virusId = 8
building.virusInstalledBy = None
log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction") 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 //dispatch to players aligned with the capturing faction within the SOI
val events = building.Zone.LocalEvents val events = building.Zone.LocalEvents
val msg = LocalAction.SendGenericActionMessage(Service.defaultPlayerGUID, GenericAction.FacilityCaptureFanfare) val msg = LocalAction.SendGenericActionMessage(Service.defaultPlayerGUID, GenericAction.FacilityCaptureFanfare)
@ -275,19 +279,34 @@ class HackCaptureActor extends Actor {
.collect { case p if p.Faction == hackedByFaction => .collect { case p if p.Faction == hackedByFaction =>
events ! LocalServiceMessage(p.Name, msg) events ! LocalServiceMessage(p.Name, msg)
} }
val zoneBases = building.Zone.Buildings.filter(base => val buildings = building.Zone.Buildings.values
base._2.BuildingType == StructureType.Facility) val hackedBaseId = building.GUID
val ownedBases = building.Zone.Buildings.filter(base => val facilities = if (building.Zone.id.startsWith("c")) {
base._2.BuildingType == StructureType.Facility && base._2.Faction == hackedByFaction buildings.filter(b =>
&& base._2.GUID != building.GUID) b.Name.startsWith("N") || b.Name.startsWith("S")).toSeq
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)
} }
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 { } else {
log.info("Base hack completed, but base was out of NTU.") log.info("Base hack completed, but base was out of NTU.")
} }
@ -317,23 +336,10 @@ class HackCaptureActor extends Actor {
if (buildingIterator.hasNext) { if (buildingIterator.hasNext) {
val building = buildingIterator.next() val building = buildingIterator.next()
val terminal = building.CaptureTerminal.get val terminal = building.CaptureTerminal.get
val zone = building.Zone
val zoneActor = zone.actor
val buildingActor = building.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.SetFaction(faction)
buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false))
//push for map updates again buildingActor ! BuildingActor.MapUpdate()
zoneActor ! ZoneActor.ZoneMapUpdate()
} }
}, },
0, 0,

View file

@ -3,7 +3,7 @@ package net.psforever.services.local.support
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import akka.actor.{Actor, Cancellable} 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.hackable.Hackable
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
@ -30,8 +30,12 @@ class HackClearActor() extends Actor {
def receive: Receive = { def receive: Receive = {
case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) => case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) =>
val durationMillis = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS) 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 // Restart the timer, in case this is the first object in the hacked objects list
RestartTimer() RestartTimer()
@ -49,6 +53,9 @@ class HackClearActor() extends Actor {
entry.unk1, entry.unk1,
entry.unk2 entry.unk2
) //call up to the main event system ) //call up to the main event system
if (entry.target.Definition == GlobalDefinitions.main_terminal) {
ClearVirusFromBuilding(entry.target)
}
}) })
RestartTimer() 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. * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
* Separate the original `List` into two: * Separate the original `List` into two:

View file

@ -156,7 +156,7 @@ object Zones {
"vanu_vehicle_station" "vanu_vehicle_station"
) )
private val basicTerminalTypes = 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( private val spawnPadTerminalTypes = Seq(
"ground_vehicle_terminal", "ground_vehicle_terminal",
"air_vehicle_terminal", "air_vehicle_terminal",
@ -345,6 +345,15 @@ object Zones {
), ),
owningBuildingGuid = buildingGuid 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 = val filteredZoneEntities =
@ -557,7 +566,7 @@ object Zones {
case "adv_med_terminal" | "repair_silo" | "pad_landing_frame" | "pad_landing_tower_frame" | "medical_terminal" | 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_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( zoneMap.addLocalObject(
obj.guid, obj.guid,
ProximityTerminal ProximityTerminal

View file

@ -30,8 +30,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
unk4, unk4,
unk5, unk5,
unk6, unk6,
unk7, virus_id,
unk7x, virus_installed_by,
boost_spawn_pain, boost_spawn_pain,
boost_generator_pain boost_generator_pain
) => ) =>
@ -53,8 +53,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
unk4.isEmpty mustEqual true unk4.isEmpty mustEqual true
unk5 mustEqual 0 unk5 mustEqual 0
unk6 mustEqual false unk6 mustEqual false
unk7 mustEqual 8 virus_id mustEqual 8
unk7x.isEmpty mustEqual true virus_installed_by.isEmpty mustEqual true
boost_spawn_pain mustEqual false boost_spawn_pain mustEqual false
boost_generator_pain mustEqual false boost_generator_pain mustEqual false
case _ => case _ =>

View file

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

View file

@ -39,7 +39,7 @@ class MountedVehiclesTest extends Specification {
vdata.no_mount_points mustEqual false vdata.no_mount_points mustEqual false
vdata.driveState mustEqual DriveState.Mobile vdata.driveState mustEqual DriveState.Mobile
vdata.cloak mustEqual false vdata.cloak mustEqual false
vdata.unk3 mustEqual false vdata.boostMaxHealth mustEqual false
vdata.unk4 mustEqual false vdata.unk4 mustEqual false
vdata.unk5 mustEqual false vdata.unk5 mustEqual false
vdata.unk6 mustEqual false vdata.unk6 mustEqual false

View file

@ -36,7 +36,7 @@ class UtilityVehiclesTest extends Specification {
ant.driveState mustEqual DriveState.Mobile ant.driveState mustEqual DriveState.Mobile
ant.health mustEqual 255 ant.health mustEqual 255
ant.cloak mustEqual false ant.cloak mustEqual false
ant.unk3 mustEqual false ant.boostMaxHealth mustEqual false
ant.unk4 mustEqual false ant.unk4 mustEqual false
ant.unk5 mustEqual false ant.unk5 mustEqual false
ant.unk6 mustEqual false ant.unk6 mustEqual false
@ -67,7 +67,7 @@ class UtilityVehiclesTest extends Specification {
ams.vehicle_format_data mustEqual Some(UtilityVehicleData(60)) ams.vehicle_format_data mustEqual Some(UtilityVehicleData(60))
ams.health mustEqual 236 ams.health mustEqual 236
ams.cloak mustEqual true ams.cloak mustEqual true
ams.unk3 mustEqual false ams.boostMaxHealth mustEqual false
ams.unk4 mustEqual false ams.unk4 mustEqual false
ams.unk5 mustEqual false ams.unk5 mustEqual false
ams.unk6 mustEqual true ams.unk6 mustEqual true
@ -120,7 +120,7 @@ class UtilityVehiclesTest extends Specification {
case _ => case _ =>
ko ko
} }
ams.unk3 mustEqual false ams.boostMaxHealth mustEqual false
ams.health mustEqual 255 ams.health mustEqual 255
ams.unk4 mustEqual false ams.unk4 mustEqual false
ams.no_mount_points mustEqual false ams.no_mount_points mustEqual false
@ -320,7 +320,7 @@ class UtilityVehiclesTest extends Specification {
Some(Vector3(27.3375f, -0.78749996f, 0.1125f)) Some(Vector3(27.3375f, -0.78749996f, 0.1125f))
), ),
CommonFieldData(PlanetSideEmpire.TR, false, false, false, None, false, Some(false), None, PlanetSideGUID(3087)), CommonFieldData(PlanetSideEmpire.TR, false, false, false, None, false, Some(false), None, PlanetSideGUID(3087)),
unk3 = false, boostMaxHealth = false,
health = 255, health = 255,
unk4 = false, unk4 = false,
no_mount_points = false, no_mount_points = false,