diff --git a/server/src/main/resources/db/migration/V013__Spectator.sql b/server/src/main/resources/db/migration/V013__Spectator.sql new file mode 100644 index 000000000..950cd39b1 --- /dev/null +++ b/server/src/main/resources/db/migration/V013__Spectator.sql @@ -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) +); diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 4f56c0299..c4d861051 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -7,6 +7,8 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy} import java.util.concurrent.atomic.AtomicInteger 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.serverobject.affinity.FactionAffinity import net.psforever.objects.sourcing.{TurretSource, VehicleSource} @@ -958,6 +960,28 @@ object AvatarActor { 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 = { val bep = avatar.bep val convertedCosmetics = if (BattleRank.showCosmetics(bep)) { @@ -2046,9 +2070,10 @@ class AvatarActor( shortcuts <- loadShortcuts(avatarId) saved <- AvatarActor.loadSavedAvatarData(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 { - case Success((loadoutList, friendsList, ignoredList, shortcutList, saved, card)) => + case Success((loadoutList, friendsList, ignoredList, shortcutList, saved, card, perms)) => avatarCopy( avatar.copy( loadouts = avatar.loadouts.copy(suit = loadoutList), @@ -2058,7 +2083,8 @@ class AvatarActor( purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log), use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log) ), - scorecard = card + scorecard = card, + permissions = perms ) ) sessionActor ! SessionActor.AvatarLoadingSync(step = 2) @@ -2239,13 +2265,15 @@ class AvatarActor( if (implant.active) { deactivateImplant(implant.definition.implantType) } - session.get.zone.AvatarEvents ! AvatarServiceMessage( - session.get.zone.id, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0) + if (implant.initialized) { + session.get.zone.AvatarEvents ! AvatarServiceMessage( + session.get.zone.id, + AvatarAction.SendResponse( + Service.defaultPlayerGUID, + AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0) + ) ) - ) + } Some(implant.copy(initialized = false, active = false)) case (None, _) => None })) diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index b68258390..a05dea1ec 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -162,7 +162,7 @@ object ChatActor { sendTo ! SessionActor.SendResponse(CreateShortcutMessage( guid, index + 1, - Some(Shortcut.Medkit()) + Some(Shortcut.Medkit) )) } } diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index af585ad6d..dc599e017 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -4,6 +4,7 @@ package net.psforever.actors.session.normal import akka.actor.ActorContext import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.objects.Session +import net.psforever.objects.avatar.ModePermissions import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} import net.psforever.services.chat.DefaultChannel import net.psforever.types.ChatMessageType @@ -18,10 +19,12 @@ object ChatLogic { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { def sessionLogic: SessionData = ops.sessionLogic - def handleChatMsg(session: Session, message: ChatMsg): Unit = { + def handleChatMsg(message: ChatMsg): Unit = { import net.psforever.types.ChatMessageType._ - val gmCommandAllowed = - session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType) + val isAlive = if (player != null) player.isAlive else false + 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 { /** Messages starting with ! are custom chat commands */ 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 => ops.commandSpeed(message, contents) - case (CMT_TOGGLESPECTATORMODE, _, contents) if gmCommandAllowed => + case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive && (gmCommandAllowed || perms.canSpectate) => ops.commandToggleSpectatorMode(session, contents) case (CMT_RECALL, _, _) => @@ -82,19 +85,19 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed => ops.commandSendToRecipient(session, message, DefaultChannel) - case (CMT_OPEN, _, _) if !session.player.silenced => + case (CMT_OPEN, _, _) if !player.silenced => ops.commandSendToRecipient(session, message, DefaultChannel) case (CMT_VOICE, _, contents) => ops.commandVoice(session, message, contents, DefaultChannel) - case (CMT_TELL, _, _) if !session.player.silenced => + case (CMT_TELL, _, _) if !player.silenced => ops.commandTellOrIgnore(session, message, DefaultChannel) - case (CMT_BROADCAST, _, _) if !session.player.silenced => + case (CMT_BROADCAST, _, _) if !player.silenced => ops.commandSendToRecipient(session, message, DefaultChannel) - case (CMT_PLATOON, _, _) if !session.player.silenced => + case (CMT_PLATOON, _, _) if !player.silenced => ops.commandSendToRecipient(session, message, DefaultChannel) case (CMT_COMMAND, _, _) if gmCommandAllowed => @@ -151,7 +154,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext val SetChatFilterMessage(_, _, _) = pkt } - def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit = { + def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = { import ChatMessageType._ message.messageType match { 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 _ => ("", 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 val tryGmCommandResult = if (gmBangCommandAllowed) { command match { diff --git a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala index bf1874762..9c49e8dd5 100644 --- a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala +++ b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala @@ -71,7 +71,7 @@ class NormalModeLogic(data: SessionData) extends ModeLogic { vehicleResponse.handle(toChannel, guid, reply) case ChatService.MessageResponse(fromSession, message, _) => - chat.handleIncomingMessage(data.session, message, fromSession) + chat.handleIncomingMessage(message, fromSession) case SessionActor.SendResponse(packet) => data.sendResponse(packet) @@ -313,7 +313,7 @@ class NormalModeLogic(data: SessionData) extends ModeLogic { data.zoning.spawn.handleSpawnRequest(packet) case packet: ChatMsg => - chat.handleChatMsg(data.session, packet) + chat.handleChatMsg(packet) case packet: SetChatFilterMessage => chat.handleChatFilter(packet) diff --git a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala index d26de69b3..4b8489bee 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -3,6 +3,7 @@ package net.psforever.actors.session.spectator import akka.actor.{ActorContext, typed} import net.psforever.actors.session.support.AvatarHandlerFunctions +import net.psforever.packet.game.{AvatarImplantMessage, ImplantAction} import scala.concurrent.duration._ // @@ -404,6 +405,11 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.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) => sendResponse(msg) 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 e0c16c7c4..3b7d10edd 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -21,7 +21,7 @@ object ChatLogic { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { def sessionLogic: SessionData = ops.sessionLogic - def handleChatMsg(session: Session, message: ChatMsg): Unit = { + def handleChatMsg(message: ChatMsg): Unit = { import ChatMessageType._ (message.messageType, message.recipient.trim, message.contents.trim) match { /** Messages starting with ! are custom chat commands */ @@ -93,7 +93,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext val SetChatFilterMessage(_, _, _) = pkt } - def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit = { + def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = { import ChatMessageType._ message.messageType match { 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 "nearby" => ops.customCommandNearby(session) case "loc" => ops.customCommandLoc(session, message) - case "macro" => ops.customCommandMacro(session, params) case _ => false } } else { diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala index e749a9a30..c495d0ae4 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -6,7 +6,7 @@ import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} import net.psforever.login.WorldSession.RemoveOldEquipmentFromInventory 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.ce.{Deployable, TelepadLike} 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 additionalImplants: Seq[CreateShortcutMessage] = Seq() + def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): 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) .collect { case implant if implant.active => - 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)) + customImplantOff(slot, implant) case implant => customImplants = customImplants.updated(slot, implant.copy(active = true)) sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 1)) @@ -349,7 +349,34 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex 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 */ } @@ -629,4 +656,23 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex tplayer.death_by = -1 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) + } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala index b261538c7..4c0f483f3 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala @@ -61,7 +61,6 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorEnabled")) continent.actor ! ZoneActor.RemoveFromBlockMap(player) - data.general.avatarActor ! AvatarActor.DeactivateActiveImplants() continent .GUID(data.terminals.usingMedicalTerminal) .foreach { case term: Terminal with ProximityUnit => @@ -116,11 +115,21 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { val originalEvent = player.History.headOption player.ClearHistory() player.LogActivity(originalEvent) - // - player.spectator = true + player.avatar + .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) { data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0")) } + // + player.spectator = true data.chat.JoinChannel(SpectatorChannel) val newPlayer = SpectatorModeLogic.spectatorCharacter(player) val cud = new SimpleItem(GlobalDefinitions.command_detonater) @@ -147,13 +156,21 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { import scala.concurrent.duration._ val player = data.player val zoning = data.zoning + val pguid = player.GUID 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) player.spectator = false sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot (from cud) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled")) + zoning.zoneReload = true zoning.spawn.randomRespawn(0.seconds) //to sanctuary } @@ -184,7 +201,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { vehicleResponse.handle(toChannel, guid, reply) case ChatService.MessageResponse(fromSession, message, _) => - chat.handleIncomingMessage(data.session, message, fromSession) + chat.handleIncomingMessage(message, fromSession) case SessionActor.SendResponse(packet) => data.sendResponse(packet) @@ -417,7 +434,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { data.zoning.spawn.handleSpawnRequest(packet) case packet: ChatMsg => - chat.handleChatMsg(data.session, packet) + chat.handleChatMsg(packet) case packet: SetChatFilterMessage => chat.handleChatFilter(packet) 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 520515465..67193f293 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -43,11 +43,11 @@ import net.psforever.zones.Zones trait ChatFunctions extends CommonSessionInterfacingFunctionality { def ops: ChatOperations - def handleChatMsg(session: Session, message: ChatMsg): Unit + def handleChatMsg(message: ChatMsg): Unit def handleChatFilter(pkt: SetChatFilterMessage): Unit - def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit + def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit } class ChatOperations( @@ -811,7 +811,7 @@ class ChatOperations( sendResponse(CreateShortcutMessage( guid, index + 1, - Some(Shortcut.Medkit()) + Some(Shortcut.Medkit) )) } } diff --git a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala index e05f5ec5b..3dec2e243 100644 --- a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala +++ b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala @@ -41,7 +41,7 @@ trait CommonSessionInterfacingFunctionality { 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 */ } } diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index 889646993..9ae7677a5 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -741,7 +741,7 @@ class GeneralOperations( sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None)) } - override protected[support] def actionsToCancel(): Unit = { + override protected[session] def actionsToCancel(): Unit = { progressBarValue = None kitToBeUsed = None collisionHistory.clear() diff --git a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala index 73254d6fe..21cc9f9d6 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala @@ -208,7 +208,7 @@ class SessionTerminalHandlers( ) } - override protected[support] def actionsToCancel(): Unit = { + override protected[session] def actionsToCancel(): Unit = { lastTerminalOrderFulfillment = true usingMedicalTerminal = None } diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 6f0b289f3..7741fa86f 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -323,7 +323,7 @@ class WeaponAndProjectileOperations( ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0)) } - override protected[support] def actionsToCancel(): Unit = { + override protected[session] def actionsToCancel(): Unit = { shootingStart.clear() shootingStop.clear() (prefire ++ shooting).foreach { guid => 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 3c494a8b2..bb0c93584 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3142,17 +3142,11 @@ class ZoningOperations( * Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet. */ def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = { - avatar.shortcuts - .zipWithIndex - .collect { case (Some(shortcut), index) => - sendResponse(CreateShortcutMessage( - guid, - index + 1, - Some(AvatarShortcut.convert(shortcut)) - )) - } - sendResponse(ChangeShortcutBankMessage(guid, 0)) + initializeShortcutsAndBank(guid, avatar.shortcuts) } + /** + * Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet. + */ def initializeShortcutsAndBank(guid: PlanetSideGUID, shortcuts: Array[Option[AvatarShortcut]]): Unit = { shortcuts .zipWithIndex diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 94f5136a5..24c277b3a 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -19,7 +19,7 @@ import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFro import net.psforever.objects.vital.damage.DamageProfile import net.psforever.objects.vital.interaction.DamageInteraction 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.types._ @@ -47,7 +47,7 @@ class Player(var avatar: Avatar) new WithGantry(avatar.name), new WithMovementTrigger() ))) - interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10)) + interaction(new InteractWithMines(range = 10)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) @@ -653,14 +653,3 @@ object Player { 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) - } - } -} diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index 166137c2d..4e349ec55 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -115,6 +115,11 @@ case class MemberLists( ignored: List[Ignored] = List[Ignored]() ) +case class ModePermissions( + canSpectate: Boolean = false, + canGM: Boolean = false + ) + case class Avatar( /** unique identifier corresponding to a database table row index */ id: Int, @@ -134,7 +139,8 @@ case class Avatar( loadouts: Loadouts = Loadouts(), cooldowns: Cooldowns = Cooldowns(), people: MemberLists = MemberLists(), - scorecard: ScoreCard = new ScoreCard() + scorecard: ScoreCard = new ScoreCard(), + permissions: ModePermissions = ModePermissions() ) { assert(bep >= 0) assert(cep >= 0) diff --git a/src/main/scala/net/psforever/objects/avatar/Shortcut.scala b/src/main/scala/net/psforever/objects/avatar/Shortcut.scala index 0251ad7ac..bcb1a3a6d 100644 --- a/src/main/scala/net/psforever/objects/avatar/Shortcut.scala +++ b/src/main/scala/net/psforever/objects/avatar/Shortcut.scala @@ -30,7 +30,7 @@ object Shortcut { */ def convert(shortcut: Shortcut): GameShortcut = { shortcut.tile match { - case "medkit" => GameShortcut.Medkit() + case "medkit" => GameShortcut.Medkit case "shortcut_macro" => GameShortcut.Macro(shortcut.effect1, shortcut.effect2) case _ => GameShortcut.Implant(shortcut.tile) } @@ -67,7 +67,7 @@ object Shortcut { */ private def typeEquals(a: Shortcut, b: GameShortcut): Boolean = { 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.Implant(tile) => tile.equals(a.tile) case _ => true diff --git a/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala b/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala index ba518fc27..d194bc880 100644 --- a/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala +++ b/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala @@ -71,7 +71,7 @@ final case class CreateShortcutMessage( object Shortcut extends Marshallable[Shortcut] { /** 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" } @@ -98,14 +98,14 @@ object Shortcut extends Marshallable[Shortcut] { /** * Main transcoder for medkit shortcuts. */ - val medkitCodec: Codec[Medkit] = ( + val medkitCodec: Codec[Shortcut] = ( ("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) :: ("effect1" | PacketHelpers.encodedWideString) :: ("effect2" | PacketHelpers.encodedWideString) - ).xmap[Medkit]( - _ => Medkit(), + ).xmap[Shortcut]( + _ => Medkit, { - case Medkit() => "medkit" :: "" :: "" :: HNil + case Medkit => "medkit" :: "" :: "" :: HNil } ) diff --git a/src/main/scala/net/psforever/persistence/Avatarmodepermission.scala b/src/main/scala/net/psforever/persistence/Avatarmodepermission.scala new file mode 100644 index 000000000..437ad0344 --- /dev/null +++ b/src/main/scala/net/psforever/persistence/Avatarmodepermission.scala @@ -0,0 +1,8 @@ +// Copyright (c) 2024 PSForever +package net.psforever.persistence + +case class Avatarmodepermission( + avatarId: Int, + canSpectate: Boolean = false, + canGm: Boolean = false + ) diff --git a/src/test/scala/game/CreateShortcutMessageTest.scala b/src/test/scala/game/CreateShortcutMessageTest.scala index 2ac619e78..b741b2d7f 100644 --- a/src/test/scala/game/CreateShortcutMessageTest.scala +++ b/src/test/scala/game/CreateShortcutMessageTest.scala @@ -19,7 +19,7 @@ class CreateShortcutMessageTest extends Specification { player_guid mustEqual PlanetSideGUID(4210) slot mustEqual 1 shortcut match { - case Some(Shortcut.Medkit()) => ok + case Some(Shortcut.Medkit) => ok case _ => ko } case _ => @@ -53,7 +53,7 @@ class CreateShortcutMessageTest extends Specification { } "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 pkt mustEqual stringMedkit @@ -90,8 +90,8 @@ class CreateShortcutMessageTest extends Specification { ImplantType.DarklightVision.shortcut.tile mustEqual "darklight_vision" ImplantType.Targeting.shortcut.code mustEqual 2 ImplantType.Targeting.shortcut.tile mustEqual "targeting" - Shortcut.Medkit().code mustEqual 0 - Shortcut.Medkit().tile mustEqual "medkit" + Shortcut.Medkit.code mustEqual 0 + Shortcut.Medkit.tile mustEqual "medkit" ImplantType.MeleeBooster.shortcut.code mustEqual 2 ImplantType.MeleeBooster.shortcut.tile mustEqual "melee_booster" ImplantType.PersonalShield.shortcut.code mustEqual 2