diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst
index dc45b8850..26a2c85ad 100644
--- a/server/src/main/resources/overrides/game_objects0.adb.lst
+++ b/server/src/main/resources/overrides/game_objects0.adb.lst
@@ -91,7 +91,7 @@ add_property super_staminakit nodrop false
add_property super_staminakit requirement_award0 false
add_property suppressor equiptime 600
add_property suppressor holstertime 600
-add_property trek allowed false
+add_property trek allowed true
add_property trek equiptime 500
add_property trek holstertime 500
add_property vulture requirement_award0 false
diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
index 539e50935..b7dc548eb 100644
--- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
@@ -1258,7 +1258,12 @@ class GeneralOperations(
def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
equipment match {
case Some(item) =>
- sendUseGeneralEntityMessage(terminal, item)
+ if (terminal.Definition == GlobalDefinitions.main_terminal) {
+ sendUseMainTerminalMessage(terminal, item, msg.unk2)
+ }
+ else {
+ sendUseGeneralEntityMessage(terminal, item)
+ }
case None
if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction ||
terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL =>
@@ -1484,6 +1489,14 @@ class GeneralOperations(
obj.Actor ! CommonMessages.Use(player, Some(equipment))
}
+ def sendUseMainTerminalMessage(obj: PlanetSideServerObject, equipment: Equipment, virus: Long): Unit = {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ if (player.Faction == obj.Faction)
+ obj.Actor ! CommonMessages.RemoveVirus(player, Some(equipment))
+ else
+ obj.Actor ! CommonMessages.UploadVirus(player, Some(equipment), virus)
+ }
+
def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = {
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
equipment match {
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index ddeff7e31..b7b2e99bf 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -1116,6 +1116,17 @@ class ZoningOperations(
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)
case _ =>
sessionLogic.general.hackObject(amenity.GUID, unk1 = 1114636288L, HackState7.Unk8) //generic hackable object
}
@@ -2928,7 +2939,8 @@ class ZoningOperations(
val searhusBenefit = Zones.zones.find(_.Number == 9).exists(_.benefitRecipient == player.Faction)
//biolabs have/grant benefits
val cryoBenefit: Float = toSpawnPoint.Owner match {
- case b: Building if b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) || (b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked && searhusBenefit) => 0.5f
+ case b: Building if (b.hasLatticeBenefit(LatticeBenefit.BioLaboratory) && b.virusId != 1) ||
+ (b.BuildingType == StructureType.Facility && !b.CaptureTerminalIsHacked && searhusBenefit) => 0.5f
case _ => 1f
}
//TODO cumulative death penalty
diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala
index 546a4c4a8..47415ec3a 100644
--- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala
+++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala
@@ -158,6 +158,7 @@ case object MajorFacilityLogic
* @return the next behavior for this control agency messaging system
*/
def amenityStateChange(details: BuildingWrapper, entity: Amenity, data: Option[Any]): Behavior[Command] = {
+ import net.psforever.objects.GlobalDefinitions
entity match {
case gen: Generator =>
if (generatorStateChange(details, gen, data)) {
@@ -176,12 +177,24 @@ case object MajorFacilityLogic
case _ =>
log(details).warn("CaptureTerminal AmenityStateChange was received with no attached data.")
}
- // When a CC is hacked (or resecured) all currently hacked amenities for the base should return to their default unhacked state
- building.HackableAmenities.foreach(amenity => {
- if (amenity.HackedBy.isDefined) {
- building.Zone.LocalEvents ! LocalServiceMessage(amenity.Zone.id,LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity))
- }
- })
+ // When a CC is hacked (or resecured) clear hacks on amenities based on currently installed virus
+ val hackedAmenities = building.HackableAmenities.filter(_.HackedBy.isDefined)
+ val amenitiesToClear = building.virusId match {
+ case 0 =>
+ hackedAmenities.filterNot(a => a.Definition == GlobalDefinitions.lock_external || a.Definition == GlobalDefinitions.main_terminal)
+ case 4 =>
+ hackedAmenities.filterNot(a => a.Definition == GlobalDefinitions.order_terminal || a.Definition == GlobalDefinitions.main_terminal)
+ case 8 =>
+ hackedAmenities
+ case _ =>
+ hackedAmenities
+ }
+ amenitiesToClear.foreach { amenity =>
+ building.Zone.LocalEvents ! LocalServiceMessage(
+ amenity.Zone.id,
+ LocalAction.ClearTemporaryHack(PlanetSideGUID(0), amenity)
+ )
+ }
// No map update needed - will be sent by `HackCaptureActor` when required
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index 96aae6eb6..39731ad4e 100644
--- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -1239,6 +1239,8 @@ object GlobalDefinitions {
val vanu_control_console = new CaptureTerminalDefinition(930) // Cavern CC
+ val main_terminal = new MainTerminalDefinition(473)
+
val llm_socket = new CaptureFlagSocketDefinition()
val capture_flag = new CaptureFlagDefinition()
diff --git a/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala b/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala
index a527095ea..7ce23c412 100644
--- a/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala
+++ b/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala
@@ -72,9 +72,9 @@ object FirstTimeEvents {
)
val Other: Set[String] = Set(
- "used_nchev_scattercannon",
- "used_nchev_falcon",
- "used_nchev_sparrow",
+ "used_nc_hev_scattercannon",
+ "used_nc_hev_falcon",
+ "used_nc_hev_sparrow",
"used_energy_gun_nc",
"visited_portable_manned_turret_nc"
)
diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala
index d60c9d5ed..551d62a40 100644
--- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala
+++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala
@@ -720,6 +720,10 @@ object GlobalDefinitionsMiscellaneous {
vanu_control_console.Repairable = false
vanu_control_console.FacilityHackTime = 10.minutes
+ main_terminal.Name = "main_terminal"
+ main_terminal.Damageable = false
+ main_terminal.Repairable = false
+
lodestar_repair_terminal.Name = "lodestar_repair_terminal"
lodestar_repair_terminal.Interval = 1000
lodestar_repair_terminal.HealAmount = 60
diff --git a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
index ef1d4679b..df35f955a 100644
--- a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
@@ -11,7 +11,9 @@ object CommonMessages {
final case class Hack(player: Player, obj: PlanetSideServerObject with Hackable, data: Option[Any] = None)
final case class ClearHack()
final case class EntityHackState(obj: PlanetSideGameObject with Hackable, hackState: Boolean)
-
+ final case class UploadVirus(player: Player, data: Option[Any] = None, virus: Long)
+ final case class RemoveVirus(player: Player, data: Option[Any])
+
/**
* The message that progresses some form of user-driven activity with a certain eventual outcome
* and potential feedback per cycle.
diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
index 5c6aca70c..8f654ded9 100644
--- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
@@ -1,11 +1,13 @@
// Copyright (c) 2020 PSForever
package net.psforever.objects.serverobject.hackable
+import net.psforever.actors.zone.BuildingActor
import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
+import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
-import net.psforever.objects.{Player, Vehicle}
+import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
-import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7}
+import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, TriggeredSound}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@@ -140,6 +142,116 @@ 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)
+ )
+ val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.GUID, 60)
+ val events = zone.AvatarEvents
+ building.PlayersInSOI.foreach { player =>
+ events ! AvatarServiceMessage(player.Name, msg)
+ }
+ 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 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)
+ )
+ val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.GUID, 58)
+ val events = zone.AvatarEvents
+ building.PlayersInSOI.foreach { player =>
+ events ! AvatarServiceMessage(player.Name, msg)
+ }
+ //amenities if applicable
+ virus match {
+ case 0L =>
+ building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.lock_external).foreach{ iff =>
+ var setHacked = iff.asInstanceOf[PlanetSideServerObject with Hackable]
+ setHacked.HackedBy = tplayer
+ zone.LocalEvents ! LocalServiceMessage(
+ zoneId,
+ LocalAction.HackTemporarily(pguid, zone, iff, hackValue, hackClearValue, installedVirusDuration)
+ )
+ }
+ case 4L =>
+ building.HackableAmenities.filter(d => d.Definition == GlobalDefinitions.order_terminal).foreach{ term =>
+ var setHacked = term.asInstanceOf[PlanetSideServerObject with Hackable]
+ setHacked.HackedBy = tplayer
+ zone.LocalEvents ! LocalServiceMessage(
+ zoneId,
+ LocalAction.HackTemporarily(pguid, zone, term, hackValue, hackClearValue, installedVirusDuration)
+ )
+ }
+ case _ => ()
+ }
+ building.Actor ! BuildingActor.MapUpdate()
+ }
+ case Failure(_) =>
+ log.warn(s"Virus action failed on target: ${target.Definition.Name}@${target.GUID.guid}")
+ }
+ }
+
/**
* Check if the state of connected facilities has changed since the hack progress began. It accounts for a friendly facility
* on the other side of a warpgate as well in case there are no friendly facilities in the same zone
diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala
index 1d2f7c73b..b880045d4 100644
--- a/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/repair/AmenityAutoRepair.scala
@@ -270,20 +270,19 @@ trait AmenityAutoRepair
autoRepairTimer.cancel()
autoRepairQueueTask = Some(System.currentTimeMillis() + delay)
val modifiedDrain = drain * Config.app.game.amenityAutorepairDrainRate //doubled intentionally
- autoRepairTimer = if(AutoRepairObject.Owner == Building.NoBuilding) {
- //without an owner, auto-repair freely
- context.system.scheduler.scheduleOnce(
- delay milliseconds,
- self,
- NtuCommand.Grant(null, modifiedDrain)
- )
- } else {
- //ask politely
- context.system.scheduler.scheduleOnce(
- delay milliseconds,
- AutoRepairObject.Owner.Actor,
- BuildingActor.Ntu(NtuCommand.Request(modifiedDrain, ntuGrantActorRef))
- )
- }
+ AutoRepairObject.Owner match {
+ case Building.NoBuilding =>
+ autoRepairTimer = context.system.scheduler.scheduleOnce(
+ delay.milliseconds,
+ self,
+ NtuCommand.Grant(null, modifiedDrain))
+ case b: Building =>
+ val doubledDrain = if (b.virusId == 2) modifiedDrain * 2 else modifiedDrain
+ autoRepairTimer = context.system.scheduler.scheduleOnce(
+ delay.milliseconds,
+ b.Actor,
+ BuildingActor.Ntu(NtuCommand.Request(doubledDrain, ntuGrantActorRef)))
+ case _ => ()
+ }
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
index adcf381b1..f0c06e71d 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
@@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.zones.Zone
import net.psforever.objects.zones.blockmap.BlockMapEntity
-import net.psforever.packet.game.{BuildingInfoUpdateMessage, DensityLevelUpdateMessage}
+import net.psforever.packet.game.{Additional3, BuildingInfoUpdateMessage, DensityLevelUpdateMessage}
import net.psforever.types._
import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._
@@ -34,6 +34,8 @@ class Building(
private var playersInSOI: List[Player] = List.empty
private var forceDomeActive: Boolean = false
private var participationFunc: ParticipationLogic = NoParticipation
+ var virusId: Long = 8 // 8 default = no virus
+ var virusInstalledBy: Option[Int] = None // faction id
super.Zone_=(zone)
super.GUID_=(PlanetSideGUID(building_guid)) //set
Invalidate() //unset; guid can be used during setup, but does not stop being registered properly later
@@ -211,6 +213,12 @@ class Building(
} else {
Set(CavernBenefit.None)
}
+ val (installedVirus, installedByFac) = if (virusId == 8) {
+ (8, None)
+ }
+ else {
+ (virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3))))
+ }
BuildingInfoUpdateMessage(
Zone.Number,
@@ -230,8 +238,8 @@ class Building(
unk4 = Nil,
unk5 = 0,
unk6 = false,
- unk7 = 8, // unk7 != 8 will cause malformed packet
- unk7x = None,
+ installedVirus,
+ installedByFac,
boostSpawnPain,
boostGeneratorPain
)
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
index 90c750b00..b454fcfb2 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
@@ -45,6 +45,7 @@ trait FacilityHackParticipation extends ParticipationLogic {
.filterNot { p =>
playerContribution.exists { case (u, _) => p.CharId == u }
}
+ informOfInstalledVirus(newParticipants)
playerContribution =
vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++
newParticipants.map { p => (p.CharId, (p, 1, curr)) } ++
@@ -96,6 +97,25 @@ trait FacilityHackParticipation extends ParticipationLogic {
}) :+ newEntry
}
}
+
+ /**
+ * send packet that makes building lights green in case a virus was installed before this player got there
+ * @param list new players to the SOI
+ */
+ protected def informOfInstalledVirus(list : List[Player]): Unit = {
+ if (building.virusId != 8) {
+ import net.psforever.objects.serverobject.terminals.Terminal
+ import net.psforever.objects.GlobalDefinitions
+ import net.psforever.services.Service
+ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+ val mainTerm = building.Amenities.filter(x => x.isInstanceOf[Terminal] && x.Definition == GlobalDefinitions.main_terminal).head.GUID
+ val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, mainTerm, 58)
+ val events = building.Zone.AvatarEvents
+ list.foreach(p =>
+ events ! AvatarServiceMessage(p.Name, msg)
+ )
+ }
+ }
}
object FacilityHackParticipation {
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/MainTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/MainTerminalDefinition.scala
new file mode 100644
index 000000000..270d6bfaf
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/MainTerminalDefinition.scala
@@ -0,0 +1,16 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.serverobject.terminals
+
+import akka.actor.ActorContext
+import net.psforever.objects.{Default, Player}
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.serverobject.structures.Amenity
+
+/**
+ * The definition for any `Terminal` that is of a type "main_terminal".
+ * Main terminal objects are used to upload or remove a virus from a major facility
+ * @param objectId the object's identifier number
+ */
+class MainTerminalDefinition(objectId: Int) extends TerminalDefinition(objectId) {
+ def Request(player: Player, msg: Any): Terminal.Exchange = Terminal.NoDeal()
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala
index 94c340fde..b3a80776e 100644
--- a/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala
@@ -2,7 +2,7 @@
package net.psforever.objects.serverobject.terminals
import akka.actor.ActorRef
-import net.psforever.objects.{GlobalDefinitions, SimpleItem}
+import net.psforever.objects.{GlobalDefinitions, SimpleItem, Tool}
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
@@ -57,7 +57,28 @@ class TerminalControl(term: Terminal)
)
case _ => ()
}
-
+ case CommonMessages.UploadVirus(player, Some(item: Tool), virus)
+ if item.Definition == GlobalDefinitions.trek =>
+ term.Owner match {
+ case _: Building =>
+ sender() ! CommonMessages.Progress(
+ 1.66f,
+ GenericHackables.FinishVirusAction(term, player, hackValue = -1, hackClearValue = -1, virus),
+ GenericHackables.HackingTickAction(HackState1.Unk1, player, term, item.GUID)
+ )
+ case _ => ()
+ }
+ case CommonMessages.RemoveVirus(player, Some(item: SimpleItem))
+ if item.Definition == GlobalDefinitions.remote_electronics_kit =>
+ term.Owner match {
+ case _: Building =>
+ sender() ! CommonMessages.Progress(
+ 1.66f,
+ GenericHackables.FinishVirusAction(term, player, hackValue = -1, hackClearValue = -1, virus=8L),
+ GenericHackables.HackingTickAction(HackState1.Unk1, player, term, item.GUID)
+ )
+ case _ => ()
+ }
case _ => ()
}
diff --git a/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala b/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala
index c59bed321..d8b6d86fa 100644
--- a/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala
@@ -24,11 +24,12 @@ final case class Additional1(unk1: String, unk2: Int, unk3: Long)
final case class Additional2(unk1: Int, unk2: Long)
/**
- * na
- * @param unk1 na
- * @param unk2 na
+ * Used for building information window on map. Tells the empire who installed the virus which one
+ * and tells the defending faction generic "Infected"
+ * @param inform_defenders na
+ * @param installed_by_id na
*/
-final case class Additional3(unk1: Boolean, unk2: Int)
+final case class Additional3(inform_defenders: Boolean, installed_by_id: Int)
/**
* Update the state of map asset for a client's specific building's state.
@@ -73,9 +74,14 @@ final case class Additional3(unk1: Boolean, unk2: Int)
* @param unk4 na
* @param unk5 na
* @param unk6 na
- * @param unk7 na;
- * value != 8 causes the next field to be defined
- * @param unk7x na
+ * @param virus_id id of virus installed. value != 8 causes the next field to be defined.
+ * 0 - unlock all doors
+ * 1 - disable linked benefits
+ * 2 - double ntu drain
+ * 3 - disable enemy radar
+ * 4 - access equipment terminals
+ * 8 - no virus installed - if 8, virus_installed_by is None
+ * @param virus_installed_by if virus_id = 8, None, else this has bool and id of the empire that installed the virus
* @param boost_spawn_pain if the building has spawn tubes, the (boosted) strength of its enemy pain field
* @param boost_generator_pain if the building has a generator, the (boosted) strength of its enemy pain field
*/
@@ -97,8 +103,8 @@ final case class BuildingInfoUpdateMessage(
unk4: List[Additional2],
unk5: Long,
unk6: Boolean,
- unk7: Int,
- unk7x: Option[Additional3],
+ virus_id: Int,
+ virus_installed_by: Option[Additional3],
boost_spawn_pain: Boolean,
boost_generator_pain: Boolean
) extends PlanetSideGamePacket {
@@ -129,8 +135,8 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
* A `Codec` for a set of additional fields.
*/
private val additional3_codec: Codec[Additional3] = (
- ("unk1" | bool) ::
- ("unk2" | uint2L)
+ ("inform_defenders" | bool) ::
+ ("installed_by_id" | uint2L)
).as[Additional3]
/**
@@ -190,8 +196,8 @@ object BuildingInfoUpdateMessage extends Marshallable[BuildingInfoUpdateMessage]
("unk4" | listOfN(uint4L, additional2_codec)) ::
("unk5" | uint32L) ::
("unk6" | bool) ::
- (("unk7" | uint4L) >>:~ { unk7 =>
- conditional(unk7 != 8, codec = "unk7x" | additional3_codec) ::
+ (("virus_id" | uint4L) >>:~ { virus_id =>
+ conditional(virus_id != 8, codec = "virus_installed_by" | additional3_codec) ::
("boost_spawn_pain" | bool) ::
("boost_generator_pain" | bool)
})
diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala
index 8d4b7e4cb..71cc8cd2b 100644
--- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala
@@ -50,6 +50,7 @@ import shapeless.{::, HNil}
* 53 - Put down an FDU
* 56 - Sets vehicle or player to be black ops
* 57 - Reverts player from black ops
+ * 58 - Virus installed, changes lighting in facility to green
*
* What are these values?
* 90? - for observed driven BFR's, model pitches up slightly and stops idle animation
diff --git a/src/main/scala/net/psforever/packet/game/HackMessage.scala b/src/main/scala/net/psforever/packet/game/HackMessage.scala
index 1df1afb5d..b5d0c7e7b 100644
--- a/src/main/scala/net/psforever/packet/game/HackMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/HackMessage.scala
@@ -28,11 +28,11 @@ sealed abstract class HackState7(val value: Int) extends IntEnumEntry
object HackState7 extends IntEnum[HackState7] {
val values: IndexedSeq[HackState7] = findValues
- case object Unk0 extends HackState7(value = 0)
- case object Unk1 extends HackState7(value = 1)
- case object Unk2 extends HackState7(value = 2)
- case object Unk3 extends HackState7(value = 3)
- case object Unk4 extends HackState7(value = 4)
+ case object UnlockDoors extends HackState7(value = 0)
+ case object DisableLatticeBenefits extends HackState7(value = 1)
+ case object NTUDrain extends HackState7(value = 2)
+ case object DisableRadar extends HackState7(value = 3)
+ case object AccessEquipmentTerms extends HackState7(value = 4)
case object Unk5 extends HackState7(value = 5)
case object Unk6 extends HackState7(value = 6)
case object Unk7 extends HackState7(value = 7)
diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala
index dc66a9281..a37b70389 100644
--- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala
+++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala
@@ -265,8 +265,10 @@ class HackCaptureActor extends Actor {
private def HackCompleted(terminal: CaptureTerminal with Hackable, hackedByFaction: PlanetSideEmpire.Value): Unit = {
val building = terminal.Owner.asInstanceOf[Building]
if (building.NtuLevel > 0) {
+ building.virusId = 8
+ building.virusInstalledBy = None
log.info(s"Setting base ${building.GUID} / MapId: ${building.MapId} as owned by $hackedByFaction")
- building.Actor! BuildingActor.SetFaction(hackedByFaction)
+ building.Actor ! BuildingActor.SetFaction(hackedByFaction)
//dispatch to players aligned with the capturing faction within the SOI
val events = building.Zone.LocalEvents
val msg = LocalAction.SendGenericActionMessage(Service.defaultPlayerGUID, GenericAction.FacilityCaptureFanfare)
diff --git a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala
index 6cb3e5bc6..c93ae2696 100644
--- a/src/main/scala/net/psforever/services/local/support/HackClearActor.scala
+++ b/src/main/scala/net/psforever/services/local/support/HackClearActor.scala
@@ -3,7 +3,7 @@ package net.psforever.services.local.support
import java.util.concurrent.TimeUnit
import akka.actor.{Actor, Cancellable}
-import net.psforever.objects.Default
+import net.psforever.objects.{Default, GlobalDefinitions}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.zones.Zone
@@ -30,8 +30,12 @@ class HackClearActor() extends Actor {
def receive: Receive = {
case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, duration, time) =>
val durationMillis = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS)
- hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationMillis)
-
+ val newEntry = HackClearActor.HackEntry(target, zone, unk1, unk2, time, durationMillis)
+ // Remove any existing entry for this GUID + zone in case of virus adding an entry for same target
+ hackedObjects = hackedObjects.filterNot(e => e.target.GUID == target.GUID && e.zone.id == zone.id)
+ hackedObjects = newEntry :: hackedObjects
+ // Sort so they are removed in the correct order
+ hackedObjects = hackedObjects.sortBy(e => e.time + e.duration)
// Restart the timer, in case this is the first object in the hacked objects list
RestartTimer()
@@ -49,6 +53,9 @@ class HackClearActor() extends Actor {
entry.unk1,
entry.unk2
) //call up to the main event system
+ if (entry.target.Definition == GlobalDefinitions.main_terminal) {
+ ClearVirusFromBuilding(entry.target)
+ }
})
RestartTimer()
@@ -93,6 +100,29 @@ class HackClearActor() extends Actor {
}
}
+ /**
+ * When the hack timer expires on a main_terminal, clear the virus from the building and
+ * inform the players in the area
+ * @param target main_terminal object
+ */
+ private def ClearVirusFromBuilding(target: PlanetSideServerObject): Unit = {
+ import net.psforever.objects.serverobject.structures.Building
+ import net.psforever.objects.serverobject.terminals.Terminal
+ import net.psforever.actors.zone.BuildingActor
+ import net.psforever.services.Service
+ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+
+ val building = target.asInstanceOf[Terminal].Owner.asInstanceOf[Building]
+ building.virusId = 8
+ building.virusInstalledBy = None
+ val msg = AvatarAction.GenericObjectAction(Service.defaultPlayerGUID, target.GUID, 60)
+ val events = building.Zone.AvatarEvents
+ building.PlayersInSOI.foreach { player =>
+ events ! AvatarServiceMessage(player.Name, msg)
+ }
+ building.Actor ! BuildingActor.MapUpdate()
+ }
+
/**
* Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
* Separate the original `List` into two:
diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala
index f1463dfda..44228780e 100644
--- a/src/main/scala/net/psforever/zones/Zones.scala
+++ b/src/main/scala/net/psforever/zones/Zones.scala
@@ -156,7 +156,7 @@ object Zones {
"vanu_vehicle_station"
)
private val basicTerminalTypes =
- Seq("order_terminal", "spawn_terminal", "cert_terminal", "order_terminal", "vanu_equipment_term")
+ Seq("order_terminal", "spawn_terminal", "cert_terminal", "order_terminal", "vanu_equipment_term", "main_terminal")
private val spawnPadTerminalTypes = Seq(
"ground_vehicle_terminal",
"air_vehicle_terminal",
diff --git a/src/test/scala/game/BuildingInfoUpdateMessageTest.scala b/src/test/scala/game/BuildingInfoUpdateMessageTest.scala
index 848139214..7731c947a 100644
--- a/src/test/scala/game/BuildingInfoUpdateMessageTest.scala
+++ b/src/test/scala/game/BuildingInfoUpdateMessageTest.scala
@@ -30,8 +30,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
unk4,
unk5,
unk6,
- unk7,
- unk7x,
+ virus_id,
+ virus_installed_by,
boost_spawn_pain,
boost_generator_pain
) =>
@@ -53,8 +53,8 @@ class BuildingInfoUpdateMessageTest extends Specification {
unk4.isEmpty mustEqual true
unk5 mustEqual 0
unk6 mustEqual false
- unk7 mustEqual 8
- unk7x.isEmpty mustEqual true
+ virus_id mustEqual 8
+ virus_installed_by.isEmpty mustEqual true
boost_spawn_pain mustEqual false
boost_generator_pain mustEqual false
case _ =>