resolved issues with spectator implants, at least enough that implants should be stable; created an exclusive permission for spectator mode; database changes to persist permissions for different modes

This commit is contained in:
Fate-JH 2024-05-07 11:45:36 -04:00
parent 8e7be33a15
commit e748f45c2f
21 changed files with 206 additions and 78 deletions

View file

@ -0,0 +1,30 @@
/* Original: V008__Scoring.sql, overrode by V011__ScoringPatch2.sql */
CREATE OR REPLACE FUNCTION fn_assistactivity_updateRelatedStats()
RETURNS TRIGGER
AS
$$
DECLARE killerSessionId Int;
DECLARE killerId Int;
DECLARE weaponId Int;
DECLARE out integer;
BEGIN
killerId := NEW.killer_id;
weaponId := NEW.weapon_id;
killerSessionId := proc_sessionnumber_get(killerId);
out := proc_weaponstatsession_addEntryIfNoneWithSessionId(killerId, weaponId, killerSessionId);
BEGIN
UPDATE weaponstatsession
SET assists = assists + 1
WHERE avatar_id = killerId AND weapon_id = weaponId AND session_id = killerSessionId;
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
/* New */
CREATE TABLE IF NOT EXISTS "avatarmodepermission" (
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"can_spectate" BOOLEAN NOT NULL DEFAULT FALSE,
"can_gm" BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE(avatar_id)
);

View file

@ -7,6 +7,8 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.Session
import net.psforever.objects.avatar.ModePermissions
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity} import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{TurretSource, VehicleSource} import net.psforever.objects.sourcing.{TurretSource, VehicleSource}
@ -958,6 +960,28 @@ object AvatarActor {
out.future out.future
} }
def loadSpectatorModePermissions(avatarId: Long): Future[ModePermissions] = {
import ctx._
import scala.concurrent.ExecutionContext.Implicits.global
val out: Promise[ModePermissions] = Promise()
val result = ctx.run(query[persistence.Avatarmodepermission].filter(_.avatarId == lift(avatarId)))
result.onComplete {
case Success(res) =>
res.headOption
.collect {
case perms: persistence.Avatarmodepermission =>
out.completeWith(Future(ModePermissions(perms.canSpectate, perms.canGm)))
}
.orElse {
out.completeWith(Future(ModePermissions()))
None
}
case _ =>
out.completeWith(Future(ModePermissions()))
}
out.future
}
def toAvatar(avatar: persistence.Avatar): Avatar = { def toAvatar(avatar: persistence.Avatar): Avatar = {
val bep = avatar.bep val bep = avatar.bep
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) { val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
@ -2046,9 +2070,10 @@ class AvatarActor(
shortcuts <- loadShortcuts(avatarId) shortcuts <- loadShortcuts(avatarId)
saved <- AvatarActor.loadSavedAvatarData(avatarId) saved <- AvatarActor.loadSavedAvatarData(avatarId)
card <- AvatarActor.loadCampaignKdaData(avatarId) card <- AvatarActor.loadCampaignKdaData(avatarId)
} yield (loadouts, friends, ignored, shortcuts, saved, card) perms <- AvatarActor.loadSpectatorModePermissions(avatarId)
} yield (loadouts, friends, ignored, shortcuts, saved, card, perms)
result.onComplete { result.onComplete {
case Success((loadoutList, friendsList, ignoredList, shortcutList, saved, card)) => case Success((loadoutList, friendsList, ignoredList, shortcutList, saved, card, perms)) =>
avatarCopy( avatarCopy(
avatar.copy( avatar.copy(
loadouts = avatar.loadouts.copy(suit = loadoutList), loadouts = avatar.loadouts.copy(suit = loadoutList),
@ -2058,7 +2083,8 @@ class AvatarActor(
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log), purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log) use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
), ),
scorecard = card scorecard = card,
permissions = perms
) )
) )
sessionActor ! SessionActor.AvatarLoadingSync(step = 2) sessionActor ! SessionActor.AvatarLoadingSync(step = 2)
@ -2239,13 +2265,15 @@ class AvatarActor(
if (implant.active) { if (implant.active) {
deactivateImplant(implant.definition.implantType) deactivateImplant(implant.definition.implantType)
} }
session.get.zone.AvatarEvents ! AvatarServiceMessage( if (implant.initialized) {
session.get.zone.id, session.get.zone.AvatarEvents ! AvatarServiceMessage(
AvatarAction.SendResponse( session.get.zone.id,
Service.defaultPlayerGUID, AvatarAction.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0) Service.defaultPlayerGUID,
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
)
) )
) }
Some(implant.copy(initialized = false, active = false)) Some(implant.copy(initialized = false, active = false))
case (None, _) => None case (None, _) => None
})) }))

View file

@ -162,7 +162,7 @@ object ChatActor {
sendTo ! SessionActor.SendResponse(CreateShortcutMessage( sendTo ! SessionActor.SendResponse(CreateShortcutMessage(
guid, guid,
index + 1, index + 1,
Some(Shortcut.Medkit()) Some(Shortcut.Medkit)
)) ))
} }
} }

View file

@ -4,6 +4,7 @@ package net.psforever.actors.session.normal
import akka.actor.ActorContext import akka.actor.ActorContext
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
import net.psforever.objects.Session import net.psforever.objects.Session
import net.psforever.objects.avatar.ModePermissions
import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
import net.psforever.services.chat.DefaultChannel import net.psforever.services.chat.DefaultChannel
import net.psforever.types.ChatMessageType import net.psforever.types.ChatMessageType
@ -18,10 +19,12 @@ object ChatLogic {
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
def handleChatMsg(session: Session, message: ChatMsg): Unit = { def handleChatMsg(message: ChatMsg): Unit = {
import net.psforever.types.ChatMessageType._ import net.psforever.types.ChatMessageType._
val gmCommandAllowed = val isAlive = if (player != null) player.isAlive else false
session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType) val perms = if (avatar != null) avatar.permissions else ModePermissions()
val gmCommandAllowed = (session.account.gm && perms.canGM) ||
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 */ /** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") && case (_, _, contents) if contents.startsWith("!") &&
@ -42,7 +45,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_SPEED, _, contents) if gmCommandAllowed => case (CMT_SPEED, _, contents) if gmCommandAllowed =>
ops.commandSpeed(message, contents) ops.commandSpeed(message, contents)
case (CMT_TOGGLESPECTATORMODE, _, contents) if gmCommandAllowed => case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive && (gmCommandAllowed || perms.canSpectate) =>
ops.commandToggleSpectatorMode(session, contents) ops.commandToggleSpectatorMode(session, contents)
case (CMT_RECALL, _, _) => case (CMT_RECALL, _, _) =>
@ -82,19 +85,19 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed => case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed =>
ops.commandSendToRecipient(session, message, DefaultChannel) ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_OPEN, _, _) if !session.player.silenced => case (CMT_OPEN, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, DefaultChannel) ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_VOICE, _, contents) => case (CMT_VOICE, _, contents) =>
ops.commandVoice(session, message, contents, DefaultChannel) ops.commandVoice(session, message, contents, DefaultChannel)
case (CMT_TELL, _, _) if !session.player.silenced => case (CMT_TELL, _, _) if !player.silenced =>
ops.commandTellOrIgnore(session, message, DefaultChannel) ops.commandTellOrIgnore(session, message, DefaultChannel)
case (CMT_BROADCAST, _, _) if !session.player.silenced => case (CMT_BROADCAST, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, DefaultChannel) ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_PLATOON, _, _) if !session.player.silenced => case (CMT_PLATOON, _, _) if !player.silenced =>
ops.commandSendToRecipient(session, message, DefaultChannel) ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_COMMAND, _, _) if gmCommandAllowed => case (CMT_COMMAND, _, _) if gmCommandAllowed =>
@ -151,7 +154,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
val SetChatFilterMessage(_, _, _) = pkt val SetChatFilterMessage(_, _, _) = pkt
} }
def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit = { def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
import ChatMessageType._ import ChatMessageType._
message.messageType match { message.messageType match {
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE => case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
@ -186,7 +189,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case a :: b => (a, b) case a :: b => (a, b)
case _ => ("", Seq("")) case _ => ("", Seq(""))
} }
val gmBangCommandAllowed = session.account.gm || Config.app.development.unprivilegedGmBangCommands.contains(command) val perms = if (avatar != null) avatar.permissions else ModePermissions()
val gmBangCommandAllowed = (session.account.gm && perms.canGM) ||
Config.app.development.unprivilegedGmBangCommands.contains(command)
//try gm commands //try gm commands
val tryGmCommandResult = if (gmBangCommandAllowed) { val tryGmCommandResult = if (gmBangCommandAllowed) {
command match { command match {

View file

@ -71,7 +71,7 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
vehicleResponse.handle(toChannel, guid, reply) vehicleResponse.handle(toChannel, guid, reply)
case ChatService.MessageResponse(fromSession, message, _) => case ChatService.MessageResponse(fromSession, message, _) =>
chat.handleIncomingMessage(data.session, message, fromSession) chat.handleIncomingMessage(message, fromSession)
case SessionActor.SendResponse(packet) => case SessionActor.SendResponse(packet) =>
data.sendResponse(packet) data.sendResponse(packet)
@ -313,7 +313,7 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
data.zoning.spawn.handleSpawnRequest(packet) data.zoning.spawn.handleSpawnRequest(packet)
case packet: ChatMsg => case packet: ChatMsg =>
chat.handleChatMsg(data.session, packet) chat.handleChatMsg(packet)
case packet: SetChatFilterMessage => case packet: SetChatFilterMessage =>
chat.handleChatFilter(packet) chat.handleChatFilter(packet)

View file

@ -3,6 +3,7 @@ package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, typed} import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.support.AvatarHandlerFunctions import net.psforever.actors.session.support.AvatarHandlerFunctions
import net.psforever.packet.game.{AvatarImplantMessage, ImplantAction}
import scala.concurrent.duration._ import scala.concurrent.duration._
// //
@ -404,6 +405,11 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) => case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
ops.facilityCaptureRewards(buildingId, zoneNumber, cep) ops.facilityCaptureRewards(buildingId, zoneNumber, cep)
case AvatarResponse.SendResponse(pkt: AvatarImplantMessage)
if pkt.player_guid == player.GUID && pkt.action == ImplantAction.Initialization =>
//special spectator implants stay initialized and do not deinitialize
()
case AvatarResponse.SendResponse(msg) => case AvatarResponse.SendResponse(msg) =>
sendResponse(msg) sendResponse(msg)

View file

@ -21,7 +21,7 @@ object ChatLogic {
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
def sessionLogic: SessionData = ops.sessionLogic def sessionLogic: SessionData = ops.sessionLogic
def handleChatMsg(session: Session, message: ChatMsg): Unit = { def handleChatMsg(message: ChatMsg): Unit = {
import ChatMessageType._ import ChatMessageType._
(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 */ /** Messages starting with ! are custom chat commands */
@ -93,7 +93,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
val SetChatFilterMessage(_, _, _) = pkt val SetChatFilterMessage(_, _, _) = pkt
} }
def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit = { def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
import ChatMessageType._ import ChatMessageType._
message.messageType match { message.messageType match {
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE => case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
@ -125,7 +125,6 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case "list" => ops.customCommandList(session, params, message) case "list" => ops.customCommandList(session, params, message)
case "nearby" => ops.customCommandNearby(session) case "nearby" => ops.customCommandNearby(session)
case "loc" => ops.customCommandLoc(session, message) case "loc" => ops.customCommandLoc(session, message)
case "macro" => ops.customCommandMacro(session, params)
case _ => false case _ => false
} }
} else { } else {

View file

@ -6,7 +6,7 @@ import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.login.WorldSession.RemoveOldEquipmentFromInventory import net.psforever.login.WorldSession.RemoveOldEquipmentFromInventory
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle} import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle}
import net.psforever.objects.avatar.Avatar import net.psforever.objects.avatar.{Avatar, Implant}
import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.{Deployable, TelepadLike} import net.psforever.objects.ce.{Deployable, TelepadLike}
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
@ -42,6 +42,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
private var customImplants = SpectatorModeLogic.SpectatorImplants.map(_.get) private var customImplants = SpectatorModeLogic.SpectatorImplants.map(_.get)
private var additionalImplants: Seq[CreateShortcutMessage] = Seq()
def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ } def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ }
def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { /* intentionally blank */ } def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { /* intentionally blank */ }
@ -181,9 +183,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
customImplants.lift(slot) customImplants.lift(slot)
.collect { .collect {
case implant if implant.active => case implant if implant.active =>
customImplants = customImplants.updated(slot, implant.copy(active = false)) customImplantOff(slot, implant)
sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 0))
sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
case implant => case implant =>
customImplants = customImplants.updated(slot, implant.copy(active = true)) customImplants = customImplants.updated(slot, implant.copy(active = true))
sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 1)) sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 1))
@ -349,7 +349,34 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt
} }
def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { /* intentionally blank */ } def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
val CreateShortcutMessage(_, slot, wouldBeImplant) = pkt
val pguid = player.GUID
if (slot > 1 && slot < 5) {
//protected
customImplants
.zipWithIndex
.find { case (_, index) => index + 2 == slot}
.foreach {
case (implant, _) if wouldBeImplant.contains(implant.definition.implantType.shortcut) => ()
case (implant, _) if implant.active =>
sendResponse(CreateShortcutMessage(pguid, slot, Some(implant.definition.implantType.shortcut)))
customImplantOff(slot, implant)
case (implant, _) =>
sendResponse(CreateShortcutMessage(pguid, slot, Some(implant.definition.implantType.shortcut)))
}
} else {
additionalImplants.indexWhere(_.slot == slot) match {
case -1 => ()
case index =>
additionalImplants = additionalImplants.take(index) ++ additionalImplants.drop(index + 1)
}
wouldBeImplant.collect {
case _ =>
additionalImplants = additionalImplants :+ pkt
}
}
}
def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { /* intentionally blank */ } def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { /* intentionally blank */ }
@ -629,4 +656,23 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
tplayer.death_by = -1 tplayer.death_by = -1
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name)
} }
private def customImplantOff(slot: Int, implant: Implant): Unit = {
customImplants = customImplants.updated(slot, implant.copy(active = false))
sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 0))
sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
}
override protected[session] def stop(): Unit = {
val pguid = player.GUID
//set only originally blank slots blank again; rest will be overwrote later
val originalBlankSlots = ((player.avatar.shortcuts.head, 1) +:
player.avatar.shortcuts.drop(4).zipWithIndex.map { case (scut, slot) => (scut, slot + 4) })
.collect { case (None, slot) => slot }
additionalImplants
.map(_.slot)
.filter(originalBlankSlots.contains)
.map(slot => CreateShortcutMessage(pguid, slot, None))
.foreach(sendResponse)
}
} }

View file

@ -61,7 +61,6 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on")) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on"))
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorEnabled")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorEnabled"))
continent.actor ! ZoneActor.RemoveFromBlockMap(player) continent.actor ! ZoneActor.RemoveFromBlockMap(player)
data.general.avatarActor ! AvatarActor.DeactivateActiveImplants()
continent continent
.GUID(data.terminals.usingMedicalTerminal) .GUID(data.terminals.usingMedicalTerminal)
.foreach { case term: Terminal with ProximityUnit => .foreach { case term: Terminal with ProximityUnit =>
@ -116,11 +115,21 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
val originalEvent = player.History.headOption val originalEvent = player.History.headOption
player.ClearHistory() player.ClearHistory()
player.LogActivity(originalEvent) player.LogActivity(originalEvent)
// player.avatar
player.spectator = true .shortcuts
.zipWithIndex
.collect { case (Some(_), index) => index + 1 }
.map(CreateShortcutMessage(pguid, _, None))
.foreach(sendResponse)
player.avatar.implants
.collect { case Some(implant) if implant.active =>
data.general.avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
}
if (player.silenced) { if (player.silenced) {
data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0")) data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0"))
} }
//
player.spectator = true
data.chat.JoinChannel(SpectatorChannel) data.chat.JoinChannel(SpectatorChannel)
val newPlayer = SpectatorModeLogic.spectatorCharacter(player) val newPlayer = SpectatorModeLogic.spectatorCharacter(player)
val cud = new SimpleItem(GlobalDefinitions.command_detonater) val cud = new SimpleItem(GlobalDefinitions.command_detonater)
@ -147,13 +156,21 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
import scala.concurrent.duration._ import scala.concurrent.duration._
val player = data.player val player = data.player
val zoning = data.zoning val zoning = data.zoning
val pguid = player.GUID
val sendResponse: PlanetSidePacket => Unit = data.sendResponse val sendResponse: PlanetSidePacket => Unit = data.sendResponse
// //
data.general.stop()
player.avatar.shortcuts.slice(1, 4)
.zipWithIndex
.collect { case (None, slot) => slot + 1 } //set only actual blank slots blank
.map(CreateShortcutMessage(pguid, _, None))
.foreach(sendResponse)
data.chat.LeaveChannel(SpectatorChannel) data.chat.LeaveChannel(SpectatorChannel)
player.spectator = false player.spectator = false
sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot (from cud) sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot (from cud)
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off")) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled"))
zoning.zoneReload = true
zoning.spawn.randomRespawn(0.seconds) //to sanctuary zoning.spawn.randomRespawn(0.seconds) //to sanctuary
} }
@ -184,7 +201,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
vehicleResponse.handle(toChannel, guid, reply) vehicleResponse.handle(toChannel, guid, reply)
case ChatService.MessageResponse(fromSession, message, _) => case ChatService.MessageResponse(fromSession, message, _) =>
chat.handleIncomingMessage(data.session, message, fromSession) chat.handleIncomingMessage(message, fromSession)
case SessionActor.SendResponse(packet) => case SessionActor.SendResponse(packet) =>
data.sendResponse(packet) data.sendResponse(packet)
@ -417,7 +434,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
data.zoning.spawn.handleSpawnRequest(packet) data.zoning.spawn.handleSpawnRequest(packet)
case packet: ChatMsg => case packet: ChatMsg =>
chat.handleChatMsg(data.session, packet) chat.handleChatMsg(packet)
case packet: SetChatFilterMessage => case packet: SetChatFilterMessage =>
chat.handleChatFilter(packet) chat.handleChatFilter(packet)

View file

@ -43,11 +43,11 @@ import net.psforever.zones.Zones
trait ChatFunctions extends CommonSessionInterfacingFunctionality { trait ChatFunctions extends CommonSessionInterfacingFunctionality {
def ops: ChatOperations def ops: ChatOperations
def handleChatMsg(session: Session, message: ChatMsg): Unit def handleChatMsg(message: ChatMsg): Unit
def handleChatFilter(pkt: SetChatFilterMessage): Unit def handleChatFilter(pkt: SetChatFilterMessage): Unit
def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit
} }
class ChatOperations( class ChatOperations(
@ -811,7 +811,7 @@ class ChatOperations(
sendResponse(CreateShortcutMessage( sendResponse(CreateShortcutMessage(
guid, guid,
index + 1, index + 1,
Some(Shortcut.Medkit()) Some(Shortcut.Medkit)
)) ))
} }
} }

View file

@ -41,7 +41,7 @@ trait CommonSessionInterfacingFunctionality {
protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionLogic.sendResponse(pkt) protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionLogic.sendResponse(pkt)
protected[support] def actionsToCancel(): Unit = { /* to override */ } protected[session] def actionsToCancel(): Unit = { /* to override */ }
protected[support] def stop(): Unit = { /* to override */ } protected[session] def stop(): Unit = { /* to override */ }
} }

View file

@ -741,7 +741,7 @@ class GeneralOperations(
sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None)) sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None))
} }
override protected[support] def actionsToCancel(): Unit = { override protected[session] def actionsToCancel(): Unit = {
progressBarValue = None progressBarValue = None
kitToBeUsed = None kitToBeUsed = None
collisionHistory.clear() collisionHistory.clear()

View file

@ -208,7 +208,7 @@ class SessionTerminalHandlers(
) )
} }
override protected[support] def actionsToCancel(): Unit = { override protected[session] def actionsToCancel(): Unit = {
lastTerminalOrderFulfillment = true lastTerminalOrderFulfillment = true
usingMedicalTerminal = None usingMedicalTerminal = None
} }

View file

@ -323,7 +323,7 @@ class WeaponAndProjectileOperations(
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0)) ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
} }
override protected[support] def actionsToCancel(): Unit = { override protected[session] def actionsToCancel(): Unit = {
shootingStart.clear() shootingStart.clear()
shootingStop.clear() shootingStop.clear()
(prefire ++ shooting).foreach { guid => (prefire ++ shooting).foreach { guid =>

View file

@ -3142,17 +3142,11 @@ class ZoningOperations(
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet. * Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
*/ */
def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = { def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = {
avatar.shortcuts initializeShortcutsAndBank(guid, avatar.shortcuts)
.zipWithIndex
.collect { case (Some(shortcut), index) =>
sendResponse(CreateShortcutMessage(
guid,
index + 1,
Some(AvatarShortcut.convert(shortcut))
))
}
sendResponse(ChangeShortcutBankMessage(guid, 0))
} }
/**
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
*/
def initializeShortcutsAndBank(guid: PlanetSideGUID, shortcuts: Array[Option[AvatarShortcut]]): Unit = { def initializeShortcutsAndBank(guid: PlanetSideGUID, shortcuts: Array[Option[AvatarShortcut]]): Unit = {
shortcuts shortcuts
.zipWithIndex .zipWithIndex

View file

@ -19,7 +19,7 @@ import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFro
import net.psforever.objects.vital.damage.DamageProfile import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning} import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning}
import net.psforever.types._ import net.psforever.types._
@ -47,7 +47,7 @@ class Player(var avatar: Avatar)
new WithGantry(avatar.name), new WithGantry(avatar.name),
new WithMovementTrigger() new WithMovementTrigger()
))) )))
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10)) interaction(new InteractWithMines(range = 10))
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
@ -653,14 +653,3 @@ object Player {
false false
} }
} }
private class InteractWithMinesUnlessSpectating(
private val obj: Player,
override val range: Float
) extends InteractWithMines(range) {
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
if (!obj.spectator) {
super.interaction(sector, target)
}
}
}

View file

@ -115,6 +115,11 @@ case class MemberLists(
ignored: List[Ignored] = List[Ignored]() ignored: List[Ignored] = List[Ignored]()
) )
case class ModePermissions(
canSpectate: Boolean = false,
canGM: Boolean = false
)
case class Avatar( case class Avatar(
/** unique identifier corresponding to a database table row index */ /** unique identifier corresponding to a database table row index */
id: Int, id: Int,
@ -134,7 +139,8 @@ case class Avatar(
loadouts: Loadouts = Loadouts(), loadouts: Loadouts = Loadouts(),
cooldowns: Cooldowns = Cooldowns(), cooldowns: Cooldowns = Cooldowns(),
people: MemberLists = MemberLists(), people: MemberLists = MemberLists(),
scorecard: ScoreCard = new ScoreCard() scorecard: ScoreCard = new ScoreCard(),
permissions: ModePermissions = ModePermissions()
) { ) {
assert(bep >= 0) assert(bep >= 0)
assert(cep >= 0) assert(cep >= 0)

View file

@ -30,7 +30,7 @@ object Shortcut {
*/ */
def convert(shortcut: Shortcut): GameShortcut = { def convert(shortcut: Shortcut): GameShortcut = {
shortcut.tile match { shortcut.tile match {
case "medkit" => GameShortcut.Medkit() case "medkit" => GameShortcut.Medkit
case "shortcut_macro" => GameShortcut.Macro(shortcut.effect1, shortcut.effect2) case "shortcut_macro" => GameShortcut.Macro(shortcut.effect1, shortcut.effect2)
case _ => GameShortcut.Implant(shortcut.tile) case _ => GameShortcut.Implant(shortcut.tile)
} }
@ -67,7 +67,7 @@ object Shortcut {
*/ */
private def typeEquals(a: Shortcut, b: GameShortcut): Boolean = { private def typeEquals(a: Shortcut, b: GameShortcut): Boolean = {
b match { b match {
case GameShortcut.Medkit() => true case GameShortcut.Medkit => true
case GameShortcut.Macro(x, y) => x.equals(a.effect1) && y.equals(a.effect2) case GameShortcut.Macro(x, y) => x.equals(a.effect1) && y.equals(a.effect2)
case GameShortcut.Implant(tile) => tile.equals(a.tile) case GameShortcut.Implant(tile) => tile.equals(a.tile)
case _ => true case _ => true

View file

@ -71,7 +71,7 @@ final case class CreateShortcutMessage(
object Shortcut extends Marshallable[Shortcut] { object Shortcut extends Marshallable[Shortcut] {
/** Preset for the medkit quick-use option. */ /** Preset for the medkit quick-use option. */
final case class Medkit() extends Shortcut(code=0) { case object Medkit extends Shortcut(code=0) {
def tile = "medkit" def tile = "medkit"
} }
@ -98,14 +98,14 @@ object Shortcut extends Marshallable[Shortcut] {
/** /**
* Main transcoder for medkit shortcuts. * Main transcoder for medkit shortcuts.
*/ */
val medkitCodec: Codec[Medkit] = ( val medkitCodec: Codec[Shortcut] = (
("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) :: ("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) ::
("effect1" | PacketHelpers.encodedWideString) :: ("effect1" | PacketHelpers.encodedWideString) ::
("effect2" | PacketHelpers.encodedWideString) ("effect2" | PacketHelpers.encodedWideString)
).xmap[Medkit]( ).xmap[Shortcut](
_ => Medkit(), _ => Medkit,
{ {
case Medkit() => "medkit" :: "" :: "" :: HNil case Medkit => "medkit" :: "" :: "" :: HNil
} }
) )

View file

@ -0,0 +1,8 @@
// Copyright (c) 2024 PSForever
package net.psforever.persistence
case class Avatarmodepermission(
avatarId: Int,
canSpectate: Boolean = false,
canGm: Boolean = false
)

View file

@ -19,7 +19,7 @@ class CreateShortcutMessageTest extends Specification {
player_guid mustEqual PlanetSideGUID(4210) player_guid mustEqual PlanetSideGUID(4210)
slot mustEqual 1 slot mustEqual 1
shortcut match { shortcut match {
case Some(Shortcut.Medkit()) => ok case Some(Shortcut.Medkit) => ok
case _ => ko case _ => ko
} }
case _ => case _ =>
@ -53,7 +53,7 @@ class CreateShortcutMessageTest extends Specification {
} }
"encode (medkit)" in { "encode (medkit)" in {
val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, Some(Shortcut.Medkit())) val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, Some(Shortcut.Medkit))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual stringMedkit pkt mustEqual stringMedkit
@ -90,8 +90,8 @@ class CreateShortcutMessageTest extends Specification {
ImplantType.DarklightVision.shortcut.tile mustEqual "darklight_vision" ImplantType.DarklightVision.shortcut.tile mustEqual "darklight_vision"
ImplantType.Targeting.shortcut.code mustEqual 2 ImplantType.Targeting.shortcut.code mustEqual 2
ImplantType.Targeting.shortcut.tile mustEqual "targeting" ImplantType.Targeting.shortcut.tile mustEqual "targeting"
Shortcut.Medkit().code mustEqual 0 Shortcut.Medkit.code mustEqual 0
Shortcut.Medkit().tile mustEqual "medkit" Shortcut.Medkit.tile mustEqual "medkit"
ImplantType.MeleeBooster.shortcut.code mustEqual 2 ImplantType.MeleeBooster.shortcut.code mustEqual 2
ImplantType.MeleeBooster.shortcut.tile mustEqual "melee_booster" ImplantType.MeleeBooster.shortcut.tile mustEqual "melee_booster"
ImplantType.PersonalShield.shortcut.code mustEqual 2 ImplantType.PersonalShield.shortcut.code mustEqual 2