From 8e2732681c53687d3d080aa25081c85ee96462db Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:13:32 -0400 Subject: [PATCH] No Safe Spaces (#1283) * local zone maintains information about weapon fire capability per faction * map reload by faction to represent a change in weapons fire permissions via LMM --- .../actors/session/csr/ChatLogic.scala | 130 ++++++++++++++++++ .../session/normal/LocalHandlerLogic.scala | 23 +++- .../actors/session/spectator/ChatLogic.scala | 2 +- .../session/support/ChatOperations.scala | 24 +++- .../session/support/ZoningOperations.scala | 4 +- .../net/psforever/objects/zones/Zone.scala | 46 +++++++ .../services/local/LocalService.scala | 8 ++ .../services/local/LocalServiceMessage.scala | 1 + .../services/local/LocalServiceResponse.scala | 2 + 9 files changed, 234 insertions(+), 6 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala index 24c5c885..f90de34d 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -18,6 +18,7 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.chat.{ChatChannel, DefaultChannel, SpectatorChannel} import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} import net.psforever.types.{ChatMessageType, PlanetSideEmpire} +import net.psforever.zones.Zones import scala.util.Success @@ -225,6 +226,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "hidespectators" => customCommandHideSpectators() case "sayspectator" => customCommandSpeakAsSpectator(params, message) case "setempire" => customCommandSetEmpire(params) + case "weaponlock" => customCommandZoneWeaponUnlock(session, params) case _ => // command was not handled sendResponse( @@ -411,6 +413,134 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } } + def customCommandZoneWeaponUnlock(session: Session, params: Seq[String]): Boolean = { + val usageMessage: Boolean = params.exists(_.matches("--help")) || params.exists(_.matches("-h")) + val formattedParams = ops.cliCommaSeparatedParams(params) + //handle params + val (zoneList, verifiedZones, factionList, verifiedFactions, stateOpt) = (formattedParams.headOption, formattedParams.lift(1), formattedParams.lift(2)) match { + case _ if usageMessage => + (Nil, Nil, Nil, Nil, None) + + case (None, None, None) => + ( + Seq(session.zone.id), + Seq(session.zone), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + Some(true) + ) + + case (Some(zoneOrFaction), Some(factionOrZone), stateOpt) => + val factionOrZoneSplit = factionOrZone.split(",").toSeq + val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq + val tryToFactions = factionOrZoneSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))) + if (tryToFactions.isEmpty) { + ( + factionOrZoneSplit, + customCommandZoneParse(factionOrZoneSplit), + zoneOrFactionSplit, + zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))), + customCommandOnOffStateOrNone(stateOpt) + ) + } else { + ( + zoneOrFactionSplit, + customCommandZoneParse(zoneOrFactionSplit), + factionOrZoneSplit, + tryToFactions, + customCommandOnOffStateOrNone(stateOpt) + ) + } + + case (Some(zoneOrFaction), stateOpt, None) => + val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq + val tryToFactions = zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))) + if (tryToFactions.isEmpty) { + ( + zoneOrFactionSplit, + customCommandZoneParse(zoneOrFactionSplit), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + customCommandOnOffStateOrNone(stateOpt) + ) + } else { + ( + Seq(session.zone.id), + Seq(session.zone), + zoneOrFactionSplit, + tryToFactions, + customCommandOnOffStateOrNone(stateOpt) + ) + } + + case (stateOpt, None, None) => + ( + Seq(session.zone.id), + Seq(session.zone), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + customCommandOnOffState(stateOpt) + ) + } + //resolve + if (usageMessage) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "!weaponlock [zone[,...]] [faction[,...]] [o[n]|of[f]]")) + } else if (zoneList.isEmpty || verifiedZones.isEmpty || zoneList.size != verifiedZones.size) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "some zones can not be verified")) + } else if (factionList.isEmpty || verifiedFactions.isEmpty || factionList.size != verifiedFactions.size) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "some factions can not be verified")) + } else if (stateOpt.isEmpty) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "state must be on or off")) + } else { + val state = !stateOpt.get + verifiedZones.foreach { zone => + val events = zone.AvatarEvents + val zoneId = zone.id + //val reloadZoneMsg = AvatarAction.ReloadZone(zone) + zone + .UpdateLiveFireAllowed(state, verifiedFactions) + .foreach { + case (_, false, _) => () + case (faction, true, _) => + //events ! AvatarServiceMessage(s"$faction", reloadZoneMsg) + } + } + } + true + } + + private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = { + stateOpt match { + case None => + Some(true) + case _ => + customCommandOnOffState(stateOpt) + } + } + + private def customCommandOnOffState(stateOpt: Option[String]): Option[Boolean] = { + stateOpt match { + case Some("o") | Some("on") => + Some(false) + case Some("of") | Some("off") => + Some(true) + case _ => + None + } + } + + def customCommandZoneParse(potentialZones: Seq[String]): Seq[Zone] = { + potentialZones.flatten { potentialZone => + if (potentialZone.toIntOption.nonEmpty) { + val xInt = potentialZone.toInt + Zones.zones.find(_.Number == xInt) + } else { + Zones.zones.find(z => z.id.equals(potentialZone)) + } + } + } + + override def stop(): Unit = { super.stop() seeSpectatorsIn.foreach(_ => customCommandHideSpectators()) diff --git a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala index 3b7538a5..1b176474 100644 --- a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala @@ -8,9 +8,9 @@ import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.vehicles.MountableWeapons import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable} import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} -import net.psforever.services.Service +import net.psforever.services.{InterstellarClusterService, Service} import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SpawnGroup} object LocalHandlerLogic { def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { @@ -240,6 +240,25 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) } + case LocalResponse.ForceZoneChange(zone) => + //todo we might be able to piggyback this for squad recalls later + if(session.zone eq zone) { + sessionLogic.zoning.zoneReload = true + zone.AvatarEvents ! Service.Leave() + zone.LocalEvents ! Service.Leave() + zone.VehicleEvents ! Service.Leave() + zone.AvatarEvents ! Service.Join(player.Name) //must manually restore this subscriptions + sessionLogic.zoning.spawn.handleNewPlayerLoaded(player) //will restart subscriptions and dispatch a LoadMapMessage + } else { + import akka.actor.typed.scaladsl.adapter._ + sessionLogic.cluster ! InterstellarClusterService.GetRandomSpawnPoint( + zone.Number, + player.Faction, + Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS), + context.self + ) + } + case _ => () } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala index ae285706..c17b2fdc 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -140,7 +140,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case _ => ("", Seq("")) } command match { - case "list" => ops.customCommandList(session, params, message) + case "list" => ops.customCommandList(session, params.toSeq, message) case "nearby" => ops.customCommandNearby(session) case "loc" => ops.customCommandLoc(session, message) case _ => diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 5aebd100..48a724db 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -1046,7 +1046,7 @@ class ChatOperations( } } - private def captureBaseParamFaction( + def captureBaseParamFaction( @unused session: Session, token: Option[String] ): Option[PlanetSideEmpire.Value] = { @@ -1360,6 +1360,28 @@ class ChatOperations( str.replaceAll("\\s+", " ").trim.split("\\s").toList.filter(!_.equals("")) } + def cliCommaSeparatedParams(params: Seq[String]): Seq[String] = { + var len = 0 + var appendNext = false + var formattedParams: Seq[String] = Seq() + params.foreach { + case "," => + appendNext = true + case param if appendNext || param.startsWith(",") => + formattedParams = formattedParams.slice(0, len - 1) :+ formattedParams(len - 1) + "," + param.replaceAll(",", "") + appendNext = param.endsWith(",") + case param if param.endsWith(",") => + formattedParams = formattedParams :+ param.take(param.length-1) + len += 1 + appendNext = true + case param => + formattedParams = formattedParams :+ param + len += 1 + appendNext = false + } + formattedParams + } + def commandIncomingSend(message: ChatMsg): Unit = { sendResponse(message) } 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 ae62987a..68b5d140 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2187,8 +2187,8 @@ class ZoningOperations( tplayer.avatar = avatar session = session.copy(player = tplayer) //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar - val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) - sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum)) + //val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) + sendResponse(LoadMapMessage(mapName, id, 40100, 25, zone.LiveFireAllowed(tplayer.Faction), map.checksum)) if (isAcceptableNextSpawnPoint) { //important! the LoadMapMessage must be processed by the client before the avatar is created player.allowInteraction = true diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index cbf828d0..4deac170 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -182,6 +182,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { */ private var vehicleEvents: ActorRef = Default.Actor + /** + * Is any player permitted to engage in weapons discharge in this zone? + */ + private var liveFireAllowed: mutable.HashMap[PlanetSideEmpire.Value, Boolean] = + mutable.HashMap.from(PlanetSideEmpire.values.map { f => (f, true) }) + /** * When the zone has completed initializing, fulfill this promise. * @see `init(ActorContext)` @@ -593,6 +599,46 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { vehicleEvents = bus VehicleEvents } + + def LiveFireAllowed(): Boolean = liveFireAllowed.exists { case (_, v) => v } + + def LiveFireAllowed(faction: PlanetSideEmpire.Value): Boolean = liveFireAllowed.getOrElse(faction, false) + + def UpdateLiveFireAllowed(state: Boolean): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + (f, v == state, state) + } + output.foreach { case (f, _, v) => + liveFireAllowed.update(f, v) + } + output.toList + } + + def UpdateLiveFireAllowed(state: Boolean, faction: PlanetSideEmpire.Value): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + if (f == faction) { + (f, v == state, state) + } else { + (f, false, v) + } + } + liveFireAllowed.update(faction, state) + output.toList + } + + def UpdateLiveFireAllowed(state: Boolean, factions: Seq[PlanetSideEmpire.Value]): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + if (factions.contains(f)) { + (f, v == state, state) + } else { + (f, false, v) + } + } + factions.foreach { f => + liveFireAllowed.update(f, state) + } + output.toList + } } /** diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 91d900d3..751dce97 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -306,6 +306,14 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) ) ) + case LocalAction.ForceZoneChange(zone) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ForceZoneChange(zone) + ) + ) case _ => ; } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index de4545cc..ced62945 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -138,4 +138,5 @@ object LocalAction { mountable_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID ) extends Action + final case class ForceZoneChange(zone: Zone) extends Action } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index ffb1bb3f..8834ab19 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -6,6 +6,7 @@ import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle} import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.vehicles.Utility +import net.psforever.objects.zones.Zone import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.PlanetSideGamePacket @@ -86,4 +87,5 @@ object LocalResponse { final case class TriggerSound(sound: TriggeredSound.Value, pos: Vector3, unk: Int, volume: Float) extends Response final case class UpdateForceDomeStatus(building_guid: PlanetSideGUID, activated: Boolean) extends Response final case class RechargeVehicleWeapon(mountable_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Response + final case class ForceZoneChange(zone: Zone) extends Response }