WIP: Custom commands handling

This commit is contained in:
Resaec 2024-03-25 02:51:26 +01:00
parent 6bb002e2e2
commit 75d7877f8a

View file

@ -224,6 +224,10 @@ class ChatActor(
session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType) session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType)
(message.messageType, message.recipient.trim, message.contents.trim) match { (message.messageType, message.recipient.trim, message.contents.trim) match {
/** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ;
case (CMT_FLY, recipient, contents) if gmCommandAllowed => case (CMT_FLY, recipient, contents) if gmCommandAllowed =>
val flying = contents match { val flying = contents match {
case "on" => true case "on" => true
@ -899,10 +903,6 @@ class ChatActor(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt) 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 _ => case _ =>
log.warn(s"Unhandled chat message $message") log.warn(s"Unhandled chat message $message")
} }
@ -1107,212 +1107,379 @@ class ChatActor(
} }
} }
def customCommandMessages( private def customCommandMessages(
message: ChatMsg, message: ChatMsg,
session: Session, session: Session,
chatService: ActorRef[ChatService.Command], chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command], cluster: ActorRef[InterstellarClusterService.Command],
gmCommandAllowed: Boolean isGM: Boolean
): Boolean = { ): Boolean = {
// val messageType = message.messageType
// val recipient = message.recipient var content = message.contents
val contents = message.contents
if (contents.startsWith("!")) { // stop processing if content is not a custom command
if (contents.startsWith("!whitetext ") && gmCommandAllowed) { if (!content.startsWith("!")) {
chatService ! ChatService.Message( return false
session, }
ChatMsg(UNK_227, wideContents=true, "", contents.replace("!whitetext ", ""), None),
ChatChannel.Default() // remove ! character
content = content.drop(1)
// get command from contents
val command = content.split(" ")(0)
// remove command and space from contents
content = content.drop(command.length() + 1)
// match command and execute relevant function
// if player is GM, process GM commands first
if (isGM) {
val handled = command match {
case "whitetext" => commandGMWhiteText(message, command, content, session, chatService, cluster, isGM);
case "loc" => commandGMLoc(message, command, content, session, chatService, cluster, isGM);
case "list" => commandGMList(message, command, content, session, chatService, cluster, isGM);
case "ntu" => commandGMNTU(message, command, content, session, chatService, cluster, isGM);
case "zonerotate" => commandGMZoneRotate(message, command, content, session, chatService, cluster, isGM);
// command is not a GM command, continue with normal commands
case _ => false;
}
// stop processing if GM command was handled
if (handled) {
return true
}
}
// process commands accessible to all players
val handled = command match {
case "suicide" => commandSuicide(message, command, content, session, chatService, cluster, isGM);
case "grenade" => commandGrenade(message, command, content, session, chatService, cluster, isGM);
case "macro" => commandMacro(message, command, content, session, chatService, cluster, isGM);
case "progress" => commandProgress(message, command, content, session, chatService, cluster, isGM);
// if no command was matched the command does not exist / is a GM only command but player is not GM
case _ => false;
}
if (handled) {
return true
}
// command was not handled
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
// stop sending the failed command in chat
true
}
private def commandGMWhiteText(
message: ChatMsg,
command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
chatService ! ChatService.Message(
session,
ChatMsg(UNK_227, wideContents = true, "", content, None),
ChatChannel.Default()
)
true
}
private def commandGMLoc(
message: ChatMsg,
command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
val continent = session.zone
val player = session.player
val loc = s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
log.info(loc)
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
true
}
private def commandGMList(
message: ChatMsg,
command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
val zone = content.split(" ").lift(0) match {
case Some("") =>
Some(session.zone)
case Some(id) =>
Zones.zones.find(_.id == id)
}
zone match {
case Some(inZone) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
"\\#8Name (Faction) [ID] at PosX PosY PosZ",
message.note
)
) )
true (inZone.LivePlayers ++ inZone.Corpses)
.filter(_.CharId != session.player.CharId)
} else if (contents.startsWith("!loc ")) { .filter(!_.spectator)
val continent = session.zone .sortBy(p => (p.Name, !p.isAlive))
val player = session.player .foreach(player => {
val loc = val color = if (!player.isAlive) "\\#7" else ""
s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
log.info(loc)
sessionActor ! SessionActor.SendResponse(message.copy(contents = loc))
true
} else if (contents.startsWith("!list")) {
val zone = dropFirstWord(contents).split("\\s+").headOption match {
case Some("") | None =>
Some(session.zone)
case Some(id) =>
Zones.zones.find(_.id == id)
}
zone match {
case Some(inZone) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg( ChatMsg(
CMT_GMOPEN, CMT_GMOPEN,
message.wideContents, message.wideContents,
"Server", "Server",
"\\#8Name (Faction) [ID] at PosX PosY PosZ", s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
message.note message.note
) )
) )
(inZone.LivePlayers ++ inZone.Corpses) })
.filter(_.CharId != session.player.CharId)
.sortBy(p => (p.Name, !p.isAlive))
.foreach(player => {
val color = if (!player.isAlive) "\\#7" else ""
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
message.note
)
)
})
case None =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
CMT_GMOPEN,
message.wideContents,
"Server",
"Invalid zone ID",
message.note
)
)
}
true
} else if (contents.startsWith("!ntu") && gmCommandAllowed) { case None =>
val buffer = dropFirstWord(contents).split("\\s+") sessionActor ! SessionActor.SendResponse(
val (facility, customNtuValue) = (buffer.headOption, buffer.lift(1)) match { ChatMsg(
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt)) CMT_GMOPEN,
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt)) message.wideContents,
case _ => (None, None) "Server",
} "Invalid zone ID",
val silos = (facility match { message.note
case Some(cur) if cur.toLowerCase().startsWith("curr") => )
val position = session.player.Position )
session.zone.Buildings.values }
.filter { building =>
val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius true
Vector3.DistanceSquared(building.Position, position) < soi2 }
}
case Some(all) if all.toLowerCase.startsWith("all") => private def commandGMNTU(
session.zone.Buildings.values message: ChatMsg,
case Some(x) => command: String,
session.zone.Buildings.values.find { content: String,
_.Name.equalsIgnoreCase(x) session: Session,
}.toList chatService: ActorRef[ChatService.Command],
case _ => cluster: ActorRef[InterstellarClusterService.Command],
session.zone.Buildings.values isGM: Boolean
}) ): Boolean = {
.flatMap { building =>
building.Amenities.filter { val buffer = content.toLowerCase.split("\\s+")
_.isInstanceOf[ResourceSilo] val (facility, customNtuValue) = (buffer.headOption, buffer.lift(1)) match {
} case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
case _ => (None, None)
}
val silos = (facility match {
case Some(cur) if cur.toLowerCase().startsWith("curr") =>
val position = session.player.Position
session.zone.Buildings.values
.filter { building =>
val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
Vector3.DistanceSquared(building.Position, position) < soi2
} }
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
true
} else if (contents.startsWith("!zonerotate") && gmCommandAllowed) { case Some(all) if all.toLowerCase.startsWith("all") =>
val buffer = dropFirstWord(contents).split("\\s+") session.zone.Buildings.values
cluster ! InterstellarClusterService.CavernRotation(buffer.headOption match {
case Some("-list") | Some("-l") =>
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
case _ =>
CavernRotationService.HurryNextRotation
})
true
} else if (contents.startsWith("!suicide")) { case Some(x) =>
//this is like CMT_SUICIDE but it ignores checks and forces a suicide state session.zone.Buildings.values.find {
val tplayer = session.player _.Name.equalsIgnoreCase(x)
tplayer.Revive }.toList
tplayer.Actor ! Player.Die()
true
} else if (contents.startsWith("!grenade")) { case _ =>
WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log) session.zone.Buildings.values
true })
.flatMap { building =>
building.Amenities.filter {
_.isInstanceOf[ResourceSilo]
}
}
} else if (contents.startsWith("!macro")) { ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
val avatar = session.avatar
val args = dropFirstWord(contents).split(" ").filter(_ != "") true
(args.headOption, args.lift(1)) match { }
case (Some(cmd), other) =>
cmd.toLowerCase() match { private def commandGMZoneRotate(
case "medkit" => message: ChatMsg,
medkitSanityTest(session.player.GUID, avatar.shortcuts) command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
val buffer = content.toLowerCase.split("\\s+")
cluster ! InterstellarClusterService.CavernRotation(buffer.headOption match {
case Some("-list") | Some("-l") =>
CavernRotationService.ReportRotationOrder(sessionActor.toClassic)
case _ =>
CavernRotationService.HurryNextRotation
})
true
}
private def commandSuicide(
message: ChatMsg,
command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
// this is like CMT_SUICIDE but it ignores checks and forces a suicide state
val tplayer = session.player
tplayer.Revive
tplayer.Actor ! Player.Die()
true
}
private def commandGrenade(
message: ChatMsg,
command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
true
}
private def commandMacro(
message: ChatMsg,
command: String,
content: String,
session: Session,
chatService: ActorRef[ChatService.Command],
cluster: ActorRef[InterstellarClusterService.Command],
isGM: Boolean
): Boolean = {
val avatar = session.avatar
val args = content.split(" ").filter(_ != "")
(args.headOption, args.lift(1)) match {
case (Some(cmd), other) =>
cmd.toLowerCase() match {
case "medkit" =>
medkitSanityTest(session.player.GUID, avatar.shortcuts)
true
case "implants" =>
//implant shortcut sanity test
implantSanityTest(
session.player.GUID,
avatar.implants.collect {
case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
},
avatar.shortcuts
)
true
case name
if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
avatar.implants.find {
case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
case None => false
} match {
case Some(Some(implant)) =>
//specific implant shortcut sanity test
implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
true true
case "implants" => case _ if other.nonEmpty =>
//implant shortcut sanity test //add macro?
implantSanityTest(
session.player.GUID,
avatar.implants.collect {
case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
},
avatar.shortcuts
)
true
case name
if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
avatar.implants.find {
case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
case None => false
} match {
case Some(Some(implant)) =>
//specific implant shortcut sanity test
implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
true
case _ if other.nonEmpty =>
//add macro?
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
true
case _ =>
false
}
case name
if name.nonEmpty && other.nonEmpty =>
//add macro
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts) macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
true true
case _ => case _ =>
false false
} }
case name
if name.nonEmpty && other.nonEmpty =>
//add macro
macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
true
case _ => case _ =>
false false
} }
} else if (contents.startsWith("!progress")) {
val ourRank = BattleRank.withExperience(session.avatar.bep).value case _ =>
if (!session.account.gm && false
(ourRank <= Config.app.game.promotion.broadcastBattleRank || }
ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) { }
setBattleRank(dropFirstWord(contents), session, AvatarActor.Progress)
true private def commandProgress(
} else { message: ChatMsg,
setBattleRank(contents="1", session, AvatarActor.Progress) command: String,
false content: String,
} session: Session,
} else { chatService: ActorRef[ChatService.Command],
false // unknown ! commands are ignored cluster: ActorRef[InterstellarClusterService.Command],
} isGM: Boolean
): Boolean = {
val ourRank = BattleRank.withExperience(session.avatar.bep).value
if (!session.account.gm &&
(ourRank <= Config.app.game.promotion.broadcastBattleRank ||
ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
setBattleRank(content, session, AvatarActor.Progress)
true
} else { } else {
false // unknown ! commands are ignored setBattleRank(contents = "1", session, AvatarActor.Progress)
false
} }
} }
private def dropFirstWord(str: String): String = { private def setBattleRank(
val noExtraSpaces = str.replaceAll("\\s+", " ").toLowerCase.trim
noExtraSpaces.indexOf(" ") match {
case -1 => ""
case beforeFirstBlank => noExtraSpaces.drop(beforeFirstBlank + 1)
}
}
def setBattleRank(
contents: String, contents: String,
session: Session, session: Session,
msgFunc: Long => AvatarActor.Command msgFunc: Long => AvatarActor.Command
@ -1345,7 +1512,7 @@ class ChatActor(
} }
} }
def setCommandRank( private def setCommandRank(
contents: String, contents: String,
session: Session session: Session
): Boolean = { ): Boolean = {