diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index 41b224adc..11059fbab 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -165,7 +165,7 @@ object GlobalDefinitionsMiscellaneous { cert_terminal.Geometry = GeometryForm.representByCylinder(radius = 0.66405f, height = 1.09374f) implant_terminal_mech.Name = "implant_terminal_mech" - implant_terminal_mech.MaxHealth = 1500 //TODO 1000; right now, 1000 (mech) + 500 (interface) + implant_terminal_mech.MaxHealth = 1000 implant_terminal_mech.Damageable = true implant_terminal_mech.Repairable = true implant_terminal_mech.autoRepair = AutoRepairStats(1.6f, 5000, 2400, 0.05f) @@ -176,7 +176,7 @@ object GlobalDefinitionsMiscellaneous { implant_terminal_interface.Name = "implant_terminal_interface" implant_terminal_interface.Tab += 0 -> ImplantPage(ImplantTerminalDefinition.implants) implant_terminal_interface.MaxHealth = 500 - implant_terminal_interface.Damageable = false //TODO true + implant_terminal_interface.Damageable = false //true implant_terminal_interface.Repairable = true implant_terminal_interface.autoRepair = AutoRepairStats(1, 5000, 200, 1) implant_terminal_interface.RepairIfDestroyed = true diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala index f12a2e3b5..fc9fda538 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala @@ -1,9 +1,7 @@ package net.psforever.objects.serverobject.terminals.capture import akka.actor.Actor.Receive -import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.structures.Amenity -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.annotation.unused @@ -24,28 +22,7 @@ trait CaptureTerminalAwareBehavior { protected def captureTerminalIsResecured(@unused terminal: CaptureTerminal): Unit = { /* intentionally blank */ } - protected def captureTerminalIsHacked(@unused terminal: CaptureTerminal): Unit = { - // Remove seated occupants for mountables - CaptureTerminalAwareObject match { - case mountable: Mountable => - val guid = mountable.GUID - val zone = mountable.Zone - val zoneId = zone.id - val events = zone.VehicleEvents - mountable.Seats.values.zipWithIndex.foreach { - case (seat, seat_num) => - seat.occupant.collect { - case player => - seat.unmount(player) - player.VehicleSeated = None - if (player.HasGUID) { - events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, unk2=true, guid)) - } - } - } - case _ => () - } - } + protected def captureTerminalIsHacked(@unused terminal: CaptureTerminal): Unit = { /* intentionally blank */ } } object CaptureTerminalAwareBehavior { diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala index 7144d4803..ab06e4981 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala @@ -23,13 +23,13 @@ object CaptureTerminals {import scala.concurrent.duration._ def FinishHackingCaptureConsole(target: CaptureTerminal, hackingPlayer: Player, unk: Long)(): Unit = { import akka.pattern.ask - log.info(s"${hackingPlayer.toString} hacked a ${target.Definition.Name}") // Wait for the target actor to set the HackedBy property import scala.concurrent.ExecutionContext.Implicits.global ask(target.Actor, CommonMessages.Hack(hackingPlayer, target))(timeout = 2 second) .mapTo[CommonMessages.EntityHackState] .onComplete { case Success(_) => + log.info(s"${hackingPlayer.toString} hacked a ${target.Definition.Name}") val zone = target.Zone val zoneid = zone.id val events = zone.LocalEvents diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantInterfaceControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantInterfaceControl.scala new file mode 100644 index 000000000..576b50a03 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantInterfaceControl.scala @@ -0,0 +1,39 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.serverobject.terminals.implant + +import akka.actor.ActorRef +import net.psforever.objects.Player +import net.psforever.objects.serverobject.hackable.{GenericHackables, Hackable} +import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl} +import net.psforever.objects.zones.Zone +import net.psforever.types.PlanetSideGUID + +object ImplantInterfaceControl { + private def FindPairedTerminalMech( + zone: Zone, + interfaceGuid: PlanetSideGUID + ): Option[Amenity with Hackable] = { + zone + .map + .terminalToInterface + .find { case (_, guid) => guid == interfaceGuid.guid } + .flatMap { case (mechGuid, _) => zone.GUID(mechGuid) } + .collect { case mech: ImplantTerminalMech if !mech.Destroyed && mech.HackedBy.isEmpty => mech } + } +} + +class ImplantInterfaceControl(private val terminal: Terminal) + extends TerminalControl(terminal) { + + override def performHack(player: Player, data: Option[Any], replyTo: ActorRef): Unit = { + HackableObject.HackedBy + .orElse { + super.performHack(player, data, replyTo) + ImplantInterfaceControl + .FindPairedTerminalMech(terminal.Zone, terminal.GUID) + .foreach(GenericHackables.FinishHacking(_, player, unk = 3212836864L)()) + None + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalInterface.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalInterface.scala new file mode 100644 index 000000000..5d4b18aff --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalInterface.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.serverobject.terminals.implant + +import akka.actor.ActorContext +import net.psforever.objects.serverobject.terminals.{Terminal, TerminalDefinition} +import net.psforever.types.Vector3 + +object ImplantTerminalInterface { + /** + * Instantiate and configure a `Terminal` object + * @param pdef `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @param pos position + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality + * @return the `Terminal` object + */ + def Constructor(pos: Vector3, pdef: TerminalDefinition)(id: Int, context: ActorContext): Terminal = { + import akka.actor.Props + + val obj = Terminal(pdef) + obj.Position = pos + obj.Actor = context.actorOf(Props(classOf[ImplantInterfaceControl], obj), s"${obj.Definition.Name}_$id") + obj + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala index f4f53b662..f2762ef6c 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMechControl.scala @@ -1,25 +1,46 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.terminals.implant +import akka.actor.ActorRef import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity, DamageableMountable} -import net.psforever.objects.serverobject.hackable.{GenericHackables, HackableBehavior} +import net.psforever.objects.serverobject.hackable.{GenericHackables, Hackable, HackableBehavior} import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableAmenity, RepairableEntity} -import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} -import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior +import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl} +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAwareBehavior} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.zones.Zone import net.psforever.objects.{GlobalDefinitions, Player, SimpleItem} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} + +import scala.annotation.unused + +object ImplantTerminalMechControl { + private def FindPairedTerminalInterface( + zone: Zone, + mechGuid: PlanetSideGUID + ): Option[Amenity with Hackable] = { + zone + .map + .terminalToInterface + .find { case (guid, _) => guid == mechGuid.guid } + .flatMap { case (_, interfaceGuid) => zone.GUID(interfaceGuid) } + .collect { case terminal: Terminal if !terminal.Destroyed && terminal.HackedBy.isEmpty => terminal } + } +} /** - * An `Actor` that handles messages being dispatched to a specific `ImplantTerminalMech`. - * @param mech the "mech" object being governed - */ + * An `Actor` that handles messages being dispatched to a specific `ImplantTerminalMech`. + * @param mech the "mech" object being governed + */ class ImplantTerminalMechControl(mech: ImplantTerminalMech) - extends PoweredAmenityControl + extends PoweredAmenityControl with FactionAffinityBehavior.Check with MountableBehavior with HackableBehavior.GenericHackable @@ -41,9 +62,11 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) .orElse(autoRepairBehavior) + .orElse(captureTerminalAwareBehaviour) def poweredStateLogic : Receive = commonBehavior + .orElse(hackableBehavior) .orElse(mountBehavior) .orElse { case CommonMessages.Use(player, Some(item: SimpleItem)) @@ -56,15 +79,16 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) GenericHackables.FinishHacking(mech, player, 3212836864L), GenericHackables.HackingTickAction(progressType = 1, player, mech, item.GUID) ) - case _ => ; + case _ => () } - case _ => ; + case _ => () } def unpoweredStateLogic: Receive = commonBehavior + .orElse(hackableBehavior) .orElse { - case _ => ; + case _ => () } override protected def mountTest( @@ -120,14 +144,11 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) val zoneId = zone.id val events = zone.VehicleEvents mech.Seats.values.foreach(seat => - seat.occupant match { - case Some(player) => + seat.occupant.collect { + case player => seat.unmount(player) player.VehicleSeated = None - if (player.HasGUID) { - events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2 = false, guid)) - } - case None => ; + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid)) } ) } @@ -140,4 +161,123 @@ class ImplantTerminalMechControl(mech: ImplantTerminalMech) super.Restoration(obj) RepairableAmenity.RestorationOfHistory(obj) } + + override def performHack(player: Player, data: Option[Any], replyTo: ActorRef): Unit = { + //todo don't now how to properly hack this amenity + super.performHack(player, data, replyTo) + val zone = HackableObject.Zone + val guid = HackableObject.GUID + val localFaction = mech.Faction + val events = zone.LocalEvents + if (player.Faction == localFaction) { + if (mech.Owner.asInstanceOf[Building].CaptureTerminalIsHacked) { + //this is actually futile, as a hacked base does not grant access to the terminal + events ! LocalServiceMessage(localFaction.toString, LocalAction.SetEmpire(guid, localFaction)) + } + kickAllOccupantsNotOfFaction(zone, guid, mech, localFaction) + } else { + opposingFactionsMayAccess(zone, guid, localFaction) + kickAllOccupantsOfFaction(zone, guid, mech, localFaction) + } + ImplantTerminalMechControl + .FindPairedTerminalInterface(zone, guid) + .foreach(GenericHackables.FinishHacking(_, player, unk = 3212836864L)()) + } + + override def performClearHack(data: Option[Any], replyTo: ActorRef): Unit = { + //todo don't now how to properly unhack this amenity + HackableObject.HackedBy.collect { _ => + super.performClearHack(data, replyTo) + val toFaction = HackableObject.Faction + val zone = HackableObject.Zone + val guid = HackableObject.GUID + noAccessByOpposingFactions(zone, guid, toFaction) + kickAllOccupantsNotOfFaction(zone, guid, mech, toFaction) + } + } + + override protected def captureTerminalIsHacked(@unused terminal: CaptureTerminal): Unit = { + //todo don't now how to properly handle a hacked mech + super.captureTerminalIsHacked(terminal) + val zone = HackableObject.Zone + val guid = HackableObject.GUID + kickAllOccupantsNotOfFactionWithTest(zone, guid, mech, (a: PlanetSideEmpire.Value) => { true }) + } + + override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = { + //todo don't now how to properly handle a hacked mech + super.captureTerminalIsResecured(terminal) + //if hacked, correct + val zone = HackableObject.Zone + val guid = HackableObject.GUID + val toFaction = HackableObject.Faction + HackableObject.HackedBy.collect { + case hackInfo if hackInfo.hackerFaction != toFaction => + opposingFactionsMayAccess(zone, guid, toFaction) + } + } + + private def opposingFactionsMayAccess( + zone: Zone, + guid: PlanetSideGUID, + setToFaction: PlanetSideEmpire.Value + ): Unit = { + val events = zone.LocalEvents + opposingFactionsAre(setToFaction).foreach { faction => + events ! LocalServiceMessage(faction.toString, LocalAction.SetEmpire(guid, faction)) + } + } + + private def noAccessByOpposingFactions( + zone: Zone, + guid: PlanetSideGUID, + setToFaction: PlanetSideEmpire.Value + ): Unit = { + val events = zone.LocalEvents + opposingFactionsAre(setToFaction).foreach { faction => + events ! LocalServiceMessage(faction.toString, LocalAction.SetEmpire(guid, setToFaction)) + } + } + + private def opposingFactionsAre(faction: PlanetSideEmpire.Value): PlanetSideEmpire.ValueSet = { + PlanetSideEmpire + .values + .filterNot { f => f == PlanetSideEmpire.NEUTRAL && f == faction } + } + + private def kickAllOccupantsOfFaction( + zone: Zone, + guid: PlanetSideGUID, + obj: Mountable, + isFaction: PlanetSideEmpire.Value + ): Unit = { + kickAllOccupantsNotOfFactionWithTest(zone, guid, obj, (a: PlanetSideEmpire.Value) => { a == isFaction }) + } + + private def kickAllOccupantsNotOfFaction( + zone: Zone, + guid: PlanetSideGUID, + obj: Mountable, + isFaction: PlanetSideEmpire.Value + ): Unit = { + kickAllOccupantsNotOfFactionWithTest(zone, guid, obj, (a: PlanetSideEmpire.Value) => { a != isFaction }) + } + + private def kickAllOccupantsNotOfFactionWithTest( + zone: Zone, + guid: PlanetSideGUID, + obj: Mountable, + test: PlanetSideEmpire.Value => Boolean + ): Unit = { + val zoneId = zone.id + val events = zone.LocalEvents + obj.Seats.values.foreach(seat => + seat.occupant.collect { + case player if test(player.Faction) => + seat.unmount(player) + player.VehicleSeated = None + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2 = false, guid)) + } + ) + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala index 09e0973f1..6ecddd0e8 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -330,6 +330,21 @@ class FacilityTurretControl(turret: FacilityTurret) } override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = { + super.captureTerminalIsHacked(terminal) + // Remove seated occupants + val guid = turret.GUID + val zone = turret.Zone + val zoneId = zone.id + val events = zone.VehicleEvents + turret.Seats.values.zipWithIndex.foreach { + case (seat, seat_num) => + seat.occupant.collect { + case player => + seat.unmount(player) + player.VehicleSeated = None + events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, unk2=true, guid)) + } + } captureTerminalChanges(terminal, super.captureTerminalIsHacked, actionDelays = 3000L) } diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index ba5db7aea..6f9694688 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -21,7 +21,7 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad import net.psforever.objects.serverobject.structures.{Building, BuildingDefinition, FoundationBuilder, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalDefinition} -import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech +import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalInterface, ImplantTerminalMech} import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition, VanuSentry} import net.psforever.objects.serverobject.zipline.ZipLinePath @@ -637,7 +637,7 @@ object Zones { zoneMap.addLocalObject( closestTerminal.guid, - Terminal.Constructor(closestTerminal.position, GlobalDefinitions.implant_terminal_interface), + ImplantTerminalInterface.Constructor(closestTerminal.position, GlobalDefinitions.implant_terminal_interface), owningBuildingGuid = ownerGuid )