diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index 61aae9c2..bc6e96e2 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -7,9 +7,11 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.adapter._ import net.psforever.actors.zone.ZoneActor import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.zones.ZoneInfo import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import scala.collection.mutable +import scala.annotation.unused +import scala.collection.{Seq, mutable} import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ // @@ -204,7 +206,8 @@ class ChatActor( Behaviors .receiveMessagePartial[Command] { case SetSession(newSession) => - active(newSession, chatService,cluster) + this.session = Some(newSession) + active(newSession, chatService, cluster) case JoinChannel(channel) => chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel) @@ -398,133 +401,114 @@ class ChatActor( case (U_CMT_ZONEROTATE, _, contents) if gmCommandAllowed => cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation) - /** Messages starting with ! are custom chat commands */ - case (_, _, contents) if contents.startsWith("!") && - customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ; - case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed => - val args = contents.split(" ").filter(_ != "") - val (faction, factionPos): (PlanetSideEmpire.Value, Option[Int]) = args.zipWithIndex - .map { case (factionName, pos) => (factionName.toLowerCase, pos) } - .flatMap { - case ("tr", pos) => Some(PlanetSideEmpire.TR, pos) - case ("nc", pos) => Some(PlanetSideEmpire.NC, pos) - case ("vs", pos) => Some(PlanetSideEmpire.VS, pos) - case ("none", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case ("bo", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case ("neutral", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case _ => None - } - .headOption match { - case Some((isFaction, pos)) => (isFaction, Some(pos)) - case None => (session.player.Faction, None) - } - val (buildingsOption, buildingPos): (Option[Seq[Building]], Option[Int]) = args.zipWithIndex.flatMap { - case (_, pos) if factionPos.isDefined && factionPos.get == pos => None - case ("all", pos) => - Some( - Some( - session.zone.Buildings - .filter { - case (_, building) => building.CaptureTerminal.isDefined - } - .values - .toSeq - ), - Some(pos) - ) - case (name: String, pos) => - session.zone.Buildings.find { - case (_, building) => name.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined - } match { - case Some((_, building)) => Some(Some(Seq(building)), Some(pos)) - case None => - try { - // check if we have a timer - name.toInt + val buffer = contents.split(" ").filterNot(_ == "").take(3) + //walk through the param buffer + val (foundFacilities, foundFacilitiesTag, factionBuffer) = firstParam(session, buffer, captureBaseParamFacilities) + val (foundFaction, foundFactionTag, timerBuffer) = firstParam(session, factionBuffer, captureBaseParamFaction) + val (foundTimer, foundTimerTag, _) = firstParam(session, timerBuffer, captureBaseParamTimer) + //resolve issues with the initial params + var facilityError: Int = 0 + var factionError: Boolean = false + var timerError: Boolean = false + var usageMessage: Boolean = false + val resolvedFacilities = foundFacilities + .orElse { + if (foundFacilitiesTag.nonEmpty) { + if (foundFaction.isEmpty) { + /* /capturebase OR /capturebase */ + //malformed facility tag error + facilityError = 2 + None + } else if (!foundFacilitiesTag.contains("curr")) { //did we do this next check already + /* /capturebase , potentially */ + val buildings = captureBaseCurrSoi(session) + if (buildings.nonEmpty) { + //convert facilities to faction + Some(buildings.toSeq) + } else { + //no facilities error + facilityError = 1 None - } catch { - case _: Throwable => - Some(None, Some(pos)) } - } - }.headOption match { - case Some((buildings, pos)) => (buildings, pos) - case None => (None, None) - } - val (timerOption, timerPos): (Option[Int], Option[Int]) = args.zipWithIndex.flatMap { - case (_, pos) - if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos => - None - case (timer: String, pos) => - try { - val t = timer.toInt // TODO what is the timer format supposed to be? - Some(Some(t), Some(pos)) - } catch { - case _: Throwable => - Some(None, Some(pos)) - } - }.headOption match { - case Some((timer, posOption)) => (timer, posOption) - case None => (None, None) - } - - (factionPos, buildingPos, timerPos, buildingsOption, timerOption) match { - case // [[|none []] - (Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) | - (None, None, None, None, None) | - // [ [|none [timer]]] - (None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) | - // [all [|none]] - (Some(1) | None, Some(0), None, Some(_), None) => - val buildings: Seq[Building] = buildingsOption.getOrElse( - session.zone.Buildings.values.filter { building => - building.PlayersInSOI.exists { soiPlayer => - session.player.CharId == soiPlayer.CharId - } - }.toSeq - ) - buildings foreach { building => - // TODO implement timer - - val terminal = building.CaptureTerminal.get - - building.Actor ! BuildingActor.SetFaction(faction) - building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false)) - - // clear any previous hack via "resecure" - if (building.CaptureTerminalIsHacked) { - building.Zone.LocalEvents ! LocalServiceMessage(terminal.Zone.id,LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)) + } else { + //no facilities error + facilityError = 1 + None } - - // push any updates this might cause to clients - building.Zone.actor ! ZoneActor.ZoneMapUpdate() + } else { + //no params; post command usage reminder + usageMessage = true + None } - - case (_, Some(0), _, None, _) => - sessionActor ! SessionActor.SendResponse( - ChatMsg( - UNK_229, - wideContents=true, - "", - s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.", + } + val resolvedFaction = foundFaction + .orElse { + if (resolvedFacilities.nonEmpty) { + /* /capturebase OR /capturebase */ + if (foundFactionTag.isEmpty || foundTimer.nonEmpty) { + //convert facilities to OUR PLAYER'S faction + Some(session.player.Faction) + } else { + //malformed faction tag error + factionError = true None - ) - ) - case (Some(0), _, Some(1), _, None) | (Some(1), Some(0), Some(2), _, None) => - sessionActor ! SessionActor.SendResponse( - ChatMsg( - UNK_229, - wideContents=true, - "", - s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.", - None - ) - ) + } + } else { + //incorrect params; already posted an error message + None + } + } + val resolvedTimer = foundTimer + .orElse { + //todo stop command execution? post command usage reminder? + if (resolvedFaction.nonEmpty && foundTimerTag.nonEmpty) { + /* /capturebase */ + //malformed timer tag error + timerError = true + None + } else { + //eh + Some(1) + } + } + //evaluate results + (resolvedFacilities, resolvedFaction, resolvedTimer) match { + case (Some(buildings), Some(faction), Some(_)) => + buildings.foreach { building => + //TODO implement timer + val terminal = building.CaptureTerminal.get + val zone = building.Zone + val zoneActor = zone.actor + val buildingActor = building.Actor + //clear any previous hack + if (building.CaptureTerminalIsHacked) { + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody) + ) + } + //push any updates this might cause + zoneActor ! ZoneActor.ZoneMapUpdate() + //convert faction affiliation + buildingActor ! BuildingActor.SetFaction(faction) + buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) + //push for map updates again + zoneActor ! ZoneActor.ZoneMapUpdate() + } case _ => - sessionActor ! SessionActor.SendResponse( - message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") - ) + if (usageMessage) { + sessionActor ! SessionActor.SendResponse( + message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") + ) + } else { + val msg = if (facilityError == 1) { "can not contextually determine building target" } + else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" } + else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" } + else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" } + else { "malformed params; check usage" } + sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None)) + } } case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) @@ -549,26 +533,6 @@ class ChatActor( ChatChannel.Default() ) - case (_, "tr", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0) - ) - - case (_, "nc", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0) - ) - - case (_, "vs", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0) - ) - - case (_, "bo", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt) - ) - case (CMT_OPEN, _, _) if !session.player.silenced => chatService ! ChatService.Message( session, @@ -915,6 +879,30 @@ class ChatActor( ) } + case (_, "tr", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0) + ) + + case (_, "nc", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0) + ) + + case (_, "vs", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0) + ) + + case (_, "bo", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt) + ) + + /** Messages starting with ! are custom chat commands */ + case (_, _, contents) if contents.startsWith("!") && + customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ; + case _ => log.warn(s"Unhandled chat message $message") } @@ -1386,4 +1374,83 @@ class ChatActor( false } } + + private def captureBaseParamFacilities(session: Session, token: Option[String]): Option[Seq[Building]] = { + token.collect { + case "curr" => + val list = captureBaseCurrSoi(session) + if (list.nonEmpty) { + Some(list.toSeq) + } else { + None + } + case "all" => + val list = session.zone.Buildings.values.filter(_.CaptureTerminal.isDefined) + if (list.nonEmpty) { + Some(list.toSeq) + } else { + None + } + case name => + val trueName = ZoneInfo + .values + .find(_.id.equals(session.zone.id)) + .flatMap { info => + info.aliases + .facilities + .collectFirst { case (key, internalName) if key.equalsIgnoreCase(name) => internalName } + } + .getOrElse(name) + session.zone.Buildings + .values + .find { + building => trueName.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined + } + .map(b => Seq(b)) + } + .flatten + } + + private def captureBaseCurrSoi(session: Session): Iterable[Building] = { + val charId = session.player.CharId + session.zone.Buildings.values.filter { building => + building.PlayersInSOI.exists(_.CharId == charId) + } + } + + private def captureBaseParamFaction(@unused session: Session, token: Option[String]): Option[PlanetSideEmpire.Value] = { + token.collect { + case faction => + faction.toLowerCase() match { + case "tr" => Some(PlanetSideEmpire.TR) + case "nc" => Some(PlanetSideEmpire.NC) + case "vs" => Some(PlanetSideEmpire.VS) + case "none" => Some(PlanetSideEmpire.NEUTRAL) + case "bo" => Some(PlanetSideEmpire.NEUTRAL) + case "neutral" => Some(PlanetSideEmpire.NEUTRAL) + case _ => None + } + }.flatten + } + + private def captureBaseParamTimer(@unused session: Session, token: Option[String]): Option[Int] = { + token.collect { + case n if n.forall(Character.isDigit) => n.toInt + } + } + + private def firstParam[T]( + session: Session, + buffer: Array[String], + func: (Session, Option[String])=>Option[T] + ): (Option[T], Option[String], Array[String]) = { + val tokenOpt = buffer.headOption + val valueOpt = func(session, tokenOpt) + val outBuffer = if (valueOpt.nonEmpty) { + buffer.drop(1) + } else { + buffer + } + (valueOpt, tokenOpt, outBuffer) + } } 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 17731a01..7c49124c 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -656,7 +656,6 @@ class ZoningOperations( spawn.handleNewPlayerLoaded(player) } else { //alive but doesn't have a GUID; probably logging in? - session = session.copy(zone = Zone.Nowhere) context.self ! ICS.ZoneResponse(Some(player.Zone)) } } else { diff --git a/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala b/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala index 54cb1c9f..f9e289d5 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala @@ -2,15 +2,21 @@ package net.psforever.objects.zones import enumeratum.values.{IntEnum, IntEnumEntry} +final case class AliasLookup( + zone: Seq[String] = Seq(), + facilities: Map[String, String] = Map() + ) + sealed abstract class ZoneInfo( val value: Int, val name: String, val id: String, val map: MapInfo, - val aliases: Seq[String] = Seq() + val aliases: AliasLookup = ZoneInfo.defaultAliases, ) extends IntEnumEntry {} case object ZoneInfo extends IntEnum[ZoneInfo] { + private val defaultAliases = AliasLookup(Nil, Map.empty[String, String]) case object Solsar extends ZoneInfo( @@ -98,7 +104,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { name = "NC Sanctuary", id = "home1", map = MapInfo.Map11, - aliases = Seq("nc-sanctuary") + aliases = AliasLookup(zone = Seq("nc-sanctuary")) ) case object TrSanctuary @@ -107,7 +113,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { name = "TR Sanctuary", id = "home2", map = MapInfo.Map12, - aliases = Seq("tr-sanctuary") + aliases = AliasLookup(zone = Seq("tr-sanctuary")) ) case object VsSanctuary @@ -116,7 +122,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { name = "VS Sanctuary", id = "home3", map = MapInfo.Map13, - aliases = Seq("vs-sanctuary") + aliases = AliasLookup(zone = Seq("vs-sanctuary")) ) case object tzshtr @@ -124,7 +130,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 14, name = "tzshtr", id = "tzshtr", - map = MapInfo.Map14 + map = MapInfo.Map14, + aliases = AliasLookup(zone = Seq("tr-shooting")) ) case object tzdrtr @@ -132,7 +139,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 15, name = "tzdrtr", id = "tzdrtr", - map = MapInfo.Map15 + map = MapInfo.Map15, + aliases = AliasLookup(zone = Seq("tr-driving")) ) case object tzcotr @@ -148,7 +156,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 17, name = "tzshnc", id = "tzshnc", - map = MapInfo.Map14 + map = MapInfo.Map14, + aliases = AliasLookup(zone = Seq("nc-shooting")) ) case object tzdrnc @@ -156,7 +165,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 18, name = "tzdrnc", id = "tzdrnc", - map = MapInfo.Map15 + map = MapInfo.Map15, + aliases = AliasLookup(zone = Seq("nc-driving")) ) case object tzconc @@ -172,7 +182,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 20, name = "tzshvs", id = "tzshvs", - map = MapInfo.Map14 + map = MapInfo.Map14, + aliases = AliasLookup(zone = Seq("vs-shooting")) ) case object tzdrvs @@ -180,7 +191,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 21, name = "tzdrvs", id = "tzdrvs", - map = MapInfo.Map15 + map = MapInfo.Map15, + aliases = AliasLookup(zone = Seq("vs-driving")) ) case object tzcovs @@ -244,7 +256,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 29, name = "Extinction", id = "i1", - map = MapInfo.Map99 + map = MapInfo.Map99, + aliases = AliasLookup(facilities = Map( + ("Mithra", "Blue_Base"), + ("Yazata", "Red_Base"), + ("Hvar", "Indigo_Base") + )) ) case object Ascension @@ -252,7 +269,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 30, name = "Ascension", id = "i2", - map = MapInfo.Map98 + map = MapInfo.Map98, + aliases = AliasLookup(facilities = Map( + ("Zal", "Base_Alpha"), + ("Rashnu", "Base_Bravo"), + ("Sraosha", "Base_Charlie") + )) ) case object Desolation @@ -260,7 +282,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 31, name = "Desolation", id = "i3", - map = MapInfo.Map97 + map = MapInfo.Map97, + aliases = AliasLookup(facilities = Map( + ("Dahaka", "Red_Base_97"), + ("Jamshid", "Blue_Base_97"), + ("Izha", "Indigo_Base_97") + )) ) case object Nexus @@ -268,16 +295,20 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 32, name = "Nexus", id = "i4", - map = MapInfo.Map96 + map = MapInfo.Map96, + aliases = AliasLookup(facilities = Map( + ("Atar", "Nexus_base") + )) ) val values: IndexedSeq[ZoneInfo] = findValues def findName(name: String): ZoneInfo = findNameOpt(name).get - def findNameOpt(name: String): Option[ZoneInfo] = + def findNameOpt(name: String): Option[ZoneInfo] = { + val lowerName = name.toLowerCase() values.find(v => - v.name.toLowerCase() == name.toLowerCase() || v.aliases.map(_.toLowerCase()).contains(name.toLowerCase()) + v.name.toLowerCase().equals(lowerName) || v.aliases.zone.map(_.toLowerCase()).contains(lowerName) ) - + } }