diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala index 33abb72f3..b41de7dd5 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -6,8 +6,11 @@ import net.psforever.actors.session.SessionActor import net.psforever.actors.session.normal.NormalMode import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.objects.Session +import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} -import net.psforever.services.chat.DefaultChannel +import net.psforever.services.Service +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.{ChatChannel, DefaultChannel, SpectatorChannel} import net.psforever.types.ChatMessageType object ChatLogic { @@ -21,6 +24,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext ops.SpectatorMode = SpectateAsCustomerServiceRepresentativeMode + private var comms: ChatChannel = DefaultChannel + private var seeSpectatorsIn: Option[Zone] = None + def handleChatMsg(message: ChatMsg): Unit = { import net.psforever.types.ChatMessageType._ val isAlive = if (player != null) player.isAlive else false @@ -74,40 +80,40 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext ops.commandCaptureBase(session, message, contents) case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) => - ops.commandSendToRecipient(session, message, DefaultChannel) + ops.commandSendToRecipient(session, message, comms) case (CMT_GMTELL, _, _) => - ops.commandSend(session, message, DefaultChannel) + ops.commandSend(session, message, comms) case (CMT_GMBROADCASTPOPUP, _, _) => - ops.commandSendToRecipient(session, message, DefaultChannel) + ops.commandSendToRecipient(session, message, comms) case (CMT_OPEN, _, _) if !player.silenced => - ops.commandSendToRecipient(session, message, DefaultChannel) + ops.commandSendToRecipient(session, message, comms) case (CMT_VOICE, _, contents) => - ops.commandVoice(session, message, contents, DefaultChannel) + ops.commandVoice(session, message, contents, comms) case (CMT_TELL, _, _) if !player.silenced => - ops.commandTellOrIgnore(session, message, DefaultChannel) + ops.commandTellOrIgnore(session, message, comms) case (CMT_BROADCAST, _, _) if !player.silenced => - ops.commandSendToRecipient(session, message, DefaultChannel) + ops.commandSendToRecipient(session, message, comms) case (CMT_PLATOON, _, _) if !player.silenced => - ops.commandSendToRecipient(session, message, DefaultChannel) + ops.commandSendToRecipient(session, message, comms) case (CMT_COMMAND, _, _) => - ops.commandSendToRecipient(session, message, DefaultChannel) + ops.commandSendToRecipient(session, message, comms) case (CMT_NOTE, _, _) => - ops.commandSend(session, message, DefaultChannel) + ops.commandSend(session, message, comms) case (CMT_SILENCE, _, _) => - ops.commandSend(session, message, DefaultChannel) + ops.commandSend(session, message, comms) case (CMT_SQUAD, _, _) => - ops.commandSquad(session, message, DefaultChannel) //todo SquadChannel, but what is the guid + ops.commandSquad(session, message, comms) //todo SquadChannel, but what is the guid case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => ops.commandWho(session) @@ -197,6 +203,10 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "ntu" => ops.customCommandNtu(session, params) case "zonerotate" => ops.customCommandZonerotate(params) case "nearby" => ops.customCommandNearby(session) + case "togglespectators" => customCommandToggleSpectators(params) + case "showspectators" => customCommandShowSpectators() + case "hidespectators" => customCommandHideSpectators() + case "sayspectator" => customCommandSpeakAsSpectator(params, message) case _ => // command was not handled sendResponse( @@ -240,4 +250,68 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } true } + + private def customCommandToggleSpectators(contents: Seq[String]): Boolean = { + contents + .headOption + .map(_.toLowerCase()) + .collect { + case "on" | "o" | "" if !seeSpectatorsIn.contains(continent) => + customCommandShowSpectators() + case "off" | "of" if seeSpectatorsIn.contains(continent) => + customCommandHideSpectators() + case _ => () + } + true + } + + private def customCommandShowSpectators(): Boolean = { + val channel = player.Name + val events = continent.AvatarEvents + seeSpectatorsIn = Some(continent) + events ! Service.Join(s"spectator") + continent + .AllPlayers + .filter(_.spectator) + .foreach { spectator => + val guid = spectator.GUID + val definition = spectator.Definition + events ! AvatarServiceMessage( + channel, + AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(spectator).get, None) + ) + } + true + } + + private def customCommandHideSpectators(): Boolean = { + val channel = player.Name + val events = continent.AvatarEvents + seeSpectatorsIn = None + events ! Service.Leave(Some("spectator")) + continent + .AllPlayers + .filter(_.spectator) + .foreach { spectator => + val guid = spectator.GUID + events ! AvatarServiceMessage( + channel, + AvatarAction.ObjectDelete(guid, guid) + ) + } + true + } + + private def customCommandSpeakAsSpectator(params: Seq[String], message: ChatMsg): Boolean = { + comms = SpectatorChannel + handleChatMsg(message.copy(contents = params.mkString(" "))) + comms = DefaultChannel + true + } + + override def stop(): Unit = { + super.stop() + seeSpectatorsIn.foreach(_ => customCommandHideSpectators()) + seeSpectatorsIn = None + } } diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index 08b409543..0640ceb4a 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -151,30 +151,33 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ops.unaccessContainer(container) case _ => () } - if (!player.spectator) { + val channel = if (!player.spectator) { sessionLogic.updateBlockMap(player, pos) - val eagleEye: Boolean = ops.canSeeReallyFar - val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || - (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlayerState( - avatarGuid, - player.Position, - player.Velocity, - yaw, - pitch, - yawUpper, - seqTime, - isCrouching, - isJumping, - jumpThrust, - isCloaking, - isNotVisible, - eagleEye - ) - ) + continent.id + } else { + "spectator" } + val eagleEye: Boolean = ops.canSeeReallyFar + val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || + (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) + continent.AvatarEvents ! AvatarServiceMessage( + channel, + AvatarAction.PlayerState( + avatarGuid, + player.Position, + player.Velocity, + yaw, + pitch, + yawUpper, + seqTime, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + isNotVisible, + eagleEye + ) + ) sessionLogic.squad.updateSquad() } @@ -294,55 +297,60 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case _ => None } - sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { - case Some(door: Door) => - handleUseDoor(door, equipment) - case Some(resourceSilo: ResourceSilo) => - ops.handleUseResourceSilo(resourceSilo, equipment) - case Some(panel: IFFLock) => - ops.handleUseGeneralEntity(panel, equipment) - case Some(obj: Player) => - ops.handleUsePlayer(obj, equipment, pkt) - case Some(locker: Locker) => - ops.handleUseLocker(locker, equipment, pkt) - case Some(gen: Generator) => - ops.handleUseGeneralEntity(gen, equipment) - case Some(mech: ImplantTerminalMech) => - ops.handleUseGeneralEntity(mech, equipment) - case Some(captureTerminal: CaptureTerminal) => - ops.handleUseCaptureTerminal(captureTerminal, equipment) - case Some(obj: FacilityTurret) => - ops.handleUseFacilityTurret(obj, equipment, pkt) - case Some(obj: Vehicle) => - ops.handleUseVehicle(obj, equipment, pkt) - case Some(terminal: Terminal) => - ops.handleUseTerminal(terminal, equipment, pkt) - case Some(obj: SpawnTube) => - ops.handleUseSpawnTube(obj, equipment) - case Some(obj: SensorDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: TurretDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: TrapDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: ShieldGeneratorDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: TelepadDeployable) if player.spectator => - ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystemSecretly) - case Some(obj: Utility.InternalTelepad) if player.spectator => - ops.handleUseInternalTelepad (obj, pkt, ops.useRouterTelepadSystemSecretly) - case Some(obj: TelepadDeployable) => - ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) - case Some(obj: Utility.InternalTelepad) => - ops.handleUseInternalTelepad (obj, pkt, ops.useRouterTelepadSystem) - case Some(obj: CaptureFlag) => - ops.handleUseCaptureFlag(obj) - case Some(_: WarpGate) => - ops.handleUseWarpGate(equipment) - case Some(obj) => - ops.handleUseDefaultEntity(obj, equipment) - case None => () - } + cancelZoningWhenGeneralHandled( + sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { + case Some(door: Door) => + handleUseDoor(door, equipment) + GeneralOperations.UseItem.Unhandled + case Some(resourceSilo: ResourceSilo) => + ops.handleUseResourceSilo(resourceSilo, equipment) + case Some(panel: IFFLock) => + ops.handleUseGeneralEntity(panel, equipment) + case Some(obj: Player) => + ops.handleUsePlayer(obj, equipment, pkt) + GeneralOperations.UseItem.Unhandled + case Some(locker: Locker) => + ops.handleUseLocker(locker, equipment, pkt) + case Some(gen: Generator) => + ops.handleUseGeneralEntity(gen, equipment) + case Some(mech: ImplantTerminalMech) => + ops.handleUseGeneralEntity(mech, equipment) + case Some(captureTerminal: CaptureTerminal) => + ops.handleUseCaptureTerminal(captureTerminal, equipment) + case Some(obj: FacilityTurret) => + ops.handleUseFacilityTurret(obj, equipment, pkt) + case Some(obj: Vehicle) => + ops.handleUseVehicle(obj, equipment, pkt) + case Some(terminal: Terminal) => + ops.handleUseTerminal(terminal, equipment, pkt) + case Some(obj: SpawnTube) => + ops.handleUseSpawnTube(obj, equipment) + case Some(obj: SensorDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TurretDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TrapDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: ShieldGeneratorDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TelepadDeployable) if player.spectator => + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystemSecretly) + case Some(obj: Utility.InternalTelepad) if player.spectator => + ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystemSecretly) + case Some(obj: TelepadDeployable) => + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) + case Some(obj: Utility.InternalTelepad) => + ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem) + case Some(obj: CaptureFlag) => + ops.handleUseCaptureFlag(obj) + case Some(_: WarpGate) => + ops.handleUseWarpGate(equipment) + case Some(obj) => + ops.handleUseDefaultEntity(obj, equipment) + case None => + GeneralOperations.UseItem.Unhandled + } + ) } def handleUnuseItem(pkt: UnuseItemMessage): Unit = { @@ -767,4 +775,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.CapacitorState = CapacitorStateType.Idle } } + + private def cancelZoningWhenGeneralHandled(results: GeneralOperations.UseItem.Behavior): Unit = { + results match { + case GeneralOperations.UseItem.Unhandled => () + case _ => sessionLogic.zoning.CancelZoningProcess() + } + } } diff --git a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala index 0cff4d5da..2903a75e3 100644 --- a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala @@ -7,6 +7,7 @@ import net.psforever.objects.serverobject.ServerObject import net.psforever.objects.{Session, Vehicle} import net.psforever.packet.PlanetSidePacket import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.SpectatorChannel import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage} import net.psforever.types.{ChatMessageType, SquadRequestType} // @@ -52,6 +53,7 @@ class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { player.spectator = true //player.bops = true player.allowInteraction = false + data.chat.JoinChannel(SpectatorChannel) continent.actor ! ZoneActor.RemoveFromBlockMap(player) continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid)) sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE ON")) @@ -67,6 +69,7 @@ class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { player.spectator = false player.bops = false player.allowInteraction = true + data.chat.LeaveChannel(SpectatorChannel) data.continent.actor ! ZoneActor.AddToBlockMap(player, player.Position) continent.AvatarEvents ! AvatarServiceMessage( continent.id, diff --git a/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala index 8f6ee7605..36ee884ab 100644 --- a/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala @@ -5,16 +5,11 @@ import akka.actor.{ActorContext, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions} import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory} -import net.psforever.objects.definition.VehicleDefinition -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.guid.TaskWorkflow -import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal} -import net.psforever.objects.sourcing.AmenitySource -import net.psforever.objects.vital.TerminalUsedActivity -import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} -import net.psforever.types.{TransactionType, Vector3} -import net.psforever.util.DefinitionUtil +import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage} +import net.psforever.types.TransactionType object TerminalHandlerLogic { def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = { @@ -28,7 +23,7 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { - if ( player.spectator) { + if (player.spectator) { val ItemTransactionMessage(terminal_guid, _, _, _, _, _) = pkt sessionLogic.zoning.CancelZoningProcess() continent diff --git a/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala index cf01a73d0..3b2d780d2 100644 --- a/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala @@ -3,21 +3,13 @@ package net.psforever.actors.session.csr import akka.actor.ActorContext import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} -import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory} -import net.psforever.objects.equipment.ChargeFireModeDefinition import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.{BoomerDeployable, BoomerTrigger, GlobalDefinitions, Player, SpecialEmp, Tool, Tools, Vehicle} -import net.psforever.objects.serverobject.turret.{FacilityTurret, VanuSentry} +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle} import net.psforever.objects.vital.base.{DamageResolution, DamageType} import net.psforever.objects.zones.{Zone, ZoneProjectile} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{PlanetSideGUID, Vector3} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.types.Vector3 object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { @@ -55,15 +47,15 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit if (ops.shooting.isEmpty) { sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStartWhenPlayer(tool, item_guid) + ops.fireStateStartWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStartWhenMounted(tool, item_guid) + ops.fireStateStartWhenMounted(tool, item_guid) case Some(_) if player.VehicleSeated.isEmpty => - fireStateStartSetup(item_guid) - fireStateStartPlayerMessages(item_guid) + ops.fireStateStartSetup(item_guid) + ops.fireStateStartPlayerMessages(item_guid) case Some(_) => - fireStateStartSetup(item_guid) - fireStateStartMountedMessages(item_guid) + ops.fireStateStartSetup(item_guid) + ops.fireStateStartMountedMessages(item_guid) case None => log.warn(s"ChangeFireState_Start: can not find $item_guid") } @@ -78,9 +70,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ops.shooting -= item_guid sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStopWhenPlayer(tool, item_guid) + ops.fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStopWhenMounted(tool, item_guid) + ops.fireStateStopWhenMounted(tool, item_guid) case Some(trigger: BoomerTrigger) => ops.fireStateStopPlayerMessages(item_guid) continent.GUID(trigger.Companion).collect { @@ -102,9 +94,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val ReloadMessage(item_guid, _, unk1) = pkt ops.FindContainedWeapon match { case (Some(obj: Player), tools) => - handleReloadWhenPlayer(item_guid, obj, tools, unk1) + ops.handleReloadWhenPlayer(item_guid, obj, tools, unk1) case (Some(obj: PlanetSideServerObject with Container), tools) => - handleReloadWhenMountable(item_guid, obj, tools, unk1) + ops.handleReloadWhenMountable(item_guid, obj, tools, unk1) case (_, _) => log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") } @@ -213,204 +205,4 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } } } - - /* support code */ - - /** - * After a weapon has finished shooting, determine if it needs to be sorted in a special way. - * @param tool a weapon - */ - private def FireCycleCleanup(tool: Tool): Unit = { - //TODO replaced by more appropriate functionality in the future - val tdef = tool.Definition - if (GlobalDefinitions.isGrenade(tdef)) { - val ammoType = tool.AmmoType - FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters - case Nil => - RemoveOldEquipmentFromInventory(player)(tool) - - case x :: xs => //this is similar to ReloadMessage - val box = x.obj.asInstanceOf[Tool] - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } - val sumReloadValue: Int = box.Magazine + tailReloadValue - val actualReloadValue = if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player)(x.obj) - sumReloadValue - } else { - ops.modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - } - ops.modifyAmmunition(player)( - tool.AmmoSlot.Box, - -actualReloadValue - ) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) - } - } else if (tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player)(tool) - } - } - - /* - used by ChangeFireStateMessage_Start handling - */ - private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { - ops.prefire -= itemGuid - ops.shooting += itemGuid - ops.shootingStart += itemGuid -> System.currentTimeMillis() - } - - private def fireStateStartChargeMode(tool: Tool): Unit = { - //charge ammunition drain - tool.FireMode match { - case mode: ChargeFireModeDefinition => - sessionLogic.general.progressBarValue = Some(0f) - sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( - (mode.Time + mode.DrainInterval) milliseconds, - context.self, - CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) - ) - case _ => () - } - } - - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - if (!player.spectator) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - } - - private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionLogic.findContainedEquipment()._1.collect { - case turret: FacilityTurret if continent.map.cavern => - turret.Actor ! VanuSentry.ChangeFireStart - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - private def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { - tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || ops.prefire.contains(itemGuid) - } - - private def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - log.warn( - s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" - ) - ops.emptyMagazine(itemGuid, tool) - } - - private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - //special case - suppress the decimator's alternate fire mode, by projectile - if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - fireStateStartMountedMessages(itemGuid) - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - /* - used by ChangeFireStateMessage_Stop handling - */ - private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => () - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - } - - private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - //suppress the decimator's alternate fire mode, however - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopPlayerMessages(itemGuid) - } - - private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopMountedMessages(itemGuid) - } - - /* - used by ReloadMessage handling - */ - private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - if (!player.spectator) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, itemGuid) - ) - } - } - - private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.Reload(player.GUID, itemGuid) - ) - } - - private def handleReloadWhenPlayer( - itemGuid: PlanetSideGUID, - obj: Player, - tools: Set[Tool], - unk1: Int - ): Unit = { - ops.handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ops.modifyAmmunition(obj)(_, _), - reloadPlayerMessages - ) - } - - private def handleReloadWhenMountable( - itemGuid: PlanetSideGUID, - obj: PlanetSideServerObject with Container, - tools: Set[Tool], - unk1: Int - ): Unit = { - ops.handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ops.modifyAmmunitionInMountable(obj)(_, _), - reloadVehicleMessages - ) - } } diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 627cf62d3..0a4993876 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -333,51 +333,56 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case _ => None } - sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { - case Some(door: Door) => - ops.handleUseDoor(door, equipment) - case Some(resourceSilo: ResourceSilo) => - ops.handleUseResourceSilo(resourceSilo, equipment) - case Some(panel: IFFLock) => - ops.handleUseGeneralEntity(panel, equipment) - case Some(obj: Player) => - ops.handleUsePlayer(obj, equipment, pkt) - case Some(locker: Locker) => - ops.handleUseLocker(locker, equipment, pkt) - case Some(gen: Generator) => - ops.handleUseGeneralEntity(gen, equipment) - case Some(mech: ImplantTerminalMech) => - ops.handleUseGeneralEntity(mech, equipment) - case Some(captureTerminal: CaptureTerminal) => - ops.handleUseCaptureTerminal(captureTerminal, equipment) - case Some(obj: FacilityTurret) => - ops.handleUseFacilityTurret(obj, equipment, pkt) - case Some(obj: Vehicle) => - ops.handleUseVehicle(obj, equipment, pkt) - case Some(terminal: Terminal) => - ops.handleUseTerminal(terminal, equipment, pkt) - case Some(obj: SpawnTube) => - ops.handleUseSpawnTube(obj, equipment) - case Some(obj: SensorDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: TurretDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: TrapDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: ShieldGeneratorDeployable) => - ops.handleUseGeneralEntity(obj, equipment) - case Some(obj: TelepadDeployable) => - ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) - case Some(obj: Utility.InternalTelepad) => - ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem) - case Some(obj: CaptureFlag) => - ops.handleUseCaptureFlag(obj) - case Some(_: WarpGate) => - ops.handleUseWarpGate(equipment) - case Some(obj) => - ops.handleUseDefaultEntity(obj, equipment) - case None => () - } + cancelZoningWhenGeneralHandled( + sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { + case Some(door: Door) => + ops.handleUseDoor(door, equipment) + GeneralOperations.UseItem.Unhandled + case Some(resourceSilo: ResourceSilo) => + ops.handleUseResourceSilo(resourceSilo, equipment) + case Some(panel: IFFLock) => + ops.handleUseGeneralEntity(panel, equipment) + case Some(obj: Player) => + ops.handleUsePlayer(obj, equipment, pkt) + GeneralOperations.UseItem.Unhandled + case Some(locker: Locker) => + ops.handleUseLocker(locker, equipment, pkt) + case Some(gen: Generator) => + ops.handleUseGeneralEntity(gen, equipment) + case Some(mech: ImplantTerminalMech) => + ops.handleUseGeneralEntity(mech, equipment) + case Some(captureTerminal: CaptureTerminal) => + ops.handleUseCaptureTerminal(captureTerminal, equipment) + case Some(obj: FacilityTurret) => + ops.handleUseFacilityTurret(obj, equipment, pkt) + case Some(obj: Vehicle) => + ops.handleUseVehicle(obj, equipment, pkt) + case Some(terminal: Terminal) => + ops.handleUseTerminal(terminal, equipment, pkt) + case Some(obj: SpawnTube) => + ops.handleUseSpawnTube(obj, equipment) + case Some(obj: SensorDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TurretDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TrapDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: ShieldGeneratorDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TelepadDeployable) => + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) + case Some(obj: Utility.InternalTelepad) => + ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem) + case Some(obj: CaptureFlag) => + ops.handleUseCaptureFlag(obj) + case Some(_: WarpGate) => + ops.handleUseWarpGate(equipment) + case Some(obj) => + ops.handleUseDefaultEntity(obj, equipment) + case None => + GeneralOperations.UseItem.Unhandled + } + ) } def handleUnuseItem(pkt: UnuseItemMessage): Unit = { @@ -971,4 +976,14 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) ) } + + private def cancelZoningWhenGeneralHandled(results: GeneralOperations.UseItem.Behavior): Unit = { + results match { + case GeneralOperations.UseItem.Handled => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + case GeneralOperations.UseItem.HandledPassive => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") + case _ => () + } + } } diff --git a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala index 69824b026..5c766a279 100644 --- a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala @@ -2,24 +2,14 @@ package net.psforever.actors.session.normal import akka.actor.ActorContext -//import akka.actor.typed -//import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} -import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory} -import net.psforever.objects.equipment.ChargeFireModeDefinition import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.{BoomerDeployable, BoomerTrigger, GlobalDefinitions, Player, SpecialEmp, Tool, Tools, Vehicle} -import net.psforever.objects.serverobject.turret.{FacilityTurret, VanuSentry} +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle} import net.psforever.objects.vital.base.{DamageResolution, DamageType} import net.psforever.objects.zones.{Zone, ZoneProjectile} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{PlanetSideGUID, Vector3} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.types.Vector3 object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { @@ -74,15 +64,15 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit if (ops.shooting.isEmpty) { sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStartWhenPlayer(tool, item_guid) + ops.fireStateStartWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStartWhenMounted(tool, item_guid) + ops.fireStateStartWhenMounted(tool, item_guid) case Some(_) if player.VehicleSeated.isEmpty => - fireStateStartSetup(item_guid) - fireStateStartPlayerMessages(item_guid) + ops.fireStateStartSetup(item_guid) + ops.fireStateStartPlayerMessages(item_guid) case Some(_) => - fireStateStartSetup(item_guid) - fireStateStartMountedMessages(item_guid) + ops.fireStateStartSetup(item_guid) + ops.fireStateStartMountedMessages(item_guid) case None => log.warn(s"ChangeFireState_Start: can not find $item_guid") } @@ -97,9 +87,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ops.shooting -= item_guid sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStopWhenPlayer(tool, item_guid) + ops.fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStopWhenMounted(tool, item_guid) + ops.fireStateStopWhenMounted(tool, item_guid) case Some(trigger: BoomerTrigger) => ops.fireStateStopPlayerMessages(item_guid) continent.GUID(trigger.Companion).collect { @@ -121,9 +111,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val ReloadMessage(item_guid, _, unk1) = pkt ops.FindContainedWeapon match { case (Some(obj: Player), tools) => - handleReloadWhenPlayer(item_guid, obj, tools, unk1) + ops.handleReloadWhenPlayer(item_guid, obj, tools, unk1) case (Some(obj: PlanetSideServerObject with Container), tools) => - handleReloadWhenMountable(item_guid, obj, tools, unk1) + ops.handleReloadWhenMountable(item_guid, obj, tools, unk1) case (_, _) => log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") } @@ -239,198 +229,4 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } } } - - /* support code */ - - /** - * After a weapon has finished shooting, determine if it needs to be sorted in a special way. - * @param tool a weapon - */ - private def FireCycleCleanup(tool: Tool): Unit = { - //TODO replaced by more appropriate functionality in the future - val tdef = tool.Definition - if (GlobalDefinitions.isGrenade(tdef)) { - val ammoType = tool.AmmoType - FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters - case Nil => - log.info(s"${player.Name} has no more $ammoType grenades to throw") - RemoveOldEquipmentFromInventory(player)(tool) - - case x :: xs => //this is similar to ReloadMessage - val box = x.obj.asInstanceOf[Tool] - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } - val sumReloadValue: Int = box.Magazine + tailReloadValue - val actualReloadValue = if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player)(x.obj) - sumReloadValue - } else { - ops.modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - } - log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ops.modifyAmmunition(player)( - tool.AmmoSlot.Box, - -actualReloadValue - ) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) - } - } else if (tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player)(tool) - } - } - - /* - used by ChangeFireStateMessage_Start handling - */ - private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { - ops.prefire -= itemGuid - ops.shooting += itemGuid - ops.shootingStart += itemGuid -> System.currentTimeMillis() - } - - private def fireStateStartChargeMode(tool: Tool): Unit = { - //charge ammunition drain - tool.FireMode match { - case mode: ChargeFireModeDefinition => - sessionLogic.general.progressBarValue = Some(0f) - sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( - (mode.Time + mode.DrainInterval) milliseconds, - context.self, - CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) - ) - case _ => () - } - } - - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionLogic.findContainedEquipment()._1.collect { - case turret: FacilityTurret if continent.map.cavern => - turret.Actor ! VanuSentry.ChangeFireStart - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - private def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - log.warn( - s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" - ) - ops.emptyMagazine(itemGuid, tool) - } - - private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (ops.allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - //special case - suppress the decimator's alternate fire mode, by projectile - if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (ops.allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - fireStateStartMountedMessages(itemGuid) - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - /* - used by ChangeFireStateMessage_Stop handling - */ - private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => () - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - } - - private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - //suppress the decimator's alternate fire mode, however - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopPlayerMessages(itemGuid) - } - - private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopMountedMessages(itemGuid) - } - - /* - used by ReloadMessage handling - */ - private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, itemGuid) - ) - } - - private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.Reload(player.GUID, itemGuid) - ) - } - - private def handleReloadWhenPlayer( - itemGuid: PlanetSideGUID, - obj: Player, - tools: Set[Tool], - unk1: Int - ): Unit = { - ops.handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ops.modifyAmmunition(obj)(_, _), - reloadPlayerMessages - ) - } - - private def handleReloadWhenMountable( - itemGuid: PlanetSideGUID, - obj: PlanetSideServerObject with Container, - tools: Set[Tool], - unk1: Int - ): Unit = { - ops.handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ops.modifyAmmunitionInMountable(obj)(_, _), - reloadVehicleMessages - ) - } } 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 67a569c23..f09d66b5f 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -221,7 +221,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.HitHint(sourceGuid) if player.isAlive => sendResponse(HitHint(sourceGuid, guid)) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + sessionLogic.zoning.CancelZoningProcess() case AvatarResponse.Destroy(victim, killer, weapon, pos) => // guid = victim // killer = killer @@ -447,7 +447,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A ) sessionLogic.shooting.shotsWhileDead = 0 } - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") + sessionLogic.zoning.CancelZoningProcess() //player state changes AvatarActor.updateToolDischargeFor(avatar) @@ -510,7 +510,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.EnvironmentalDamage(_, _, _) => //TODO damage marker? - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + sessionLogic.zoning.CancelZoningProcess() case AvatarResponse.DropItem(pkt) if isNotSameTarget => sendResponse(pkt) 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 ce287f6fc..c5c6c243c 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -55,19 +55,19 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext commandToggleSpectatorMode(contents = "off") case (CMT_OPEN, _, _) => - ops.commandSendToRecipient(session, message, SpectatorChannel) + ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_VOICE, _, contents) => ops.commandVoice(session, message, contents, SpectatorChannel) case (CMT_TELL, _, _) => - ops.commandTellOrIgnore(session, message, SpectatorChannel) + ops.commandTellOrIgnore(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_BROADCAST, _, _) => - ops.commandSendToRecipient(session, message, SpectatorChannel) + ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_PLATOON, _, _) => - ops.commandSendToRecipient(session, message, SpectatorChannel) + ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_GMTELL, _, _) => ops.commandSend(session, message, SpectatorChannel) @@ -114,6 +114,16 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } } + private def spectatorColoredMessage(message: ChatMsg): ChatMsg = { + if (message.contents.nonEmpty) { + val colorlessText = message.contents.replaceAll("//#\\d", "").trim + val colorCodedText = s"/#5$colorlessText/#0" + message.copy(recipient = s"", contents = colorCodedText) + } else { + message + } + } + private def customCommandMessages( message: ChatMsg, session: Session 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 28e89a6c3..c8a32df66 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -47,7 +47,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex yaw, pitch, yawUpper, - _/*seqTime*/, + seqTime, _, isCrouching, isJumping, @@ -69,6 +69,24 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.Crouching = isCrouching player.Jumping = isJumping player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking + continent.AvatarEvents ! AvatarServiceMessage( + "spectator", + AvatarAction.PlayerState( + avatarGuid, + player.Position, + player.Velocity, + yaw, + pitch, + yawUpper, + seqTime, + isCrouching, + isJumping, + jump_thrust = false, + is_cloaked = isCloaking, + spectator = false, + weaponInHand = false + ) + ) if (player.death_by == -1) { sessionLogic.kickedByAdministration() } 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 89c0321e4..cc65f07a8 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala @@ -118,6 +118,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0")) } // + player.spectator = true data.chat.JoinChannel(SpectatorChannel) val newPlayer = SpectatorModeLogic.spectatorCharacter(player) newPlayer.LogActivity(player.History.headOption) @@ -151,6 +152,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { val pguid = player.GUID val sendResponse: PlanetSidePacket => Unit = data.sendResponse // + player.spectator = false data.general.stop() player.avatar.shortcuts.slice(1, 4) .zipWithIndex @@ -176,7 +178,7 @@ object SpectatorModeLogic { private def spectatorCharacter(player: Player): Player = { val avatar = player.avatar val newAvatar = avatar.copy( - basic = avatar.basic.copy(name = "spectator"), + basic = avatar.basic, bep = BattleRank.BR18.experience, cep = CommandRank.CR5.experience, certifications = Set(), diff --git a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala index c6992abb2..89a95dac1 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala @@ -3,13 +3,9 @@ package net.psforever.actors.session.spectator import akka.actor.ActorContext import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} -import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory} -import net.psforever.objects.equipment.ChargeFireModeDefinition import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.{BoomerDeployable, BoomerTrigger, GlobalDefinitions, Tool} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} -import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.types.PlanetSideGUID +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Tool} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { @@ -57,9 +53,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ops.shooting -= item_guid sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStopWhenPlayer(tool, item_guid) + ops.fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStopWhenMounted(tool, item_guid) + ops.fireStateStopWhenMounted(tool, item_guid) case Some(trigger: BoomerTrigger) => ops.fireStateStopPlayerMessages(item_guid) continent.GUID(trigger.Companion).collect { @@ -95,83 +91,4 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ } def handleAIDamage(pkt: AIDamage): Unit = { /* intentionally blank */ } - - /* support code */ - - /** - * After a weapon has finished shooting, determine if it needs to be sorted in a special way. - * @param tool a weapon - */ - private def FireCycleCleanup(tool: Tool): Unit = { - //TODO replaced by more appropriate functionality in the future - val tdef = tool.Definition - if (GlobalDefinitions.isGrenade(tdef)) { - val ammoType = tool.AmmoType - FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters - case Nil => - log.info(s"${player.Name} has no more $ammoType grenades to throw") - RemoveOldEquipmentFromInventory(player)(tool) - - case x :: xs => //this is similar to ReloadMessage - val box = x.obj.asInstanceOf[Tool] - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } - val sumReloadValue: Int = box.Magazine + tailReloadValue - val actualReloadValue = if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player)(x.obj) - sumReloadValue - } else { - ops.modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - } - log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ops.modifyAmmunition(player)( - tool.AmmoSlot.Box, - -actualReloadValue - ) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) - } - } else if (tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player)(tool) - } - } - - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - /* - used by ChangeFireStateMessage_Stop handling - */ - private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => () - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - } - - private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - //suppress the decimator's alternate fire mode, however - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopPlayerMessages(itemGuid) - } - - private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopMountedMessages(itemGuid) - } } 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 f0d313547..44e92efe6 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -1081,7 +1081,7 @@ class GeneralOperations( TaskWorkflow.execute(CallBackForTask(tasking, zone.Deployables, zoneBuildCommand, context.self)) } - def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { + def handleUseDoor(door: Door, equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { equipment match { case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => val distance: Float = math.max( @@ -1092,72 +1092,88 @@ class GeneralOperations( case _ => door.Actor ! CommonMessages.Use(player) } + GeneralOperations.UseItem.Handled } - def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { val vehicleOpt = continent.GUID(player.avatar.vehicle) (vehicleOpt, equipment) match { case (Some(vehicle: Vehicle), Some(item)) if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) + GeneralOperations.UseItem.Handled case (Some(vehicle: Vehicle), _) if vehicle.Definition == GlobalDefinitions.ant && vehicle.DeploymentState == DriveState.Deployed && Vector3.DistanceSquared(resourceSilo.Position.xy, vehicle.Position.xy) < math.pow(resourceSilo.Definition.UseRadius, 2) => resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) - case _ => () + GeneralOperations.UseItem.Handled + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): GeneralOperations.UseItem.Behavior = { if (obj.isBackpack) { if (equipment.isEmpty) { log.info(s"${player.Name} is looting the corpse of ${obj.Name}") sendResponse(msg) accessContainer(obj) + GeneralOperations.UseItem.Handled + } else { + GeneralOperations.UseItem.Unhandled } } else if (!msg.unk3 && player.isAlive) { //potential kit use (continent.GUID(msg.item_used_guid), kitToBeUsed) match { case (Some(kit: Kit), None) => kitToBeUsed = Some(msg.item_used_guid) player.Actor ! CommonMessages.Use(player, Some(kit)) + GeneralOperations.UseItem.Handled case (Some(_: Kit), Some(_)) | (None, Some(_)) => //a kit is already queued to be used; ignore this request sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None)) + GeneralOperations.UseItem.Unhandled case (Some(item), _) => log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") + GeneralOperations.UseItem.Unhandled case (None, None) => - log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") } + log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") + GeneralOperations.UseItem.Unhandled + } } else if (msg.object_id == ObjectClass.avatar && msg.unk3) { equipment match { case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => obj.Actor ! CommonMessages.Use(player, equipment) - + GeneralOperations.UseItem.Handled case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => obj.Actor ! CommonMessages.Use(player, equipment) - case _ => () + GeneralOperations.UseItem.Handled + case _ => + GeneralOperations.UseItem.Unhandled } + } else { + GeneralOperations.UseItem.Unhandled } } - def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): GeneralOperations.UseItem.Behavior = { equipment match { case Some(item) => sendUseGeneralEntityMessage(locker, item) + GeneralOperations.UseItem.Unhandled case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => log.info(s"${player.Name} is accessing a locker") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") val playerLocker = player.avatar.locker sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) accessContainer(playerLocker) - case _ => () + GeneralOperations.UseItem.Handled + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = { + def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { equipment match { case Some(item) => sendUseGeneralEntityMessage(captureTerminal, item) @@ -1166,25 +1182,33 @@ class GeneralOperations( case Some(llu: CaptureFlag) => if (llu.Target.GUID == captureTerminal.Owner.GUID) { continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) + GeneralOperations.UseItem.Handled } else { log.info( s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}" ) } - case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") + GeneralOperations.UseItem.Unhandled + case _ => + log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") + GeneralOperations.UseItem.Unhandled } - case _ => () + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment.foreach { item => - sendUseGeneralEntityMessage(obj, item) - obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path - } + def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): GeneralOperations.UseItem.Behavior = { + equipment + .collect { item => + sendUseGeneralEntityMessage(obj, item) + obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path + GeneralOperations.UseItem.Handled + } + .getOrElse(GeneralOperations.UseItem.Unhandled) } - def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): GeneralOperations.UseItem.Behavior = { equipment match { case Some(item) => sendUseGeneralEntityMessage(obj, item) @@ -1196,29 +1220,33 @@ class GeneralOperations( .contains(player.GUID)) ) { log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") obj.AccessingTrunk = player.GUID accessContainer(obj) sendResponse(msg) + GeneralOperations.UseItem.Handled + } else { + GeneralOperations.UseItem.Unhandled } - case _ => () + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): GeneralOperations.UseItem.Behavior = { equipment match { case Some(item) => sendUseGeneralEntityMessage(terminal, item) + GeneralOperations.UseItem.Handled case None if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => val tdef = terminal.Definition if (tdef.isInstanceOf[MatrixTerminalDefinition]) { //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sendResponse( BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) ) + GeneralOperations.UseItem.Handled } else if ( tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal @@ -1230,45 +1258,49 @@ class GeneralOperations( ) sendResponse(msg) sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId)) + GeneralOperations.UseItem.Handled case None => log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") + GeneralOperations.UseItem.Unhandled } } else if (tdef == GlobalDefinitions.teleportpad_terminal) { //explicit request log.info(s"${player.Name} is purchasing a router telepad") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") terminal.Actor ! Terminal.Request( player, ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) ) + GeneralOperations.UseItem.Handled } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { //explicit request log.info(s"${player.Name} is purchasing a targeting laser") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") terminal.Actor ! Terminal.Request( player, ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0)) ) + GeneralOperations.UseItem.Handled } else { log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sendResponse(msg) + GeneralOperations.UseItem.Handled } - case _ => () + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = { + def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { equipment match { case Some(item) => sendUseGeneralEntityMessage(obj, item) case None if player.Faction == obj.Faction => //deconstruction - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.actionsToCancel() sessionLogic.terminals.CancelAllProximityUnits() sessionLogic.zoning.spawn.startDeconstructing(obj) - case _ => () + GeneralOperations.UseItem.Handled + case _ => + GeneralOperations.UseItem.Unhandled } } @@ -1277,7 +1309,7 @@ class GeneralOperations( equipment: Option[Equipment], msg: UseItemMessage, useTelepadFunc: (Vehicle, InternalTelepad, TelepadDeployable, PlanetSideGameObject with TelepadLike, PlanetSideGameObject with TelepadLike) => Unit - ): Unit = { + ): GeneralOperations.UseItem.Behavior = { if (equipment.isEmpty) { (continent.GUID(obj.Router) match { case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) @@ -1285,20 +1317,25 @@ class GeneralOperations( case None => None }) match { case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") player.WhichSide = vehicle.WhichSide useTelepadFunc(vehicle, util, obj, obj, util) + GeneralOperations.UseItem.HandledPassive case Some((vehicle: Vehicle, None)) => log.error( s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" ) + GeneralOperations.UseItem.Unhandled case Some((o, _)) => log.error( s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" ) obj.Actor ! Deployable.Deconstruct() - case _ => () + GeneralOperations.UseItem.Unhandled + case _ => + GeneralOperations.UseItem.Unhandled } + } else { + GeneralOperations.UseItem.Unhandled } } @@ -1306,16 +1343,19 @@ class GeneralOperations( obj: InternalTelepad, msg: UseItemMessage, useTelepadFunc: (Vehicle, InternalTelepad, TelepadDeployable, PlanetSideGameObject with TelepadLike, PlanetSideGameObject with TelepadLike) => Unit - ): Unit = { + ): GeneralOperations.UseItem.Behavior = { continent.GUID(obj.Telepad) match { case Some(pad: TelepadDeployable) => player.WhichSide = pad.WhichSide useTelepadFunc(obj.Owner.asInstanceOf[Vehicle], obj, pad, obj, pad) + GeneralOperations.UseItem.HandledPassive case Some(o) => log.error( s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" ) - case None => () + GeneralOperations.UseItem.Unhandled + case None => + GeneralOperations.UseItem.Unhandled } } @@ -1393,55 +1433,63 @@ class GeneralOperations( recentTeleportAttempt = time } - def handleUseCaptureFlag(obj: CaptureFlag): Unit = { + def handleUseCaptureFlag(obj: CaptureFlag): GeneralOperations.UseItem.Behavior = { // LLU can normally only be picked up the faction that owns it specialItemSlotGuid match { case None if obj.Faction == player.Faction => specialItemSlotGuid = Some(obj.GUID) player.Carrying = SpecialCarry.CaptureFlag continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) + GeneralOperations.UseItem.Handled case None => log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}") + GeneralOperations.UseItem.Unhandled case Some(guid) if guid != obj.GUID => // Ignore duplicate pickup requests log.warn( s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid" ) - case _ => () + GeneralOperations.UseItem.Unhandled + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUseWarpGate(equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + def handleUseWarpGate(equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { (continent.GUID(player.VehicleSeated), equipment) match { case (Some(vehicle: Vehicle), Some(item)) if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => vehicle.Actor ! CommonMessages.Use(player, equipment) - case _ => () + GeneralOperations.UseItem.Handled + case _ => + GeneralOperations.UseItem.Unhandled } } - def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { - equipment.foreach { item => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(item)) - } + def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { + equipment + .collect { item => + obj.Actor ! CommonMessages.Use(player, Some(item)) + GeneralOperations.UseItem.Handled + } + .getOrElse(GeneralOperations.UseItem.Unhandled) } - def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): GeneralOperations.UseItem.Behavior = { obj.Actor ! CommonMessages.Use(player, Some(equipment)) + GeneralOperations.UseItem.Handled } - def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): GeneralOperations.UseItem.Behavior = { equipment match { case Some(item) if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => () + GeneralOperations.UseItem.Handled case _ => log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") + GeneralOperations.UseItem.Unhandled } } @@ -1488,6 +1536,13 @@ class GeneralOperations( } object GeneralOperations { + object UseItem { + sealed trait Behavior + case object Handled extends Behavior + case object HandledPassive extends Behavior + case object Unhandled extends Behavior + } + object ItemDropState { sealed trait Behavior case object Dropped extends Behavior 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 a21a2e18e..98bf7d2d9 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -2,13 +2,13 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} -import net.psforever.login.WorldSession.{CountAmmunition, FindAmmoBoxThatUses, FindEquipmentStock, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop} +import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects.ballistics.ProjectileQuality import net.psforever.objects.definition.{ProjectileDefinition, SpecialExoSuitDefinition} import net.psforever.objects.entity.SimpleWorldEntity import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, FireModeSwitch} import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} -import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.doors.InteriorDoorPassage import net.psforever.objects.serverobject.interior.Sidedness @@ -347,7 +347,7 @@ class WeaponAndProjectileOperations( } sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) continent.AvatarEvents ! AvatarServiceMessage( - continent.id, + sessionLogic.zoning.zoneChannel, AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) ) } @@ -882,32 +882,220 @@ class WeaponAndProjectileOperations( */ def FindWeapon: Set[Tool] = FindContainedWeapon._2 + /* + used by ChangeFireStateMessage_Start handling + */ + def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { + prefire -= itemGuid + shooting += itemGuid + shootingStart += itemGuid -> System.currentTimeMillis() + } + + def fireStateStartChargeMode(tool: Tool): Unit = { + //charge ammunition drain + tool.FireMode match { + case mode: ChargeFireModeDefinition => + sessionLogic.general.progressBarValue = Some(0f) + sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( + (mode.Time + mode.DrainInterval) milliseconds, + context.self, + CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) + ) + case _ => () + } + } + def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(itemGuid) } - def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - if (!player.spectator) { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) - ) + def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + log.warn( + s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" + ) + emptyMagazine(itemGuid, tool) + } + + def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + //special case - suppress the decimator's alternate fire mode, by projectile + if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) } } - def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { - if (!player.spectator) { - sessionLogic.findContainedEquipment()._1.collect { - case turret: FacilityTurret if continent.map.cavern => - turret.Actor ! VanuSentry.ChangeFireStop - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid) - ) + def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + fireStateStartMountedMessages(itemGuid) + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) } } + def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + sessionLogic.zoning.zoneChannel, + AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { + sessionLogic.findContainedEquipment()._1.collect { + case turret: FacilityTurret if continent.map.cavern => + turret.Actor ! VanuSentry.ChangeFireStart + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + /* + used by ChangeFireStateMessage_Stop handling + */ + def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why + //suppress the decimator's alternate fire mode, however + if ( + tool.Definition == GlobalDefinitions.phoenix && + tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile + ) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStopUpdateChargeAndCleanup(tool) + fireStateStopPlayerMessages(itemGuid) + } + + def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + fireStateStopUpdateChargeAndCleanup(tool) + fireStateStopMountedMessages(itemGuid) + } + + def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + sessionLogic.zoning.zoneChannel, + AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) + ) + } + + def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { + sessionLogic.findContainedEquipment()._1.collect { + case turret: FacilityTurret if continent.map.cavern => + turret.Actor ! VanuSentry.ChangeFireStop + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid) + ) + } + + /** + * After a weapon has finished shooting, determine if it needs to be sorted in a special way. + * @param tool a weapon + */ + def fireCycleCleanup(tool: Tool): Unit = { + //TODO replaced by more appropriate functionality in the future + val tdef = tool.Definition + if (GlobalDefinitions.isGrenade(tdef)) { + val ammoType = tool.AmmoType + FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters + case Nil => + RemoveOldEquipmentFromInventory(player)(tool) + + case x :: xs => //this is similar to ReloadMessage + val box = x.obj.asInstanceOf[Tool] + val tailReloadValue: Int = if (xs.isEmpty) { 0 } + else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } + val sumReloadValue: Int = box.Magazine + tailReloadValue + val actualReloadValue = if (sumReloadValue <= 3) { + RemoveOldEquipmentFromInventory(player)(x.obj) + sumReloadValue + } else { + modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + 3 + } + modifyAmmunition(player)( + tool.AmmoSlot.Box, + -actualReloadValue + ) //grenade item already in holster (negative because empty) + xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) + } + } else if (tdef == GlobalDefinitions.phoenix) { + RemoveOldEquipmentFromInventory(player)(tool) + } + } + + def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { + tool.FireMode match { + case _: ChargeFireModeDefinition => + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) + case _ => () + } + if (tool.Magazine == 0) { + fireCycleCleanup(tool) + } + } + + + + /* + used by ReloadMessage handling + */ + def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + sessionLogic.zoning.zoneChannel, + AvatarAction.Reload(player.GUID, itemGuid) + ) + } + + def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.Reload(player.GUID, itemGuid) + ) + } + + def handleReloadWhenPlayer( + itemGuid: PlanetSideGUID, + obj: Player, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + modifyAmmunition(obj)(_, _), + reloadPlayerMessages + ) + } + + def handleReloadWhenMountable( + itemGuid: PlanetSideGUID, + obj: PlanetSideServerObject with Container, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + modifyAmmunitionInMountable(obj)(_, _), + reloadVehicleMessages + ) + } + private def addShotsFired(weaponId: Int, shots: Int): Unit = { addShotsToMap(shotsFired, weaponId, shots) } @@ -1045,7 +1233,7 @@ class WeaponAndProjectileOperations( val boxDef = box.Definition sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) continent.AvatarEvents ! AvatarServiceMessage( - continent.id, + sessionLogic.zoning.zoneChannel, AvatarAction.ChangeAmmo( player.GUID, tool_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 edd630c9d..0aa00bba0 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -194,6 +194,8 @@ class ZoningOperations( private[session] val spawn: SpawnOperations = new SpawnOperations() private[session] var maintainInitialGmState: Boolean = false + private[session] var zoneChannel: String = Zone.Nowhere.id + private var loadConfZone: Boolean = false private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None private var zoningType: Zoning.Method = Zoning.Method.None @@ -2129,6 +2131,7 @@ class ZoningOperations( val map = zone.map val mapName = map.name log.info(s"${tplayer.Name} has spawned into $id") + sessionLogic.zoning.zoneChannel = Players.ZoneChannelIfSpectating(tplayer, zone.id) sessionLogic.oldRefsMap.clear() sessionLogic.persist = UpdatePersistenceAndRefs tplayer.avatar = avatar @@ -2442,7 +2445,13 @@ class ZoningOperations( case _ if player.spectator => player.VehicleSeated = None + val definition = player.avatar.definition + val guid = player.GUID sendResponse(OCM.detailed(player)) + continent.AvatarEvents ! AvatarServiceMessage( + s"spectator", + AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(player).get, None) + ) case _ => player.VehicleSeated = None @@ -2869,7 +2878,6 @@ class ZoningOperations( ) ) nextSpawnPoint = physSpawnPoint - prevSpawnPoint = physSpawnPoint shiftPosition = Some(pos) shiftOrientation = Some(ori) val toZoneNumber = if (continent.id.equals(zoneId)) { @@ -3019,6 +3027,7 @@ class ZoningOperations( case _ => NormalKeepAlive } + prevSpawnPoint = nextSpawnPoint nextSpawnPoint = None } //if not the condition above, player has started playing normally diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index a69c18dfa..d5140c0d5 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -488,4 +488,35 @@ object Players { } player.HistoryAndContributions() } + + /** + * Select the player's zone channel. + * If the player is spectating, then that is their channel instead. + * The resulting channel name should never be used for subscribing - only for publishing. + * @param player player in a zone + * @return channel name + */ + def ZoneChannelIfSpectating(player: Player): String = { + if (player.spectator) { + "spectator" + } else { + player.Zone.id + } + } + + /** + * Select the player's zone channel. + * If the player is spectating, then that is their channel instead. + * The resulting channel name should never be used for subscribing - only for publishing. + * @param player player in a zone + * @param zoneid custom zone name to be used as the channel name + * @return channel name + */ + def ZoneChannelIfSpectating(player: Player, zoneid: String): String = { + if (player.spectator) { + "spectator" + } else { + zoneid + } + } } diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 4fb846813..186fe1ef5 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -320,7 +320,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } else if ((!resistance && before != slot && (player.DrawnSlot = slot) != before) && ItemSwapSlot != before) { val mySlot = if (updateMyHolsterArm) slot else -1 //use as a short-circuit events ! AvatarServiceMessage( - player.Continent, + Players.ZoneChannelIfSpectating(player), AvatarAction.ObjectHeld(player.GUID, mySlot, player.LastDrawnSlot) ) val isHolsters = player.VisibleSlots.contains(slot) @@ -332,11 +332,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { //rek beam/icon colour must match the player's correct hack level events ! AvatarServiceMessage( - player.Continent, + Players.ZoneChannelIfSpectating(player), AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel()) ) } - case None => ; + case None => () } } else { equipment match { @@ -479,7 +479,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm avatarActor ! AvatarActor.DeactivateActiveImplants val zone = player.Zone zone.AvatarEvents ! AvatarServiceMessage( - zone.id, + Players.ZoneChannelIfSpectating(player), AvatarAction.ChangeLoadout( player.GUID, toArmor, @@ -674,7 +674,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //deactivate non-passive implants avatarActor ! AvatarActor.DeactivateActiveImplants player.Zone.AvatarEvents ! AvatarServiceMessage( - player.Zone.id, + Players.ZoneChannelIfSpectating(player), AvatarAction.ChangeExosuit( player.GUID, toArmor, diff --git a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index 28aafe07c..1d2f16fe6 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -65,8 +65,14 @@ object AvatarConverter { */ def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = { val alt_model_flag: Boolean = obj.isBackpack + val avatar = obj.avatar + val tempAvatarInfo = if (obj.spectator) { + avatar.basic.copy(name = s"") + } else { + avatar.basic + } val aa: Int => CharacterAppearanceA = CharacterAppearanceA( - obj.avatar.basic, + tempAvatarInfo, CommonFieldData( obj.Faction, bops = obj.bops, @@ -106,7 +112,7 @@ object AvatarConverter { unk7 = false, on_zipline = None ) - CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars) + CharacterAppearanceData(aa, ab, avatar.decoration.ribbonBars) } def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = { diff --git a/src/main/scala/net/psforever/packet/game/ChatMsg.scala b/src/main/scala/net/psforever/packet/game/ChatMsg.scala index 313f1ecc1..4c64451c1 100644 --- a/src/main/scala/net/psforever/packet/game/ChatMsg.scala +++ b/src/main/scala/net/psforever/packet/game/ChatMsg.scala @@ -2,11 +2,34 @@ package net.psforever.packet.game import net.psforever.newcodecs._ +import net.psforever.packet.GamePacketOpcode.Type import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.types.ChatMessageType -import scodec.Codec +import scodec.bits.BitVector +import scodec.{Attempt, Codec} import scodec.codecs._ +/* +For colors, type '/#n' before text, where `n` is one of the following hexadecimal numbers: + 0 white + 1 black + 2 cyan + 3 yellow + 4 green + 5 light blue + 6 brown + 7 violet + 8 magneta + 9 purple + a purple + b yellow green + c blue + d light pink + e light green + f beige +All other options result in white text. +*/ + /** * Instructs client to display and/or process a chat message/command when sent server to client. * Instructs server to route and/or process a chat message/command when sent client to server. @@ -35,8 +58,8 @@ final case class ChatMsg( assert(note.isEmpty, "Note contents found, but message type isnt Note") type Packet = ChatMsg - def opcode = GamePacketOpcode.ChatMsg - def encode = ChatMsg.encode(this) + def opcode: Type = GamePacketOpcode.ChatMsg + def encode: Attempt[BitVector] = ChatMsg.encode(this) } object ChatMsg extends Marshallable[ChatMsg] {