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 _ =>