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
This commit is contained in:
Fate-JH 2025-07-31 01:13:32 -04:00
parent b8a47016da
commit 8e2732681c
9 changed files with 234 additions and 6 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 _ => ;
}

View file

@ -138,4 +138,5 @@ object LocalAction {
mountable_guid: PlanetSideGUID,
weapon_guid: PlanetSideGUID
) extends Action
final case class ForceZoneChange(zone: Zone) extends Action
}

View file

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