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..b27f882ba 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -7,11 +7,11 @@ 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}
-import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity}
-import net.psforever.objects.vehicles.MountedWeapons
+import net.psforever.objects.vital.ReconstructionActivity
import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
import org.joda.time.{LocalDateTime, Seconds}
@@ -42,7 +42,6 @@ import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
-import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity}
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
@@ -958,6 +957,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 +2067,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 +2080,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 +2262,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
}))
@@ -3177,16 +3202,7 @@ class AvatarActor(
val zone = _session.zone
val player = _session.player
val playerSource = PlayerSource(player)
- val historyTranscript = {
- (killStat.info.interaction.cause match {
- case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
- case _ => None
- }).collect {
- case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
- player.ContributionFrom(mount)
- }
- player.HistoryAndContributions()
- }
+ val historyTranscript = Players.produceContributionTranscriptFromKill(zone, player, killStat)
val target = killStat.info.targetAfter.asInstanceOf[PlayerSource]
val targetMounted = target.seatedIn
.collect {
diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala
deleted file mode 100644
index 624d88117..000000000
--- a/src/main/scala/net/psforever/actors/session/ChatActor.scala
+++ /dev/null
@@ -1,1548 +0,0 @@
-package net.psforever.actors.session
-
-import akka.actor.Cancellable
-import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
-import akka.actor.typed.receptionist.Receptionist
-import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
-import akka.actor.typed.scaladsl.adapter._
-import net.psforever.actors.zone.ZoneActor
-import net.psforever.objects.sourcing.PlayerSource
-import net.psforever.objects.zones.ZoneInfo
-import net.psforever.services.local.{LocalAction, LocalServiceMessage}
-import org.log4s.Logger
-
-import scala.annotation.unused
-import scala.collection.{Seq, mutable}
-import scala.concurrent.ExecutionContextExecutor
-import scala.concurrent.duration._
-//
-import net.psforever.actors.zone.BuildingActor
-import net.psforever.login.WorldSession
-import net.psforever.objects.{Default, Player, Session}
-import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Shortcut => AvatarShortcut}
-import net.psforever.objects.definition.ImplantDefinition
-import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
-import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
-import net.psforever.objects.serverobject.structures.{Amenity, Building}
-import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
-import net.psforever.objects.zones.Zoning
-import net.psforever.packet.game.objectcreate.DrawnSlot
-import net.psforever.packet.game.{ChatMsg, CreateShortcutMessage, DeadState, RequestDestroyMessage, Shortcut, ZonePopulationUpdateMessage}
-import net.psforever.services.{CavernRotationService, InterstellarClusterService}
-import net.psforever.services.chat.ChatService
-import net.psforever.services.chat.ChatService.ChatChannel
-import net.psforever.types.ChatMessageType.{CMT_GMOPEN, UNK_227, UNK_229}
-import net.psforever.types.{ChatMessageType, Cosmetic, ExperienceType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
-import net.psforever.util.{Config, PointOfInterest}
-import net.psforever.zones.Zones
-
-object ChatActor {
- def apply(
- sessionActor: ActorRef[SessionActor.Command],
- avatarActor: ActorRef[AvatarActor.Command]
- ): Behavior[Command] =
- Behaviors
- .supervise[Command] {
- Behaviors.withStash(100) { buffer =>
- Behaviors.setup(context => new ChatActor(context, buffer, sessionActor, avatarActor).start())
- }
- }
- .onFailure[Exception](SupervisorStrategy.restart)
-
- sealed trait Command
-
- final case class JoinChannel(channel: ChatChannel) extends Command
- final case class LeaveChannel(channel: ChatChannel) extends Command
- final case class Message(message: ChatMsg) extends Command
- final case class SetSession(session: Session) extends Command
-
- private case class ListingResponse(listing: Receptionist.Listing) extends Command
- private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
-
- /**
- * For a provided number of facility nanite transfer unit resource silos,
- * charge the facility's silo with an expected amount of nanite transfer units.
- * @see `Amenity`
- * @see `ChatMsg`
- * @see `ResourceSilo`
- * @see `ResourceSilo.UpdateChargeLevel`
- * @see `SessionActor.Command`
- * @see `SessionActor.SendResponse`
- * @param session messaging reference back tothe target session
- * @param resources the optional number of resources to set to each silo;
- * different values provide different resources as indicated below;
- * an undefined value also has a condition
- * @param silos where to deposit the resources
- * @param debugContent something for log output context
- */
- private def setBaseResources(
- session: ActorRef[SessionActor.Command],
- resources: Option[Int],
- silos: Iterable[Amenity],
- debugContent: String
- ): Unit = {
- if (silos.isEmpty) {
- session ! SessionActor.SendResponse(
- ChatMsg(UNK_229, wideContents=true, "Server", s"no targets for ntu found with parameters $debugContent", None)
- )
- }
- resources match {
- // x = n0% of maximum capacitance
- case Some(value) if value > -1 && value < 11 =>
- silos.collect {
- case silo: ResourceSilo =>
- silo.Actor ! ResourceSilo.UpdateChargeLevel(
- value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
- )
- }
- // capacitance set to x (where x > 10) exactly, within limits
- case Some(value) =>
- silos.collect {
- case silo: ResourceSilo =>
- silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
- }
- case None =>
- // x >= n0% of maximum capacitance and x <= maximum capacitance
- val rand = new scala.util.Random
- silos.collect {
- case silo: ResourceSilo =>
- val a = 7
- val b = 10 - a
- val tenth = silo.MaxNtuCapacitor * 0.1f
- silo.Actor ! ResourceSilo.UpdateChargeLevel(
- a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
- )
- }
- }
- }
-
- /**
- * Create a medkit shortcut if there is no medkit shortcut on the hotbar.
- * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
- * or cancel / invalidate the shortcut creation.
- * @see `Array::indexWhere`
- * @see `CreateShortcutMessage`
- * @see `net.psforever.objects.avatar.Shortcut`
- * @see `net.psforever.packet.game.Shortcut.Medkit`
- * @see `SessionActor.SendResponse`
- * @param guid current player unique identifier for the target client
- * @param shortcuts list of all existing shortcuts, used for early validation
- */
- private def medkitSanityTest(
- guid: PlanetSideGUID,
- shortcuts: Array[Option[AvatarShortcut]],
- sendTo: ActorRef[SessionActor.Command]
- ): Unit = {
- if (!shortcuts.exists {
- case Some(a) => a.purpose == 0
- case None => false
- }) {
- shortcuts.indexWhere(_.isEmpty) match {
- case -1 => ()
- case index =>
- //new shortcut
- sendTo ! SessionActor.SendResponse(CreateShortcutMessage(
- guid,
- index + 1,
- Some(Shortcut.Medkit())
- ))
- }
- }
- }
-
- /**
- * Create all implant macro shortcuts for all implants whose shortcuts have been removed from the hotbar.
- * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
- * or cancel / invalidate the shortcut creation.
- * @see `CreateShortcutMessage`
- * @see `ImplantDefinition`
- * @see `net.psforever.objects.avatar.Shortcut`
- * @see `SessionActor.SendResponse`
- * @param guid current player unique identifier for the target client
- * @param haveImplants list of implants the player possesses
- * @param shortcuts list of all existing shortcuts, used for early validation
- */
- private def implantSanityTest(
- guid: PlanetSideGUID,
- haveImplants: Iterable[ImplantDefinition],
- shortcuts: Array[Option[AvatarShortcut]],
- sendTo: ActorRef[SessionActor.Command]
- ): Unit = {
- val haveImplantShortcuts = shortcuts.collect {
- case Some(shortcut) if shortcut.purpose == 2 => shortcut.tile
- }
- var start: Int = 0
- haveImplants.filterNot { imp => haveImplantShortcuts.contains(imp.Name) }
- .foreach { implant =>
- shortcuts.indexWhere(_.isEmpty, start) match {
- case -1 => ()
- case index =>
- //new shortcut
- start = index + 1
- sendTo ! SessionActor.SendResponse(CreateShortcutMessage(
- guid,
- start,
- Some(implant.implantType.shortcut)
- ))
- }
- }
- }
-
- /**
- * Create a text chat macro shortcut if it doesn't already exist.
- * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
- * or cancel / invalidate the shortcut creation.
- * @see `Array::indexWhere`
- * @see `CreateShortcutMessage`
- * @see `net.psforever.objects.avatar.Shortcut`
- * @see `net.psforever.packet.game.Shortcut.Macro`
- * @see `SessionActor.SendResponse`
- * @param guid current player unique identifier for the target client
- * @param acronym three letters emblazoned on the shortcut icon
- * @param msg the message published to text chat
- * @param shortcuts a list of all existing shortcuts, used for early validation
- */
- private def macroSanityTest(
- guid: PlanetSideGUID,
- acronym: String,
- msg: String,
- shortcuts: Array[Option[AvatarShortcut]],
- sendTo: ActorRef[SessionActor.Command]
- ): Unit = {
- shortcuts.indexWhere(_.isEmpty) match {
- case -1 => ()
- case index =>
- //new shortcut
- sendTo ! SessionActor.SendResponse(CreateShortcutMessage(
- guid,
- index + 1,
- Some(Shortcut.Macro(acronym, msg))
- ))
- }
- }
-
- private def setBattleRank(
- session: Session,
- params: Seq[String],
- msgFunc: Long => AvatarActor.Command,
- sendTo: ActorRef[AvatarActor.Command]
- ): Boolean = {
- val (target, rank) = (params.headOption, params.lift(1)) match {
- case (Some(target), Some(rank)) if target == session.avatar.name =>
- rank.toIntOption match {
- case Some(rank) => (None, BattleRank.withValueOpt(rank))
- case None => (None, None)
- }
- case (Some("-h"), _) | (Some("-help"), _) =>
- (None, Some(BattleRank.BR1))
- case (Some(_), Some(_)) =>
- // picking other targets is not supported for now
- (None, None)
- case (Some(rank), None) =>
- rank.toIntOption match {
- case Some(rank) => (None, BattleRank.withValueOpt(rank))
- case None => (None, None)
- }
- case _ => (None, None)
- }
- (target, rank) match {
- case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
- sendTo ! msgFunc(rank.experience)
- true
- case _ =>
- false
- }
- }
-
- private def setCommandRank(
- contents: String,
- session: Session,
- sendTo: ActorRef[AvatarActor.Command]
- ): Boolean = {
- val buffer = cliTokenization(contents)
- val (target, rank) = (buffer.headOption, buffer.lift(1)) match {
- case (Some(target), Some(rank)) if target == session.avatar.name =>
- rank.toIntOption match {
- case Some(rank) => (None, CommandRank.withValueOpt(rank))
- case None => (None, None)
- }
- case (Some(_), Some(_)) =>
- // picking other targets is not supported for now
- (None, None)
- case (Some(rank), None) =>
- rank.toIntOption match {
- case Some(rank) => (None, CommandRank.withValueOpt(rank))
- case None => (None, None)
- }
- case _ => (None, None)
- }
- (target, rank) match {
- case (_, Some(rank)) =>
- sendTo ! AvatarActor.SetCep(rank.experience)
- true
- case _ =>
- false
- }
- }
-
- private def captureBaseParamFacilities(
- session: Session,
- token: Option[String]
- ): Option[Seq[Building]] = {
- token.collect {
- case "curr" =>
- val list = captureBaseCurrSoi(session)
- if (list.nonEmpty) {
- Some(list.toSeq)
- } else {
- None
- }
- case "all" =>
- val list = session.zone.Buildings.values.filter(_.CaptureTerminal.isDefined)
- if (list.nonEmpty) {
- Some(list.toSeq)
- } else {
- None
- }
- case name =>
- val trueName = ZoneInfo
- .values
- .find(_.id.equals(session.zone.id))
- .flatMap { info =>
- info.aliases
- .facilities
- .collectFirst { case (key, internalName) if key.equalsIgnoreCase(name) => internalName }
- }
- .getOrElse(name)
- session.zone.Buildings
- .values
- .find {
- building => trueName.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined
- }
- .map(b => Seq(b))
- }
- .flatten
- }
-
- private def captureBaseCurrSoi(
- session: Session
- ): Iterable[Building] = {
- val charId = session.player.CharId
- session.zone.Buildings.values.filter { building =>
- building.PlayersInSOI.exists(_.CharId == charId)
- }
- }
-
- private def captureBaseParamFaction(
- @unused session: Session,
- token: Option[String]
- ): Option[PlanetSideEmpire.Value] = {
- token.collect {
- case faction =>
- faction.toLowerCase() match {
- case "tr" => Some(PlanetSideEmpire.TR)
- case "nc" => Some(PlanetSideEmpire.NC)
- case "vs" => Some(PlanetSideEmpire.VS)
- case "none" => Some(PlanetSideEmpire.NEUTRAL)
- case "bo" => Some(PlanetSideEmpire.NEUTRAL)
- case "neutral" => Some(PlanetSideEmpire.NEUTRAL)
- case _ => None
- }
- }.flatten
- }
-
- private def captureBaseParamTimer(
- @unused session: Session,
- token: Option[String]
- ): Option[Int] = {
- token.flatMap(_.toIntOption)
- }
-
-
-
- private def customCommandWhitetext(
- session: Session,
- content: Seq[String],
- sendTo: ActorRef[ChatService.Command]
- ): Boolean = {
- sendTo ! ChatService.Message(
- session,
- ChatMsg(UNK_227, wideContents=true, "", content.mkString(" "), None),
- ChatChannel.Default()
- )
- true
- }
-
- private def customCommandLoc(
- session: Session,
- message: ChatMsg,
- sendTo: ActorRef[SessionActor.Command]
- ): Boolean = {
- val continent = session.zone
- val player = session.player
- val loc =
- s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
- sendTo ! SessionActor.SendResponse(message.copy(contents = loc))
- true
- }
-
- private def customCommandList(
- session: Session,
- params: Seq[String],
- message: ChatMsg,
- sendTo: ActorRef[SessionActor.Command]
- ): Boolean = {
- val zone = params.headOption match {
- case Some("") | None =>
- Some(session.zone)
- case Some(id) =>
- Zones.zones.find(_.id == id)
- }
- zone match {
- case Some(inZone) =>
- sendTo ! SessionActor.SendResponse(
- ChatMsg(
- CMT_GMOPEN,
- message.wideContents,
- "Server",
- "\\#8Name (Faction) [ID] at PosX PosY PosZ",
- message.note
- )
- )
- (inZone.LivePlayers ++ inZone.Corpses)
- .filter(_.CharId != session.player.CharId)
- .sortBy(p => (p.Name, !p.isAlive))
- .foreach(player => {
- val color = if (!player.isAlive) "\\#7" else ""
- sendTo ! SessionActor.SendResponse(
- ChatMsg(
- CMT_GMOPEN,
- message.wideContents,
- "Server",
- s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
- message.note
- )
- )
- })
- case None =>
- sendTo ! SessionActor.SendResponse(
- ChatMsg(
- CMT_GMOPEN,
- message.wideContents,
- "Server",
- "Invalid zone ID",
- message.note
- )
- )
- }
- true
- }
-
- private def customCommandNtu(
- session: Session,
- params: Seq[String],
- sendTo: ActorRef[SessionActor.Command]
- ): Boolean = {
- val (facility, customNtuValue) = (params.headOption, params.lift(1)) match {
- case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
- case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
- case _ => (None, None)
- }
- val silos = (facility match {
- case Some(cur) if cur.toLowerCase().startsWith("curr") =>
- val position = session.player.Position
- session.zone.Buildings.values
- .filter { building =>
- val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
- Vector3.DistanceSquared(building.Position, position) < soi2
- }
- case Some(all) if all.toLowerCase.startsWith("all") =>
- session.zone.Buildings.values
- case Some(x) =>
- session.zone.Buildings.values.find(_.Name.equalsIgnoreCase(x)).toList
- case _ =>
- session.zone.Buildings.values
- })
- .flatMap { building =>
- building.Amenities.filter(_.isInstanceOf[ResourceSilo])
- }
- ChatActor.setBaseResources(sendTo, customNtuValue, silos, debugContent = s"$facility")
- true
- }
-
- private def customCommandZonerotate(
- params: Seq[String],
- sendTo: ActorRef[InterstellarClusterService.Command],
- replyTo: ActorRef[SessionActor.Command]
- ): Boolean = {
- sendTo ! InterstellarClusterService.CavernRotation(params.headOption match {
- case Some("-list") | Some("-l") =>
- CavernRotationService.ReportRotationOrder(replyTo.toClassic)
- case _ =>
- CavernRotationService.HurryNextRotation
- })
- true
- }
-
- private def customCommandSuicide(
- session: Session
- ): Boolean = {
- //this is like CMT_SUICIDE but it ignores checks and forces a suicide state
- val tplayer = session.player
- tplayer.Revive
- tplayer.Actor ! Player.Die()
- true
- }
-
- private def customCommandGrenade(
- session: Session,
- log: Logger
- ): Boolean = {
- WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
- true
- }
-
- private def customCommandMacro(
- session: Session,
- params: Seq[String],
- sendTo: ActorRef[SessionActor.Command]
- ): Boolean = {
- val avatar = session.avatar
- (params.headOption, params.lift(1)) match {
- case (Some(cmd), other) =>
- cmd.toLowerCase() match {
- case "medkit" =>
- medkitSanityTest(session.player.GUID, avatar.shortcuts, sendTo)
- true
-
- case "implants" =>
- //implant shortcut sanity test
- implantSanityTest(
- session.player.GUID,
- avatar.implants.collect {
- case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
- },
- avatar.shortcuts,
- sendTo
- )
- true
-
- case name
- if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
- avatar.implants.find {
- case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
- case None => false
- } match {
- case Some(Some(implant)) =>
- //specific implant shortcut sanity test
- implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts, sendTo)
- true
- case _ if other.nonEmpty =>
- //add macro?
- macroSanityTest(session.player.GUID, name, params.drop(2).mkString(" "), avatar.shortcuts, sendTo)
- true
- case _ =>
- false
- }
-
- case name
- if name.nonEmpty && other.nonEmpty =>
- //add macro
- macroSanityTest(session.player.GUID, name, params.drop(2).mkString(" "), avatar.shortcuts, sendTo)
- true
-
- case _ =>
- false
- }
- case _ =>
- false
- }
- }
-
- private def customCommandProgress(
- session: Session,
- params: Seq[String],
- sendTo: ActorRef[AvatarActor.Command]
- ): Boolean = {
- val ourRank = BattleRank.withExperience(session.avatar.bep).value
- if (!session.account.gm &&
- (ourRank <= Config.app.game.promotion.broadcastBattleRank ||
- ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
- setBattleRank(session, params, AvatarActor.Progress, sendTo)
- true
- } else {
- setBattleRank(session, Seq("1"), AvatarActor.Progress, sendTo)
- false
- }
- }
-
- private def customCommandNearby(
- session: Session,
- sendTo: ActorRef[SessionActor.Command]
- ): Boolean = {
- val playerPos = session.player.Position.xy
- val closest = session.zone
- .Buildings
- .values
- .toSeq
- .minByOption(base => Vector3.DistanceSquared(playerPos, base.Position.xy))
- .map(base => s"${base.Name} - ${base.Definition.Name}")
- sendTo ! SessionActor.SendResponse(
- ChatMsg(CMT_GMOPEN, wideContents = false, "Server", s"closest facility: $closest", None)
- )
- true
- }
-
- private def firstParam[T](
- session: Session,
- buffer: Iterable[String],
- func: (Session, Option[String])=>Option[T]
- ): (Option[T], Option[String], Iterable[String]) = {
- val tokenOpt = buffer.headOption
- val valueOpt = func(session, tokenOpt)
- val outBuffer = if (valueOpt.nonEmpty) {
- buffer.drop(1)
- } else {
- buffer
- }
- (valueOpt, tokenOpt, outBuffer)
- }
-
- private def cliTokenization(str: String): List[String] = {
- str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList
- }
-}
-
-class ChatActor(
- context: ActorContext[ChatActor.Command],
- buffer: StashBuffer[ChatActor.Command],
- sessionActor: ActorRef[SessionActor.Command],
- avatarActor: ActorRef[AvatarActor.Command]
-) {
-
- import ChatActor._
-
- implicit val ec: ExecutionContextExecutor = context.executionContext
-
- private[this] val log = org.log4s.getLogger
- var channels: List[ChatChannel] = List()
- var session: Option[Session] = None
- var chatService: Option[ActorRef[ChatService.Command]] = None
- var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
- var silenceTimer: Cancellable = Default.Cancellable
- /**
- * when another player is listed as one of our ignored players,
- * and that other player sends an emote,
- * that player is assigned a cooldown and only one emote per period will be seen
- * key - character unique avatar identifier, value - when the current cooldown period will end
- */
- var ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]()
-
- val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
- case ChatService.MessageResponse(_session, message, channel) => IncomingMessage(_session, message, channel)
- }
-
- context.system.receptionist ! Receptionist.Find(
- ChatService.ChatServiceKey,
- context.messageAdapter[Receptionist.Listing](ListingResponse)
- )
-
- context.system.receptionist ! Receptionist.Find(
- InterstellarClusterService.InterstellarClusterServiceKey,
- context.messageAdapter[Receptionist.Listing](ListingResponse)
- )
-
- def start(): Behavior[Command] = {
- Behaviors
- .receiveMessage[Command] {
- case ListingResponse(InterstellarClusterService.InterstellarClusterServiceKey.Listing(listings)) =>
- listings.headOption match {
- case Some(ref) =>
- cluster = Some(ref)
- postStartBehaviour()
- case None =>
- context.system.receptionist ! Receptionist.Find(
- InterstellarClusterService.InterstellarClusterServiceKey,
- context.messageAdapter[Receptionist.Listing](ListingResponse)
- )
- Behaviors.same
- }
-
- case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
- chatService = Some(listings.head)
- channels ++= List(ChatChannel.Default())
- postStartBehaviour()
-
- case SetSession(newSession) =>
- session = Some(newSession)
- postStartBehaviour()
-
- case other =>
- buffer.stash(other)
- Behaviors.same
- }
- }
-
- def postStartBehaviour(): Behavior[Command] = {
- (session, chatService, cluster) match {
- case (Some(_session), Some(_chatService), Some(_cluster)) if _session.player != null =>
- _chatService ! ChatService.JoinChannel(chatServiceAdapter, _session, ChatChannel.Default())
- buffer.unstashAll(active(_session, _chatService, _cluster))
- case _ =>
- Behaviors.same
- }
- }
-
- def active(
- session: Session,
- chatService: ActorRef[ChatService.Command],
- cluster: ActorRef[InterstellarClusterService.Command]
- ): Behavior[Command] = {
- import ChatMessageType._
-
- Behaviors
- .receiveMessagePartial[Command] {
- case SetSession(newSession) =>
- this.session = Some(newSession)
- active(newSession, chatService, cluster)
-
- case JoinChannel(channel) =>
- chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
- channels ++= List(channel)
- Behaviors.same
-
- case LeaveChannel(channel) =>
- chatService ! ChatService.LeaveChannel(chatServiceAdapter, channel)
- channels = channels.filterNot(_ == channel)
- Behaviors.same
-
- case Message(message) =>
- val gmCommandAllowed =
- session.account.gm || 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("!") &&
- customCommandMessages(message, session, chatService, cluster) => ()
-
- case (CMT_FLY, recipient, contents) if gmCommandAllowed =>
- val (token, flying) = contents match {
- case "on" => (contents, true)
- case "off" => (contents, false)
- case _ => ("off", false)
- }
- sessionActor ! SessionActor.SetFlying(flying)
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(CMT_FLY, wideContents=false, recipient, token, None)
- )
-
- case (CMT_ANONYMOUS, _, _) =>
- // ?
-
- case (CMT_TOGGLE_GM, _, _) =>
- // ?
-
- case (CMT_CULLWATERMARK, _, contents) =>
- val connectionState =
- if (contents.contains("40 80")) 100
- else if (contents.contains("120 200")) 25
- else 50
- sessionActor ! SessionActor.SetConnectionState(connectionState)
-
- case (CMT_SPEED, _, contents) if gmCommandAllowed =>
- val speed =
- try {
- contents.toFloat
- } catch {
- case _: Throwable =>
- 1f
- }
- sessionActor ! SessionActor.SetSpeed(speed)
- sessionActor ! SessionActor.SendResponse(message.copy(contents = f"$speed%.3f"))
-
- case (CMT_TOGGLESPECTATORMODE, _, contents) if gmCommandAllowed =>
- val spectator = contents match {
- case "on" => true
- case "off" => false
- case _ => !session.player.spectator
- }
- sessionActor ! SessionActor.SetSpectator(spectator)
- sessionActor ! SessionActor.SendResponse(message.copy(contents = if (spectator) "on" else "off"))
- sessionActor ! SessionActor.SendResponse(
- message.copy(
- messageType = UNK_227,
- contents = if (spectator) "@SpectatorEnabled" else "@SpectatorDisabled"
- )
- )
-
- case (CMT_RECALL, _, _) =>
- val errorMessage = session.zoningType match {
- case Zoning.Method.Quit => Some("You can't recall to your sanctuary continent while quitting")
- case Zoning.Method.InstantAction =>
- Some("You can't recall to your sanctuary continent while instant actioning")
- case Zoning.Method.Recall => Some("You already requested to recall to your sanctuary continent")
- case _ if session.zone.id == Zones.sanctuaryZoneId(session.player.Faction) =>
- Some("You can't recall to your sanctuary when you are already in your sanctuary")
- case _ if !session.player.isAlive || session.deadState != DeadState.Alive =>
- Some(if (session.player.isAlive) "@norecall_deconstructing" else "@norecall_dead")
- case _ if session.player.VehicleSeated.nonEmpty => Some("@norecall_invehicle")
- case _ => None
- }
- errorMessage match {
- case Some(errorMessage) =>
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, errorMessage))
- case None =>
- sessionActor ! SessionActor.Recall()
- }
-
- case (CMT_INSTANTACTION, _, _) =>
- if (session.zoningType == Zoning.Method.Quit) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "You can't instant action while quitting."))
- } else if (session.zoningType == Zoning.Method.InstantAction) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_instantactionting"))
- } else if (session.zoningType == Zoning.Method.Recall) {
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(CMT_QUIT, "You won't instant action. You already requested to recall to your sanctuary continent")
- )
- } else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
- if (session.player.isAlive) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_deconstructing"))
- } else {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_dead"))
- }
- } else if (session.player.VehicleSeated.nonEmpty) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_invehicle"))
- } else {
- sessionActor ! SessionActor.InstantAction()
- }
-
- case (CMT_QUIT, _, _) =>
- if (session.zoningType == Zoning.Method.Quit) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_quitting"))
- } else if (!session.player.isAlive || session.deadState != DeadState.Alive) {
- if (session.player.isAlive) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_deconstructing"))
- } else {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_dead"))
- }
- } else if (session.player.VehicleSeated.nonEmpty) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, "@noquit_invehicle"))
- } else {
- sessionActor ! SessionActor.Quit()
- }
-
- case (CMT_SUICIDE, _, _) =>
- if (session.player.isAlive && session.deadState != DeadState.Release) {
- sessionActor ! SessionActor.Suicide()
- }
-
- case (CMT_DESTROY, _, contents) if contents.matches("\\d+") =>
- val guid = contents.toInt
- session.zone.GUID(session.zone.map.terminalToSpawnPad.getOrElse(guid, guid)) match {
- case Some(pad: VehicleSpawnPad) =>
- pad.Actor ! VehicleSpawnControl.ProcessControl.Flush
- case Some(turret: FacilityTurret) if turret.isUpgrading =>
- WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None)
- case _ =>
- // FIXME we shouldn't do it like that
- sessionActor.toClassic ! RequestDestroyMessage(PlanetSideGUID(guid))
- }
- sessionActor ! SessionActor.SendResponse(message)
-
- case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
- val buffer = cliTokenization(contents)
- val customNtuValue = buffer.lift(1) match {
- case Some(x) if x.toIntOption.nonEmpty => Some(x.toInt)
- case _ => None
- }
- val silos = {
- val position = session.player.Position
- session.zone.Buildings.values
- .filter { building =>
- val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
- Vector3.DistanceSquared(building.Position, position) < soi2
- }
- }
- .flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
- ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent="")
-
- case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
- val buffer = cliTokenization(contents)
- val (zoneOpt, lockVal) = (buffer.lift(1), buffer.lift(2)) match {
- case (Some(x), Some(y)) =>
- val zone = if (x.toIntOption.nonEmpty) {
- val xInt = x.toInt
- Zones.zones.find(_.Number == xInt)
- } else {
- Zones.zones.find(z => z.id.equals(x))
- }
- val value = if (y.toIntOption.nonEmpty && y.toInt == 0) {
- 0
- } else {
- 1
- }
- (zone, Some(value))
- case _ =>
- (None, None)
- }
- (zoneOpt, lockVal) match {
- case (Some(zone), Some(lock)) if zone.map.cavern =>
- //caverns must be rotated in an order
- if (lock == 0) {
- cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneUnlock(zone.id))
- } else {
- cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneLock(zone.id))
- }
- case (Some(_), Some(_)) =>
- //normal zones can lock when all facilities and towers on it belong to the same faction
- //normal zones can lock when ???
- case _ => ;
- }
-
- case (U_CMT_ZONEROTATE, _, _) if gmCommandAllowed =>
- cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
-
- case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
- val buffer = cliTokenization(contents).take(3)
- //walk through the param buffer
- val (foundFacilities, foundFacilitiesTag, factionBuffer) = firstParam(session, buffer, captureBaseParamFacilities)
- val (foundFaction, foundFactionTag, timerBuffer) = firstParam(session, factionBuffer, captureBaseParamFaction)
- val (foundTimer, foundTimerTag, _) = firstParam(session, timerBuffer, captureBaseParamTimer)
- //resolve issues with the initial params
- var facilityError: Int = 0
- var factionError: Boolean = false
- var timerError: Boolean = false
- var usageMessage: Boolean = false
- val resolvedFacilities = foundFacilities
- .orElse {
- if (foundFacilitiesTag.nonEmpty) {
- if (foundFaction.isEmpty) {
- /* /capturebase OR /capturebase */
- //malformed facility tag error
- facilityError = 2
- None
- } else if (!foundFacilitiesTag.contains("curr")) { //did we do this next check already
- /* /capturebase , potentially */
- val buildings = captureBaseCurrSoi(session)
- if (buildings.nonEmpty) {
- //convert facilities to faction
- Some(buildings.toSeq)
- } else {
- //no facilities error
- facilityError = 1
- None
- }
- } else {
- //no facilities error
- facilityError = 1
- None
- }
- } else {
- //no params; post command usage reminder
- usageMessage = true
- None
- }
- }
- val resolvedFaction = foundFaction
- .orElse {
- if (resolvedFacilities.nonEmpty) {
- /* /capturebase OR /capturebase */
- if (foundFactionTag.isEmpty || foundTimer.nonEmpty) {
- //convert facilities to OUR PLAYER'S faction
- Some(session.player.Faction)
- } else {
- //malformed faction tag error
- factionError = true
- None
- }
- } else {
- //incorrect params; already posted an error message
- None
- }
- }
- val resolvedTimer = foundTimer
- .orElse {
- //todo stop command execution? post command usage reminder?
- if (resolvedFaction.nonEmpty && foundTimerTag.nonEmpty) {
- /* /capturebase > > */
- //malformed timer tag error
- timerError = true
- None
- } else {
- //eh
- Some(1)
- }
- }
- //evaluate results
- (resolvedFacilities, resolvedFaction, resolvedTimer) match {
- case (Some(buildings), Some(faction), Some(_)) =>
- buildings.foreach { building =>
- //TODO implement timer
- val terminal = building.CaptureTerminal.get
- val zone = building.Zone
- val zoneActor = zone.actor
- val buildingActor = building.Actor
- //clear any previous hack
- if (building.CaptureTerminalIsHacked) {
- zone.LocalEvents ! LocalServiceMessage(
- zone.id,
- LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
- )
- }
- //push any updates this might cause
- zoneActor ! ZoneActor.ZoneMapUpdate()
- //convert faction affiliation
- buildingActor ! BuildingActor.SetFaction(faction)
- buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false))
- //push for map updates again
- zoneActor ! ZoneActor.ZoneMapUpdate()
- }
- case _ =>
- if (usageMessage) {
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
- )
- } else {
- val msg = if (facilityError == 1) { "can not contextually determine building target" }
- else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" }
- else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" }
- else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" }
- else { "malformed params; check usage" }
- sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None))
- }
- }
-
- case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
- if gmCommandAllowed =>
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
-
- case (CMT_GMTELL, _, _) if gmCommandAllowed =>
- chatService ! ChatService.Message(
- session,
- message,
- ChatChannel.Default()
- )
-
- case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed =>
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
-
- case (CMT_OPEN, _, _) if !session.player.silenced =>
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
-
- case (CMT_VOICE, _, contents) =>
- // SH prefix are tactical voice macros only sent to squad
- if (contents.startsWith("SH")) {
- channels.foreach {
- case channel: ChatChannel.Squad =>
- chatService ! ChatService.Message(session, message.copy(recipient = session.player.Name), channel)
- case _ =>
- }
- } else {
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
- }
-
- case (CMT_TELL, _, _) if !session.player.silenced =>
- if (AvatarActor.onlineIfNotIgnored(message.recipient, session.avatar.name)) {
- chatService ! ChatService.Message(
- session,
- message,
- ChatChannel.Default()
- )
- } else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_target", None)
- )
- } else {
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_ignore", None)
- )
- }
-
- case (CMT_BROADCAST, _, _) if !session.player.silenced =>
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
-
- case (CMT_PLATOON, _, _) if !session.player.silenced =>
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
-
- case (CMT_COMMAND, _, _) if gmCommandAllowed =>
- chatService ! ChatService.Message(
- session,
- message.copy(recipient = session.player.Name),
- ChatChannel.Default()
- )
-
- case (CMT_NOTE, _, _) =>
- chatService ! ChatService.Message(session, message, ChatChannel.Default())
-
- case (CMT_SILENCE, _, _) if gmCommandAllowed =>
- chatService ! ChatService.Message(session, message, ChatChannel.Default())
-
- case (CMT_SQUAD, _, _) =>
- channels.foreach {
- case channel: ChatChannel.Squad =>
- chatService ! ChatService.Message(session, message.copy(recipient = session.player.Name), channel)
- case _ =>
- }
-
- case (
- CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS,
- _,
- _
- ) =>
- val players = session.zone.Players
- val popTR = players.count(_.faction == PlanetSideEmpire.TR)
- val popNC = players.count(_.faction == PlanetSideEmpire.NC)
- val popVS = players.count(_.faction == PlanetSideEmpire.VS)
-
- if (popNC + popTR + popVS == 0) {
- sessionActor ! SessionActor.SendResponse(ChatMsg(ChatMessageType.CMT_WHO, "@Nomatches"))
- } else {
- val contName = session.zone.map.name
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "That command doesn't work for now, but : ", None)
- )
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "NC online : " + popNC + " on " + contName, None)
- )
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "TR online : " + popTR + " on " + contName, None)
- )
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "VS online : " + popVS + " on " + contName, None)
- )
- }
-
- case (CMT_ZONE, _, contents) if gmCommandAllowed =>
- val buffer = cliTokenization(contents)
- val (zone, gate, list) = (buffer.headOption, buffer.lift(1)) match {
- case (Some("-list"), None) =>
- (None, None, true)
- case (Some(zoneId), Some("-list")) =>
- (PointOfInterest.get(zoneId), None, true)
- case (Some(zoneId), gateId) =>
- val zone = PointOfInterest.get(zoneId)
- val gate = (zone, gateId) match {
- case (Some(zone), Some(gateId)) => PointOfInterest.getWarpgate(zone, gateId)
- case (Some(zone), None) => Some(PointOfInterest.selectRandom(zone))
- case _ => None
- }
- (zone, gate, false)
- case _ =>
- (None, None, false)
- }
- (zone, gate, list) match {
- case (None, None, true) =>
- sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.list, None))
- case (Some(zone), None, true) =>
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listWarpgates(zone), None)
- )
- case (Some(zone), Some(gate), false) =>
- sessionActor ! SessionActor.SetZone(zone.zonename, gate)
- case (_, None, false) =>
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(UNK_229, wideContents=true, "", "Gate id not defined (use '/zone -list')", None)
- )
- case (_, _, _) if buffer.isEmpty || buffer.headOption.contains("-help") =>
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_ZONE_usage")
- )
- case _ => ()
- }
-
- case (CMT_WARP, _, contents) if gmCommandAllowed =>
- val buffer = cliTokenization(contents)
- val (coordinates, waypoint) = (buffer.headOption, buffer.lift(1), buffer.lift(2)) match {
- case (Some(x), Some(y), Some(z)) => (Some(x, y, z), None)
- case (Some("to"), Some(_/*character*/), None) => (None, None) // TODO not implemented
- case (Some("near"), Some(_/*objectName*/), None) => (None, None) // TODO not implemented
- case (Some(waypoint), None, None) if waypoint.nonEmpty => (None, Some(waypoint))
- case _ => (None, None)
- }
- (coordinates, waypoint) match {
- case (Some((x, y, z)), None) if List(x, y, z).forall { str =>
- val coordinate = str.toFloatOption
- coordinate.isDefined && coordinate.get >= 0 && coordinate.get <= 8191
- } =>
- sessionActor ! SessionActor.SetPosition(Vector3(x.toFloat, y.toFloat, z.toFloat))
- case (None, Some(waypoint)) if waypoint == "-list" =>
- val zone = PointOfInterest.get(session.player.Zone.id)
- zone match {
- case Some(zone: PointOfInterest) =>
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listAll(zone), None)
- )
- case _ => ChatMsg(UNK_229, wideContents=true, "", s"unknown player zone '${session.player.Zone.id}'", None)
- }
- case (None, Some(waypoint)) if waypoint != "-help" =>
- PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
- case Some(location) => sessionActor ! SessionActor.SetPosition(location)
- case None =>
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(UNK_229, wideContents=true, "", s"unknown location '$waypoint'", None)
- )
- }
- case _ =>
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_WARP_usage")
- )
- }
-
- case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
- if (!setBattleRank(session, cliTokenization(contents), AvatarActor.SetBep, avatarActor)) {
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
- )
- }
-
- case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
- if (!setCommandRank(contents, session, avatarActor)) {
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
- )
- }
-
- case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
- contents.toIntOption match {
- case Some(bep) => avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Normal)
- case None =>
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_ADDBATTLEEXPERIENCE_usage")
- )
- }
-
- case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if gmCommandAllowed =>
- contents.toIntOption match {
- case Some(cep) => avatarActor ! AvatarActor.AwardCep(cep)
- case None =>
- sessionActor ! SessionActor.SendResponse(
- message.copy(messageType = UNK_229, contents = "@CMT_ADDCOMMANDEXPERIENCE_usage")
- )
- }
-
- case (CMT_TOGGLE_HAT, _, contents) =>
- val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
- val nextCosmetics = contents match {
- case "off" =>
- cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
- case _ =>
- if (cosmetics.contains(Cosmetic.BrimmedCap)) {
- cosmetics.diff(Set(Cosmetic.BrimmedCap)) + Cosmetic.Beret
- } else if (cosmetics.contains(Cosmetic.Beret)) {
- cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
- } else {
- cosmetics + Cosmetic.BrimmedCap
- }
- }
- val on = nextCosmetics.contains(Cosmetic.BrimmedCap) || nextCosmetics.contains(Cosmetic.Beret)
-
- avatarActor ! AvatarActor.SetCosmetics(nextCosmetics)
- sessionActor ! SessionActor.SendResponse(
- message.copy(
- messageType = UNK_229,
- contents = s"@CMT_TOGGLE_HAT_${if (on) "on" else "off"}"
- )
- )
-
- case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
- val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
-
- val cosmetic = message.messageType match {
- case CMT_HIDE_HELMET => Cosmetic.NoHelmet
- case CMT_TOGGLE_SHADES => Cosmetic.Sunglasses
- case CMT_TOGGLE_EARPIECE => Cosmetic.Earpiece
- case _ => null
- }
-
- val on = contents match {
- case "on" => true
- case "off" => false
- case _ => !cosmetics.contains(cosmetic)
- }
-
- avatarActor ! AvatarActor.SetCosmetics(
- if (on) cosmetics + cosmetic
- else cosmetics.diff(Set(cosmetic))
- )
-
- sessionActor ! SessionActor.SendResponse(
- message.copy(
- messageType = UNK_229,
- contents = s"@${message.messageType.toString}_${if (on) "on" else "off"}"
- )
- )
-
- case (CMT_ADDCERTIFICATION, _, contents) if gmCommandAllowed =>
- val certs = cliTokenization(contents).map(name => Certification.values.find(_.name == name))
- val result = if (certs.nonEmpty) {
- if (certs.contains(None)) {
- s"@AckErrorCertifications"
- } else {
- avatarActor ! AvatarActor.SetCertifications(session.avatar.certifications ++ certs.flatten)
- s"@AckSuccessCertifications"
- }
- } else {
- if (session.avatar.certifications.size < Certification.values.size) {
- avatarActor ! AvatarActor.SetCertifications(Certification.values.toSet)
- } else {
- avatarActor ! AvatarActor.SetCertifications(Certification.values.filter(_.cost == 0).toSet)
- }
- s"@AckSuccessCertifications"
- }
- sessionActor ! SessionActor.SendResponse(message.copy(messageType = UNK_229, contents = result))
-
- case (CMT_KICK, _, contents) if gmCommandAllowed =>
- val inputs = cliTokenization(contents)
- inputs.headOption match {
- case Some(input) =>
- val determination: Player => Boolean = input.toLongOption match {
- case Some(id) => _.CharId == id
- case _ => _.Name.equals(input)
- }
- session.zone.LivePlayers
- .find(determination)
- .orElse(session.zone.Corpses.find(determination)) match {
- case Some(player) =>
- inputs.lift(1).map(_.toLongOption) match {
- case Some(Some(time)) =>
- sessionActor ! SessionActor.Kick(player, Some(time))
- case _ =>
- sessionActor ! SessionActor.Kick(player)
- }
-
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(
- UNK_229,
- message.wideContents,
- "Server",
- "@kick_i",
- message.note
- )
- )
- case None =>
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(
- UNK_229,
- message.wideContents,
- "Server",
- "@kick_o",
- message.note
- )
- )
- }
- case None =>
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(
- UNK_229,
- message.wideContents,
- "Server",
- "@kick_o",
- message.note
- )
- )
- }
-
- case (_, "tr", contents) =>
- sessionActor ! SessionActor.SendResponse(
- ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0)
- )
-
- case (_, "nc", contents) =>
- sessionActor ! SessionActor.SendResponse(
- ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0)
- )
-
- case (_, "vs", contents) =>
- sessionActor ! SessionActor.SendResponse(
- ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0)
- )
-
- case (_, "bo", contents) =>
- sessionActor ! SessionActor.SendResponse(
- ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt)
- )
-
- case _ =>
- log.warn(s"Unhandled chat message $message")
- }
- Behaviors.same
-
- case IncomingMessage(fromSession, message, _/*channel*/) =>
- message.messageType match {
- case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
- if (AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)) {
- sessionActor ! SessionActor.SendResponse(message)
- }
- case CMT_OPEN =>
- if (
- session.zone == fromSession.zone &&
- Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 625 &&
- session.player.Faction == fromSession.player.Faction &&
- AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)
- ) {
- sessionActor ! SessionActor.SendResponse(message)
- }
- case CMT_TELL | U_CMT_TELLFROM |
- CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
- CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
- sessionActor ! SessionActor.SendResponse(message)
- case CMT_VOICE =>
- if (
- (session.zone == fromSession.zone || message.contents.startsWith("SH")) && /*tactical squad voice macro*/
- Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 1600
- ) {
- val name = fromSession.avatar.name
- if (!session.avatar.people.ignored.exists { f => f.name.equals(name) } ||
- {
- val id = fromSession.avatar.id.toLong
- val curr = System.currentTimeMillis()
- ignoredEmoteCooldown.get(id) match {
- case None =>
- ignoredEmoteCooldown.put(id, curr + 15000L)
- true
- case Some(time) if time < curr =>
- ignoredEmoteCooldown.put(id, curr + 15000L)
- true
- case _ =>
- false
- }}
- ) {
- sessionActor ! SessionActor.SendResponse(message)
- }
- }
- case CMT_SILENCE =>
- val args = cliTokenization(message.contents)
- val (name, time) = (args.headOption, args.lift(1)) match {
- case (Some(name), _) if name != session.player.Name =>
- log.error("Received silence message for other player")
- (None, None)
- case (Some(name), None) => (Some(name), Some(5))
- case (Some(name), Some(time)) if time.toIntOption.isDefined => (Some(name), Some(time.toInt))
- case _ => (None, None)
- }
- (name, time) match {
- case (Some(_), Some(time)) =>
- if (session.player.silenced) {
- sessionActor ! SessionActor.SetSilenced(false)
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_off", None)
- )
- if (!silenceTimer.isCancelled) silenceTimer.cancel()
- } else {
- sessionActor ! SessionActor.SetSilenced(true)
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_on", None)
- )
- silenceTimer = context.system.scheduler.scheduleOnce(
- time minutes,
- () => {
- sessionActor ! SessionActor.SetSilenced(false)
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_timeout", None)
- )
- }
- )
- }
-
- case (name, time) =>
- log.warn(s"Bad silence args $name $time")
- }
-
- case _ =>
- log.warn(s"Unexpected messageType $message")
-
- }
- Behaviors.same
- }
- .receiveSignal {
- case (_, _: PostStop) =>
- silenceTimer.cancel()
- chatService ! ChatService.LeaveAllChannels(chatServiceAdapter)
- Behaviors.same
- case _ =>
- Behaviors.same
- }
- }
-
- private def customCommandMessages(
- message: ChatMsg,
- session: Session,
- chatService: ActorRef[ChatService.Command],
- cluster: ActorRef[InterstellarClusterService.Command]
- ): Boolean = {
- val contents = message.contents
- if (contents.startsWith("!")) {
- val (command, params) = cliTokenization(contents.drop(1)) match {
- case a :: b => (a, b)
- case _ => ("", Seq(""))
- }
- val gmBangCommandAllowed = session.account.gm || Config.app.development.unprivilegedGmBangCommands.contains(command)
- //try gm commands
- val tryGmCommandResult = if (gmBangCommandAllowed) {
- command match {
- case "whitetext" => Some(customCommandWhitetext(session, params, chatService))
- case "list" => Some(customCommandList(session, params, message, sessionActor))
- case "ntu" => Some(customCommandNtu(session, params, sessionActor))
- case "zonerotate" => Some(customCommandZonerotate(params, cluster, sessionActor))
- case "nearby" => Some(customCommandNearby(session, sessionActor))
- case _ => None
- }
- } else {
- None
- }
- //try commands for all players if not caught as a gm command
- val result = tryGmCommandResult match {
- case None =>
- command match {
- case "loc" => customCommandLoc(session, message, sessionActor)
- case "suicide" => customCommandSuicide(session)
- case "grenade" => customCommandGrenade(session, log)
- case "macro" => customCommandMacro(session, params, sessionActor)
- case "progress" => customCommandProgress(session, params, avatarActor)
- case _ => false
- }
- case Some(out) =>
- out
- }
- if (!result) {
- // command was not handled
- sessionActor ! SessionActor.SendResponse(
- ChatMsg(
- CMT_GMOPEN, // CMT_GMTELL
- message.wideContents,
- "Server",
- s"Unknown command !$command",
- message.note
- )
- )
- }
- result
- } else {
- false // not a handled command
- }
- }
-}
diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala
index a3ee36e54..7ea23d760 100644
--- a/src/main/scala/net/psforever/actors/session/SessionActor.scala
+++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala
@@ -1,44 +1,25 @@
-// Copyright (c) 2016, 2020 PSForever
+// Copyright (c) 2016, 2020, 2024 PSForever
package net.psforever.actors.session
-import akka.actor.typed.receptionist.Receptionist
-import akka.actor.typed.scaladsl.adapter._
-import akka.actor.{Actor, MDCContextAware, typed}
+import akka.actor.{Actor, Cancellable, MDCContextAware, typed}
+import net.psforever.actors.session.normal.NormalMode
import org.joda.time.LocalDateTime
import org.log4s.MDC
+
import scala.collection.mutable
//
import net.psforever.actors.net.MiddlewareActor
-import net.psforever.actors.session.support.SessionData
-import net.psforever.objects._
-import net.psforever.objects.avatar._
-import net.psforever.objects.definition._
-import net.psforever.objects.guid._
-import net.psforever.objects.serverobject.containable.Containable
-import net.psforever.objects.serverobject.deploy.Deployment
-import net.psforever.objects.serverobject.mount.Mountable
-import net.psforever.objects.serverobject.terminals._
-import net.psforever.objects.serverobject.CommonMessages
-import net.psforever.objects.zones._
-import net.psforever.packet._
-import net.psforever.packet.game._
-import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
-import net.psforever.services.ServiceManager.{Lookup, LookupResult}
-import net.psforever.services.account.{PlayerToken, ReceiveAccountData}
-import net.psforever.services.avatar.AvatarServiceResponse
-import net.psforever.services.galaxy.GalaxyServiceResponse
-import net.psforever.services.local.LocalServiceResponse
-import net.psforever.services.teamwork.SquadServiceResponse
-import net.psforever.services.vehicle.VehicleServiceResponse
-import net.psforever.services.{CavernRotationService, ServiceManager, InterstellarClusterService => ICS}
-import net.psforever.types._
-import net.psforever.util.Config
+import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData}
+import net.psforever.objects.{Default, Player}
+import net.psforever.objects.avatar.Avatar
+import net.psforever.objects.definition.BasicDefinition
+import net.psforever.packet.PlanetSidePacket
+import net.psforever.packet.game.{FriendsResponse, KeepAliveMessage}
+import net.psforever.types.Vector3
object SessionActor {
sealed trait Command
- private[session] final case class PokeClient()
-
private[session] final case class ServerLoaded()
private[session] final case class NewPlayerLoaded(tplayer: Player)
@@ -87,521 +68,71 @@ object SessionActor {
private[session] case object CharSavedMsg extends Command
- /**
- * The message that progresses some form of user-driven activity with a certain eventual outcome
- * and potential feedback per cycle.
- * @param delta how much the progress value changes each tick, which will be treated as a percentage;
- * must be a positive value
- * @param completionAction a finalizing action performed once the progress reaches 100(%)
- * @param tickAction an action that is performed for each increase of progress
- * @param tickTime how long between each `tickAction` (ms);
- * defaults to 250 milliseconds
- */
- private[session] final case class ProgressEvent(
- delta: Float,
- completionAction: () => Unit,
- tickAction: Float => Boolean,
- tickTime: Long = 250L
- )
+ final case object StartHeartbeat extends Command
- private[session] final case class AvatarAwardMessageBundle(
- bundle: Iterable[Iterable[PlanetSideGamePacket]],
- delay: Long
- )
+ private final case object PokeClient extends Command
+
+ final case class SetMode(mode: PlayerMode) extends Command
}
class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
- extends Actor
+ extends Actor
with MDCContextAware {
MDC("connectionId") = connectionId
+ private var clientKeepAlive: Cancellable = Default.Cancellable
private[this] val buffer: mutable.ListBuffer[Any] = new mutable.ListBuffer[Any]()
- private[this] val sessionFuncs = new SessionData(middlewareActor, context)
-
- ServiceManager.serviceManager ! Lookup("accountIntermediary")
- ServiceManager.serviceManager ! Lookup("accountPersistence")
- ServiceManager.serviceManager ! Lookup("galaxy")
- ServiceManager.serviceManager ! Lookup("squad")
- ServiceManager.receptionist ! Receptionist.Find(ICS.InterstellarClusterServiceKey, context.self)
+ private[this] val data = new SessionData(middlewareActor, context)
+ private[this] var mode: PlayerMode = NormalMode
+ private[this] var logic: ModeLogic = _
override def postStop(): Unit = {
- //normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
- //TODO put any temporary values back into the avatar
- sessionFuncs.stop()
+ clientKeepAlive.cancel()
+ data.stop()
}
def receive: Receive = startup
- def startup: Receive = {
- case msg if !sessionFuncs.assignEventBus(msg) =>
+ private def startup: Receive = {
+ case msg if !data.assignEventBus(msg) =>
buffer.addOne(msg)
- case _ if sessionFuncs.whenAllEventBusesLoaded() =>
+ case _ if data.whenAllEventBusesLoaded() =>
context.become(inTheGame)
+ logic = mode.setup(data)
buffer.foreach { self.tell(_, self) } //we forget the original sender, shouldn't be doing callbacks at this point
buffer.clear()
case _ => ()
}
- def inTheGame: Receive = {
- /* really common messages (very frequently, every life) */
- case packet: PlanetSideGamePacket =>
- handleGamePkt(packet)
+ private def inTheGame: Receive = {
+ /* used for the game's heartbeat */
+ case SessionActor.StartHeartbeat =>
+ startHeartbeat()
- case AvatarServiceResponse(toChannel, guid, reply) =>
- sessionFuncs.avatarResponse.handle(toChannel, guid, reply)
+ case SessionActor.PokeClient =>
+ middlewareActor ! MiddlewareActor.Send(KeepAliveMessage())
- case GalaxyServiceResponse(_, reply) =>
- sessionFuncs.galaxyResponseHanders.handle(reply)
-
- case LocalServiceResponse(toChannel, guid, reply) =>
- sessionFuncs.localResponse.handle(toChannel, guid, reply)
-
- case Mountable.MountMessages(tplayer, reply) =>
- sessionFuncs.mountResponse.handle(tplayer, reply)
-
- case SquadServiceResponse(_, excluded, response) =>
- sessionFuncs.squad.handle(response, excluded)
-
- case Terminal.TerminalMessage(tplayer, msg, order) =>
- sessionFuncs.terminals.handle(tplayer, msg, order)
-
- case VehicleServiceResponse(toChannel, guid, reply) =>
- sessionFuncs.vehicleResponseOperations.handle(toChannel, guid, reply)
-
- case SessionActor.PokeClient() =>
- sessionFuncs.sendResponse(KeepAliveMessage())
-
- case SessionActor.SendResponse(packet) =>
- sessionFuncs.sendResponse(packet)
-
- case SessionActor.CharSaved =>
- sessionFuncs.renewCharSavedTimer(
- Config.app.game.savedMsg.interruptedByAction.fixed,
- Config.app.game.savedMsg.interruptedByAction.variable
- )
-
- case SessionActor.CharSavedMsg =>
- sessionFuncs.displayCharSavedMsgThenRenewTimer(
- Config.app.game.savedMsg.renewal.fixed,
- Config.app.game.savedMsg.renewal.variable
- )
-
- /* common messages (maybe once every respawn) */
- case ICS.SpawnPointResponse(response) =>
- sessionFuncs.zoning.handleSpawnPointResponse(response)
-
- case SessionActor.NewPlayerLoaded(tplayer) =>
- sessionFuncs.zoning.spawn.handleNewPlayerLoaded(tplayer)
-
- case SessionActor.PlayerLoaded(tplayer) =>
- sessionFuncs.zoning.spawn.handlePlayerLoaded(tplayer)
-
- case Zone.Population.PlayerHasLeft(zone, None) =>
- log.debug(s"PlayerHasLeft: ${sessionFuncs.player.Name} does not have a body on ${zone.id}")
-
- case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
- if (tplayer.isAlive) {
- log.info(s"${tplayer.Name} has left zone ${zone.id}")
+ case SessionActor.SetMode(newMode) =>
+ if (mode != newMode) {
+ logic.switchFrom(data.session)
}
+ mode = newMode
+ logic = mode.setup(data)
+ logic.switchTo(data.session)
- case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
- log.warning(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
-
- case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
- log.warning(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
-
- case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
- log.warning(
- s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason"
- )
-
- case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
- log.warning(
- s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason"
- )
-
- case ICS.ZoneResponse(Some(zone)) =>
- sessionFuncs.zoning.handleZoneResponse(zone)
-
- /* uncommon messages (once a session) */
- case ICS.ZonesResponse(zones) =>
- sessionFuncs.zoning.handleZonesResponse(zones)
-
- case SessionActor.SetAvatar(avatar) =>
- sessionFuncs.handleSetAvatar(avatar)
-
- case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
- sessionFuncs.zoning.spawn.handleLoginInfoNowhere(name, sender())
-
- case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
- sessionFuncs.zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender())
-
- case PlayerToken.RestoreInfo(playerName, inZone, pos) =>
- sessionFuncs.zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender())
-
- case PlayerToken.CanNotLogin(playerName, reason) =>
- sessionFuncs.zoning.spawn.handleLoginCanNot(playerName, reason)
-
- case ReceiveAccountData(account) =>
- sessionFuncs.handleReceiveAccountData(account)
-
- case AvatarActor.AvatarResponse(avatar) =>
- sessionFuncs.handleAvatarResponse(avatar)
-
- case AvatarActor.AvatarLoginResponse(avatar) =>
- sessionFuncs.zoning.spawn.avatarLoginResponse(avatar)
-
- case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) =>
- sessionFuncs.zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt)
-
- case SessionActor.SetConnectionState(state) =>
- sessionFuncs.connectionState = state
-
- case SessionActor.AvatarLoadingSync(state) =>
- sessionFuncs.zoning.spawn.handleAvatarLoadingSync(state)
-
- /* uncommon messages (utility, or once in a while) */
- case SessionActor.AvatarAwardMessageBundle(pkts, delay) =>
- sessionFuncs.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)
-
- case CommonMessages.Progress(rate, finishedAction, stepAction) =>
- sessionFuncs.setupProgressChange(rate, finishedAction, stepAction)
-
- case SessionActor.ProgressEvent(delta, finishedAction, stepAction, tick) =>
- sessionFuncs.handleProgressChange(delta, finishedAction, stepAction, tick)
-
- case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
- listings.head ! SendCavernRotationUpdates(context.self)
-
- case LookupResult("propertyOverrideManager", endpoint) =>
- sessionFuncs.zoning.propertyOverrideManagerLoadOverrides(endpoint)
-
- case SessionActor.UpdateIgnoredPlayers(msg) =>
- sessionFuncs.handleUpdateIgnoredPlayers(msg)
-
- case SessionActor.UseCooldownRenewed(definition, _) =>
- sessionFuncs.handleUseCooldownRenew(definition)
-
- case Deployment.CanDeploy(obj, state) =>
- sessionFuncs.vehicles.handleCanDeploy(obj, state)
-
- case Deployment.CanUndeploy(obj, state) =>
- sessionFuncs.vehicles.handleCanUndeploy(obj, state)
-
- case Deployment.CanNotChangeDeployment(obj, state, reason) =>
- sessionFuncs.vehicles.handleCanNotChangeDeployment(obj, state, reason)
-
- /* rare messages */
- case ProximityUnit.StopAction(term, _) =>
- sessionFuncs.terminals.LocalStopUsingProximityUnit(term)
-
- case SessionActor.Suicide() =>
- sessionFuncs.suicide(sessionFuncs.player)
-
- case SessionActor.Recall() =>
- sessionFuncs.zoning.handleRecall()
-
- case SessionActor.InstantAction() =>
- sessionFuncs.zoning.handleInstantAction()
-
- case SessionActor.Quit() =>
- sessionFuncs.zoning.handleQuit()
-
- case ICS.DroppodLaunchDenial(errorCode, _) =>
- sessionFuncs.zoning.handleDroppodLaunchDenial(errorCode)
-
- case ICS.DroppodLaunchConfirmation(zone, position) =>
- sessionFuncs.zoning.LoadZoneLaunchDroppod(zone, position)
-
- case SessionActor.PlayerFailedToLoad(tplayer) =>
- sessionFuncs.failWithError(s"${tplayer.Name} failed to load anywhere")
-
- /* csr only */
- case SessionActor.SetSpeed(speed) =>
- sessionFuncs.handleSetSpeed(speed)
-
- case SessionActor.SetFlying(isFlying) =>
- sessionFuncs.handleSetFlying(isFlying)
-
- case SessionActor.SetSpectator(isSpectator) =>
- sessionFuncs.handleSetSpectator(isSpectator)
-
- case SessionActor.Kick(player, time) =>
- sessionFuncs.handleKick(player, time)
-
- case SessionActor.SetZone(zoneId, position) =>
- sessionFuncs.zoning.handleSetZone(zoneId, position)
-
- case SessionActor.SetPosition(position) =>
- sessionFuncs.zoning.spawn.handleSetPosition(position)
-
- case SessionActor.SetSilenced(silenced) =>
- sessionFuncs.handleSilenced(silenced)
-
- /* catch these messages */
- case _: ProximityUnit.Action => ;
-
- case _: Zone.Vehicle.HasSpawned => ;
-
- case _: Zone.Vehicle.HasDespawned => ;
-
- case Zone.Deployable.IsDismissed(obj: TurretDeployable) => //only if target deployable was never fully introduced
- TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(sessionFuncs.continent.GUID, obj))
-
- case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced
- TaskWorkflow.execute(GUIDTask.unregisterObject(sessionFuncs.continent.GUID, obj))
-
- case msg: Containable.ItemPutInSlot =>
- log.debug(s"ItemPutInSlot: $msg")
-
- case msg: Containable.CanNotPutItemInSlot =>
- log.debug(s"CanNotPutItemInSlot: $msg")
-
- case default =>
- log.warning(s"Invalid packet class received: $default from ${sender()}")
+ case packet =>
+ logic.parse(sender())(packet)
}
- private def handleGamePkt: PlanetSideGamePacket => Unit = {
- case packet: ConnectToWorldRequestMessage =>
- sessionFuncs.handleConnectToWorldRequest(packet)
-
- case packet: MountVehicleCargoMsg =>
- sessionFuncs.vehicles.handleMountVehicleCargo(packet)
-
- case packet: DismountVehicleCargoMsg =>
- sessionFuncs.vehicles.handleDismountVehicleCargo(packet)
-
- case packet: CharacterCreateRequestMessage =>
- sessionFuncs.handleCharacterCreateRequest(packet)
-
- case packet: CharacterRequestMessage =>
- sessionFuncs.handleCharacterRequest(packet)
-
- case _: KeepAliveMessage =>
- sessionFuncs.keepAliveFunc()
-
- case packet: BeginZoningMessage =>
- sessionFuncs.zoning.handleBeginZoning(packet)
-
- case packet: PlayerStateMessageUpstream =>
- sessionFuncs.handlePlayerStateUpstream(packet)
-
- case packet: ChildObjectStateMessage =>
- sessionFuncs.vehicles.handleChildObjectState(packet)
-
- case packet: VehicleStateMessage =>
- sessionFuncs.vehicles.handleVehicleState(packet)
-
- case packet: VehicleSubStateMessage =>
- sessionFuncs.vehicles.handleVehicleSubState(packet)
-
- case packet: FrameVehicleStateMessage =>
- sessionFuncs.vehicles.handleFrameVehicleState(packet)
-
- case packet: ProjectileStateMessage =>
- sessionFuncs.shooting.handleProjectileState(packet)
-
- case packet: LongRangeProjectileInfoMessage =>
- sessionFuncs.shooting.handleLongRangeProjectileState(packet)
-
- case packet: ReleaseAvatarRequestMessage =>
- sessionFuncs.zoning.spawn.handleReleaseAvatarRequest(packet)
-
- case packet: SpawnRequestMessage =>
- sessionFuncs.zoning.spawn.handleSpawnRequest(packet)
-
- case packet: ChatMsg =>
- sessionFuncs.handleChat(packet)
-
- case packet: SetChatFilterMessage =>
- sessionFuncs.handleChatFilter(packet)
-
- case packet: VoiceHostRequest =>
- sessionFuncs.handleVoiceHostRequest(packet)
-
- case packet: VoiceHostInfo =>
- sessionFuncs.handleVoiceHostInfo(packet)
-
- case packet: ChangeAmmoMessage =>
- sessionFuncs.shooting.handleChangeAmmo(packet)
-
- case packet: ChangeFireModeMessage =>
- sessionFuncs.shooting.handleChangeFireMode(packet)
-
- case packet: ChangeFireStateMessage_Start =>
- sessionFuncs.shooting.handleChangeFireStateStart(packet)
-
- case packet: ChangeFireStateMessage_Stop =>
- sessionFuncs.shooting.handleChangeFireStateStop(packet)
-
- case packet: EmoteMsg =>
- sessionFuncs.handleEmote(packet)
-
- case packet: DropItemMessage =>
- sessionFuncs.handleDropItem(packet)
-
- case packet: PickupItemMessage =>
- sessionFuncs.handlePickupItem(packet)
-
- case packet: ReloadMessage =>
- sessionFuncs.shooting.handleReload(packet)
-
- case packet: ObjectHeldMessage =>
- sessionFuncs.handleObjectHeld(packet)
-
- case packet: AvatarJumpMessage =>
- sessionFuncs.handleAvatarJump(packet)
-
- case packet: ZipLineMessage =>
- sessionFuncs.handleZipLine(packet)
-
- case packet: RequestDestroyMessage =>
- sessionFuncs.handleRequestDestroy(packet)
-
- case packet: MoveItemMessage =>
- sessionFuncs.handleMoveItem(packet)
-
- case packet: LootItemMessage =>
- sessionFuncs.handleLootItem(packet)
-
- case packet: AvatarImplantMessage =>
- sessionFuncs.handleAvatarImplant(packet)
-
- case packet: UseItemMessage =>
- sessionFuncs.handleUseItem(packet)
-
- case packet: UnuseItemMessage =>
- sessionFuncs.handleUnuseItem(packet)
-
- case packet: ProximityTerminalUseMessage =>
- sessionFuncs.terminals.handleProximityTerminalUse(packet)
-
- case packet: DeployObjectMessage =>
- sessionFuncs.handleDeployObject(packet)
-
- case packet: GenericObjectActionMessage =>
- sessionFuncs.handleGenericObjectAction(packet)
-
- case packet: GenericObjectActionAtPositionMessage =>
- sessionFuncs.handleGenericObjectActionAtPosition(packet)
-
- case packet: GenericObjectStateMsg =>
- sessionFuncs.handleGenericObjectState(packet)
-
- case packet: GenericActionMessage =>
- sessionFuncs.handleGenericAction(packet)
-
- case packet: ItemTransactionMessage =>
- sessionFuncs.terminals.handleItemTransaction(packet)
-
- case packet: FavoritesRequest =>
- sessionFuncs.handleFavoritesRequest(packet)
-
- case packet: WeaponDelayFireMessage =>
- sessionFuncs.shooting.handleWeaponDelayFire(packet)
-
- case packet: WeaponDryFireMessage =>
- sessionFuncs.shooting.handleWeaponDryFire(packet)
-
- case packet: WeaponFireMessage =>
- sessionFuncs.shooting.handleWeaponFire(packet)
-
- case packet: WeaponLazeTargetPositionMessage =>
- sessionFuncs.shooting.handleWeaponLazeTargetPosition(packet)
-
- case packet: HitMessage =>
- sessionFuncs.shooting.handleDirectHit(packet)
-
- case packet: SplashHitMessage =>
- sessionFuncs.shooting.handleSplashHit(packet)
-
- case packet: LashMessage =>
- sessionFuncs.shooting.handleLashHit(packet)
-
- case packet: AIDamage =>
- sessionFuncs.shooting.handleAIDamage(packet)
-
- case packet: AvatarFirstTimeEventMessage =>
- sessionFuncs.handleAvatarFirstTimeEvent(packet)
-
- case packet: WarpgateRequest =>
- sessionFuncs.zoning.handleWarpgateRequest(packet)
-
- case packet: MountVehicleMsg =>
- sessionFuncs.vehicles.handleMountVehicle(packet)
-
- case packet: DismountVehicleMsg =>
- sessionFuncs.vehicles.handleDismountVehicle(packet)
-
- case packet: DeployRequestMessage =>
- sessionFuncs.vehicles.handleDeployRequest(packet)
-
- case packet: AvatarGrenadeStateMessage =>
- sessionFuncs.shooting.handleAvatarGrenadeState(packet)
-
- case packet: SquadDefinitionActionMessage =>
- sessionFuncs.squad.handleSquadDefinitionAction(packet)
-
- case packet: SquadMembershipRequest =>
- sessionFuncs.squad.handleSquadMemberRequest(packet)
-
- case packet: SquadWaypointRequest =>
- sessionFuncs.squad.handleSquadWaypointRequest(packet)
-
- case packet: GenericCollisionMsg =>
- sessionFuncs.handleGenericCollision(packet)
-
- case packet: BugReportMessage =>
- sessionFuncs.handleBugReport(packet)
-
- case packet: BindPlayerMessage =>
- sessionFuncs.handleBindPlayer(packet)
-
- case packet: PlanetsideAttributeMessage =>
- sessionFuncs.handlePlanetsideAttribute(packet)
-
- case packet: FacilityBenefitShieldChargeRequestMessage =>
- sessionFuncs.handleFacilityBenefitShieldChargeRequest(packet)
-
- case packet: BattleplanMessage =>
- sessionFuncs.handleBattleplan(packet)
-
- case packet: CreateShortcutMessage =>
- sessionFuncs.handleCreateShortcut(packet)
-
- case packet: ChangeShortcutBankMessage =>
- sessionFuncs.handleChangeShortcutBank(packet)
-
- case packet: FriendsRequest =>
- sessionFuncs.handleFriendRequest(packet)
-
- case packet: DroppodLaunchRequestMessage =>
- sessionFuncs.zoning.handleDroppodLaunchRequest(packet)
-
- case packet: InvalidTerrainMessage =>
- sessionFuncs.handleInvalidTerrain(packet)
-
- case packet: ActionCancelMessage =>
- sessionFuncs.handleActionCancel(packet)
-
- case packet: TradeMessage =>
- sessionFuncs.handleTrade(packet)
-
- case packet: DisplayedAwardMessage =>
- sessionFuncs.handleDisplayedAward(packet)
-
- case packet: ObjectDetectedMessage =>
- sessionFuncs.handleObjectDetected(packet)
-
- case packet: TargetingImplantRequest =>
- sessionFuncs.handleTargetingImplantRequest(packet)
-
- case packet: HitHint =>
- sessionFuncs.handleHitHint(packet)
-
- case _: OutfitRequest => ()
-
- case pkt =>
- log.warning(s"Unhandled GamePacket $pkt")
+ private def startHeartbeat(): Unit = {
+ import scala.concurrent.duration._
+ import scala.concurrent.ExecutionContext.Implicits.global
+ clientKeepAlive.cancel()
+ clientKeepAlive = context.system.scheduler.scheduleWithFixedDelay(
+ initialDelay = 0.seconds,
+ delay = 500.milliseconds,
+ context.self,
+ SessionActor.PokeClient
+ )
}
}
diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala
new file mode 100644
index 000000000..0a84b1d3a
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala
@@ -0,0 +1,573 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.support.AvatarHandlerFunctions
+
+import scala.concurrent.duration._
+//
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionAvatarHandlers, SessionData}
+import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
+import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle}
+import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
+import net.psforever.objects.inventory.InventoryItem
+import net.psforever.objects.serverobject.pad.VehicleSpawnPad
+import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
+import net.psforever.objects.vital.etc.ExplodingEntityReason
+import net.psforever.objects.zones.Zoning
+import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
+import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage}
+import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage}
+import net.psforever.services.Service
+import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3}
+import net.psforever.util.Config
+
+object AvatarHandlerLogic {
+ def apply(ops: SessionAvatarHandlers): AvatarHandlerLogic = {
+ new AvatarHandlerLogic(ops, ops.context)
+ }
+}
+
+class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: ActorContext) extends AvatarHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /**
+ * na
+ * @param toChannel na
+ * @param guid na
+ * @param reply na
+ */
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = {
+ val resolvedPlayerGuid = if (player != null && player.HasGUID) {
+ player.GUID
+ } else {
+ Service.defaultPlayerGUID
+ }
+ val isNotSameTarget = resolvedPlayerGuid != guid
+ val isSameTarget = !isNotSameTarget
+ reply match {
+ /* special messages */
+ case AvatarResponse.TeardownConnection() =>
+ log.trace(s"ending ${player.Name}'s old session by event system request (relog)")
+ context.stop(context.self)
+
+ /* really common messages (very frequently, every life) */
+ case pstate @ AvatarResponse.PlayerState(
+ pos,
+ vel,
+ yaw,
+ pitch,
+ yawUpper,
+ _,
+ isCrouching,
+ isJumping,
+ jumpThrust,
+ isCloaking,
+ isNotRendered,
+ canSeeReallyFar
+ ) if isNotSameTarget =>
+ val pstateToSave = pstate.copy(timestamp = 0)
+ val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = ops.lastSeenStreamMessage.get(guid.guid) match {
+ case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting)
+ case _ => (None, 0L, Vector3.Zero, false, None)
+ }
+ val drawConfig = Config.app.game.playerDraw //m
+ val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m
+ val ourPosition = player.Position //xyz
+ val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m
+ val inDrawableRange = currentDistance <= maxRange
+ val now = System.currentTimeMillis() //ms
+ if (
+ sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing &&
+ !isNotRendered && inDrawableRange
+ ) {
+ //conditions where visibility is assured
+ val durationSince = now - lastTime //ms
+ lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange
+ lazy val targetDelay = {
+ val populationOver = math.max(
+ 0,
+ sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold
+ )
+ val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m
+ val adjustedDistance = currentDistance + distanceAdjustment //sq.m
+ drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match {
+ case -1 => 1
+ case index => drawConfig.delays(index)
+ }
+ } //ms
+ if (!wasVisible ||
+ !previouslyInDrawableRange ||
+ durationSince > drawConfig.delayMax ||
+ (!lastMsg.contains(pstateToSave) &&
+ (canSeeReallyFar ||
+ currentDistance < drawConfig.rangeMin * drawConfig.rangeMin ||
+ sessionLogic.general.canSeeReallyFar ||
+ durationSince > targetDelay
+ )
+ )
+ ) {
+ //must draw
+ sendResponse(
+ PlayerStateMessage(
+ guid,
+ pos,
+ vel,
+ yaw,
+ pitch,
+ yawUpper,
+ timestamp = 0, //is this okay?
+ isCrouching,
+ isJumping,
+ jumpThrust,
+ isCloaking
+ )
+ )
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now))
+ } else {
+ //is visible, but skip reinforcement
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime))
+ }
+ } else {
+ //conditions where the target is not currently visible
+ if (wasVisible) {
+ //the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance
+ val lat = (1 + ops.hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat
+ sendResponse(
+ PlayerStateMessage(
+ guid,
+ Vector3(1f, lat, 1f),
+ vel=None,
+ facingYaw=0f,
+ facingPitch=0f,
+ facingYawUpper=0f,
+ timestamp=0, //is this okay?
+ is_cloaked = isCloaking
+ )
+ )
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now))
+ } else {
+ //skip drawing altogether
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime))
+ }
+ }
+
+ case AvatarResponse.ObjectHeld(slot, _)
+ if isSameTarget && player.VisibleSlots.contains(slot) =>
+ sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
+ //Stop using proximity terminals if player unholsters a weapon
+ continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect {
+ case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term)
+ }
+ if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
+ sessionLogic.zoning.spawn.stopDeconstructing()
+ }
+
+ case AvatarResponse.ObjectHeld(slot, _)
+ if isSameTarget && slot > -1 =>
+ sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
+
+ case AvatarResponse.ObjectHeld(_, _)
+ if isSameTarget => ()
+
+ case AvatarResponse.ObjectHeld(_, previousSlot) =>
+ sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false))
+
+ case AvatarResponse.ChangeFireState_Start(weaponGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ sendResponse(ChangeFireStateMessage_Start(weaponGuid))
+ val entry = ops.lastSeenStreamMessage(guid.guid)
+ ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid)))
+
+ case AvatarResponse.ChangeFireState_Start(weaponGuid)
+ if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Start(weaponGuid))
+
+ case AvatarResponse.ChangeFireState_Stop(weaponGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } =>
+ sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+ val entry = ops.lastSeenStreamMessage(guid.guid)
+ ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None))
+
+ case AvatarResponse.ChangeFireState_Stop(weaponGuid)
+ if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+
+ case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
+
+ case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) =>
+ sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
+
+ case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
+
+ case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
+ sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
+
+ case AvatarResponse.HitHint(sourceGuid) if player.isAlive =>
+ sendResponse(HitHint(sourceGuid, guid))
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
+
+ case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
+ // guid = victim // killer = killer
+ sendResponse(DestroyMessage(victim, killer, weapon, pos))
+
+ case AvatarResponse.DestroyDisplay(killer, victim, method, unk) =>
+ sendResponse(ops.destroyDisplayMessage(killer, victim, method, unk))
+
+ case AvatarResponse.TerminalOrderResult(terminalGuid, action, result)
+ if result && (action == TransactionType.Buy || action == TransactionType.Loadout) =>
+ sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
+ sessionLogic.terminals.lastTerminalOrderFulfillment = true
+ AvatarActor.savePlayerData(player)
+ sessionLogic.general.renewCharSavedTimer(
+ Config.app.game.savedMsg.interruptedByAction.fixed,
+ Config.app.game.savedMsg.interruptedByAction.variable
+ )
+
+ case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
+ sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
+ sessionLogic.terminals.lastTerminalOrderFulfillment = true
+
+ case AvatarResponse.ChangeExosuit(
+ target,
+ armor,
+ exosuit,
+ subtype,
+ _,
+ maxhand,
+ oldHolsters,
+ holsters,
+ oldInventory,
+ inventory,
+ drop,
+ delete
+ ) if resolvedPlayerGuid == target =>
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
+ //happening to this player
+ //cleanup
+ sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false))
+ (oldHolsters ++ oldInventory ++ delete).foreach {
+ case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+ }
+ //functionally delete
+ delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) }
+ //redraw
+ if (maxhand) {
+ TaskWorkflow.execute(HoldNewEquipmentUp(player)(
+ Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
+ 0
+ ))
+ }
+ //draw free hand
+ player.FreeHand.Equipment.foreach { obj =>
+ val definition = obj.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(target, Player.FreeHandSlot),
+ definition.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ }
+ //draw holsters and inventory
+ (holsters ++ inventory).foreach {
+ case InventoryItem(obj, index) =>
+ val definition = obj.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(target, index),
+ definition.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ }
+ DropLeftovers(player)(drop)
+
+ case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, _, delete) =>
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
+ //happening to some other player
+ sendResponse(ObjectHeldMessage(target, slot, unk1 = false))
+ //cleanup
+ (oldHolsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
+ //draw holsters
+ holsters.foreach {
+ case InventoryItem(obj, index) =>
+ val definition = obj.Definition
+ sendResponse(
+ ObjectCreateMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(target, index),
+ definition.Packet.ConstructorData(obj).get
+ )
+ )
+ }
+
+ case AvatarResponse.ChangeLoadout(
+ target,
+ armor,
+ exosuit,
+ subtype,
+ _,
+ maxhand,
+ oldHolsters,
+ holsters,
+ oldInventory,
+ inventory,
+ drops
+ ) if resolvedPlayerGuid == target =>
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type = 4, armor))
+ //happening to this player
+ sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true))
+ //cleanup
+ (oldHolsters ++ oldInventory).foreach {
+ case (obj, objGuid) =>
+ sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
+ TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
+ }
+ drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
+ //redraw
+ if (maxhand) {
+ TaskWorkflow.execute(HoldNewEquipmentUp(player)(
+ Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
+ slot = 0
+ ))
+ }
+ sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
+ DropLeftovers(player)(drops)
+
+ case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) =>
+ //redraw handled by callbacks
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
+ //happening to some other player
+ sendResponse(ObjectHeldMessage(target, slot, unk1=false))
+ //cleanup
+ oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
+
+ case AvatarResponse.UseKit(kguid, kObjId) =>
+ sendResponse(
+ UseItemMessage(
+ resolvedPlayerGuid,
+ kguid,
+ resolvedPlayerGuid,
+ unk2 = 4294967295L,
+ unk3 = false,
+ unk4 = Vector3.Zero,
+ unk5 = Vector3.Zero,
+ unk6 = 126,
+ unk7 = 0, //sequence time?
+ unk8 = 137,
+ kObjId
+ )
+ )
+ sendResponse(ObjectDeleteMessage(kguid, unk1=0))
+
+ case AvatarResponse.KitNotUsed(_, "") =>
+ sessionLogic.general.kitToBeUsed = None
+
+ case AvatarResponse.KitNotUsed(_, msg) =>
+ sessionLogic.general.kitToBeUsed = None
+ sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
+
+ case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
+ avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
+
+ case AvatarResponse.AwardBep(charId, bep, expType) =>
+ //if the target player, always award (some) BEP
+ if (charId == player.CharId) {
+ avatarActor ! AvatarActor.AwardBep(bep, expType)
+ }
+
+ case AvatarResponse.AwardCep(charId, cep) =>
+ //if the target player, always award (some) CEP
+ if (charId == player.CharId) {
+ avatarActor ! AvatarActor.AwardCep(cep)
+ }
+
+ case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
+ ops.facilityCaptureRewards(buildingId, zoneNumber, cep)
+
+ case AvatarResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid =>
+ sendResponse(msg)
+
+ /* common messages (maybe once every respawn) */
+ case AvatarResponse.Reload(itemGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
+
+ case AvatarResponse.Killed(mount) =>
+ //log and chat messages
+ val cause = player.LastDamage.flatMap { damage =>
+ val interaction = damage.interaction
+ val reason = interaction.cause
+ val adversarial = interaction.adversarial.map { _.attacker }
+ reason match {
+ case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] =>
+ //also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
+ case _ => ()
+ }
+ adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
+ }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
+ log.info(s"${player.Name} has died, killed by $cause")
+ if (sessionLogic.shooting.shotsWhileDead > 0) {
+ log.warn(
+ s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server"
+ )
+ sessionLogic.shooting.shotsWhileDead = 0
+ }
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel")
+ sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
+
+ //player state changes
+ AvatarActor.updateToolDischargeFor(avatar)
+ player.FreeHand.Equipment.foreach { item =>
+ DropEquipmentFromInventory(player)(item)
+ }
+ sessionLogic.general.dropSpecialSlotItem()
+ sessionLogic.general.toggleMaxSpecialState(enable = false)
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+ sessionLogic.zoning.zoningStatus = Zoning.Status.None
+ sessionLogic.zoning.spawn.deadState = DeadState.Dead
+ continent.GUID(mount).collect { case obj: Vehicle =>
+ sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
+ sessionLogic.general.unaccessContainer(obj)
+ }
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
+ AvatarActor.savePlayerLocation(player)
+ sessionLogic.zoning.spawn.shiftPosition = Some(player.Position)
+
+ //respawn
+ sessionLogic.zoning.spawn.reviveTimer.cancel()
+ if (player.death_by == 0) {
+ sessionLogic.zoning.spawn.randomRespawn(300.seconds)
+ } else {
+ sessionLogic.zoning.spawn.HandleReleaseAvatar(player, continent)
+ }
+
+ case AvatarResponse.Release(tplayer) if isNotSameTarget =>
+ sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer)
+
+ case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid =>
+ log.info(s"No time for rest, ${player.Name}. Back on your feet!")
+ sessionLogic.zoning.spawn.reviveTimer.cancel()
+ sessionLogic.zoning.spawn.deadState = DeadState.Alive
+ player.Revive
+ val health = player.Health
+ sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health))
+ sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true))
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health)
+ )
+
+ /* uncommon messages (utility, or once in a while) */
+ case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
+ sendResponse(ChangeAmmoMessage(weapon_guid, 1))
+
+ case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
+ if isNotSameTarget =>
+ ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
+
+ case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget =>
+ sendResponse(ChangeFireModeMessage(itemGuid, mode))
+
+ case AvatarResponse.ConcealPlayer() =>
+ sendResponse(GenericObjectActionMessage(guid, code=9))
+
+ case AvatarResponse.EnvironmentalDamage(_, _, _) =>
+ //TODO damage marker?
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
+
+ case AvatarResponse.DropItem(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget =>
+ sendResponse(ObjectDeleteMessage(itemGuid, unk))
+
+ /* rare messages */
+ case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget =>
+ sendResponse(SetEmpireMessage(objectGuid, faction))
+
+ case AvatarResponse.DropSpecialItem() =>
+ sessionLogic.general.dropSpecialSlotItem()
+
+ case AvatarResponse.OxygenState(player, vehicle) =>
+ sendResponse(OxygenStateMessage(
+ DrowningTarget(player.guid, player.progress, player.state),
+ vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) }
+ ))
+
+ case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget =>
+ sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid))
+
+ case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) =>
+ sendResponse(
+ ProjectileStateMessage(
+ projectileGuid,
+ projectile.Position,
+ shot_vel = Vector3.Zero,
+ projectile.Orientation,
+ sequence_num=0,
+ end=true,
+ hit_target_guid=PlanetSideGUID(0)
+ )
+ )
+ sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2))
+
+ case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
+ sendResponse(GenericActionMessage(mode))
+
+ case AvatarResponse.PutDownFDU(target) if isNotSameTarget =>
+ sendResponse(GenericObjectActionMessage(target, code=53))
+
+ case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget =>
+ val definition = item.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ item.GUID,
+ ObjectCreateMessageParent(target, slot),
+ definition.Packet.DetailedConstructorData(item).get
+ )
+ )
+
+ case AvatarResponse.WeaponDryFire(weaponGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ continent.GUID(weaponGuid).collect {
+ case tool: Tool if tool.Magazine == 0 =>
+ // check that the magazine is still empty before sending WeaponDryFireMessage
+ // if it has been reloaded since then, other clients will not see it firing
+ sendResponse(WeaponDryFireMessage(weaponGuid))
+ }
+
+ case _ => ()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala
new file mode 100644
index 000000000..dc599e017
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala
@@ -0,0 +1,239 @@
+// Copyright (c) 2024 PSForever
+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
+import net.psforever.util.Config
+
+object ChatLogic {
+ def apply(ops: ChatOperations): ChatLogic = {
+ new ChatLogic(ops, ops.context)
+ }
+}
+
+class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ def handleChatMsg(message: ChatMsg): Unit = {
+ import net.psforever.types.ChatMessageType._
+ 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("!") &&
+ customCommandMessages(message, session) => ()
+
+ case (CMT_FLY, recipient, contents) if gmCommandAllowed =>
+ ops.commandFly(contents, recipient)
+
+ case (CMT_ANONYMOUS, _, _) =>
+ // ?
+
+ case (CMT_TOGGLE_GM, _, _) =>
+ // ?
+
+ case (CMT_CULLWATERMARK, _, contents) =>
+ ops.commandWatermark(contents)
+
+ case (CMT_SPEED, _, contents) if gmCommandAllowed =>
+ ops.commandSpeed(message, contents)
+
+ case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive && (gmCommandAllowed || perms.canSpectate) =>
+ ops.commandToggleSpectatorMode(session, contents)
+
+ case (CMT_RECALL, _, _) =>
+ ops.commandRecall(session)
+
+ case (CMT_INSTANTACTION, _, _) =>
+ ops.commandInstantAction(session)
+
+ case (CMT_QUIT, _, _) =>
+ ops.commandQuit(session)
+
+ case (CMT_SUICIDE, _, _) =>
+ ops.commandSuicide(session)
+
+ case (CMT_DESTROY, _, contents) if contents.matches("\\d+") =>
+ ops.commandDestroy(session, message, contents)
+
+ case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
+ ops.commandSetBaseResources(session, contents)
+
+ case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
+ ops.commandZoneLock(contents)
+
+ case (U_CMT_ZONEROTATE, _, _) if gmCommandAllowed =>
+ ops.commandZoneRotate()
+
+ case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
+ ops.commandCaptureBase(session, message, contents)
+
+ case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
+ if gmCommandAllowed =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_GMTELL, _, _) if gmCommandAllowed =>
+ ops.commandSend(session, message, DefaultChannel)
+
+ case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ 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 !player.silenced =>
+ ops.commandTellOrIgnore(session, message, DefaultChannel)
+
+ case (CMT_BROADCAST, _, _) if !player.silenced =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_PLATOON, _, _) if !player.silenced =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_COMMAND, _, _) if gmCommandAllowed =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_NOTE, _, _) =>
+ ops.commandSend(session, message, DefaultChannel)
+
+ case (CMT_SILENCE, _, _) if gmCommandAllowed =>
+ ops.commandSend(session, message, DefaultChannel)
+
+ case (CMT_SQUAD, _, _) =>
+ ops.commandSquad(session, message, DefaultChannel) //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)
+
+ case (CMT_ZONE, _, contents) if gmCommandAllowed =>
+ ops.commandZone(message, contents)
+
+ case (CMT_WARP, _, contents) if gmCommandAllowed =>
+ ops.commandWarp(session, message, contents)
+
+ case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
+ ops.commandSetBattleRank(session, message, contents)
+
+ case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
+ ops.commandSetCommandRank(session, message, contents)
+
+ case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
+ ops.commandAddBattleExperience(message, contents)
+
+ case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if gmCommandAllowed =>
+ ops.commandAddCommandExperience(message, contents)
+
+ case (CMT_TOGGLE_HAT, _, contents) =>
+ ops.commandToggleHat(session, message, contents)
+
+ case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
+ ops.commandToggleCosmetics(session, message, contents)
+
+ case (CMT_ADDCERTIFICATION, _, contents) if gmCommandAllowed =>
+ ops.commandAddCertification(session, message, contents)
+
+ case (CMT_KICK, _, contents) if gmCommandAllowed =>
+ ops.commandKick(session, message, contents)
+
+ case _ =>
+ log.warn(s"Unhandled chat message $message")
+ }
+ }
+
+ def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
+ val SetChatFilterMessage(_, _, _) = pkt
+ }
+
+ def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
+ import ChatMessageType._
+ message.messageType match {
+ case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
+ ops.commandIncomingSendAllIfOnline(session, message)
+
+ case CMT_OPEN =>
+ ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
+
+ case CMT_TELL | U_CMT_TELLFROM |
+ CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
+ CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
+ ops.commandIncomingSend(message)
+
+ case CMT_VOICE =>
+ ops.commandIncomingVoice(session, fromSession, message)
+
+ case CMT_SILENCE =>
+ ops.commandIncomingSilence(session, message)
+
+ case _ =>
+ log.warn(s"Unexpected messageType $message")
+ }
+ }
+
+ private def customCommandMessages(
+ message: ChatMsg,
+ session: Session
+ ): Boolean = {
+ val contents = message.contents
+ if (contents.startsWith("!")) {
+ val (command, params) = ops.cliTokenization(contents.drop(1)) match {
+ case a :: b => (a, b)
+ case _ => ("", Seq(""))
+ }
+ 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 {
+ case "whitetext" => Some(ops.customCommandWhitetext(session, params))
+ case "list" => Some(ops.customCommandList(session, params, message))
+ case "ntu" => Some(ops.customCommandNtu(session, params))
+ case "zonerotate" => Some(ops.customCommandZonerotate(params))
+ case "nearby" => Some(ops.customCommandNearby(session))
+ case _ => None
+ }
+ } else {
+ None
+ }
+ //try commands for all players if not caught as a gm command
+ val result = tryGmCommandResult match {
+ case None =>
+ command match {
+ case "loc" => ops.customCommandLoc(session, message)
+ case "suicide" => ops.customCommandSuicide(session)
+ case "grenade" => ops.customCommandGrenade(session, log)
+ case "macro" => ops.customCommandMacro(session, params)
+ case "progress" => ops.customCommandProgress(session, params)
+ case _ => false
+ }
+ case Some(out) =>
+ out
+ }
+ if (!result) {
+ // command was not handled
+ sendResponse(
+ ChatMsg(
+ ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
+ message.wideContents,
+ "Server",
+ s"Unknown command !$command",
+ message.note
+ )
+ )
+ }
+ result
+ } else {
+ false // not a handled command
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala
new file mode 100644
index 000000000..f50d1e15b
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala
@@ -0,0 +1,85 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData}
+import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo}
+import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage}
+import net.psforever.types.{MemberAction, PlanetSideEmpire}
+
+object GalaxyHandlerLogic {
+ def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = {
+ new GalaxyHandlerLogic(ops, ops.context)
+ }
+}
+
+class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ private val galaxyService: ActorRef = ops.galaxyService
+
+ /* packets */
+
+ def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = {
+ sendResponse(pkt)
+ pkt.friends.foreach { f =>
+ galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name))
+ }
+ }
+
+ /* response handlers */
+
+ def handle(reply: GalaxyResponse.Response): Unit = {
+ reply match {
+ case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) =>
+ sendResponse(
+ HotSpotUpdateMessage(
+ zone_index,
+ priority,
+ hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
+ )
+ )
+
+ case GalaxyResponse.MapUpdate(msg) =>
+ sendResponse(msg)
+
+ case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
+ val faction = player.Faction
+ val from = fromFactions.contains(faction)
+ val to = toFactions.contains(faction)
+ if (from && !to) {
+ sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
+ } else if (!from && to) {
+ sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
+ }
+
+ case GalaxyResponse.FlagMapUpdate(msg) =>
+ sendResponse(msg)
+
+ case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
+ sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest)
+
+ case GalaxyResponse.LockedZoneUpdate(zone, time) =>
+ sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
+
+ case GalaxyResponse.UnlockedZoneUpdate(zone) => ;
+ sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
+ val popBO = 0
+ val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
+ val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
+ val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
+ sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
+
+ case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) =>
+ avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
+
+ case GalaxyResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case _ => ()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
new file mode 100644
index 000000000..7c493e365
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
@@ -0,0 +1,1517 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.typed.scaladsl.adapter._
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.{AvatarActor, SessionActor}
+import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
+import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory}
+import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
+import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry}
+import net.psforever.objects.ballistics.Projectile
+import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike}
+import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
+import net.psforever.objects.entity.WorldEntity
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow}
+import net.psforever.objects.inventory.Container
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject}
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.doors.Door
+import net.psforever.objects.serverobject.generator.Generator
+import net.psforever.objects.serverobject.llu.CaptureFlag
+import net.psforever.objects.serverobject.locks.IFFLock
+import net.psforever.objects.serverobject.mblocker.Locker
+import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
+import net.psforever.objects.serverobject.structures.{Building, WarpGate}
+import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
+import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityUnit, Terminal}
+import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
+import net.psforever.objects.serverobject.tube.SpawnTube
+import net.psforever.objects.serverobject.turret.FacilityTurret
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
+import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, UtilityType, VehicleLockState}
+import net.psforever.objects.vehicles.Utility.InternalTelepad
+import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity, Vitality}
+import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason}
+import net.psforever.objects.vital.etc.SuicideReason
+import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning}
+import net.psforever.packet.PlanetSideGamePacket
+import net.psforever.packet.game.objectcreate.ObjectClass
+import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
+import net.psforever.services.RemoverActor
+import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.services.local.support.CaptureFlagManager
+import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, DriveState, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, TransactionType, Vector3}
+import net.psforever.util.Config
+
+import scala.concurrent.duration._
+
+object GeneralLogic {
+ def apply(ops: GeneralOperations): GeneralLogic = {
+ new GeneralLogic(ops, ops.context)
+ }
+}
+
+class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContext) extends GeneralFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = {
+ val ConnectToWorldRequestMessage(_, token, majorVersion, minorVersion, revision, buildDate, _, _) = pkt
+ log.trace(
+ s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server"
+ )
+ sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None))
+ context.self ! SessionActor.StartHeartbeat
+ sessionLogic.accountIntermediary ! RetrieveAccountData(token)
+ }
+
+ def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = {
+ val CharacterCreateRequestMessage(name, head, voice, gender, empire) = pkt
+ avatarActor ! AvatarActor.CreateAvatar(name, head, voice, gender, empire)
+ }
+
+ def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = {
+ val CharacterRequestMessage(charId, action) = pkt
+ action match {
+ case CharacterRequestAction.Delete =>
+ avatarActor ! AvatarActor.DeleteAvatar(charId.toInt)
+ case CharacterRequestAction.Select =>
+ avatarActor ! AvatarActor.SelectAvatar(charId.toInt, context.self)
+ }
+ }
+
+ def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = {
+ val PlayerStateMessageUpstream(
+ avatarGuid,
+ pos,
+ vel,
+ yaw,
+ pitch,
+ yawUpper,
+ seqTime,
+ _,
+ isCrouching,
+ isJumping,
+ jumpThrust,
+ isCloaking,
+ _,
+ _
+ )= pkt
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(avatarGuid)
+ sessionLogic.updateBlockMap(player, pos)
+ val isMoving = WorldEntity.isMoving(vel)
+ val isMovingPlus = isMoving || isJumping || jumpThrust
+ if (isMovingPlus) {
+ if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
+ sessionLogic.zoning.spawn.stopDeconstructing()
+ } else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion")
+ }
+ }
+ ops.fallHeightTracker(pos.z)
+ // if (isCrouching && !player.Crouching) {
+ // //dev stuff goes here
+ // }
+ player.Position = pos
+ player.Velocity = vel
+ player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
+ player.FacingYawUpper = yawUpper
+ player.Crouching = isCrouching
+ player.Jumping = isJumping
+ if (isCloaking && !player.Cloaked) {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak")
+ }
+ player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking
+ maxCapacitorTick(jumpThrust)
+ if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) {
+ continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match {
+ case Some(term: Terminal with ProximityUnit) =>
+ sessionLogic.terminals.StopUsingProximityUnit(term)
+ case _ => ()
+ }
+ }
+ ops.accessedContainer match {
+ // Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle.
+ // This can happen if PSUM arrives during the mounting process
+ case Some(veh: Vehicle) if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID =>
+ if (isMoving || veh.isMoving(test = 1) || Vector3.DistanceSquared(player.Position, veh.TrunkLocation) > 9) {
+ val guid = player.GUID
+ sendResponse(UnuseItemMessage(guid, veh.GUID))
+ sendResponse(UnuseItemMessage(guid, guid))
+ ops.unaccessContainer(veh)
+ }
+ case Some(container) => //just in case
+ if (isMovingPlus && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID)) {
+ // Ensure we don't close the container if the player is seated in it
+ val guid = player.GUID
+ // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first.
+ if (container.HasGUID) {
+ sendResponse(UnuseItemMessage(guid, container.GUID))
+ }
+ sendResponse(UnuseItemMessage(guid, guid))
+ ops.unaccessContainer(container)
+ }
+ case None => ()
+ }
+ val eagleEye: Boolean = ops.canSeeReallyFar
+ val isNotVisible: Boolean = player.spectator ||
+ 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
+ )
+ )
+ sessionLogic.squad.updateSquad()
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ player.zoneInteractions()
+ }
+
+ def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
+ log.debug(s"$pkt")
+ sendResponse(VoiceHostKill())
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
+ )
+ }
+
+ def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = {
+ log.debug(s"$pkt")
+ sendResponse(VoiceHostKill())
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
+ )
+ }
+
+ def handleEmote(pkt: EmoteMsg): Unit = {
+ val EmoteMsg(avatarGuid, emote) = pkt
+ sendResponse(EmoteMsg(avatarGuid, emote))
+ }
+
+ def handleDropItem(pkt: DropItemMessage): Unit = {
+ val DropItemMessage(itemGuid) = pkt
+ (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match {
+ case (Some(anItem: Equipment), Some(heldItem))
+ if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ RemoveOldEquipmentFromInventory(player)(heldItem)
+ case (Some(anItem: Equipment), Some(heldItem))
+ if anItem eq heldItem =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ DropEquipmentFromInventory(player)(heldItem)
+ case (Some(anItem: Equipment), _)
+ if continent.GUID(player.VehicleSeated).isEmpty =>
+ //suppress the warning message if in a vehicle
+ log.warn(s"DropItem: ${player.Name} wanted to drop a ${anItem.Definition.Name}, but it wasn't at hand")
+ case (Some(obj), _) =>
+ log.warn(s"DropItem: ${player.Name} wanted to drop a ${obj.Definition.Name}, but it was not equipment")
+ case _ => ()
+ }
+ }
+
+ def handlePickupItem(pkt: PickupItemMessage): Unit = {
+ val PickupItemMessage(itemGuid, _, _, _) = pkt
+ sessionLogic.validObject(itemGuid, decorator = "PickupItem").collect {
+ case item: Equipment if player.Fit(item).nonEmpty =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ PickUpEquipmentFromGround(player)(item)
+ case _: Equipment =>
+ sendResponse(ActionResultMessage.Fail(16)) //error code?
+ }
+ }
+
+ def handleObjectHeld(pkt: ObjectHeldMessage): Unit = {
+ val ObjectHeldMessage(_, heldHolsters, _) = pkt
+ player.Actor ! PlayerControl.ObjectHeld(heldHolsters)
+ }
+
+ def handleAvatarJump(pkt: AvatarJumpMessage): Unit = {
+ val AvatarJumpMessage(_) = pkt
+ avatarActor ! AvatarActor.ConsumeStamina(10)
+ avatarActor ! AvatarActor.SuspendStaminaRegeneration(2.5 seconds)
+ }
+
+ def handleZipLine(pkt: ZipLineMessage): Unit = {
+ val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt
+ continent.zipLinePaths.find(x => x.PathId == pathId) match {
+ case Some(path) if path.IsTeleporter =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
+ val endPoint = path.ZipLinePoints.last
+ sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
+ //todo: send to zone to show teleport animation to all clients
+ sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
+ case Some(_) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion")
+ action match {
+ case 0 =>
+ //travel along the zipline in the direction specified
+ sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
+ case 1 =>
+ //disembark from zipline at destination!
+ sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
+ case 2 =>
+ //get off by force
+ sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
+ case _ =>
+ log.warn(
+ s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}"
+ )
+ }
+ case _ =>
+ log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}")
+ }
+ }
+
+ def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = {
+ val RequestDestroyMessage(objectGuid) = pkt
+ //make sure this is the correct response for all cases
+ sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match {
+ case Some(vehicle: Vehicle) =>
+ /* line 1a: player is admin (and overrules other access requirements) */
+ /* line 1b: vehicle and player (as the owner) acknowledge each other */
+ /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */
+ /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
+ if (
+ (session.account.gm ||
+ (player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
+ (player.Faction == vehicle.Faction &&
+ (vehicle.Definition.CanBeOwned.nonEmpty &&
+ (vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) &&
+ (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied))
+ ) {
+ vehicle.Actor ! Vehicle.Deconstruct()
+ //log.info(s"RequestDestroy: vehicle $vehicle")
+ } else {
+ log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it")
+ }
+
+ case Some(obj: Projectile) =>
+ if (!obj.isResolved) {
+ obj.Miss()
+ }
+ continent.Projectile ! ZoneProjectile.Remove(objectGuid)
+
+ case Some(obj: BoomerTrigger) =>
+ if (findEquipmentToDelete(objectGuid, obj)) {
+ continent.GUID(obj.Companion) match {
+ case Some(boomer: BoomerDeployable) =>
+ boomer.Trigger = None
+ boomer.Actor ! Deployable.Deconstruct()
+ case Some(thing) =>
+ log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing")
+ case None => ()
+ }
+ }
+
+ case Some(obj: Deployable) =>
+ if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
+ obj.Actor ! Deployable.Deconstruct()
+ } else {
+ log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it")
+ }
+
+ case Some(obj: Equipment) =>
+ findEquipmentToDelete(objectGuid, obj)
+
+ case Some(thing) =>
+ log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}")
+
+ case None => ()
+ }
+ }
+
+ def handleMoveItem(pkt: MoveItemMessage): Unit = {
+ val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt
+ (
+ continent.GUID(sourceGuid),
+ continent.GUID(destinationGuid),
+ sessionLogic.validObject(itemGuid, decorator = "MoveItem")
+ ) match {
+ case (
+ Some(source: PlanetSideServerObject with Container),
+ Some(destination: PlanetSideServerObject with Container),
+ Some(item: Equipment)
+ ) =>
+ ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest))
+ case (None, _, _) =>
+ log.error(
+ s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object"
+ )
+ case (_, None, _) =>
+ log.error(
+ s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object"
+ )
+ case (_, _, None) => ()
+ case _ =>
+ log.error(
+ s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered"
+ )
+ }
+ }
+
+ def handleLootItem(pkt: LootItemMessage): Unit = {
+ val LootItemMessage(itemGuid, targetGuid) = pkt
+ (sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match {
+ case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) =>
+ //figure out the source
+ (
+ {
+ val findFunc: PlanetSideServerObject with Container => Option[
+ (PlanetSideServerObject with Container, Option[Int])
+ ] = ops.findInLocalContainer(itemGuid)
+ findFunc(player.avatar.locker)
+ .orElse(findFunc(player))
+ .orElse(ops.accessedContainer match {
+ case Some(parent: PlanetSideServerObject) =>
+ findFunc(parent)
+ case _ =>
+ None
+ })
+ },
+ destination.Fit(item)
+ ) match {
+ case (Some((source, Some(_))), Some(dest)) =>
+ ContainableMoveItem(player.Name, source, destination, item, dest)
+ case (None, _) =>
+ log.error(s"LootItem: ${player.Name} can not find where $item is put currently")
+ case (_, None) =>
+ log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination")
+ case _ =>
+ log.error(
+ s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered"
+ )
+ }
+ case (Some(obj), _) =>
+ log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}")
+ case (None, _) => ()
+ case (_, None) =>
+ log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid")
+ }
+ }
+
+ def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = {
+ val AvatarImplantMessage(_, action, slot, status) = pkt
+ if (action == ImplantAction.Activation) {
+ if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
+ //do not activate; play deactivation sound instead
+ sessionLogic.zoning.spawn.stopDeconstructing()
+ avatar.implants(slot).collect {
+ case implant if implant.active =>
+ avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
+ case implant =>
+ sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
+ }
+ } else {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant")
+ avatar.implants(slot) match {
+ case Some(implant) =>
+ if (status == 1) {
+ avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType)
+ } else {
+ avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
+ }
+ case _ =>
+ log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot")
+ }
+ }
+ }
+ }
+
+ def handleUseItem(pkt: UseItemMessage): Unit = {
+ val equipment = ops.findContainedEquipment(pkt.item_used_guid) match {
+ case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) =>
+ sessionLogic.shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption
+ case (Some(_), a) =>
+ a.headOption
+ case _ =>
+ None
+ }
+ sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match {
+ case Some(door: Door) =>
+ handleUseDoor(door, equipment)
+ case Some(resourceSilo: ResourceSilo) =>
+ handleUseResourceSilo(resourceSilo, equipment)
+ case Some(panel: IFFLock) =>
+ handleUseGeneralEntity(panel, equipment)
+ case Some(obj: Player) =>
+ handleUsePlayer(obj, equipment, pkt)
+ case Some(locker: Locker) =>
+ handleUseLocker(locker, equipment, pkt)
+ case Some(gen: Generator) =>
+ handleUseGeneralEntity(gen, equipment)
+ case Some(mech: ImplantTerminalMech) =>
+ handleUseGeneralEntity(mech, equipment)
+ case Some(captureTerminal: CaptureTerminal) =>
+ handleUseCaptureTerminal(captureTerminal, equipment)
+ case Some(obj: FacilityTurret) =>
+ handleUseFacilityTurret(obj, equipment, pkt)
+ case Some(obj: Vehicle) =>
+ handleUseVehicle(obj, equipment, pkt)
+ case Some(terminal: Terminal) =>
+ handleUseTerminal(terminal, equipment, pkt)
+ case Some(obj: SpawnTube) =>
+ handleUseSpawnTube(obj, equipment)
+ case Some(obj: SensorDeployable) =>
+ handleUseGeneralEntity(obj, equipment)
+ case Some(obj: TurretDeployable) =>
+ handleUseGeneralEntity(obj, equipment)
+ case Some(obj: TrapDeployable) =>
+ handleUseGeneralEntity(obj, equipment)
+ case Some(obj: ShieldGeneratorDeployable) =>
+ handleUseGeneralEntity(obj, equipment)
+ case Some(obj: TelepadDeployable) =>
+ handleUseTelepadDeployable(obj, equipment, pkt)
+ case Some(obj: Utility.InternalTelepad) =>
+ handleUseInternalTelepad(obj, pkt)
+ case Some(obj: CaptureFlag) =>
+ handleUseCaptureFlag(obj)
+ case Some(_: WarpGate) =>
+ handleUseWarpGate(equipment)
+ case Some(obj) =>
+ handleUseDefaultEntity(obj, equipment)
+ case None => ()
+ }
+ }
+
+ def handleUnuseItem(pkt: UnuseItemMessage): Unit = {
+ val UnuseItemMessage(_, objectGuid) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "UnuseItem") match {
+ case Some(obj: Player) =>
+ ops.unaccessContainer(obj)
+ sessionLogic.zoning.spawn.TryDisposeOfLootedCorpse(obj)
+ case Some(obj: Container) =>
+ // Make sure we don't unload the contents of the vehicle the player is seated in
+ // An example scenario of this would be closing the trunk contents when rearming at a landing pad
+ if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) {
+ ops.unaccessContainer(obj)
+ }
+ case _ => ()
+ }
+ }
+
+ def handleDeployObject(pkt: DeployObjectMessage): Unit = {
+ val DeployObjectMessage(guid, _, pos, orient, _) = pkt
+ player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid).flatMap { slot => slot.Equipment } match {
+ case Some(obj: ConstructionItem) =>
+ val ammoType = obj.AmmoType match {
+ case DeployedItem.portable_manned_turret => GlobalDefinitions.PortableMannedTurret(player.Faction).Item
+ case dtype => dtype
+ }
+ log.info(s"${player.Name} is constructing a $ammoType deployable")
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ val dObj: Deployable = Deployables.Make(ammoType)()
+ dObj.Position = pos
+ dObj.Orientation = orient
+ dObj.WhichSide = player.WhichSide
+ dObj.Faction = player.Faction
+ dObj.AssignOwnership(player)
+ val tasking: TaskBundle = dObj match {
+ case turret: TurretDeployable =>
+ GUIDTask.registerDeployableTurret(continent.GUID, turret)
+ case _ =>
+ GUIDTask.registerObject(continent.GUID, dObj)
+ }
+ TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj)))
+ case Some(obj) =>
+ log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!")
+ case None =>
+ log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!")
+ }
+ }
+
+ def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = {
+ val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match {
+ case Some(vehicle: Vehicle) if player.avatar.vehicle.contains(vehicle.GUID) =>
+ vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue)
+ case Some(vehicle: Vehicle) =>
+ log.warn(s"PlanetsideAttribute: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it")
+ // Cosmetics options
+ case Some(_: Player) if attributeType == 106 =>
+ avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue))
+ case Some(obj) =>
+ log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}")
+ case _ => ()
+ }
+ }
+
+ def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = {
+ val GenericObjectActionMessage(objectGuid, code) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match {
+ case Some(vehicle: Vehicle)
+ if vehicle.OwnerName.contains(player.Name) =>
+ vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID))
+
+ case Some(tool: Tool) =>
+ if (code == 35 &&
+ (tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser"))
+ ) {
+ //maelstrom primary fire mode discharge (no target)
+ //aphelion_laser discharge (no target)
+ sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID))
+ } else {
+ sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match {
+ case Some(vehicle: Vehicle)
+ if vehicle.OwnerName.contains(player.Name) =>
+ vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool))
+ case _ =>
+ }
+ }
+ case _ =>
+ log.info(s"${player.Name} - $pkt")
+ }
+ }
+
+ def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = {
+ val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match {
+ case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) =>
+ sessionLogic.shooting.FindContainedWeapon match {
+ case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) =>
+ vehicle.Actor ! SpecialEmp.Burst()
+ case _ => ()
+ }
+ case _ =>
+ log.info(s"${player.Name} - $pkt")
+ }
+ }
+
+ def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = {
+ val GenericObjectStateMsg(_, _) = pkt
+ log.info(s"${player.Name} - $pkt")
+ }
+
+ def handleGenericAction(pkt: GenericActionMessage): Unit = {
+ val GenericActionMessage(action) = pkt
+ if (player == null) {
+ if (action == GenericAction.AwayFromKeyboard_RCV) {
+ log.debug("GenericObjectState: AFK state reported during login")
+ }
+ } else {
+ val (toolOpt, definition) = player.Slot(0).Equipment match {
+ case Some(tool: Tool) =>
+ (Some(tool), tool.Definition)
+ case _ =>
+ (None, GlobalDefinitions.bullet_9mm)
+ }
+ action match {
+ case GenericAction.DropSpecialItem =>
+ ops.dropSpecialSlotItem()
+ case GenericAction.MaxAnchorsExtend_RCV =>
+ log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground")
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttribute(player.GUID, 19, 1)
+ )
+ definition match {
+ case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
+ val tool = toolOpt.get
+ tool.ToFireMode = 1
+ sendResponse(ChangeFireModeMessage(tool.GUID, 1))
+ case GlobalDefinitions.trhev_pounder =>
+ val tool = toolOpt.get
+ val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 }
+ else { 4 }
+ tool.ToFireMode = convertFireModeIndex
+ sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
+ case _ =>
+ log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}")
+ }
+ case GenericAction.MaxAnchorsRelease_RCV =>
+ log.info(s"${player.Name} has released the anchors")
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttribute(player.GUID, 19, 0)
+ )
+ definition match {
+ case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
+ val tool = toolOpt.get
+ tool.ToFireMode = 0
+ sendResponse(ChangeFireModeMessage(tool.GUID, 0))
+ case GlobalDefinitions.trhev_pounder =>
+ val tool = toolOpt.get
+ val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 }
+ tool.ToFireMode = convertFireModeIndex
+ sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
+ case _ =>
+ log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}")
+ }
+ case GenericAction.MaxSpecialEffect_RCV =>
+ if (player.ExoSuit == ExoSuitType.MAX) {
+ ops.toggleMaxSpecialState(enable = true)
+ } else {
+ log.warn(s"GenericActionMessage: ${player.Name} can't handle MAX special effect")
+ }
+ case GenericAction.StopMaxSpecialEffect_RCV =>
+ if (player.ExoSuit == ExoSuitType.MAX) {
+ player.Faction match {
+ case PlanetSideEmpire.NC =>
+ ops.toggleMaxSpecialState(enable = false)
+ case _ =>
+ log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability")
+ }
+ } else {
+ log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect")
+ }
+ case GenericAction.AwayFromKeyboard_RCV =>
+ log.info(s"${player.Name} is AFK")
+ AvatarActor.savePlayerLocation(player)
+ ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
+ player.AwayFromKeyboard = true
+ case GenericAction.BackInGame_RCV =>
+ log.info(s"${player.Name} is back")
+ player.AwayFromKeyboard = false
+ ops.renewCharSavedTimer(
+ Config.app.game.savedMsg.renewal.fixed,
+ Config.app.game.savedMsg.renewal.variable
+ )
+ case GenericAction.LookingForSquad_RCV => //Looking For Squad ON
+ if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
+ avatarActor ! AvatarActor.SetLookingForSquad(true)
+ }
+ case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF
+ if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
+ avatarActor ! AvatarActor.SetLookingForSquad(false)
+ }
+ case _ =>
+ log.warn(s"GenericActionMessage: ${player.Name} can't handle $action")
+ }
+ }
+ }
+
+ def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
+ val GenericCollisionMsg(ctype, p, _, ppos, pv, t, _, tpos, tv, _, _, _) = pkt
+ val fallHeight = {
+ if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
+ if (ops.heightTrend) {
+ val fall = ops.heightLast - ops.heightHistory
+ ops.heightHistory = ops.heightLast
+ fall
+ }
+ else {
+ val fall = ops.heightHistory - ops.heightLast
+ ops.heightLast = ops.heightHistory
+ fall
+ }
+ } else {
+ 0f
+ }
+ }
+ val (target1, target2, bailProtectStatus, velocity) = (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match {
+ case (CollisionIs.OfInfantry, out @ Some(user: Player))
+ if user == player =>
+ val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection
+ player.BailProtection = false
+ val v = if (player.avatar.implants.exists {
+ case Some(implant) => implant.definition.implantType == ImplantType.Surge && implant.active
+ case _ => false
+ }) {
+ Vector3.Zero
+ } else {
+ pv
+ }
+ (out, None, bailStatus, v)
+ case (CollisionIs.OfGroundVehicle, out @ Some(v: Vehicle))
+ if v.Seats(0).occupant.contains(player) =>
+ val bailStatus = v.BailProtection
+ v.BailProtection = false
+ (out, sessionLogic.validObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv)
+ case (CollisionIs.OfAircraft, out @ Some(v: Vehicle))
+ if v.Definition.CanFly && v.Seats(0).occupant.contains(player) =>
+ (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv)
+ case (CollisionIs.BetweenThings, _) =>
+ log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
+ (None, None, false, Vector3.Zero)
+ case _ =>
+ (None, None, false, Vector3.Zero)
+ }
+ val curr = System.currentTimeMillis()
+ (target1, t, target2) match {
+ case (None, _, _) => ()
+
+ case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
+ if (updateCollisionHistoryForTarget(us, curr)) {
+ if (!bailProtectStatus) {
+ sessionLogic.handleDealingDamage(
+ us,
+ DamageInteraction(
+ SourceEntry(us),
+ CollisionReason(velocity, fallHeight, us.DamageModel),
+ ppos
+ )
+ )
+ }
+ }
+
+ case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
+ collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
+
+ case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty =>
+ collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
+
+ case (
+ Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _,
+ Some(victim: PlanetSideServerObject with Vitality with FactionAffinity)
+ ) =>
+ if (updateCollisionHistoryForTarget(victim, curr)) {
+ val usSource = SourceEntry(us)
+ val victimSource = SourceEntry(victim)
+ //we take damage from the collision
+ if (!bailProtectStatus) {
+ performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv)
+ }
+ //get dealt damage from our own collision (no protection)
+ ops.collisionHistory.put(us.Actor, curr)
+ performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity)
+ }
+
+ case _ => ()
+ }
+ }
+
+ def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = {
+ val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt
+ avatarActor ! AvatarActor.AddFirstTimeEvent(eventName)
+ }
+
+ def handleBugReport(pkt: PlanetSideGamePacket): Unit = {
+ val BugReportMessage(
+ _/*versionMajor*/,
+ _/*versionMinor*/,
+ _/*versionDate*/,
+ _/*bugType*/,
+ _/*repeatable*/,
+ _/*location*/,
+ _/*zone*/,
+ _/*pos*/,
+ _/*summary*/,
+ _/*desc*/
+ ) = pkt
+ log.warn(s"${player.Name} filed a bug report - it might be something important")
+ log.debug(s"$pkt")
+ }
+
+ def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = {
+ val FacilityBenefitShieldChargeRequestMessage(_) = pkt
+ val vehicleGuid = player.VehicleSeated
+ continent
+ .GUID(vehicleGuid)
+ .foreach {
+ case obj: Vehicle if !obj.Destroyed && obj.MountedIn.isEmpty => // vehicle will try to charge even if destroyed & cargo vehicles need to be excluded
+ obj.Actor ! CommonMessages.ChargeShields(
+ 15,
+ Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius))
+ )
+ case obj: Vehicle if obj.MountedIn.nonEmpty =>
+ false
+ case _ if vehicleGuid.nonEmpty =>
+ log.warn(
+ s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find vehicle ${vehicleGuid.get.guid} in zone ${continent.id}"
+ )
+ case _ =>
+ log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in a vehicle")
+ }
+ }
+
+ def handleBattleplan(pkt: BattleplanMessage): Unit = {
+ val BattleplanMessage(_, name, _, _) = pkt
+ val lament: String = s"$name has a brilliant idea that no one will ever see"
+ log.info(lament)
+ log.debug(s"Battleplan: $lament - $pkt")
+ }
+
+ def handleBindPlayer(pkt: BindPlayerMessage): Unit = {
+ val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt
+ }
+
+ def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
+ val CreateShortcutMessage(_, slot, shortcutOpt) = pkt
+ shortcutOpt match {
+ case Some(shortcut) =>
+ avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut)
+ case None =>
+ avatarActor ! AvatarActor.RemoveShortcut(slot - 1)
+ }
+ }
+
+ def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = {
+ val ChangeShortcutBankMessage(_, _) = pkt
+ }
+
+ def handleFriendRequest(pkt: FriendsRequest): Unit = {
+ val FriendsRequest(action, name) = pkt
+ avatarActor ! AvatarActor.MemberListRequest(action, name)
+ }
+
+ def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = {
+ val InvalidTerrainMessage(_, vehicleGuid, alert, _) = pkt
+ (continent.GUID(vehicleGuid), continent.GUID(player.VehicleSeated)) match {
+ case (Some(packetVehicle: Vehicle), Some(playerVehicle: Vehicle)) if packetVehicle eq playerVehicle =>
+ if (alert == TerrainCondition.Unsafe) {
+ log.info(s"${player.Name}'s ${packetVehicle.Definition.Name} is approaching terrain unsuitable for idling")
+ }
+ case (Some(packetVehicle: Vehicle), Some(_: Vehicle)) =>
+ if (alert == TerrainCondition.Unsafe) {
+ log.info(s"${packetVehicle.Definition.Name}@${packetVehicle.GUID} is approaching terrain unsuitable for idling, but is not ${player.Name}'s vehicle")
+ }
+ case (Some(_: Vehicle), _) =>
+ log.warn(s"InvalidTerrain: ${player.Name} is not seated in a(ny) vehicle near unsuitable terrain")
+ case (Some(packetThing), _) =>
+ log.warn(s"InvalidTerrain: ${player.Name} thinks that ${packetThing.Definition.Name}@${packetThing.GUID} is near unsuitable terrain")
+ case _ =>
+ log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicleGuid that can not be found")
+ }
+ }
+
+ def handleActionCancel(pkt: ActionCancelMessage): Unit = {
+ val ActionCancelMessage(_, _, _) = pkt
+ ops.progressBarUpdate.cancel()
+ ops.progressBarValue = None
+ }
+
+ def handleTrade(pkt: TradeMessage): Unit = {
+ val TradeMessage(trade) = pkt
+ log.trace(s"${player.Name} wants to trade for some reason - $trade")
+ }
+
+ def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = {
+ val DisplayedAwardMessage(_, ribbon, bar) = pkt
+ log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon")
+ avatarActor ! AvatarActor.SetRibbon(ribbon, bar)
+ }
+
+ def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = {
+ val ObjectDetectedMessage(_, _, _, targets) = pkt
+ sessionLogic.shooting.FindWeapon.foreach {
+ case weapon if weapon.Projectile.AutoLock =>
+ //projectile with auto-lock instigates a warning on the target
+ val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets)
+ val mode = 7 + (if (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) 1 else 0)
+ detectedTargets.foreach { target =>
+ continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode))
+ }
+ case _ => ()
+ }
+ }
+
+ def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
+ val TargetingImplantRequest(list) = pkt
+ val targetInfo: List[TargetInfo] = list.flatMap { x =>
+ continent.GUID(x.target_guid) match {
+ case Some(player: Player) =>
+ val health = player.Health.toFloat / player.MaxHealth
+ val armor = if (player.MaxArmor > 0) {
+ player.Armor.toFloat / player.MaxArmor
+ } else {
+ 0
+ }
+ Some(TargetInfo(player.GUID, health, armor))
+ case _ =>
+ log.warn(
+ s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
+ )
+ None
+ }
+ }
+ sendResponse(TargetingInfoMessage(targetInfo))
+ }
+
+ def handleHitHint(pkt: HitHint): Unit = {
+ val HitHint(_, _) = pkt
+ }
+
+ /* messages */
+
+ def handleSetAvatar(avatar: Avatar): Unit = {
+ session = session.copy(avatar = avatar)
+ if (session.player != null) {
+ session.player.avatar = avatar
+ }
+ LivePlayerList.Update(avatar.id, avatar)
+ }
+
+ def handleReceiveAccountData(account: Account): Unit = {
+ log.trace(s"ReceiveAccountData $account")
+ session = session.copy(account = account)
+ avatarActor ! AvatarActor.SetAccount(account)
+ }
+
+ def handleUseCooldownRenew: BasicDefinition => Unit = {
+ case _: KitDefinition => ops.kitToBeUsed = None
+ case _ => ()
+ }
+
+ def handleAvatarResponse(avatar: Avatar): Unit = {
+ session = session.copy(avatar = avatar)
+ sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id)
+ }
+
+ def handleSetSpeed(speed: Float): Unit = {
+ session = session.copy(speed = speed)
+ }
+
+ def handleSetFlying(flying: Boolean): Unit = {
+ session = session.copy(flying = flying)
+ }
+
+ def handleSetSpectator(spectator: Boolean): Unit = {
+ session.player.spectator = spectator
+ }
+
+ def handleKick(player: Player, time: Option[Long]): Unit = {
+ ops.administrativeKick(player)
+ sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
+ }
+
+ def handleSilenced(isSilenced: Boolean): Unit = {
+ player.silenced = isSilenced
+ }
+
+ /* supporting functions */
+
+ private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
+ equipment match {
+ case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
+ val distance: Float = math.max(
+ Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance,
+ door.Definition.initialOpeningDistance
+ )
+ door.Actor ! CommonMessages.Use(player, Some(distance))
+ case _ =>
+ door.Actor ! CommonMessages.Use(player)
+ }
+ }
+
+ private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ (continent.GUID(player.VehicleSeated), equipment) match {
+ case (Some(vehicle: Vehicle), Some(item))
+ if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) &&
+ GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) =>
+ resourceSilo.Actor ! CommonMessages.Use(player, equipment)
+ case _ =>
+ resourceSilo.Actor ! CommonMessages.Use(player)
+ }
+ }
+
+ private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ if (obj.isBackpack) {
+ if (equipment.isEmpty) {
+ log.info(s"${player.Name} is looting the corpse of ${obj.Name}")
+ sendResponse(msg)
+ ops.accessContainer(obj)
+ }
+ } else if (!msg.unk3 && player.isAlive) { //potential kit use
+ (continent.GUID(msg.item_used_guid), ops.kitToBeUsed) match {
+ case (Some(kit: Kit), None) =>
+ ops.kitToBeUsed = Some(msg.item_used_guid)
+ player.Actor ! CommonMessages.Use(player, Some(kit))
+ 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))
+ case (Some(item), _) =>
+ log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead")
+ case (None, None) =>
+ log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") }
+ } 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)
+
+ case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
+ obj.Actor ! CommonMessages.Use(player, equipment)
+ case _ => ()
+ }
+ }
+ }
+
+ private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
+ equipment match {
+ case Some(item) =>
+ sendUseGeneralEntityMessage(locker, item)
+ 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))
+ ops.accessContainer(playerLocker)
+ case _ => ()
+ }
+ }
+
+ private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = {
+ equipment match {
+ case Some(item) =>
+ sendUseGeneralEntityMessage(captureTerminal, item)
+ case _ if ops.specialItemSlotGuid.nonEmpty =>
+ continent.GUID(ops.specialItemSlotGuid) match {
+ case Some(llu: CaptureFlag) =>
+ if (llu.Target.GUID == captureTerminal.Owner.GUID) {
+ continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu))
+ } 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")
+ }
+ case _ => ()
+ }
+ }
+
+ private 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
+ }
+ }
+
+ private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
+ equipment match {
+ case Some(item) =>
+ sendUseGeneralEntityMessage(obj, item)
+ case None if player.Faction == obj.Faction =>
+ //access to trunk
+ if (
+ obj.AccessingTrunk.isEmpty &&
+ (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
+ .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
+ ops.accessContainer(obj)
+ sendResponse(msg)
+ }
+ case _ => ()
+ }
+ }
+
+ private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
+ equipment match {
+ case Some(item) =>
+ sendUseGeneralEntityMessage(terminal, item)
+ 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)
+ )
+ } else if (
+ tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal ||
+ tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal
+ ) {
+ findLocalVehicle match {
+ case Some(vehicle) =>
+ log.info(
+ s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}"
+ )
+ sendResponse(msg)
+ sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId))
+ case None =>
+ log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none")
+ }
+ } 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))
+ )
+ } 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))
+ )
+ } else {
+ log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}")
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ sendResponse(msg)
+ }
+ case _ => ()
+ }
+ }
+
+ private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = {
+ 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 _ => ()
+ }
+ }
+
+ private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
+ if (equipment.isEmpty) {
+ (continent.GUID(obj.Router) match {
+ case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
+ case Some(vehicle) => Some(vehicle, None)
+ case None => None
+ }) match {
+ case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
+ player.WhichSide = vehicle.WhichSide
+ useRouterTelepadSystem(
+ router = vehicle,
+ internalTelepad = util,
+ remoteTelepad = obj,
+ src = obj,
+ dest = util
+ )
+ case Some((vehicle: Vehicle, None)) =>
+ log.error(
+ s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
+ )
+ 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 _ => ()
+ }
+ }
+ }
+
+ private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = {
+ continent.GUID(obj.Telepad) match {
+ case Some(pad: TelepadDeployable) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel")
+ player.WhichSide = pad.WhichSide
+ useRouterTelepadSystem(
+ router = obj.Owner.asInstanceOf[Vehicle],
+ internalTelepad = obj,
+ remoteTelepad = pad,
+ src = obj,
+ dest = pad
+ )
+ 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 => ()
+ }
+ }
+
+ private def handleUseCaptureFlag(obj: CaptureFlag): Unit = {
+ // LLU can normally only be picked up the faction that owns it
+ ops.specialItemSlotGuid match {
+ case None if obj.Faction == player.Faction =>
+ ops.specialItemSlotGuid = Some(obj.GUID)
+ player.Carrying = SpecialCarry.CaptureFlag
+ continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player)
+ case None =>
+ log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}")
+ 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 _ => ()
+ }
+ }
+
+ private def handleUseWarpGate(equipment: Option[Equipment]): Unit = {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ (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 _ => ()
+ }
+ }
+
+ private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = {
+ equipment.foreach { item =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ obj.Actor ! CommonMessages.Use(player, Some(item))
+ }
+ }
+
+ private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ obj.Actor ! CommonMessages.Use(player, Some(equipment))
+ }
+
+ private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = {
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ equipment match {
+ case Some(item)
+ if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) ||
+ GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => ()
+ case _ =>
+ log.warn(s"UseItem: ${player.Name} does not know how to handle $obj")
+ }
+ }
+
+ /**
+ * Get the current `Vehicle` object that the player is riding/driving.
+ * The vehicle must be found solely through use of `player.VehicleSeated`.
+ * @return the vehicle
+ */
+ private def findLocalVehicle: Option[Vehicle] = {
+ continent.GUID(player.VehicleSeated) match {
+ case Some(obj: Vehicle) => Some(obj)
+ case _ => None
+ }
+ }
+
+ /**
+ * A simple object searching algorithm that is limited to containers currently known and accessible by the player.
+ * If all relatively local containers are checked and the object is not found,
+ * the player's locker inventory will be checked, and then
+ * the game environment (items on the ground) will be checked too.
+ * If the target object is discovered, it is removed from its current location and is completely destroyed.
+ * @see `RequestDestroyMessage`
+ * @see `Zone.ItemIs.Where`
+ * @param objectGuid the target object's globally unique identifier;
+ * it is not expected that the object will be unregistered, but it is also not gauranteed
+ * @param obj the target object
+ * @return `true`, if the target object was discovered and removed;
+ * `false`, otherwise
+ */
+ private def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = {
+ val findFunc
+ : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] =
+ ops.findInLocalContainer(objectGuid)
+
+ findFunc(player)
+ .orElse(ops.accessedContainer match {
+ case Some(parent: PlanetSideServerObject) =>
+ findFunc(parent)
+ case _ =>
+ None
+ })
+ .orElse(findLocalVehicle match {
+ case Some(parent: PlanetSideServerObject) =>
+ findFunc(parent)
+ case _ =>
+ None
+ }) match {
+ case Some((parent, Some(_))) =>
+ obj.Position = Vector3.Zero
+ RemoveOldEquipmentFromInventory(parent)(obj)
+ true
+ case _ if player.avatar.locker.Inventory.Remove(objectGuid) =>
+ sendResponse(ObjectDeleteMessage(objectGuid, 0))
+ true
+ case _ if continent.EquipmentOnGround.contains(obj) =>
+ obj.Position = Vector3.Zero
+ continent.Ground ! Zone.Ground.RemoveItem(objectGuid)
+ continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
+ true
+ case _ =>
+ Zone.EquipmentIs.Where(obj, objectGuid, continent) match {
+ case None =>
+ true
+ case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID =>
+ TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
+ true
+ case Some(Zone.EquipmentIs.Orphaned()) =>
+ true
+ case _ =>
+ log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it")
+ false
+ }
+ }
+ }
+
+ /**
+ * A player uses a fully-linked Router teleportation system.
+ * @param router the Router vehicle
+ * @param internalTelepad the internal telepad within the Router vehicle
+ * @param remoteTelepad the remote telepad that is currently associated with this Router
+ * @param src the origin of the teleportation (where the player starts)
+ * @param dest the destination of the teleportation (where the player is going)
+ */
+ private def useRouterTelepadSystem(
+ router: Vehicle,
+ internalTelepad: InternalTelepad,
+ remoteTelepad: TelepadDeployable,
+ src: PlanetSideGameObject with TelepadLike,
+ dest: PlanetSideGameObject with TelepadLike
+ ): Unit = {
+ val time = System.currentTimeMillis()
+ if (
+ time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
+ internalTelepad.Active &&
+ remoteTelepad.Active
+ ) {
+ val pguid = player.GUID
+ val sguid = src.GUID
+ val dguid = dest.GUID
+ sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
+ ops.useRouterTelepadEffect(pguid, sguid, dguid)
+ continent.LocalEvents ! LocalServiceMessage(
+ continent.id,
+ LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
+ )
+ val vSource = VehicleSource(router)
+ val zoneNumber = continent.Number
+ player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber))
+ player.Position = dest.Position
+ player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber))
+ } else {
+ log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
+ }
+ ops.recentTeleportAttempt = time
+ }
+
+ private def maxCapacitorTick(jumpThrust: Boolean): Unit = {
+ if (player.ExoSuit == ExoSuitType.MAX) {
+ val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0
+ player.CapacitorState match {
+ case CapacitorStateType.Idle => maxCapacitorTickIdle(activate)
+ case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate)
+ case CapacitorStateType.ChargeDelay => maxCapacitorTickChargeDelay(activate)
+ case CapacitorStateType.Charging => maxCapacitorTickCharging(activate)
+ }
+ } else if (player.CapacitorState != CapacitorStateType.Idle) {
+ player.CapacitorState = CapacitorStateType.Idle
+ }
+ }
+
+ private def maxCapacitorTickIdle(activate: Boolean): Unit = {
+ if (activate) {
+ player.CapacitorState = CapacitorStateType.Discharging
+ //maxCapacitorTickDischarging(activate)
+ } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
+ player.CapacitorState = CapacitorStateType.ChargeDelay
+ maxCapacitorTickChargeDelay(activate)
+ }
+ }
+
+ private def maxCapacitorTickDischarging(activate: Boolean): Unit = {
+ if (activate) {
+ val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000
+ val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff
+ player.Capacitor -= drainAmount
+ sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
+ } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
+ if (player.Faction != PlanetSideEmpire.VS) {
+ ops.toggleMaxSpecialState(enable = false)
+ }
+ player.CapacitorState = CapacitorStateType.ChargeDelay
+ maxCapacitorTickChargeDelay(activate)
+ } else {
+ player.CapacitorState = CapacitorStateType.Idle
+ }
+ }
+
+ private def maxCapacitorTickChargeDelay(activate: Boolean): Unit = {
+ if (activate) {
+ player.CapacitorState = CapacitorStateType.Discharging
+ //maxCapacitorTickDischarging(activate)
+ } else if (player.Capacitor == player.ExoSuitDef.MaxCapacitor) {
+ player.CapacitorState = CapacitorStateType.Idle
+ } else if (System.currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis) {
+ player.CapacitorState = CapacitorStateType.Charging
+ //maxCapacitorTickCharging(activate)
+ }
+ }
+
+ private def maxCapacitorTickCharging(activate: Boolean): Unit = {
+ if (activate) {
+ player.CapacitorState = CapacitorStateType.Discharging
+ //maxCapacitorTickDischarging(activate)
+ } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
+ val timeDiff = (System.currentTimeMillis() - player.CapacitorLastChargedMillis).toFloat / 1000
+ val chargeAmount = player.ExoSuitDef.CapacitorRechargePerSecond * timeDiff
+ player.Capacitor += chargeAmount
+ sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
+ } else {
+ player.CapacitorState = CapacitorStateType.Idle
+ }
+ }
+
+ private def updateCollisionHistoryForTarget(
+ target: PlanetSideServerObject with Vitality with FactionAffinity,
+ curr: Long
+ ): Boolean = {
+ ops.collisionHistory.get(target.Actor) match {
+ case Some(lastCollision) if curr - lastCollision <= 1000L =>
+ false
+ case _ =>
+ ops.collisionHistory.put(target.Actor, curr)
+ true
+ }
+ }
+
+ private def collisionBetweenVehicleAndFragileDeployable(
+ vehicle: Vehicle,
+ vehiclePosition: Vector3,
+ smallDeployable: Deployable,
+ smallDeployablePosition: Vector3,
+ velocity: Vector3,
+ fallHeight: Float,
+ collisionTime: Long
+ ): Unit = {
+ if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) {
+ val smallDeployableSource = SourceEntry(smallDeployable)
+ //vehicle takes damage from the collision (ignore bail protection in this case)
+ performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity)
+ //deployable gets absolutely destroyed
+ ops.collisionHistory.put(vehicle.Actor, collisionTime)
+ sessionLogic.handleDealingDamage(
+ smallDeployable,
+ DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition)
+ )
+ }
+ }
+
+ private def performCollisionWithSomethingDamage(
+ target: PlanetSideServerObject with Vitality with FactionAffinity,
+ targetSource: SourceEntry,
+ targetPosition: Vector3,
+ victimSource: SourceEntry,
+ fallHeight: Float,
+ velocity: Vector3
+ ): Unit = {
+ sessionLogic.handleDealingDamage(
+ target,
+ DamageInteraction(
+ targetSource,
+ CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource),
+ targetPosition
+ )
+ )
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala
new file mode 100644
index 000000000..b638b0d12
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala
@@ -0,0 +1,248 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.ActorContext
+import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers}
+import net.psforever.objects.ce.Deployable
+import net.psforever.objects.vehicles.MountableWeapons
+import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable}
+import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage}
+import net.psforever.services.Service
+import net.psforever.services.local.LocalResponse
+import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
+
+object LocalHandlerLogic {
+ def apply(ops: SessionLocalHandlers): LocalHandlerLogic = {
+ new LocalHandlerLogic(ops, ops.context)
+ }
+}
+
+class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ /* response handlers */
+
+ /**
+ * na
+ * @param toChannel na
+ * @param guid na
+ * @param reply na
+ */
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = {
+ val resolvedPlayerGuid = if (player.HasGUID) {
+ player.GUID
+ } else {
+ Service.defaultPlayerGUID
+ }
+ val isNotSameTarget = resolvedPlayerGuid != guid
+ reply match {
+ case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget =>
+ sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
+
+ case LocalResponse.DeployableUIFor(item) =>
+ sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item))
+
+ case LocalResponse.Detonate(dguid, _: BoomerDeployable) =>
+ sendResponse(TriggerEffectMessage(dguid, "detonate_boomer"))
+ sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) =>
+ sendResponse(GenericObjectActionMessage(dguid, code=19))
+ sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.Detonate(_, obj) =>
+ log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
+
+ case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget =>
+ sendResponse(GenericObjectStateMsg(doorGuid, state=16))
+
+ case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone
+ sendResponse(GenericObjectStateMsg(doorGuid, state=17))
+
+ case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed =>
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
+ obj.Destroyed = true
+ DeconstructDeployable(
+ obj,
+ dguid,
+ pos,
+ obj.Orientation,
+ deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 }
+ )
+
+ case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _)
+ if obj.Destroyed || obj.Jammed || obj.Health == 0 =>
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed =>
+ //if active, deactivate
+ obj.Active = false
+ sendResponse(GenericObjectActionMessage(dguid, code=29))
+ sendResponse(GenericObjectActionMessage(dguid, code=30))
+ //standard deployable elimination behavior
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active =>
+ //if active, deactivate
+ obj.Active = false
+ sendResponse(GenericObjectActionMessage(dguid, code=29))
+ sendResponse(GenericObjectActionMessage(dguid, code=30))
+ //standard deployable elimination behavior
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed =>
+ //standard deployable elimination behavior
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
+ //standard deployable elimination behavior
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
+
+ case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed =>
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
+
+ case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) =>
+ sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2))
+
+ case LocalResponse.HackObject(targetGuid, unk1, unk2) =>
+ sessionLogic.general.hackObject(targetGuid, unk1, unk2)
+
+ case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) =>
+ sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue)
+
+ case LocalResponse.GenericObjectAction(targetGuid, actionNumber) =>
+ sendResponse(GenericObjectActionMessage(targetGuid, actionNumber))
+
+ case LocalResponse.GenericActionMessage(actionNumber) =>
+ sendResponse(GenericActionMessage(actionNumber))
+
+ case LocalResponse.ChatMessage(msg) =>
+ sendResponse(msg)
+
+ case LocalResponse.SendPacket(packet) =>
+ sendResponse(packet)
+
+ case LocalResponse.LluSpawned(llu) =>
+ // Create LLU on client
+ sendResponse(ObjectCreateMessage(
+ llu.Definition.ObjectId,
+ llu.GUID,
+ llu.Definition.Packet.ConstructorData(llu).get
+ ))
+ sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f))
+
+ case LocalResponse.LluDespawned(lluGuid, position) =>
+ sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f))
+ sendResponse(ObjectDeleteMessage(lluGuid, unk1=0))
+ // If the player was holding the LLU, remove it from their tracked special item slot
+ sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid =>
+ sessionLogic.general.specialItemSlotGuid = None
+ player.Carrying = None
+ }
+
+ case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget =>
+ sendResponse(ObjectDeleteMessage(objectGuid, unk))
+
+ case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
+ sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true))
+
+ case LocalResponse.ProximityTerminalEffect(objectGuid, false) =>
+ sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false))
+ sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid)
+
+ case LocalResponse.RouterTelepadMessage(msg) =>
+ sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None))
+
+ case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) =>
+ sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid)
+
+ case LocalResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case LocalResponse.SetEmpire(objectGuid, empire) =>
+ sendResponse(SetEmpireMessage(objectGuid, empire))
+
+ case LocalResponse.ShuttleEvent(ev) =>
+ val msg = OrbitalShuttleTimeMsg(
+ ev.u1,
+ ev.u2,
+ ev.t1,
+ ev.t2,
+ ev.t3,
+ pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) }
+ )
+ sendResponse(msg)
+
+ case LocalResponse.ShuttleDock(pguid, sguid, slot) =>
+ sendResponse(ObjectAttachMessage(pguid, sguid, slot))
+
+ case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) =>
+ sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient))
+
+ case LocalResponse.ShuttleState(sguid, pos, orient, state) =>
+ sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false))
+
+ case LocalResponse.ToggleTeleportSystem(router, systemPlan) =>
+ sessionLogic.general.toggleTeleportSystem(router, systemPlan)
+
+ case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) =>
+ sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation))
+
+ case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
+ sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
+
+ case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) =>
+ sendResponse(GenericObjectActionMessage(buildingGuid, 11))
+
+ case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) =>
+ sendResponse(GenericObjectActionMessage(buildingGuid, 12))
+
+ case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid =>
+ continent.GUID(vehicleGuid)
+ .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
+ .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
+ .getOrElse(Set.empty)
+ .collect { case weapon: Tool if weapon.GUID == weaponGuid =>
+ sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
+ }
+
+ case _ => ()
+ }
+ }
+
+ /* support functions */
+
+ /**
+ * Common behavior for deconstructing deployables in the game environment.
+ * @param obj the deployable
+ * @param guid the globally unique identifier for the deployable
+ * @param pos the previous position of the deployable
+ * @param orient the previous orientation of the deployable
+ * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
+ */
+ def DeconstructDeployable(
+ obj: Deployable,
+ guid: PlanetSideGUID,
+ pos: Vector3,
+ orient: Vector3,
+ deletionType: Int
+ ): Unit = {
+ sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
+ sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
+ sendResponse(ObjectDeleteMessage(guid, deletionType))
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
new file mode 100644
index 000000000..b4325d743
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
@@ -0,0 +1,520 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers}
+import net.psforever.actors.zone.ZoneActor
+import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
+import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition}
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
+import net.psforever.objects.serverobject.hackable.GenericHackables
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
+import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
+import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior}
+import net.psforever.objects.vital.InGameHistory
+import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
+import net.psforever.services.Service
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
+
+import scala.concurrent.duration._
+
+object MountHandlerLogic {
+ def apply(ops: SessionMountHandlers): MountHandlerLogic = {
+ new MountHandlerLogic(ops, ops.context)
+ }
+}
+
+class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /* packets */
+
+ def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
+ val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
+ sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect {
+ case obj: Mountable =>
+ obj.Actor ! Mountable.TryMount(player, entry_point)
+ case _ =>
+ log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
+ }
+ }
+
+ def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
+ val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt
+ val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
+ //TODO optimize this later
+ //common warning for this section
+ if (player.GUID == player_guid) {
+ //normally disembarking from a mount
+ (sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
+ case out @ Some(obj: Vehicle) =>
+ continent.GUID(obj.MountedIn) match {
+ case Some(_: Vehicle) => None //cargo vehicle
+ case _ => out //arrangement "may" be permissible
+ }
+ case out @ Some(_: Mountable) =>
+ out
+ case _ =>
+ dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
+ None
+ }) match {
+ case Some(obj: Mountable) =>
+ obj.PassengerInSeat(player) match {
+ case Some(seat_num) =>
+ obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
+ //short-circuit the temporary channel for transferring between zones, the player is no longer doing that
+ sessionLogic.zoning.interstellarFerry = None
+ // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
+ //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
+ //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
+ //todo: kick cargo passengers out. To be added after PR #216 is merged
+ obj match {
+ case v: Vehicle
+ if bailType == BailType.Bailed &&
+ v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) &&
+ v.isFlying =>
+ v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
+ case _ => ;
+ }
+
+ case None =>
+ dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
+ }
+ case _ =>
+ dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
+ }
+ } else {
+ //kicking someone else out of a mount; need to own that mount/mountable
+ val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
+ player.avatar.vehicle match {
+ case Some(obj_guid) =>
+ (
+ (
+ sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
+ sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player")
+ ) match {
+ case (vehicle @ Some(obj: Vehicle), tplayer) =>
+ if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
+ case (mount @ Some(_: Mountable), tplayer) =>
+ (mount, tplayer)
+ case _ =>
+ (None, None)
+ }) match {
+ case (Some(obj: Mountable), Some(tplayer: Player)) =>
+ obj.PassengerInSeat(tplayer) match {
+ case Some(seat_num) =>
+ obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
+ case None =>
+ dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
+ }
+ case (None, _) =>
+ dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
+ case (_, None) =>
+ dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
+ case _ =>
+ dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
+ }
+ case None =>
+ dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
+ }
+ }
+ }
+
+ def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
+ val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt
+ (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
+ case (Some(cargo: Vehicle), Some(carrier: Vehicle)) =>
+ carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
+ case Some((mountPoint, _)) =>
+ cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint)
+ case _ =>
+ log.warn(
+ s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold"
+ )
+ }
+ case (None, _) | (Some(_), None) =>
+ log.warn(
+ s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid"
+ )
+ case _ => ()
+ }
+ }
+
+ def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
+ val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt
+ continent.GUID(cargo_guid) match {
+ case Some(cargo: Vehicle) =>
+ cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
+ case _ => ()
+ }
+ }
+
+ /* response handlers */
+
+ /**
+ * na
+ *
+ * @param tplayer na
+ * @param reply na
+ */
+ def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
+ reply match {
+ case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ log.info(s"${player.Name} mounts an implant terminal")
+ sessionLogic.terminals.CancelAllProximityUnits()
+ MountingAction(tplayer, obj, seatNumber)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if obj.Definition == GlobalDefinitions.orbital_shuttle =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the orbital shuttle")
+ sessionLogic.terminals.CancelAllProximityUnits()
+ MountingAction(tplayer, obj, seatNumber)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if obj.Definition == GlobalDefinitions.ant =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
+ sendResponse(GenericObjectActionMessage(obj_guid, code=11))
+ sessionLogic.general.accessContainer(obj)
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if obj.Definition == GlobalDefinitions.quadstealth =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ //exclusive to the wraith, cloak state matches the cloak state of the driver
+ //phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks
+ obj.Cloaked = tplayer.Cloaked
+ sendResponse(GenericObjectActionMessage(obj_guid, code=11))
+ sessionLogic.general.accessContainer(obj)
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
+ sendResponse(GenericObjectActionMessage(obj_guid, code=11))
+ sessionLogic.general.accessContainer(obj)
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if seatNumber == 0 =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sendResponse(GenericObjectActionMessage(obj_guid, code=11))
+ sessionLogic.general.accessContainer(obj)
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if obj.Definition.MaxCapacitor > 0 =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts ${
+ obj.SeatPermissionGroup(seatNumber) match {
+ case Some(seatType) => s"a $seatType seat (#$seatNumber)"
+ case None => "a seat"
+ }
+ } of the ${obj.Definition.Name}")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
+ sessionLogic.general.accessContainer(obj)
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the ${
+ obj.SeatPermissionGroup(seatNumber) match {
+ case Some(seatType) => s"a $seatType seat (#$seatNumber)"
+ case None => "a seat"
+ }
+ } of the ${obj.Definition.Name}")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sessionLogic.general.accessContainer(obj)
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
+ if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
+ obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
+ sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
+ if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
+ obj.setMiddleOfUpgrade(false)
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
+ sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: FacilityTurret, _, _) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.warn(
+ s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
+ )
+
+ case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
+ sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
+ ops.updateWeaponAtSeatPosition(obj, seatNumber)
+ MountingAction(tplayer, obj, seatNumber)
+
+ case Mountable.CanMount(obj: Mountable, _, _) =>
+ log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}")
+
+ case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
+ log.info(s"${tplayer.Name} dismounts the implant terminal")
+ DismountAction(tplayer, obj, seatNum)
+
+ case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
+ if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
+ //dismount to hart lobby
+ val pguid = player.GUID
+ log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
+ val sguid = obj.GUID
+ val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint)
+ tplayer.Position = pos
+ sendResponse(DelayedPathMountMsg(pguid, sguid, u1=60, u2=true))
+ continent.LocalEvents ! LocalServiceMessage(
+ continent.id,
+ LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang))
+ )
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+
+ case Mountable.CanDismount(obj: Vehicle, seatNum, _)
+ if obj.Definition == GlobalDefinitions.orbital_shuttle =>
+ //get ready for orbital drop
+ val pguid = player.GUID
+ val events = continent.VehicleEvents
+ log.info(s"${player.Name} is prepped for dropping")
+ DismountAction(tplayer, obj, seatNum)
+ continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
+ //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
+ events ! VehicleServiceMessage(
+ player.Name,
+ VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message
+ )
+ //when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky
+ //the player will fall to the ground and is perfectly vulnerable in this state
+ //additionally, our player must exist in the current zone
+ //having no in-game avatar target will throw us out of the map screen when deploying and cause softlock
+ events ! VehicleServiceMessage(
+ player.Name,
+ VehicleAction.SendResponse(
+ Service.defaultPlayerGUID,
+ PlayerStateShiftMessage(ShiftState(unk=0, obj.Position, obj.Orientation.z, vel=None)) //cower in the shuttle bay
+ )
+ )
+ events ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player
+ )
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+
+ case Mountable.CanDismount(obj: Vehicle, seatNum, _)
+ if obj.Definition == GlobalDefinitions.droppod =>
+ log.info(s"${tplayer.Name} has landed on ${continent.id}")
+ sessionLogic.general.unaccessContainer(obj)
+ DismountAction(tplayer, obj, seatNum)
+ obj.Actor ! Vehicle.Deconstruct()
+
+ case Mountable.CanDismount(obj: Vehicle, seatNum, _)
+ if tplayer.GUID == player.GUID =>
+ //disembarking self
+ log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${
+ obj.SeatPermissionGroup(seatNum) match {
+ case Some(AccessPermissionGroup.Driver) => "driver seat"
+ case Some(seatType) => s"$seatType seat (#$seatNum)"
+ case None => "seat"
+ }
+ }")
+ sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
+ sessionLogic.general.unaccessContainer(obj)
+ DismountVehicleAction(tplayer, obj, seatNum)
+
+ case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
+ )
+
+ case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
+ log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
+ DismountAction(tplayer, obj, seatNum)
+
+ case Mountable.CanDismount(obj: Mountable, _, _) =>
+ log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}")
+
+ case Mountable.CanNotMount(obj: Vehicle, seatNumber) =>
+ log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
+ obj.GetSeatFromMountPoint(seatNumber).collect {
+ case seatNum if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, recipient="", "You are not the driver of this vehicle.", note=None)
+ )
+ }
+
+ case Mountable.CanNotMount(obj: Mountable, seatNumber) =>
+ log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
+
+ case Mountable.CanNotDismount(obj, seatNum) =>
+ log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed")
+ }
+ }
+
+ /* support functions */
+
+ private def dismountWarning(
+ bailAs: BailType.Value,
+ kickedByDriver: Boolean
+ )
+ (
+ note: String,
+ player: Player
+ ): Unit = {
+ log.warn(note)
+ player.VehicleSeated = None
+ sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
+ }
+
+ private def dismountError(
+ bailAs: BailType.Value,
+ kickedByDriver: Boolean
+ )
+ (
+ note: String,
+ player: Player
+ ): Unit = {
+ log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
+ player.VehicleSeated = None
+ sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
+ }
+
+ /**
+ * Common activities/procedure when a player mounts a valid object.
+ * @param tplayer the player
+ * @param obj the mountable object
+ * @param seatNum the mount into which the player is mounting
+ */
+ private def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
+ val playerGuid: PlanetSideGUID = tplayer.GUID
+ val objGuid: PlanetSideGUID = obj.GUID
+ sessionLogic.actionsToCancel()
+ avatarActor ! AvatarActor.DeactivateActiveImplants()
+ avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
+ sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.MountVehicle(playerGuid, objGuid, seatNum)
+ )
+ }
+
+ /**
+ * Common activities/procedure when a player dismounts a valid mountable object.
+ * @param tplayer the player
+ * @param obj the mountable object
+ * @param seatNum the mount out of which which the player is disembarking
+ */
+ private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
+ DismountAction(tplayer, obj, seatNum)
+ //until vehicles maintain synchronized momentum without a driver
+ obj match {
+ case v: Vehicle
+ if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
+ sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ =>
+ sessionLogic.vehicles.ServerVehicleOverrideStop(v)
+ }
+ v.Velocity = Vector3.Zero
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.VehicleState(
+ tplayer.GUID,
+ v.GUID,
+ unk1 = 0,
+ v.Position,
+ v.Orientation,
+ vel = None,
+ v.Flying,
+ unk3 = 0,
+ unk4 = 0,
+ wheel_direction = 15,
+ unk5 = false,
+ unk6 = v.Cloaked
+ )
+ )
+ case _ => ()
+ }
+ }
+
+ /**
+ * Common activities/procedure when a player dismounts a valid mountable object.
+ * @param tplayer the player
+ * @param obj the mountable object
+ * @param seatNum the mount out of which which the player is disembarking
+ */
+ private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
+ val playerGuid: PlanetSideGUID = tplayer.GUID
+ tplayer.ContributionFrom(obj)
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+ val bailType = if (tplayer.BailProtection) {
+ BailType.Bailed
+ } else {
+ BailType.Normal
+ }
+ sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
+ )
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala
new file mode 100644
index 000000000..9c49e8dd5
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala
@@ -0,0 +1,517 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.Actor.Receive
+import akka.actor.ActorRef
+import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
+import net.psforever.packet.game.UplinkRequest
+import net.psforever.services.chat.ChatService
+//
+import net.psforever.actors.session.{AvatarActor, SessionActor}
+import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData, ZoningOperations}
+import net.psforever.objects.TurretDeployable
+import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
+import net.psforever.objects.serverobject.CommonMessages
+import net.psforever.objects.serverobject.containable.Containable
+import net.psforever.objects.serverobject.deploy.Deployment
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.PlanetSideGamePacket
+import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, KeepAliveMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage}
+import net.psforever.services.{InterstellarClusterService => ICS}
+import net.psforever.services.CavernRotationService
+import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
+import net.psforever.services.ServiceManager.LookupResult
+import net.psforever.services.account.{PlayerToken, ReceiveAccountData}
+import net.psforever.services.avatar.AvatarServiceResponse
+import net.psforever.services.galaxy.GalaxyServiceResponse
+import net.psforever.services.local.LocalServiceResponse
+import net.psforever.services.teamwork.SquadServiceResponse
+import net.psforever.services.vehicle.VehicleServiceResponse
+import net.psforever.util.Config
+
+class NormalModeLogic(data: SessionData) extends ModeLogic {
+ val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse)
+ val chat: ChatFunctions = ChatLogic(data.chat)
+ val galaxy: GalaxyHandlerLogic = GalaxyHandlerLogic(data.galaxyResponseHandlers)
+ val general: GeneralFunctions = GeneralLogic(data.general)
+ val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
+ val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
+ val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
+ val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
+ val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals)
+ val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
+ val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations)
+
+ def parse(sender: ActorRef): Receive = {
+ /* really common messages (very frequently, every life) */
+ case packet: PlanetSideGamePacket =>
+ handleGamePkt(packet)
+
+ case AvatarServiceResponse(toChannel, guid, reply) =>
+ avatarResponse.handle(toChannel, guid, reply)
+
+ case GalaxyServiceResponse(_, reply) =>
+ galaxy.handle(reply)
+
+ case LocalServiceResponse(toChannel, guid, reply) =>
+ local.handle(toChannel, guid, reply)
+
+ case Mountable.MountMessages(tplayer, reply) =>
+ mountResponse.handle(tplayer, reply)
+
+ case SquadServiceResponse(_, excluded, response) =>
+ squad.handle(response, excluded)
+
+ case Terminal.TerminalMessage(tplayer, msg, order) =>
+ terminals.handle(tplayer, msg, order)
+
+ case VehicleServiceResponse(toChannel, guid, reply) =>
+ vehicleResponse.handle(toChannel, guid, reply)
+
+ case ChatService.MessageResponse(fromSession, message, _) =>
+ chat.handleIncomingMessage(message, fromSession)
+
+ case SessionActor.SendResponse(packet) =>
+ data.sendResponse(packet)
+
+ case SessionActor.CharSaved =>
+ general.ops.renewCharSavedTimer(
+ Config.app.game.savedMsg.interruptedByAction.fixed,
+ Config.app.game.savedMsg.interruptedByAction.variable
+ )
+
+ case SessionActor.CharSavedMsg =>
+ general.ops.displayCharSavedMsgThenRenewTimer(
+ Config.app.game.savedMsg.renewal.fixed,
+ Config.app.game.savedMsg.renewal.variable
+ )
+
+ /* common messages (maybe once every respawn) */
+ case ICS.SpawnPointResponse(response) =>
+ data.zoning.handleSpawnPointResponse(response)
+
+ case SessionActor.NewPlayerLoaded(tplayer) =>
+ data.zoning.spawn.handleNewPlayerLoaded(tplayer)
+
+ case SessionActor.PlayerLoaded(tplayer) =>
+ data.zoning.spawn.handlePlayerLoaded(tplayer)
+
+ case Zone.Population.PlayerHasLeft(zone, None) =>
+ data.log.debug(s"PlayerHasLeft: ${data.player.Name} does not have a body on ${zone.id}")
+
+ case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
+ if (tplayer.isAlive) {
+ data.log.info(s"${tplayer.Name} has left zone ${zone.id}")
+ }
+
+ case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
+ data.log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
+
+ case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
+ data.log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
+
+ case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
+ data.log.warn(
+ s"${data.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason"
+ )
+
+ case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
+ data.log.warn(
+ s"${data.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason"
+ )
+
+ case ICS.ZoneResponse(Some(zone)) =>
+ data.zoning.handleZoneResponse(zone)
+
+ /* uncommon messages (once a session) */
+ case ICS.ZonesResponse(zones) =>
+ data.zoning.handleZonesResponse(zones)
+
+ case SessionActor.SetAvatar(avatar) =>
+ general.handleSetAvatar(avatar)
+
+ case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
+ data.zoning.spawn.handleLoginInfoNowhere(name, sender)
+
+ case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
+ data.zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender)
+
+ case PlayerToken.RestoreInfo(playerName, inZone, pos) =>
+ data.zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender)
+
+ case PlayerToken.CanNotLogin(playerName, reason) =>
+ data.zoning.spawn.handleLoginCanNot(playerName, reason)
+
+ case ReceiveAccountData(account) =>
+ general.handleReceiveAccountData(account)
+
+ case AvatarActor.AvatarResponse(avatar) =>
+ general.handleAvatarResponse(avatar)
+
+ case AvatarActor.AvatarLoginResponse(avatar) =>
+ data.zoning.spawn.avatarLoginResponse(avatar)
+
+ case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) =>
+ data.zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt)
+
+ case SessionActor.SetConnectionState(state) =>
+ data.connectionState = state
+
+ case SessionActor.AvatarLoadingSync(state) =>
+ data.zoning.spawn.handleAvatarLoadingSync(state)
+
+ /* uncommon messages (utility, or once in a while) */
+ case ZoningOperations.AvatarAwardMessageBundle(pkts, delay) =>
+ data.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)
+
+ case CommonMessages.ProgressEvent(delta, finishedAction, stepAction, tick) =>
+ general.ops.handleProgressChange(delta, finishedAction, stepAction, tick)
+
+ case CommonMessages.Progress(rate, finishedAction, stepAction) =>
+ general.ops.setupProgressChange(rate, finishedAction, stepAction)
+
+ case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
+ listings.head ! SendCavernRotationUpdates(data.context.self)
+
+ case LookupResult("propertyOverrideManager", endpoint) =>
+ data.zoning.propertyOverrideManagerLoadOverrides(endpoint)
+
+ case SessionActor.UpdateIgnoredPlayers(msg) =>
+ galaxy.handleUpdateIgnoredPlayers(msg)
+
+ case SessionActor.UseCooldownRenewed(definition, _) =>
+ general.handleUseCooldownRenew(definition)
+
+ case Deployment.CanDeploy(obj, state) =>
+ vehicles.handleCanDeploy(obj, state)
+
+ case Deployment.CanUndeploy(obj, state) =>
+ vehicles.handleCanUndeploy(obj, state)
+
+ case Deployment.CanNotChangeDeployment(obj, state, reason) =>
+ vehicles.handleCanNotChangeDeployment(obj, state, reason)
+
+ /* rare messages */
+ case ProximityUnit.StopAction(term, _) =>
+ terminals.ops.LocalStopUsingProximityUnit(term)
+
+ case SessionActor.Suicide() =>
+ general.ops.suicide(data.player)
+
+ case SessionActor.Recall() =>
+ data.zoning.handleRecall()
+
+ case SessionActor.InstantAction() =>
+ data.zoning.handleInstantAction()
+
+ case SessionActor.Quit() =>
+ data.zoning.handleQuit()
+
+ case ICS.DroppodLaunchDenial(errorCode, _) =>
+ data.zoning.handleDroppodLaunchDenial(errorCode)
+
+ case ICS.DroppodLaunchConfirmation(zone, position) =>
+ data.zoning.LoadZoneLaunchDroppod(zone, position)
+
+ case SessionActor.PlayerFailedToLoad(tplayer) =>
+ data.failWithError(s"${tplayer.Name} failed to load anywhere")
+
+ /* csr only */
+ case SessionActor.SetSpeed(speed) =>
+ general.handleSetSpeed(speed)
+
+ case SessionActor.SetFlying(isFlying) =>
+ general.handleSetFlying(isFlying)
+
+ case SessionActor.SetSpectator(isSpectator) =>
+ general.handleSetSpectator(isSpectator)
+
+ case SessionActor.Kick(player, time) =>
+ general.handleKick(player, time)
+
+ case SessionActor.SetZone(zoneId, position) =>
+ data.zoning.handleSetZone(zoneId, position)
+
+ case SessionActor.SetPosition(position) =>
+ data.zoning.spawn.handleSetPosition(position)
+
+ case SessionActor.SetSilenced(silenced) =>
+ general.handleSilenced(silenced)
+
+ /* catch these messages */
+ case _: ProximityUnit.Action => ;
+
+ case _: Zone.Vehicle.HasSpawned => ;
+
+ case _: Zone.Vehicle.HasDespawned => ;
+
+ case Zone.Deployable.IsDismissed(obj: TurretDeployable) => //only if target deployable was never fully introduced
+ TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(data.continent.GUID, obj))
+
+ case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced
+ TaskWorkflow.execute(GUIDTask.unregisterObject(data.continent.GUID, obj))
+
+ case msg: Containable.ItemPutInSlot =>
+ data.log.debug(s"ItemPutInSlot: $msg")
+
+ case msg: Containable.CanNotPutItemInSlot =>
+ data.log.debug(s"CanNotPutItemInSlot: $msg")
+
+ case default =>
+ data.log.warn(s"Invalid packet class received: $default from $sender")
+ }
+
+ private def handleGamePkt: PlanetSideGamePacket => Unit = {
+ case packet: ConnectToWorldRequestMessage =>
+ general.handleConnectToWorldRequest(packet)
+
+ case packet: MountVehicleCargoMsg =>
+ mountResponse.handleMountVehicleCargo(packet)
+
+ case packet: DismountVehicleCargoMsg =>
+ mountResponse.handleDismountVehicleCargo(packet)
+
+ case packet: CharacterCreateRequestMessage =>
+ general.handleCharacterCreateRequest(packet)
+
+ case packet: CharacterRequestMessage =>
+ general.handleCharacterRequest(packet)
+
+ case _: KeepAliveMessage =>
+ data.keepAliveFunc()
+
+ case packet: BeginZoningMessage =>
+ data.zoning.handleBeginZoning(packet)
+
+ case packet: PlayerStateMessageUpstream =>
+ general.handlePlayerStateUpstream(packet)
+
+ case packet: ChildObjectStateMessage =>
+ vehicles.handleChildObjectState(packet)
+
+ case packet: VehicleStateMessage =>
+ vehicles.handleVehicleState(packet)
+
+ case packet: VehicleSubStateMessage =>
+ vehicles.handleVehicleSubState(packet)
+
+ case packet: FrameVehicleStateMessage =>
+ vehicles.handleFrameVehicleState(packet)
+
+ case packet: ProjectileStateMessage =>
+ shooting.handleProjectileState(packet)
+
+ case packet: LongRangeProjectileInfoMessage =>
+ shooting.handleLongRangeProjectileState(packet)
+
+ case packet: ReleaseAvatarRequestMessage =>
+ data.zoning.spawn.handleReleaseAvatarRequest(packet)
+
+ case packet: SpawnRequestMessage =>
+ data.zoning.spawn.handleSpawnRequest(packet)
+
+ case packet: ChatMsg =>
+ chat.handleChatMsg(packet)
+
+ case packet: SetChatFilterMessage =>
+ chat.handleChatFilter(packet)
+
+ case packet: VoiceHostRequest =>
+ general.handleVoiceHostRequest(packet)
+
+ case packet: VoiceHostInfo =>
+ general.handleVoiceHostInfo(packet)
+
+ case packet: ChangeAmmoMessage =>
+ shooting.handleChangeAmmo(packet)
+
+ case packet: ChangeFireModeMessage =>
+ shooting.handleChangeFireMode(packet)
+
+ case packet: ChangeFireStateMessage_Start =>
+ shooting.handleChangeFireStateStart(packet)
+
+ case packet: ChangeFireStateMessage_Stop =>
+ shooting.handleChangeFireStateStop(packet)
+
+ case packet: EmoteMsg =>
+ general.handleEmote(packet)
+
+ case packet: DropItemMessage =>
+ general.handleDropItem(packet)
+
+ case packet: PickupItemMessage =>
+ general.handlePickupItem(packet)
+
+ case packet: ReloadMessage =>
+ shooting.handleReload(packet)
+
+ case packet: ObjectHeldMessage =>
+ general.handleObjectHeld(packet)
+
+ case packet: AvatarJumpMessage =>
+ general.handleAvatarJump(packet)
+
+ case packet: ZipLineMessage =>
+ general.handleZipLine(packet)
+
+ case packet: RequestDestroyMessage =>
+ general.handleRequestDestroy(packet)
+
+ case packet: MoveItemMessage =>
+ general.handleMoveItem(packet)
+
+ case packet: LootItemMessage =>
+ general.handleLootItem(packet)
+
+ case packet: AvatarImplantMessage =>
+ general.handleAvatarImplant(packet)
+
+ case packet: UseItemMessage =>
+ general.handleUseItem(packet)
+
+ case packet: UnuseItemMessage =>
+ general.handleUnuseItem(packet)
+
+ case packet: ProximityTerminalUseMessage =>
+ terminals.handleProximityTerminalUse(packet)
+
+ case packet: DeployObjectMessage =>
+ general.handleDeployObject(packet)
+
+ case packet: GenericObjectActionMessage =>
+ general.handleGenericObjectAction(packet)
+
+ case packet: GenericObjectActionAtPositionMessage =>
+ general.handleGenericObjectActionAtPosition(packet)
+
+ case packet: GenericObjectStateMsg =>
+ general.handleGenericObjectState(packet)
+
+ case packet: GenericActionMessage =>
+ general.handleGenericAction(packet)
+
+ case packet: ItemTransactionMessage =>
+ terminals.handleItemTransaction(packet)
+
+ case packet: FavoritesRequest =>
+ terminals.handleFavoritesRequest(packet)
+
+ case packet: WeaponDelayFireMessage =>
+ shooting.handleWeaponDelayFire(packet)
+
+ case packet: WeaponDryFireMessage =>
+ shooting.handleWeaponDryFire(packet)
+
+ case packet: WeaponFireMessage =>
+ shooting.handleWeaponFire(packet)
+
+ case packet: WeaponLazeTargetPositionMessage =>
+ shooting.handleWeaponLazeTargetPosition(packet)
+
+ case _: UplinkRequest => ()
+
+ case packet: HitMessage =>
+ shooting.handleDirectHit(packet)
+
+ case packet: SplashHitMessage =>
+ shooting.handleSplashHit(packet)
+
+ case packet: LashMessage =>
+ shooting.handleLashHit(packet)
+
+ case packet: AIDamage =>
+ shooting.handleAIDamage(packet)
+
+ case packet: AvatarFirstTimeEventMessage =>
+ general.handleAvatarFirstTimeEvent(packet)
+
+ case packet: WarpgateRequest =>
+ data.zoning.handleWarpgateRequest(packet)
+
+ case packet: MountVehicleMsg =>
+ mountResponse.handleMountVehicle(packet)
+
+ case packet: DismountVehicleMsg =>
+ mountResponse.handleDismountVehicle(packet)
+
+ case packet: DeployRequestMessage =>
+ vehicles.handleDeployRequest(packet)
+
+ case packet: AvatarGrenadeStateMessage =>
+ shooting.handleAvatarGrenadeState(packet)
+
+ case packet: SquadDefinitionActionMessage =>
+ squad.handleSquadDefinitionAction(packet)
+
+ case packet: SquadMembershipRequest =>
+ squad.handleSquadMemberRequest(packet)
+
+ case packet: SquadWaypointRequest =>
+ squad.handleSquadWaypointRequest(packet)
+
+ case packet: GenericCollisionMsg =>
+ general.handleGenericCollision(packet)
+
+ case packet: BugReportMessage =>
+ general.handleBugReport(packet)
+
+ case packet: BindPlayerMessage =>
+ general.handleBindPlayer(packet)
+
+ case packet: PlanetsideAttributeMessage =>
+ general.handlePlanetsideAttribute(packet)
+
+ case packet: FacilityBenefitShieldChargeRequestMessage =>
+ general.handleFacilityBenefitShieldChargeRequest(packet)
+
+ case packet: BattleplanMessage =>
+ general.handleBattleplan(packet)
+
+ case packet: CreateShortcutMessage =>
+ general.handleCreateShortcut(packet)
+
+ case packet: ChangeShortcutBankMessage =>
+ general.handleChangeShortcutBank(packet)
+
+ case packet: FriendsRequest =>
+ general.handleFriendRequest(packet)
+
+ case packet: DroppodLaunchRequestMessage =>
+ data.zoning.handleDroppodLaunchRequest(packet)
+
+ case packet: InvalidTerrainMessage =>
+ general.handleInvalidTerrain(packet)
+
+ case packet: ActionCancelMessage =>
+ general.handleActionCancel(packet)
+
+ case packet: TradeMessage =>
+ general.handleTrade(packet)
+
+ case packet: DisplayedAwardMessage =>
+ general.handleDisplayedAward(packet)
+
+ case packet: ObjectDetectedMessage =>
+ general.handleObjectDetected(packet)
+
+ case packet: TargetingImplantRequest =>
+ general.handleTargetingImplantRequest(packet)
+
+ case packet: HitHint =>
+ general.handleHitHint(packet)
+
+ case _: OutfitRequest => ()
+
+ case pkt =>
+ data.log.warn(s"Unhandled GamePacket $pkt")
+ }
+}
+
+case object NormalMode extends PlayerMode {
+ def setup(data: SessionData): ModeLogic = {
+ new NormalModeLogic(data)
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala
new file mode 100644
index 000000000..9197211f8
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala
@@ -0,0 +1,358 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions}
+import net.psforever.objects.{Default, LivePlayerList}
+import net.psforever.objects.avatar.Avatar
+import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction}
+import net.psforever.services.chat.SquadChannel
+import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
+import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype}
+
+object SquadHandlerLogic {
+ def apply(ops: SessionSquadHandlers): SquadHandlerLogic = {
+ new SquadHandlerLogic(ops, ops.context)
+ }
+}
+
+class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ private val squadService: ActorRef = ops.squadService
+
+ private var waypointCooldown: Long = 0L
+
+ /* packet */
+
+ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
+ val SquadDefinitionActionMessage(u1, u2, action) = pkt
+ squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
+ }
+
+ def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
+ val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt
+ squadService ! SquadServiceMessage(
+ player,
+ continent,
+ SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5)
+ )
+ }
+
+ def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {
+ val SquadWaypointRequest(request, _, wtype, unk, info) = pkt
+ val time = System.currentTimeMillis()
+ val subtype = wtype.subtype
+ if(subtype == WaypointSubtype.Squad) {
+ squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
+ } else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) {
+ //guarding against duplicating laze waypoints
+ waypointCooldown = time
+ squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
+ }
+ }
+
+ /* response handlers */
+
+ def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
+ if (!excluded.exists(_ == avatar.id)) {
+ response match {
+ case SquadResponse.ListSquadFavorite(line, task) =>
+ sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
+
+ case SquadResponse.InitList(infos) =>
+ sendResponse(ReplicationStreamMessage(infos))
+
+ case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
+ sendResponse(
+ ReplicationStreamMessage(
+ 6,
+ None,
+ infos.map {
+ case (index, squadInfo) =>
+ SquadListing(index, squadInfo)
+ }.toVector
+ )
+ )
+
+ case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
+ sendResponse(
+ ReplicationStreamMessage(
+ 1,
+ None,
+ infos.map { index =>
+ SquadListing(index, None)
+ }.toVector
+ )
+ )
+
+ case SquadResponse.SquadDecoration(guid, squad) =>
+ val decoration = if (
+ ops.squadUI.nonEmpty ||
+ squad.Size == squad.Capacity ||
+ {
+ val offer = avatar.certifications
+ !squad.Membership.exists { _.isAvailable(offer) }
+ }
+ ) {
+ SquadListDecoration.NotAvailable
+ } else {
+ SquadListDecoration.Available
+ }
+ sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration)))
+
+ case SquadResponse.Detail(guid, detail) =>
+ sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
+
+ case SquadResponse.IdentifyAsSquadLeader(squad_guid) =>
+ sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
+
+ case SquadResponse.SetListSquad(squad_guid) =>
+ sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
+
+ case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) =>
+ val name = request_type match {
+ case SquadResponseType.Invite if unk5 =>
+ //the name of the player indicated by unk3 is needed
+ LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
+ case Some(player) =>
+ player.name
+ case None =>
+ player_name
+ }
+ case _ =>
+ player_name
+ }
+ sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6))
+
+ case SquadResponse.WantsSquadPosition(_, name) =>
+ sendResponse(
+ ChatMsg(
+ ChatMessageType.CMT_SQUAD,
+ wideContents=true,
+ name,
+ s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
+ None
+ )
+ )
+
+ case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
+ val avatarId = avatar.id
+ val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
+ .filter { case (mem, index) =>
+ mem.CharId > 0 && positionsToUpdate.contains(index)
+ }
+ membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
+ case Some((ourMember, ourIndex)) =>
+ //we are joining the squad
+ //load each member's entry (our own too)
+ ops.squad_supplement_id = squad.GUID.guid + 1
+ membershipPositions.foreach {
+ case (member, index) =>
+ sendResponse(
+ SquadMemberEvent.Add(
+ ops.squad_supplement_id,
+ member.CharId,
+ index,
+ member.Name,
+ member.ZoneId,
+ outfit_id = 0
+ )
+ )
+ ops.squadUI(member.CharId) =
+ SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
+ }
+ //repeat our entry
+ sendResponse(
+ SquadMemberEvent.Add(
+ ops.squad_supplement_id,
+ ourMember.CharId,
+ ourIndex,
+ ourMember.Name,
+ ourMember.ZoneId,
+ outfit_id = 0
+ )
+ )
+ //turn lfs off
+ if (avatar.lookingForSquad) {
+ avatarActor ! AvatarActor.SetLookingForSquad(false)
+ }
+ val playerGuid = player.GUID
+ val factionChannel = s"${player.Faction}"
+ //squad colors
+ ops.GiveSquadColorsToMembers()
+ ops.GiveSquadColorsForOthers(playerGuid, factionChannel, ops.squad_supplement_id)
+ //associate with member position in squad
+ sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
+ //a finalization? what does this do?
+ sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
+ squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
+ ops.updateSquadRef = ref
+ ops.updateSquad = ops.PeriodicUpdatesWhenEnrolledInSquad
+ sessionLogic.chat.JoinChannel(SquadChannel(squad.GUID))
+ case _ =>
+ //other player is joining our squad
+ //load each member's entry
+ ops.GiveSquadColorsToMembers(
+ membershipPositions.map {
+ case (member, index) =>
+ val charId = member.CharId
+ sendResponse(
+ SquadMemberEvent.Add(ops.squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
+ )
+ ops.squadUI(charId) =
+ SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
+ charId
+ }
+ )
+ }
+ //send an initial dummy update for map icon(s)
+ sendResponse(
+ SquadState(
+ PlanetSideGUID(ops.squad_supplement_id),
+ membershipPositions.map { case (member, _) =>
+ SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
+ }
+ )
+ )
+
+ case SquadResponse.Leave(squad, positionsToUpdate) =>
+ positionsToUpdate.find({ case (member, _) => member == avatar.id }) match {
+ case Some((ourMember, ourIndex)) =>
+ //we are leaving the squad
+ //remove each member's entry (our own too)
+ ops.updateSquadRef = Default.Actor
+ positionsToUpdate.foreach {
+ case (member, index) =>
+ sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
+ ops.squadUI.remove(member)
+ }
+ //uninitialize
+ val playerGuid = player.GUID
+ sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
+ ops.GiveSquadColorsToSelf(value = 0)
+ sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
+ sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
+ avatarActor ! AvatarActor.SetLookingForSquad(false)
+ //a finalization? what does this do?
+ sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+ ops.squad_supplement_id = 0
+ ops.squadUpdateCounter = 0
+ ops.updateSquad = ops.NoSquadUpdates
+ sessionLogic.chat.LeaveChannel(SquadChannel(squad.GUID))
+ case _ =>
+ //remove each member's entry
+ ops.GiveSquadColorsToMembers(
+ positionsToUpdate.map {
+ case (member, index) =>
+ sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
+ ops.squadUI.remove(member)
+ member
+ },
+ value = 0
+ )
+ }
+
+ case SquadResponse.AssignMember(squad, from_index, to_index) =>
+ //we've already swapped position internally; now we swap the cards
+ ops.SwapSquadUIElements(squad, from_index, to_index)
+
+ case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
+ if (promotedPlayer != player.CharId) {
+ //demoted from leader; no longer lfsm
+ if (player.avatar.lookingForSquad) {
+ avatarActor ! AvatarActor.SetLookingForSquad(false)
+ }
+ }
+ sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
+ //the players have already been swapped in the backend object
+ ops.PromoteSquadUIElements(squad, from_index)
+
+ case SquadResponse.UpdateMembers(_, positions) =>
+ val pairedEntries = positions.collect {
+ case entry if ops.squadUI.contains(entry.char_id) =>
+ (entry, ops.squadUI(entry.char_id))
+ }
+ //prune entries
+ val updatedEntries = pairedEntries
+ .collect({
+ case (entry, element) if entry.zone_number != element.zone =>
+ //zone gets updated for these entries
+ sendResponse(
+ SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number)
+ )
+ ops.squadUI(entry.char_id) =
+ SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
+ entry
+ case (entry, element)
+ if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
+ //other elements that need to be updated
+ ops.squadUI(entry.char_id) =
+ SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
+ entry
+ })
+ .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend
+ if (updatedEntries.nonEmpty) {
+ sendResponse(
+ SquadState(
+ PlanetSideGUID(ops.squad_supplement_id),
+ updatedEntries.map { entry =>
+ SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
+ }
+ )
+ )
+ }
+
+ case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
+ sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
+
+ case SquadResponse.SquadSearchResults(_/*results*/) =>
+ //TODO positive squad search results message?
+ // if(results.nonEmpty) {
+ // results.foreach { guid =>
+ // sendResponse(SquadDefinitionActionMessage(
+ // guid,
+ // 0,
+ // SquadAction.SquadListDecorator(SquadListDecoration.SearchResult))
+ // )
+ // }
+ // } else {
+ // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults()))
+ // }
+ // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch()))
+
+ case SquadResponse.InitWaypoints(char_id, waypoints) =>
+ waypoints.foreach {
+ case (waypoint_type, info, unk) =>
+ sendResponse(
+ SquadWaypointEvent.Add(
+ ops.squad_supplement_id,
+ char_id,
+ waypoint_type,
+ WaypointEvent(info.zone_number, info.pos, unk)
+ )
+ )
+ }
+
+ case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
+ sendResponse(
+ SquadWaypointEvent.Add(
+ ops.squad_supplement_id,
+ char_id,
+ waypoint_type,
+ WaypointEvent(info.zone_number, info.pos, unk)
+ )
+ )
+
+ case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
+ sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type))
+
+ case _ => ()
+ }
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala
new file mode 100644
index 000000000..5b6618562
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala
@@ -0,0 +1,184 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+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.{GlobalDefinitions, Player, Vehicle}
+import net.psforever.objects.guid.TaskWorkflow
+import net.psforever.objects.serverobject.pad.VehicleSpawnPad
+import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
+import net.psforever.objects.sourcing.AmenitySource
+import net.psforever.objects.vital.TerminalUsedActivity
+import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage}
+import net.psforever.types.{TransactionType, Vector3}
+
+object TerminalHandlerLogic {
+ def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = {
+ new TerminalHandlerLogic(ops, ops.context)
+ }
+}
+
+class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val context: ActorContext) extends TerminalHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
+ val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
+ continent.GUID(terminalGuid) match {
+ case Some(term: Terminal) if ops.lastTerminalOrderFulfillment =>
+ val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
+ log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
+ ops.lastTerminalOrderFulfillment = false
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ term.Actor ! Terminal.Request(player, pkt)
+ case Some(_: Terminal) =>
+ log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}")
+ case Some(obj) =>
+ log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}")
+ case _ =>
+ log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}")
+ }
+ }
+
+ def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
+ val ProximityTerminalUseMessage(_, objectGuid, _) = pkt
+ continent.GUID(objectGuid) match {
+ case Some(obj: Terminal with ProximityUnit) =>
+ ops.HandleProximityTerminalUse(obj)
+ case Some(obj) =>
+ log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects")
+ case None =>
+ log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}")
+ }
+ }
+
+ def handleFavoritesRequest(pkt: FavoritesRequest): Unit = {
+ val FavoritesRequest(_, loadoutType, action, line, label) = pkt
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
+ action match {
+ case FavoritesAction.Save =>
+ avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line)
+ case FavoritesAction.Delete =>
+ avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line)
+ case FavoritesAction.Unknown =>
+ log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action")
+ }
+ }
+
+ /**
+ * na
+ * @param tplayer na
+ * @param msg na
+ * @param order na
+ */
+ def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
+ order match {
+ case Terminal.BuyEquipment(item)
+ if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
+ sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.BuyEquipment(item) =>
+ avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
+ TaskWorkflow.execute(BuyNewEquipmentPutInInventory(
+ continent.GUID(tplayer.VehicleSeated) match {
+ case Some(v: Vehicle) => v
+ case _ => player
+ },
+ tplayer,
+ msg.terminal_guid
+ )(item))
+
+ case Terminal.SellEquipment() =>
+ SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
+
+ case Terminal.LearnCertification(cert) =>
+ avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert)
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.SellCertification(cert) =>
+ avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert)
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.LearnImplant(implant) =>
+ avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant)
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.SellImplant(implant) =>
+ avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant)
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.BuyVehicle(vehicle, _, _)
+ if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || tplayer.spectator =>
+ sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
+ continent.map.terminalToSpawnPad
+ .find { case (termid, _) => termid == msg.terminal_guid.guid }
+ .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) }
+ .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
+ avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition)
+ vehicle.Faction = tplayer.Faction
+ vehicle.Position = pad.Position
+ vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
+ //default loadout, weapons
+ val vWeapons = vehicle.Weapons
+ weapons.foreach { entry =>
+ vWeapons.get(entry.start) match {
+ case Some(slot) =>
+ entry.obj.Faction = tplayer.Faction
+ slot.Equipment = None
+ slot.Equipment = entry.obj
+ case None =>
+ log.warn(
+ s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}"
+ )
+ }
+ }
+ //default loadout, trunk
+ val vTrunk = vehicle.Trunk
+ vTrunk.Clear()
+ trunk.foreach { entry =>
+ entry.obj.Faction = tplayer.Faction
+ vTrunk.InsertQuickly(entry.start, entry.obj)
+ }
+ TaskWorkflow.execute(ops.registerVehicleFromSpawnPad(vehicle, pad, term))
+ sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true))
+ if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
+ sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
+ }
+ player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
+ }
+ .orElse {
+ log.error(
+ s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
+ )
+ sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
+ None
+ }
+ ops.lastTerminalOrderFulfillment = true
+
+ case Terminal.NoDeal() if msg != null =>
+ val transaction = msg.transaction_type
+ log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order")
+ sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false))
+ ops.lastTerminalOrderFulfillment = true
+
+ case _ =>
+ val terminal = msg.terminal_guid.guid
+ continent.GUID(terminal) match {
+ case Some(term: Terminal) =>
+ log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order")
+ case Some(_) =>
+ log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal")
+ case None =>
+ log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal")
+ }
+ ops.lastTerminalOrderFulfillment = true
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala
new file mode 100644
index 000000000..6d8f9c1a4
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala
@@ -0,0 +1,399 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions}
+import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
+import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
+import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.pad.VehicleSpawnPad
+import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
+import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage}
+import net.psforever.services.Service
+import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
+import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
+
+object VehicleHandlerLogic {
+ def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = {
+ new VehicleHandlerLogic(ops, ops.context)
+ }
+}
+
+class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ private val galaxyService: ActorRef = ops.galaxyService
+
+ /**
+ * na
+ *
+ * @param toChannel na
+ * @param guid na
+ * @param reply na
+ */
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = {
+ val resolvedPlayerGuid = if (player.HasGUID) {
+ player.GUID
+ } else {
+ PlanetSideGUID(-1)
+ }
+ val isNotSameTarget = resolvedPlayerGuid != guid
+ reply match {
+ case VehicleResponse.VehicleState(
+ vehicleGuid,
+ unk1,
+ pos,
+ orient,
+ vel,
+ unk2,
+ unk3,
+ unk4,
+ wheelDirection,
+ unk5,
+ unk6
+ ) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) =>
+ //player who is also in the vehicle (not driver)
+ sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
+ player.Position = pos
+ player.Orientation = orient
+ player.Velocity = vel
+ sessionLogic.updateLocalBlockMap(pos)
+
+ case VehicleResponse.VehicleState(
+ vehicleGuid,
+ unk1,
+ pos,
+ ang,
+ vel,
+ unk2,
+ unk3,
+ unk4,
+ wheelDirection,
+ unk5,
+ unk6
+ ) if isNotSameTarget =>
+ //player who is watching the vehicle from the outside
+ sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, ang, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
+
+ case VehicleResponse.ChildObjectState(objectGuid, pitch, yaw) if isNotSameTarget =>
+ sendResponse(ChildObjectStateMessage(objectGuid, pitch, yaw))
+
+ case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)
+ if isNotSameTarget =>
+ sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA))
+
+ case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Start(weaponGuid))
+
+ case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+
+ case VehicleResponse.Reload(itemGuid) if isNotSameTarget =>
+ sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
+
+ case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget =>
+ sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
+ //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0))
+ sendResponse(
+ ObjectCreateMessage(
+ ammo_id,
+ ammo_guid,
+ ObjectCreateMessageParent(weapon_guid, weapon_slot),
+ ammo_data
+ )
+ )
+ sendResponse(ChangeAmmoMessage(weapon_guid, 1))
+
+ case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget =>
+ continent.GUID(weaponGuid).collect {
+ case tool: Tool if tool.Magazine == 0 =>
+ // check that the magazine is still empty before sending WeaponDryFireMessage
+ // if it has been reloaded since then, other clients will not see it firing
+ sendResponse(WeaponDryFireMessage(weaponGuid))
+ }
+
+ case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget =>
+ sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver))
+
+ case VehicleResponse.MountVehicle(vehicleGuid, seat) if isNotSameTarget =>
+ sendResponse(ObjectAttachMessage(vehicleGuid, guid, seat))
+
+ case VehicleResponse.DeployRequest(objectGuid, state, unk1, unk2, pos) if isNotSameTarget =>
+ sendResponse(DeployRequestMessage(guid, objectGuid, state, unk1, unk2, pos))
+
+ case VehicleResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case VehicleResponse.AttachToRails(vehicleGuid, padGuid) =>
+ sendResponse(ObjectAttachMessage(padGuid, vehicleGuid, slot=3))
+
+ case VehicleResponse.ConcealPlayer(playerGuid) =>
+ sendResponse(GenericObjectActionMessage(playerGuid, code=9))
+
+ case VehicleResponse.DetachFromRails(vehicleGuid, padGuid, padPosition, padOrientationZ) =>
+ val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad].Definition
+ sendResponse(
+ ObjectDetachMessage(
+ padGuid,
+ vehicleGuid,
+ padPosition + Vector3.z(pad.VehicleCreationZOffset),
+ padOrientationZ + pad.VehicleCreationZOrientOffset
+ )
+ )
+
+ case VehicleResponse.EquipmentInSlot(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget =>
+ sendResponse(GenericObjectActionMessage(objectGuid, action))
+
+ case VehicleResponse.HitHint(sourceGuid) if player.isAlive =>
+ sendResponse(HitHint(sourceGuid, player.GUID))
+
+ case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget =>
+ //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
+ val objGuid = obj.GUID
+ sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
+ sendResponse(ObjectCreateDetailedMessage(
+ obj.Definition.ObjectId,
+ objGuid,
+ ObjectCreateMessageParent(parentGuid, start),
+ conData
+ ))
+
+ case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicleGuid) if resolvedPlayerGuid == guid =>
+ //seat number (first field) seems to be correct if passenger is kicked manually by driver
+ //but always seems to return 4 if user is kicked by mount permissions changing
+ sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
+ val typeOfRide = continent.GUID(vehicleGuid) match {
+ case Some(obj: Vehicle) =>
+ sessionLogic.general.unaccessContainer(obj)
+ s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}"
+ case _ =>
+ s"${player.Sex.possessive} ride"
+ }
+ log.info(s"${player.Name} has been kicked from $typeOfRide!")
+
+ case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) =>
+ //seat number (first field) seems to be correct if passenger is kicked manually by driver
+ //but always seems to return 4 if user is kicked by mount permissions changing
+ sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
+
+ case VehicleResponse.InventoryState2(objGuid, parentGuid, value) if isNotSameTarget =>
+ sendResponse(InventoryStateMessage(objGuid, unk=0, parentGuid, value))
+
+ case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) if isNotSameTarget =>
+ //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible)
+ sendResponse(ObjectCreateMessage(vtype, vguid, vdata))
+ Vehicles.ReloadAccessPermissions(vehicle, player.Name)
+
+ case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget =>
+ sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
+
+ case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid =>
+ //Only the player that owns this vehicle needs the ownership packet
+ avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))
+ sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid))
+
+ case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue))
+
+ case VehicleResponse.ResetSpawnPad(padGuid) =>
+ sendResponse(GenericObjectActionMessage(padGuid, code=23))
+
+ case VehicleResponse.RevealPlayer(playerGuid) =>
+ sendResponse(GenericObjectActionMessage(playerGuid, code=10))
+
+ case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission))
+
+ case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget =>
+ //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
+ sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData))
+
+ case VehicleResponse.UnloadVehicle(_, vehicleGuid) =>
+ sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0))
+
+ case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget =>
+ //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
+ sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
+
+ case VehicleResponse.UpdateAmsSpawnPoint(list) =>
+ sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
+ sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint()
+
+ case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget =>
+ sessionLogic.zoning.interstellarFerry = Some(vehicle)
+ sessionLogic.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete)
+ continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
+ galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel
+ log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating")
+
+ case VehicleResponse.KickCargo(vehicle, speed, delay)
+ if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 =>
+ val strafe = 1 + Vehicles.CargoOrientation(vehicle)
+ val reverseSpeed = if (strafe > 1) { 0 } else { speed }
+ //strafe or reverse, not both
+ sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
+ vehicle,
+ ServerVehicleOverrideMsg(
+ lock_accelerator=true,
+ lock_wheel=true,
+ reverse=true,
+ unk4=false,
+ lock_vthrust=0,
+ strafe,
+ reverseSpeed,
+ unk8=Some(0)
+ )
+ )
+ import scala.concurrent.ExecutionContext.Implicits.global
+ import scala.concurrent.duration._
+ context.system.scheduler.scheduleOnce(
+ delay milliseconds,
+ context.self,
+ VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay))
+ )
+
+ case VehicleResponse.KickCargo(cargo, _, _)
+ if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive =>
+ sessionLogic.vehicles.TotalDriverVehicleControl(cargo)
+
+ case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _)
+ if player.VisibleSlots.contains(player.DrawnSlot) =>
+ player.DrawnSlot = Player.HandsDownSlot
+ startPlayerSeatedInVehicle(vehicle)
+
+ case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
+ startPlayerSeatedInVehicle(vehicle)
+
+ case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
+ Vehicles.ReloadAccessPermissions(vehicle, player.Name)
+ sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
+ vehicle,
+ ServerVehicleOverrideMsg(
+ lock_accelerator=true,
+ lock_wheel=true,
+ reverse=true,
+ unk4=false,
+ lock_vthrust=1,
+ lock_strafe=0,
+ movement_speed=0,
+ unk8=Some(0)
+ )
+ )
+ sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
+
+ case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
+ val vdef = vehicle.Definition
+ sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
+ vehicle,
+ ServerVehicleOverrideMsg(
+ lock_accelerator=true,
+ lock_wheel=true,
+ reverse=false,
+ unk4=false,
+ lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 },
+ lock_strafe=0,
+ movement_speed=vdef.AutoPilotSpeed1,
+ unk8=Some(0)
+ )
+ )
+
+ case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
+ sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle)
+
+ case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
+ sendResponse(ChatMsg(
+ ChatMessageType.CMT_OPEN,
+ wideContents=true,
+ recipient="",
+ s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
+ note=None
+ ))
+
+ case VehicleResponse.PeriodicReminder(_, data) =>
+ val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match {
+ case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg)
+ case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg)
+ case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.")
+ }
+ sendResponse(ChatMsg(isType, flag, recipient="", msg, None))
+
+ case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory)
+ if player.avatar.vehicle.contains(target) =>
+ //TODO when vehicle weapons can be changed without visual glitches, rewrite this
+ continent.GUID(target).collect { case vehicle: Vehicle =>
+ import net.psforever.login.WorldSession.boolToInt
+ //owner: must unregister old equipment, and register and install new equipment
+ (oldWeapons ++ oldInventory).foreach {
+ case (obj, eguid) =>
+ sendResponse(ObjectDeleteMessage(eguid, unk1=0))
+ TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
+ }
+ sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory)
+ //jammer or unjamm new weapons based on vehicle status
+ val vehicleJammered = vehicle.Jammed
+ addedWeapons
+ .map { _.obj }
+ .collect {
+ case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered =>
+ jamItem.Jammed = vehicleJammered
+ JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered)
+ }
+ changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
+ }
+
+ case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _)
+ if sessionLogic.general.accessedContainer.map(_.GUID).contains(target) =>
+ //TODO when vehicle weapons can be changed without visual glitches, rewrite this
+ continent.GUID(target).collect { case vehicle: Vehicle =>
+ //external participant: observe changes to equipment
+ (oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
+ changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
+ }
+
+ case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) =>
+ //TODO when vehicle weapons can be changed without visual glitches, rewrite this
+ continent.GUID(target).collect { case vehicle: Vehicle =>
+ changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
+ }
+
+ case _ => ()
+ }
+ }
+
+ private def changeLoadoutDeleteOldEquipment(
+ vehicle: Vehicle,
+ oldWeapons: Iterable[(Equipment, PlanetSideGUID)],
+ oldInventory: Iterable[(Equipment, PlanetSideGUID)]
+ ): Unit = {
+ vehicle.PassengerInSeat(player) match {
+ case Some(seatNum) =>
+ //participant: observe changes to equipment
+ (oldWeapons ++ oldInventory).foreach {
+ case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0))
+ }
+ sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seatNum)
+ case None =>
+ //observer: observe changes to external equipment
+ oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
+ }
+ }
+
+ private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = {
+ val vehicle_guid = vehicle.GUID
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
+ sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off
+ sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership
+ vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect {
+ case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala
new file mode 100644
index 000000000..8d39fcc6b
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala
@@ -0,0 +1,391 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles}
+import net.psforever.objects.serverobject.deploy.Deployment
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.vehicles.control.BfrFlight
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
+import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.{DriveState, Vector3}
+
+object VehicleLogic {
+ def apply(ops: VehicleOperations): VehicleLogic = {
+ new VehicleLogic(ops, ops.context)
+ }
+}
+
+class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /* packets */
+
+ def handleVehicleState(pkt: VehicleStateMessage): Unit = {
+ val VehicleStateMessage(
+ vehicle_guid,
+ unk1,
+ pos,
+ ang,
+ vel,
+ is_flying,
+ unk6,
+ unk7,
+ wheels,
+ is_decelerating,
+ is_cloaked
+ ) = pkt
+ GetVehicleAndSeat() match {
+ case (Some(obj), Some(0)) =>
+ //we're driving the vehicle
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ sessionLogic.general.fallHeightTracker(pos.z)
+ if (obj.MountedIn.isEmpty) {
+ sessionLogic.updateBlockMap(obj, pos)
+ }
+ player.Position = pos //convenient
+ if (obj.WeaponControlledFromSeat(0).isEmpty) {
+ player.Orientation = Vector3.z(ang.z) //convenient
+ }
+ obj.Position = pos
+ obj.Orientation = ang
+ if (obj.MountedIn.isEmpty) {
+ if (obj.DeploymentState != DriveState.Deployed) {
+ obj.Velocity = vel
+ } else {
+ obj.Velocity = Some(Vector3.Zero)
+ }
+ if (obj.Definition.CanFly) {
+ obj.Flying = is_flying //usually Some(7)
+ }
+ obj.Cloaked = obj.Definition.CanCloak && is_cloaked
+ } else {
+ obj.Velocity = None
+ obj.Flying = None
+ }
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.VehicleState(
+ player.GUID,
+ vehicle_guid,
+ unk1,
+ obj.Position,
+ ang,
+ obj.Velocity,
+ if (obj.isFlying) {
+ is_flying
+ } else {
+ None
+ },
+ unk6,
+ unk7,
+ wheels,
+ is_decelerating,
+ obj.Cloaked
+ )
+ )
+ sessionLogic.squad.updateSquad()
+ obj.zoneInteractions()
+ case (None, _) =>
+ //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
+ //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
+ case (_, Some(index)) =>
+ log.error(
+ s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
+ )
+ case _ => ;
+ }
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
+ val FrameVehicleStateMessage(
+ vehicle_guid,
+ unk1,
+ pos,
+ ang,
+ vel,
+ unk2,
+ unk3,
+ unk4,
+ is_crouched,
+ is_airborne,
+ ascending_flight,
+ flight_time,
+ unk9,
+ unkA
+ ) = pkt
+ GetVehicleAndSeat() match {
+ case (Some(obj), Some(0)) =>
+ //we're driving the vehicle
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
+ case Some(v: Vehicle) =>
+ sessionLogic.updateBlockMap(obj, pos)
+ (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
+ case _ =>
+ (pos, ang, vel, true)
+ }
+ player.Position = position //convenient
+ if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
+ player.Orientation = Vector3.z(ang.z) //convenient
+ }
+ obj.Position = position
+ obj.Orientation = angle
+ obj.Velocity = velocity
+ // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
+ // //dev stuff goes here
+ // }
+ // else
+ // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
+ // //dev stuff goes here
+ // }
+ obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
+ if (notMountedState) {
+ if (obj.DeploymentState != DriveState.Kneeling) {
+ if (is_airborne) {
+ val flight = if (ascending_flight) flight_time else -flight_time
+ obj.Flying = Some(flight)
+ obj.Actor ! BfrFlight.Soaring(flight)
+ } else if (obj.Flying.nonEmpty) {
+ obj.Flying = None
+ obj.Actor ! BfrFlight.Landed
+ }
+ } else {
+ obj.Velocity = None
+ obj.Flying = None
+ }
+ obj.zoneInteractions()
+ } else {
+ obj.Velocity = None
+ obj.Flying = None
+ }
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.FrameVehicleState(
+ player.GUID,
+ vehicle_guid,
+ unk1,
+ position,
+ angle,
+ velocity,
+ unk2,
+ unk3,
+ unk4,
+ is_crouched,
+ is_airborne,
+ ascending_flight,
+ flight_time,
+ unk9,
+ unkA
+ )
+ )
+ sessionLogic.squad.updateSquad()
+ case (None, _) =>
+ //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
+ //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
+ case (_, Some(index)) =>
+ log.error(
+ s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
+ )
+ case _ => ;
+ }
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
+ val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
+ val (o, tools) = sessionLogic.shooting.FindContainedWeapon
+ //is COSM our primary upstream packet?
+ (o match {
+ case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
+ case _ => (None, None)
+ }) match {
+ case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ;
+ case _ =>
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ }
+ //the majority of the following check retrieves information to determine if we are in control of the child
+ tools.find { _.GUID == object_guid } match {
+ case None =>
+ //todo: old warning; this state is problematic, but can trigger in otherwise valid instances
+ //log.warn(
+ // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
+ //)
+ case Some(_) =>
+ //TODO set tool orientation?
+ player.Orientation = Vector3(0f, pitch, yaw)
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
+ )
+ }
+ //TODO status condition of "playing getting out of vehicle to allow for late packets without warning
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
+ val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
+ sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState") match {
+ case Some(obj: Vehicle) =>
+ import net.psforever.login.WorldSession.boolToInt
+ obj.Position = pos
+ obj.Orientation = ang
+ obj.Velocity = vel
+ sessionLogic.updateBlockMap(obj, pos)
+ obj.zoneInteractions()
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.VehicleState(
+ player.GUID,
+ vehicle_guid,
+ unk1,
+ pos,
+ ang,
+ obj.Velocity,
+ obj.Flying,
+ 0,
+ 0,
+ 15,
+ unk5 = false,
+ obj.Cloaked
+ )
+ )
+ case _ => ()
+ }
+ }
+
+ def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
+ val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
+ val vehicle = player.avatar.vehicle
+ if (vehicle.contains(vehicle_guid)) {
+ if (vehicle == player.VehicleSeated) {
+ continent.GUID(vehicle_guid) match {
+ case Some(obj: Vehicle) =>
+ log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state")
+ obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
+
+ case _ =>
+ log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid")
+ avatarActor ! AvatarActor.SetVehicle(None)
+ }
+ } else {
+ log.warn(s"${player.Name} must be mounted to request a deployment change")
+ }
+ } else {
+ log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object")
+ }
+ }
+
+ /* messages */
+
+ def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
+ if (state == DriveState.Deploying) {
+ log.trace(s"DeployRequest: $obj transitioning to deploy state")
+ } else if (state == DriveState.Deployed) {
+ log.trace(s"DeployRequest: $obj has been Deployed")
+ } else {
+ CanNotChangeDeployment(obj, state, "incorrect deploy state")
+ }
+ }
+
+ def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
+ if (state == DriveState.Undeploying) {
+ log.trace(s"DeployRequest: $obj transitioning to undeploy state")
+ } else if (state == DriveState.Mobile) {
+ log.trace(s"DeployRequest: $obj is Mobile")
+ } else {
+ CanNotChangeDeployment(obj, state, "incorrect undeploy state")
+ }
+ }
+
+ def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = {
+ if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) {
+ CanNotChangeDeployment(obj, state, reason = "ground too steep")
+ } else {
+ CanNotChangeDeployment(obj, state, reason)
+ }
+ }
+
+ /* support functions */
+
+ /**
+ * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat.
+ * The priority of object confirmation is `direct` then `occupant.VehicleSeated`.
+ * Once an object is found, the remainder are ignored.
+ * @param direct a game object in which the player may be sat
+ * @param occupant the player who is sat and may have specified the game object in which mounted
+ * @return a tuple consisting of a vehicle reference and a mount index
+ * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
+ * `(None, None)`, otherwise (even if the vehicle can be determined)
+ */
+ private def GetMountableAndSeat(
+ direct: Option[PlanetSideGameObject with Mountable],
+ occupant: Player,
+ zone: Zone
+ ): (Option[PlanetSideGameObject with Mountable], Option[Int]) =
+ direct.orElse(zone.GUID(occupant.VehicleSeated)) match {
+ case Some(obj: PlanetSideGameObject with Mountable) =>
+ obj.PassengerInSeat(occupant) match {
+ case index @ Some(_) =>
+ (Some(obj), index)
+ case None =>
+ (None, None)
+ }
+ case _ =>
+ (None, None)
+ }
+
+ /**
+ * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
+ * @see `GetMountableAndSeat`
+ * @return a tuple consisting of a vehicle reference and a mount index
+ * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
+ * `(None, None)`, otherwise (even if the vehicle can be determined)
+ */
+ private def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
+ GetMountableAndSeat(None, player, continent) match {
+ case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
+ case _ => (None, None)
+ }
+
+ /**
+ * Common reporting behavior when a `Deployment` object fails to properly transition between states.
+ * @param obj the game object that could not
+ * @param state the `DriveState` that could not be promoted
+ * @param reason a string explaining why the state can not or will not change
+ */
+ private def CanNotChangeDeployment(
+ obj: PlanetSideServerObject with Deployment,
+ state: DriveState.Value,
+ reason: String
+ ): Unit = {
+ val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) {
+ obj.DeploymentState = DriveState.Mobile
+ sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero))
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero)
+ )
+ "; enforcing Mobile deployment state"
+ } else {
+ ""
+ }
+ log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift")
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala
new file mode 100644
index 000000000..38f398828
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala
@@ -0,0 +1,1342 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations}
+import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
+import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
+import net.psforever.objects.definition.ProjectileDefinition
+import net.psforever.objects.entity.SimpleWorldEntity
+import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch}
+import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow}
+import net.psforever.objects.inventory.Container
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
+import net.psforever.objects.serverobject.doors.InteriorDoorPassage
+import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, Player, SpecialEmp, Tool, Tools, Vehicle}
+import net.psforever.objects.serverobject.interior.Sidedness
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.turret.{FacilityTurret, VanuSentry}
+import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
+import net.psforever.objects.vital.Vitality
+import net.psforever.objects.vital.base.{DamageResolution, DamageType}
+import net.psforever.objects.vital.etc.OicwLilBuddyReason
+import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.vital.projectile.ProjectileReason
+import net.psforever.objects.zones.{Zone, ZoneProjectile}
+import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ObjectAttachMessage, ObjectDeleteMessage, ObjectDetachMessage, 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 net.psforever.util.Config
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+object WeaponAndProjectileLogic {
+ def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
+ new WeaponAndProjectileLogic(ops, ops.context)
+ }
+
+ /**
+ * Does a line segment line intersect with a sphere?
+ * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
+ * @param start first point of the line segment
+ * @param end second point of the line segment
+ * @param center center of the sphere
+ * @param radius radius of the sphere
+ * @return list of all points of intersection, if any
+ * @see `Vector3.DistanceSquared`
+ * @see `Vector3.MagnitudeSquared`
+ */
+ private def quickLineSphereIntersectionPoints(
+ start: Vector3,
+ end: Vector3,
+ center: Vector3,
+ radius: Float
+ ): Iterable[Vector3] = {
+ /*
+ Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
+ because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
+ */
+ val Vector3(cx, cy, cz) = center
+ val Vector3(sx, sy, sz) = start
+ val vector = end - start
+ //speed our way through a quadratic equation
+ val (a, b) = {
+ val Vector3(dx, dy, dz) = vector
+ (
+ dx * dx + dy * dy + dz * dz,
+ 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
+ )
+ }
+ val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
+ val result = b * b - 4 * a * c
+ if (result < 0f) {
+ //negative, no intersection
+ Seq()
+ } else if (result < 0.00001f) {
+ //zero-ish, one intersection point
+ Seq(start - vector * (b / (2f * a)))
+ } else {
+ //positive, two intersection points
+ val sqrt = math.sqrt(result).toFloat
+ val endStart = vector / (2f * a)
+ Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
+ }.filter(p => Vector3.DistanceSquared(start, p) <= a)
+ }
+ /**
+ * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
+ * The main difference from "normal" server-side explosion
+ * is that the owner of the projectile must be clarified explicitly.
+ * @see `Zone::serverSideDamage`
+ * @param zone where the explosion is taking place
+ * (`source` contains the coordinate location)
+ * @param source a game object that represents the source of the explosion
+ * @param owner who or what to accredit damage from the explosion to;
+ * clarifies a normal `SourceEntry(source)` accreditation
+ */
+ private def detonateLittleBuddy(
+ zone: Zone,
+ source: PlanetSideGameObject with FactionAffinity with Vitality,
+ proxy: Projectile,
+ owner: SourceEntry
+ )(): Unit = {
+ Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
+ }
+
+ /**
+ * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
+ * The main difference from "normal" server-side explosion
+ * is that the owner of the projectile must be clarified explicitly.
+ * The sub-projectiles will be the product of a normal projectile rather than a standard game object
+ * so a custom `source` entity must wrap around it and fulfill the requirements of the field.
+ * @see `Zone::explosionDamage`
+ * @param owner who or what to accredit damage from the explosion to
+ * @param explosionPosition where the explosion will be positioned in the game world
+ * @param source a game object that represents the source of the explosion
+ * @param target a game object that is affected by the explosion
+ * @return a `DamageInteraction` object
+ */
+ private def littleBuddyExplosionDamage(
+ owner: SourceEntry,
+ projectileId: Long,
+ explosionPosition: Vector3
+ )
+ (
+ source: PlanetSideGameObject with FactionAffinity with Vitality,
+ target: PlanetSideGameObject with FactionAffinity with Vitality
+ ): DamageInteraction = {
+ DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
+ }
+}
+
+class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /* packets */
+
+ def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
+ val WeaponFireMessage(
+ _,
+ weapon_guid,
+ projectile_guid,
+ shot_origin,
+ _,
+ _,
+ _,
+ _/*max_distance,*/,
+ _,
+ _/*projectile_type,*/,
+ thrown_projectile_vel
+ ) = pkt
+ HandleWeaponFireOperations(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten)
+ }
+
+ def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = {
+ val WeaponDelayFireMessage(_, _) = pkt
+ log.info(s"${player.Name} - $pkt")
+ }
+
+ def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = {
+ val WeaponDryFireMessage(weapon_guid) = pkt
+ val (containerOpt, tools) = ops.FindContainedWeapon
+ tools
+ .find { _.GUID == weapon_guid }
+ .orElse { continent.GUID(weapon_guid) }
+ .collect {
+ case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) =>
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.WeaponDryFire(player.GUID, weapon_guid)
+ )
+ case _: Equipment =>
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.WeaponDryFire(player.GUID, weapon_guid)
+ )
+ }
+ .orElse {
+ log.warn(
+ s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist"
+ )
+ None
+ }
+ }
+
+ def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = {
+ val WeaponLazeTargetPositionMessage(_, _, _) = pkt
+ //do not need to handle the progress bar animation/state on the server
+ //laze waypoint is requested by client upon completion (see SquadWaypointRequest)
+ val purpose = if (sessionLogic.squad.squad_supplement_id > 0) {
+ s" for ${player.Sex.possessive} squad (#${sessionLogic.squad.squad_supplement_id -1})"
+ } else {
+ " ..."
+ }
+ log.info(s"${player.Name} is lazing a position$purpose")
+ }
+
+ def handleUplinkRequest(packet: UplinkRequest): Unit = {
+ sessionLogic.administrativeKick(player)
+ }
+
+ def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = {
+ val AvatarGrenadeStateMessage(_, state) = pkt
+ //TODO I thought I had this working?
+ log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade")
+ }
+
+ def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = {
+ val ChangeFireStateMessage_Start(item_guid) = pkt
+ if (ops.shooting.isEmpty) {
+ sessionLogic.findEquipment(item_guid) match {
+ case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
+ fireStateStartWhenPlayer(tool, item_guid)
+ case Some(tool: Tool) =>
+ fireStateStartWhenMounted(tool, item_guid)
+ case Some(_) if player.VehicleSeated.isEmpty =>
+ fireStateStartSetup(item_guid)
+ fireStateStartPlayerMessages(item_guid)
+ case Some(_) =>
+ fireStateStartSetup(item_guid)
+ fireStateStartMountedMessages(item_guid)
+ case None =>
+ log.warn(s"ChangeFireState_Start: can not find $item_guid")
+ }
+ }
+ }
+
+ def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = {
+ val ChangeFireStateMessage_Stop(item_guid) = pkt
+ val now = System.currentTimeMillis()
+ ops.prefire -= item_guid
+ ops.shootingStop += item_guid -> now
+ ops.shooting -= item_guid
+ sessionLogic.findEquipment(item_guid) match {
+ case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
+ fireStateStopWhenPlayer(tool, item_guid)
+ case Some(tool: Tool) =>
+ fireStateStopWhenMounted(tool, item_guid)
+ case Some(trigger: BoomerTrigger) =>
+ ops.fireStateStopPlayerMessages(item_guid)
+ continent.GUID(trigger.Companion).collect {
+ case boomer: BoomerDeployable =>
+ boomer.Actor ! CommonMessages.Use(player, Some(trigger))
+ }
+ case Some(_) if player.VehicleSeated.isEmpty =>
+ ops.fireStateStopPlayerMessages(item_guid)
+ case Some(_) =>
+ ops.fireStateStopMountedMessages(item_guid)
+ case _ =>
+ log.warn(s"ChangeFireState_Stop: can not find $item_guid")
+ }
+ sessionLogic.general.progressBarUpdate.cancel()
+ sessionLogic.general.progressBarValue = None
+ }
+
+ def handleReload(pkt: ReloadMessage): Unit = {
+ val ReloadMessage(item_guid, _, unk1) = pkt
+ ops.FindContainedWeapon match {
+ case (Some(obj: Player), tools) =>
+ handleReloadWhenPlayer(item_guid, obj, tools, unk1)
+ case (Some(obj: PlanetSideServerObject with Container), tools) =>
+ 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")
+ }
+ }
+
+ def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = {
+ val ChangeAmmoMessage(item_guid, _) = pkt
+ val (thing, equipment) = sessionLogic.findContainedEquipment()
+ if (equipment.isEmpty) {
+ log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment")
+ } else {
+ equipment foreach {
+ case obj: ConstructionItem =>
+ if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) {
+ log.info(
+ s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})"
+ )
+ sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex))
+ }
+ case tool: Tool =>
+ thing match {
+ case Some(player: Player) =>
+ PerformToolAmmoChange(tool, player, ModifyAmmunition(player))
+ case Some(mountable: PlanetSideServerObject with Container) =>
+ PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable))
+ case _ =>
+ log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type")
+ }
+ case obj =>
+ log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition")
+ }
+ }
+ }
+
+ def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = {
+ val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt
+ sessionLogic.findEquipment(item_guid) match {
+ case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) =>
+ val originalModeIndex = obj.FireModeIndex
+ if (obj match {
+ case citem: ConstructionItem =>
+ val modeChanged = Deployables.performConstructionItemFireModeChange(
+ player.avatar.certifications,
+ citem,
+ originalModeIndex
+ )
+ modeChanged
+ case _ =>
+ obj.NextFireMode
+ obj.FireModeIndex != originalModeIndex
+ }) {
+ val modeIndex = obj.FireModeIndex
+ obj match {
+ case citem: ConstructionItem =>
+ log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)")
+ case _ =>
+ log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex")
+ }
+ sendResponse(ChangeFireModeMessage(item_guid, modeIndex))
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex)
+ )
+ }
+ case Some(_) =>
+ log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes")
+ case None =>
+ log.warn(s"ChangeFireMode: can not find $item_guid")
+ }
+ }
+
+ def handleProjectileState(pkt: ProjectileStateMessage): Unit = {
+ val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt
+ val index = projectile_guid.guid - Projectile.baseUID
+ ops.projectiles(index) match {
+ case Some(projectile) if projectile.HasGUID =>
+ val projectileGlobalUID = projectile.GUID
+ projectile.Position = shot_pos
+ projectile.Orientation = shot_orient
+ projectile.Velocity = shot_vel
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.ProjectileState(
+ player.GUID,
+ projectileGlobalUID,
+ shot_pos,
+ shot_vel,
+ shot_orient,
+ seq,
+ end,
+ target_guid
+ )
+ )
+ case _ if seq == 0 =>
+ /* missing the first packet in the sequence is permissible */
+ case _ =>
+ log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
+ }
+ }
+
+ def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = {
+ val LongRangeProjectileInfoMessage(guid, _, _) = pkt
+ ops.FindContainedWeapon match {
+ case (Some(_: Vehicle), weapons)
+ if weapons.exists { _.GUID == guid } => () //now what?
+ case _ => ()
+ }
+ }
+
+ def handleDirectHit(pkt: HitMessage): Unit = {
+ val HitMessage(
+ _,
+ projectile_guid,
+ _,
+ hit_info,
+ _,
+ _,
+ _
+ ) = pkt
+ //find defined projectile
+ ops.FindProjectileEntry(projectile_guid) match {
+ case Some(projectile) =>
+ //find target(s)
+ (hit_info match {
+ case Some(hitInfo) =>
+ val hitPos = hitInfo.hit_pos
+ sessionLogic.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match {
+ case _ if projectile.profile == GlobalDefinitions.flail_projectile =>
+ val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius
+ val targets = Zone.findAllTargets(continent, player, hitPos, projectile.profile)
+ .filter { target =>
+ Vector3.DistanceSquared(target.Position, hitPos) <= radius
+ }
+ targets.map { target =>
+ CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target)
+ (target, projectile, hitPos, target.Position)
+ }
+
+ case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
+ CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target)
+ List((target, projectile, hitInfo.shot_origin, hitPos))
+
+ case None =>
+ HandleDamageProxy(projectile, projectile_guid, hitPos)
+
+ case _ =>
+ Nil
+ }
+ case None =>
+ Nil
+ })
+ .foreach {
+ case (
+ target: PlanetSideGameObject with FactionAffinity with Vitality,
+ proj: Projectile,
+ _: Vector3,
+ hitPos: Vector3
+ ) =>
+ ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ case _ => ()
+ }
+ case None =>
+ log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
+ }
+ }
+
+ def handleSplashHit(pkt: SplashHitMessage): Unit = {
+ val SplashHitMessage(
+ _,
+ projectile_guid,
+ explosion_pos,
+ direct_victim_uid,
+ _,
+ projectile_vel,
+ _,
+ targets
+ ) = pkt
+ ops.FindProjectileEntry(projectile_guid) match {
+ case Some(projectile) =>
+ val profile = projectile.profile
+ projectile.Velocity = projectile_vel
+ val (resolution1, resolution2) = profile.Aggravated match {
+ case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
+ (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
+ case _ =>
+ (DamageResolution.Splash, DamageResolution.Splash)
+ }
+ //direct_victim_uid
+ sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
+ case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
+ CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
+ ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ case _ => ()
+ }
+ //other victims
+ targets.foreach(elem => {
+ sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
+ case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
+ CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
+ ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ case _ => ()
+ }
+ })
+ //...
+ HandleDamageProxy(projectile, projectile_guid, explosion_pos)
+ if (
+ projectile.profile.HasJammedEffectDuration ||
+ projectile.profile.JammerProjectile ||
+ projectile.profile.SympatheticExplosion
+ ) {
+ //can also substitute 'projectile.profile' for 'SpecialEmp.emp'
+ Zone.serverSideDamage(
+ continent,
+ player,
+ SpecialEmp.emp,
+ SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
+ SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
+ SpecialEmp.findAllBoomers(profile.DamageRadius)
+ )
+ }
+ if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
+ //cleanup
+ if (projectile.HasGUID) {
+ continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
+ }
+ }
+ case None => ()
+ }
+ }
+
+ def handleLashHit(pkt: LashMessage): Unit = {
+ val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt
+ sessionLogic.validObject(victim_guid, decorator = "Lash") match {
+ case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
+ CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
+ ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach {
+ resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ case _ => ()
+ }
+ }
+
+ def handleAIDamage(pkt: AIDamage): Unit = {
+ val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
+ (continent.GUID(player.VehicleSeated) match {
+ case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
+ if tobj.GUID == targetGuid &&
+ tobj.OwnerGuid.contains(player.GUID) =>
+ //deployable turrets
+ Some(tobj)
+ case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
+ if tobj.GUID == targetGuid &&
+ tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
+ //facility turrets, etc.
+ Some(tobj)
+ case _
+ if player.GUID == targetGuid =>
+ //player avatars
+ Some(player)
+ case _ =>
+ None
+ }).collect {
+ case target: AutomatedTurret.Target =>
+ sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
+ .collect {
+ case turret: AutomatedTurret if turret.Target.isEmpty =>
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ Some(target)
+
+ case turret: AutomatedTurret =>
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
+ Some(target)
+ }
+ }
+ .orElse {
+ //occasionally, something that is not technically a turret's natural target may be attacked
+ sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target")
+ .collect {
+ case target: PlanetSideServerObject with FactionAffinity with Vitality =>
+ sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker")
+ .collect {
+ case turret: AutomatedTurret if turret.Target.nonEmpty =>
+ //the turret must be shooting at something (else) first
+ HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
+ }
+ Some(target)
+ }
+ }
+ }
+
+ /* support code */
+
+ private def HandleWeaponFireOperations(
+ weaponGUID: PlanetSideGUID,
+ projectileGUID: PlanetSideGUID,
+ shotOrigin: Vector3,
+ shotVelocity: Option[Vector3]
+ ): Unit = {
+ ops.HandleWeaponFireAccountability(weaponGUID, projectileGUID) match {
+ case (Some(obj), Some(tool)) =>
+ val projectileIndex = projectileGUID.guid - Projectile.baseUID
+ val projectilePlace = ops.projectiles(projectileIndex)
+ if (
+ projectilePlace match {
+ case Some(projectile) =>
+ !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong
+ case None =>
+ false
+ }
+ ) {
+ log.debug(
+ s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}"
+ )
+ }
+ val (angle, attribution, acceptableDistanceToOwner) = obj match {
+ case p: Player =>
+ (
+ SimpleWorldEntity.validateOrientationEntry(
+ p.Orientation + Vector3.z(p.FacingYawUpper)
+ ),
+ tool.Definition.ObjectId,
+ 10f + (if (p.Velocity.nonEmpty) {
+ 5f
+ } else {
+ 0f
+ })
+ )
+ case v: Vehicle if v.Definition.CanFly =>
+ (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle
+ case _: Vehicle =>
+ (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle
+ case _ =>
+ (obj.Orientation, obj.Definition.ObjectId, 300f)
+ }
+ val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position)
+ if (distanceToOwner <= acceptableDistanceToOwner) {
+ val projectile_info = tool.Projectile
+ val wguid = weaponGUID.guid
+ val mountedIn = (continent.turretToWeapon
+ .find { case (guid, _) => guid == wguid } match {
+ case Some((_, turretGuid)) => Some((
+ turretGuid,
+ continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
+ ))
+ case _ => None
+ }) match {
+ case Some((guid, Some(entity))) => Some((guid, entity))
+ case _ => None
+ }
+ val projectile = new Projectile(
+ projectile_info,
+ tool.Definition,
+ tool.FireMode,
+ mountedIn,
+ PlayerSource(player),
+ attribution,
+ shotOrigin,
+ angle,
+ shotVelocity
+ )
+ val initialQuality = tool.FireMode match {
+ case mode: ChargeFireModeDefinition =>
+ ProjectileQuality.Modified(
+ {
+ val timeInterval = projectile.fire_time - ops.shootingStart.getOrElse(tool.GUID, System.currentTimeMillis())
+ timeInterval.toFloat / mode.Time.toFloat
+ }
+ )
+ case _ =>
+ ProjectileQuality.Normal
+ }
+ val qualityprojectile = projectile.quality(initialQuality)
+ qualityprojectile.WhichSide = player.WhichSide
+ ops.projectiles(projectileIndex) = Some(qualityprojectile)
+ if (projectile_info.ExistsOnRemoteClients) {
+ log.trace(
+ s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile"
+ )
+ continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile)
+ }
+ } else {
+ log.warn(
+ s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect"
+ )
+ }
+
+ case _ => ()
+ }
+ }
+
+ /**
+ * 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 {
+ ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue)
+ 3
+ }
+ log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw")
+ 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)
+ }
+ }
+
+ /**
+ * Given an object that contains a box of amunition in its `Inventory` at a certain location,
+ * change the amount of ammunition within that box.
+ * @param obj the `Container`
+ * @param box an `AmmoBox` to modify
+ * @param reloadValue the value to modify the `AmmoBox`;
+ * subtracted from the current `Capacity` of `Box`
+ */
+ private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
+ val capacity = box.Capacity - reloadValue
+ box.Capacity = capacity
+ sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity))
+ }
+
+ /**
+ * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location,
+ * change the amount of ammunition within that box.
+ * @param obj the `Container`
+ * @param box an `AmmoBox` to modify
+ * @param reloadValue the value to modify the `AmmoBox`;
+ * subtracted from the current `Capacity` of `Box`
+ */
+ private def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
+ ModifyAmmunition(obj)(box, reloadValue)
+ obj.Find(box).collect { index =>
+ continent.VehicleEvents ! VehicleServiceMessage(
+ s"${obj.Actor}",
+ VehicleAction.InventoryState(
+ player.GUID,
+ box,
+ obj.GUID,
+ index,
+ box.Definition.Packet.DetailedConstructorData(box).get
+ )
+ )
+ }
+ }
+
+ /**
+ * na
+ * @param tool na
+ * @param obj na
+ */
+ private def PerformToolAmmoChange(
+ tool: Tool,
+ obj: PlanetSideServerObject with Container,
+ modifyFunc: (AmmoBox, Int) => Unit
+ ): Unit = {
+ val originalAmmoType = tool.AmmoType
+ do {
+ val requestedAmmoType = tool.NextAmmoType
+ val fullMagazine = tool.MaxMagazine
+ if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) {
+ FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match {
+ case Nil => ()
+ case x :: xs =>
+ val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj)
+ val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj)
+
+ xs.foreach(item => {
+ obj.Inventory -= item.start
+ sendResponse(ObjectDeleteMessage(item.obj.GUID, 0))
+ TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj))
+ })
+
+ //box will be the replacement ammo; give it the discovered magazine and load it into the weapon
+ val box = x.obj.asInstanceOf[AmmoBox]
+ //previousBox is the current magazine in tool; it will be removed from the weapon
+ val previousBox = tool.AmmoSlot.Box
+ val originalBoxCapacity = box.Capacity
+ val tailReloadValue: Int = if (xs.isEmpty) {
+ 0
+ } else {
+ xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum
+ }
+ val sumReloadValue: Int = originalBoxCapacity + tailReloadValue
+ val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
+ val box_guid = box.GUID
+ val tool_guid = tool.GUID
+ obj.Inventory -= x.start //remove replacement ammo from inventory
+ tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool
+ sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f))
+ sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f))
+ sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex))
+
+ //announce swapped ammunition box in weapon
+ val previous_box_guid = previousBox.GUID
+ val boxDef = box.Definition
+ sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity))
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.ChangeAmmo(
+ player.GUID,
+ tool_guid,
+ ammoSlotIndex,
+ previous_box_guid,
+ boxDef.ObjectId,
+ box.GUID,
+ boxDef.Packet.ConstructorData(box).get
+ )
+ )
+
+ //handle inventory contents
+ box.Capacity = if (sumReloadValue <= fullMagazine) {
+ sumReloadValue
+ } else {
+ val splitReloadAmmo: Int = sumReloadValue - fullMagazine
+ log.trace(
+ s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo"
+ )
+ val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo)
+ TaskWorkflow.execute(stowNewFunc(boxForInventory))
+ fullMagazine
+ }
+ sendResponse(
+ InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)
+ ) //should work for both players and vehicles
+ log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}")
+ if (previousBox.Capacity > 0) {
+ //divide capacity across other existing and not full boxes of that ammo type
+ var capacity = previousBox.Capacity
+ val iter = obj.Inventory.Items
+ .filter(entry => {
+ entry.obj match {
+ case item: AmmoBox =>
+ item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity
+ case _ =>
+ false
+ }
+ })
+ .sortBy(_.start)
+ .iterator
+ while (capacity > 0 && iter.hasNext) {
+ val entry = iter.next()
+ val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox]
+ val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity)
+ log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType")
+ capacity -= ammoAllocated
+ modifyFunc(item, -ammoAllocated)
+ }
+ previousBox.Capacity = capacity
+ }
+
+ if (previousBox.Capacity > 0) {
+ //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm
+ obj.Inventory.Fit(previousBox) match {
+ case Some(_) =>
+ stowFunc(previousBox)
+ case None =>
+ sessionLogic.general.normalItemDrop(player, continent)(previousBox)
+ }
+ AmmoBox.Split(previousBox) match {
+ case Nil | List(_) => () //done (the former case is technically not possible)
+ case _ :: toUpdate =>
+ modifyFunc(previousBox, 0) //update to changed capacity value
+ toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
+ }
+ } else {
+ TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox))
+ }
+ }
+ }
+ } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType)
+ }
+
+ private def CheckForHitPositionDiscrepancy(
+ projectile_guid: PlanetSideGUID,
+ hitPos: Vector3,
+ target: PlanetSideGameObject with FactionAffinity with Vitality
+ ): Unit = {
+ val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position)
+ if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) {
+ // If the target position on the server does not match the position where the projectile landed within reason there may be foul play
+ log.warn(
+ s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect"
+ )
+ }
+ }
+
+ /**
+ * Find a projectile with the given globally unique identifier and mark it as a resolved shot.
+ * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle.
+ * @param projectile_guid the projectile GUID
+ * @param resolution the resolution status to promote the projectile
+ * @return the projectile
+ */
+ private def ResolveProjectileInteraction(
+ projectile_guid: PlanetSideGUID,
+ resolution: DamageResolution.Value,
+ target: PlanetSideGameObject with FactionAffinity with Vitality,
+ pos: Vector3
+ ): Option[DamageInteraction] = {
+ ops.FindProjectileEntry(projectile_guid) match {
+ case Some(projectile) =>
+ ResolveProjectileInteraction(projectile, resolution, target, pos)
+ case None =>
+ log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found")
+ None
+ }
+ }
+
+ /**
+ * na
+ * @param projectile the projectile object
+ * @param resolution the resolution status to promote the projectile
+ * @return a copy of the projectile
+ */
+ private def ResolveProjectileInteraction(
+ projectile: Projectile,
+ resolution: DamageResolution.Value,
+ target: PlanetSideGameObject with FactionAffinity with Vitality,
+ pos: Vector3
+ ): Option[DamageInteraction] = {
+ if (projectile.isMiss) {
+ log.warn("expected projectile was already counted as a missed shot; can not resolve any further")
+ None
+ } else {
+ val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player))
+ if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) {
+ avatarActor ! AvatarActor.ConsumeStamina(10)
+ }
+ Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos))
+ }
+ }
+
+ /**
+ * Take a projectile that was introduced into the game world and
+ * determine if it generates a secondary damage projectile or
+ * an method of damage causation that requires additional management.
+ * @param projectile the projectile
+ * @param pguid the client-local projectile identifier
+ * @param hitPos the game world position where the projectile is being recorded
+ * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location;
+ * nothing if no targets were affected
+ */
+ private def HandleDamageProxy(
+ projectile: Projectile,
+ pguid: PlanetSideGUID,
+ hitPos: Vector3
+ ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = {
+ GlobalDefinitions.getDamageProxy(projectile, hitPos) match {
+ case Nil =>
+ Nil
+ case list if list.isEmpty =>
+ Nil
+ case list =>
+ HandleDamageProxySetupLittleBuddy(list, hitPos)
+ UpdateProjectileSidednessAfterHit(projectile, hitPos)
+ val projectileSide = projectile.WhichSide
+ list.flatMap { proxy =>
+ if (proxy.profile.ExistsOnRemoteClients) {
+ proxy.Position = hitPos
+ proxy.WhichSide = projectileSide
+ continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy)
+ Nil
+ } else if (proxy.tool_def == GlobalDefinitions.maelstrom) {
+ //server-side maelstrom grenade target selection
+ val radius = proxy.profile.LashRadius * proxy.profile.LashRadius
+ val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList })
+ .filter { target =>
+ Vector3.DistanceSquared(target.Position, hitPos) <= radius
+ }
+ //chainlash is separated from the actual damage application for convenience
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.SendResponse(
+ PlanetSideGUID(0),
+ ChainLashMessage(
+ hitPos,
+ projectile.profile.ObjectId,
+ targets.map { _.GUID }
+ )
+ )
+ )
+ targets.map { target =>
+ CheckForHitPositionDiscrepancy(pguid, hitPos, target)
+ (target, proxy, hitPos, target.Position)
+ }
+ } else {
+ Nil
+ }
+ }
+ }
+ }
+
+ private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = {
+ val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw }
+ val size: Int = listOfLittleBuddies.size
+ if (size > 0) {
+ val desiredDownwardsProjectiles: Int = 2
+ val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down
+ val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out
+ val z: Float = player.Orientation.z //player's standing direction
+ val north: Vector3 = Vector3(0,1,0) //map North
+ val speed: Float = 144f //speed (packet discovered)
+ val dist: Float = 25 //distance (client defined)
+ val downwardsAngle: Float = -85f
+ val flaredAngle: Float = -70f
+ //angle of separation for downwards, degrees from vertical for flared out
+ val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) {
+ (360f / firstHalf, downwardsAngle)
+ } else {
+ (0f, 0f)
+ }
+ val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) {
+ (360f / secondHalf, flaredAngle)
+ } else {
+ (0f, 0f)
+ }
+ val smallRotOffset: Float = z + 90f
+ val largeRotOffset: Float = z + math.random().toFloat * 45f
+ val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat)
+ //downwards projectiles
+ var i: Int = 0
+ listOfLittleBuddies.take(firstHalf).foreach { proxy =>
+ val facing = (smallRotOffset + smallStep * i.toFloat) % 360
+ val dir = north.Rx(smallAngle).Rz(facing)
+ proxy.Position = detonationPosition + dir.xy + verticalCorrection
+ proxy.Velocity = dir * speed
+ proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing)
+ HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
+ i += 1
+ }
+ //flared out projectiles
+ i = 0
+ listOfLittleBuddies.drop(firstHalf).foreach { proxy =>
+ val facing = (largeRotOffset + largeStep * i.toFloat) % 360
+ val dir = north.Rx(largeAngle).Rz(facing)
+ proxy.Position = detonationPosition + dir
+ proxy.Velocity = dir * speed
+ proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing)
+ HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
+ i += 1
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = {
+ //explosion
+ val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction)
+ obj.Position = obj.Position + orientation * distance
+ val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner)
+ context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() }
+ }
+
+ /*
+ 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 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 = {
+ 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 handleReloadProcedure(
+ itemGuid: PlanetSideGUID,
+ obj: PlanetSideGameObject with Container,
+ tools: Set[Tool],
+ unk1: Int,
+ deleteFunc: Equipment => Future[Any],
+ modifyFunc: (AmmoBox, Int) => Unit,
+ messageFunc: PlanetSideGUID => Unit
+ ): Unit = {
+ tools
+ .filter { _.GUID == itemGuid }
+ .foreach { tool =>
+ val currentMagazine : Int = tool.Magazine
+ val magazineSize : Int = tool.MaxMagazine
+ val reloadValue : Int = magazineSize - currentMagazine
+ if (magazineSize > 0 && reloadValue > 0) {
+ FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match {
+ case Nil => ()
+ case x :: xs =>
+ xs.foreach { item => deleteFunc(item.obj) }
+ val box = x.obj.asInstanceOf[AmmoBox]
+ val tailReloadValue : Int = if (xs.isEmpty) {
+ 0
+ }
+ else {
+ xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum
+ }
+ val sumReloadValue : Int = box.Capacity + tailReloadValue
+ val actualReloadValue = if (sumReloadValue <= reloadValue) {
+ deleteFunc(box)
+ sumReloadValue
+ }
+ else {
+ modifyFunc(box, reloadValue - tailReloadValue)
+ reloadValue
+ }
+ val finalReloadValue = actualReloadValue + currentMagazine
+ log.info(
+ s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}"
+ )
+ tool.Magazine = finalReloadValue
+ sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1))
+ messageFunc(itemGuid)
+ }
+ } else {
+ //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it
+ sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize))
+ }
+ }
+ }
+
+ private def handleReloadWhenPlayer(
+ itemGuid: PlanetSideGUID,
+ obj: Player,
+ tools: Set[Tool],
+ unk1: Int
+ ): Unit = {
+ handleReloadProcedure(
+ itemGuid,
+ obj,
+ tools,
+ unk1,
+ RemoveOldEquipmentFromInventory(obj)(_),
+ ModifyAmmunition(obj)(_, _),
+ reloadPlayerMessages
+ )
+ }
+
+ private def handleReloadWhenMountable(
+ itemGuid: PlanetSideGUID,
+ obj: PlanetSideServerObject with Container,
+ tools: Set[Tool],
+ unk1: Int
+ ): Unit = {
+ handleReloadProcedure(
+ itemGuid,
+ obj,
+ tools,
+ unk1,
+ RemoveOldEquipmentFromInventory(obj)(_),
+ ModifyAmmunitionInMountable(obj)(_, _),
+ reloadVehicleMessages
+ )
+ }
+
+ //noinspection SameParameterValue
+ private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
+ ops.addShotsToMap(ops.shotsLanded, weaponId, shots)
+ }
+
+ private def CompileAutomatedTurretDamageData(
+ turret: AutomatedTurret,
+ owner: SourceEntry,
+ projectileTypeId: Long
+ ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
+ turret.Weapons
+ .values
+ .flatMap { _.Equipment }
+ .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
+ .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
+ }
+
+ private def HandleAIDamage(
+ target: PlanetSideServerObject with FactionAffinity with Vitality,
+ results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
+ ): Unit = {
+ results.collect {
+ case (obj, tool, owner, projectileInfo) =>
+ val angle = Vector3.Unit(target.Position - obj.Position)
+ val proj = new Projectile(
+ projectileInfo,
+ tool.Definition,
+ tool.FireMode,
+ None,
+ owner,
+ obj.Definition.ObjectId,
+ obj.Position + Vector3.z(value = 1f),
+ angle,
+ Some(angle * projectileInfo.FinalVelocity)
+ )
+ val hitPos = target.Position + Vector3.z(value = 1f)
+ ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ }
+ }
+
+ private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = {
+ val origin = projectile.Position
+ val distance = Vector3.Magnitude(hitPosition - origin)
+ continent.blockMap
+ .sector(hitPosition, distance)
+ .environmentList
+ .collect { case o: InteriorDoorPassage =>
+ val door = o.door
+ val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints(
+ origin,
+ hitPosition,
+ door.Position,
+ door.Definition.UseRadius + 0.1f
+ )
+ (door, intersectTest)
+ }
+ .collect { case (door, intersectionTest) if intersectionTest.nonEmpty =>
+ (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest)
+ }
+ .minByOption { case (_, dist, _) => dist }
+ .foreach { case (door, _, intersects) =>
+ val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) {
+ Sidedness.OutsideOf
+ } else {
+ Sidedness.InsideOf
+ }
+ projectile.WhichSide = if (intersects.size == 1) {
+ Sidedness.InBetweenSides(door, strictly)
+ } else {
+ strictly
+ }
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala
new file mode 100644
index 000000000..e6ef12b6d
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala
@@ -0,0 +1,588 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.support.AvatarHandlerFunctions
+import net.psforever.actors.zone.ZoneActor
+import net.psforever.objects.Players
+import net.psforever.objects.avatar.scoring.Kill
+import net.psforever.objects.sourcing.PlayerSource
+import net.psforever.packet.game.{AvatarImplantMessage, ImplantAction}
+
+import scala.concurrent.duration._
+//
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionAvatarHandlers, SessionData}
+import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
+import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle}
+import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
+import net.psforever.objects.inventory.InventoryItem
+import net.psforever.objects.serverobject.pad.VehicleSpawnPad
+import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
+import net.psforever.objects.vital.etc.ExplodingEntityReason
+import net.psforever.objects.zones.Zoning
+import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
+import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage}
+import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage}
+import net.psforever.services.Service
+import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3}
+import net.psforever.util.Config
+
+object AvatarHandlerLogic {
+ def apply(ops: SessionAvatarHandlers): AvatarHandlerLogic = {
+ new AvatarHandlerLogic(ops, ops.context)
+ }
+}
+
+class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: ActorContext) extends AvatarHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /**
+ * na
+ * @param toChannel na
+ * @param guid na
+ * @param reply na
+ */
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = {
+ val resolvedPlayerGuid = if (player != null && player.HasGUID) {
+ player.GUID
+ } else {
+ Service.defaultPlayerGUID
+ }
+ val isNotSameTarget = resolvedPlayerGuid != guid
+ val isSameTarget = !isNotSameTarget
+ reply match {
+ /* special messages */
+ case AvatarResponse.TeardownConnection() =>
+ log.trace(s"ending ${player.Name}'s old session by event system request (relog)")
+ context.stop(context.self)
+
+ /* really common messages (very frequently, every life) */
+ case pstate @ AvatarResponse.PlayerState(
+ pos,
+ vel,
+ yaw,
+ pitch,
+ yawUpper,
+ _,
+ isCrouching,
+ isJumping,
+ jumpThrust,
+ isCloaking,
+ isNotRendered,
+ canSeeReallyFar
+ ) if isNotSameTarget =>
+ val pstateToSave = pstate.copy(timestamp = 0)
+ val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = ops.lastSeenStreamMessage.get(guid.guid) match {
+ case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting)
+ case _ => (None, 0L, Vector3.Zero, false, None)
+ }
+ val drawConfig = Config.app.game.playerDraw //m
+ val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m
+ val ourPosition = player.Position //xyz
+ val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m
+ val inDrawableRange = currentDistance <= maxRange
+ val now = System.currentTimeMillis() //ms
+ if (
+ sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing &&
+ !isNotRendered && inDrawableRange
+ ) {
+ //conditions where visibility is assured
+ val durationSince = now - lastTime //ms
+ lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange
+ lazy val targetDelay = {
+ val populationOver = math.max(
+ 0,
+ sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold
+ )
+ val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m
+ val adjustedDistance = currentDistance + distanceAdjustment //sq.m
+ drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match {
+ case -1 => 1
+ case index => drawConfig.delays(index)
+ }
+ } //ms
+ if (!wasVisible ||
+ !previouslyInDrawableRange ||
+ durationSince > drawConfig.delayMax ||
+ (!lastMsg.contains(pstateToSave) &&
+ (canSeeReallyFar ||
+ currentDistance < drawConfig.rangeMin * drawConfig.rangeMin ||
+ sessionLogic.general.canSeeReallyFar ||
+ durationSince > targetDelay
+ )
+ )
+ ) {
+ //must draw
+ sendResponse(
+ PlayerStateMessage(
+ guid,
+ pos,
+ vel,
+ yaw,
+ pitch,
+ yawUpper,
+ timestamp = 0, //is this okay?
+ isCrouching,
+ isJumping,
+ jumpThrust,
+ isCloaking
+ )
+ )
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now))
+ } else {
+ //is visible, but skip reinforcement
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime))
+ }
+ } else {
+ //conditions where the target is not currently visible
+ if (wasVisible) {
+ //the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance
+ val lat = (1 + ops.hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat
+ sendResponse(
+ PlayerStateMessage(
+ guid,
+ Vector3(1f, lat, 1f),
+ vel=None,
+ facingYaw=0f,
+ facingPitch=0f,
+ facingYawUpper=0f,
+ timestamp=0, //is this okay?
+ is_cloaked = isCloaking
+ )
+ )
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now))
+ } else {
+ //skip drawing altogether
+ ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime))
+ }
+ }
+
+ case AvatarResponse.ObjectHeld(slot, _)
+ if isSameTarget && player.VisibleSlots.contains(slot) =>
+ sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
+ //Stop using proximity terminals if player unholsters a weapon
+ continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect {
+ case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term)
+ }
+ if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
+ sessionLogic.zoning.spawn.stopDeconstructing()
+ }
+
+ case AvatarResponse.ObjectHeld(slot, _)
+ if isSameTarget && slot > -1 =>
+ sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
+
+ case AvatarResponse.ObjectHeld(_, _)
+ if isSameTarget => ()
+
+ case AvatarResponse.ObjectHeld(_, previousSlot) =>
+ sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false))
+
+ case AvatarResponse.ChangeFireState_Start(weaponGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ sendResponse(ChangeFireStateMessage_Start(weaponGuid))
+ val entry = ops.lastSeenStreamMessage(guid.guid)
+ ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid)))
+
+ case AvatarResponse.ChangeFireState_Start(weaponGuid)
+ if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Start(weaponGuid))
+
+ case AvatarResponse.ChangeFireState_Stop(weaponGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } =>
+ sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+ val entry = ops.lastSeenStreamMessage(guid.guid)
+ ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None))
+
+ case AvatarResponse.ChangeFireState_Stop(weaponGuid)
+ if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+
+ case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
+
+ case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) =>
+ sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
+
+ case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
+
+ case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
+ sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
+
+ case AvatarResponse.HitHint(sourceGuid) if player.isAlive =>
+ sendResponse(HitHint(sourceGuid, guid))
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
+
+ case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
+ // guid = victim // killer = killer
+ sendResponse(DestroyMessage(victim, killer, weapon, pos))
+
+ case AvatarResponse.DestroyDisplay(killer, victim, method, unk) =>
+ sendResponse(ops.destroyDisplayMessage(killer, victim, method, unk))
+
+ case AvatarResponse.TerminalOrderResult(terminalGuid, action, result)
+ if result && (action == TransactionType.Buy || action == TransactionType.Loadout) =>
+ sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
+ sessionLogic.terminals.lastTerminalOrderFulfillment = true
+ AvatarActor.savePlayerData(player)
+ sessionLogic.general.renewCharSavedTimer(
+ Config.app.game.savedMsg.interruptedByAction.fixed,
+ Config.app.game.savedMsg.interruptedByAction.variable
+ )
+
+ case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
+ sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
+ sessionLogic.terminals.lastTerminalOrderFulfillment = true
+
+ case AvatarResponse.ChangeExosuit(
+ target,
+ armor,
+ exosuit,
+ subtype,
+ _,
+ maxhand,
+ oldHolsters,
+ holsters,
+ oldInventory,
+ inventory,
+ drop,
+ delete
+ ) if resolvedPlayerGuid == target =>
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
+ //happening to this player
+ //cleanup
+ sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false))
+ (oldHolsters ++ oldInventory ++ delete).foreach {
+ case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+ }
+ //functionally delete
+ delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) }
+ //redraw
+ if (maxhand) {
+ TaskWorkflow.execute(HoldNewEquipmentUp(player)(
+ Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
+ 0
+ ))
+ }
+ //draw free hand
+ player.FreeHand.Equipment.foreach { obj =>
+ val definition = obj.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(target, Player.FreeHandSlot),
+ definition.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ }
+ //draw holsters and inventory
+ (holsters ++ inventory).foreach {
+ case InventoryItem(obj, index) =>
+ val definition = obj.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(target, index),
+ definition.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ }
+ DropLeftovers(player)(drop)
+
+ case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, _, delete) =>
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
+ //happening to some other player
+ sendResponse(ObjectHeldMessage(target, slot, unk1 = false))
+ //cleanup
+ (oldHolsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
+ //draw holsters
+ holsters.foreach {
+ case InventoryItem(obj, index) =>
+ val definition = obj.Definition
+ sendResponse(
+ ObjectCreateMessage(
+ definition.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(target, index),
+ definition.Packet.ConstructorData(obj).get
+ )
+ )
+ }
+
+ case AvatarResponse.ChangeLoadout(
+ target,
+ armor,
+ exosuit,
+ subtype,
+ _,
+ maxhand,
+ oldHolsters,
+ holsters,
+ oldInventory,
+ inventory,
+ drops
+ ) if resolvedPlayerGuid == target =>
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type = 4, armor))
+ //happening to this player
+ sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true))
+ //cleanup
+ (oldHolsters ++ oldInventory).foreach {
+ case (obj, objGuid) =>
+ sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
+ TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
+ }
+ drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
+ //redraw
+ if (maxhand) {
+ TaskWorkflow.execute(HoldNewEquipmentUp(player)(
+ Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
+ slot = 0
+ ))
+ }
+ sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
+ DropLeftovers(player)(drops)
+
+ case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) =>
+ //redraw handled by callbacks
+ sendResponse(ArmorChangedMessage(target, exosuit, subtype))
+ sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
+ //happening to some other player
+ sendResponse(ObjectHeldMessage(target, slot, unk1=false))
+ //cleanup
+ oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
+
+ case AvatarResponse.UseKit(kguid, kObjId) =>
+ sendResponse(
+ UseItemMessage(
+ resolvedPlayerGuid,
+ kguid,
+ resolvedPlayerGuid,
+ unk2 = 4294967295L,
+ unk3 = false,
+ unk4 = Vector3.Zero,
+ unk5 = Vector3.Zero,
+ unk6 = 126,
+ unk7 = 0, //sequence time?
+ unk8 = 137,
+ kObjId
+ )
+ )
+ sendResponse(ObjectDeleteMessage(kguid, unk1=0))
+
+ case AvatarResponse.KitNotUsed(_, "") =>
+ sessionLogic.general.kitToBeUsed = None
+
+ case AvatarResponse.KitNotUsed(_, msg) =>
+ sessionLogic.general.kitToBeUsed = None
+ sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
+
+ case AvatarResponse.UpdateKillsDeathsAssists(_, kda: Kill) if kda.experienceEarned > 0 =>
+ continent.actor ! ZoneActor.RewardOurSupporters(
+ PlayerSource(player),
+ Players.produceContributionTranscriptFromKill(continent, player, kda),
+ kda,
+ kda.experienceEarned
+ )
+
+ case AvatarResponse.AwardBep(charId, bep, expType) =>
+ //if the target player, always award (some) BEP
+ if (charId == player.CharId) {
+ avatarActor ! AvatarActor.AwardBep(bep, expType)
+ }
+
+ case AvatarResponse.AwardCep(charId, cep) =>
+ //if the target player, always award (some) CEP
+ if (charId == player.CharId) {
+ avatarActor ! AvatarActor.AwardCep(cep)
+ }
+
+ 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)
+
+ case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid =>
+ sendResponse(msg)
+
+ /* common messages (maybe once every respawn) */
+ case AvatarResponse.Reload(itemGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
+
+ case AvatarResponse.Killed(mount) =>
+ //log and chat messages
+ val cause = player.LastDamage.flatMap { damage =>
+ val interaction = damage.interaction
+ val reason = interaction.cause
+ val adversarial = interaction.adversarial.map { _.attacker }
+ reason match {
+ case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] =>
+ //also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
+ case _ => ()
+ }
+ adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
+ }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
+ log.info(s"${player.Name} has died, killed by $cause")
+ if (sessionLogic.shooting.shotsWhileDead > 0) {
+ log.warn(
+ s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server"
+ )
+ sessionLogic.shooting.shotsWhileDead = 0
+ }
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel")
+ sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
+
+ //player state changes
+ AvatarActor.updateToolDischargeFor(avatar)
+ player.FreeHand.Equipment.foreach { item =>
+ DropEquipmentFromInventory(player)(item)
+ }
+ sessionLogic.general.dropSpecialSlotItem()
+ sessionLogic.general.toggleMaxSpecialState(enable = false)
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+ sessionLogic.zoning.zoningStatus = Zoning.Status.None
+ sessionLogic.zoning.spawn.deadState = DeadState.Dead
+ continent.GUID(mount).collect { case obj: Vehicle =>
+ sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
+ sessionLogic.general.unaccessContainer(obj)
+ }
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
+ AvatarActor.savePlayerLocation(player)
+ sessionLogic.zoning.spawn.shiftPosition = Some(player.Position)
+
+ //respawn
+ sessionLogic.zoning.spawn.reviveTimer.cancel()
+ if (player.death_by == 0) {
+ sessionLogic.zoning.spawn.randomRespawn(300.seconds)
+ } else {
+ sessionLogic.zoning.spawn.HandleReleaseAvatar(player, continent)
+ }
+
+ case AvatarResponse.Release(tplayer) if isNotSameTarget =>
+ sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer)
+
+ case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid =>
+ log.info(s"No time for rest, ${player.Name}. Back on your feet!")
+ sessionLogic.zoning.spawn.reviveTimer.cancel()
+ sessionLogic.zoning.spawn.deadState = DeadState.Alive
+ player.Revive
+ val health = player.Health
+ sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health))
+ sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true))
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health)
+ )
+
+ /* uncommon messages (utility, or once in a while) */
+ case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
+ sendResponse(ChangeAmmoMessage(weapon_guid, 1))
+
+ case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
+ if isNotSameTarget =>
+ ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
+
+ case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget =>
+ sendResponse(ChangeFireModeMessage(itemGuid, mode))
+
+ case AvatarResponse.ConcealPlayer() =>
+ sendResponse(GenericObjectActionMessage(guid, code=9))
+
+ case AvatarResponse.EnvironmentalDamage(_, _, _) =>
+ //TODO damage marker?
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
+
+ case AvatarResponse.DropItem(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget =>
+ sendResponse(ObjectDeleteMessage(itemGuid, unk))
+
+ /* rare messages */
+ case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget =>
+ sendResponse(SetEmpireMessage(objectGuid, faction))
+
+ case AvatarResponse.DropSpecialItem() =>
+ sessionLogic.general.dropSpecialSlotItem()
+
+ case AvatarResponse.OxygenState(player, vehicle) =>
+ sendResponse(OxygenStateMessage(
+ DrowningTarget(player.guid, player.progress, player.state),
+ vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) }
+ ))
+
+ case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget =>
+ sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid))
+
+ case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) =>
+ sendResponse(
+ ProjectileStateMessage(
+ projectileGuid,
+ projectile.Position,
+ shot_vel = Vector3.Zero,
+ projectile.Orientation,
+ sequence_num=0,
+ end=true,
+ hit_target_guid=PlanetSideGUID(0)
+ )
+ )
+ sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2))
+
+ case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
+ sendResponse(GenericActionMessage(mode))
+
+ case AvatarResponse.PutDownFDU(target) if isNotSameTarget =>
+ sendResponse(GenericObjectActionMessage(target, code=53))
+
+ case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget =>
+ val definition = item.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ definition.ObjectId,
+ item.GUID,
+ ObjectCreateMessageParent(target, slot),
+ definition.Packet.DetailedConstructorData(item).get
+ )
+ )
+
+ case AvatarResponse.WeaponDryFire(weaponGuid)
+ if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
+ continent.GUID(weaponGuid).collect {
+ case tool: Tool if tool.Magazine == 0 =>
+ // check that the magazine is still empty before sending WeaponDryFireMessage
+ // if it has been reloaded since then, other clients will not see it firing
+ sendResponse(WeaponDryFireMessage(weaponGuid))
+ }
+
+ case _ => ()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala
new file mode 100644
index 000000000..3b7d10edd
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala
@@ -0,0 +1,142 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.ActorContext
+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.packet.game.{ChatMsg, SetChatFilterMessage}
+import net.psforever.services.chat.SpectatorChannel
+import net.psforever.types.ChatMessageType
+
+import scala.collection.Seq
+
+object ChatLogic {
+ def apply(ops: ChatOperations): ChatLogic = {
+ new ChatLogic(ops, ops.context)
+ }
+}
+
+class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ def handleChatMsg(message: ChatMsg): Unit = {
+ import ChatMessageType._
+ (message.messageType, message.recipient.trim, message.contents.trim) match {
+ /** Messages starting with ! are custom chat commands */
+ case (_, _, contents) if contents.startsWith("!") &&
+ customCommandMessages(message, session) => ()
+
+ case (CMT_FLY, recipient, contents) =>
+ ops.commandFly(contents, recipient)
+
+ case (CMT_ANONYMOUS, _, _) =>
+ // ?
+
+ case (CMT_TOGGLE_GM, _, _) =>
+ // ?
+
+ case (CMT_CULLWATERMARK, _, contents) =>
+ ops.commandWatermark(contents)
+
+ case (CMT_SPEED, _, contents) =>
+ ops.commandSpeed(message, contents)
+
+ case (CMT_TOGGLESPECTATORMODE, _, contents) =>
+ commandToggleSpectatorMode(contents)
+
+ case (CMT_RECALL, _, _) =>
+ commandToggleSpectatorMode(contents = "off")
+
+ case (CMT_QUIT, _, _) =>
+ ops.commandQuit(session)
+
+ case (CMT_SUICIDE, _, _) =>
+ commandToggleSpectatorMode(contents = "off")
+
+ case (CMT_OPEN, _, _) =>
+ ops.commandSendToRecipient(session, message, SpectatorChannel)
+
+ case (CMT_VOICE, _, contents) =>
+ ops.commandVoice(session, message, contents, SpectatorChannel)
+
+ case (CMT_TELL, _, _) =>
+ ops.commandTellOrIgnore(session, message, SpectatorChannel)
+
+ case (CMT_BROADCAST, _, _) =>
+ ops.commandSendToRecipient(session, message, SpectatorChannel)
+
+ case (CMT_PLATOON, _, _) =>
+ ops.commandSendToRecipient(session, message, SpectatorChannel)
+
+ case (CMT_GMTELL, _, _) =>
+ ops.commandSend(session, message, SpectatorChannel)
+
+ case (CMT_NOTE, _, _) =>
+ ops.commandSend(session, message, SpectatorChannel)
+
+ case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
+ ops.commandWho(session)
+
+ case (CMT_ZONE, _, contents) =>
+ ops.commandZone(message, contents)
+
+ case (CMT_WARP, _, contents) =>
+ ops.commandWarp(session, message, contents)
+
+ case _ => ()
+ }
+ }
+
+ def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
+ val SetChatFilterMessage(_, _, _) = pkt
+ }
+
+ def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
+ import ChatMessageType._
+ message.messageType match {
+ case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
+ ops.commandIncomingSendAllIfOnline(session, message)
+
+ case CMT_OPEN =>
+ ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
+
+ case CMT_TELL | U_CMT_TELLFROM |
+ CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
+ CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
+ ops.commandIncomingSend(message)
+
+ case _ => ()
+ }
+ }
+
+ private def customCommandMessages(
+ message: ChatMsg,
+ session: Session
+ ): Boolean = {
+ val contents = message.contents
+ if (contents.startsWith("!")) {
+ val (command, params) = ops.cliTokenization(contents.drop(1)) match {
+ case a :: b => (a, b)
+ case _ => ("", Seq(""))
+ }
+ command match {
+ case "list" => ops.customCommandList(session, params, message)
+ case "nearby" => ops.customCommandNearby(session)
+ case "loc" => ops.customCommandLoc(session, message)
+ case _ => false
+ }
+ } else {
+ false
+ }
+ }
+
+ private def commandToggleSpectatorMode(contents: String): Unit = {
+ contents.toLowerCase() match {
+ case "off" | "of" =>
+ context.self ! SessionActor.SetMode(NormalMode)
+ case _ => ()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala
new file mode 100644
index 000000000..7ecff65b9
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala
@@ -0,0 +1,85 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData}
+import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo}
+import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage}
+import net.psforever.types.{MemberAction, PlanetSideEmpire}
+
+object GalaxyHandlerLogic {
+ def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = {
+ new GalaxyHandlerLogic(ops, ops.context)
+ }
+}
+
+class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ private val galaxyService: ActorRef = ops.galaxyService
+
+ /* packets */
+
+ def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = {
+ sendResponse(pkt)
+ pkt.friends.foreach { f =>
+ galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name))
+ }
+ }
+
+ /* response handlers */
+
+ def handle(reply: GalaxyResponse.Response): Unit = {
+ reply match {
+ case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) =>
+ sendResponse(
+ HotSpotUpdateMessage(
+ zone_index,
+ priority,
+ hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
+ )
+ )
+
+ case GalaxyResponse.MapUpdate(msg) =>
+ sendResponse(msg)
+
+ case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
+ val faction = player.Faction
+ val from = fromFactions.contains(faction)
+ val to = toFactions.contains(faction)
+ if (from && !to) {
+ sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
+ } else if (!from && to) {
+ sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
+ }
+
+ case GalaxyResponse.FlagMapUpdate(msg) =>
+ sendResponse(msg)
+
+ case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
+ sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest)
+
+ case GalaxyResponse.LockedZoneUpdate(zone, time) =>
+ sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
+
+ case GalaxyResponse.UnlockedZoneUpdate(zone) => ;
+ sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
+ val popBO = 0
+ val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
+ val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
+ val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
+ sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
+
+ case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) =>
+ avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
+
+ case GalaxyResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case _ => ()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
new file mode 100644
index 000000000..2332b5115
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
@@ -0,0 +1,579 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
+import net.psforever.objects.{Account, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle}
+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}
+import net.psforever.objects.equipment.Equipment
+import net.psforever.objects.serverobject.CommonMessages
+import net.psforever.objects.serverobject.doors.Door
+import net.psforever.objects.vehicles.{Utility, UtilityType}
+import net.psforever.objects.vehicles.Utility.InternalTelepad
+import net.psforever.objects.zones.ZoneProjectile
+import net.psforever.packet.PlanetSideGamePacket
+import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
+import net.psforever.services.account.AccountPersistenceService
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.types.{ChatMessageType, DriveState, ExoSuitType, PlanetSideGUID, Vector3}
+import net.psforever.util.Config
+
+object GeneralLogic {
+ def apply(ops: GeneralOperations): GeneralLogic = {
+ new GeneralLogic(ops, ops.context)
+ }
+}
+
+class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContext) extends GeneralFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ 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 */ }
+
+ def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { /* intentionally blank */ }
+
+ def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = {
+ val PlayerStateMessageUpstream(
+ avatarGuid,
+ pos,
+ vel,
+ yaw,
+ pitch,
+ yawUpper,
+ _/*seqTime*/,
+ _,
+ isCrouching,
+ isJumping,
+ _/*jumpThrust*/,
+ isCloaking,
+ _,
+ _
+ )= pkt
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(avatarGuid)
+ ops.fallHeightTracker(pos.z)
+ // if (isCrouching && !player.Crouching) {
+ // //dev stuff goes here
+ // }
+ player.Position = pos
+ player.Velocity = vel
+ player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
+ player.FacingYawUpper = yawUpper
+ player.Crouching = isCrouching
+ player.Jumping = isJumping
+ player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
+ log.debug(s"$pkt")
+ sendResponse(VoiceHostKill())
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
+ )
+ }
+
+ def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = {
+ log.debug(s"$pkt")
+ sendResponse(VoiceHostKill())
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
+ )
+ }
+
+ def handleEmote(pkt: EmoteMsg): Unit = {
+ val EmoteMsg(avatarGuid, emote) = pkt
+ sendResponse(EmoteMsg(avatarGuid, emote))
+ }
+
+ def handleDropItem(pkt: DropItemMessage): Unit = { /* intentionally blank */ }
+
+ def handlePickupItem(pkt: PickupItemMessage): Unit = { /* intentionally blank */ }
+
+ def handleObjectHeld(pkt: ObjectHeldMessage): Unit = {
+ val ObjectHeldMessage(_, heldHolsters, _) = pkt
+ if (heldHolsters != Player.HandsDownSlot && heldHolsters != 4) {
+ sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, unk1=true))
+ }
+ }
+
+ def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* intentionally blank */ }
+
+ def handleZipLine(pkt: ZipLineMessage): Unit = {
+ val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt
+ continent.zipLinePaths.find(x => x.PathId == pathId) match {
+ case Some(path) if path.IsTeleporter =>
+ val endPoint = path.ZipLinePoints.last
+ sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
+ //todo: send to zone to show teleport animation to all clients
+ sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
+ case Some(_) =>
+ action match {
+ case 0 =>
+ //travel along the zipline in the direction specified
+ sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
+ case 1 =>
+ //disembark from zipline at destination!
+ sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
+ case 2 =>
+ //get off by force
+ sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
+ case _ =>
+ log.warn(
+ s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}"
+ )
+ }
+ case _ =>
+ log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}")
+ }
+ }
+
+ def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = {
+ val RequestDestroyMessage(objectGuid) = pkt
+ //make sure this is the correct response for all cases
+ sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match {
+ case Some(obj: Projectile) =>
+ if (!obj.isResolved) {
+ obj.Miss()
+ }
+ continent.Projectile ! ZoneProjectile.Remove(objectGuid)
+
+ case _ => ()
+ }
+ }
+
+ def handleMoveItem(pkt: MoveItemMessage): Unit = { /* intentionally blank */ }
+
+ def handleLootItem(pkt: LootItemMessage): Unit = { /* intentionally blank */ }
+
+ def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = {
+ val AvatarImplantMessage(_, _, slot, _) = pkt
+ customImplants.lift(slot)
+ .collect {
+ case implant if implant.active =>
+ customImplantOff(slot, implant)
+ case implant =>
+ customImplants = customImplants.updated(slot, implant.copy(active = true))
+ sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 1))
+ sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2 + 1))
+ }
+ }
+
+ def handleUseItem(pkt: UseItemMessage): Unit = {
+ sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match {
+ case Some(door: Door) =>
+ handleUseDoor(door, None)
+ case Some(obj: TelepadDeployable) =>
+ handleUseTelepadDeployable(obj, None, pkt)
+ case Some(obj: Utility.InternalTelepad) =>
+ handleUseInternalTelepad(obj, pkt)
+ case _ => ()
+ }
+ }
+
+ def handleUnuseItem(pkt: UnuseItemMessage): Unit = { /* intentionally blank */ }
+
+ def handleDeployObject(pkt: DeployObjectMessage): Unit = { /* intentionally blank */ }
+
+ def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = {
+ val PlanetsideAttributeMessage(objectGuid, attributeType, _/*attributeValue*/) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match {
+ case Some(_: Vehicle) => ()
+ case Some(_: Player) if attributeType == 106 => ()
+ case Some(obj) =>
+ log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}")
+ case _ => ()
+ }
+ }
+
+ def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = {
+ val GenericObjectActionMessage(objectGuid, _/*code*/) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match {
+ case Some(_: Vehicle) => ()
+ case Some(_: Tool) => ()
+ case _ => log.info(s"${player.Name} - $pkt")
+ }
+ }
+
+ def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = {
+ val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt
+ sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match {
+ case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => ()
+ case _ => log.info(s"${player.Name} - $pkt")
+ }
+ }
+
+ def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = {
+ val GenericObjectStateMsg(_, _) = pkt
+ log.info(s"${player.Name} - $pkt")
+ }
+
+ def handleGenericAction(pkt: GenericActionMessage): Unit = {
+ val GenericActionMessage(action) = pkt
+ val (toolOpt, definition) = player.Slot(0).Equipment match {
+ case Some(tool: Tool) =>
+ (Some(tool), tool.Definition)
+ case _ =>
+ (None, GlobalDefinitions.bullet_9mm)
+ }
+ action match {
+ case GenericAction.MaxAnchorsExtend_RCV =>
+ log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground")
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttribute(player.GUID, 19, 1)
+ )
+ definition match {
+ case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
+ val tool = toolOpt.get
+ tool.ToFireMode = 1
+ sendResponse(ChangeFireModeMessage(tool.GUID, 1))
+ case GlobalDefinitions.trhev_pounder =>
+ val tool = toolOpt.get
+ val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 }
+ else { 4 }
+ tool.ToFireMode = convertFireModeIndex
+ sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
+ case _ =>
+ log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}")
+ }
+ case GenericAction.MaxAnchorsRelease_RCV =>
+ log.info(s"${player.Name} has released the anchors")
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttribute(player.GUID, 19, 0)
+ )
+ definition match {
+ case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
+ val tool = toolOpt.get
+ tool.ToFireMode = 0
+ sendResponse(ChangeFireModeMessage(tool.GUID, 0))
+ case GlobalDefinitions.trhev_pounder =>
+ val tool = toolOpt.get
+ val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 }
+ tool.ToFireMode = convertFireModeIndex
+ sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
+ case _ =>
+ log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}")
+ }
+ case GenericAction.AwayFromKeyboard_RCV =>
+ log.info(s"${player.Name} is AFK")
+ AvatarActor.savePlayerLocation(player)
+ ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
+ player.AwayFromKeyboard = true
+ case GenericAction.BackInGame_RCV =>
+ log.info(s"${player.Name} is back")
+ player.AwayFromKeyboard = false
+ ops.renewCharSavedTimer(
+ Config.app.game.savedMsg.renewal.fixed,
+ Config.app.game.savedMsg.renewal.variable
+ )
+ case GenericAction.LookingForSquad_RCV => //Looking For Squad ON
+ if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
+ avatarActor ! AvatarActor.SetLookingForSquad(true)
+ }
+ case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF
+ if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
+ avatarActor ! AvatarActor.SetLookingForSquad(false)
+ }
+ case _ =>
+ log.warn(s"GenericActionMessage: ${player.Name} can't handle $action")
+ }
+ }
+
+ def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { /* intentionally blank */ }
+
+ def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* intentionally blank */ }
+
+ def handleBugReport(pkt: PlanetSideGamePacket): Unit = {
+ val BugReportMessage(
+ _/*versionMajor*/,
+ _/*versionMinor*/,
+ _/*versionDate*/,
+ _/*bugType*/,
+ _/*repeatable*/,
+ _/*location*/,
+ _/*zone*/,
+ _/*pos*/,
+ _/*summary*/,
+ _/*desc*/
+ ) = pkt
+ log.warn(s"${player.Name} filed a bug report - it might be something important")
+ log.debug(s"$pkt")
+ }
+
+ def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = { /* intentionally blank */ }
+
+ def handleBattleplan(pkt: BattleplanMessage): Unit = {
+ val BattleplanMessage(_, name, _, _) = pkt
+ val lament: String = s"$name has a brilliant idea that no one will ever see"
+ log.info(lament)
+ log.debug(s"Battleplan: $lament - $pkt")
+ }
+
+ def handleBindPlayer(pkt: BindPlayerMessage): Unit = {
+ val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt
+ }
+
+ def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
+ val CreateShortcutMessage(_, slot, wouldBeImplant) = pkt
+ val pguid = player.GUID
+ if (slot > 1 && slot < 5) {
+ //protected
+ customImplants
+ .zipWithIndex
+ .find { case (_, index) => index + 2 == slot}
+ .foreach {
+ case (implant, _) if wouldBeImplant.contains(implant.definition.implantType.shortcut) => ()
+ case (implant, _) if implant.active =>
+ sendResponse(CreateShortcutMessage(pguid, slot, Some(implant.definition.implantType.shortcut)))
+ customImplantOff(slot, implant)
+ case (implant, _) =>
+ sendResponse(CreateShortcutMessage(pguid, slot, Some(implant.definition.implantType.shortcut)))
+ }
+ } else {
+ additionalImplants.indexWhere(_.slot == slot) match {
+ case -1 => ()
+ case index =>
+ additionalImplants = additionalImplants.take(index) ++ additionalImplants.drop(index + 1)
+ }
+ wouldBeImplant.collect {
+ case _ =>
+ additionalImplants = additionalImplants :+ pkt
+ }
+ }
+ }
+
+ def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { /* intentionally blank */ }
+
+ def handleFriendRequest(pkt: FriendsRequest): Unit = {
+ val FriendsRequest(action, name) = pkt
+ avatarActor ! AvatarActor.MemberListRequest(action, name)
+ }
+
+ def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = { /* intentionally blank */ }
+
+ def handleActionCancel(pkt: ActionCancelMessage): Unit = {
+ val ActionCancelMessage(_, _, _) = pkt
+ ops.progressBarUpdate.cancel()
+ ops.progressBarValue = None
+ }
+
+ def handleTrade(pkt: TradeMessage): Unit = { /* intentionally blank */ }
+
+ def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = {
+ val DisplayedAwardMessage(_, ribbon, bar) = pkt
+ log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon")
+ avatarActor ! AvatarActor.SetRibbon(ribbon, bar)
+ }
+
+ def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { /* intentionally blank */ }
+
+ def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
+ val TargetingImplantRequest(list) = pkt
+ val targetInfo: List[TargetInfo] = list.flatMap { x =>
+ continent.GUID(x.target_guid) match {
+ case Some(player: Player) =>
+ val health = player.Health.toFloat / player.MaxHealth
+ val armor = if (player.MaxArmor > 0) {
+ player.Armor.toFloat / player.MaxArmor
+ } else {
+ 0
+ }
+ Some(TargetInfo(player.GUID, health, armor))
+ case _ =>
+ log.warn(
+ s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
+ )
+ None
+ }
+ }
+ sendResponse(TargetingInfoMessage(targetInfo))
+ }
+
+ def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ }
+
+ /* messages */
+
+ def handleSetAvatar(avatar: Avatar): Unit = {
+ session = session.copy(avatar = avatar)
+ if (session.player != null) {
+ session.player.avatar = avatar
+ }
+ LivePlayerList.Update(avatar.id, avatar)
+ }
+
+ def handleReceiveAccountData(account: Account): Unit = {
+ log.trace(s"ReceiveAccountData $account")
+ session = session.copy(account = account)
+ avatarActor ! AvatarActor.SetAccount(account)
+ }
+
+ def handleUseCooldownRenew: BasicDefinition => Unit = {
+ case _: KitDefinition => ops.kitToBeUsed = None
+ case _ => ()
+ }
+
+ def handleAvatarResponse(avatar: Avatar): Unit = {
+ session = session.copy(avatar = avatar)
+ sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id)
+ }
+
+ def handleSetSpeed(speed: Float): Unit = {
+ session = session.copy(speed = speed)
+ }
+
+ def handleSetFlying(flying: Boolean): Unit = {
+ session = session.copy(flying = flying)
+ }
+
+ def handleSetSpectator(spectator: Boolean): Unit = {
+ session.player.spectator = spectator
+ }
+
+ def handleKick(player: Player, time: Option[Long]): Unit = {
+ administrativeKick(player)
+ sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
+ }
+
+ def handleSilenced(isSilenced: Boolean): Unit = {
+ player.silenced = isSilenced
+ }
+
+ /* supporting functions */
+
+ private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
+ equipment match {
+ case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
+ val distance: Float = math.max(
+ Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance,
+ door.Definition.initialOpeningDistance
+ )
+ door.Actor ! CommonMessages.Use(player, Some(distance))
+ case _ =>
+ door.Actor ! CommonMessages.Use(player)
+ }
+ }
+
+ private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
+ if (equipment.isEmpty) {
+ (continent.GUID(obj.Router) match {
+ case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
+ case Some(vehicle) => Some(vehicle, None)
+ case None => None
+ }) match {
+ case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
+ player.WhichSide = vehicle.WhichSide
+ useRouterTelepadSystem(
+ router = vehicle,
+ internalTelepad = util,
+ remoteTelepad = obj,
+ src = obj,
+ dest = util
+ )
+ case Some((vehicle: Vehicle, None)) =>
+ log.error(
+ s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
+ )
+ 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 _ => ()
+ }
+ }
+ }
+
+ private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = {
+ continent.GUID(obj.Telepad) match {
+ case Some(pad: TelepadDeployable) =>
+ player.WhichSide = pad.WhichSide
+ useRouterTelepadSystem(
+ router = obj.Owner.asInstanceOf[Vehicle],
+ internalTelepad = obj,
+ remoteTelepad = pad,
+ src = obj,
+ dest = pad
+ )
+ 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 => ()
+ }
+ }
+
+ /**
+ * A player uses a fully-linked Router teleportation system.
+ * @param router the Router vehicle
+ * @param internalTelepad the internal telepad within the Router vehicle
+ * @param remoteTelepad the remote telepad that is currently associated with this Router
+ * @param src the origin of the teleportation (where the player starts)
+ * @param dest the destination of the teleportation (where the player is going)
+ */
+ private def useRouterTelepadSystem(
+ router: Vehicle,
+ internalTelepad: InternalTelepad,
+ remoteTelepad: TelepadDeployable,
+ src: PlanetSideGameObject with TelepadLike,
+ dest: PlanetSideGameObject with TelepadLike
+ ): Unit = {
+ val time = System.currentTimeMillis()
+ if (
+ time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
+ internalTelepad.Active &&
+ remoteTelepad.Active
+ ) {
+ val pguid = player.GUID
+ val sguid = src.GUID
+ val dguid = dest.GUID
+ sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
+ ops.useRouterTelepadEffect(pguid, sguid, dguid)
+ player.Position = dest.Position
+ } else {
+ log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
+ }
+ ops.recentTeleportAttempt = time
+ }
+
+ private def administrativeKick(tplayer: Player): Unit = {
+ log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
+ 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/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/LocalHandlerLogic.scala
new file mode 100644
index 000000000..e0406f564
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/LocalHandlerLogic.scala
@@ -0,0 +1,248 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.ActorContext
+import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers}
+import net.psforever.objects.ce.Deployable
+import net.psforever.objects.vehicles.MountableWeapons
+import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable}
+import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage}
+import net.psforever.services.Service
+import net.psforever.services.local.LocalResponse
+import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
+
+object LocalHandlerLogic {
+ def apply(ops: SessionLocalHandlers): LocalHandlerLogic = {
+ new LocalHandlerLogic(ops, ops.context)
+ }
+}
+
+class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ /* response handlers */
+
+ /**
+ * na
+ * @param toChannel na
+ * @param guid na
+ * @param reply na
+ */
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = {
+ val resolvedPlayerGuid = if (player.HasGUID) {
+ player.GUID
+ } else {
+ Service.defaultPlayerGUID
+ }
+ val isNotSameTarget = resolvedPlayerGuid != guid
+ reply match {
+ case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget =>
+ sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
+
+ case LocalResponse.DeployableUIFor(item) =>
+ sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item))
+
+ case LocalResponse.Detonate(dguid, _: BoomerDeployable) =>
+ sendResponse(TriggerEffectMessage(dguid, "detonate_boomer"))
+ sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) =>
+ sendResponse(GenericObjectActionMessage(dguid, code=19))
+ sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.Detonate(_, obj) =>
+ log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
+
+ case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget =>
+ sendResponse(GenericObjectStateMsg(doorGuid, state=16))
+
+ case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone
+ sendResponse(GenericObjectStateMsg(doorGuid, state=17))
+
+ case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed =>
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
+ obj.Destroyed = true
+ DeconstructDeployable(
+ obj,
+ dguid,
+ pos,
+ obj.Orientation,
+ deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 }
+ )
+
+ case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _)
+ if obj.Destroyed || obj.Jammed || obj.Health == 0 =>
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed =>
+ //if active, deactivate
+ obj.Active = false
+ sendResponse(GenericObjectActionMessage(dguid, code=29))
+ sendResponse(GenericObjectActionMessage(dguid, code=30))
+ //standard deployable elimination behavior
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active =>
+ //if active, deactivate
+ obj.Active = false
+ sendResponse(GenericObjectActionMessage(dguid, code=29))
+ sendResponse(GenericObjectActionMessage(dguid, code=30))
+ //standard deployable elimination behavior
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed =>
+ //standard deployable elimination behavior
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
+ //standard deployable elimination behavior
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
+
+ case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed =>
+ sendResponse(ObjectDeleteMessage(dguid, unk1=0))
+
+ case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
+ obj.Destroyed = true
+ DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
+
+ case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) =>
+ sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2))
+
+ case LocalResponse.HackObject(targetGuid, unk1, unk2) =>
+ sessionLogic.general.hackObject(targetGuid, unk1, unk2)
+
+ case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) =>
+ sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue)
+
+ case LocalResponse.GenericObjectAction(targetGuid, actionNumber) =>
+ sendResponse(GenericObjectActionMessage(targetGuid, actionNumber))
+
+ case LocalResponse.GenericActionMessage(actionNumber) =>
+ sendResponse(GenericActionMessage(actionNumber))
+
+ case LocalResponse.ChatMessage(msg) =>
+ sendResponse(msg)
+
+ case LocalResponse.SendPacket(packet) =>
+ sendResponse(packet)
+
+ case LocalResponse.LluSpawned(llu) =>
+ // Create LLU on client
+ sendResponse(ObjectCreateMessage(
+ llu.Definition.ObjectId,
+ llu.GUID,
+ llu.Definition.Packet.ConstructorData(llu).get
+ ))
+ sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f))
+
+ case LocalResponse.LluDespawned(lluGuid, position) =>
+ sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f))
+ sendResponse(ObjectDeleteMessage(lluGuid, unk1=0))
+ // If the player was holding the LLU, remove it from their tracked special item slot
+ sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid =>
+ sessionLogic.general.specialItemSlotGuid = None
+ player.Carrying = None
+ }
+
+ case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget =>
+ sendResponse(ObjectDeleteMessage(objectGuid, unk))
+
+ case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
+ sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true))
+
+ case LocalResponse.ProximityTerminalEffect(objectGuid, false) =>
+ sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false))
+ sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid)
+
+ case LocalResponse.RouterTelepadMessage(msg) =>
+ sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None))
+
+ case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) =>
+ sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid)
+
+ case LocalResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case LocalResponse.SetEmpire(objectGuid, empire) =>
+ sendResponse(SetEmpireMessage(objectGuid, empire))
+
+ case LocalResponse.ShuttleEvent(ev) =>
+ val msg = OrbitalShuttleTimeMsg(
+ ev.u1,
+ ev.u2,
+ ev.t1,
+ ev.t2,
+ ev.t3,
+ pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) }
+ )
+ sendResponse(msg)
+
+ case LocalResponse.ShuttleDock(pguid, sguid, slot) =>
+ sendResponse(ObjectAttachMessage(pguid, sguid, slot))
+
+ case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) =>
+ sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient))
+
+ case LocalResponse.ShuttleState(sguid, pos, orient, state) =>
+ sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false))
+
+ case LocalResponse.ToggleTeleportSystem(router, systemPlan) =>
+ sessionLogic.general.toggleTeleportSystem(router, systemPlan)
+
+ case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) =>
+ sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation))
+
+ case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
+ sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
+
+ case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) =>
+ sendResponse(GenericObjectActionMessage(buildingGuid, 11))
+
+ case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) =>
+ sendResponse(GenericObjectActionMessage(buildingGuid, 12))
+
+ case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid =>
+ continent.GUID(vehicleGuid)
+ .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
+ .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
+ .getOrElse(Set.empty)
+ .collect { case weapon: Tool if weapon.GUID == weaponGuid =>
+ sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
+ }
+
+ case _ => ()
+ }
+ }
+
+ /* support functions */
+
+ /**
+ * Common behavior for deconstructing deployables in the game environment.
+ * @param obj the deployable
+ * @param guid the globally unique identifier for the deployable
+ * @param pos the previous position of the deployable
+ * @param orient the previous orientation of the deployable
+ * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
+ */
+ def DeconstructDeployable(
+ obj: Deployable,
+ guid: PlanetSideGUID,
+ pos: Vector3,
+ orient: Vector3,
+ deletionType: Int
+ ): Unit = {
+ sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
+ sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
+ sendResponse(ObjectDeleteMessage(guid, deletionType))
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala
new file mode 100644
index 000000000..f5a6caea8
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala
@@ -0,0 +1,302 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.ActorContext
+import net.psforever.actors.session.SessionActor
+import net.psforever.actors.session.normal.NormalMode
+import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers}
+import net.psforever.actors.zone.ZoneActor
+import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
+import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior}
+import net.psforever.objects.vital.InGameHistory
+import net.psforever.packet.game.{DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
+import net.psforever.services.Service
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.{BailType, PlanetSideGUID, Vector3}
+
+object MountHandlerLogic {
+ def apply(ops: SessionMountHandlers): MountHandlerLogic = {
+ new MountHandlerLogic(ops, ops.context)
+ }
+}
+
+class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ /* packets */
+
+ def handleMountVehicle(pkt: MountVehicleMsg): Unit = { /* intentionally blank */ }
+
+ def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
+ val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt
+ val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
+ //TODO optimize this later
+ //common warning for this section
+ if (player.GUID == player_guid) {
+ //normally disembarking from a mount
+ (sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
+ case out @ Some(obj: Vehicle) =>
+ continent.GUID(obj.MountedIn) match {
+ case Some(_: Vehicle) => None //cargo vehicle
+ case _ => out //arrangement "may" be permissible
+ }
+ case out @ Some(_: Mountable) =>
+ out
+ case _ =>
+ dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
+ None
+ }) match {
+ case Some(obj: Mountable) =>
+ obj.PassengerInSeat(player) match {
+ case Some(seat_num) =>
+ obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
+ //short-circuit the temporary channel for transferring between zones, the player is no longer doing that
+ sessionLogic.zoning.interstellarFerry = None
+ // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
+ //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
+ //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
+ //todo: kick cargo passengers out. To be added after PR #216 is merged
+ obj match {
+ case v: Vehicle
+ if bailType == BailType.Bailed &&
+ v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) &&
+ v.isFlying =>
+ v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
+ case _ => ;
+ }
+
+ case None =>
+ dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
+ }
+ case _ =>
+ dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
+ }
+ } else {
+ //kicking someone else out of a mount; need to own that mount/mountable
+ val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
+ player.avatar.vehicle match {
+ case Some(obj_guid) =>
+ (
+ (
+ sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
+ sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player")
+ ) match {
+ case (vehicle @ Some(obj: Vehicle), tplayer) =>
+ if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
+ case (mount @ Some(_: Mountable), tplayer) =>
+ (mount, tplayer)
+ case _ =>
+ (None, None)
+ }) match {
+ case (Some(obj: Mountable), Some(tplayer: Player)) =>
+ obj.PassengerInSeat(tplayer) match {
+ case Some(seat_num) =>
+ obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
+ case None =>
+ dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
+ }
+ case (None, _) =>
+ dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
+ case (_, None) =>
+ dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
+ case _ =>
+ dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
+ }
+ case None =>
+ dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
+ }
+ }
+ }
+
+ def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { /* intentionally blank */ }
+
+ def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
+ val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt
+ continent.GUID(cargo_guid) match {
+ case Some(cargo: Vehicle) =>
+ cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
+ case _ => ()
+ }
+ }
+
+ /* response handlers */
+
+ /**
+ * na
+ *
+ * @param tplayer na
+ * @param reply na
+ */
+ def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
+ reply match {
+ case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
+ DismountAction(tplayer, obj, seatNum)
+ obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
+
+ case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
+ if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
+ //dismount to hart lobby
+ val pguid = player.GUID
+ val sguid = obj.GUID
+ val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint)
+ tplayer.Position = pos
+ sendResponse(DelayedPathMountMsg(pguid, sguid, u1=60, u2=true))
+ continent.LocalEvents ! LocalServiceMessage(
+ continent.id,
+ LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang))
+ )
+ obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+
+ case Mountable.CanDismount(obj: Vehicle, seatNum, _)
+ if obj.Definition == GlobalDefinitions.orbital_shuttle =>
+ //get ready for orbital drop
+ val pguid = player.GUID
+ val events = continent.VehicleEvents
+ DismountAction(tplayer, obj, seatNum)
+ continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
+ //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
+ events ! VehicleServiceMessage(
+ player.Name,
+ VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message
+ )
+ //when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky
+ //the player will fall to the ground and is perfectly vulnerable in this state
+ //additionally, our player must exist in the current zone
+ //having no in-game avatar target will throw us out of the map screen when deploying and cause softlock
+ events ! VehicleServiceMessage(
+ player.Name,
+ VehicleAction.SendResponse(
+ Service.defaultPlayerGUID,
+ PlayerStateShiftMessage(ShiftState(unk=0, obj.Position, obj.Orientation.z, vel=None)) //cower in the shuttle bay
+ )
+ )
+ events ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player
+ )
+ context.self ! SessionActor.SetMode(NormalMode)
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+
+ case Mountable.CanDismount(obj: Vehicle, seatNum, _)
+ if obj.Definition == GlobalDefinitions.droppod =>
+ sessionLogic.general.unaccessContainer(obj)
+ DismountAction(tplayer, obj, seatNum)
+ obj.Actor ! Vehicle.Deconstruct()
+
+ case Mountable.CanDismount(obj: Vehicle, seatNum, _)
+ if tplayer.GUID == player.GUID =>
+ sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
+ sessionLogic.general.unaccessContainer(obj)
+ DismountVehicleAction(tplayer, obj, seatNum)
+
+ case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
+ )
+
+ case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
+ DismountAction(tplayer, obj, seatNum)
+
+ case Mountable.CanDismount(obj: Mountable, _, _) => ()
+
+ case Mountable.CanNotDismount(obj: Vehicle, seatNum) =>
+ obj.Actor ! Vehicle.Deconstruct()
+
+ case _ => ()
+ }
+ }
+
+ /* support functions */
+
+ private def dismountWarning(
+ bailAs: BailType.Value,
+ kickedByDriver: Boolean
+ )
+ (
+ note: String,
+ player: Player
+ ): Unit = {
+ log.warn(note)
+ player.VehicleSeated = None
+ sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
+ }
+
+ private def dismountError(
+ bailAs: BailType.Value,
+ kickedByDriver: Boolean
+ )
+ (
+ note: String,
+ player: Player
+ ): Unit = {
+ log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
+ player.VehicleSeated = None
+ sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
+ }
+
+ /**
+ * Common activities/procedure when a player dismounts a valid mountable object.
+ * @param tplayer the player
+ * @param obj the mountable object
+ * @param seatNum the mount out of which which the player is disembarking
+ */
+ private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
+ DismountAction(tplayer, obj, seatNum)
+ //until vehicles maintain synchronized momentum without a driver
+ obj match {
+ case v: Vehicle
+ if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
+ sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ =>
+ sessionLogic.vehicles.ServerVehicleOverrideStop(v)
+ }
+ v.Velocity = Vector3.Zero
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.VehicleState(
+ tplayer.GUID,
+ v.GUID,
+ unk1 = 0,
+ v.Position,
+ v.Orientation,
+ vel = None,
+ v.Flying,
+ unk3 = 0,
+ unk4 = 0,
+ wheel_direction = 15,
+ unk5 = false,
+ unk6 = v.Cloaked
+ )
+ )
+ v.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
+ case _ => ()
+ }
+ }
+
+ /**
+ * Common activities/procedure when a player dismounts a valid mountable object.
+ * @param tplayer the player
+ * @param obj the mountable object
+ * @param seatNum the mount out of which which the player is disembarking
+ */
+ private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
+ val playerGuid: PlanetSideGUID = tplayer.GUID
+ tplayer.ContributionFrom(obj)
+ sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
+ val bailType = if (tplayer.BailProtection) {
+ BailType.Bailed
+ } else {
+ BailType.Normal
+ }
+ sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
+ )
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala
new file mode 100644
index 000000000..2f5647711
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala
@@ -0,0 +1,701 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.Actor.Receive
+import akka.actor.ActorRef
+import net.psforever.actors.session.support.{AvatarHandlerFunctions, ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
+import net.psforever.actors.zone.ZoneActor
+import net.psforever.objects.avatar.{BattleRank, CommandRank, DeployableToolbox, FirstTimeEvents, Implant, ProgressDecoration, Shortcut => AvatarShortcut}
+import net.psforever.objects.ce.Deployable
+import net.psforever.objects.serverobject.ServerObject
+import net.psforever.objects.{GlobalDefinitions, Player, Session, SimpleItem, Vehicle}
+import net.psforever.packet.PlanetSidePacket
+import net.psforever.packet.game.{DeployableInfo, DeployableObjectsInfoMessage, DeploymentAction, ObjectCreateDetailedMessage, ObjectDeleteMessage}
+import net.psforever.packet.game.objectcreate.{ObjectClass, ObjectCreateMessageParent, RibbonBars}
+import net.psforever.services.Service
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.services.chat.{ChatService, SpectatorChannel}
+import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage}
+import net.psforever.types.{CapacitorStateType, ChatMessageType, ExoSuitType, MeritCommendation, SquadRequestType}
+//
+import net.psforever.actors.session.{AvatarActor, SessionActor}
+import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData, ZoningOperations}
+import net.psforever.objects.TurretDeployable
+import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
+import net.psforever.objects.serverobject.CommonMessages
+import net.psforever.objects.serverobject.containable.Containable
+import net.psforever.objects.serverobject.deploy.Deployment
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.PlanetSideGamePacket
+import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, KeepAliveMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage}
+import net.psforever.services.{InterstellarClusterService => ICS}
+import net.psforever.services.CavernRotationService
+import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
+import net.psforever.services.ServiceManager.LookupResult
+import net.psforever.services.account.{PlayerToken, ReceiveAccountData}
+import net.psforever.services.avatar.AvatarServiceResponse
+import net.psforever.services.galaxy.GalaxyServiceResponse
+import net.psforever.services.local.LocalServiceResponse
+import net.psforever.services.teamwork.SquadServiceResponse
+import net.psforever.services.vehicle.VehicleServiceResponse
+
+class SpectatorModeLogic(data: SessionData) extends ModeLogic {
+ val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse)
+ val chat: ChatFunctions = ChatLogic(data.chat)
+ val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers)
+ val general: GeneralFunctions = GeneralLogic(data.general)
+ val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
+ val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
+ val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
+ val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
+ val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals)
+ val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
+ val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations)
+
+ override def switchTo(session: Session): Unit = {
+ val player = session.player
+ val continent = session.zone
+ val pguid = player.GUID
+ val sendResponse: PlanetSidePacket=>Unit = data.sendResponse
+ //
+ continent.actor ! ZoneActor.RemoveFromBlockMap(player)
+ continent
+ .GUID(data.terminals.usingMedicalTerminal)
+ .foreach { case term: Terminal with ProximityUnit =>
+ data.terminals.StopUsingProximityUnit(term)
+ }
+ data.general.accessedContainer
+ .collect {
+ case veh: Vehicle if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID =>
+ sendResponse(UnuseItemMessage(pguid, veh.GUID))
+ sendResponse(UnuseItemMessage(pguid, pguid))
+ data.general.unaccessContainer(veh)
+ case container => //just in case
+ if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID) {
+ // Ensure we don't close the container if the player is seated in it
+ // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first.
+ if (container.HasGUID) {
+ sendResponse(UnuseItemMessage(pguid, container.GUID))
+ }
+ sendResponse(UnuseItemMessage(pguid, pguid))
+ data.general.unaccessContainer(container)
+ }
+ }
+ player.CapacitorState = CapacitorStateType.Idle
+ player.Capacitor = 0f
+ player.Inventory.Items
+ .foreach { entry => sendResponse(ObjectDeleteMessage(entry.GUID, 0)) }
+ sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0))
+ continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid))
+ player.Holsters()
+ .collect { case slot if slot.Equipment.nonEmpty => sendResponse(ObjectDeleteMessage(slot.Equipment.get.GUID, 0)) }
+ val vehicleAndSeat = data.vehicles.GetMountableAndSeat(None, player, continent) match {
+ case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 =>
+ data.vehicles.ServerVehicleOverrideStop(obj)
+ obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat
+ obj.Seat(seatNum).foreach(_.unmount(player))
+ player.VehicleSeated = None
+ Some(ObjectCreateMessageParent(obj.GUID, seatNum))
+ case (Some(obj), Some(seatNum)) =>
+ obj.Seat(seatNum).foreach(_.unmount(player))
+ player.VehicleSeated = None
+ Some(ObjectCreateMessageParent(obj.GUID, seatNum))
+ case _ =>
+ None
+ }
+ data.general.dropSpecialSlotItem()
+ data.general.toggleMaxSpecialState(enable = false)
+ data.terminals.CancelAllProximityUnits()
+ data.terminals.lastTerminalOrderFulfillment = true
+ data.squadService ! SquadServiceMessage(
+ player,
+ continent,
+ SquadAction.Membership(SquadRequestType.Leave, player.CharId, Some(player.CharId), player.Name, None)
+ )
+ 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)
+ }
+ val playerFaction = player.Faction
+ continent
+ .DeployableList
+ .filter(_.Faction == playerFaction)
+ .foreach { obj =>
+ sendResponse(DeployableObjectsInfoMessage(
+ DeploymentAction.Dismiss,
+ DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, Service.defaultPlayerGUID)
+ ))
+ }
+ 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)
+ newPlayer.LogActivity(player.History.headOption)
+ val simpleHandHeldThing = GlobalDefinitions.flail_targeting_laser
+ val handheld = new SimpleItem(simpleHandHeldThing)
+ handheld.GUID = player.avatar.locker.GUID
+ sendResponse(ObjectCreateDetailedMessage(
+ 0L,
+ ObjectClass.avatar,
+ pguid,
+ vehicleAndSeat,
+ newPlayer.Definition.Packet.DetailedConstructorData(newPlayer).get
+ ))
+ sendResponse(ObjectCreateDetailedMessage(
+ 0L,
+ simpleHandHeldThing.ObjectId,
+ handheld.GUID,
+ Some(ObjectCreateMessageParent(pguid, 4)),
+ handheld.Definition.Packet.DetailedConstructorData(handheld).get
+ ))
+ data.zoning.spawn.HandleSetCurrentAvatar(newPlayer)
+ sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on"))
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorEnabled"))
+ data.session = session.copy(player = player)
+ }
+
+ override def switchFrom(session: Session): Unit = {
+ 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
+ sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled"))
+ zoning.zoneReload = true
+ zoning.spawn.randomRespawn(0.seconds) //to sanctuary
+ }
+
+ def parse(sender: ActorRef): Receive = {
+ /* really common messages (very frequently, every life) */
+ case packet: PlanetSideGamePacket =>
+ handleGamePkt(packet)
+
+ case AvatarServiceResponse(toChannel, guid, reply) =>
+ avatarResponse.handle(toChannel, guid, reply)
+
+ case GalaxyServiceResponse(_, reply) =>
+ galaxy.handle(reply)
+
+ case LocalServiceResponse(toChannel, guid, reply) =>
+ local.handle(toChannel, guid, reply)
+
+ case Mountable.MountMessages(tplayer, reply) =>
+ mountResponse.handle(tplayer, reply)
+
+ case SquadServiceResponse(_, excluded, response) =>
+ squad.handle(response, excluded)
+
+ case Terminal.TerminalMessage(tplayer, msg, order) =>
+ terminals.handle(tplayer, msg, order)
+
+ case VehicleServiceResponse(toChannel, guid, reply) =>
+ vehicleResponse.handle(toChannel, guid, reply)
+
+ case ChatService.MessageResponse(fromSession, message, _) =>
+ chat.handleIncomingMessage(message, fromSession)
+
+ case SessionActor.SendResponse(packet) =>
+ data.sendResponse(packet)
+
+ case SessionActor.CharSaved => ()
+
+ case SessionActor.CharSavedMsg => ()
+
+ /* common messages (maybe once every respawn) */
+ case ICS.SpawnPointResponse(response) =>
+ data.zoning.handleSpawnPointResponse(response)
+
+ case SessionActor.NewPlayerLoaded(tplayer) =>
+ data.zoning.spawn.handleNewPlayerLoaded(tplayer)
+
+ case SessionActor.PlayerLoaded(tplayer) =>
+ data.zoning.spawn.handlePlayerLoaded(tplayer)
+
+ case Zone.Population.PlayerHasLeft(zone, None) =>
+ data.log.debug(s"PlayerHasLeft: ${data.player.Name} does not have a body on ${zone.id}")
+
+ case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
+ if (tplayer.isAlive) {
+ data.log.info(s"${tplayer.Name} has left zone ${zone.id}")
+ }
+
+ case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
+ data.log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
+
+ case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
+ data.log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
+
+ case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
+ data.log.warn(
+ s"${data.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason"
+ )
+
+ case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
+ data.log.warn(
+ s"${data.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason"
+ )
+
+ case ICS.ZoneResponse(Some(zone)) =>
+ data.zoning.handleZoneResponse(zone)
+
+ /* uncommon messages (once a session) */
+ case ICS.ZonesResponse(zones) =>
+ data.zoning.handleZonesResponse(zones)
+
+ case SessionActor.SetAvatar(avatar) =>
+ general.handleSetAvatar(avatar)
+
+ case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
+ data.zoning.spawn.handleLoginInfoNowhere(name, sender)
+
+ case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
+ data.zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender)
+
+ case PlayerToken.RestoreInfo(playerName, inZone, pos) =>
+ data.zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender)
+
+ case PlayerToken.CanNotLogin(playerName, reason) =>
+ data.zoning.spawn.handleLoginCanNot(playerName, reason)
+
+ case ReceiveAccountData(account) =>
+ general.handleReceiveAccountData(account)
+
+ case AvatarActor.AvatarResponse(avatar) =>
+ general.handleAvatarResponse(avatar)
+
+ case AvatarActor.AvatarLoginResponse(avatar) =>
+ data.zoning.spawn.avatarLoginResponse(avatar)
+
+ case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) =>
+ data.zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt)
+
+ case SessionActor.SetConnectionState(state) =>
+ data.connectionState = state
+
+ case SessionActor.AvatarLoadingSync(state) =>
+ data.zoning.spawn.handleAvatarLoadingSync(state)
+
+ /* uncommon messages (utility, or once in a while) */
+ case ZoningOperations.AvatarAwardMessageBundle(pkts, delay) =>
+ data.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)
+
+ case CommonMessages.ProgressEvent(delta, finishedAction, stepAction, tick) =>
+ general.ops.handleProgressChange(delta, finishedAction, stepAction, tick)
+
+ case CommonMessages.Progress(rate, finishedAction, stepAction) =>
+ general.ops.setupProgressChange(rate, finishedAction, stepAction)
+
+ case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
+ listings.head ! SendCavernRotationUpdates(data.context.self)
+
+ case LookupResult("propertyOverrideManager", endpoint) =>
+ data.zoning.propertyOverrideManagerLoadOverrides(endpoint)
+
+ case SessionActor.UpdateIgnoredPlayers(msg) =>
+ galaxy.handleUpdateIgnoredPlayers(msg)
+
+ case SessionActor.UseCooldownRenewed(definition, _) =>
+ general.handleUseCooldownRenew(definition)
+
+ case Deployment.CanDeploy(obj, state) =>
+ vehicles.handleCanDeploy(obj, state)
+
+ case Deployment.CanUndeploy(obj, state) =>
+ vehicles.handleCanUndeploy(obj, state)
+
+ case Deployment.CanNotChangeDeployment(obj, state, reason) =>
+ vehicles.handleCanNotChangeDeployment(obj, state, reason)
+
+ /* rare messages */
+ case ProximityUnit.StopAction(term, _) =>
+ terminals.ops.LocalStopUsingProximityUnit(term)
+
+ case SessionActor.Suicide() =>
+ general.ops.suicide(data.player)
+
+ case SessionActor.Recall() =>
+ data.zoning.handleRecall()
+
+ case SessionActor.InstantAction() =>
+ data.zoning.handleInstantAction()
+
+ case SessionActor.Quit() =>
+ data.zoning.handleQuit()
+
+ case ICS.DroppodLaunchDenial(errorCode, _) =>
+ data.zoning.handleDroppodLaunchDenial(errorCode)
+
+ case ICS.DroppodLaunchConfirmation(zone, position) =>
+ data.zoning.LoadZoneLaunchDroppod(zone, position)
+
+ case SessionActor.PlayerFailedToLoad(tplayer) =>
+ data.failWithError(s"${tplayer.Name} failed to load anywhere")
+
+ /* csr only */
+ case SessionActor.SetSpeed(speed) =>
+ general.handleSetSpeed(speed)
+
+ case SessionActor.SetFlying(isFlying) =>
+ general.handleSetFlying(isFlying)
+
+ case SessionActor.SetSpectator(isSpectator) =>
+ general.handleSetSpectator(isSpectator)
+
+ case SessionActor.Kick(player, time) =>
+ general.handleKick(player, time)
+
+ case SessionActor.SetZone(zoneId, position) =>
+ data.zoning.handleSetZone(zoneId, position)
+
+ case SessionActor.SetPosition(position) =>
+ data.zoning.spawn.handleSetPosition(position)
+
+ case SessionActor.SetSilenced(silenced) =>
+ general.handleSilenced(silenced)
+
+ /* catch these messages */
+ case _: ProximityUnit.Action => ;
+
+ case _: Zone.Vehicle.HasSpawned => ;
+
+ case _: Zone.Vehicle.HasDespawned => ;
+
+ case Zone.Deployable.IsDismissed(obj: TurretDeployable) => //only if target deployable was never fully introduced
+ TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(data.continent.GUID, obj))
+
+ case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced
+ TaskWorkflow.execute(GUIDTask.unregisterObject(data.continent.GUID, obj))
+
+ case msg: Containable.ItemPutInSlot =>
+ data.log.debug(s"ItemPutInSlot: $msg")
+
+ case msg: Containable.CanNotPutItemInSlot =>
+ data.log.debug(s"CanNotPutItemInSlot: $msg")
+
+ case _ => ()
+ }
+
+ private def handleGamePkt: PlanetSideGamePacket => Unit = {
+ case packet: ConnectToWorldRequestMessage =>
+ general.handleConnectToWorldRequest(packet)
+
+ case packet: MountVehicleCargoMsg =>
+ mountResponse.handleMountVehicleCargo(packet)
+
+ case packet: DismountVehicleCargoMsg =>
+ mountResponse.handleDismountVehicleCargo(packet)
+
+ case packet: CharacterCreateRequestMessage =>
+ general.handleCharacterCreateRequest(packet)
+
+ case packet: CharacterRequestMessage =>
+ general.handleCharacterRequest(packet)
+
+ case _: KeepAliveMessage =>
+ data.keepAliveFunc()
+
+ case packet: BeginZoningMessage =>
+ data.zoning.handleBeginZoning(packet)
+
+ case packet: PlayerStateMessageUpstream =>
+ general.handlePlayerStateUpstream(packet)
+
+ case packet: ChildObjectStateMessage =>
+ vehicles.handleChildObjectState(packet)
+
+ case packet: VehicleStateMessage =>
+ vehicles.handleVehicleState(packet)
+
+ case packet: VehicleSubStateMessage =>
+ vehicles.handleVehicleSubState(packet)
+
+ case packet: FrameVehicleStateMessage =>
+ vehicles.handleFrameVehicleState(packet)
+
+ case packet: ProjectileStateMessage =>
+ shooting.handleProjectileState(packet)
+
+ case packet: LongRangeProjectileInfoMessage =>
+ shooting.handleLongRangeProjectileState(packet)
+
+ case packet: ReleaseAvatarRequestMessage =>
+ data.zoning.spawn.handleReleaseAvatarRequest(packet)
+
+ case packet: SpawnRequestMessage =>
+ data.zoning.spawn.handleSpawnRequest(packet)
+
+ case packet: ChatMsg =>
+ chat.handleChatMsg(packet)
+
+ case packet: SetChatFilterMessage =>
+ chat.handleChatFilter(packet)
+
+ case packet: VoiceHostRequest =>
+ general.handleVoiceHostRequest(packet)
+
+ case packet: VoiceHostInfo =>
+ general.handleVoiceHostInfo(packet)
+
+ case packet: ChangeAmmoMessage =>
+ shooting.handleChangeAmmo(packet)
+
+ case packet: ChangeFireModeMessage =>
+ shooting.handleChangeFireMode(packet)
+
+ case packet: ChangeFireStateMessage_Start =>
+ shooting.handleChangeFireStateStart(packet)
+
+ case packet: ChangeFireStateMessage_Stop =>
+ shooting.handleChangeFireStateStop(packet)
+
+ case packet: EmoteMsg =>
+ general.handleEmote(packet)
+
+ case packet: DropItemMessage =>
+ general.handleDropItem(packet)
+
+ case packet: PickupItemMessage =>
+ general.handlePickupItem(packet)
+
+ case packet: ReloadMessage =>
+ shooting.handleReload(packet)
+
+ case packet: ObjectHeldMessage =>
+ general.handleObjectHeld(packet)
+
+ case packet: AvatarJumpMessage =>
+ general.handleAvatarJump(packet)
+
+ case packet: ZipLineMessage =>
+ general.handleZipLine(packet)
+
+ case packet: RequestDestroyMessage =>
+ general.handleRequestDestroy(packet)
+
+ case packet: MoveItemMessage =>
+ general.handleMoveItem(packet)
+
+ case packet: LootItemMessage =>
+ general.handleLootItem(packet)
+
+ case packet: AvatarImplantMessage =>
+ general.handleAvatarImplant(packet)
+
+ case packet: UseItemMessage =>
+ general.handleUseItem(packet)
+
+ case packet: UnuseItemMessage =>
+ general.handleUnuseItem(packet)
+
+ case packet: ProximityTerminalUseMessage =>
+ terminals.handleProximityTerminalUse(packet)
+
+ case packet: DeployObjectMessage =>
+ general.handleDeployObject(packet)
+
+ case packet: GenericObjectActionMessage =>
+ general.handleGenericObjectAction(packet)
+
+ case packet: GenericObjectActionAtPositionMessage =>
+ general.handleGenericObjectActionAtPosition(packet)
+
+ case packet: GenericObjectStateMsg =>
+ general.handleGenericObjectState(packet)
+
+ case packet: GenericActionMessage =>
+ general.handleGenericAction(packet)
+
+ case packet: ItemTransactionMessage =>
+ terminals.handleItemTransaction(packet)
+
+ case packet: FavoritesRequest =>
+ terminals.handleFavoritesRequest(packet)
+
+ case packet: WeaponDelayFireMessage =>
+ shooting.handleWeaponDelayFire(packet)
+
+ case packet: WeaponDryFireMessage =>
+ shooting.handleWeaponDryFire(packet)
+
+ case packet: WeaponFireMessage =>
+ shooting.handleWeaponFire(packet)
+
+ case packet: WeaponLazeTargetPositionMessage =>
+ shooting.handleWeaponLazeTargetPosition(packet)
+
+ case packet: UplinkRequest =>
+ shooting.handleUplinkRequest(packet)
+
+ case packet: HitMessage =>
+ shooting.handleDirectHit(packet)
+
+ case packet: SplashHitMessage =>
+ shooting.handleSplashHit(packet)
+
+ case packet: LashMessage =>
+ shooting.handleLashHit(packet)
+
+ case packet: AIDamage =>
+ shooting.handleAIDamage(packet)
+
+ case packet: AvatarFirstTimeEventMessage =>
+ general.handleAvatarFirstTimeEvent(packet)
+
+ case packet: WarpgateRequest =>
+ data.zoning.handleWarpgateRequest(packet)
+
+ case packet: MountVehicleMsg =>
+ mountResponse.handleMountVehicle(packet)
+
+ case packet: DismountVehicleMsg =>
+ mountResponse.handleDismountVehicle(packet)
+
+ case packet: DeployRequestMessage =>
+ vehicles.handleDeployRequest(packet)
+
+ case packet: AvatarGrenadeStateMessage =>
+ shooting.handleAvatarGrenadeState(packet)
+
+ case packet: SquadDefinitionActionMessage =>
+ squad.handleSquadDefinitionAction(packet)
+
+ case packet: SquadMembershipRequest =>
+ squad.handleSquadMemberRequest(packet)
+
+ case packet: SquadWaypointRequest =>
+ squad.handleSquadWaypointRequest(packet)
+
+ case packet: GenericCollisionMsg =>
+ general.handleGenericCollision(packet)
+
+ case packet: BugReportMessage =>
+ general.handleBugReport(packet)
+
+ case packet: BindPlayerMessage =>
+ general.handleBindPlayer(packet)
+
+ case packet: PlanetsideAttributeMessage =>
+ general.handlePlanetsideAttribute(packet)
+
+ case packet: FacilityBenefitShieldChargeRequestMessage =>
+ general.handleFacilityBenefitShieldChargeRequest(packet)
+
+ case packet: BattleplanMessage =>
+ general.handleBattleplan(packet)
+
+ case packet: CreateShortcutMessage =>
+ general.handleCreateShortcut(packet)
+
+ case packet: ChangeShortcutBankMessage =>
+ general.handleChangeShortcutBank(packet)
+
+ case packet: FriendsRequest =>
+ general.handleFriendRequest(packet)
+
+ case packet: DroppodLaunchRequestMessage =>
+ data.zoning.handleDroppodLaunchRequest(packet)
+
+ case packet: InvalidTerrainMessage =>
+ general.handleInvalidTerrain(packet)
+
+ case packet: ActionCancelMessage =>
+ general.handleActionCancel(packet)
+
+ case packet: TradeMessage =>
+ general.handleTrade(packet)
+
+ case packet: DisplayedAwardMessage =>
+ general.handleDisplayedAward(packet)
+
+ case packet: ObjectDetectedMessage =>
+ general.handleObjectDetected(packet)
+
+ case packet: TargetingImplantRequest =>
+ general.handleTargetingImplantRequest(packet)
+
+ case packet: HitHint =>
+ general.handleHitHint(packet)
+
+ case _: OutfitRequest => ()
+
+ case pkt =>
+ data.log.warn(s"Unhandled GamePacket $pkt")
+ }
+}
+
+object SpectatorModeLogic {
+ final val SpectatorImplants: Seq[Option[Implant]] = Seq(
+ Some(Implant(GlobalDefinitions.targeting, initialized = true)),
+ Some(Implant(GlobalDefinitions.darklight_vision, initialized = true)),
+ Some(Implant(GlobalDefinitions.range_magnifier, initialized = true))
+ )
+
+ private def spectatorCharacter(player: Player): Player = {
+ val avatar = player.avatar
+ val newAvatar = avatar.copy(
+ basic = avatar.basic.copy(name = "spectator"),
+ bep = BattleRank.BR18.experience,
+ cep = CommandRank.CR5.experience,
+ certifications = Set(),
+ decoration = ProgressDecoration(
+ ribbonBars = RibbonBars(
+ MeritCommendation.BendingMovieActor,
+ MeritCommendation.BendingMovieActor,
+ MeritCommendation.BendingMovieActor,
+ MeritCommendation.BendingMovieActor
+ ),
+ firstTimeEvents = FirstTimeEvents.All
+ ),
+ deployables = {
+ val dt = new DeployableToolbox()
+ dt.Initialize(Set())
+ dt
+ },
+ implants = SpectatorImplants,
+ lookingForSquad = false,
+ shortcuts = {
+ val allShortcuts: Array[Option[AvatarShortcut]] = Array.fill[Option[AvatarShortcut]](64)(None)
+ SpectatorImplants.zipWithIndex.collect { case (Some(implant), slot) =>
+ allShortcuts.update(slot + 1, Some(AvatarShortcut(2, implant.definition.Name)))
+ }
+ allShortcuts
+ }
+ )
+ val newPlayer = Player(newAvatar)
+ newPlayer.GUID = player.GUID
+ newPlayer.ExoSuit = ExoSuitType.Infiltration
+ newPlayer.Position = player.Position
+ newPlayer.Orientation = player.Orientation
+ newPlayer.spectator = true
+ newPlayer.Spawn()
+ newPlayer
+ }
+}
+
+case object SpectatorMode extends PlayerMode {
+ def setup(data: SessionData): ModeLogic = {
+ new SpectatorModeLogic(data)
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala
new file mode 100644
index 000000000..fae3a6817
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala
@@ -0,0 +1,180 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions}
+import net.psforever.objects.{Default, LivePlayerList}
+import net.psforever.objects.avatar.Avatar
+import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEventAction}
+import net.psforever.services.chat.SquadChannel
+import net.psforever.services.teamwork.SquadResponse
+import net.psforever.types.{PlanetSideGUID, SquadListDecoration, SquadResponseType}
+
+object SquadHandlerLogic {
+ def apply(ops: SessionSquadHandlers): SquadHandlerLogic = {
+ new SquadHandlerLogic(ops, ops.context)
+ }
+}
+
+class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /* packet */
+
+ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { /* intentionally blank */ }
+
+ def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { /* intentionally blank */ }
+
+ def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = { /* intentionally blank */ }
+
+ /* response handlers */
+
+ def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
+ if (!excluded.exists(_ == avatar.id)) {
+ response match {
+ case SquadResponse.InitList(infos) =>
+ sendResponse(ReplicationStreamMessage(infos))
+
+ case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
+ sendResponse(
+ ReplicationStreamMessage(
+ 6,
+ None,
+ infos.map {
+ case (index, squadInfo) =>
+ SquadListing(index, squadInfo)
+ }.toVector
+ )
+ )
+
+ case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
+ sendResponse(
+ ReplicationStreamMessage(
+ 1,
+ None,
+ infos.map { index =>
+ SquadListing(index, None)
+ }.toVector
+ )
+ )
+
+ case SquadResponse.SquadDecoration(guid, squad) =>
+ val decoration = if (
+ ops.squadUI.nonEmpty ||
+ squad.Size == squad.Capacity ||
+ {
+ val offer = avatar.certifications
+ !squad.Membership.exists { _.isAvailable(offer) }
+ }
+ ) {
+ SquadListDecoration.NotAvailable
+ } else {
+ SquadListDecoration.Available
+ }
+ sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration)))
+
+ case SquadResponse.Detail(guid, detail) =>
+ sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
+
+ case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) =>
+ val name = request_type match {
+ case SquadResponseType.Invite if unk5 =>
+ //the name of the player indicated by unk3 is needed
+ LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
+ case Some(player) =>
+ player.name
+ case None =>
+ player_name
+ }
+ case _ =>
+ player_name
+ }
+ sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6))
+
+ case SquadResponse.Leave(squad, positionsToUpdate) =>
+ positionsToUpdate.find({ case (member, _) => member == avatar.id }) match {
+ case Some((ourMember, ourIndex)) =>
+ //we are leaving the squad
+ //remove each member's entry (our own too)
+ ops.updateSquadRef = Default.Actor
+ positionsToUpdate.foreach {
+ case (member, index) =>
+ sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
+ ops.squadUI.remove(member)
+ }
+ //uninitialize
+ val playerGuid = player.GUID
+ sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
+ ops.GiveSquadColorsToSelf(value = 0)
+ sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
+ sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
+ avatarActor ! AvatarActor.SetLookingForSquad(false)
+ //a finalization? what does this do?
+ sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+ ops.squad_supplement_id = 0
+ ops.squadUpdateCounter = 0
+ ops.updateSquad = ops.NoSquadUpdates
+ sessionLogic.chat.LeaveChannel(SquadChannel(squad.GUID))
+ case _ =>
+ //remove each member's entry
+ ops.GiveSquadColorsToMembers(
+ positionsToUpdate.map {
+ case (member, index) =>
+ sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
+ ops.squadUI.remove(member)
+ member
+ },
+ value = 0
+ )
+ }
+
+ case SquadResponse.UpdateMembers(_, positions) =>
+ val pairedEntries = positions.collect {
+ case entry if ops.squadUI.contains(entry.char_id) =>
+ (entry, ops.squadUI(entry.char_id))
+ }
+ //prune entries
+ val updatedEntries = pairedEntries
+ .collect({
+ case (entry, element) if entry.zone_number != element.zone =>
+ //zone gets updated for these entries
+ sendResponse(
+ SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number)
+ )
+ ops.squadUI(entry.char_id) =
+ SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
+ entry
+ case (entry, element)
+ if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
+ //other elements that need to be updated
+ ops.squadUI(entry.char_id) =
+ SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
+ entry
+ })
+ .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend
+ if (updatedEntries.nonEmpty) {
+ sendResponse(
+ SquadState(
+ PlanetSideGUID(ops.squad_supplement_id),
+ updatedEntries.map { entry =>
+ SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
+ }
+ )
+ )
+ }
+
+ case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
+ sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
+
+ case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
+ sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type))
+
+ case _ => ()
+ }
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/TerminalHandlerLogic.scala
new file mode 100644
index 000000000..2187ec4ba
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/TerminalHandlerLogic.scala
@@ -0,0 +1,45 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.ActorContext
+import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions}
+import net.psforever.login.WorldSession.SellEquipmentFromInventory
+import net.psforever.objects.Player
+import net.psforever.objects.serverobject.terminals.Terminal
+import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage}
+
+object TerminalHandlerLogic {
+ def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = {
+ new TerminalHandlerLogic(ops, ops.context)
+ }
+}
+
+class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val context: ActorContext) extends TerminalHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { /* intentionally blank */ }
+
+ def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { /* intentionally blank */ }
+
+ def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { /* intentionally blank */ }
+
+ /**
+ * na
+ * @param tplayer na
+ * @param msg na
+ * @param order na
+ */
+ def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
+ order match {
+ case Terminal.SellEquipment() =>
+ SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
+
+ case _ if msg != null =>
+ sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, success = false))
+ ops.lastTerminalOrderFulfillment = true
+
+ case _ =>
+ ops.lastTerminalOrderFulfillment = true
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala
new file mode 100644
index 000000000..ee66f69e9
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala
@@ -0,0 +1,399 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions}
+import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
+import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
+import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.pad.VehicleSpawnPad
+import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
+import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage}
+import net.psforever.services.Service
+import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
+import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
+
+object VehicleHandlerLogic {
+ def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = {
+ new VehicleHandlerLogic(ops, ops.context)
+ }
+}
+
+class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ private val galaxyService: ActorRef = ops.galaxyService
+
+ /**
+ * na
+ *
+ * @param toChannel na
+ * @param guid na
+ * @param reply na
+ */
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = {
+ val resolvedPlayerGuid = if (player.HasGUID) {
+ player.GUID
+ } else {
+ PlanetSideGUID(-1)
+ }
+ val isNotSameTarget = resolvedPlayerGuid != guid
+ reply match {
+ case VehicleResponse.VehicleState(
+ vehicleGuid,
+ unk1,
+ pos,
+ orient,
+ vel,
+ unk2,
+ unk3,
+ unk4,
+ wheelDirection,
+ unk5,
+ unk6
+ ) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) =>
+ //player who is also in the vehicle (not driver)
+ sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
+ player.Position = pos
+ player.Orientation = orient
+ player.Velocity = vel
+ sessionLogic.updateLocalBlockMap(pos)
+
+ case VehicleResponse.VehicleState(
+ vehicleGuid,
+ unk1,
+ pos,
+ ang,
+ vel,
+ unk2,
+ unk3,
+ unk4,
+ wheelDirection,
+ unk5,
+ unk6
+ ) if isNotSameTarget =>
+ //player who is watching the vehicle from the outside
+ sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, ang, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
+
+ case VehicleResponse.ChildObjectState(objectGuid, pitch, yaw) if isNotSameTarget =>
+ sendResponse(ChildObjectStateMessage(objectGuid, pitch, yaw))
+
+ case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)
+ if isNotSameTarget =>
+ sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA))
+
+ case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Start(weaponGuid))
+
+ case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget =>
+ sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+
+ case VehicleResponse.Reload(itemGuid) if isNotSameTarget =>
+ sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
+
+ case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget =>
+ sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
+ //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0))
+ sendResponse(
+ ObjectCreateMessage(
+ ammo_id,
+ ammo_guid,
+ ObjectCreateMessageParent(weapon_guid, weapon_slot),
+ ammo_data
+ )
+ )
+ sendResponse(ChangeAmmoMessage(weapon_guid, 1))
+
+ case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget =>
+ continent.GUID(weaponGuid).collect {
+ case tool: Tool if tool.Magazine == 0 =>
+ // check that the magazine is still empty before sending WeaponDryFireMessage
+ // if it has been reloaded since then, other clients will not see it firing
+ sendResponse(WeaponDryFireMessage(weaponGuid))
+ }
+
+ case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget =>
+ sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver))
+
+ case VehicleResponse.MountVehicle(vehicleGuid, seat) if isNotSameTarget =>
+ sendResponse(ObjectAttachMessage(vehicleGuid, guid, seat))
+
+ case VehicleResponse.DeployRequest(objectGuid, state, unk1, unk2, pos) if isNotSameTarget =>
+ sendResponse(DeployRequestMessage(guid, objectGuid, state, unk1, unk2, pos))
+
+ case VehicleResponse.SendResponse(msg) =>
+ sendResponse(msg)
+
+ case VehicleResponse.AttachToRails(vehicleGuid, padGuid) =>
+ sendResponse(ObjectAttachMessage(padGuid, vehicleGuid, slot=3))
+
+ case VehicleResponse.ConcealPlayer(playerGuid) =>
+ sendResponse(GenericObjectActionMessage(playerGuid, code=9))
+
+ case VehicleResponse.DetachFromRails(vehicleGuid, padGuid, padPosition, padOrientationZ) =>
+ val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad].Definition
+ sendResponse(
+ ObjectDetachMessage(
+ padGuid,
+ vehicleGuid,
+ padPosition + Vector3.z(pad.VehicleCreationZOffset),
+ padOrientationZ + pad.VehicleCreationZOrientOffset
+ )
+ )
+
+ case VehicleResponse.EquipmentInSlot(pkt) if isNotSameTarget =>
+ sendResponse(pkt)
+
+ case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget =>
+ sendResponse(GenericObjectActionMessage(objectGuid, action))
+
+ case VehicleResponse.HitHint(sourceGuid) if player.isAlive =>
+ sendResponse(HitHint(sourceGuid, player.GUID))
+
+ case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget =>
+ //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
+ val objGuid = obj.GUID
+ sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
+ sendResponse(ObjectCreateDetailedMessage(
+ obj.Definition.ObjectId,
+ objGuid,
+ ObjectCreateMessageParent(parentGuid, start),
+ conData
+ ))
+
+ case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicleGuid) if resolvedPlayerGuid == guid =>
+ //seat number (first field) seems to be correct if passenger is kicked manually by driver
+ //but always seems to return 4 if user is kicked by mount permissions changing
+ sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
+ val typeOfRide = continent.GUID(vehicleGuid) match {
+ case Some(obj: Vehicle) =>
+ sessionLogic.general.unaccessContainer(obj)
+ s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}"
+ case _ =>
+ s"${player.Sex.possessive} ride"
+ }
+ log.info(s"${player.Name} has been kicked from $typeOfRide!")
+
+ case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) =>
+ //seat number (first field) seems to be correct if passenger is kicked manually by driver
+ //but always seems to return 4 if user is kicked by mount permissions changing
+ sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
+
+ case VehicleResponse.InventoryState2(objGuid, parentGuid, value) if isNotSameTarget =>
+ sendResponse(InventoryStateMessage(objGuid, unk=0, parentGuid, value))
+
+ case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) if isNotSameTarget =>
+ //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible)
+ sendResponse(ObjectCreateMessage(vtype, vguid, vdata))
+ Vehicles.ReloadAccessPermissions(vehicle, player.Name)
+
+ case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget =>
+ sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
+
+ case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid =>
+ //Only the player that owns this vehicle needs the ownership packet
+ avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))
+ sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid))
+
+ case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue))
+
+ case VehicleResponse.ResetSpawnPad(padGuid) =>
+ sendResponse(GenericObjectActionMessage(padGuid, code=23))
+
+ case VehicleResponse.RevealPlayer(playerGuid) =>
+ sendResponse(GenericObjectActionMessage(playerGuid, code=10))
+
+ case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget =>
+ sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission))
+
+ case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget =>
+ //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
+ sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData))
+
+ case VehicleResponse.UnloadVehicle(_, vehicleGuid) =>
+ sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0))
+
+ case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget =>
+ //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
+ sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
+
+ case VehicleResponse.UpdateAmsSpawnPoint(list) =>
+ sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
+ sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint()
+
+ case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget =>
+ sessionLogic.zoning.interstellarFerry = Some(vehicle)
+ sessionLogic.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete)
+ continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
+ galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel
+ log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating")
+
+ case VehicleResponse.KickCargo(vehicle, speed, delay)
+ if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 =>
+ val strafe = 1 + Vehicles.CargoOrientation(vehicle)
+ val reverseSpeed = if (strafe > 1) { 0 } else { speed }
+ //strafe or reverse, not both
+ sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
+ vehicle,
+ ServerVehicleOverrideMsg(
+ lock_accelerator=true,
+ lock_wheel=true,
+ reverse=true,
+ unk4=false,
+ lock_vthrust=0,
+ strafe,
+ reverseSpeed,
+ unk8=Some(0)
+ )
+ )
+ import scala.concurrent.ExecutionContext.Implicits.global
+ import scala.concurrent.duration._
+ context.system.scheduler.scheduleOnce(
+ delay milliseconds,
+ context.self,
+ VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay))
+ )
+
+ case VehicleResponse.KickCargo(cargo, _, _)
+ if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive =>
+ sessionLogic.vehicles.TotalDriverVehicleControl(cargo)
+
+ case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _)
+ if player.VisibleSlots.contains(player.DrawnSlot) =>
+ player.DrawnSlot = Player.HandsDownSlot
+ startPlayerSeatedInVehicle(vehicle)
+
+ case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
+ startPlayerSeatedInVehicle(vehicle)
+
+ case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
+ Vehicles.ReloadAccessPermissions(vehicle, player.Name)
+ sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
+ vehicle,
+ ServerVehicleOverrideMsg(
+ lock_accelerator=true,
+ lock_wheel=true,
+ reverse=true,
+ unk4=false,
+ lock_vthrust=1,
+ lock_strafe=0,
+ movement_speed=0,
+ unk8=Some(0)
+ )
+ )
+ sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
+
+ case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
+ val vdef = vehicle.Definition
+ sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
+ vehicle,
+ ServerVehicleOverrideMsg(
+ lock_accelerator=true,
+ lock_wheel=true,
+ reverse=false,
+ unk4=false,
+ lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 },
+ lock_strafe=0,
+ movement_speed=vdef.AutoPilotSpeed1,
+ unk8=Some(0)
+ )
+ )
+
+ case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
+ sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle)
+
+ case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
+ sendResponse(ChatMsg(
+ ChatMessageType.CMT_OPEN,
+ wideContents=true,
+ recipient="",
+ s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
+ note=None
+ ))
+
+ case VehicleResponse.PeriodicReminder(_, data) =>
+ val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match {
+ case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg)
+ case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg)
+ case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.")
+ }
+ sendResponse(ChatMsg(isType, flag, recipient="", msg, None))
+
+ case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory)
+ if player.avatar.vehicle.contains(target) =>
+ //TODO when vehicle weapons can be changed without visual glitches, rewrite this
+ continent.GUID(target).collect { case vehicle: Vehicle =>
+ import net.psforever.login.WorldSession.boolToInt
+ //owner: must unregister old equipment, and register and install new equipment
+ (oldWeapons ++ oldInventory).foreach {
+ case (obj, eguid) =>
+ sendResponse(ObjectDeleteMessage(eguid, unk1=0))
+ TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
+ }
+ sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory)
+ //jammer or unjamm new weapons based on vehicle status
+ val vehicleJammered = vehicle.Jammed
+ addedWeapons
+ .map { _.obj }
+ .collect {
+ case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered =>
+ jamItem.Jammed = vehicleJammered
+ JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered)
+ }
+ changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
+ }
+
+ case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _)
+ if sessionLogic.general.accessedContainer.map(_.GUID).contains(target) =>
+ //TODO when vehicle weapons can be changed without visual glitches, rewrite this
+ continent.GUID(target).collect { case vehicle: Vehicle =>
+ //external participant: observe changes to equipment
+ (oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
+ changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
+ }
+
+ case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) =>
+ //TODO when vehicle weapons can be changed without visual glitches, rewrite this
+ continent.GUID(target).collect { case vehicle: Vehicle =>
+ changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
+ }
+
+ case _ => ()
+ }
+ }
+
+ private def changeLoadoutDeleteOldEquipment(
+ vehicle: Vehicle,
+ oldWeapons: Iterable[(Equipment, PlanetSideGUID)],
+ oldInventory: Iterable[(Equipment, PlanetSideGUID)]
+ ): Unit = {
+ vehicle.PassengerInSeat(player) match {
+ case Some(seatNum) =>
+ //participant: observe changes to equipment
+ (oldWeapons ++ oldInventory).foreach {
+ case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0))
+ }
+ sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seatNum)
+ case None =>
+ //observer: observe changes to external equipment
+ oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
+ }
+ }
+
+ private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = {
+ val vehicle_guid = vehicle.GUID
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
+ sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off
+ sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership
+ vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect {
+ case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala
new file mode 100644
index 000000000..355c1fdc5
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala
@@ -0,0 +1,363 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.AvatarActor
+import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles}
+import net.psforever.objects.serverobject.deploy.Deployment
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.vehicles.control.BfrFlight
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
+import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.{DriveState, Vector3}
+
+object VehicleLogic {
+ def apply(ops: VehicleOperations): VehicleLogic = {
+ new VehicleLogic(ops, ops.context)
+ }
+}
+
+class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /* packets */
+
+ def handleVehicleState(pkt: VehicleStateMessage): Unit = {
+ val VehicleStateMessage(
+ vehicle_guid,
+ unk1,
+ pos,
+ ang,
+ vel,
+ is_flying,
+ unk6,
+ unk7,
+ wheels,
+ is_decelerating,
+ is_cloaked
+ ) = pkt
+ GetVehicleAndSeat() match {
+ case (Some(obj), Some(0)) =>
+ //we're driving the vehicle
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ sessionLogic.general.fallHeightTracker(pos.z)
+ if (obj.MountedIn.isEmpty) {
+ sessionLogic.updateBlockMap(obj, pos)
+ }
+ player.Position = pos //convenient
+ if (obj.WeaponControlledFromSeat(0).isEmpty) {
+ player.Orientation = Vector3.z(ang.z) //convenient
+ }
+ obj.Position = pos
+ obj.Orientation = ang
+ if (obj.MountedIn.isEmpty) {
+ if (obj.DeploymentState != DriveState.Deployed) {
+ obj.Velocity = vel
+ } else {
+ obj.Velocity = Some(Vector3.Zero)
+ }
+ if (obj.Definition.CanFly) {
+ obj.Flying = is_flying //usually Some(7)
+ }
+ obj.Cloaked = obj.Definition.CanCloak && is_cloaked
+ } else {
+ obj.Velocity = None
+ obj.Flying = None
+ }
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.VehicleState(
+ player.GUID,
+ vehicle_guid,
+ unk1,
+ obj.Position,
+ ang,
+ obj.Velocity,
+ if (obj.isFlying) {
+ is_flying
+ } else {
+ None
+ },
+ unk6,
+ unk7,
+ wheels,
+ is_decelerating,
+ obj.Cloaked
+ )
+ )
+ sessionLogic.squad.updateSquad()
+ obj.zoneInteractions()
+ case (None, _) =>
+ //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
+ //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
+ case (_, Some(index)) =>
+ log.error(
+ s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
+ )
+ case _ => ;
+ }
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
+ val FrameVehicleStateMessage(
+ vehicle_guid,
+ unk1,
+ pos,
+ ang,
+ vel,
+ unk2,
+ unk3,
+ unk4,
+ is_crouched,
+ is_airborne,
+ ascending_flight,
+ flight_time,
+ unk9,
+ unkA
+ ) = pkt
+ GetVehicleAndSeat() match {
+ case (Some(obj), Some(0)) =>
+ //we're driving the vehicle
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
+ case Some(v: Vehicle) =>
+ sessionLogic.updateBlockMap(obj, pos)
+ (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
+ case _ =>
+ (pos, ang, vel, true)
+ }
+ player.Position = position //convenient
+ if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
+ player.Orientation = Vector3.z(ang.z) //convenient
+ }
+ obj.Position = position
+ obj.Orientation = angle
+ obj.Velocity = velocity
+ // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
+ // //dev stuff goes here
+ // }
+ // else
+ // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
+ // //dev stuff goes here
+ // }
+ obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
+ if (notMountedState) {
+ if (obj.DeploymentState != DriveState.Kneeling) {
+ if (is_airborne) {
+ val flight = if (ascending_flight) flight_time else -flight_time
+ obj.Flying = Some(flight)
+ obj.Actor ! BfrFlight.Soaring(flight)
+ } else if (obj.Flying.nonEmpty) {
+ obj.Flying = None
+ obj.Actor ! BfrFlight.Landed
+ }
+ } else {
+ obj.Velocity = None
+ obj.Flying = None
+ }
+ obj.zoneInteractions()
+ } else {
+ obj.Velocity = None
+ obj.Flying = None
+ }
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.FrameVehicleState(
+ player.GUID,
+ vehicle_guid,
+ unk1,
+ position,
+ angle,
+ velocity,
+ unk2,
+ unk3,
+ unk4,
+ is_crouched,
+ is_airborne,
+ ascending_flight,
+ flight_time,
+ unk9,
+ unkA
+ )
+ )
+ sessionLogic.squad.updateSquad()
+ case (None, _) =>
+ //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
+ //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
+ case (_, Some(index)) =>
+ log.error(
+ s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
+ )
+ case _ => ()
+ }
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
+ val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
+ val (o, tools) = sessionLogic.shooting.FindContainedWeapon
+ //is COSM our primary upstream packet?
+ (o match {
+ case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
+ case _ => (None, None)
+ }) match {
+ case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ()
+ case _ =>
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ }
+ //the majority of the following check retrieves information to determine if we are in control of the child
+ tools.find { _.GUID == object_guid } match {
+ case None => ()
+ case Some(_) => player.Orientation = Vector3(0f, pitch, yaw)
+ }
+ if (player.death_by == -1) {
+ sessionLogic.kickedByAdministration()
+ }
+ }
+
+ def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
+ val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
+ sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState") match {
+ case Some(obj: Vehicle) =>
+ import net.psforever.login.WorldSession.boolToInt
+ obj.Position = pos
+ obj.Orientation = ang
+ obj.Velocity = vel
+ sessionLogic.updateBlockMap(obj, pos)
+ obj.zoneInteractions()
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.VehicleState(
+ player.GUID,
+ vehicle_guid,
+ unk1,
+ pos,
+ ang,
+ obj.Velocity,
+ obj.Flying,
+ 0,
+ 0,
+ 15,
+ unk5 = false,
+ obj.Cloaked
+ )
+ )
+ case _ => ()
+ }
+ }
+
+ def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
+ val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
+ val vehicle = player.avatar.vehicle
+ if (vehicle.contains(vehicle_guid)) {
+ if (vehicle == player.VehicleSeated) {
+ continent.GUID(vehicle_guid) match {
+ case Some(obj: Vehicle) =>
+ if (obj.DeploymentState == DriveState.Deployed) {
+ obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
+ }
+ case _ => ()
+ avatarActor ! AvatarActor.SetVehicle(None)
+ }
+ }
+ }
+ }
+
+ /* messages */
+
+ def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { /* intentionally blank */ }
+
+ def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
+ if (state != DriveState.Undeploying && state != DriveState.Mobile) {
+ CanNotChangeDeployment(obj, state, "incorrect undeploy state")
+ }
+ }
+
+ def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = {
+ if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) {
+ CanNotChangeDeployment(obj, state, reason = "ground too steep")
+ } else {
+ CanNotChangeDeployment(obj, state, reason)
+ }
+ }
+
+ /* support functions */
+
+ /**
+ * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat.
+ * The priority of object confirmation is `direct` then `occupant.VehicleSeated`.
+ * Once an object is found, the remainder are ignored.
+ * @param direct a game object in which the player may be sat
+ * @param occupant the player who is sat and may have specified the game object in which mounted
+ * @return a tuple consisting of a vehicle reference and a mount index
+ * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
+ * `(None, None)`, otherwise (even if the vehicle can be determined)
+ */
+ private def GetMountableAndSeat(
+ direct: Option[PlanetSideGameObject with Mountable],
+ occupant: Player,
+ zone: Zone
+ ): (Option[PlanetSideGameObject with Mountable], Option[Int]) =
+ direct.orElse(zone.GUID(occupant.VehicleSeated)) match {
+ case Some(obj: PlanetSideGameObject with Mountable) =>
+ obj.PassengerInSeat(occupant) match {
+ case index @ Some(_) =>
+ (Some(obj), index)
+ case None =>
+ (None, None)
+ }
+ case _ =>
+ (None, None)
+ }
+
+ /**
+ * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
+ * @see `GetMountableAndSeat`
+ * @return a tuple consisting of a vehicle reference and a mount index
+ * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
+ * `(None, None)`, otherwise (even if the vehicle can be determined)
+ */
+ private def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
+ GetMountableAndSeat(None, player, continent) match {
+ case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
+ case _ => (None, None)
+ }
+
+ /**
+ * Common reporting behavior when a `Deployment` object fails to properly transition between states.
+ * @param obj the game object that could not
+ * @param state the `DriveState` that could not be promoted
+ * @param reason a string explaining why the state can not or will not change
+ */
+ private def CanNotChangeDeployment(
+ obj: PlanetSideServerObject with Deployment,
+ state: DriveState.Value,
+ reason: String
+ ): Unit = {
+ val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) {
+ obj.DeploymentState = DriveState.Mobile
+ sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero))
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero)
+ )
+ "; enforcing Mobile deployment state"
+ } else {
+ ""
+ }
+ log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift")
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala
new file mode 100644
index 000000000..7f63dc73d
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala
@@ -0,0 +1,681 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.{ActorContext, 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.ballistics.{Projectile, ProjectileQuality}
+import net.psforever.objects.definition.ProjectileDefinition
+import net.psforever.objects.equipment.{ChargeFireModeDefinition, EquipmentSize}
+import net.psforever.objects.inventory.Container
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
+import net.psforever.objects.serverobject.doors.InteriorDoorPassage
+import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, SpecialEmp, Tool}
+import net.psforever.objects.serverobject.interior.Sidedness
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
+import net.psforever.objects.sourcing.SourceEntry
+import net.psforever.objects.vital.Vitality
+import net.psforever.objects.vital.base.{DamageResolution, DamageType}
+import net.psforever.objects.vital.etc.OicwLilBuddyReason
+import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.vital.projectile.ProjectileReason
+import net.psforever.objects.zones.{Zone, ZoneProjectile}
+import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, 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, Vector3}
+import net.psforever.util.Config
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+
+object WeaponAndProjectileLogic {
+ def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
+ new WeaponAndProjectileLogic(ops, ops.context)
+ }
+
+ /**
+ * Does a line segment line intersect with a sphere?
+ * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
+ * @param start first point of the line segment
+ * @param end second point of the line segment
+ * @param center center of the sphere
+ * @param radius radius of the sphere
+ * @return list of all points of intersection, if any
+ * @see `Vector3.DistanceSquared`
+ * @see `Vector3.MagnitudeSquared`
+ */
+ private def quickLineSphereIntersectionPoints(
+ start: Vector3,
+ end: Vector3,
+ center: Vector3,
+ radius: Float
+ ): Iterable[Vector3] = {
+ /*
+ Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
+ because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
+ */
+ val Vector3(cx, cy, cz) = center
+ val Vector3(sx, sy, sz) = start
+ val vector = end - start
+ //speed our way through a quadratic equation
+ val (a, b) = {
+ val Vector3(dx, dy, dz) = vector
+ (
+ dx * dx + dy * dy + dz * dz,
+ 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
+ )
+ }
+ val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
+ val result = b * b - 4 * a * c
+ if (result < 0f) {
+ //negative, no intersection
+ Seq()
+ } else if (result < 0.00001f) {
+ //zero-ish, one intersection point
+ Seq(start - vector * (b / (2f * a)))
+ } else {
+ //positive, two intersection points
+ val sqrt = math.sqrt(result).toFloat
+ val endStart = vector / (2f * a)
+ Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
+ }.filter(p => Vector3.DistanceSquared(start, p) <= a)
+ }
+ /**
+ * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
+ * The main difference from "normal" server-side explosion
+ * is that the owner of the projectile must be clarified explicitly.
+ * @see `Zone::serverSideDamage`
+ * @param zone where the explosion is taking place
+ * (`source` contains the coordinate location)
+ * @param source a game object that represents the source of the explosion
+ * @param owner who or what to accredit damage from the explosion to;
+ * clarifies a normal `SourceEntry(source)` accreditation
+ */
+ private def detonateLittleBuddy(
+ zone: Zone,
+ source: PlanetSideGameObject with FactionAffinity with Vitality,
+ proxy: Projectile,
+ owner: SourceEntry
+ )(): Unit = {
+ Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
+ }
+
+ /**
+ * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
+ * The main difference from "normal" server-side explosion
+ * is that the owner of the projectile must be clarified explicitly.
+ * The sub-projectiles will be the product of a normal projectile rather than a standard game object
+ * so a custom `source` entity must wrap around it and fulfill the requirements of the field.
+ * @see `Zone::explosionDamage`
+ * @param owner who or what to accredit damage from the explosion to
+ * @param explosionPosition where the explosion will be positioned in the game world
+ * @param source a game object that represents the source of the explosion
+ * @param target a game object that is affected by the explosion
+ * @return a `DamageInteraction` object
+ */
+ private def littleBuddyExplosionDamage(
+ owner: SourceEntry,
+ projectileId: Long,
+ explosionPosition: Vector3
+ )
+ (
+ source: PlanetSideGameObject with FactionAffinity with Vitality,
+ target: PlanetSideGameObject with FactionAffinity with Vitality
+ ): DamageInteraction = {
+ DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
+ }
+}
+
+class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
+
+ /* packets */
+
+ def handleWeaponFire(pkt: WeaponFireMessage): Unit = { /* intentionally blank */ }
+
+ def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { /* intentionally blank */ }
+
+ def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { /* intentionally blank */ }
+
+ def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* intentionally blank */ }
+
+ def handleUplinkRequest(packet: UplinkRequest): Unit = {
+ val UplinkRequest(code, _, _) = packet
+ val playerFaction = player.Faction
+ //todo this is not correct
+ code match {
+ case UplinkRequestType.RevealFriendlies =>
+ sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction == playerFaction)))
+ case UplinkRequestType.RevealEnemies =>
+ sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction != playerFaction)))
+ case _ => ()
+ }
+ }
+
+ def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* intentionally blank */ }
+
+ def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = { /* intentionally blank */ }
+
+ def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = {
+ val ChangeFireStateMessage_Stop(item_guid) = pkt
+ val now = System.currentTimeMillis()
+ ops.prefire -= item_guid
+ ops.shootingStop += item_guid -> now
+ ops.shooting -= item_guid
+ sessionLogic.findEquipment(item_guid) match {
+ case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
+ fireStateStopWhenPlayer(tool, item_guid)
+ case Some(tool: Tool) =>
+ fireStateStopWhenMounted(tool, item_guid)
+ case Some(trigger: BoomerTrigger) =>
+ ops.fireStateStopPlayerMessages(item_guid)
+ continent.GUID(trigger.Companion).collect {
+ case boomer: BoomerDeployable =>
+ boomer.Actor ! CommonMessages.Use(player, Some(trigger))
+ }
+ case Some(_) if player.VehicleSeated.isEmpty =>
+ ops.fireStateStopPlayerMessages(item_guid)
+ case Some(_) =>
+ ops.fireStateStopMountedMessages(item_guid)
+ case _ => ()
+ }
+ sessionLogic.general.progressBarUpdate.cancel()
+ sessionLogic.general.progressBarValue = None
+ }
+
+ def handleReload(pkt: ReloadMessage): Unit = { /* intentionally blank */ }
+
+ def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { /* intentionally blank */ }
+
+ def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { /* intentionally blank */ }
+
+ def handleProjectileState(pkt: ProjectileStateMessage): Unit = {
+ val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt
+ val index = projectile_guid.guid - Projectile.baseUID
+ ops.projectiles(index) match {
+ case Some(projectile) if projectile.HasGUID =>
+ val projectileGlobalUID = projectile.GUID
+ projectile.Position = shot_pos
+ projectile.Orientation = shot_orient
+ projectile.Velocity = shot_vel
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.ProjectileState(
+ player.GUID,
+ projectileGlobalUID,
+ shot_pos,
+ shot_vel,
+ shot_orient,
+ seq,
+ end,
+ target_guid
+ )
+ )
+ case _ if seq == 0 =>
+ /* missing the first packet in the sequence is permissible */
+ case _ =>
+ log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
+ }
+ }
+
+ def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { /* intentionally blank */ }
+
+ def handleDirectHit(pkt: HitMessage): Unit = { /* intentionally blank */ }
+
+ def handleSplashHit(pkt: SplashHitMessage): Unit = {
+ val SplashHitMessage(
+ _,
+ projectile_guid,
+ explosion_pos,
+ direct_victim_uid,
+ _,
+ projectile_vel,
+ _,
+ targets
+ ) = pkt
+ ops.FindProjectileEntry(projectile_guid) match {
+ case Some(projectile) =>
+ val profile = projectile.profile
+ projectile.Velocity = projectile_vel
+ val (resolution1, resolution2) = profile.Aggravated match {
+ case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
+ (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
+ case _ =>
+ (DamageResolution.Splash, DamageResolution.Splash)
+ }
+ //direct_victim_uid
+ sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
+ case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
+ CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
+ ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ case _ => ()
+ }
+ //other victims
+ targets.foreach(elem => {
+ sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
+ case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
+ CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
+ ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ case _ => ()
+ }
+ })
+ //...
+ HandleDamageProxy(projectile, projectile_guid, explosion_pos)
+ if (
+ projectile.profile.HasJammedEffectDuration ||
+ projectile.profile.JammerProjectile ||
+ projectile.profile.SympatheticExplosion
+ ) {
+ //can also substitute 'projectile.profile' for 'SpecialEmp.emp'
+ Zone.serverSideDamage(
+ continent,
+ player,
+ SpecialEmp.emp,
+ SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
+ SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
+ SpecialEmp.findAllBoomers(profile.DamageRadius)
+ )
+ }
+ if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
+ //cleanup
+ if (projectile.HasGUID) {
+ continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
+ }
+ }
+ case None => ()
+ }
+ }
+
+ def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ }
+
+ def handleAIDamage(pkt: AIDamage): Unit = {
+ val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
+ (continent.GUID(player.VehicleSeated) match {
+ case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
+ if tobj.GUID == targetGuid &&
+ tobj.OwnerGuid.contains(player.GUID) =>
+ //deployable turrets
+ Some(tobj)
+ case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
+ if tobj.GUID == targetGuid &&
+ tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
+ //facility turrets, etc.
+ Some(tobj)
+ case _
+ if player.GUID == targetGuid =>
+ //player avatars
+ Some(player)
+ case _ =>
+ None
+ }).collect {
+ case target: AutomatedTurret.Target =>
+ sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
+ .collect {
+ case turret: AutomatedTurret if turret.Target.isEmpty =>
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ Some(target)
+
+ case turret: AutomatedTurret =>
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
+ Some(target)
+ }
+ }
+ .orElse {
+ //occasionally, something that is not technically a turret's natural target may be attacked
+ sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target")
+ .collect {
+ case target: PlanetSideServerObject with FactionAffinity with Vitality =>
+ sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker")
+ .collect {
+ case turret: AutomatedTurret if turret.Target.nonEmpty =>
+ //the turret must be shooting at something (else) first
+ HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
+ }
+ Some(target)
+ }
+ }
+ }
+
+ /* 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 {
+ ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue)
+ 3
+ }
+ log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw")
+ 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)
+ }
+ }
+
+ /**
+ * Given an object that contains a box of amunition in its `Inventory` at a certain location,
+ * change the amount of ammunition within that box.
+ * @param obj the `Container`
+ * @param box an `AmmoBox` to modify
+ * @param reloadValue the value to modify the `AmmoBox`;
+ * subtracted from the current `Capacity` of `Box`
+ */
+ private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
+ val capacity = box.Capacity - reloadValue
+ box.Capacity = capacity
+ sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity))
+ }
+
+ private def CheckForHitPositionDiscrepancy(
+ projectile_guid: PlanetSideGUID,
+ hitPos: Vector3,
+ target: PlanetSideGameObject with FactionAffinity with Vitality
+ ): Unit = {
+ val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position)
+ if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) {
+ // If the target position on the server does not match the position where the projectile landed within reason there may be foul play
+ log.warn(
+ s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect"
+ )
+ }
+ }
+
+ /**
+ * na
+ * @param projectile the projectile object
+ * @param resolution the resolution status to promote the projectile
+ * @return a copy of the projectile
+ */
+ private def ResolveProjectileInteraction(
+ projectile: Projectile,
+ resolution: DamageResolution.Value,
+ target: PlanetSideGameObject with FactionAffinity with Vitality,
+ pos: Vector3
+ ): Option[DamageInteraction] = {
+ if (projectile.isMiss) {
+ log.warn("expected projectile was already counted as a missed shot; can not resolve any further")
+ None
+ } else {
+ val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player))
+ if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) {
+ avatarActor ! AvatarActor.ConsumeStamina(10)
+ }
+ Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos))
+ }
+ }
+
+ /**
+ * Take a projectile that was introduced into the game world and
+ * determine if it generates a secondary damage projectile or
+ * an method of damage causation that requires additional management.
+ * @param projectile the projectile
+ * @param pguid the client-local projectile identifier
+ * @param hitPos the game world position where the projectile is being recorded
+ * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location;
+ * nothing if no targets were affected
+ */
+ private def HandleDamageProxy(
+ projectile: Projectile,
+ pguid: PlanetSideGUID,
+ hitPos: Vector3
+ ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = {
+ GlobalDefinitions.getDamageProxy(projectile, hitPos) match {
+ case Nil =>
+ Nil
+ case list if list.isEmpty =>
+ Nil
+ case list =>
+ HandleDamageProxySetupLittleBuddy(list, hitPos)
+ UpdateProjectileSidednessAfterHit(projectile, hitPos)
+ val projectileSide = projectile.WhichSide
+ list.flatMap { proxy =>
+ if (proxy.profile.ExistsOnRemoteClients) {
+ proxy.Position = hitPos
+ proxy.WhichSide = projectileSide
+ continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy)
+ Nil
+ } else if (proxy.tool_def == GlobalDefinitions.maelstrom) {
+ //server-side maelstrom grenade target selection
+ val radius = proxy.profile.LashRadius * proxy.profile.LashRadius
+ val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList })
+ .filter { target =>
+ Vector3.DistanceSquared(target.Position, hitPos) <= radius
+ }
+ //chainlash is separated from the actual damage application for convenience
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.SendResponse(
+ PlanetSideGUID(0),
+ ChainLashMessage(
+ hitPos,
+ projectile.profile.ObjectId,
+ targets.map { _.GUID }
+ )
+ )
+ )
+ targets.map { target =>
+ CheckForHitPositionDiscrepancy(pguid, hitPos, target)
+ (target, proxy, hitPos, target.Position)
+ }
+ } else {
+ Nil
+ }
+ }
+ }
+ }
+
+ private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = {
+ val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw }
+ val size: Int = listOfLittleBuddies.size
+ if (size > 0) {
+ val desiredDownwardsProjectiles: Int = 2
+ val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down
+ val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out
+ val z: Float = player.Orientation.z //player's standing direction
+ val north: Vector3 = Vector3(0,1,0) //map North
+ val speed: Float = 144f //speed (packet discovered)
+ val dist: Float = 25 //distance (client defined)
+ val downwardsAngle: Float = -85f
+ val flaredAngle: Float = -70f
+ //angle of separation for downwards, degrees from vertical for flared out
+ val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) {
+ (360f / firstHalf, downwardsAngle)
+ } else {
+ (0f, 0f)
+ }
+ val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) {
+ (360f / secondHalf, flaredAngle)
+ } else {
+ (0f, 0f)
+ }
+ val smallRotOffset: Float = z + 90f
+ val largeRotOffset: Float = z + math.random().toFloat * 45f
+ val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat)
+ //downwards projectiles
+ var i: Int = 0
+ listOfLittleBuddies.take(firstHalf).foreach { proxy =>
+ val facing = (smallRotOffset + smallStep * i.toFloat) % 360
+ val dir = north.Rx(smallAngle).Rz(facing)
+ proxy.Position = detonationPosition + dir.xy + verticalCorrection
+ proxy.Velocity = dir * speed
+ proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing)
+ HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
+ i += 1
+ }
+ //flared out projectiles
+ i = 0
+ listOfLittleBuddies.drop(firstHalf).foreach { proxy =>
+ val facing = (largeRotOffset + largeStep * i.toFloat) % 360
+ val dir = north.Rx(largeAngle).Rz(facing)
+ proxy.Position = detonationPosition + dir
+ proxy.Velocity = dir * speed
+ proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing)
+ HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
+ i += 1
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = {
+ //explosion
+ val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction)
+ obj.Position = obj.Position + orientation * distance
+ val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner)
+ context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() }
+ }
+
+ 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)
+ }
+
+ //noinspection SameParameterValue
+ private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
+ ops.addShotsToMap(ops.shotsLanded, weaponId, shots)
+ }
+
+ private def CompileAutomatedTurretDamageData(
+ turret: AutomatedTurret,
+ owner: SourceEntry,
+ projectileTypeId: Long
+ ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
+ turret.Weapons
+ .values
+ .flatMap { _.Equipment }
+ .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
+ .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
+ }
+
+ private def HandleAIDamage(
+ target: PlanetSideServerObject with FactionAffinity with Vitality,
+ results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
+ ): Unit = {
+ results.collect {
+ case (obj, tool, owner, projectileInfo) =>
+ val angle = Vector3.Unit(target.Position - obj.Position)
+ val proj = new Projectile(
+ projectileInfo,
+ tool.Definition,
+ tool.FireMode,
+ None,
+ owner,
+ obj.Definition.ObjectId,
+ obj.Position + Vector3.z(value = 1f),
+ angle,
+ Some(angle * projectileInfo.FinalVelocity)
+ )
+ val hitPos = target.Position + Vector3.z(value = 1f)
+ ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionLogic.handleDealingDamage(target, resprojectile)
+ }
+ }
+ }
+
+ private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = {
+ val origin = projectile.Position
+ val distance = Vector3.Magnitude(hitPosition - origin)
+ continent.blockMap
+ .sector(hitPosition, distance)
+ .environmentList
+ .collect { case o: InteriorDoorPassage =>
+ val door = o.door
+ val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints(
+ origin,
+ hitPosition,
+ door.Position,
+ door.Definition.UseRadius + 0.1f
+ )
+ (door, intersectTest)
+ }
+ .collect { case (door, intersectionTest) if intersectionTest.nonEmpty =>
+ (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest)
+ }
+ .minByOption { case (_, dist, _) => dist }
+ .foreach { case (door, _, intersects) =>
+ val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) {
+ Sidedness.OutsideOf
+ } else {
+ Sidedness.InsideOf
+ }
+ projectile.WhichSide = if (intersects.size == 1) {
+ Sidedness.InBetweenSides(door, strictly)
+ } else {
+ strictly
+ }
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
new file mode 100644
index 000000000..412a4c343
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
@@ -0,0 +1,1292 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.support
+
+import akka.actor.Cancellable
+import akka.actor.typed.ActorRef
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.{AvatarActor, SessionActor}
+import net.psforever.actors.session.normal.{NormalMode => SessionNormalMode}
+import net.psforever.actors.session.spectator.{SpectatorMode => SessionSpectatorMode}
+import net.psforever.actors.zone.ZoneActor
+import net.psforever.objects.sourcing.PlayerSource
+import net.psforever.objects.zones.ZoneInfo
+import net.psforever.packet.game.SetChatFilterMessage
+import net.psforever.services.chat.{DefaultChannel, SquadChannel}
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.types.ChatMessageType.CMT_QUIT
+import org.log4s.Logger
+
+import scala.annotation.unused
+import scala.collection.{Seq, mutable}
+import scala.concurrent.duration._
+//
+import net.psforever.actors.zone.BuildingActor
+import net.psforever.login.WorldSession
+import net.psforever.objects.{Default, Player, Session}
+import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Shortcut => AvatarShortcut}
+import net.psforever.objects.definition.ImplantDefinition
+import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
+import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
+import net.psforever.objects.serverobject.structures.{Amenity, Building}
+import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
+import net.psforever.objects.zones.Zoning
+import net.psforever.packet.game.objectcreate.DrawnSlot
+import net.psforever.packet.game.{ChatMsg, CreateShortcutMessage, DeadState, RequestDestroyMessage, Shortcut}
+import net.psforever.services.{CavernRotationService, InterstellarClusterService}
+import net.psforever.services.chat.ChatService
+import net.psforever.services.chat.ChatChannel
+import net.psforever.types.ChatMessageType.{CMT_GMOPEN, UNK_227, UNK_229}
+import net.psforever.types.{ChatMessageType, Cosmetic, ExperienceType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
+import net.psforever.util.{Config, PointOfInterest}
+import net.psforever.zones.Zones
+
+trait ChatFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: ChatOperations
+
+ def handleChatMsg(message: ChatMsg): Unit
+
+ def handleChatFilter(pkt: SetChatFilterMessage): Unit
+
+ def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit
+}
+
+class ChatOperations(
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ val chatService: typed.ActorRef[ChatService.Command],
+ val cluster: typed.ActorRef[InterstellarClusterService.Command],
+ implicit val context: ActorContext
+ ) extends CommonSessionInterfacingFunctionality {
+ private var channels: List[ChatChannel] = List()
+ private var silenceTimer: Cancellable = Default.Cancellable
+ /**
+ * when another player is listed as one of our ignored players,
+ * and that other player sends an emote,
+ * that player is assigned a cooldown and only one emote per period will be seen
+ * key - character unique avatar identifier, value - when the current cooldown period will end
+ */
+ private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]()
+
+ import akka.actor.typed.scaladsl.adapter._
+ private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse]
+
+ def JoinChannel(channel: ChatChannel): Unit = {
+ chatService ! ChatService.JoinChannel(chatServiceAdapter, sessionLogic, channel)
+ channels ++= List(channel)
+ }
+
+ def LeaveChannel(channel: ChatChannel): Unit = {
+ chatService ! ChatService.LeaveChannel(chatServiceAdapter, channel)
+ channels = channels.filterNot(_ == channel)
+ }
+
+ def commandFly(contents: String, recipient: String): Unit = {
+ val (token, flying) = contents match {
+ case "on" => (contents, true)
+ case "off" => (contents, false)
+ case _ => ("off", false)
+ }
+ context.self ! SessionActor.SetFlying(flying)
+ sendResponse(ChatMsg(ChatMessageType.CMT_FLY, wideContents=false, recipient, token, None))
+ }
+
+ def commandWatermark(contents: String): Unit = {
+ val connectionState =
+ if (contents.contains("40 80")) 100
+ else if (contents.contains("120 200")) 25
+ else 50
+ context.self ! SessionActor.SetConnectionState(connectionState)
+ }
+
+ def commandSpeed(message: ChatMsg, contents: String): Unit = {
+ val speed =
+ try {
+ contents.toFloat
+ } catch {
+ case _: Throwable =>
+ 1f
+ }
+ context.self ! SessionActor.SetSpeed(speed)
+ sendResponse(message.copy(contents = f"$speed%.3f"))
+ }
+
+ def commandToggleSpectatorMode(session: Session, contents: String): Unit = {
+ val currentSpectatorActivation = session.player.spectator
+ contents.toLowerCase() match {
+ case "on" | "o" | "" if !currentSpectatorActivation =>
+ context.self ! SessionActor.SetMode(SessionSpectatorMode)
+ case "off" | "of" if currentSpectatorActivation =>
+ context.self ! SessionActor.SetMode(SessionNormalMode)
+ case _ => ()
+ }
+ }
+
+ def commandRecall(session: Session): Unit = {
+ val player = session.player
+ val errorMessage = session.zoningType match {
+ case Zoning.Method.Quit =>
+ Some("You can't recall to your sanctuary continent while quitting")
+ case Zoning.Method.InstantAction =>
+ Some("You can't recall to your sanctuary continent while instant actioning")
+ case Zoning.Method.Recall =>
+ Some("You already requested to recall to your sanctuary continent")
+ case _ if session.zone.id == Zones.sanctuaryZoneId(player.Faction) =>
+ Some("You can't recall to your sanctuary when you are already in your sanctuary")
+ case _ if !player.isAlive || session.deadState != DeadState.Alive =>
+ Some(if (player.isAlive) "@norecall_deconstructing" else "@norecall_dead")
+ case _ if player.VehicleSeated.nonEmpty =>
+ Some("@norecall_invehicle")
+ case _ =>
+ None
+ }
+ errorMessage match {
+ case Some(errorMessage) =>
+ sendResponse(ChatMsg(CMT_QUIT, errorMessage))
+ case None =>
+ context.self ! SessionActor.Recall()
+ }
+ }
+
+ def commandInstantAction(session: Session): Unit = {
+ val player = session.player
+ if (session.zoningType == Zoning.Method.Quit) {
+ sendResponse(ChatMsg(CMT_QUIT, "You can't instant action while quitting."))
+ } else if (session.zoningType == Zoning.Method.InstantAction) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_instantactionting"))
+ } else if (session.zoningType == Zoning.Method.Recall) {
+ sendResponse(
+ ChatMsg(CMT_QUIT, "You won't instant action. You already requested to recall to your sanctuary continent")
+ )
+ } else if (!player.isAlive || session.deadState != DeadState.Alive) {
+ if (player.isAlive) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_deconstructing"))
+ } else {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_dead"))
+ }
+ } else if (player.VehicleSeated.nonEmpty) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_invehicle"))
+ } else {
+ context.self ! SessionActor.InstantAction()
+ }
+ }
+
+ def commandQuit(session: Session): Unit = {
+ val player = session.player
+ if (session.zoningType == Zoning.Method.Quit) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_quitting"))
+ } else if (!player.isAlive || session.deadState != DeadState.Alive) {
+ if (player.isAlive) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_deconstructing"))
+ } else {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_dead"))
+ }
+ } else if (player.VehicleSeated.nonEmpty) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_invehicle"))
+ } else {
+ context.self ! SessionActor.Quit()
+ }
+ }
+
+ def commandSuicide(session: Session): Unit = {
+ if (session.player.isAlive && session.deadState != DeadState.Release) {
+ context.self ! SessionActor.Suicide()
+ }
+ }
+
+ def commandDestroy(session: Session, message: ChatMsg, contents: String): Unit = {
+ val guid = contents.toInt
+ session.zone.GUID(session.zone.map.terminalToSpawnPad.getOrElse(guid, guid)) match {
+ case Some(pad: VehicleSpawnPad) =>
+ pad.Actor ! VehicleSpawnControl.ProcessControl.Flush
+ case Some(turret: FacilityTurret) if turret.isUpgrading =>
+ WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None)
+ case _ =>
+ // FIXME we shouldn't do it like that
+ context.self ! RequestDestroyMessage(PlanetSideGUID(guid))
+ }
+ sendResponse(message)
+ }
+
+ def commandSetBaseResources(session: Session, contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val customNtuValue = buffer.lift(1) match {
+ case Some(x) if x.toIntOption.nonEmpty => Some(x.toInt)
+ case _ => None
+ }
+ val silos = {
+ val position = session.player.Position
+ session.zone.Buildings.values
+ .filter { building =>
+ val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
+ Vector3.DistanceSquared(building.Position, position) < soi2
+ }
+ }
+ .flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
+ setBaseResources(customNtuValue, silos, debugContent="")
+ }
+
+ def commandZoneLock(contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val (zoneOpt, lockVal) = (buffer.lift(1), buffer.lift(2)) match {
+ case (Some(x), Some(y)) =>
+ val zone = if (x.toIntOption.nonEmpty) {
+ val xInt = x.toInt
+ Zones.zones.find(_.Number == xInt)
+ } else {
+ Zones.zones.find(z => z.id.equals(x))
+ }
+ val value = if (y.toIntOption.nonEmpty && y.toInt == 0) {
+ 0
+ } else {
+ 1
+ }
+ (zone, Some(value))
+ case _ =>
+ (None, None)
+ }
+ (zoneOpt, lockVal) match {
+ case (Some(zone), Some(lock)) if zone.map.cavern =>
+ //caverns must be rotated in an order
+ if (lock == 0) {
+ cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneUnlock(zone.id))
+ } else {
+ cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneLock(zone.id))
+ }
+ case (Some(_), Some(_)) =>
+ //normal zones can lock when all facilities and towers on it belong to the same faction
+ //normal zones can lock when ???
+ case _ => ()
+ }
+ }
+
+ def commandZoneRotate(): Unit = {
+ cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
+ }
+
+ def commandCaptureBase(session: Session, message: ChatMsg, contents: String): Unit = {
+ val buffer = cliTokenization(contents).take(3)
+ //walk through the param buffer
+ val (foundFacilities, foundFacilitiesTag, factionBuffer) = firstParam(session, buffer, captureBaseParamFacilities)
+ val (foundFaction, foundFactionTag, timerBuffer) = firstParam(session, factionBuffer, captureBaseParamFaction)
+ val (foundTimer, foundTimerTag, _) = firstParam(session, timerBuffer, captureBaseParamTimer)
+ //resolve issues with the initial params
+ var facilityError: Int = 0
+ var factionError: Boolean = false
+ var timerError: Boolean = false
+ var usageMessage: Boolean = false
+ val resolvedFacilities = foundFacilities
+ .orElse {
+ if (foundFacilitiesTag.nonEmpty) {
+ if (foundFaction.isEmpty) {
+ /* /capturebase OR /capturebase */
+ //malformed facility tag error
+ facilityError = 2
+ None
+ } else if (!foundFacilitiesTag.contains("curr")) { //did we do this next check already
+ /* /capturebase , potentially */
+ val buildings = captureBaseCurrSoi(session)
+ if (buildings.nonEmpty) {
+ //convert facilities to faction
+ Some(buildings.toSeq)
+ } else {
+ //no facilities error
+ facilityError = 1
+ None
+ }
+ } else {
+ //no facilities error
+ facilityError = 1
+ None
+ }
+ } else {
+ //no params; post command usage reminder
+ usageMessage = true
+ None
+ }
+ }
+ val resolvedFaction = foundFaction
+ .orElse {
+ if (resolvedFacilities.nonEmpty) {
+ /* /capturebase OR /capturebase */
+ if (foundFactionTag.isEmpty || foundTimer.nonEmpty) {
+ //convert facilities to OUR PLAYER'S faction
+ Some(session.player.Faction)
+ } else {
+ //malformed faction tag error
+ factionError = true
+ None
+ }
+ } else {
+ //incorrect params; already posted an error message
+ None
+ }
+ }
+ val resolvedTimer = foundTimer
+ .orElse {
+ //todo stop command execution? post command usage reminder?
+ if (resolvedFaction.nonEmpty && foundTimerTag.nonEmpty) {
+ /* /capturebase > > */
+ //malformed timer tag error
+ timerError = true
+ None
+ } else {
+ //eh
+ Some(1)
+ }
+ }
+ //evaluate results
+ (resolvedFacilities, resolvedFaction, resolvedTimer) match {
+ case (Some(buildings), Some(faction), Some(_)) =>
+ buildings.foreach { building =>
+ //TODO implement timer
+ val terminal = building.CaptureTerminal.get
+ val zone = building.Zone
+ val zoneActor = zone.actor
+ val buildingActor = building.Actor
+ //clear any previous hack
+ if (building.CaptureTerminalIsHacked) {
+ zone.LocalEvents ! LocalServiceMessage(
+ zone.id,
+ LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
+ )
+ }
+ //push any updates this might cause
+ zoneActor ! ZoneActor.ZoneMapUpdate()
+ //convert faction affiliation
+ buildingActor ! BuildingActor.SetFaction(faction)
+ buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false))
+ //push for map updates again
+ zoneActor ! ZoneActor.ZoneMapUpdate()
+ }
+ case _ =>
+ if (usageMessage) {
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
+ )
+ } else {
+ val msg = if (facilityError == 1) { "can not contextually determine building target" }
+ else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" }
+ else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" }
+ else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" }
+ else { "malformed params; check usage" }
+ sendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None))
+ }
+ }
+ }
+
+ def commandVoice(session: Session, message: ChatMsg, contents: String, toChannel: ChatChannel): Unit = {
+ // SH prefix are tactical voice macros only sent to squad
+ if (contents.startsWith("SH")) {
+ channels.foreach {
+ case _/*channel*/: SquadChannel =>
+ commandSendToRecipient(session, message, toChannel)
+ case _ => ()
+ }
+ } else {
+ commandSendToRecipient(session, message, toChannel)
+ }
+ }
+
+ def commandTellOrIgnore(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ if (AvatarActor.onlineIfNotIgnored(message.recipient, session.avatar.name)) {
+ commandSend(session, message, toChannel)
+ } else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_target", None)
+ )
+ } else {
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_ignore", None)
+ )
+ }
+ }
+
+ def commandSquad(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ channels.foreach {
+ case _/*channel*/: SquadChannel =>
+ commandSendToRecipient(session, message, toChannel)
+ case _ => ()
+ }
+ }
+
+ def commandWho(session: Session): Unit = {
+ val players = session.zone.Players
+ val popTR = players.count(_.faction == PlanetSideEmpire.TR)
+ val popNC = players.count(_.faction == PlanetSideEmpire.NC)
+ val popVS = players.count(_.faction == PlanetSideEmpire.VS)
+ if (popNC + popTR + popVS == 0) {
+ sendResponse(ChatMsg(ChatMessageType.CMT_WHO, "@Nomatches"))
+ } else {
+ val contName = session.zone.map.name
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "That command doesn't work for now, but : ", None)
+ )
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "NC online : " + popNC + " on " + contName, None)
+ )
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "TR online : " + popTR + " on " + contName, None)
+ )
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "VS online : " + popVS + " on " + contName, None)
+ )
+ }
+ }
+
+ def commandZone(message: ChatMsg, contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val (zone, gate, list) = (buffer.headOption, buffer.lift(1)) match {
+ case (Some("-list"), None) =>
+ (None, None, true)
+ case (Some(zoneId), Some("-list")) =>
+ (PointOfInterest.get(zoneId), None, true)
+ case (Some(zoneId), gateId) =>
+ val zone = PointOfInterest.get(zoneId)
+ val gate = (zone, gateId) match {
+ case (Some(zone), Some(gateId)) => PointOfInterest.getWarpgate(zone, gateId)
+ case (Some(zone), None) => Some(PointOfInterest.selectRandom(zone))
+ case _ => None
+ }
+ (zone, gate, false)
+ case _ =>
+ (None, None, false)
+ }
+ (zone, gate, list) match {
+ case (None, None, true) =>
+ sendResponse(ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.list, None))
+ case (Some(zone), None, true) =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listWarpgates(zone), None)
+ )
+ case (Some(zone), Some(gate), false) =>
+ context.self ! SessionActor.SetZone(zone.zonename, gate)
+ case (_, None, false) =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", "Gate id not defined (use '/zone -list')", None)
+ )
+ case (_, _, _) if buffer.isEmpty || buffer.headOption.contains("-help") =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_ZONE_usage")
+ )
+ case _ => ()
+ }
+ }
+
+ def commandWarp(session: Session, message: ChatMsg, contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val (coordinates, waypoint) = (buffer.headOption, buffer.lift(1), buffer.lift(2)) match {
+ case (Some(x), Some(y), Some(z)) => (Some(x, y, z), None)
+ case (Some("to"), Some(_/*character*/), None) => (None, None) // TODO not implemented
+ case (Some("near"), Some(_/*objectName*/), None) => (None, None) // TODO not implemented
+ case (Some(waypoint), None, None) if waypoint.nonEmpty => (None, Some(waypoint))
+ case _ => (None, None)
+ }
+ (coordinates, waypoint) match {
+ case (Some((x, y, z)), None) if List(x, y, z).forall { str =>
+ val coordinate = str.toFloatOption
+ coordinate.isDefined && coordinate.get >= 0 && coordinate.get <= 8191
+ } =>
+ context.self ! SessionActor.SetPosition(Vector3(x.toFloat, y.toFloat, z.toFloat))
+ case (None, Some(waypoint)) if waypoint == "-list" =>
+ val zone = PointOfInterest.get(session.player.Zone.id)
+ zone match {
+ case Some(zone: PointOfInterest) =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listAll(zone), None)
+ )
+ case _ =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", s"unknown player zone '${session.player.Zone.id}'", None)
+ )
+ }
+ case (None, Some(waypoint)) if waypoint != "-help" =>
+ PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
+ case Some(location) =>
+ context.self ! SessionActor.SetPosition(location)
+ case None =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", s"unknown location '$waypoint'", None)
+ )
+ }
+ case _ =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_WARP_usage")
+ )
+ }
+ }
+
+ def commandSetBattleRank(session: Session, message: ChatMsg, contents: String): Unit = {
+ if (!setBattleRank(session, cliTokenization(contents), AvatarActor.SetBep)) {
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
+ )
+ }
+ }
+
+ def commandSetCommandRank(session: Session, message: ChatMsg, contents: String): Unit = {
+ if (!setCommandRank(contents, session)) {
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
+ )
+ }
+ }
+
+ def commandAddBattleExperience(message: ChatMsg, contents: String): Unit = {
+ contents.toIntOption match {
+ case Some(bep) => avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Normal)
+ case None =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_ADDBATTLEEXPERIENCE_usage")
+ )
+ }
+ }
+
+ def commandAddCommandExperience(message: ChatMsg, contents: String): Unit = {
+ contents.toIntOption match {
+ case Some(cep) => avatarActor ! AvatarActor.AwardCep(cep)
+ case None =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_ADDCOMMANDEXPERIENCE_usage")
+ )
+ }
+ }
+
+ def commandToggleHat(session: Session, message: ChatMsg, contents: String): Unit = {
+ val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
+ val nextCosmetics = contents match {
+ case "off" =>
+ cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
+ case _ =>
+ if (cosmetics.contains(Cosmetic.BrimmedCap)) {
+ cosmetics.diff(Set(Cosmetic.BrimmedCap)) + Cosmetic.Beret
+ } else if (cosmetics.contains(Cosmetic.Beret)) {
+ cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
+ } else {
+ cosmetics + Cosmetic.BrimmedCap
+ }
+ }
+ val on = nextCosmetics.contains(Cosmetic.BrimmedCap) || nextCosmetics.contains(Cosmetic.Beret)
+ avatarActor ! AvatarActor.SetCosmetics(nextCosmetics)
+ sendResponse(
+ message.copy(
+ messageType = UNK_229,
+ contents = s"@CMT_TOGGLE_HAT_${if (on) "on" else "off"}"
+ )
+ )
+ }
+
+ def commandToggleCosmetics(session: Session, message: ChatMsg, contents: String): Unit = {
+ val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
+ val cosmetic = message.messageType match {
+ case ChatMessageType.CMT_HIDE_HELMET => Cosmetic.NoHelmet
+ case ChatMessageType.CMT_TOGGLE_SHADES => Cosmetic.Sunglasses
+ case ChatMessageType.CMT_TOGGLE_EARPIECE => Cosmetic.Earpiece
+ case _ => null
+ }
+ val on = contents match {
+ case "on" => true
+ case "off" => false
+ case _ => !cosmetics.contains(cosmetic)
+ }
+ avatarActor ! AvatarActor.SetCosmetics(
+ if (on) cosmetics + cosmetic
+ else cosmetics.diff(Set(cosmetic))
+ )
+ sendResponse(
+ message.copy(
+ messageType = UNK_229,
+ contents = s"@${message.messageType.toString}_${if (on) "on" else "off"}"
+ )
+ )
+ }
+
+ def commandAddCertification(session: Session, message: ChatMsg, contents: String): Unit = {
+ val certs = cliTokenization(contents).map(name => Certification.values.find(_.name == name))
+ val result = if (certs.nonEmpty) {
+ if (certs.contains(None)) {
+ s"@AckErrorCertifications"
+ } else {
+ avatarActor ! AvatarActor.SetCertifications(session.avatar.certifications ++ certs.flatten)
+ s"@AckSuccessCertifications"
+ }
+ } else {
+ if (session.avatar.certifications.size < Certification.values.size) {
+ avatarActor ! AvatarActor.SetCertifications(Certification.values.toSet)
+ } else {
+ avatarActor ! AvatarActor.SetCertifications(Certification.values.filter(_.cost == 0).toSet)
+ }
+ s"@AckSuccessCertifications"
+ }
+ sendResponse(message.copy(messageType = UNK_229, contents = result))
+ }
+
+ def commandKick(session: Session, message: ChatMsg, contents: String): Unit = {
+ val inputs = cliTokenization(contents)
+ inputs.headOption match {
+ case Some(input) =>
+ val determination: Player => Boolean = input.toLongOption match {
+ case Some(id) => _.CharId == id
+ case _ => _.Name.equals(input)
+ }
+ session.zone.LivePlayers
+ .find(determination)
+ .orElse(session.zone.Corpses.find(determination)) match {
+ case Some(player) =>
+ inputs.lift(1).map(_.toLongOption) match {
+ case Some(Some(time)) =>
+ context.self ! SessionActor.Kick(player, Some(time))
+ case _ =>
+ context.self ! SessionActor.Kick(player)
+ }
+ sendResponse(message.copy(messageType = UNK_229, recipient = "Server", contents = "@kick_i"))
+ case None =>
+ sendResponse(message.copy(messageType = UNK_229, recipient = "Server", contents = "@kick_o"))
+ }
+ case None =>
+ sendResponse(message.copy(messageType = UNK_229, recipient = "Server", contents = "@kick_o"))
+ }
+ }
+
+ def commandIncomingSendAllIfOnline(session: Session, message: ChatMsg): Unit = {
+ if (AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)) {
+ sendResponse(message)
+ }
+ }
+
+ def commandIncomingSendToLocalIfOnline(session: Session, fromSession: Session, message: ChatMsg): Unit = {
+ if (
+ session.zone == fromSession.zone &&
+ Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 625 &&
+ session.player.Faction == fromSession.player.Faction &&
+ AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)
+ ) {
+ sendResponse(message)
+ }
+ }
+
+ def commandIncomingVoice(session: Session, fromSession: Session, message: ChatMsg): Unit = {
+ if (
+ (session.zone == fromSession.zone || message.contents.startsWith("SH")) && /*tactical squad voice macro*/
+ Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 1600
+ ) {
+ val name = fromSession.avatar.name
+ if (!session.avatar.people.ignored.exists { f => f.name.equals(name) } ||
+ {
+ val id = fromSession.avatar.id.toLong
+ val curr = System.currentTimeMillis()
+ ignoredEmoteCooldown.get(id) match {
+ case None =>
+ ignoredEmoteCooldown.put(id, curr + 15000L)
+ true
+ case Some(time) if time < curr =>
+ ignoredEmoteCooldown.put(id, curr + 15000L)
+ true
+ case _ =>
+ false
+ }}
+ ) {
+ sendResponse(message)
+ }
+ }
+ }
+
+ def commandIncomingSilence(session: Session, message: ChatMsg): Unit = {
+ val args = cliTokenization(message.contents)
+ val (name, time) = (args.headOption, args.lift(1)) match {
+ case (Some(name), _) if name != session.player.Name =>
+ log.error("Received silence message for other player")
+ (None, None)
+ case (Some(name), None) => (Some(name), Some(5))
+ case (Some(name), Some(time)) if time.toIntOption.isDefined => (Some(name), Some(time.toInt))
+ case _ => (None, None)
+ }
+ (name, time) match {
+ case (Some(_), Some(time)) =>
+ if (session.player.silenced) {
+ context.self ! SessionActor.SetSilenced(false)
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_off", None)
+ )
+ if (!silenceTimer.isCancelled) silenceTimer.cancel()
+ } else {
+ context.self ! SessionActor.SetSilenced(true)
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_on", None)
+ )
+ import scala.concurrent.ExecutionContext.Implicits.global
+ silenceTimer = context.system.scheduler.scheduleOnce(
+ time minutes,
+ new Runnable {
+ def run(): Unit = {
+ context.self ! SessionActor.SetSilenced(false)
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_timeout", None)
+ )
+ }
+ }
+ )
+ }
+ case (name, time) =>
+ log.warn(s"Bad silence args $name $time")
+ }
+ }
+
+
+
+ /**
+ * For a provided number of facility nanite transfer unit resource silos,
+ * charge the facility's silo with an expected amount of nanite transfer units.
+ * @see `Amenity`
+ * @see `ChatMsg`
+ * @see `ResourceSilo`
+ * @see `ResourceSilo.UpdateChargeLevel`
+ * @see `SessionActor.Command`
+ * @see `SessionActor.SendResponse`
+ * @param resources the optional number of resources to set to each silo;
+ * different values provide different resources as indicated below;
+ * an undefined value also has a condition
+ * @param silos where to deposit the resources
+ * @param debugContent something for log output context
+ */
+ private def setBaseResources(resources: Option[Int], silos: Iterable[Amenity], debugContent: String): Unit = {
+ if (silos.isEmpty) {
+ context.self ! SessionActor.SendResponse(
+ ChatMsg(UNK_229, wideContents=true, "Server", s"no targets for ntu found with parameters $debugContent", None)
+ )
+ }
+ resources match {
+ // x = n0% of maximum capacitance
+ case Some(value) if value > -1 && value < 11 =>
+ silos.collect {
+ case silo: ResourceSilo =>
+ silo.Actor ! ResourceSilo.UpdateChargeLevel(
+ value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
+ )
+ }
+ // capacitance set to x (where x > 10) exactly, within limits
+ case Some(value) =>
+ silos.collect {
+ case silo: ResourceSilo =>
+ silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
+ }
+ case None =>
+ // x >= n0% of maximum capacitance and x <= maximum capacitance
+ val rand = new scala.util.Random
+ silos.collect {
+ case silo: ResourceSilo =>
+ val a = 7
+ val b = 10 - a
+ val tenth = silo.MaxNtuCapacitor * 0.1f
+ silo.Actor ! ResourceSilo.UpdateChargeLevel(
+ a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
+ )
+ }
+ }
+ }
+
+ /**
+ * Create a medkit shortcut if there is no medkit shortcut on the hotbar.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `Array::indexWhere`
+ * @see `CreateShortcutMessage`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `net.psforever.packet.game.Shortcut.Medkit`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param shortcuts list of all existing shortcuts, used for early validation
+ */
+ private def medkitSanityTest(
+ guid: PlanetSideGUID,
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ if (!shortcuts.exists {
+ case Some(a) => a.purpose == 0
+ case None => false
+ }) {
+ shortcuts.indexWhere(_.isEmpty) match {
+ case -1 => ()
+ case index =>
+ //new shortcut
+ sendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(Shortcut.Medkit)
+ ))
+ }
+ }
+ }
+
+ /**
+ * Create all implant macro shortcuts for all implants whose shortcuts have been removed from the hotbar.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `CreateShortcutMessage`
+ * @see `ImplantDefinition`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param haveImplants list of implants the player possesses
+ * @param shortcuts list of all existing shortcuts, used for early validation
+ */
+ private def implantSanityTest(
+ guid: PlanetSideGUID,
+ haveImplants: Iterable[ImplantDefinition],
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ val haveImplantShortcuts = shortcuts.collect {
+ case Some(shortcut) if shortcut.purpose == 2 => shortcut.tile
+ }
+ var start: Int = 0
+ haveImplants.filterNot { imp => haveImplantShortcuts.contains(imp.Name) }
+ .foreach { implant =>
+ shortcuts.indexWhere(_.isEmpty, start) match {
+ case -1 => ()
+ case index =>
+ //new shortcut
+ start = index + 1
+ sendResponse(CreateShortcutMessage(
+ guid,
+ start,
+ Some(implant.implantType.shortcut)
+ ))
+ }
+ }
+ }
+
+ /**
+ * Create a text chat macro shortcut if it doesn't already exist.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `Array::indexWhere`
+ * @see `CreateShortcutMessage`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `net.psforever.packet.game.Shortcut.Macro`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param acronym three letters emblazoned on the shortcut icon
+ * @param msg the message published to text chat
+ * @param shortcuts a list of all existing shortcuts, used for early validation
+ */
+ private def macroSanityTest(
+ guid: PlanetSideGUID,
+ acronym: String,
+ msg: String,
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ shortcuts.indexWhere(_.isEmpty) match {
+ case -1 => ()
+ case index =>
+ //new shortcut
+ sendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(Shortcut.Macro(acronym, msg))
+ ))
+ }
+ }
+
+ private def setBattleRank(
+ session: Session,
+ params: Seq[String],
+ msgFunc: Long => AvatarActor.Command
+ ): Boolean = {
+ val (target, rank) = (params.headOption, params.lift(1)) match {
+ case (Some(target), Some(rank)) if target == session.avatar.name =>
+ rank.toIntOption match {
+ case Some(rank) => (None, BattleRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case (Some("-h"), _) | (Some("-help"), _) =>
+ (None, Some(BattleRank.BR1))
+ case (Some(_), Some(_)) =>
+ // picking other targets is not supported for now
+ (None, None)
+ case (Some(rank), None) =>
+ rank.toIntOption match {
+ case Some(rank) => (None, BattleRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case _ => (None, None)
+ }
+ (target, rank) match {
+ case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
+ avatarActor ! msgFunc(rank.experience)
+ true
+ case _ =>
+ false
+ }
+ }
+
+ private def setCommandRank(
+ contents: String,
+ session: Session
+ ): Boolean = {
+ val buffer = cliTokenization(contents)
+ val (target, rank) = (buffer.headOption, buffer.lift(1)) match {
+ case (Some(target), Some(rank)) if target == session.avatar.name =>
+ rank.toIntOption match {
+ case Some(rank) => (None, CommandRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case (Some(_), Some(_)) =>
+ // picking other targets is not supported for now
+ (None, None)
+ case (Some(rank), None) =>
+ rank.toIntOption match {
+ case Some(rank) => (None, CommandRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case _ => (None, None)
+ }
+ (target, rank) match {
+ case (_, Some(rank)) =>
+ avatarActor ! AvatarActor.SetCep(rank.experience)
+ true
+ case _ =>
+ false
+ }
+ }
+
+ private def captureBaseParamFacilities(
+ session: Session,
+ token: Option[String]
+ ): Option[Seq[Building]] = {
+ token.collect {
+ case "curr" =>
+ val list = captureBaseCurrSoi(session)
+ if (list.nonEmpty) {
+ Some(list.toSeq)
+ } else {
+ None
+ }
+ case "all" =>
+ val list = session.zone.Buildings.values.filter(_.CaptureTerminal.isDefined)
+ if (list.nonEmpty) {
+ Some(list.toSeq)
+ } else {
+ None
+ }
+ case name =>
+ val trueName = ZoneInfo
+ .values
+ .find(_.id.equals(session.zone.id))
+ .flatMap { info =>
+ info.aliases
+ .facilities
+ .collectFirst { case (key, internalName) if key.equalsIgnoreCase(name) => internalName }
+ }
+ .getOrElse(name)
+ session.zone.Buildings
+ .values
+ .find {
+ building => trueName.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined
+ }
+ .map(b => Seq(b))
+ }
+ .flatten
+ }
+
+ private def captureBaseCurrSoi(
+ session: Session
+ ): Iterable[Building] = {
+ val charId = session.player.CharId
+ session.zone.Buildings.values.filter { building =>
+ building.PlayersInSOI.exists(_.CharId == charId)
+ }
+ }
+
+ private def captureBaseParamFaction(
+ @unused session: Session,
+ token: Option[String]
+ ): Option[PlanetSideEmpire.Value] = {
+ token.collect {
+ case faction =>
+ faction.toLowerCase() match {
+ case "tr" => Some(PlanetSideEmpire.TR)
+ case "nc" => Some(PlanetSideEmpire.NC)
+ case "vs" => Some(PlanetSideEmpire.VS)
+ case "none" => Some(PlanetSideEmpire.NEUTRAL)
+ case "bo" => Some(PlanetSideEmpire.NEUTRAL)
+ case "neutral" => Some(PlanetSideEmpire.NEUTRAL)
+ case _ => None
+ }
+ }.flatten
+ }
+
+ private def captureBaseParamTimer(
+ @unused session: Session,
+ token: Option[String]
+ ): Option[Int] = {
+ token.flatMap(_.toIntOption)
+ }
+
+
+
+ def customCommandWhitetext(
+ session: Session,
+ content: Seq[String]
+ ): Boolean = {
+ chatService ! ChatService.Message(
+ session,
+ ChatMsg(UNK_227, wideContents=true, "", content.mkString(" "), None),
+ DefaultChannel
+ )
+ true
+ }
+
+ def customCommandLoc(
+ session: Session,
+ message: ChatMsg
+ ): Boolean = {
+ val continent = session.zone
+ val player = session.player
+ val loc =
+ s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
+ sendResponse(message.copy(contents = loc))
+ true
+ }
+
+ def customCommandList(
+ session: Session,
+ params: Seq[String],
+ message: ChatMsg
+ ): Boolean = {
+ val zone = params.headOption match {
+ case Some("") | None =>
+ Some(session.zone)
+ case Some(id) =>
+ Zones.zones.find(_.id == id)
+ }
+ zone match {
+ case Some(inZone) =>
+ sendResponse(
+ ChatMsg(
+ CMT_GMOPEN,
+ message.wideContents,
+ "Server",
+ "\\#8Name (Faction) [ID] at PosX PosY PosZ",
+ message.note
+ )
+ )
+ (inZone.LivePlayers ++ inZone.Corpses)
+ .filter(_.CharId != session.player.CharId)
+ .sortBy(p => (p.Name, !p.isAlive))
+ .foreach(player => {
+ val color = if (!player.isAlive) "\\#7" else ""
+ sendResponse(
+ ChatMsg(
+ CMT_GMOPEN,
+ message.wideContents,
+ "Server",
+ s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
+ message.note
+ )
+ )
+ })
+ case None =>
+ sendResponse(
+ ChatMsg(
+ CMT_GMOPEN,
+ message.wideContents,
+ "Server",
+ "Invalid zone ID",
+ message.note
+ )
+ )
+ }
+ true
+ }
+
+ def customCommandNtu(
+ session: Session,
+ params: Seq[String]
+ ): Boolean = {
+ val (facility, customNtuValue) = (params.headOption, params.lift(1)) match {
+ case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
+ case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
+ case _ => (None, None)
+ }
+ val silos = (facility match {
+ case Some(cur) if cur.toLowerCase().startsWith("curr") =>
+ val position = session.player.Position
+ session.zone.Buildings.values
+ .filter { building =>
+ val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
+ Vector3.DistanceSquared(building.Position, position) < soi2
+ }
+ case Some(all) if all.toLowerCase.startsWith("all") =>
+ session.zone.Buildings.values
+ case Some(x) =>
+ session.zone.Buildings.values.find(_.Name.equalsIgnoreCase(x)).toList
+ case _ =>
+ session.zone.Buildings.values
+ })
+ .flatMap { building =>
+ building.Amenities.filter(_.isInstanceOf[ResourceSilo])
+ }
+ setBaseResources(customNtuValue, silos, debugContent = s"$facility")
+ true
+ }
+
+ def customCommandZonerotate(
+ params: Seq[String]
+ ): Boolean = {
+ cluster ! InterstellarClusterService.CavernRotation(params.headOption match {
+ case Some("-list") | Some("-l") =>
+ CavernRotationService.ReportRotationOrder(context.self)
+ case _ =>
+ CavernRotationService.HurryNextRotation
+ })
+ true
+ }
+
+ def customCommandSuicide(
+ session: Session
+ ): Boolean = {
+ //this is like CMT_SUICIDE but it ignores checks and forces a suicide state
+ val tplayer = session.player
+ tplayer.Revive
+ tplayer.Actor ! Player.Die()
+ true
+ }
+
+ def customCommandGrenade(
+ session: Session,
+ log: Logger
+ ): Boolean = {
+ WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
+ true
+ }
+
+ def customCommandMacro(
+ session: Session,
+ params: Seq[String]
+ ): Boolean = {
+ val avatar = session.avatar
+ (params.headOption, params.lift(1)) match {
+ case (Some(cmd), other) =>
+ cmd.toLowerCase() match {
+ case "medkit" =>
+ medkitSanityTest(session.player.GUID, avatar.shortcuts)
+ true
+
+ case "implants" =>
+ //implant shortcut sanity test
+ implantSanityTest(
+ session.player.GUID,
+ avatar.implants.collect {
+ case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
+ },
+ avatar.shortcuts
+ )
+ true
+
+ case name
+ if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
+ avatar.implants.find {
+ case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
+ case None => false
+ } match {
+ case Some(Some(implant)) =>
+ //specific implant shortcut sanity test
+ implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
+ true
+ case _ if other.nonEmpty =>
+ //add macro?
+ macroSanityTest(session.player.GUID, name, params.drop(2).mkString(" "), avatar.shortcuts)
+ true
+ case _ =>
+ false
+ }
+
+ case name
+ if name.nonEmpty && other.nonEmpty =>
+ //add macro
+ macroSanityTest(session.player.GUID, name, params.drop(2).mkString(" "), avatar.shortcuts)
+ true
+
+ case _ =>
+ false
+ }
+ case _ =>
+ false
+ }
+ }
+
+ def customCommandProgress(
+ session: Session,
+ params: Seq[String]
+ ): Boolean = {
+ val ourRank = BattleRank.withExperience(session.avatar.bep).value
+ if (!session.account.gm &&
+ (ourRank <= Config.app.game.promotion.broadcastBattleRank ||
+ ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
+ setBattleRank(session, params, AvatarActor.Progress)
+ true
+ } else {
+ setBattleRank(session, Seq("1"), AvatarActor.Progress)
+ false
+ }
+ }
+
+ def customCommandNearby(
+ session: Session
+ ): Boolean = {
+ val playerPos = session.player.Position.xy
+ val closest = session.zone
+ .Buildings
+ .values
+ .toSeq
+ .minByOption(base => Vector3.DistanceSquared(playerPos, base.Position.xy))
+ .map(base => s"${base.Name} - ${base.Definition.Name}")
+ sendResponse(
+ ChatMsg(CMT_GMOPEN, wideContents = false, "Server", s"closest facility: $closest", None)
+ )
+ true
+ }
+
+ def firstParam[T](
+ session: Session,
+ buffer: Iterable[String],
+ func: (Session, Option[String])=>Option[T]
+ ): (Option[T], Option[String], Iterable[String]) = {
+ val tokenOpt = buffer.headOption
+ val valueOpt = func(session, tokenOpt)
+ val outBuffer = if (valueOpt.nonEmpty) {
+ buffer.drop(1)
+ } else {
+ buffer
+ }
+ (valueOpt, tokenOpt, outBuffer)
+ }
+
+ def cliTokenization(str: String): List[String] = {
+ str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList
+ }
+
+ def commandIncomingSend(message: ChatMsg): Unit = {
+ sendResponse(message)
+ }
+
+ def commandSend(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ chatService ! ChatService.Message(
+ session,
+ message,
+ toChannel
+ )
+ }
+
+ def commandSendToRecipient(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ chatService ! ChatService.Message(
+ session,
+ message.copy(recipient = session.player.Name),
+ toChannel
+ )
+ }
+
+ override protected[session] def stop(): Unit = {
+ silenceTimer.cancel()
+ chatService ! ChatService.LeaveAllChannels(chatServiceAdapter)
+ }
+}
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 0856a2e98..3dec2e243 100644
--- a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala
+++ b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala
@@ -23,23 +23,25 @@ trait CommonSessionInterfacingFunctionality {
protected def context: ActorContext
- protected def sessionData: SessionData
+ protected def sessionLogic: SessionData
- protected def session: Session = sessionData.session
+ protected def session: Session = sessionLogic.session
- protected def session_=(newsession: Session): Unit = sessionData.session_=(newsession)
+ protected def session_=(newsession: Session): Unit = sessionLogic.session_=(newsession)
- protected def account: Account = sessionData.account
+ protected def account: Account = sessionLogic.account
- protected def continent: Zone = sessionData.continent
+ protected def continent: Zone = sessionLogic.continent
- protected def player: Player = sessionData.player
+ protected def player: Player = sessionLogic.player
- protected def avatar: Avatar = sessionData.avatar
+ protected def avatar: Avatar = sessionLogic.avatar
- protected def log: Logger = sessionData.log
+ protected def log: Logger = sessionLogic.log
- protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionData.sendResponse(pkt)
+ protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionLogic.sendResponse(pkt)
+
+ protected[session] def actionsToCancel(): 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
new file mode 100644
index 000000000..9ae7677a5
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
@@ -0,0 +1,777 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.support
+
+import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
+import net.psforever.objects.sourcing.PlayerSource
+
+import scala.collection.mutable
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+//
+import net.psforever.actors.session.{AvatarActor, SessionActor}
+import net.psforever.login.WorldSession._
+import net.psforever.objects._
+import net.psforever.objects.avatar._
+import net.psforever.objects.ce._
+import net.psforever.objects.definition._
+import net.psforever.objects.equipment._
+import net.psforever.objects.guid._
+import net.psforever.objects.inventory.{Container, InventoryItem}
+import net.psforever.objects.locker.LockerContainer
+import net.psforever.objects.serverobject.llu.CaptureFlag
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
+import net.psforever.objects.vehicles._
+import net.psforever.objects.vital._
+import net.psforever.objects.zones._
+import net.psforever.packet._
+import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage}
+import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
+import net.psforever.packet.game.objectcreate._
+import net.psforever.packet.game._
+import net.psforever.services.account.AccountPersistenceService
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.services.local.support.CaptureFlagManager
+import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.services.Service
+import net.psforever.types._
+import net.psforever.util.Config
+
+trait GeneralFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: GeneralOperations
+
+ def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit
+
+ def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit
+
+ def handleCharacterRequest(pkt: CharacterRequestMessage): Unit
+
+ def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit
+
+ def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit
+
+ def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit
+
+ def handleEmote(pkt: EmoteMsg): Unit
+
+ def handleDropItem(pkt: DropItemMessage): Unit
+
+ def handlePickupItem(pkt: PickupItemMessage): Unit
+
+ def handleObjectHeld(pkt: ObjectHeldMessage): Unit
+
+ def handleAvatarJump(pkt: AvatarJumpMessage): Unit
+
+ def handleZipLine(pkt: ZipLineMessage): Unit
+
+ def handleRequestDestroy(pkt: RequestDestroyMessage): Unit
+
+ def handleMoveItem(pkt: MoveItemMessage): Unit
+
+ def handleLootItem(pkt: LootItemMessage): Unit
+
+ def handleAvatarImplant(pkt: AvatarImplantMessage): Unit
+
+ def handleUseItem(pkt: UseItemMessage): Unit
+
+ def handleUnuseItem(pkt: UnuseItemMessage): Unit
+
+ def handleDeployObject(pkt: DeployObjectMessage): Unit
+
+ def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit
+
+ def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit
+
+ def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit
+
+ def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit
+
+ def handleGenericAction(pkt: GenericActionMessage): Unit
+
+ def handleGenericCollision(pkt: GenericCollisionMsg): Unit
+
+ def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit
+
+ def handleBugReport(pkt: PlanetSideGamePacket): Unit
+
+ def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit
+
+ def handleBattleplan(pkt: BattleplanMessage): Unit
+
+ def handleBindPlayer(pkt: BindPlayerMessage): Unit
+
+ def handleCreateShortcut(pkt: CreateShortcutMessage): Unit
+
+ def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit
+
+ def handleFriendRequest(pkt: FriendsRequest): Unit
+
+ def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit
+
+ def handleActionCancel(pkt: ActionCancelMessage): Unit
+
+ def handleTrade(pkt: TradeMessage): Unit
+
+ def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit
+
+ def handleObjectDetected(pkt: ObjectDetectedMessage): Unit
+
+ def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit
+
+ def handleHitHint(pkt: HitHint): Unit
+
+ /* messages */
+
+ def handleSetAvatar(avatar: Avatar): Unit
+
+ def handleReceiveAccountData(account: Account): Unit
+
+ def handleUseCooldownRenew: BasicDefinition => Unit
+
+ def handleAvatarResponse(avatar: Avatar): Unit
+
+ def handleSetSpeed(speed: Float): Unit
+
+ def handleSetFlying(flying: Boolean): Unit
+
+ def handleSetSpectator(spectator: Boolean): Unit
+
+ def handleKick(player: Player, time: Option[Long]): Unit
+
+ def handleSilenced(isSilenced: Boolean): Unit
+}
+
+class GeneralOperations(
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ implicit val context: ActorContext
+ ) extends CommonSessionInterfacingFunctionality {
+ private[session] var progressBarValue: Option[Float] = None
+ private[session] var accessedContainer: Option[PlanetSideGameObject with Container] = None
+ private[session] var recentTeleportAttempt: Long = 0
+ private[session] var kitToBeUsed: Option[PlanetSideGUID] = None
+ // If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped.
+ private[session] var specialItemSlotGuid: Option[PlanetSideGUID] = None
+ private[session] val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap()
+ private[session] var heightLast: Float = 0f
+ private[session] var heightTrend: Boolean = false //up = true, down = false
+ private[session] var heightHistory: Float = 0f
+ private[session] var progressBarUpdate: Cancellable = Default.Cancellable
+ private var charSavedTimer: Cancellable = Default.Cancellable
+
+ /**
+ * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays.
+ * Intended to assist in sanitizing loadout information from the perspective of the player, or target owner.
+ * The equipment is expected to be unregistered and already fitted to their ultimate slot in the target container.
+ * @param player the player whose purchasing constraints are to be tested
+ * @param target the location in which the equipment will be stowed
+ * @param slots the equipment, in the standard object-slot format container
+ */
+ def applyPurchaseTimersBeforePackingLoadout(
+ player: Player,
+ target: PlanetSideServerObject with Container,
+ slots: List[InventoryItem]
+ ): Unit = {
+ slots.foreach { item =>
+ player.avatar.purchaseCooldown(item.obj.Definition) match {
+ case Some(_) => ()
+ case None if Avatar.purchaseCooldowns.contains(item.obj.Definition) =>
+ avatarActor ! AvatarActor.UpdatePurchaseTime(item.obj.Definition)
+ TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start))
+ case None =>
+ TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start))
+ }
+ }
+ }
+
+ def dropSpecialSlotItem(): Unit = {
+ specialItemSlotGuid.foreach { guid =>
+ specialItemSlotGuid = None
+ player.Carrying = None
+ (continent.GUID(guid) match {
+ case Some(llu: CaptureFlag) => Some((llu, llu.Carrier))
+ case _ => None
+ }) match {
+ case Some((llu, Some(carrier: Player)))
+ if carrier.GUID == player.GUID && !player.isAlive =>
+ player.LastDamage.foreach { damage =>
+ damage
+ .interaction
+ .adversarial
+ .map { _.attacker }
+ .collect {
+ case attacker
+ if attacker.Faction != player.Faction &&
+ System.currentTimeMillis() - llu.LastCollectionTime >= Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis =>
+ continent.AvatarEvents ! AvatarServiceMessage(
+ attacker.Name,
+ AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
+ )
+ }
+ }
+ continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
+ case Some((llu, Some(carrier: Player))) if carrier.GUID == player.GUID =>
+ continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
+ case Some((_, Some(carrier: Player))) =>
+ log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}")
+ case Some((_, None)) =>
+ log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.")
+ case None =>
+ log.warn(s"${player.toString} tried to drop a special item that wasn't recognized. GUID: $guid")
+ }
+ }
+ }
+
+ def setupProgressChange(rate: Float, finishedAction: () => Unit, stepAction: Float => Boolean): Unit = {
+ if (progressBarValue.isEmpty) {
+ progressBarValue = Some(-rate)
+ context.self ! CommonMessages.ProgressEvent(rate, finishedAction, stepAction)
+ }
+ }
+
+ /**
+ * Handle the message that indicates the level of completion of a process.
+ * The process is any form of user-driven activity with a certain eventual outcome
+ * but indeterminate progress feedback per cycle.
+ *
+ * This task is broken down into the "progression" from its initial state to the eventual outcome
+ * as is reported back to the player through some means of messaging window feedback.
+ * Though common in practice, this is not a requirement
+ * and the progress can accumulate without a user reportable method.
+ * To ensure that completion is reported properly,
+ * an exception is made that 99% completion is accounted uniquely
+ * before the final 100% is achieved.
+ * If the background process recording value is never set before running the initial operation
+ * or gets unset by failing a `tickAction` check
+ * the process is stopped.
+ * @see `progressBarUpdate`
+ * @see `progressBarValue`
+ * @see `essionActor.Progress`
+ * @param delta how much the progress changes each tick
+ * @param completionAction a custom action performed once the process is completed
+ * @param tickAction an optional action is is performed for each tick of progress;
+ * also performs a continuity check to determine if the process has been disrupted
+ */
+ def handleProgressChange(
+ delta: Float,
+ completionAction: () => Unit,
+ tickAction: Float => Boolean,
+ tick: Long
+ ): Unit = {
+ progressBarUpdate.cancel()
+ progressBarValue.foreach { value =>
+ val next = value + delta
+ if (value >= 100f) {
+ //complete
+ progressBarValue = None
+ tickAction(100)
+ completionAction()
+ } else if (value < 100f && next >= 100f) {
+ if (tickAction(99)) {
+ //will complete after this turn
+ progressBarValue = Some(next)
+ import scala.concurrent.ExecutionContext.Implicits.global
+ progressBarUpdate = context.system.scheduler.scheduleOnce(
+ delay = 100 milliseconds,
+ context.self,
+ CommonMessages.ProgressEvent(delta, completionAction, tickAction)
+ )
+ } else {
+ progressBarValue = None
+ }
+ } else {
+ if (tickAction(next)) {
+ //normal progress activity
+ progressBarValue = Some(next)
+ import scala.concurrent.ExecutionContext.Implicits.global
+ progressBarUpdate = context.system.scheduler.scheduleOnce(
+ tick.milliseconds,
+ context.self,
+ CommonMessages.ProgressEvent(delta, completionAction, tickAction, tick)
+ )
+ } else {
+ progressBarValue = None
+ }
+ }
+ }
+ }
+
+ /**
+ * For whatever container the character considers itself trying to access,
+ * initiate protocol to "access" it.
+ */
+ def accessContainer(container: Container): Unit = {
+ container match {
+ case v: Vehicle =>
+ accessVehicleContents(v)
+ case o: LockerContainer =>
+ accessGenericContainer(o)
+ case p: Player if p.isBackpack =>
+ accessCorpseContents(p)
+ case p: PlanetSideServerObject with Container =>
+ accessedContainer = Some(p)
+ case _ => ()
+ }
+ }
+
+ /**
+ * For the target container, initiate protocol to "access" it.
+ */
+ private def accessGenericContainer(container: PlanetSideServerObject with Container): Unit = {
+ accessedContainer = Some(container)
+ displayContainerContents(container.GUID, container.Inventory.Items)
+ }
+
+ /**
+ * Common preparation for interfacing with a vehicle trunk.
+ * Join a vehicle-specific group for shared updates.
+ * Construct every object in the vehicle's inventory for shared manipulation updates.
+ * @see `Container.Inventory`
+ * @see `GridInventory.Items`
+ * @param vehicle the vehicle
+ */
+ private def accessVehicleContents(vehicle: Vehicle): Unit = {
+ accessedContainer = Some(vehicle)
+ accessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString)
+ displayContainerContents(vehicle.GUID, vehicle.Inventory.Items)
+ }
+
+ /**
+ * Common preparation for interfacing with a corpse (former player's backpack).
+ * Join a corpse-specific group for shared updates.
+ * Construct every object in the player's hands and inventory for shared manipulation updates.
+ * @see `Container.Inventory`
+ * @see `GridInventory.Items`
+ * @see `Player.HolsterItems`
+ * @param tplayer the corpse
+ */
+ private def accessCorpseContents(tplayer: Player): Unit = {
+ accessedContainer = Some(tplayer)
+ accessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString)
+ displayContainerContents(tplayer.GUID, tplayer.HolsterItems())
+ displayContainerContents(tplayer.GUID, tplayer.Inventory.Items)
+ }
+
+ /**
+ * Join an entity-specific group for shared updates.
+ * @param events the event system bus to which to subscribe
+ * @param channel the channel name
+ */
+ private def accessContainerChannel(events: ActorRef, channel: String): Unit = {
+ events ! Service.Join(channel)
+ }
+
+ /**
+ * Depict the contents of a container by building them in the local client
+ * in their container as a group of detailed entities.
+ * @see `ObjectCreateDetailedMessage`
+ * @see `ObjectCreateMessageParent`
+ * @see `PacketConverter.DetailedConstructorData`
+ * @param containerId the container's unique identifier
+ * @param items a list of the entities to be depicted
+ */
+ private def displayContainerContents(containerId: PlanetSideGUID, items: Iterable[InventoryItem]): Unit = {
+ items.foreach(entry => {
+ val obj = entry.obj
+ val objDef = obj.Definition
+ sendResponse(
+ ObjectCreateDetailedMessage(
+ objDef.ObjectId,
+ obj.GUID,
+ ObjectCreateMessageParent(containerId, entry.start),
+ objDef.Packet.DetailedConstructorData(obj).get
+ )
+ )
+ })
+ }
+
+ /**
+ * For whatever container the character considers itself accessing,
+ * initiate protocol to release it from "access".
+ */
+ def unaccessContainer(): Unit = {
+ accessedContainer.foreach { container => unaccessContainer(container) }
+ }
+
+ /**
+ * For the target container, initiate protocol to release it from "access".
+ */
+ def unaccessContainer(container: Container): Unit = {
+ container match {
+ case v: Vehicle =>
+ unaccessVehicleContainer(v)
+ case o: LockerContainer =>
+ unaccessGenericContainer(o)
+ avatarActor ! AvatarActor.SaveLocker()
+ case p: Player if p.isBackpack =>
+ unaccessCorpseContainer(p)
+ case _: PlanetSideServerObject with Container =>
+ accessedContainer = None
+ case _ => ()
+ }
+ }
+
+ private def unaccessGenericContainer(container: Container): Unit = {
+ accessedContainer = None
+ hideContainerContents(container.Inventory.Items)
+ }
+
+ /**
+ * Common preparation for disengaging from a vehicle.
+ * Leave the vehicle-specific group that was used for shared updates.
+ * Deconstruct every object in the vehicle's inventory.
+ * @param vehicle the vehicle
+ */
+ private def unaccessVehicleContainer(vehicle: Vehicle): Unit = {
+ accessedContainer = None
+ if (vehicle.AccessingTrunk.contains(player.GUID)) {
+ vehicle.AccessingTrunk = None
+ }
+ unaccessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString)
+ hideContainerContents(vehicle.Inventory.Items)
+ }
+
+ /**
+ * Common preparation for disengaging from a corpse.
+ * Leave the corpse-specific group that was used for shared updates.
+ * Deconstruct every object in the backpack's inventory.
+ * @param tplayer the corpse
+ */
+ private def unaccessCorpseContainer(tplayer: Player): Unit = {
+ accessedContainer = None
+ unaccessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString)
+ hideContainerContents(tplayer.HolsterItems())
+ hideContainerContents(tplayer.Inventory.Items)
+ }
+
+ /**
+ * Leave an entity-specific group for shared updates.
+ * @param events the event system bus to which to subscribe
+ * @param channel the channel name
+ */
+ private def unaccessContainerChannel(events: ActorRef, channel: String): Unit = {
+ events ! Service.Leave(Some(channel))
+ }
+
+ /**
+ * Forget the contents of a container by deleting that content from the local client.
+ * @see `InventoryItem`
+ * @see `ObjectDeleteMessage`
+ * @param items a list of the entities to be depicted
+ */
+ private def hideContainerContents(items: List[InventoryItem]): Unit = {
+ items.foreach { entry =>
+ sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
+ }
+ }
+
+ /**
+ * Check two locations for a controlled piece of equipment that is associated with the `player`.
+ *
+ * The first location is dependent on whether the avatar is in a vehicle.
+ * Some vehicle seats may have a "controlled weapon" which counts as the first location to be checked.
+ * The second location is dependent on whether the avatar has a raised hand.
+ * That is only possible if the player has something in their hand at the moment, hence the second location.
+ * Players do have a concept called a "last drawn slot" (hand) but that former location is not eligible.
+ *
+ * Along with any discovered item, a containing object such that the statement:
+ * `container.Find(object) = Some(slot)`
+ * ... will return a proper result.
+ * For a mount controlled weapon, the vehicle is returned.
+ * For the player's hand, the player is returned.
+ * @return a `Tuple` of the returned values;
+ * the first value is a `Container` object;
+ * the second value is an `Equipment` object in the former
+ */
+ def findContainedEquipment(): (Option[PlanetSideGameObject with Container], Set[Equipment]) = {
+ continent.GUID(player.VehicleSeated) match {
+ case Some(vehicle: Mountable with MountableWeapons with Container) =>
+ vehicle.PassengerInSeat(player) match {
+ case Some(seatNum) =>
+ (Some(vehicle), vehicle.WeaponControlledFromSeat(seatNum))
+ case None =>
+ (None, Set.empty)
+ }
+ case _ =>
+ player.Slot(player.DrawnSlot).Equipment match {
+ case Some(a) =>
+ (Some(player), Set(a))
+ case _ =>
+ (None, Set.empty)
+ }
+ }
+ }
+
+ /**
+ * Check two locations for a controlled piece of equipment that is associated with the `player`
+ * and has the specified global unique identifier number.
+ */
+ def findContainedEquipment(
+ guid: PlanetSideGUID
+ ): (Option[PlanetSideGameObject with Container], Set[Equipment]) = {
+ val (o, equipment) = findContainedEquipment()
+ equipment.find { _.GUID == guid } match {
+ case Some(equip) => (o, Set(equip))
+ case None => (None, Set.empty)
+ }
+ }
+
+ /**
+ * Drop an `Equipment` item onto the ground.
+ * Specifically, instruct the item where it will appear,
+ * add it to the list of items that are visible to multiple users,
+ * and then inform others that the item has been dropped.
+ * @param obj a `Container` object that represents where the item will be dropped;
+ * curried for callback
+ * @param zone the continent in which the item is being dropped;
+ * curried for callback
+ * @param item the item
+ */
+ def normalItemDrop(obj: PlanetSideServerObject with Container, zone: Zone)(item: Equipment): Unit = {
+ zone.Ground.tell(Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)), obj.Actor)
+ }
+
+ /**
+ * Given an object globally unique identifier, search in a given location for it.
+ * @param objectGuid the object
+ * @param parent a `Container` object wherein to search
+ * @return an optional tuple that contains two values;
+ * the first value is the container that matched correctly with the object's GUID;
+ * the second value is the slot position of the object
+ */
+ def findInLocalContainer(
+ objectGuid: PlanetSideGUID
+ )(parent: PlanetSideServerObject with Container): Option[(PlanetSideServerObject with Container, Option[Int])] = {
+ parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) }
+ }
+
+ /**
+ * na
+ * @param targetGuid na
+ * @param unk1 na
+ * @param unk2 na
+ */
+ def hackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = {
+ sendResponse(HackMessage(unk1=0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1, HackState.Hacked, unk2))
+ }
+
+ /**
+ * Send a PlanetsideAttributeMessage packet to the client
+ * @param targetGuid The target of the attribute
+ * @param attributeNumber The attribute number
+ * @param attributeValue The attribute value
+ */
+ def sendPlanetsideAttributeMessage(
+ targetGuid: PlanetSideGUID,
+ attributeNumber: PlanetsideAttributeEnum,
+ attributeValue: Long
+ ): Unit = {
+ sendResponse(PlanetsideAttributeMessage(targetGuid, attributeNumber, attributeValue))
+ }
+
+ /**
+ * The player has lost the will to live and must be killed.
+ * @see `Vitality`
+ * `PlayerSuicide`
+ * @param tplayer the player to be killed
+ */
+ def suicide(tplayer: Player): Unit = {
+ tplayer.LogActivity(PlayerSuicide(PlayerSource(tplayer)))
+ tplayer.Actor ! Player.Die()
+ }
+
+ /**
+ * Initialize the deployables user interface elements.
+ *
+ * All element initializations require both the maximum deployable amount and the current deployables active counts.
+ * Until initialized, all elements will be RED 0/0 as if the corresponding certification were not `learn`ed.
+ * The respective element will become a pair of numbers, the second always being non-zero, when properly initialized.
+ * The numbers will appear GREEN when more deployables of that type can be placed.
+ * The numbers will appear RED if the player can not place any more of that type of deployable.
+ * The numbers will appear YELLOW if the current deployable count is greater than the maximum count of that type
+ * such as may be the case when a player `forget`s a certification.
+ * @param list a tuple of each UI element with four numbers;
+ * even numbers are attribute ids;
+ * odd numbers are quantities;
+ * first pair is current quantity;
+ * second pair is maximum quantity
+ */
+ def updateDeployableUIElements(list: List[(Int, Int, Int, Int)]): Unit = {
+ val guid = PlanetSideGUID(0)
+ list.foreach {
+ case (currElem, curr, maxElem, max) =>
+ //fields must update in ordered pairs: max, curr
+ sendResponse(PlanetsideAttributeMessage(guid, maxElem, max))
+ sendResponse(PlanetsideAttributeMessage(guid, currElem, curr))
+ }
+ }
+
+ /**
+ * Animate(?) a player using a fully-linked Router teleportation system.
+ * In reality, this seems to do nothing visually?
+ * @param playerGUID the player being teleported
+ * @param srcGUID the origin of the teleportation
+ * @param destGUID the destination of the teleportation
+ */
+ def useRouterTelepadEffect(playerGUID: PlanetSideGUID, srcGUID: PlanetSideGUID, destGUID: PlanetSideGUID): Unit = {
+ sendResponse(PlanetsideAttributeMessage(playerGUID, 64, 1)) //what does this do?
+ sendResponse(GenericObjectActionMessage(srcGUID, 31))
+ sendResponse(GenericObjectActionMessage(destGUID, 32))
+ }
+
+ /**
+ * Attempt to link the router teleport system using the provided terminal information.
+ * Although additional states are necessary to properly use the teleportation system,
+ * e.g., deployment state, active state of the endpoints, etc.,
+ * this decision is not made factoring those other conditions.
+ * @param router the vehicle that houses one end of the teleportation system (the `InternalTelepad` object)
+ * @param systemPlan specific object identification of the two endpoints of the teleportation system;
+ * if absent, the knowable endpoint is deleted from the client reflexively
+ */
+ def toggleTeleportSystem(router: Vehicle, systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)]): Unit = {
+ systemPlan match {
+ case Some((internalTelepad, remoteTelepad)) =>
+ internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad
+ TelepadLike.StartRouterInternalTelepad(continent, router.GUID, internalTelepad)
+ TelepadLike.LinkTelepad(continent, remoteTelepad.GUID)
+ case _ =>
+ router.Utility(UtilityType.internal_router_telepad_deployable) match {
+ case Some(util: Utility.InternalTelepad) =>
+ sendResponse(ObjectDeleteMessage(util.GUID, 0))
+ case _ => ()
+ }
+ }
+ }
+
+ def toggleMaxSpecialState(enable: Boolean): Unit = {
+ if (player.ExoSuit == ExoSuitType.MAX) {
+ if (enable && player.UsingSpecial == SpecialExoSuitDefinition.Mode.Normal) {
+ player.Faction match {
+ case PlanetSideEmpire.TR if player.Capacitor == player.ExoSuitDef.MaxCapacitor =>
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Overdrive
+ activateMaxSpecialStateMessage()
+ case PlanetSideEmpire.NC if player.Capacitor > 0 =>
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded
+ activateMaxSpecialStateMessage()
+ case PlanetSideEmpire.VS =>
+ log.warn(s"${player.Name} tried to use a MAX special ability but their faction doesn't have one")
+ case _ => ()
+ }
+ } else {
+ player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 0)
+ )
+ }
+ }
+ }
+
+ private def activateMaxSpecialStateMessage(): Unit = {
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 1)
+ )
+ }
+
+ def administrativeKick(tplayer: Player): Unit = {
+ log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
+ tplayer.death_by = -1
+ sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name)
+ //get out of that vehicle
+ sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match {
+ case (Some(obj), Some(seatNum)) =>
+ tplayer.VehicleSeated = None
+ obj.Seats(seatNum).unmount(tplayer)
+ continent.VehicleEvents ! VehicleServiceMessage(
+ continent.id,
+ VehicleAction.KickPassenger(tplayer.GUID, seatNum, unk2=false, obj.GUID)
+ )
+ case _ => ()
+ }
+ }
+
+ def fallHeightTracker(zHeight: Float): Unit = {
+ if ((heightTrend && heightLast - zHeight >= 0.5f) ||
+ (!heightTrend && zHeight - heightLast >= 0.5f)) {
+ heightTrend = !heightTrend
+ heightHistory = zHeight
+ }
+ heightLast = zHeight
+ }
+
+ def canSeeReallyFar: Boolean = {
+ sessionLogic.shooting.FindContainedWeapon match {
+ case (Some(_: Vehicle), weapons) if weapons.nonEmpty =>
+ player.avatar
+ .implants
+ .exists { p =>
+ p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty
+ }
+ case (Some(_: Player), weapons) if weapons.nonEmpty =>
+ val wep = weapons.head
+ wep.Definition == GlobalDefinitions.bolt_driver ||
+ wep.Definition == GlobalDefinitions.heavy_sniper ||
+ (
+ (wep.Projectile ne GlobalDefinitions.no_projectile) &&
+ player.Crouching &&
+ player.avatar
+ .implants
+ .exists { p =>
+ p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty
+ }
+ )
+ case _ =>
+ false
+ }
+ }
+
+ def displayCharSavedMsgThenRenewTimer(fixedLen: Long, varLen: Long): Unit = {
+ charSaved()
+ renewCharSavedTimer(fixedLen, varLen)
+ }
+
+ def renewCharSavedTimer(fixedLen: Long, varLen: Long): Unit = {
+ charSavedTimer.cancel()
+ val delay = (fixedLen + (varLen * scala.math.random()).toInt).seconds
+ charSavedTimer = context.system.scheduler.scheduleOnce(delay, context.self, SessionActor.CharSavedMsg)
+ }
+
+ def charSaved(): Unit = {
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None))
+ }
+
+ override protected[session] def actionsToCancel(): Unit = {
+ progressBarValue = None
+ kitToBeUsed = None
+ collisionHistory.clear()
+ accessedContainer match {
+ case Some(v: Vehicle) =>
+ val vguid = v.GUID
+ sessionLogic.vehicles.ConditionalDriverVehicleControl(v)
+ if (v.AccessingTrunk.contains(player.GUID)) {
+ if (player.VehicleSeated.contains(vguid)) {
+ v.AccessingTrunk = None //player is seated; just stop accessing trunk
+ if (player.isAlive) {
+ sendResponse(UnuseItemMessage(player.GUID, vguid))
+ }
+ } else {
+ unaccessContainer(v)
+ }
+ }
+
+ case Some(o) =>
+ unaccessContainer(o)
+ if (player.isAlive) {
+ sendResponse(UnuseItemMessage(player.GUID, o.GUID))
+ }
+
+ case None => ()
+ }
+ }
+
+ override protected[session] def stop(): Unit = {
+ progressBarUpdate.cancel()
+ charSavedTimer.cancel()
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala b/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala
new file mode 100644
index 000000000..6c5040057
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala
@@ -0,0 +1,30 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.support
+
+import akka.actor.Actor.Receive
+import akka.actor.ActorRef
+import net.psforever.objects.Session
+
+trait ModeLogic {
+ def avatarResponse: AvatarHandlerFunctions
+ def chat: ChatFunctions
+ def galaxy: GalaxyHandlerFunctions
+ def general: GeneralFunctions
+ def local: LocalHandlerFunctions
+ def mountResponse: MountHandlerFunctions
+ def squad: SquadHandlerFunctions
+ def shooting: WeaponAndProjectileFunctions
+ def terminals: TerminalHandlerFunctions
+ def vehicles: VehicleFunctions
+ def vehicleResponse: VehicleHandlerFunctions
+
+ def switchTo(session: Session): Unit = { /* to override */ }
+
+ def switchFrom(session: Session): Unit = { /* to override */ }
+
+ def parse(sender: ActorRef): Receive
+}
+
+trait PlayerMode {
+ def setup(data: SessionData): ModeLogic
+}
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
index b3aefad4d..3e0e7f9d8 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
@@ -1,594 +1,37 @@
// Copyright (c) 2023 PSForever
package net.psforever.actors.session.support
-import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, typed}
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.packet.game.objectcreate.ConstructorData
-import net.psforever.services.Service
import net.psforever.objects.zones.exp
import scala.collection.mutable
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.duration._
//
-import net.psforever.actors.session.{AvatarActor, ChatActor}
-import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
-import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
-import net.psforever.objects.inventory.InventoryItem
-import net.psforever.objects.serverobject.pad.VehicleSpawnPad
-import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
-import net.psforever.objects.vital.etc.ExplodingEntityReason
-import net.psforever.objects.zones.Zoning
-import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle}
+import net.psforever.actors.session.AvatarActor
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.packet.game._
-import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage}
-import net.psforever.services.{InterstellarClusterService => ICS}
+import net.psforever.services.avatar.AvatarResponse
import net.psforever.types._
import net.psforever.util.Config
-import net.psforever.zones.Zones
+
+trait AvatarHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ val ops: SessionAvatarHandlers
+
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit
+}
class SessionAvatarHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- chatActor: typed.ActorRef[ChatActor.Command],
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
//TODO player characters only exist within a certain range of GUIDs for a given zone; this is overkill
- private[support] var lastSeenStreamMessage: mutable.LongMap[SessionAvatarHandlers.LastUpstream] =
+ private[session] var lastSeenStreamMessage: mutable.LongMap[SessionAvatarHandlers.LastUpstream] =
mutable.LongMap[SessionAvatarHandlers.LastUpstream]()
- private[this] val hidingPlayerRandomizer = new scala.util.Random
+ private[session] val hidingPlayerRandomizer = new scala.util.Random
- /**
- * na
- * @param toChannel na
- * @param guid na
- * @param reply na
- */
- def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = {
- val resolvedPlayerGuid = if (player != null && player.HasGUID) {
- player.GUID
- } else {
- Service.defaultPlayerGUID
- }
- val isNotSameTarget = resolvedPlayerGuid != guid
- val isSameTarget = !isNotSameTarget
- reply match {
- /* special messages */
- case AvatarResponse.TeardownConnection() =>
- log.trace(s"ending ${player.Name}'s old session by event system request (relog)")
- context.stop(context.self)
-
- /* really common messages (very frequently, every life) */
- case pstate @ AvatarResponse.PlayerState(
- pos,
- vel,
- yaw,
- pitch,
- yawUpper,
- _,
- isCrouching,
- isJumping,
- jumpThrust,
- isCloaking,
- isNotRendered,
- canSeeReallyFar
- ) if isNotSameTarget =>
- val pstateToSave = pstate.copy(timestamp = 0)
- val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = lastSeenStreamMessage.get(guid.guid) match {
- case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting)
- case _ => (None, 0L, Vector3.Zero, false, None)
- }
- val drawConfig = Config.app.game.playerDraw //m
- val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m
- val ourPosition = player.Position //xyz
- val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m
- val inDrawableRange = currentDistance <= maxRange
- val now = System.currentTimeMillis() //ms
- if (
- sessionData.zoning.zoningStatus != Zoning.Status.Deconstructing &&
- !isNotRendered && inDrawableRange
- ) {
- //conditions where visibility is assured
- val durationSince = now - lastTime //ms
- lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange
- lazy val targetDelay = {
- val populationOver = math.max(
- 0,
- sessionData.localSector.livePlayerList.size - drawConfig.populationThreshold
- )
- val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m
- val adjustedDistance = currentDistance + distanceAdjustment //sq.m
- drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match {
- case -1 => 1
- case index => drawConfig.delays(index)
- }
- } //ms
- if (!wasVisible ||
- !previouslyInDrawableRange ||
- durationSince > drawConfig.delayMax ||
- (!lastMsg.contains(pstateToSave) &&
- (canSeeReallyFar ||
- currentDistance < drawConfig.rangeMin * drawConfig.rangeMin ||
- sessionData.canSeeReallyFar ||
- durationSince > targetDelay
- )
- )
- ) {
- //must draw
- sendResponse(
- PlayerStateMessage(
- guid,
- pos,
- vel,
- yaw,
- pitch,
- yawUpper,
- timestamp = 0, //is this okay?
- isCrouching,
- isJumping,
- jumpThrust,
- isCloaking
- )
- )
- lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now))
- } else {
- //is visible, but skip reinforcement
- lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime))
- }
- } else {
- //conditions where the target is not currently visible
- if (wasVisible) {
- //the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance
- val lat = (1 + hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat
- sendResponse(
- PlayerStateMessage(
- guid,
- Vector3(1f, lat, 1f),
- vel=None,
- facingYaw=0f,
- facingPitch=0f,
- facingYawUpper=0f,
- timestamp=0, //is this okay?
- is_cloaked = isCloaking
- )
- )
- lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now))
- } else {
- //skip drawing altogether
- lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime))
- }
- }
-
- case AvatarResponse.ObjectHeld(slot, _)
- if isSameTarget && player.VisibleSlots.contains(slot) =>
- sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
- //Stop using proximity terminals if player unholsters a weapon
- continent.GUID(sessionData.terminals.usingMedicalTerminal).collect {
- case term: Terminal with ProximityUnit => sessionData.terminals.StopUsingProximityUnit(term)
- }
- if (sessionData.zoning.zoningStatus == Zoning.Status.Deconstructing) {
- sessionData.stopDeconstructing()
- }
-
- case AvatarResponse.ObjectHeld(slot, _)
- if isSameTarget && slot > -1 =>
- sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
-
- case AvatarResponse.ObjectHeld(_, _)
- if isSameTarget => ()
-
- case AvatarResponse.ObjectHeld(_, previousSlot) =>
- sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false))
-
- case AvatarResponse.ChangeFireState_Start(weaponGuid)
- if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
- sendResponse(ChangeFireStateMessage_Start(weaponGuid))
- val entry = lastSeenStreamMessage(guid.guid)
- lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid)))
-
- case AvatarResponse.ChangeFireState_Start(weaponGuid)
- if isNotSameTarget =>
- sendResponse(ChangeFireStateMessage_Start(weaponGuid))
-
- case AvatarResponse.ChangeFireState_Stop(weaponGuid)
- if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } =>
- sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
- val entry = lastSeenStreamMessage(guid.guid)
- lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None))
-
- case AvatarResponse.ChangeFireState_Stop(weaponGuid)
- if isNotSameTarget =>
- sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
-
- case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget =>
- sendResponse(pkt)
-
- case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget =>
- sendResponse(pkt)
-
- case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget =>
- sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
-
- case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) =>
- sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
-
- case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
- sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
-
- case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
- sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
-
- case AvatarResponse.HitHint(sourceGuid) if player.isAlive =>
- sendResponse(HitHint(sourceGuid, guid))
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
-
- case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
- if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
- sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
-
- case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
- // guid = victim // killer = killer
- sendResponse(DestroyMessage(victim, killer, weapon, pos))
-
- case AvatarResponse.DestroyDisplay(killer, victim, method, unk) =>
- sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
-
- case AvatarResponse.TerminalOrderResult(terminalGuid, action, result)
- if result && (action == TransactionType.Buy || action == TransactionType.Loadout) =>
- sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
- sessionData.terminals.lastTerminalOrderFulfillment = true
- AvatarActor.savePlayerData(player)
- sessionData.renewCharSavedTimer(
- Config.app.game.savedMsg.interruptedByAction.fixed,
- Config.app.game.savedMsg.interruptedByAction.variable
- )
-
- case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
- sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
- sessionData.terminals.lastTerminalOrderFulfillment = true
-
- case AvatarResponse.ChangeExosuit(
- target,
- armor,
- exosuit,
- subtype,
- _,
- maxhand,
- oldHolsters,
- holsters,
- oldInventory,
- inventory,
- drop,
- delete
- ) if resolvedPlayerGuid == target =>
- sendResponse(ArmorChangedMessage(target, exosuit, subtype))
- sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
- //happening to this player
- //cleanup
- sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false))
- (oldHolsters ++ oldInventory ++ delete).foreach {
- case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0))
- }
- //functionally delete
- delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) }
- //redraw
- if (maxhand) {
- TaskWorkflow.execute(HoldNewEquipmentUp(player)(
- Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
- 0
- ))
- }
- //draw free hand
- player.FreeHand.Equipment.foreach { obj =>
- val definition = obj.Definition
- sendResponse(
- ObjectCreateDetailedMessage(
- definition.ObjectId,
- obj.GUID,
- ObjectCreateMessageParent(target, Player.FreeHandSlot),
- definition.Packet.DetailedConstructorData(obj).get
- )
- )
- }
- //draw holsters and inventory
- (holsters ++ inventory).foreach {
- case InventoryItem(obj, index) =>
- val definition = obj.Definition
- sendResponse(
- ObjectCreateDetailedMessage(
- definition.ObjectId,
- obj.GUID,
- ObjectCreateMessageParent(target, index),
- definition.Packet.DetailedConstructorData(obj).get
- )
- )
- }
- DropLeftovers(player)(drop)
-
- case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, _, delete) =>
- sendResponse(ArmorChangedMessage(target, exosuit, subtype))
- sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
- //happening to some other player
- sendResponse(ObjectHeldMessage(target, slot, unk1 = false))
- //cleanup
- (oldHolsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
- //draw holsters
- holsters.foreach {
- case InventoryItem(obj, index) =>
- val definition = obj.Definition
- sendResponse(
- ObjectCreateMessage(
- definition.ObjectId,
- obj.GUID,
- ObjectCreateMessageParent(target, index),
- definition.Packet.ConstructorData(obj).get
- )
- )
- }
-
- case AvatarResponse.ChangeLoadout(
- target,
- armor,
- exosuit,
- subtype,
- _,
- maxhand,
- oldHolsters,
- holsters,
- oldInventory,
- inventory,
- drops
- ) if resolvedPlayerGuid == target =>
- sendResponse(ArmorChangedMessage(target, exosuit, subtype))
- sendResponse(PlanetsideAttributeMessage(target, attribute_type = 4, armor))
- //happening to this player
- sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true))
- //cleanup
- (oldHolsters ++ oldInventory).foreach {
- case (obj, objGuid) =>
- sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
- TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
- }
- drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
- //redraw
- if (maxhand) {
- TaskWorkflow.execute(HoldNewEquipmentUp(player)(
- Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
- slot = 0
- ))
- }
- sessionData.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
- DropLeftovers(player)(drops)
-
- case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) =>
- //redraw handled by callbacks
- sendResponse(ArmorChangedMessage(target, exosuit, subtype))
- sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
- //happening to some other player
- sendResponse(ObjectHeldMessage(target, slot, unk1=false))
- //cleanup
- oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
-
- case AvatarResponse.UseKit(kguid, kObjId) =>
- sendResponse(
- UseItemMessage(
- resolvedPlayerGuid,
- kguid,
- resolvedPlayerGuid,
- unk2 = 4294967295L,
- unk3 = false,
- unk4 = Vector3.Zero,
- unk5 = Vector3.Zero,
- unk6 = 126,
- unk7 = 0, //sequence time?
- unk8 = 137,
- kObjId
- )
- )
- sendResponse(ObjectDeleteMessage(kguid, unk1=0))
-
- case AvatarResponse.KitNotUsed(_, "") =>
- sessionData.kitToBeUsed = None
-
- case AvatarResponse.KitNotUsed(_, msg) =>
- sessionData.kitToBeUsed = None
- sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
-
- case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
- avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
-
- case AvatarResponse.AwardBep(charId, bep, expType) =>
- //if the target player, always award (some) BEP
- if (charId == player.CharId) {
- avatarActor ! AvatarActor.AwardBep(bep, expType)
- }
-
- case AvatarResponse.AwardCep(charId, cep) =>
- //if the target player, always award (some) CEP
- if (charId == player.CharId) {
- avatarActor ! AvatarActor.AwardCep(cep)
- }
-
- case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
- facilityCaptureRewards(buildingId, zoneNumber, cep)
-
- case AvatarResponse.SendResponse(msg) =>
- sendResponse(msg)
-
- case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid =>
- sendResponse(msg)
-
- /* common messages (maybe once every respawn) */
- case AvatarResponse.Reload(itemGuid)
- if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
- sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
-
- case AvatarResponse.Killed(mount) =>
- //log and chat messages
- val cause = player.LastDamage.flatMap { damage =>
- val interaction = damage.interaction
- val reason = interaction.cause
- val adversarial = interaction.adversarial.map { _.attacker }
- reason match {
- case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] =>
- //also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
- sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
- case _ => ()
- }
- adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
- }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
- log.info(s"${player.Name} has died, killed by $cause")
- if (sessionData.shooting.shotsWhileDead > 0) {
- log.warn(
- s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionData.shooting.shotsWhileDead} rounds while character was dead on server"
- )
- sessionData.shooting.shotsWhileDead = 0
- }
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel")
- sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
-
- //player state changes
- AvatarActor.updateToolDischargeFor(avatar)
- player.FreeHand.Equipment.foreach { item =>
- DropEquipmentFromInventory(player)(item)
- }
- sessionData.dropSpecialSlotItem()
- sessionData.toggleMaxSpecialState(enable = false)
- sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
- sessionData.zoning.zoningStatus = Zoning.Status.None
- sessionData.zoning.spawn.deadState = DeadState.Dead
- continent.GUID(mount).collect { case obj: Vehicle =>
- sessionData.vehicles.ConditionalDriverVehicleControl(obj)
- sessionData.unaccessContainer(obj)
- }
- sessionData.playerActionsToCancel()
- sessionData.terminals.CancelAllProximityUnits()
- AvatarActor.savePlayerLocation(player)
- sessionData.zoning.spawn.shiftPosition = Some(player.Position)
-
- //respawn
- val respawnTimer = 300.seconds
- sessionData.zoning.spawn.reviveTimer.cancel()
- if (player.death_by == 0) {
- sessionData.zoning.spawn.reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) {
- sessionData.cluster ! ICS.GetRandomSpawnPoint(
- Zones.sanctuaryZoneNumber(player.Faction),
- player.Faction,
- Seq(SpawnGroup.Sanctuary),
- context.self
- )
- }
- } else {
- sessionData.zoning.spawn.HandleReleaseAvatar(player, continent)
- }
-
- case AvatarResponse.Release(tplayer) if isNotSameTarget =>
- sessionData.zoning.spawn.DepictPlayerAsCorpse(tplayer)
-
- case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid =>
- log.info(s"No time for rest, ${player.Name}. Back on your feet!")
- sessionData.zoning.spawn.reviveTimer.cancel()
- sessionData.zoning.spawn.deadState = DeadState.Alive
- player.Revive
- val health = player.Health
- sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health))
- sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true))
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health)
- )
-
- /* uncommon messages (utility, or once in a while) */
- case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
- if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
- changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
- sendResponse(ChangeAmmoMessage(weapon_guid, 1))
-
- case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
- if isNotSameTarget =>
- changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
-
- case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget =>
- sendResponse(ChangeFireModeMessage(itemGuid, mode))
-
- case AvatarResponse.ConcealPlayer() =>
- sendResponse(GenericObjectActionMessage(guid, code=9))
-
- case AvatarResponse.EnvironmentalDamage(_, _, _) =>
- //TODO damage marker?
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
-
- case AvatarResponse.DropItem(pkt) if isNotSameTarget =>
- sendResponse(pkt)
-
- case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget =>
- sendResponse(ObjectDeleteMessage(itemGuid, unk))
-
- /* rare messages */
- case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget =>
- sendResponse(SetEmpireMessage(objectGuid, faction))
-
- case AvatarResponse.DropSpecialItem() =>
- sessionData.dropSpecialSlotItem()
-
- case AvatarResponse.OxygenState(player, vehicle) =>
- sendResponse(OxygenStateMessage(
- DrowningTarget(player.guid, player.progress, player.state),
- vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) }
- ))
-
- case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget =>
- sendResponse(pkt)
-
- case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget =>
- sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid))
-
- case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) =>
- sendResponse(
- ProjectileStateMessage(
- projectileGuid,
- projectile.Position,
- shot_vel = Vector3.Zero,
- projectile.Orientation,
- sequence_num=0,
- end=true,
- hit_target_guid=PlanetSideGUID(0)
- )
- )
- sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2))
-
- case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
- sendResponse(GenericActionMessage(mode))
-
- case AvatarResponse.PutDownFDU(target) if isNotSameTarget =>
- sendResponse(GenericObjectActionMessage(target, code=53))
-
- case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget =>
- val definition = item.Definition
- sendResponse(
- ObjectCreateDetailedMessage(
- definition.ObjectId,
- item.GUID,
- ObjectCreateMessageParent(target, slot),
- definition.Packet.DetailedConstructorData(item).get
- )
- )
-
- case AvatarResponse.WeaponDryFire(weaponGuid)
- if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
- continent.GUID(weaponGuid).collect {
- case tool: Tool if tool.Magazine == 0 =>
- // check that the magazine is still empty before sending WeaponDryFireMessage
- // if it has been reloaded since then, other clients will not see it firing
- sendResponse(WeaponDryFireMessage(weaponGuid))
- }
-
- case _ => ()
- }
- }
-
- private def changeAmmoProcedures(
+ def changeAmmoProcedures(
weaponGuid: PlanetSideGUID,
previousAmmoGuid: PlanetSideGUID,
ammoTypeId: Int,
@@ -608,11 +51,11 @@ class SessionAvatarHandlers(
)
}
- private def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
+ def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
//TODO squad services deactivated, participation trophy rewards for now - 11-20-2023
//must be in a squad to earn experience
val charId = player.CharId
- val squadUI = sessionData.squad.squadUI
+ val squadUI = sessionLogic.squad.squadUI
val participation = continent
.Building(buildingId)
.map { building =>
@@ -672,10 +115,49 @@ class SessionAvatarHandlers(
Some(modifiedExp)
}
}
+
+ /**
+ * Properly format a `DestroyDisplayMessage` packet
+ * given sufficient information about a target (victim) and an actor (killer).
+ * For the packet, the `charId` field is important for determining distinction between players.
+ * @param killer the killer's entry
+ * @param victim the victim's entry
+ * @param method the manner of death
+ * @param unk na;
+ * defaults to 121, the object id of `avatar`
+ * @return a `DestroyDisplayMessage` packet that is properly formatted
+ */
+ def destroyDisplayMessage(
+ killer: SourceEntry,
+ victim: SourceEntry,
+ method: Int,
+ unk: Int = 121
+ ): DestroyDisplayMessage = {
+ val killerSeated = killer match {
+ case obj: PlayerSource => obj.Seated
+ case _ => false
+ }
+ val victimSeated = victim match {
+ case obj: PlayerSource => obj.Seated
+ case _ => false
+ }
+ new DestroyDisplayMessage(
+ killer.Name,
+ killer.CharId,
+ killer.Faction,
+ killerSeated,
+ unk,
+ method,
+ victim.Name,
+ victim.CharId,
+ victim.Faction,
+ victimSeated
+ )
+ }
}
object SessionAvatarHandlers {
- private[support] case class LastUpstream(
+ private[session] case class LastUpstream(
msg: Option[AvatarResponse.PlayerState],
visible: Boolean,
shooting: Option[PlanetSideGUID],
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
index 0143400f8..5c40651fb 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
@@ -1,79 +1,68 @@
// Copyright (c) 2023 PSForever
package net.psforever.actors.session.support
+import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.adapter._
-import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
-import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
-import net.psforever.objects.vital.etc.SuicideReason
-import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
+import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.services.chat.ChatService
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
import scala.concurrent.duration._
//
import net.psforever.actors.net.MiddlewareActor
-import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor
-import net.psforever.login.WorldSession._
import net.psforever.objects._
import net.psforever.objects.avatar._
-import net.psforever.objects.ballistics._
import net.psforever.objects.ce._
-import net.psforever.objects.definition._
-import net.psforever.objects.entity.WorldEntity
import net.psforever.objects.equipment._
-import net.psforever.objects.guid._
import net.psforever.objects.inventory.{Container, InventoryItem}
-import net.psforever.objects.locker.LockerContainer
-import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.serverobject.deploy.Deployment
-import net.psforever.objects.serverobject.doors.Door
-import net.psforever.objects.serverobject.generator.Generator
-import net.psforever.objects.serverobject.llu.CaptureFlag
-import net.psforever.objects.serverobject.locks.IFFLock
-import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.mount.Mountable
-import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
-import net.psforever.objects.serverobject.structures.{Amenity, Building, WarpGate}
-import net.psforever.objects.serverobject.terminals._
-import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
-import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
-import net.psforever.objects.serverobject.tube.SpawnTube
-import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject}
-import net.psforever.objects.vehicles.Utility.InternalTelepad
+import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.vehicles._
import net.psforever.objects.vital._
-import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones._
-import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity}
+import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity, SectorGroup, SectorPopulation}
+import net.psforever.services.ServiceManager
+import net.psforever.services.ServiceManager.Lookup
import net.psforever.packet._
-import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, SetChatFilterMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage}
-import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
-import net.psforever.packet.game.objectcreate._
import net.psforever.packet.game._
-import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
-import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
-import net.psforever.services.local.support.CaptureFlagManager
-import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.services.account.AccountPersistenceService
import net.psforever.services.ServiceManager.LookupResult
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
-import net.psforever.services.{RemoverActor, Service, InterstellarClusterService => ICS}
+import net.psforever.services.{Service, InterstellarClusterService => ICS}
import net.psforever.types._
import net.psforever.util.Config
object SessionData {
//noinspection ScalaUnusedSymbol
private def NoTurnCounterYet(guid: PlanetSideGUID): Unit = { }
+
+ private def updateOldRefsMap(inventory: net.psforever.objects.inventory.GridInventory): IterableOnce[(PlanetSideGUID, String)] = {
+ inventory.Items.flatMap {
+ case InventoryItem(o, _) => updateOldRefsMap(o)
+ }
+ }
+
+ private def updateOldRefsMap(item: PlanetSideGameObject): IterableOnce[(PlanetSideGUID, String)] = {
+ item match {
+ case t: Tool =>
+ t.AmmoSlots.map { slot =>
+ val box = slot.Box
+ box.GUID -> box.Definition.Name
+ } :+ (t.GUID -> t.Definition.Name)
+ case _ =>
+ Seq(item.GUID -> item.Definition.Name)
+ }
+ }
}
class SessionData(
- middlewareActor: typed.ActorRef[MiddlewareActor.Command],
+ val middlewareActor: typed.ActorRef[MiddlewareActor.Command],
implicit val context: ActorContext
- ) {
+ ) extends SessionSource {
/**
* Hardwire an implicit `sender` to be the same as `context.self` of the `SessionActor` actor class
* for which this support class was initialized.
@@ -87,68 +76,64 @@ class SessionData(
private[this] implicit val sender: ActorRef = context.self
private val avatarActor: typed.ActorRef[AvatarActor.Command] = context.spawnAnonymous(AvatarActor(context.self))
- private val chatActor: typed.ActorRef[ChatActor.Command] = context.spawnAnonymous(ChatActor(context.self, avatarActor))
- private[support] val log = org.log4s.getLogger
- private[support] var theSession: Session = Session()
- private[support] var accountIntermediary: ActorRef = Default.Actor
- private[support] var accountPersistence: ActorRef = Default.Actor
- private[support] var galaxyService: ActorRef = Default.Actor
- private[support] var squadService: ActorRef = Default.Actor
- private[support] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor
- private[support] var progressBarValue: Option[Float] = None
- private[support] var accessedContainer: Option[PlanetSideGameObject with Container] = None
+ private[session] val log = org.log4s.getLogger
+ private[session] var theSession: Session = Session()
+ private[session] var accountIntermediary: ActorRef = Default.Actor
+ private[session] var accountPersistence: ActorRef = Default.Actor
+ private[session] var galaxyService: ActorRef = Default.Actor
+ private[session] var squadService: ActorRef = Default.Actor
+ private[session] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor
+ private[session] var chatService: typed.ActorRef[ChatService.Command] = Default.typed.Actor
private[session] var connectionState: Int = 25
- private var recentTeleportAttempt: Long = 0
- private[support] var kitToBeUsed: Option[PlanetSideGUID] = None
- private[support] var persistFunc: () => Unit = noPersistence
- private[support] var persist: () => Unit = updatePersistenceOnly
- private[support] var specialItemSlotGuid: Option[PlanetSideGUID] =
- None // If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped.
- private[support] var serverTime: Long = 0 //unused?
+ private[session] var persistFunc: () => Unit = noPersistence
+ private[session] var persist: () => Unit = updatePersistenceOnly
private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial
- private[support] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet
- private var heightLast: Float = 0f
- private var heightTrend: Boolean = false //up = true, down = false
- private var heightHistory: Float = 0f
+ private[session] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet
+ private[session] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]()
private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0)
- private val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap()
-
- private var clientKeepAlive: Cancellable = Default.Cancellable
- private[support] var progressBarUpdate: Cancellable = Default.Cancellable
- private var charSavedTimer: Cancellable = Default.Cancellable
+ val general: GeneralOperations =
+ new GeneralOperations(sessionLogic=this, avatarActor, context)
val shooting: WeaponAndProjectileOperations =
- new WeaponAndProjectileOperations(sessionData=this, avatarActor, chatActor, context)
+ new WeaponAndProjectileOperations(sessionLogic=this, avatarActor, context)
val vehicles: VehicleOperations =
- new VehicleOperations(sessionData=this, avatarActor, context)
+ new VehicleOperations(sessionLogic=this, avatarActor, context)
val avatarResponse: SessionAvatarHandlers =
- new SessionAvatarHandlers(sessionData=this, avatarActor, chatActor, context)
+ new SessionAvatarHandlers(sessionLogic=this, avatarActor, context)
val localResponse: SessionLocalHandlers =
- new SessionLocalHandlers(sessionData=this, context)
+ new SessionLocalHandlers(sessionLogic=this, context)
val mountResponse: SessionMountHandlers =
- new SessionMountHandlers(sessionData=this, avatarActor, context)
+ new SessionMountHandlers(sessionLogic=this, avatarActor, context)
val terminals: SessionTerminalHandlers =
- new SessionTerminalHandlers(sessionData=this, avatarActor, context)
+ new SessionTerminalHandlers(sessionLogic=this, avatarActor, context)
private var vehicleResponseOpt: Option[SessionVehicleHandlers] = None
private var galaxyResponseOpt: Option[SessionGalaxyHandlers] = None
private var squadResponseOpt: Option[SessionSquadHandlers] = None
private var zoningOpt: Option[ZoningOperations] = None
+ private var chatOpt: Option[ChatOperations] = None
def vehicleResponseOperations: SessionVehicleHandlers = vehicleResponseOpt.orNull
- def galaxyResponseHanders: SessionGalaxyHandlers = galaxyResponseOpt.orNull
+ def galaxyResponseHandlers: SessionGalaxyHandlers = galaxyResponseOpt.orNull
def squad: SessionSquadHandlers = squadResponseOpt.orNull
def zoning: ZoningOperations = zoningOpt.orNull
+ def chat: ChatOperations = chatOpt.orNull
+
+ ServiceManager.serviceManager ! Lookup("accountIntermediary")
+ ServiceManager.serviceManager ! Lookup("accountPersistence")
+ ServiceManager.serviceManager ! Lookup("galaxy")
+ ServiceManager.serviceManager ! Lookup("squad")
+ ServiceManager.receptionist ! Receptionist.Find(ICS.InterstellarClusterServiceKey, context.self)
+ ServiceManager.receptionist ! Receptionist.Find(ChatService.ChatServiceKey, context.self)
/**
* updated when an upstream packet arrives;
* allow to be a little stale for a short while
*/
- private[support] var localSector: SectorPopulation = SectorGroup(Nil)
+ private[session] var localSector: SectorPopulation = SectorGroup(Nil)
def session: Session = theSession
def session_=(session: Session): Unit = {
- chatActor ! ChatActor.SetSession(session)
avatarActor ! AvatarActor.SetSession(session)
theSession = session
}
@@ -161,995 +146,7 @@ class SessionData(
def avatar: Avatar = theSession.avatar
- /* packets */
-
- def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage)(implicit context: ActorContext): Unit = {
- val ConnectToWorldRequestMessage(_, token, majorVersion, minorVersion, revision, buildDate, _, _) = pkt
- log.trace(
- s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server"
- )
- sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None))
- import scala.concurrent.ExecutionContext.Implicits.global
- clientKeepAlive.cancel()
- clientKeepAlive = context.system.scheduler.scheduleWithFixedDelay(
- initialDelay = 0.seconds,
- delay = 500.milliseconds,
- context.self,
- SessionActor.PokeClient()
- )
- accountIntermediary ! RetrieveAccountData(token)
- }
-
- def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = {
- val CharacterCreateRequestMessage(name, head, voice, gender, empire) = pkt
- avatarActor ! AvatarActor.CreateAvatar(name, head, voice, gender, empire)
- }
-
- def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = {
- val CharacterRequestMessage(charId, action) = pkt
- action match {
- case CharacterRequestAction.Delete =>
- avatarActor ! AvatarActor.DeleteAvatar(charId.toInt)
- case CharacterRequestAction.Select =>
- avatarActor ! AvatarActor.SelectAvatar(charId.toInt, context.self)
- }
- }
-
- def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = {
- val PlayerStateMessageUpstream(
- avatarGuid,
- pos,
- vel,
- yaw,
- pitch,
- yawUpper,
- seqTime,
- _,
- isCrouching,
- isJumping,
- jumpThrust,
- isCloaking,
- _,
- _
- )= pkt
- persist()
- turnCounterFunc(avatarGuid)
- updateBlockMap(player, pos)
- val isMoving = WorldEntity.isMoving(vel)
- val isMovingPlus = isMoving || isJumping || jumpThrust
- if (isMovingPlus) {
- if (zoning.zoningStatus == Zoning.Status.Deconstructing) {
- stopDeconstructing()
- } else if (zoning.zoningStatus != Zoning.Status.None) {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion")
- }
- }
- fallHeightTracker(pos.z)
-// if (isCrouching && !player.Crouching) {
-// //dev stuff goes here
-// }
- player.Position = pos
- player.Velocity = vel
- player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
- player.FacingYawUpper = yawUpper
- player.Crouching = isCrouching
- player.Jumping = isJumping
- if (isCloaking && !player.Cloaked) {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak")
- }
- player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking
- maxCapacitorTick(jumpThrust)
- if (isMovingPlus && terminals.usingMedicalTerminal.isDefined) {
- continent.GUID(terminals.usingMedicalTerminal) match {
- case Some(term: Terminal with ProximityUnit) =>
- terminals.StopUsingProximityUnit(term)
- case _ => ()
- }
- }
- accessedContainer match {
- // Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle.
- // This can happen if PSUM arrives during the mounting process
- case Some(veh: Vehicle) if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID =>
- if (isMoving || veh.isMoving(test = 1) || Vector3.DistanceSquared(player.Position, veh.TrunkLocation) > 9) {
- val guid = player.GUID
- sendResponse(UnuseItemMessage(guid, veh.GUID))
- sendResponse(UnuseItemMessage(guid, guid))
- unaccessContainer(veh)
- }
- case Some(container) => //just in case
- if (isMovingPlus && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID)) {
- // Ensure we don't close the container if the player is seated in it
- val guid = player.GUID
- // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first.
- if (container.HasGUID) {
- sendResponse(UnuseItemMessage(guid, container.GUID))
- }
- sendResponse(UnuseItemMessage(guid, guid))
- unaccessContainer(container)
- }
- case None => ()
- }
- val eagleEye: Boolean = canSeeReallyFar
- val isNotVisible: Boolean = player.spectator ||
- zoning.zoningStatus == Zoning.Status.Deconstructing ||
- (player.isAlive && 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
- )
- )
- squad.updateSquad()
- if (player.death_by == -1) {
- kickedByAdministration()
- }
- player.zoneInteractions()
- }
-
- def handleChat(pkt: ChatMsg): Unit = {
- chatActor ! ChatActor.Message(pkt)
- }
-
- def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
- val SetChatFilterMessage(_, _, _) = pkt
- }
-
- def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
- log.debug(s"$pkt")
- sendResponse(VoiceHostKill())
- sendResponse(
- ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
- )
- }
-
- def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = {
- log.debug(s"$pkt")
- sendResponse(VoiceHostKill())
- sendResponse(
- ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
- )
- }
-
- def handleEmote(pkt: EmoteMsg): Unit = {
- val EmoteMsg(avatarGuid, emote) = pkt
- sendResponse(EmoteMsg(avatarGuid, emote))
- }
-
- def handleDropItem(pkt: DropItemMessage): Unit = {
- val DropItemMessage(itemGuid) = pkt
- (validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match {
- case (Some(anItem: Equipment), Some(heldItem))
- if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- RemoveOldEquipmentFromInventory(player)(heldItem)
- case (Some(anItem: Equipment), Some(heldItem))
- if anItem eq heldItem =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- DropEquipmentFromInventory(player)(heldItem)
- case (Some(anItem: Equipment), _)
- if continent.GUID(player.VehicleSeated).isEmpty =>
- //suppress the warning message if in a vehicle
- log.warn(s"DropItem: ${player.Name} wanted to drop a ${anItem.Definition.Name}, but it wasn't at hand")
- case (Some(obj), _) =>
- log.warn(s"DropItem: ${player.Name} wanted to drop a ${obj.Definition.Name}, but it was not equipment")
- case _ => ()
- }
- }
-
- def handlePickupItem(pkt: PickupItemMessage): Unit = {
- val PickupItemMessage(itemGuid, _, _, _) = pkt
- validObject(itemGuid, decorator = "PickupItem").collect {
- case item: Equipment if player.Fit(item).nonEmpty =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- PickUpEquipmentFromGround(player)(item)
- case _: Equipment =>
- sendResponse(ActionResultMessage.Fail(16)) //error code?
- }
- }
-
- def handleObjectHeld(pkt: ObjectHeldMessage): Unit = {
- val ObjectHeldMessage(_, heldHolsters, _) = pkt
- player.Actor ! PlayerControl.ObjectHeld(heldHolsters)
- }
-
- def handleAvatarJump(pkt: AvatarJumpMessage): Unit = {
- val AvatarJumpMessage(_) = pkt
- avatarActor ! AvatarActor.ConsumeStamina(10)
- avatarActor ! AvatarActor.SuspendStaminaRegeneration(2.5 seconds)
- }
-
- def handleZipLine(pkt: ZipLineMessage): Unit = {
- val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt
- continent.zipLinePaths.find(x => x.PathId == pathId) match {
- case Some(path) if path.IsTeleporter =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel")
- val endPoint = path.ZipLinePoints.last
- sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
- //todo: send to zone to show teleport animation to all clients
- sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
- case Some(_) =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion")
- action match {
- case 0 =>
- //travel along the zipline in the direction specified
- sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
- case 1 =>
- //disembark from zipline at destination!
- sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
- case 2 =>
- //get off by force
- sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
- case _ =>
- log.warn(
- s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}"
- )
- }
- case _ =>
- log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}")
- }
- }
-
- def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = {
- val RequestDestroyMessage(objectGuid) = pkt
- //make sure this is the correct response for all cases
- validObject(objectGuid, decorator = "RequestDestroy") match {
- case Some(vehicle: Vehicle) =>
- /* line 1a: player is admin (and overrules other access requirements) */
- /* line 1b: vehicle and player (as the owner) acknowledge each other */
- /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */
- /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
- if (
- (session.account.gm ||
- (player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) ||
- (player.Faction == vehicle.Faction &&
- (vehicle.Definition.CanBeOwned.nonEmpty &&
- (vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) &&
- (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied))
- ) {
- vehicle.Actor ! Vehicle.Deconstruct()
- //log.info(s"RequestDestroy: vehicle $vehicle")
- } else {
- log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it")
- }
-
- case Some(obj: Projectile) =>
- if (!obj.isResolved) {
- obj.Miss()
- }
- continent.Projectile ! ZoneProjectile.Remove(objectGuid)
-
- case Some(obj: BoomerTrigger) =>
- if (findEquipmentToDelete(objectGuid, obj)) {
- continent.GUID(obj.Companion) match {
- case Some(boomer: BoomerDeployable) =>
- boomer.Trigger = None
- boomer.Actor ! Deployable.Deconstruct()
- case Some(thing) =>
- log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing")
- case None => ()
- }
- }
-
- case Some(obj: Deployable) =>
- if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
- obj.Actor ! Deployable.Deconstruct()
- } else {
- log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it")
- }
-
- case Some(obj: Equipment) =>
- findEquipmentToDelete(objectGuid, obj)
-
- case Some(thing) =>
- log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}")
-
- case None => ()
- }
- }
-
- def handleMoveItem(pkt: MoveItemMessage): Unit = {
- val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt
- (
- continent.GUID(sourceGuid),
- continent.GUID(destinationGuid),
- validObject(itemGuid, decorator = "MoveItem")
- ) match {
- case (
- Some(source: PlanetSideServerObject with Container),
- Some(destination: PlanetSideServerObject with Container),
- Some(item: Equipment)
- ) =>
- ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest))
- case (None, _, _) =>
- log.error(
- s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object"
- )
- case (_, None, _) =>
- log.error(
- s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object"
- )
- case (_, _, None) => ()
- case _ =>
- log.error(
- s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered"
- )
- }
- }
-
- def handleLootItem(pkt: LootItemMessage): Unit = {
- val LootItemMessage(itemGuid, targetGuid) = pkt
- (validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match {
- case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) =>
- //figure out the source
- (
- {
- val findFunc: PlanetSideServerObject with Container => Option[
- (PlanetSideServerObject with Container, Option[Int])
- ] = findInLocalContainer(itemGuid)
- findFunc(player.avatar.locker)
- .orElse(findFunc(player))
- .orElse(accessedContainer match {
- case Some(parent: PlanetSideServerObject) =>
- findFunc(parent)
- case _ =>
- None
- })
- },
- destination.Fit(item)
- ) match {
- case (Some((source, Some(_))), Some(dest)) =>
- ContainableMoveItem(player.Name, source, destination, item, dest)
- case (None, _) =>
- log.error(s"LootItem: ${player.Name} can not find where $item is put currently")
- case (_, None) =>
- log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination")
- case _ =>
- log.error(
- s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered"
- )
- }
- case (Some(obj), _) =>
- log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}")
- case (None, _) => ()
- case (_, None) =>
- log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid")
- }
- }
-
- def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = {
- val AvatarImplantMessage(_, action, slot, status) = pkt
- if (action == ImplantAction.Activation) {
- if (zoning.zoningStatus == Zoning.Status.Deconstructing) {
- //do not activate; play deactivation sound instead
- stopDeconstructing()
- avatar.implants(slot).collect {
- case implant if implant.active =>
- avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
- case implant =>
- sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
- }
- } else {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant")
- avatar.implants(slot) match {
- case Some(implant) =>
- if (status == 1) {
- avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType)
- } else {
- avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
- }
- case _ =>
- log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot")
- }
- }
- }
- }
-
- def handleUseItem(pkt: UseItemMessage): Unit = {
- val equipment = findContainedEquipment(pkt.item_used_guid) match {
- case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) =>
- shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption
- case (Some(_), a) =>
- a.headOption
- case _ =>
- None
- }
- validObject(pkt.object_guid, decorator = "UseItem") match {
- case Some(door: Door) =>
- handleUseDoor(door, equipment)
- case Some(resourceSilo: ResourceSilo) =>
- handleUseResourceSilo(resourceSilo, equipment)
- case Some(panel: IFFLock) =>
- handleUseGeneralEntity(panel, equipment)
- case Some(obj: Player) =>
- handleUsePlayer(obj, equipment, pkt)
- case Some(locker: Locker) =>
- handleUseLocker(locker, equipment, pkt)
- case Some(gen: Generator) =>
- handleUseGeneralEntity(gen, equipment)
- case Some(mech: ImplantTerminalMech) =>
- handleUseGeneralEntity(mech, equipment)
- case Some(captureTerminal: CaptureTerminal) =>
- handleUseCaptureTerminal(captureTerminal, equipment)
- case Some(obj: FacilityTurret) =>
- handleUseFacilityTurret(obj, equipment, pkt)
- case Some(obj: Vehicle) =>
- handleUseVehicle(obj, equipment, pkt)
- case Some(terminal: Terminal) =>
- handleUseTerminal(terminal, equipment, pkt)
- case Some(obj: SpawnTube) =>
- handleUseSpawnTube(obj, equipment)
- case Some(obj: SensorDeployable) =>
- handleUseGeneralEntity(obj, equipment)
- case Some(obj: TurretDeployable) =>
- handleUseGeneralEntity(obj, equipment)
- case Some(obj: TrapDeployable) =>
- handleUseGeneralEntity(obj, equipment)
- case Some(obj: ShieldGeneratorDeployable) =>
- handleUseGeneralEntity(obj, equipment)
- case Some(obj: TelepadDeployable) =>
- handleUseTelepadDeployable(obj, equipment, pkt)
- case Some(obj: Utility.InternalTelepad) =>
- handleUseInternalTelepad(obj, pkt)
- case Some(obj: CaptureFlag) =>
- handleUseCaptureFlag(obj)
- case Some(_: WarpGate) =>
- handleUseWarpGate(equipment)
- case Some(obj) =>
- handleUseDefaultEntity(obj, equipment)
- case None => ()
- }
- }
-
- def handleUnuseItem(pkt: UnuseItemMessage): Unit = {
- val UnuseItemMessage(_, objectGuid) = pkt
- validObject(objectGuid, decorator = "UnuseItem") match {
- case Some(obj: Player) =>
- unaccessContainer(obj)
- zoning.spawn.TryDisposeOfLootedCorpse(obj)
- case Some(obj: Container) =>
- // Make sure we don't unload the contents of the vehicle the player is seated in
- // An example scenario of this would be closing the trunk contents when rearming at a landing pad
- if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) {
- unaccessContainer(obj)
- }
- case _ => ()
- }
- }
-
- def handleDeployObject(pkt: DeployObjectMessage): Unit = {
- val DeployObjectMessage(guid, _, pos, orient, _) = pkt
- player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid).flatMap { slot => slot.Equipment } match {
- case Some(obj: ConstructionItem) =>
- val ammoType = obj.AmmoType match {
- case DeployedItem.portable_manned_turret => GlobalDefinitions.PortableMannedTurret(player.Faction).Item
- case dtype => dtype
- }
- log.info(s"${player.Name} is constructing a $ammoType deployable")
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- val dObj: Deployable = Deployables.Make(ammoType)()
- dObj.Position = pos
- dObj.Orientation = orient
- dObj.WhichSide = player.WhichSide
- dObj.Faction = player.Faction
- dObj.AssignOwnership(player)
- val tasking: TaskBundle = dObj match {
- case turret: TurretDeployable =>
- GUIDTask.registerDeployableTurret(continent.GUID, turret)
- case _ =>
- GUIDTask.registerObject(continent.GUID, dObj)
- }
- TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj)))
- case Some(obj) =>
- log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!")
- case None =>
- log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!")
- }
- }
-
- def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = {
- val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt
- validObject(objectGuid, decorator = "PlanetsideAttribute") match {
- case Some(vehicle: Vehicle) if player.avatar.vehicle.contains(vehicle.GUID) =>
- vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue)
- case Some(vehicle: Vehicle) =>
- log.warn(s"PlanetsideAttribute: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it")
- // Cosmetics options
- case Some(_: Player) if attributeType == 106 =>
- avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue))
- case Some(obj) =>
- log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}")
- case _ => ()
- }
- }
-
- def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = {
- val GenericObjectActionMessage(objectGuid, code) = pkt
- validObject(objectGuid, decorator = "GenericObjectAction") match {
- case Some(vehicle: Vehicle)
- if vehicle.OwnerName.contains(player.Name) =>
- vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID))
-
- case Some(tool: Tool) =>
- if (code == 35 &&
- (tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser"))
- ) {
- //maelstrom primary fire mode discharge (no target)
- //aphelion_laser discharge (no target)
- shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID))
- } else {
- validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match {
- case Some(vehicle: Vehicle)
- if vehicle.OwnerName.contains(player.Name) =>
- vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool))
- case _ =>
- }
- }
- case _ =>
- log.info(s"${player.Name} - $pkt")
- }
- }
-
- def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = {
- val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt
- validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match {
- case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) =>
- shooting.FindContainedWeapon match {
- case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) =>
- vehicle.Actor ! SpecialEmp.Burst()
- case _ => ()
- }
- case _ =>
- log.info(s"${player.Name} - $pkt")
- }
- }
-
- def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = {
- val GenericObjectStateMsg(_, _) = pkt
- log.info(s"${player.Name} - $pkt")
- }
-
- def handleGenericAction(pkt: GenericActionMessage): Unit = {
- val GenericActionMessage(action) = pkt
- if (player == null) {
- if (action == GenericAction.AwayFromKeyboard_RCV) {
- log.debug("GenericObjectState: AFK state reported during login")
- }
- } else {
- val (toolOpt, definition) = player.Slot(0).Equipment match {
- case Some(tool: Tool) =>
- (Some(tool), tool.Definition)
- case _ =>
- (None, GlobalDefinitions.bullet_9mm)
- }
- action match {
- case GenericAction.DropSpecialItem =>
- dropSpecialSlotItem()
- case GenericAction.MaxAnchorsExtend_RCV =>
- log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground")
- player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.PlanetsideAttribute(player.GUID, 19, 1)
- )
- definition match {
- case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
- val tool = toolOpt.get
- tool.ToFireMode = 1
- sendResponse(ChangeFireModeMessage(tool.GUID, 1))
- case GlobalDefinitions.trhev_pounder =>
- val tool = toolOpt.get
- val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 }
- else { 4 }
- tool.ToFireMode = convertFireModeIndex
- sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
- case _ =>
- log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}")
- }
- case GenericAction.MaxAnchorsRelease_RCV =>
- log.info(s"${player.Name} has released the anchors")
- player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.PlanetsideAttribute(player.GUID, 19, 0)
- )
- definition match {
- case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
- val tool = toolOpt.get
- tool.ToFireMode = 0
- sendResponse(ChangeFireModeMessage(tool.GUID, 0))
- case GlobalDefinitions.trhev_pounder =>
- val tool = toolOpt.get
- val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 }
- tool.ToFireMode = convertFireModeIndex
- sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
- case _ =>
- log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}")
- }
- case GenericAction.MaxSpecialEffect_RCV =>
- if (player.ExoSuit == ExoSuitType.MAX) {
- toggleMaxSpecialState(enable = true)
- } else {
- log.warn(s"GenericActionMessage: ${player.Name} can't handle MAX special effect")
- }
- case GenericAction.StopMaxSpecialEffect_RCV =>
- if (player.ExoSuit == ExoSuitType.MAX) {
- player.Faction match {
- case PlanetSideEmpire.NC =>
- toggleMaxSpecialState(enable = false)
- case _ =>
- log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability")
- }
- } else {
- log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect")
- }
- case GenericAction.AwayFromKeyboard_RCV =>
- log.info(s"${player.Name} is AFK")
- AvatarActor.savePlayerLocation(player)
- displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
- player.AwayFromKeyboard = true
- case GenericAction.BackInGame_RCV =>
- log.info(s"${player.Name} is back")
- player.AwayFromKeyboard = false
- renewCharSavedTimer(
- Config.app.game.savedMsg.renewal.fixed,
- Config.app.game.savedMsg.renewal.variable
- )
- case GenericAction.LookingForSquad_RCV => //Looking For Squad ON
- if (!avatar.lookingForSquad && (squad.squadUI.isEmpty || squad.squadUI(player.CharId).index == 0)) {
- avatarActor ! AvatarActor.SetLookingForSquad(true)
- }
- case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF
- if (avatar.lookingForSquad && (squad.squadUI.isEmpty || squad.squadUI(player.CharId).index == 0)) {
- avatarActor ! AvatarActor.SetLookingForSquad(false)
- }
- case _ =>
- log.warn(s"GenericActionMessage: ${player.Name} can't handle $action")
- }
- }
- }
-
- def handleFavoritesRequest(pkt: FavoritesRequest): Unit = {
- val FavoritesRequest(_, loadoutType, action, line, label) = pkt
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- action match {
- case FavoritesAction.Save =>
- avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line)
- case FavoritesAction.Delete =>
- avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line)
- case FavoritesAction.Unknown =>
- log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action")
- }
- }
-
- def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
- val GenericCollisionMsg(ctype, p, _, ppos, pv, t, _, tpos, tv, _, _, _) = pkt
- val fallHeight = {
- if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
- if (heightTrend) {
- val fall = heightLast - heightHistory
- heightHistory = heightLast
- fall
- }
- else {
- val fall = heightHistory - heightLast
- heightLast = heightHistory
- fall
- }
- } else {
- 0f
- }
- }
- val (target1, target2, bailProtectStatus, velocity) = (ctype, validObject(p, decorator = "GenericCollision/Primary")) match {
- case (CollisionIs.OfInfantry, out @ Some(user: Player))
- if user == player =>
- val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection
- player.BailProtection = false
- val v = if (player.avatar.implants.exists {
- case Some(implant) => implant.definition.implantType == ImplantType.Surge && implant.active
- case _ => false
- }) {
- Vector3.Zero
- } else {
- pv
- }
- (out, None, bailStatus, v)
- case (CollisionIs.OfGroundVehicle, out @ Some(v: Vehicle))
- if v.Seats(0).occupant.contains(player) =>
- val bailStatus = v.BailProtection
- v.BailProtection = false
- (out, validObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv)
- case (CollisionIs.OfAircraft, out @ Some(v: Vehicle))
- if v.Definition.CanFly && v.Seats(0).occupant.contains(player) =>
- (out, validObject(t, decorator = "GenericCollision/Aircraft"), false, pv)
- case (CollisionIs.BetweenThings, _) =>
- log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
- (None, None, false, Vector3.Zero)
- case _ =>
- (None, None, false, Vector3.Zero)
- }
- val curr = System.currentTimeMillis()
- (target1, t, target2) match {
- case (None, _, _) => ()
-
- case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
- if (updateCollisionHistoryForTarget(us, curr)) {
- if (!bailProtectStatus) {
- handleDealingDamage(
- us,
- DamageInteraction(
- SourceEntry(us),
- CollisionReason(velocity, fallHeight, us.DamageModel),
- ppos
- )
- )
- }
- }
-
- case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
- collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
-
- case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty =>
- collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
-
- case (
- Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _,
- Some(victim: PlanetSideServerObject with Vitality with FactionAffinity)
- ) =>
- if (updateCollisionHistoryForTarget(victim, curr)) {
- val usSource = SourceEntry(us)
- val victimSource = SourceEntry(victim)
- //we take damage from the collision
- if (!bailProtectStatus) {
- performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv)
- }
- //get dealt damage from our own collision (no protection)
- collisionHistory.put(us.Actor, curr)
- performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity)
- }
-
- case _ => ()
- }
- }
-
- def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = {
- val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt
- avatarActor ! AvatarActor.AddFirstTimeEvent(eventName)
- }
-
- def handleBugReport(pkt: PlanetSideGamePacket): Unit = {
- val BugReportMessage(
- _/*versionMajor*/,
- _/*versionMinor*/,
- _/*versionDate*/,
- _/*bugType*/,
- _/*repeatable*/,
- _/*location*/,
- _/*zone*/,
- _/*pos*/,
- _/*summary*/,
- _/*desc*/
- ) = pkt
- log.warn(s"${player.Name} filed a bug report - it might be something important")
- log.debug(s"$pkt")
- }
-
- def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = {
- val FacilityBenefitShieldChargeRequestMessage(_) = pkt
- val vehicleGuid = player.VehicleSeated
- continent
- .GUID(vehicleGuid)
- .foreach {
- case obj: Vehicle if !obj.Destroyed && obj.MountedIn.isEmpty => // vehicle will try to charge even if destroyed & cargo vehicles need to be excluded
- obj.Actor ! CommonMessages.ChargeShields(
- 15,
- Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius))
- )
- case obj: Vehicle if obj.MountedIn.nonEmpty =>
- false
- case _ if vehicleGuid.nonEmpty =>
- log.warn(
- s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find vehicle ${vehicleGuid.get.guid} in zone ${continent.id}"
- )
- case _ =>
- log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in a vehicle")
- }
- }
-
- def handleBattleplan(pkt: BattleplanMessage): Unit = {
- val BattleplanMessage(_, name, _, _) = pkt
- val lament: String = s"$name has a brilliant idea that no one will ever see"
- log.info(lament)
- log.debug(s"Battleplan: $lament - $pkt")
- }
-
- def handleBindPlayer(pkt: BindPlayerMessage): Unit = {
- val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt
- }
-
- def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = {
- val CreateShortcutMessage(_, slot, shortcutOpt) = pkt
- shortcutOpt match {
- case Some(shortcut) =>
- avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut)
- case None =>
- avatarActor ! AvatarActor.RemoveShortcut(slot - 1)
- }
- }
-
- def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = {
- val ChangeShortcutBankMessage(_, _) = pkt
- }
-
- def handleFriendRequest(pkt: FriendsRequest): Unit = {
- val FriendsRequest(action, name) = pkt
- avatarActor ! AvatarActor.MemberListRequest(action, name)
- }
-
- def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = {
- val InvalidTerrainMessage(_, vehicleGuid, alert, _) = pkt
- (continent.GUID(vehicleGuid), continent.GUID(player.VehicleSeated)) match {
- case (Some(packetVehicle: Vehicle), Some(playerVehicle: Vehicle)) if packetVehicle eq playerVehicle =>
- if (alert == TerrainCondition.Unsafe) {
- log.info(s"${player.Name}'s ${packetVehicle.Definition.Name} is approaching terrain unsuitable for idling")
- }
- case (Some(packetVehicle: Vehicle), Some(_: Vehicle)) =>
- if (alert == TerrainCondition.Unsafe) {
- log.info(s"${packetVehicle.Definition.Name}@${packetVehicle.GUID} is approaching terrain unsuitable for idling, but is not ${player.Name}'s vehicle")
- }
- case (Some(_: Vehicle), _) =>
- log.warn(s"InvalidTerrain: ${player.Name} is not seated in a(ny) vehicle near unsuitable terrain")
- case (Some(packetThing), _) =>
- log.warn(s"InvalidTerrain: ${player.Name} thinks that ${packetThing.Definition.Name}@${packetThing.GUID} is near unsuitable terrain")
- case _ =>
- log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicleGuid that can not be found")
- }
- }
-
- def handleActionCancel(pkt: ActionCancelMessage): Unit = {
- val ActionCancelMessage(_, _, _) = pkt
- progressBarUpdate.cancel()
- progressBarValue = None
- }
-
- def handleTrade(pkt: TradeMessage): Unit = {
- val TradeMessage(trade) = pkt
- log.trace(s"${player.Name} wants to trade for some reason - $trade")
- }
-
- def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = {
- val DisplayedAwardMessage(_, ribbon, bar) = pkt
- log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon")
- avatarActor ! AvatarActor.SetRibbon(ribbon, bar)
- }
-
- def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = {
- val ObjectDetectedMessage(_, _, _, targets) = pkt
- shooting.FindWeapon.foreach {
- case weapon if weapon.Projectile.AutoLock =>
- //projectile with auto-lock instigates a warning on the target
- val detectedTargets = shooting.FindDetectedProjectileTargets(targets)
- val mode = 7 + (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile)
- detectedTargets.foreach { target =>
- continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode))
- }
- case _ => ()
- }
- }
-
- def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
- val TargetingImplantRequest(list) = pkt
- val targetInfo: List[TargetInfo] = list.flatMap { x =>
- continent.GUID(x.target_guid) match {
- case Some(player: Player) =>
- val health = player.Health.toFloat / player.MaxHealth
- val armor = if (player.MaxArmor > 0) {
- player.Armor.toFloat / player.MaxArmor
- } else {
- 0
- }
- Some(TargetInfo(player.GUID, health, armor))
- case _ =>
- log.warn(
- s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
- )
- None
- }
- }
- sendResponse(TargetingInfoMessage(targetInfo))
- }
-
- def handleHitHint(pkt: HitHint): Unit = {
- val HitHint(_, _) = pkt
- }
-
- /* messages */
-
- def handleSetAvatar(avatar: Avatar): Unit = {
- session = session.copy(avatar = avatar)
- if (session.player != null) {
- session.player.avatar = avatar
- }
- LivePlayerList.Update(avatar.id, avatar)
- }
-
- def handleReceiveAccountData(account: Account): Unit = {
- log.trace(s"ReceiveAccountData $account")
- session = session.copy(account = account)
- avatarActor ! AvatarActor.SetAccount(account)
- }
-
- def handleUpdateIgnoredPlayers: PlanetSideGamePacket => Unit = {
- case msg: FriendsResponse =>
- sendResponse(msg)
- msg.friends.foreach { f =>
- galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name))
- }
- case _ => ()
- }
-
- def handleUseCooldownRenew: BasicDefinition => Unit = {
- case _: KitDefinition => kitToBeUsed = None
- case _ => ()
- }
-
- def handleAvatarResponse(avatar: Avatar): Unit = {
- session = session.copy(avatar = avatar)
- accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id)
- }
-
- def handleSetSpeed(speed: Float): Unit = {
- session = session.copy(speed = speed)
- }
-
- def handleSetFlying(flying: Boolean): Unit = {
- session = session.copy(flying = flying)
- }
-
- def handleSetSpectator(spectator: Boolean): Unit = {
- session.player.spectator = spectator
- }
-
- def handleKick(player: Player, time: Option[Long]): Unit = {
- administrativeKick(player)
- accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
- }
-
- def handleSilenced(isSilenced: Boolean): Unit = {
- player.silenced = isSilenced
- }
-
- /* supporting functions */
-
- def buildDependentOperationsForGalaxy(galaxyActor: ActorRef): Unit = {
- if (vehicleResponseOpt.isEmpty && galaxyActor != Default.Actor) {
- galaxyResponseOpt = Some(new SessionGalaxyHandlers(sessionData=this, avatarActor, galaxyActor, context))
- vehicleResponseOpt = Some(new SessionVehicleHandlers(sessionData=this, avatarActor, galaxyActor, context))
- }
- }
-
- def buildDependentOperations(galaxyActor: ActorRef, clusterActor: typed.ActorRef[ICS.Command]): Unit = {
- if (zoningOpt.isEmpty && galaxyActor != Default.Actor && clusterActor != Default.typed.Actor) {
- zoningOpt = Some(new ZoningOperations(sessionData=this, avatarActor, galaxyActor, clusterActor, context))
- }
- }
-
- def buildDependentOperationsForSquad(squadActor: ActorRef): Unit = {
- if (squadResponseOpt.isEmpty && squadActor != Default.Actor) {
- squadResponseOpt = Some(new SessionSquadHandlers(sessionData=this, avatarActor, chatActor, squadActor, context))
- }
- }
+ /* setup functions */
def assignEventBus(msg: Any): Boolean = {
msg match {
@@ -1162,7 +159,7 @@ class SessionData(
case LookupResult("galaxy", endpoint) =>
galaxyService = endpoint
buildDependentOperationsForGalaxy(endpoint)
- buildDependentOperations(endpoint, cluster)
+ buildDependentOperationsForZoning(endpoint, cluster)
true
case LookupResult("squad", endpoint) =>
squadService = endpoint
@@ -1170,7 +167,12 @@ class SessionData(
true
case ICS.InterstellarClusterServiceKey.Listing(listings) =>
cluster = listings.head
- buildDependentOperations(galaxyService, cluster)
+ buildDependentOperationsForZoning(galaxyService, cluster)
+ buildDependentOperationsForChat(chatService, cluster)
+ true
+ case ChatService.ChatServiceKey.Listing(listings) =>
+ chatService = listings.head
+ buildDependentOperationsForChat(chatService, cluster)
true
case _ =>
@@ -1178,15 +180,43 @@ class SessionData(
}
}
+ def buildDependentOperationsForGalaxy(galaxyActor: ActorRef): Unit = {
+ if (vehicleResponseOpt.isEmpty && galaxyActor != Default.Actor) {
+ galaxyResponseOpt = Some(new SessionGalaxyHandlers(sessionLogic=this, avatarActor, galaxyActor, context))
+ vehicleResponseOpt = Some(new SessionVehicleHandlers(sessionLogic=this, avatarActor, galaxyActor, context))
+ }
+ }
+
+ def buildDependentOperationsForZoning(galaxyActor: ActorRef, clusterActor: typed.ActorRef[ICS.Command]): Unit = {
+ if (zoningOpt.isEmpty && galaxyActor != Default.Actor && clusterActor != Default.typed.Actor) {
+ zoningOpt = Some(new ZoningOperations(sessionLogic=this, avatarActor, galaxyActor, clusterActor, context))
+ }
+ }
+
+ def buildDependentOperationsForSquad(squadActor: ActorRef): Unit = {
+ if (squadResponseOpt.isEmpty && squadActor != Default.Actor) {
+ squadResponseOpt = Some(new SessionSquadHandlers(sessionLogic=this, avatarActor, squadActor, context))
+ }
+ }
+
+ def buildDependentOperationsForChat(chatService: typed.ActorRef[ChatService.Command], clusterActor: typed.ActorRef[ICS.Command]): Unit = {
+ if (chatOpt.isEmpty && chatService != Default.typed.Actor && clusterActor != Default.typed.Actor) {
+ chatOpt = Some(new ChatOperations(sessionLogic=this, avatarActor, chatService, clusterActor, context))
+ }
+ }
+
def whenAllEventBusesLoaded(): Boolean = {
accountIntermediary != Default.Actor &&
accountPersistence != Default.Actor &&
vehicleResponseOpt.nonEmpty &&
galaxyResponseOpt.nonEmpty &&
squadResponseOpt.nonEmpty &&
- zoningOpt.nonEmpty
+ zoningOpt.nonEmpty &&
+ chatOpt.nonEmpty
}
+ /* support functions */
+
def validObject(id: Int): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator = "")
def validObject(id: Int, decorator: String): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator)
@@ -1255,295 +285,6 @@ class SessionData(
}
}
- private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
- equipment match {
- case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
- val distance: Float = math.max(
- Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance,
- door.Definition.initialOpeningDistance
- )
- door.Actor ! CommonMessages.Use(player, Some(distance))
- case _ =>
- door.Actor ! CommonMessages.Use(player)
- }
- }
-
- private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- (continent.GUID(player.VehicleSeated), equipment) match {
- case (Some(vehicle: Vehicle), Some(item))
- if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) &&
- GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) =>
- resourceSilo.Actor ! CommonMessages.Use(player, equipment)
- case _ =>
- resourceSilo.Actor ! CommonMessages.Use(player)
- }
- }
-
- private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- if (obj.isBackpack) {
- if (equipment.isEmpty) {
- log.info(s"${player.Name} is looting the corpse of ${obj.Name}")
- sendResponse(msg)
- accessContainer(obj)
- }
- } 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))
- 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))
- case (Some(item), _) =>
- log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead")
- case (None, None) =>
- log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") }
- } 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)
-
- case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
- obj.Actor ! CommonMessages.Use(player, equipment)
- case _ => ()
- }
- }
- }
-
- private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
- equipment match {
- case Some(item) =>
- sendUseGeneralEntityMessage(locker, item)
- case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty =>
- log.info(s"${player.Name} is accessing a locker")
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- val playerLocker = player.avatar.locker
- sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456))
- accessContainer(playerLocker)
- case _ => ()
- }
- }
-
- private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = {
- equipment match {
- case Some(item) =>
- sendUseGeneralEntityMessage(captureTerminal, item)
- case _ if specialItemSlotGuid.nonEmpty =>
- continent.GUID(specialItemSlotGuid) match {
- case Some(llu: CaptureFlag) =>
- if (llu.Target.GUID == captureTerminal.Owner.GUID) {
- continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu))
- } 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")
- }
- case _ => ()
- }
- }
-
- private 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
- }
- }
-
- private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
- equipment match {
- case Some(item) =>
- sendUseGeneralEntityMessage(obj, item)
- case None if player.Faction == obj.Faction =>
- //access to trunk
- if (
- obj.AccessingTrunk.isEmpty &&
- (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid
- .contains(player.GUID))
- ) {
- log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk")
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- obj.AccessingTrunk = player.GUID
- accessContainer(obj)
- sendResponse(msg)
- }
- case _ => ()
- }
- }
-
- private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
- equipment match {
- case Some(item) =>
- sendUseGeneralEntityMessage(terminal, item)
- 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)
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- sendResponse(
- BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position)
- )
- } else if (
- tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal ||
- tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal
- ) {
- findLocalVehicle match {
- case Some(vehicle) =>
- log.info(
- s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}"
- )
- sendResponse(msg)
- sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId))
- case None =>
- log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none")
- }
- } else if (tdef == GlobalDefinitions.teleportpad_terminal) {
- //explicit request
- log.info(s"${player.Name} is purchasing a router telepad")
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- terminal.Actor ! Terminal.Request(
- player,
- ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0))
- )
- } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) {
- //explicit request
- log.info(s"${player.Name} is purchasing a targeting laser")
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- terminal.Actor ! Terminal.Request(
- player,
- ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0))
- )
- } else {
- log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}")
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- sendResponse(msg)
- }
- case _ => ()
- }
- }
-
- private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = {
- equipment match {
- case Some(item) =>
- sendUseGeneralEntityMessage(obj, item)
- case None if player.Faction == obj.Faction =>
- //deconstruction
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- playerActionsToCancel()
- terminals.CancelAllProximityUnits()
- startDeconstructing(obj)
- case _ => ()
- }
- }
-
- private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
- if (equipment.isEmpty) {
- (continent.GUID(obj.Router) match {
- case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
- case Some(vehicle) => Some(vehicle, None)
- case None => None
- }) match {
- case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel")
- player.WhichSide = vehicle.WhichSide
- useRouterTelepadSystem(
- router = vehicle,
- internalTelepad = util,
- remoteTelepad = obj,
- src = obj,
- dest = util
- )
- case Some((vehicle: Vehicle, None)) =>
- log.error(
- s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
- )
- 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 _ => ()
- }
- }
- }
-
- private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = {
- continent.GUID(obj.Telepad) match {
- case Some(pad: TelepadDeployable) =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel")
- player.WhichSide = pad.WhichSide
- useRouterTelepadSystem(
- router = obj.Owner.asInstanceOf[Vehicle],
- internalTelepad = obj,
- remoteTelepad = pad,
- src = obj,
- dest = pad
- )
- 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 => ()
- }
- }
-
- private def handleUseCaptureFlag(obj: CaptureFlag): Unit = {
- // 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)
- case None =>
- log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}")
- 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 _ => ()
- }
- }
-
- private def handleUseWarpGate(equipment: Option[Equipment]): Unit = {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- (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 _ => ()
- }
- }
-
- private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = {
- equipment.foreach { item =>
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- obj.Actor ! CommonMessages.Use(player, Some(item))
- }
- }
-
- private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- obj.Actor ! CommonMessages.Use(player, Some(equipment))
- }
-
- private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = {
- zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- equipment match {
- case Some(item)
- if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) ||
- GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => ()
- case _ =>
- log.warn(s"UseItem: ${player.Name} does not know how to handle $obj")
- }
- }
-
/**
* Update this player avatar for persistence.
* Set to `persist` initially.
@@ -1558,407 +299,6 @@ class SessionData(
*/
def noPersistence(): Unit = { }
- def dropSpecialSlotItem(): Unit = {
- specialItemSlotGuid.foreach { guid =>
- specialItemSlotGuid = None
- player.Carrying = None
- (continent.GUID(guid) match {
- case Some(llu: CaptureFlag) => Some((llu, llu.Carrier))
- case _ => None
- }) match {
- case Some((llu, Some(carrier: Player)))
- if carrier.GUID == player.GUID && !player.isAlive =>
- player.LastDamage.foreach { damage =>
- damage
- .interaction
- .adversarial
- .map { _.attacker }
- .collect {
- case attacker
- if attacker.Faction != player.Faction &&
- System.currentTimeMillis() - llu.LastCollectionTime >= Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis =>
- continent.AvatarEvents ! AvatarServiceMessage(
- attacker.Name,
- AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
- )
- }
- }
- continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
- case Some((llu, Some(carrier: Player))) if carrier.GUID == player.GUID =>
- continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
- case Some((_, Some(carrier: Player))) =>
- log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}")
- case Some((_, None)) =>
- log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.")
- case None =>
- log.warn(s"${player.toString} tried to drop a special item that wasn't recognized. GUID: $guid")
- }
- }
- }
-
- /**
- * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays.
- * Intended to assist in sanitizing loadout information from the perspective of the player, or target owner.
- * The equipment is expected to be unregistered and already fitted to their ultimate slot in the target container.
- * @param player the player whose purchasing constraints are to be tested
- * @param target the location in which the equipment will be stowed
- * @param slots the equipment, in the standard object-slot format container
- */
- def applyPurchaseTimersBeforePackingLoadout(
- player: Player,
- target: PlanetSideServerObject with Container,
- slots: List[InventoryItem]
- ): Unit = {
- slots.foreach { item =>
- player.avatar.purchaseCooldown(item.obj.Definition) match {
- case Some(_) => ()
- case None if Avatar.purchaseCooldowns.contains(item.obj.Definition) =>
- avatarActor ! AvatarActor.UpdatePurchaseTime(item.obj.Definition)
- TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start))
- case None =>
- TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start))
- }
- }
- }
-
- def setupProgressChange(rate: Float, finishedAction: () => Unit, stepAction: Float => Boolean): Unit = {
- if (progressBarValue.isEmpty) {
- progressBarValue = Some(-rate)
- context.self ! SessionActor.ProgressEvent(rate, finishedAction, stepAction)
- }
- }
-
- /**
- * Handle the message that indicates the level of completion of a process.
- * The process is any form of user-driven activity with a certain eventual outcome
- * but indeterminate progress feedback per cycle.
- *
- * This task is broken down into the "progression" from its initial state to the eventual outcome
- * as is reported back to the player through some means of messaging window feedback.
- * Though common in practice, this is not a requirement
- * and the progress can accumulate without a user reportable method.
- * To ensure that completion is reported properly,
- * an exception is made that 99% completion is accounted uniquely
- * before the final 100% is achieved.
- * If the background process recording value is never set before running the initial operation
- * or gets unset by failing a `tickAction` check
- * the process is stopped.
- * @see `progressBarUpdate`
- * @see `progressBarValue`
- * @see `essionActor.Progress`
- * @param delta how much the progress changes each tick
- * @param completionAction a custom action performed once the process is completed
- * @param tickAction an optional action is is performed for each tick of progress;
- * also performs a continuity check to determine if the process has been disrupted
- */
- def handleProgressChange(
- delta: Float,
- completionAction: () => Unit,
- tickAction: Float => Boolean,
- tick: Long
- ): Unit = {
- progressBarUpdate.cancel()
- progressBarValue.foreach { value =>
- val next = value + delta
- if (value >= 100f) {
- //complete
- progressBarValue = None
- tickAction(100)
- completionAction()
- } else if (value < 100f && next >= 100f) {
- if (tickAction(99)) {
- //will complete after this turn
- progressBarValue = Some(next)
- import scala.concurrent.ExecutionContext.Implicits.global
- progressBarUpdate = context.system.scheduler.scheduleOnce(
- delay = 100 milliseconds,
- context.self,
- SessionActor.ProgressEvent(delta, completionAction, tickAction)
- )
- } else {
- progressBarValue = None
- }
- } else {
- if (tickAction(next)) {
- //normal progress activity
- progressBarValue = Some(next)
- import scala.concurrent.ExecutionContext.Implicits.global
- progressBarUpdate = context.system.scheduler.scheduleOnce(
- tick.milliseconds,
- context.self,
- SessionActor.ProgressEvent(delta, completionAction, tickAction, tick)
- )
- } else {
- progressBarValue = None
- }
- }
- }
- }
-
- /**
- * Construct tasking that registers all aspects of a `Player` avatar
- * as if that player is only just being introduced.
- * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
- * @param tplayer the avatar `Player`
- * @return a `TaskBundle` message
- */
- private[session] def registerNewAvatar(tplayer: Player): TaskBundle = {
- TaskBundle(
- new StraightforwardTask() {
- private val localPlayer = tplayer
- private val localAnnounce = context.self
-
- override def description(): String = s"register new player avatar ${localPlayer.Name}"
-
- def action(): Future[Any] = {
- localAnnounce ! SessionActor.NewPlayerLoaded(localPlayer)
- Future(true)
- }
- },
- List(GUIDTask.registerAvatar(continent.GUID, tplayer))
- )
- }
-
- /**
- * Construct tasking that registers all aspects of a `Player` avatar
- * as if that player was already introduced and is just being renewed.
- * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
- * @param tplayer the avatar `Player`
- * @return a `TaskBundle` message
- */
- private[session] def registerAvatar(tplayer: Player): TaskBundle = {
- TaskBundle(
- new StraightforwardTask() {
- private val localPlayer = tplayer
- private val localAnnounce = context.self
-
- override def description(): String = s"register player avatar ${localPlayer.Name}"
-
- def action(): Future[Any] = {
- localAnnounce ! SessionActor.PlayerLoaded(localPlayer)
- Future(true)
- }
- },
- List(GUIDTask.registerPlayer(continent.GUID, tplayer))
- )
- }
-
- /**
- * Construct tasking that adds a completed and registered vehicle into the scene.
- * Use this function to renew the globally unique identifiers on a vehicle that has already been added to the scene once.
- * @param vehicle the `Vehicle` object
- * @see `RegisterVehicleFromSpawnPad`
- * @return a `TaskBundle` message
- */
- private[session] def registerVehicle(vehicle: Vehicle): TaskBundle = {
- TaskBundle(
- new StraightforwardTask() {
- private val localVehicle = vehicle
-
- override def description(): String = s"register a ${localVehicle.Definition.Name}"
-
- def action(): Future[Any] = Future(true)
- },
- List(GUIDTask.registerVehicle(continent.GUID, vehicle))
- )
- }
-
- private[session] def registerDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = {
- TaskBundle(
- new StraightforwardTask() {
- private val localVehicle = vehicle
- private val localDriver = driver
- private val localAnnounce = context.self
-
- override def description(): String = s"register a ${localVehicle.Definition.Name} driven by ${localDriver.Name}"
-
- def action(): Future[Any] = {
- localDriver.VehicleSeated = localVehicle.GUID
- Vehicles.Own(localVehicle, localDriver)
- localAnnounce ! SessionActor.NewPlayerLoaded(localDriver)
- Future(true)
- }
- },
- List(GUIDTask.registerAvatar(continent.GUID, driver), GUIDTask.registerVehicle(continent.GUID, vehicle))
- )
- }
-
- private[session] def unregisterDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = {
- TaskBundle(
- new StraightforwardTask() {
- private val localVehicle = vehicle
- private val localDriver = driver
-
- override def description(): String = s"unregister a ${localVehicle.Definition.Name} driven by ${localDriver.Name}"
-
- def action(): Future[Any] = Future(true)
- },
- List(GUIDTask.unregisterAvatar(continent.GUID, driver), GUIDTask.unregisterVehicle(continent.GUID, vehicle))
- )
- }
-
- def accessContainer(container: Container): Unit = {
- container match {
- case v: Vehicle =>
- accessVehicleContents(v)
- case o: LockerContainer =>
- accessGenericContainer(o)
- case p: Player if p.isBackpack =>
- accessCorpseContents(p)
- case p: PlanetSideServerObject with Container =>
- accessedContainer = Some(p)
- case _ => ()
- }
- }
-
- def accessGenericContainer(container: PlanetSideServerObject with Container): Unit = {
- accessedContainer = Some(container)
- displayContainerContents(container.GUID, container.Inventory.Items)
- }
-
- /**
- * Common preparation for interfacing with a vehicle trunk.
- * Join a vehicle-specific group for shared updates.
- * Construct every object in the vehicle's inventory for shared manipulation updates.
- * @see `Container.Inventory`
- * @see `GridInventory.Items`
- * @param vehicle the vehicle
- */
- def accessVehicleContents(vehicle: Vehicle): Unit = {
- accessedContainer = Some(vehicle)
- accessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString)
- displayContainerContents(vehicle.GUID, vehicle.Inventory.Items)
- }
-
- /**
- * Common preparation for interfacing with a corpse (former player's backpack).
- * Join a corpse-specific group for shared updates.
- * Construct every object in the player's hands and inventory for shared manipulation updates.
- * @see `Container.Inventory`
- * @see `GridInventory.Items`
- * @see `Player.HolsterItems`
- * @param tplayer the corpse
- */
- def accessCorpseContents(tplayer: Player): Unit = {
- accessedContainer = Some(tplayer)
- accessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString)
- displayContainerContents(tplayer.GUID, tplayer.HolsterItems())
- displayContainerContents(tplayer.GUID, tplayer.Inventory.Items)
- }
-
- /**
- * Join an entity-specific group for shared updates.
- * @param events the event system bus to which to subscribe
- * @param channel the channel name
- */
- def accessContainerChannel(events: ActorRef, channel: String): Unit = {
- events ! Service.Join(channel)
- }
-
- /**
- * Depict the contents of a container by building them in the local client
- * in their container as a group of detailed entities.
- * @see `ObjectCreateDetailedMessage`
- * @see `ObjectCreateMessageParent`
- * @see `PacketConverter.DetailedConstructorData`
- * @param containerId the container's unique identifier
- * @param items a list of the entities to be depicted
- */
- def displayContainerContents(containerId: PlanetSideGUID, items: Iterable[InventoryItem]): Unit = {
- items.foreach(entry => {
- val obj = entry.obj
- val objDef = obj.Definition
- sendResponse(
- ObjectCreateDetailedMessage(
- objDef.ObjectId,
- obj.GUID,
- ObjectCreateMessageParent(containerId, entry.start),
- objDef.Packet.DetailedConstructorData(obj).get
- )
- )
- })
- }
-
- /**
- * For whatever conatiner the character considers itself accessing,
- * initiate protocol to release it from "access".
- */
- def unaccessContainer(): Unit = {
- accessedContainer.foreach { container => unaccessContainer(container) }
- }
-
- /**
- * For the target container, initiate protocol to release it from "access".
- */
- def unaccessContainer(container: Container): Unit = {
- container match {
- case v: Vehicle =>
- unaccessVehicleContainer(v)
- case o: LockerContainer =>
- unaccessGenericContainer(o)
- avatarActor ! AvatarActor.SaveLocker()
- case p: Player if p.isBackpack =>
- unaccessCorpseContainer(p)
- case _: PlanetSideServerObject with Container =>
- accessedContainer = None
- case _ => ()
- }
- }
-
- def unaccessGenericContainer(container: Container): Unit = {
- accessedContainer = None
- hideContainerContents(container.Inventory.Items)
- }
-
- /**
- * Common preparation for disengaging from a vehicle.
- * Leave the vehicle-specific group that was used for shared updates.
- * Deconstruct every object in the vehicle's inventory.
- * @param vehicle the vehicle
- */
- def unaccessVehicleContainer(vehicle: Vehicle): Unit = {
- accessedContainer = None
- if (vehicle.AccessingTrunk.contains(player.GUID)) {
- vehicle.AccessingTrunk = None
- }
- unaccessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString)
- hideContainerContents(vehicle.Inventory.Items)
- }
-
- /**
- * Common preparation for disengaging from a corpse.
- * Leave the corpse-specific group that was used for shared updates.
- * Deconstruct every object in the backpack's inventory.
- * @param tplayer the corpse
- */
- def unaccessCorpseContainer(tplayer: Player): Unit = {
- accessedContainer = None
- unaccessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString)
- hideContainerContents(tplayer.HolsterItems())
- hideContainerContents(tplayer.Inventory.Items)
- }
-
- /**
- * Leave an entity-specific group for shared updates.
- * @param events the event system bus to which to subscribe
- * @param channel the channel name
- */
- def unaccessContainerChannel(events: ActorRef, channel: String): Unit = {
- events ! Service.Leave(Some(channel))
- }
-
- /**
- * Forget the contents of a container by deleting that content from the local client.
- * @see `InventoryItem`
- * @see `ObjectDeleteMessage`
- * @param items a list of the entities to be depicted
- */
- def hideContainerContents(items: List[InventoryItem]): Unit = {
- items.foreach { entry =>
- sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
- }
- }
-
/**
* Check two locations for a controlled piece of equipment that is associated with the `player`.
*
@@ -2023,107 +363,6 @@ class SessionData(
*/
def findEquipment(guid: PlanetSideGUID): Option[Equipment] = findEquipment().find { _.GUID == guid }
- /**
- * Get the current `Vehicle` object that the player is riding/driving.
- * The vehicle must be found solely through use of `player.VehicleSeated`.
- * @return the vehicle
- */
- def findLocalVehicle: Option[Vehicle] = {
- continent.GUID(player.VehicleSeated) match {
- case Some(obj: Vehicle) => Some(obj)
- case _ => None
- }
- }
-
- /**
- * Drop an `Equipment` item onto the ground.
- * Specifically, instruct the item where it will appear,
- * add it to the list of items that are visible to multiple users,
- * and then inform others that the item has been dropped.
- * @param obj a `Container` object that represents where the item will be dropped;
- * curried for callback
- * @param zone the continent in which the item is being dropped;
- * curried for callback
- * @param item the item
- */
- def normalItemDrop(obj: PlanetSideServerObject with Container, zone: Zone)(item: Equipment): Unit = {
- zone.Ground.tell(Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)), obj.Actor)
- }
-
- /**
- * Given an object globally unique identifier, search in a given location for it.
- * @param objectGuid the object
- * @param parent a `Container` object wherein to search
- * @return an optional tuple that contains two values;
- * the first value is the container that matched correctly with the object's GUID;
- * the second value is the slot position of the object
- */
- def findInLocalContainer(
- objectGuid: PlanetSideGUID
- )(parent: PlanetSideServerObject with Container): Option[(PlanetSideServerObject with Container, Option[Int])] = {
- parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) }
- }
-
- /**
- * Common reporting behavior when a `Deployment` object fails to properly transition between states.
- * @param obj the game object that could not
- * @param state the `DriveState` that could not be promoted
- * @param reason a string explaining why the state can not or will not change
- */
- def youCanNotChangeDeployment(
- obj: PlanetSideServerObject with Deployment,
- state: DriveState.Value,
- reason: String
- ): Unit = {
- val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) {
- obj.DeploymentState = DriveState.Mobile
- sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero))
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero)
- )
- "; enforcing Mobile deployment state"
- } else {
- ""
- }
- log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift")
- }
-
- /**
- * na
- * @param targetGuid na
- * @param unk1 na
- * @param unk2 na
- */
- def hackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = {
- sendResponse(HackMessage(0, targetGuid, PlanetSideGUID(0), 100, unk1, HackState.Hacked, unk2))
- }
-
- /**
- * Send a PlanetsideAttributeMessage packet to the client
- * @param targetGuid The target of the attribute
- * @param attributeNumber The attribute number
- * @param attributeValue The attribute value
- */
- def sendPlanetsideAttributeMessage(
- targetGuid: PlanetSideGUID,
- attributeNumber: PlanetsideAttributeEnum,
- attributeValue: Long
- ): Unit = {
- sendResponse(PlanetsideAttributeMessage(targetGuid, attributeNumber, attributeValue))
- }
-
- /**
- * The player has lost the will to live and must be killed.
- * @see `Vitality`
- * `PlayerSuicide`
- * @param tplayer the player to be killed
- */
- def suicide(tplayer: Player): Unit = {
- tplayer.LogActivity(PlayerSuicide(PlayerSource(tplayer)))
- tplayer.Actor ! Player.Die()
- }
-
/**
* An event has occurred that would cause the player character to stop certain stateful activities.
* These activities include shooting, the weapon being drawn, hacking, accessing (a container), flying, and running.
@@ -2134,53 +373,17 @@ class SessionData(
* - if the player is anchored
* This is not a complete list but, for the purpose of enforcement, some pointers will be documented here.
*/
- def playerActionsToCancel(): Unit = {
- shooting.shootingStart.clear()
- shooting.shootingStop.clear()
- progressBarUpdate.cancel()
- progressBarValue = None
- terminals.lastTerminalOrderFulfillment = true
- kitToBeUsed = None
- collisionHistory.clear()
- accessedContainer match {
- case Some(v: Vehicle) =>
- val vguid = v.GUID
- vehicles.ConditionalDriverVehicleControl(v)
- if (v.AccessingTrunk.contains(player.GUID)) {
- if (player.VehicleSeated.contains(vguid)) {
- v.AccessingTrunk = None //player is seated; just stop accessing trunk
- if (player.isAlive) {
- sendResponse(UnuseItemMessage(player.GUID, vguid))
- }
- } else {
- unaccessContainer(v)
- }
- }
-
- case Some(o) =>
- unaccessContainer(o)
- if (player.isAlive) {
- sendResponse(UnuseItemMessage(player.GUID, o.GUID))
- }
-
- case None => ()
- }
- (shooting.prefire ++ shooting.shooting).foreach { guid =>
- sendResponse(ChangeFireStateMessage_Stop(guid))
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.ChangeFireState_Stop(player.GUID, guid)
- )
- }
- shooting.prefire.clear()
- shooting.shooting.clear()
+ def actionsToCancel(): Unit = {
+ general.actionsToCancel()
+ shooting.actionsToCancel()
+ terminals.actionsToCancel()
if (session.flying) {
session = session.copy(flying = false)
- chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_FLY, wideContents=false, "", "off", None))
+ chat.commandFly(contents = "off", recipient = "")
}
if (session.speed > 1) {
session = session.copy(speed = 1)
- chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_SPEED, wideContents=false, "", "1.000", None))
+ chat.commandSpeed(ChatMsg(ChatMessageType.CMT_SPEED, "1.000"), contents = "1.000")
}
}
@@ -2239,385 +442,6 @@ class SessionData(
}
}
- /**
- * Properly format a `DestroyDisplayMessage` packet
- * given sufficient information about a target (victim) and an actor (killer).
- * For the packet, the `charId` field is important for determining distinction between players.
- * @param killer the killer's entry
- * @param victim the victim's entry
- * @param method the manner of death
- * @param unk na;
- * defaults to 121, the object id of `avatar`
- * @return a `DestroyDisplayMessage` packet that is properly formatted
- */
- def destroyDisplayMessage(
- killer: SourceEntry,
- victim: SourceEntry,
- method: Int,
- unk: Int = 121
- ): DestroyDisplayMessage = {
- val killerSeated = killer match {
- case obj: PlayerSource => obj.Seated
- case _ => false
- }
- val victimSeated = victim match {
- case obj: PlayerSource => obj.Seated
- case _ => false
- }
- new DestroyDisplayMessage(
- killer.Name,
- killer.CharId,
- killer.Faction,
- killerSeated,
- unk,
- method,
- victim.Name,
- victim.CharId,
- victim.Faction,
- victimSeated
- )
- }
-
- /**
- * Initialize the deployables user interface elements.
- *
- * All element initializations require both the maximum deployable amount and the current deployables active counts.
- * Until initialized, all elements will be RED 0/0 as if the corresponding certification were not `learn`ed.
- * The respective element will become a pair of numbers, the second always being non-zero, when properly initialized.
- * The numbers will appear GREEN when more deployables of that type can be placed.
- * The numbers will appear RED if the player can not place any more of that type of deployable.
- * The numbers will appear YELLOW if the current deployable count is greater than the maximum count of that type
- * such as may be the case when a player `forget`s a certification.
- * @param list a tuple of each UI element with four numbers;
- * even numbers are attribute ids;
- * odd numbers are quantities;
- * first pair is current quantity;
- * second pair is maximum quantity
- */
- def updateDeployableUIElements(list: List[(Int, Int, Int, Int)]): Unit = {
- val guid = PlanetSideGUID(0)
- list.foreach {
- case (currElem, curr, maxElem, max) =>
- //fields must update in ordered pairs: max, curr
- sendResponse(PlanetsideAttributeMessage(guid, maxElem, max))
- sendResponse(PlanetsideAttributeMessage(guid, currElem, curr))
- }
- }
-
- /**
- * Common actions related to constructing a new `Deployable` object in the game environment.
- *
- * The map icon for the deployable just introduced is also created on the clients of all faction-affiliated players.
- * This icon is important as, short of destroying it,
- * the owner has no other means of controlling the created object that it is associated with.
- * @param obj the `Deployable` object to be built
- */
- def deployableBuildActivity(obj: Deployable): Unit = {
- sendResponse(GenericObjectActionMessage(obj.GUID, 21)) //reset build cooldown
- updateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item))
- }
-
- /**
- * A simple object searching algorithm that is limited to containers currently known and accessible by the player.
- * If all relatively local containers are checked and the object is not found,
- * the player's locker inventory will be checked, and then
- * the game environment (items on the ground) will be checked too.
- * If the target object is discovered, it is removed from its current location and is completely destroyed.
- * @see `RequestDestroyMessage`
- * @see `Zone.ItemIs.Where`
- * @param objectGuid the target object's globally unique identifier;
- * it is not expected that the object will be unregistered, but it is also not gauranteed
- * @param obj the target object
- * @return `true`, if the target object was discovered and removed;
- * `false`, otherwise
- */
- def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = {
- val findFunc
- : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] =
- findInLocalContainer(objectGuid)
-
- findFunc(player)
- .orElse(accessedContainer match {
- case Some(parent: PlanetSideServerObject) =>
- findFunc(parent)
- case _ =>
- None
- })
- .orElse(findLocalVehicle match {
- case Some(parent: PlanetSideServerObject) =>
- findFunc(parent)
- case _ =>
- None
- }) match {
- case Some((parent, Some(_))) =>
- obj.Position = Vector3.Zero
- RemoveOldEquipmentFromInventory(parent)(obj)
- true
- case _ if player.avatar.locker.Inventory.Remove(objectGuid) =>
- sendResponse(ObjectDeleteMessage(objectGuid, 0))
- true
- case _ if continent.EquipmentOnGround.contains(obj) =>
- obj.Position = Vector3.Zero
- continent.Ground ! Zone.Ground.RemoveItem(objectGuid)
- continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
- true
- case _ =>
- Zone.EquipmentIs.Where(obj, objectGuid, continent) match {
- case None =>
- true
- case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID =>
- TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
- true
- case Some(Zone.EquipmentIs.Orphaned()) =>
- true
- case _ =>
- log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it")
- false
- }
- }
- }
-
- /**
- * Common behavior for deconstructing deployables in the game environment.
- * @param obj the deployable
- * @param guid the globally unique identifier for the deployable
- * @param pos the previous position of the deployable
- * @param orient the previous orientation of the deployable
- * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
- */
- def deconstructDeployable(
- obj: Deployable,
- guid: PlanetSideGUID,
- pos: Vector3,
- orient: Vector3,
- deletionType: Int
- ): Unit = {
- sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
- sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
- sendResponse(ObjectDeleteMessage(guid, deletionType))
- }
-
- /**
- * Search through the player's holsters and their inventory space
- * and remove all `BoomerTrigger` objects, both functionally and visually.
- * @return all discovered `BoomTrigger` objects
- */
- def removeBoomerTriggersFromInventory(): List[BoomerTrigger] = {
- val events = continent.AvatarEvents
- val zoneId = continent.id
- (player.Inventory.Items ++ player.HolsterItems())
- .collect { case InventoryItem(obj: BoomerTrigger, index) =>
- player.Slot(index).Equipment = None
- continent.GUID(obj.Companion) match {
- case Some(mine: BoomerDeployable) => mine.Actor ! Deployable.Ownership(None)
- case _ => ()
- }
- if (player.VisibleSlots.contains(index)) {
- events ! AvatarServiceMessage(
- zoneId,
- AvatarAction.ObjectDelete(Service.defaultPlayerGUID, obj.GUID)
- )
- } else {
- sendResponse(ObjectDeleteMessage(obj.GUID, 0))
- }
- obj
- }
- }
-
- /**
- * Attempt to link the router teleport system using the provided terminal information.
- * Although additional states are necessary to properly use the teleportation system,
- * e.g., deployment state, active state of the endpoints, etc.,
- * this decision is not made factoring those other conditions.
- * @param router the vehicle that houses one end of the teleportation system (the `InternalTelepad` object)
- * @param systemPlan specific object identification of the two endpoints of the teleportation system;
- * if absent, the knowable endpoint is deleted from the client reflexively
- */
- def toggleTeleportSystem(router: Vehicle, systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)]): Unit = {
- systemPlan match {
- case Some((internalTelepad, remoteTelepad)) =>
- internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad
- TelepadLike.StartRouterInternalTelepad(continent, router.GUID, internalTelepad)
- TelepadLike.LinkTelepad(continent, remoteTelepad.GUID)
- case _ =>
- router.Utility(UtilityType.internal_router_telepad_deployable) match {
- case Some(util: Utility.InternalTelepad) =>
- sendResponse(ObjectDeleteMessage(util.GUID, 0))
- case _ => ()
- }
- }
- }
-
- /**
- * A player uses a fully-linked Router teleportation system.
- * @param router the Router vehicle
- * @param internalTelepad the internal telepad within the Router vehicle
- * @param remoteTelepad the remote telepad that is currently associated with this Router
- * @param src the origin of the teleportation (where the player starts)
- * @param dest the destination of the teleportation (where the player is going)
- */
- def useRouterTelepadSystem(
- router: Vehicle,
- internalTelepad: InternalTelepad,
- remoteTelepad: TelepadDeployable,
- src: PlanetSideGameObject with TelepadLike,
- dest: PlanetSideGameObject with TelepadLike
- ): Unit = {
- val time = System.currentTimeMillis()
- if (
- time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
- internalTelepad.Active &&
- remoteTelepad.Active
- ) {
- val pguid = player.GUID
- val sguid = src.GUID
- val dguid = dest.GUID
- sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
- useRouterTelepadEffect(pguid, sguid, dguid)
- continent.LocalEvents ! LocalServiceMessage(
- continent.id,
- LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
- )
- val vSource = VehicleSource(router)
- val zoneNumber = continent.Number
- player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber))
- player.Position = dest.Position
- player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber))
- } else {
- log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
- }
- recentTeleportAttempt = time
- }
-
- /**
- * Animate(?) a player using a fully-linked Router teleportation system.
- * In reality, this seems to do nothing visually?
- * @param playerGUID the player being teleported
- * @param srcGUID the origin of the teleportation
- * @param destGUID the destination of the teleportation
- */
- def useRouterTelepadEffect(playerGUID: PlanetSideGUID, srcGUID: PlanetSideGUID, destGUID: PlanetSideGUID): Unit = {
- sendResponse(PlanetsideAttributeMessage(playerGUID, 64, 1)) //what does this do?
- sendResponse(GenericObjectActionMessage(srcGUID, 31))
- sendResponse(GenericObjectActionMessage(destGUID, 32))
- }
-
- /**
- * From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines.
- * @param objWithSeat the object that owns seats (and weaponry)
- * @param seatNum the mount
- */
- def updateWeaponAtSeatPosition(objWithSeat: MountableWeapons, seatNum: Int): Unit = {
- objWithSeat.WeaponControlledFromSeat(seatNum) foreach {
- case weapon: Tool =>
- //update mounted weapon belonging to mount
- weapon.AmmoSlots.foreach(slot => {
- //update the magazine(s) in the weapon, specifically
- val magazine = slot.Box
- sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong))
- })
- case _ => () //no weapons to update
- }
- }
-
- def maxCapacitorTick(jumpThrust: Boolean): Unit = {
- if (player.ExoSuit == ExoSuitType.MAX) {
- val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0
- player.CapacitorState match {
- case CapacitorStateType.Idle => maxCapacitorTickIdle(activate)
- case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate)
- case CapacitorStateType.ChargeDelay => maxCapacitorTickChargeDelay(activate)
- case CapacitorStateType.Charging => maxCapacitorTickCharging(activate)
- }
- } else if (player.CapacitorState != CapacitorStateType.Idle) {
- player.CapacitorState = CapacitorStateType.Idle
- }
- }
-
- private def maxCapacitorTickIdle(activate: Boolean): Unit = {
- if (activate) {
- player.CapacitorState = CapacitorStateType.Discharging
- //maxCapacitorTickDischarging(activate)
- } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
- player.CapacitorState = CapacitorStateType.ChargeDelay
- maxCapacitorTickChargeDelay(activate)
- }
- }
-
- private def maxCapacitorTickDischarging(activate: Boolean): Unit = {
- if (activate) {
- val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000
- val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff
- player.Capacitor -= drainAmount
- sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
- } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
- if (player.Faction != PlanetSideEmpire.VS) {
- toggleMaxSpecialState(enable = false)
- }
- player.CapacitorState = CapacitorStateType.ChargeDelay
- maxCapacitorTickChargeDelay(activate)
- } else {
- player.CapacitorState = CapacitorStateType.Idle
- }
- }
-
- private def maxCapacitorTickChargeDelay(activate: Boolean): Unit = {
- if (activate) {
- player.CapacitorState = CapacitorStateType.Discharging
- //maxCapacitorTickDischarging(activate)
- } else if (player.Capacitor == player.ExoSuitDef.MaxCapacitor) {
- player.CapacitorState = CapacitorStateType.Idle
- } else if (System.currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis) {
- player.CapacitorState = CapacitorStateType.Charging
- //maxCapacitorTickCharging(activate)
- }
- }
-
- private def maxCapacitorTickCharging(activate: Boolean): Unit = {
- if (activate) {
- player.CapacitorState = CapacitorStateType.Discharging
- //maxCapacitorTickDischarging(activate)
- } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) {
- val timeDiff = (System.currentTimeMillis() - player.CapacitorLastChargedMillis).toFloat / 1000
- val chargeAmount = player.ExoSuitDef.CapacitorRechargePerSecond * timeDiff
- player.Capacitor += chargeAmount
- sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
- } else {
- player.CapacitorState = CapacitorStateType.Idle
- }
- }
-
- def toggleMaxSpecialState(enable: Boolean): Unit = {
- if (player.ExoSuit == ExoSuitType.MAX) {
- if (enable && player.UsingSpecial == SpecialExoSuitDefinition.Mode.Normal) {
- player.Faction match {
- case PlanetSideEmpire.TR if player.Capacitor == player.ExoSuitDef.MaxCapacitor =>
- player.UsingSpecial = SpecialExoSuitDefinition.Mode.Overdrive
- activateMaxSpecialStateMessage()
- case PlanetSideEmpire.NC if player.Capacitor > 0 =>
- player.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded
- activateMaxSpecialStateMessage()
- case PlanetSideEmpire.VS =>
- log.warn(s"${player.Name} tried to use a MAX special ability but their faction doesn't have one")
- case _ => ()
- }
- } else {
- player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 0)
- )
- }
- }
- }
-
- private def activateMaxSpecialStateMessage(): Unit = {
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 1)
- )
- }
-
/**
* The atypical response to receiving a `KeepAliveMessage` packet from the client.
*
@@ -2649,6 +473,47 @@ class SessionData(
}
}
+ def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Unit = {
+ target.blockMapEntry.foreach { entry =>
+ val sectorIndices = BlockMap.findSectorIndices(continent.blockMap, newCoords, entry.rangeX, entry.rangeY).toSet
+ if (sectorIndices.equals(entry.sectors)) {
+ target.updateBlockMapEntry(newCoords) //soft update
+ localSector = continent.blockMap.sector(sectorIndices, Config.app.game.playerDraw.rangeMax.toFloat)
+ } else {
+ continent.actor ! ZoneActor.UpdateBlockMap(target, newCoords) //hard update
+ }
+ }
+ }
+
+ def updateLocalBlockMap(pos: Vector3): Unit = {
+ localSector = continent.blockMap.sector(pos, Config.app.game.playerDraw.rangeMax.toFloat)
+ }
+
+ def updateOldRefsMap(): Unit = {
+ if(player.HasGUID) {
+ oldRefsMap.addAll(
+ (continent.GUID(player.VehicleSeated) match {
+ case Some(v : Vehicle) =>
+ v.Weapons.toList.collect {
+ case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => SessionData.updateOldRefsMap(slot.Equipment.get)
+ }.flatten ++
+ SessionData.updateOldRefsMap(v.Inventory)
+ case _ =>
+ Map.empty[PlanetSideGUID, String]
+ }) ++
+ (general.accessedContainer match {
+ case Some(cont) => SessionData.updateOldRefsMap(cont.Inventory)
+ case None => Map.empty[PlanetSideGUID, String]
+ }) ++
+ player.Holsters().toList.collect {
+ case slot if slot.Equipment.nonEmpty => SessionData.updateOldRefsMap(slot.Equipment.get)
+ }.flatten ++
+ SessionData.updateOldRefsMap(player.Inventory) ++
+ SessionData.updateOldRefsMap(player.avatar.locker.Inventory)
+ )
+ }
+ }
+
def administrativeKick(tplayer: Player): Unit = {
log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
tplayer.death_by = -1
@@ -2669,7 +534,7 @@ class SessionData(
def kickedByAdministration(): Unit = {
sendResponse(DisconnectMessage("@kick_w"))
context.system.scheduler.scheduleOnce(
- delay = 300 milliseconds,
+ delay = 300.milliseconds,
middlewareActor.toClassic,
MiddlewareActor.Teardown()
)
@@ -2682,197 +547,6 @@ class SessionData(
middlewareActor ! MiddlewareActor.Teardown()
}
- def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Unit = {
- target.blockMapEntry.foreach { entry =>
- val sectorIndices = BlockMap.findSectorIndices(continent.blockMap, newCoords, entry.rangeX, entry.rangeY).toSet
- if (sectorIndices.equals(entry.sectors)) {
- target.updateBlockMapEntry(newCoords) //soft update
- localSector = continent.blockMap.sector(sectorIndices, Config.app.game.playerDraw.rangeMax.toFloat)
- } else {
- continent.actor ! ZoneActor.UpdateBlockMap(target, newCoords) //hard update
- }
- }
- }
-
- def updateLocalBlockMap(pos: Vector3): Unit = {
- localSector = continent.blockMap.sector(pos, Config.app.game.playerDraw.rangeMax.toFloat)
- }
-
- private[support] var oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]()
- def updateOldRefsMap(): Unit = {
- if(player.HasGUID) {
- oldRefsMap.addAll(
- (continent.GUID(player.VehicleSeated) match {
- case Some(v : Vehicle) =>
- v.Weapons.toList.collect {
- case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get)
- }.flatten ++
- updateOldRefsMap(v.Inventory)
- case _ =>
- Map.empty[PlanetSideGUID, String]
- }) ++
- (accessedContainer match {
- case Some(cont) => updateOldRefsMap(cont.Inventory)
- case None => Map.empty[PlanetSideGUID, String]
- }) ++
- player.Holsters().toList.collect {
- case slot if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get)
- }.flatten ++
- updateOldRefsMap(player.Inventory) ++
- updateOldRefsMap(player.avatar.locker.Inventory)
- )
- }
- }
-
- def updateOldRefsMap(inventory: net.psforever.objects.inventory.GridInventory): IterableOnce[(PlanetSideGUID, String)] = {
- inventory.Items.flatMap {
- case InventoryItem(o, _) => updateOldRefsMap(o)
- }
- }
-
- def updateOldRefsMap(item: PlanetSideGameObject): IterableOnce[(PlanetSideGUID, String)] = {
- item match {
- case t: Tool =>
- t.AmmoSlots.map { slot =>
- val box = slot.Box
- box.GUID -> box.Definition.Name
- } :+ (t.GUID -> t.Definition.Name)
- case _ =>
- Seq(item.GUID -> item.Definition.Name)
- }
- }
-
- def fallHeightTracker(zHeight: Float): Unit = {
- if ((heightTrend && heightLast - zHeight >= 0.5f) ||
- (!heightTrend && zHeight - heightLast >= 0.5f)) {
- heightTrend = !heightTrend
- heightHistory = zHeight
- }
- heightLast = zHeight
- }
-
- def canSeeReallyFar: Boolean = {
- shooting.FindContainedWeapon match {
- case (Some(_: Vehicle), weapons) if weapons.nonEmpty =>
- player.avatar
- .implants
- .exists { p =>
- p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty
- }
- case (Some(_: Player), weapons) if weapons.nonEmpty =>
- val wep = weapons.head
- wep.Definition == GlobalDefinitions.bolt_driver ||
- wep.Definition == GlobalDefinitions.heavy_sniper ||
- (
- (wep.Projectile ne GlobalDefinitions.no_projectile) &&
- player.Crouching &&
- player.avatar
- .implants
- .exists { p =>
- p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty
- }
- )
- case _ =>
- false
- }
- }
-
- def displayCharSavedMsgThenRenewTimer(fixedLen: Long, varLen: Long): Unit = {
- charSaved()
- renewCharSavedTimer(fixedLen, varLen)
- }
-
- def renewCharSavedTimer(fixedLen: Long, varLen: Long): Unit = {
- charSavedTimer.cancel()
- val delay = (fixedLen + (varLen * scala.math.random()).toInt).seconds
- charSavedTimer = context.system.scheduler.scheduleOnce(delay, context.self, SessionActor.CharSavedMsg)
- }
-
- def charSaved(): Unit = {
- sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None))
- }
-
- def startDeconstructing(obj: SpawnTube): Unit = {
- log.info(s"${player.Name} is deconstructing at the ${obj.Owner.Definition.Name}'s spawns")
- avatar.implants.collect {
- case Some(implant) if implant.active && !implant.definition.Passive =>
- avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
- }
- if (player.ExoSuit != ExoSuitType.MAX) {
- player.Actor ! PlayerControl.ObjectHeld(Player.HandsDownSlot, updateMyHolsterArm = true)
- }
- zoning.spawn.nextSpawnPoint = Some(obj) //set fallback
- zoning.zoningStatus = Zoning.Status.Deconstructing
- player.allowInteraction = false
- if (player.death_by == 0) {
- player.death_by = 1
- }
- zoning.spawn.GoToDeploymentMap()
- }
-
- def stopDeconstructing(): Unit = {
- zoning.zoningStatus = Zoning.Status.None
- player.death_by = math.min(player.death_by, 0)
- player.allowInteraction = true
- zoning.spawn.nextSpawnPoint.foreach { tube =>
- sendResponse(PlayerStateShiftMessage(ShiftState(0, tube.Position, tube.Orientation.z)))
- zoning.spawn.nextSpawnPoint = None
- }
- }
-
- private def updateCollisionHistoryForTarget(
- target: PlanetSideServerObject with Vitality with FactionAffinity,
- curr: Long
- ): Boolean = {
- collisionHistory.get(target.Actor) match {
- case Some(lastCollision) if curr - lastCollision <= 1000L =>
- false
- case _ =>
- collisionHistory.put(target.Actor, curr)
- true
- }
- }
-
- private def collisionBetweenVehicleAndFragileDeployable(
- vehicle: Vehicle,
- vehiclePosition: Vector3,
- smallDeployable: Deployable,
- smallDeployablePosition: Vector3,
- velocity: Vector3,
- fallHeight: Float,
- collisionTime: Long
- ): Unit = {
- if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) {
- val smallDeployableSource = SourceEntry(smallDeployable)
- //vehicle takes damage from the collision (ignore bail protection in this case)
- performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity)
- //deployable gets absolutely destroyed
- collisionHistory.put(vehicle.Actor, collisionTime)
- handleDealingDamage(
- smallDeployable,
- DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition)
- )
- }
- }
-
- private def performCollisionWithSomethingDamage(
- target: PlanetSideServerObject with Vitality with FactionAffinity,
- targetSource: SourceEntry,
- targetPosition: Vector3,
- victimSource: SourceEntry,
- fallHeight: Float,
- velocity: Vector3
- ): Unit = {
- handleDealingDamage(
- target,
- DamageInteraction(
- targetSource,
- CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource),
- targetPosition
- )
- )
- }
-
def failWithError(error: String): Unit = {
log.error(error)
middlewareActor ! MiddlewareActor.Teardown()
@@ -2883,27 +557,25 @@ class SessionData(
}
def stop(): Unit = {
- continent.AvatarEvents ! Service.Leave()
- continent.LocalEvents ! Service.Leave()
- continent.VehicleEvents ! Service.Leave()
context.stop(avatarActor)
- context.stop(chatActor)
- galaxyService ! Service.Leave()
- if (avatar != null && squadService != Default.Actor) {
- squadService ! Service.Leave(Some(s"${avatar.faction}"))
- }
- clientKeepAlive.cancel()
- progressBarUpdate.cancel()
- charSavedTimer.cancel()
+ general.stop()
shooting.stop()
vehicles.stop()
avatarResponse.stop()
localResponse.stop()
mountResponse.stop()
terminals.stop()
- vehicleResponseOpt.foreach { _.stop() }
- galaxyResponseOpt.foreach { _.stop() }
- squadResponseOpt.foreach { _.stop() }
- zoningOpt.foreach { _.stop() }
+ vehicleResponseOpt.foreach(_.stop())
+ galaxyResponseOpt.foreach(_.stop())
+ squadResponseOpt.foreach(_.stop())
+ zoningOpt.foreach(_.stop())
+ chatOpt.foreach(_.stop())
+ continent.AvatarEvents ! Service.Leave()
+ continent.LocalEvents ! Service.Leave()
+ continent.VehicleEvents ! Service.Leave()
+ galaxyService ! Service.Leave()
+ if (avatar != null && squadService != Default.Actor) {
+ squadService ! Service.Leave()
+ }
}
}
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala
index 976ee10fa..ad7614296 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala
@@ -2,177 +2,22 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, ActorRef, typed}
-import scala.concurrent.duration._
+import net.psforever.packet.game.FriendsResponse
//
import net.psforever.actors.session.AvatarActor
-import net.psforever.objects.Vehicle
-import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, DeadState, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage}
-import net.psforever.services.Service
import net.psforever.services.galaxy.GalaxyResponse
-import net.psforever.types.{MemberAction, PlanetSideEmpire}
-class SessionGalaxyHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- galaxyService: ActorRef,
- implicit val context: ActorContext
- ) extends CommonSessionInterfacingFunctionality {
- def handle(reply: GalaxyResponse.Response): Unit = {
- reply match {
- case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) =>
- sendResponse(
- HotSpotUpdateMessage(
- zone_index,
- priority,
- hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
- )
- )
+trait GalaxyHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: SessionGalaxyHandlers
- case GalaxyResponse.MapUpdate(msg) =>
- sendResponse(msg)
+ def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit
- case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
- val faction = player.Faction
- val from = fromFactions.contains(faction)
- val to = toFactions.contains(faction)
- if (from && !to) {
- sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
- } else if (!from && to) {
- sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
- }
-
- case GalaxyResponse.FlagMapUpdate(msg) =>
- sendResponse(msg)
-
- case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
- sessionData.zoning.handleTransferPassenger(temp_channel, vehicle, manifest)
-
- case GalaxyResponse.LockedZoneUpdate(zone, time) =>
- sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
-
- case GalaxyResponse.UnlockedZoneUpdate(zone) => ;
- sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
- val popBO = 0
- val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
- val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
- val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
- sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
-
- case GalaxyResponse.LogStatusChange(name) if (avatar.people.friend.exists { _.name.equals(name) }) =>
- avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
-
- case GalaxyResponse.SendResponse(msg) =>
- sendResponse(msg)
-
- case _ => ()
- }
- }
+ def handle(reply: GalaxyResponse.Response): Unit
}
-/*package net.psforever.actors.session.support
-
-import akka.actor.{ActorContext, ActorRef, typed}
-import scala.concurrent.duration._
-//
-import net.psforever.actors.session.AvatarActor
-import net.psforever.objects.Vehicle
-import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, DeadState, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage}
-import net.psforever.services.Service
-import net.psforever.services.galaxy.GalaxyResponse
-import net.psforever.types.{MemberAction, PlanetSideEmpire}
-
class SessionGalaxyHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- galaxyService: ActorRef,
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ val galaxyService: ActorRef,
implicit val context: ActorContext
- ) extends CommonSessionInterfacingFunctionality {
- def handle(reply: GalaxyResponse.Response): Unit = {
- reply match {
- case GalaxyResponse.HotSpotUpdate(zoneIndex, priority, hotSpotInfo) =>
- sendResponse(
- HotSpotUpdateMessage(
- zoneIndex,
- priority,
- hotSpotInfo.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
- )
- )
-
- case GalaxyResponse.MapUpdate(msg) =>
- sendResponse(msg)
-
- case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
- val faction = player.Faction
- val from = fromFactions.contains(faction)
- val to = toFactions.contains(faction)
- if (from && !to) {
- sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
- } else if (!from && to) {
- sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
- }
-
- case GalaxyResponse.FlagMapUpdate(msg) =>
- sendResponse(msg)
-
- case GalaxyResponse.TransferPassenger(tempChannel, vehicle, _, manifest) =>
- val playerName = player.Name
- log.debug(s"TransferPassenger: $playerName received the summons to transfer to ${vehicle.Zone.id} ...")
- manifest.passengers
- .find { _.name.equals(playerName) }
- .collect {
- case entry if vehicle.Seats(entry.mount).occupant.isEmpty =>
- player.VehicleSeated = None
- vehicle.Seats(entry.mount).mount(player)
- player.VehicleSeated = vehicle.GUID
- Some(vehicle)
- case entry if vehicle.Seats(entry.mount).occupant.contains(player) =>
- Some(vehicle)
- case entry =>
- log.warn(
- s"TransferPassenger: $playerName tried to mount seat ${entry.mount} during summoning, but it was already occupied, and ${player.Sex.pronounSubject} was rebuked"
- )
- None
- }.orElse {
- manifest.cargo.find { _.name.equals(playerName) }.flatMap { entry =>
- vehicle.CargoHolds(entry.mount).occupant.collect {
- case cargo if cargo.Seats(0).occupants.exists(_.Name.equals(playerName)) => cargo
- }
- }
- } match {
- case Some(v: Vehicle) =>
- galaxyService ! Service.Leave(Some(tempChannel)) //temporary vehicle-specific channel (see above)
- sessionData.zoning.spawn.deadState = DeadState.Release
- sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
- sessionData.zoning.interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system
- sessionData.zoning.spawn.LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None)
- case _ =>
- sessionData.zoning.interstellarFerry match {
- case None =>
- galaxyService ! Service.Leave(Some(tempChannel)) //no longer being transferred between zones
- sessionData.zoning.interstellarFerryTopLevelGUID = None
- case Some(_) => ;
- //wait patiently
- }
- }
-
- case GalaxyResponse.LockedZoneUpdate(zone, time) =>
- sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
-
- case GalaxyResponse.UnlockedZoneUpdate(zone) =>
- sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
- val popBO = 0
- val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
- val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
- val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
- sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
-
- case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists { _.name.equals(name) } =>
- avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
-
- case GalaxyResponse.SendResponse(msg) =>
- sendResponse(msg)
-
- case _ => ()
- }
- }
-}*/
+ ) extends CommonSessionInterfacingFunctionality
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala
index f0dc4f03f..df53c650e 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala
@@ -2,262 +2,16 @@
package net.psforever.actors.session.support
import akka.actor.ActorContext
-import net.psforever.objects.ce.Deployable
-import net.psforever.objects.vehicles.MountableWeapons
-import net.psforever.objects._
-import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
-import net.psforever.packet.game._
-import net.psforever.services.Service
import net.psforever.services.local.LocalResponse
-import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
+import net.psforever.types.PlanetSideGUID
+
+trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: SessionLocalHandlers
+
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit
+}
class SessionLocalHandlers(
- val sessionData: SessionData,
+ val sessionLogic: SessionData,
implicit val context: ActorContext
- ) extends CommonSessionInterfacingFunctionality {
- /**
- * na
- * @param toChannel na
- * @param guid na
- * @param reply na
- */
- def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = {
- val resolvedPlayerGuid = if (player.HasGUID) {
- player.GUID
- } else {
- Service.defaultPlayerGUID
- }
- val isNotSameTarget = resolvedPlayerGuid != guid
- reply match {
- case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget =>
- sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
-
- case LocalResponse.DeployableUIFor(item) =>
- sessionData.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item))
-
- case LocalResponse.Detonate(dguid, _: BoomerDeployable) =>
- sendResponse(TriggerEffectMessage(dguid, "detonate_boomer"))
- sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) =>
- sendResponse(GenericObjectActionMessage(dguid, code=19))
- sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.Detonate(_, obj) =>
- log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
-
- case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget =>
- sendResponse(GenericObjectStateMsg(doorGuid, state=16))
-
- case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone
- sendResponse(GenericObjectStateMsg(doorGuid, state=17))
-
- case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed =>
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
- obj.Destroyed = true
- DeconstructDeployable(
- obj,
- dguid,
- pos,
- obj.Orientation,
- deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 }
- )
-
- case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _)
- if obj.Destroyed || obj.Jammed || obj.Health == 0 =>
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
- obj.Destroyed = true
- DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
-
- case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed =>
- //if active, deactivate
- obj.Active = false
- sendResponse(GenericObjectActionMessage(dguid, code=29))
- sendResponse(GenericObjectActionMessage(dguid, code=30))
- //standard deployable elimination behavior
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active =>
- //if active, deactivate
- obj.Active = false
- sendResponse(GenericObjectActionMessage(dguid, code=29))
- sendResponse(GenericObjectActionMessage(dguid, code=30))
- //standard deployable elimination behavior
- obj.Destroyed = true
- DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
-
- case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed =>
- //standard deployable elimination behavior
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
- //standard deployable elimination behavior
- obj.Destroyed = true
- DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
-
- case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed =>
- sendResponse(ObjectDeleteMessage(dguid, unk1=0))
-
- case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
- obj.Destroyed = true
- DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
-
- case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) =>
- sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2))
-
- case LocalResponse.HackObject(targetGuid, unk1, unk2) =>
- HackObject(targetGuid, unk1, unk2)
-
- case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) =>
- SendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue)
-
- case LocalResponse.GenericObjectAction(targetGuid, actionNumber) =>
- sendResponse(GenericObjectActionMessage(targetGuid, actionNumber))
-
- case LocalResponse.GenericActionMessage(actionNumber) =>
- sendResponse(GenericActionMessage(actionNumber))
-
- case LocalResponse.ChatMessage(msg) =>
- sendResponse(msg)
-
- case LocalResponse.SendPacket(packet) =>
- sendResponse(packet)
-
- case LocalResponse.LluSpawned(llu) =>
- // Create LLU on client
- sendResponse(ObjectCreateMessage(
- llu.Definition.ObjectId,
- llu.GUID,
- llu.Definition.Packet.ConstructorData(llu).get
- ))
- sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f))
-
- case LocalResponse.LluDespawned(lluGuid, position) =>
- sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f))
- sendResponse(ObjectDeleteMessage(lluGuid, unk1=0))
- // If the player was holding the LLU, remove it from their tracked special item slot
- sessionData.specialItemSlotGuid.collect { case guid if guid == lluGuid =>
- sessionData.specialItemSlotGuid = None
- player.Carrying = None
- }
-
- case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget =>
- sendResponse(ObjectDeleteMessage(objectGuid, unk))
-
- case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
- sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true))
-
- case LocalResponse.ProximityTerminalEffect(objectGuid, false) =>
- sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false))
- sessionData.terminals.ForgetAllProximityTerminals(objectGuid)
-
- case LocalResponse.RouterTelepadMessage(msg) =>
- sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None))
-
- case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) =>
- sessionData.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid)
-
- case LocalResponse.SendResponse(msg) =>
- sendResponse(msg)
-
- case LocalResponse.SetEmpire(objectGuid, empire) =>
- sendResponse(SetEmpireMessage(objectGuid, empire))
-
- case LocalResponse.ShuttleEvent(ev) =>
- val msg = OrbitalShuttleTimeMsg(
- ev.u1,
- ev.u2,
- ev.t1,
- ev.t2,
- ev.t3,
- pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) }
- )
- sendResponse(msg)
-
- case LocalResponse.ShuttleDock(pguid, sguid, slot) =>
- sendResponse(ObjectAttachMessage(pguid, sguid, slot))
-
- case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) =>
- sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient))
-
- case LocalResponse.ShuttleState(sguid, pos, orient, state) =>
- sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false))
-
- case LocalResponse.ToggleTeleportSystem(router, systemPlan) =>
- sessionData.toggleTeleportSystem(router, systemPlan)
-
- case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) =>
- sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation))
-
- case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
- sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
-
- case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) =>
- sendResponse(GenericObjectActionMessage(buildingGuid, 11))
-
- case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) =>
- sendResponse(GenericObjectActionMessage(buildingGuid, 12))
-
- case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid =>
- continent.GUID(vehicleGuid)
- .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
- .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
- .getOrElse(Set.empty)
- .collect { case weapon: Tool if weapon.GUID == weaponGuid =>
- sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
- }
-
- case _ => ()
- }
- }
-
- /**
- * Common behavior for deconstructing deployables in the game environment.
- * @param obj the deployable
- * @param guid the globally unique identifier for the deployable
- * @param pos the previous position of the deployable
- * @param orient the previous orientation of the deployable
- * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
- */
- def DeconstructDeployable(
- obj: Deployable,
- guid: PlanetSideGUID,
- pos: Vector3,
- orient: Vector3,
- deletionType: Int
- ): Unit = {
- sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
- sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
- sendResponse(ObjectDeleteMessage(guid, deletionType))
- }
-
- /**
- * na
- * @param targetGuid na
- * @param unk1 na
- * @param unk2 na
- */
- def HackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = {
- sendResponse(HackMessage(unk1=0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1, HackState.Hacked, unk2))
- }
-
- /**
- * Send a PlanetsideAttributeMessage packet to the client
- * @param targetGuid The target of the attribute
- * @param attributeType The attribute number
- * @param attributeValue The attribute value
- */
- def SendPlanetsideAttributeMessage(
- targetGuid: PlanetSideGUID,
- attributeType: PlanetsideAttributeEnum,
- attributeValue: Long
- ): Unit = {
- sendResponse(PlanetsideAttributeMessage(targetGuid, attributeType, attributeValue))
- }
-}
+ ) extends CommonSessionInterfacingFunctionality
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala
index 7fcda14b3..c124bd182 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala
@@ -2,361 +2,49 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
-import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
-import net.psforever.objects.vital.InGameHistory
-
-import scala.concurrent.duration._
+import net.psforever.objects.Tool
+import net.psforever.objects.vehicles.MountableWeapons
+import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg}
//
import net.psforever.actors.session.AvatarActor
-import net.psforever.actors.zone.ZoneActor
-import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
-import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition}
-import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime
+import net.psforever.objects.Player
import net.psforever.objects.serverobject.mount.Mountable
-import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
-import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
-import net.psforever.objects.vehicles.AccessPermissionGroup
-import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleMsg, GenericObjectActionMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
-import net.psforever.services.Service
-import net.psforever.services.local.{LocalAction, LocalServiceMessage}
-import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
-import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
+import net.psforever.packet.game.DismountVehicleMsg
+
+trait MountHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ val ops: SessionMountHandlers
+
+ def handleMountVehicle(pkt: MountVehicleMsg): Unit
+
+ def handleDismountVehicle(pkt: DismountVehicleMsg): Unit
+
+ def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit
+
+ def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit
+
+ def handle(tplayer: Player, reply: Mountable.Exchange): Unit
+}
class SessionMountHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
/**
- * na
- *
- * @param tplayer na
- * @param reply na
+ * From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines.
+ * @param objWithSeat the object that owns seats (and weaponry)
+ * @param seatNum the mount
*/
- def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
- reply match {
- case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- log.info(s"${player.Name} mounts an implant terminal")
- sessionData.terminals.CancelAllProximityUnits()
- MountingAction(tplayer, obj, seatNumber)
- sessionData.keepAliveFunc = sessionData.keepAlivePersistence
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if obj.Definition == GlobalDefinitions.orbital_shuttle =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the orbital shuttle")
- sessionData.terminals.CancelAllProximityUnits()
- MountingAction(tplayer, obj, seatNumber)
- sessionData.keepAliveFunc = sessionData.keepAlivePersistence
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if obj.Definition == GlobalDefinitions.ant =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
- val obj_guid: PlanetSideGUID = obj.GUID
- sessionData.terminals.CancelAllProximityUnits()
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
- sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
- sendResponse(GenericObjectActionMessage(obj_guid, code=11))
- sessionData.accessContainer(obj)
- tplayer.Actor ! ResetAllEnvironmentInteractions
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if obj.Definition == GlobalDefinitions.quadstealth =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
- val obj_guid: PlanetSideGUID = obj.GUID
- sessionData.terminals.CancelAllProximityUnits()
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
- sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
- //exclusive to the wraith, cloak state matches the cloak state of the driver
- //phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks
- obj.Cloaked = tplayer.Cloaked
- sendResponse(GenericObjectActionMessage(obj_guid, code=11))
- sessionData.accessContainer(obj)
- tplayer.Actor ! ResetAllEnvironmentInteractions
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
- val obj_guid: PlanetSideGUID = obj.GUID
- sessionData.terminals.CancelAllProximityUnits()
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
- sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
- sendResponse(GenericObjectActionMessage(obj_guid, code=11))
- sessionData.accessContainer(obj)
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- tplayer.Actor ! ResetAllEnvironmentInteractions
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if seatNumber == 0 =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
- val obj_guid: PlanetSideGUID = obj.GUID
- sessionData.terminals.CancelAllProximityUnits()
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
- sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
- sendResponse(GenericObjectActionMessage(obj_guid, code=11))
- sessionData.accessContainer(obj)
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- tplayer.Actor ! ResetAllEnvironmentInteractions
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if obj.Definition.MaxCapacitor > 0 =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts ${
- obj.SeatPermissionGroup(seatNumber) match {
- case Some(seatType) => s"a $seatType seat (#$seatNumber)"
- case None => "a seat"
- }
- } of the ${obj.Definition.Name}")
- val obj_guid: PlanetSideGUID = obj.GUID
- sessionData.terminals.CancelAllProximityUnits()
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
- sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
- sessionData.accessContainer(obj)
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- sessionData.keepAliveFunc = sessionData.keepAlivePersistence
- tplayer.Actor ! ResetAllEnvironmentInteractions
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the ${
- obj.SeatPermissionGroup(seatNumber) match {
- case Some(seatType) => s"a $seatType seat (#$seatNumber)"
- case None => "a seat"
- }
- } of the ${obj.Definition.Name}")
- val obj_guid: PlanetSideGUID = obj.GUID
- sessionData.terminals.CancelAllProximityUnits()
- sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
- sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
- sessionData.accessContainer(obj)
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- sessionData.keepAliveFunc = sessionData.keepAlivePersistence
- tplayer.Actor ! ResetAllEnvironmentInteractions
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
- if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
- obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
- sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
- if !obj.isUpgrading || System.currentTimeMillis() - getTurretUpgradeTime >= 1500L =>
- obj.setMiddleOfUpgrade(false)
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
- sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: FacilityTurret, _, _) =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.warn(
- s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
- )
-
- case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
- log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
- sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
- sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
- MountingAction(tplayer, obj, seatNumber)
-
- case Mountable.CanMount(obj: Mountable, _, _) =>
- log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}")
-
- case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
- log.info(s"${tplayer.Name} dismounts the implant terminal")
- DismountAction(tplayer, obj, seatNum)
-
- case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
- if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
- //dismount to hart lobby
- val pguid = player.GUID
- log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
- val sguid = obj.GUID
- val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint)
- tplayer.Position = pos
- sendResponse(DelayedPathMountMsg(pguid, sguid, u1=60, u2=true))
- continent.LocalEvents ! LocalServiceMessage(
- continent.id,
- LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang))
- )
- sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
-
- case Mountable.CanDismount(obj: Vehicle, seatNum, _)
- if obj.Definition == GlobalDefinitions.orbital_shuttle =>
- //get ready for orbital drop
- val pguid = player.GUID
- val events = continent.VehicleEvents
- log.info(s"${player.Name} is prepped for dropping")
- DismountAction(tplayer, obj, seatNum)
- continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
- //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
- events ! VehicleServiceMessage(
- player.Name,
- VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message
- )
- //when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky
- //the player will fall to the ground and is perfectly vulnerable in this state
- //additionally, our player must exist in the current zone
- //having no in-game avatar target will throw us out of the map screen when deploying and cause softlock
- events ! VehicleServiceMessage(
- player.Name,
- VehicleAction.SendResponse(
- Service.defaultPlayerGUID,
- PlayerStateShiftMessage(ShiftState(unk=0, obj.Position, obj.Orientation.z, vel=None)) //cower in the shuttle bay
- )
- )
- events ! VehicleServiceMessage(
- continent.id,
- VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player
- )
- sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
-
- case Mountable.CanDismount(obj: Vehicle, seatNum, _)
- if obj.Definition == GlobalDefinitions.droppod =>
- log.info(s"${tplayer.Name} has landed on ${continent.id}")
- sessionData.unaccessContainer(obj)
- DismountAction(tplayer, obj, seatNum)
- obj.Actor ! Vehicle.Deconstruct()
-
- case Mountable.CanDismount(obj: Vehicle, seatNum, _)
- if tplayer.GUID == player.GUID =>
- //disembarking self
- log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${
- obj.SeatPermissionGroup(seatNum) match {
- case Some(AccessPermissionGroup.Driver) => "driver seat"
- case Some(seatType) => s"$seatType seat (#$seatNum)"
- case None => "seat"
- }
- }")
- sessionData.vehicles.ConditionalDriverVehicleControl(obj)
- sessionData.unaccessContainer(obj)
- DismountVehicleAction(tplayer, obj, seatNum)
-
- case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
- )
-
- case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
- log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
- DismountAction(tplayer, obj, seatNum)
-
- case Mountable.CanDismount(obj: Mountable, _, _) =>
- log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}")
-
- case Mountable.CanNotMount(obj: Vehicle, seatNumber) =>
- log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
- obj.GetSeatFromMountPoint(seatNumber).collect {
- case seatNum if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
- sendResponse(
- ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, recipient="", "You are not the driver of this vehicle.", note=None)
- )
- }
-
- case Mountable.CanNotMount(obj: Mountable, seatNumber) =>
- log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
-
- case Mountable.CanNotDismount(obj, seatNum) =>
- log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed")
+ def updateWeaponAtSeatPosition(objWithSeat: MountableWeapons, seatNum: Int): Unit = {
+ objWithSeat.WeaponControlledFromSeat(seatNum) foreach {
+ case weapon: Tool =>
+ //update mounted weapon belonging to mount
+ weapon.AmmoSlots.foreach(slot => {
+ //update the magazine(s) in the weapon, specifically
+ val magazine = slot.Box
+ sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong))
+ })
+ case _ => () //no weapons to update
}
}
-
- /**
- * Common activities/procedure when a player mounts a valid object.
- * @param tplayer the player
- * @param obj the mountable object
- * @param seatNum the mount into which the player is mounting
- */
- def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
- val playerGuid: PlanetSideGUID = tplayer.GUID
- val objGuid: PlanetSideGUID = obj.GUID
- sessionData.playerActionsToCancel()
- avatarActor ! AvatarActor.DeactivateActiveImplants()
- avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
- sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.MountVehicle(playerGuid, objGuid, seatNum)
- )
- }
-
- /**
- * Common activities/procedure when a player dismounts a valid mountable object.
- * @param tplayer the player
- * @param obj the mountable object
- * @param seatNum the mount out of which which the player is disembarking
- */
- def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
- DismountAction(tplayer, obj, seatNum)
- //until vehicles maintain synchronized momentum without a driver
- obj match {
- case v: Vehicle
- if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
- sessionData.vehicles.serverVehicleControlVelocity.collect { _ =>
- sessionData.vehicles.ServerVehicleOverrideStop(v)
- }
- v.Velocity = Vector3.Zero
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.VehicleState(
- tplayer.GUID,
- v.GUID,
- unk1 = 0,
- v.Position,
- v.Orientation,
- vel = None,
- v.Flying,
- unk3 = 0,
- unk4 = 0,
- wheel_direction = 15,
- unk5 = false,
- unk6 = v.Cloaked
- )
- )
- case _ => ()
- }
- }
-
- /**
- * Common activities/procedure when a player dismounts a valid mountable object.
- * @param tplayer the player
- * @param obj the mountable object
- * @param seatNum the mount out of which which the player is disembarking
- */
- def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
- val playerGuid: PlanetSideGUID = tplayer.GUID
- tplayer.ContributionFrom(obj)
- sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
- val bailType = if (tplayer.BailProtection) {
- BailType.Bailed
- } else {
- BailType.Normal
- }
- sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
- )
- }
}
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala
index e3ff2bb0e..470db3569 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala
@@ -4,40 +4,48 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, ActorRef, typed}
import scala.collection.mutable
//
-import net.psforever.actors.session.{AvatarActor, ChatActor}
-import net.psforever.objects.avatar.Avatar
+import net.psforever.actors.session.AvatarActor
import net.psforever.objects.teamwork.Squad
-import net.psforever.objects.{Default, LivePlayerList, Player}
+import net.psforever.objects.{Default, Player}
import net.psforever.packet.game._
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-import net.psforever.services.chat.ChatService
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
-import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, SquadListDecoration, SquadResponseType, Vector3, WaypointSubtype}
+import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
+
+trait SquadHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ val ops: SessionSquadHandlers
+
+ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit
+
+ def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit
+
+ def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit
+
+ def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit
+}
object SessionSquadHandlers {
- protected final case class SquadUIElement(
- name: String,
- outfit: Long,
- index: Int,
- zone: Int,
- health: Int,
- armor: Int,
- position: Vector3
- )
+ final case class SquadUIElement(
+ name: String,
+ outfit: Long,
+ index: Int,
+ zone: Int,
+ health: Int,
+ armor: Int,
+ position: Vector3
+ )
}
class SessionSquadHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- chatActor: typed.ActorRef[ChatActor.Command],
- squadService: ActorRef,
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ val squadService: ActorRef,
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
import SessionSquadHandlers._
- private var waypointCooldown: Long = 0L
- val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]()
- var squad_supplement_id: Int = 0
+ private[session] val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]()
+ private[session] var squad_supplement_id: Int = 0
/**
* When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked.
* This `WorldSessionActor`-local variable is then used to indicate the ongoing state of the LFS UI component,
@@ -46,340 +54,11 @@ class SessionSquadHandlers(
* Upon leaving or disbanding a squad, this value is made false.
* Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
*/
- private[support] var squadSetup: () => Unit = FirstTimeSquadSetup
- private var squadUpdateCounter: Int = 0
+ private[session] var squadSetup: () => Unit = FirstTimeSquadSetup
+ private[session] var squadUpdateCounter: Int = 0
+ private[session] var updateSquad: () => Unit = NoSquadUpdates
+ private[session] var updateSquadRef: ActorRef = Default.Actor
private val queuedSquadActions: Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
- private[support] var updateSquad: () => Unit = NoSquadUpdates
- private var updateSquadRef: ActorRef = Default.Actor
-
- /* packet */
-
- def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
- val SquadDefinitionActionMessage(u1, u2, action) = pkt
- squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
- }
-
- def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
- val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt
- squadService ! SquadServiceMessage(
- player,
- continent,
- SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5)
- )
- }
-
- def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {
- val SquadWaypointRequest(request, _, wtype, unk, info) = pkt
- val time = System.currentTimeMillis()
- val subtype = wtype.subtype
- if(subtype == WaypointSubtype.Squad) {
- squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
- } else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) {
- //guarding against duplicating laze waypoints
- waypointCooldown = time
- squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
- }
- }
-
- /* response handlers */
-
- def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
- if (!excluded.exists(_ == avatar.id)) {
- response match {
- case SquadResponse.ListSquadFavorite(line, task) =>
- sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
-
- case SquadResponse.InitList(infos) =>
- sendResponse(ReplicationStreamMessage(infos))
-
- case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
- sendResponse(
- ReplicationStreamMessage(
- 6,
- None,
- infos.map {
- case (index, squadInfo) =>
- SquadListing(index, squadInfo)
- }.toVector
- )
- )
-
- case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
- sendResponse(
- ReplicationStreamMessage(
- 1,
- None,
- infos.map { index =>
- SquadListing(index, None)
- }.toVector
- )
- )
-
- case SquadResponse.SquadDecoration(guid, squad) =>
- val decoration = if (
- squadUI.nonEmpty ||
- squad.Size == squad.Capacity ||
- {
- val offer = avatar.certifications
- !squad.Membership.exists { _.isAvailable(offer) }
- }
- ) {
- SquadListDecoration.NotAvailable
- } else {
- SquadListDecoration.Available
- }
- sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration)))
-
- case SquadResponse.Detail(guid, detail) =>
- sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
-
- case SquadResponse.IdentifyAsSquadLeader(squad_guid) =>
- sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
-
- case SquadResponse.SetListSquad(squad_guid) =>
- sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
-
- case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) =>
- val name = request_type match {
- case SquadResponseType.Invite if unk5 =>
- //the name of the player indicated by unk3 is needed
- LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
- case Some(player) =>
- player.name
- case None =>
- player_name
- }
- case _ =>
- player_name
- }
- sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6))
-
- case SquadResponse.WantsSquadPosition(_, name) =>
- sendResponse(
- ChatMsg(
- ChatMessageType.CMT_SQUAD,
- wideContents=true,
- name,
- s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
- None
- )
- )
-
- case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
- val avatarId = avatar.id
- val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
- .filter { case (mem, index) =>
- mem.CharId > 0 && positionsToUpdate.contains(index)
- }
- membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
- case Some((ourMember, ourIndex)) =>
- //we are joining the squad
- //load each member's entry (our own too)
- squad_supplement_id = squad.GUID.guid + 1
- membershipPositions.foreach {
- case (member, index) =>
- sendResponse(
- SquadMemberEvent.Add(
- squad_supplement_id,
- member.CharId,
- index,
- member.Name,
- member.ZoneId,
- outfit_id = 0
- )
- )
- squadUI(member.CharId) =
- SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
- }
- //repeat our entry
- sendResponse(
- SquadMemberEvent.Add(
- squad_supplement_id,
- ourMember.CharId,
- ourIndex,
- ourMember.Name,
- ourMember.ZoneId,
- outfit_id = 0
- )
- )
- //turn lfs off
- if (avatar.lookingForSquad) {
- avatarActor ! AvatarActor.SetLookingForSquad(false)
- }
- val playerGuid = player.GUID
- val factionChannel = s"${player.Faction}"
- //squad colors
- GiveSquadColorsToMembers()
- GiveSquadColorsForOthers(playerGuid, factionChannel, squad_supplement_id)
- //associate with member position in squad
- sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
- //a finalization? what does this do?
- sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
- squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
- updateSquadRef = ref
- updateSquad = PeriodicUpdatesWhenEnrolledInSquad
- chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID))
- case _ =>
- //other player is joining our squad
- //load each member's entry
- GiveSquadColorsToMembers(
- membershipPositions.map {
- case (member, index) =>
- val charId = member.CharId
- sendResponse(
- SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
- )
- squadUI(charId) =
- SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
- charId
- }
- )
- }
- //send an initial dummy update for map icon(s)
- sendResponse(
- SquadState(
- PlanetSideGUID(squad_supplement_id),
- membershipPositions.map { case (member, _) =>
- SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
- }
- )
- )
-
- case SquadResponse.Leave(squad, positionsToUpdate) =>
- positionsToUpdate.find({ case (member, _) => member == avatar.id }) match {
- case Some((ourMember, ourIndex)) =>
- //we are leaving the squad
- //remove each member's entry (our own too)
- updateSquadRef = Default.Actor
- positionsToUpdate.foreach {
- case (member, index) =>
- sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
- squadUI.remove(member)
- }
- //uninitialize
- val playerGuid = player.GUID
- sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
- GiveSquadColorsToSelf(value = 0)
- sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
- sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
- avatarActor ! AvatarActor.SetLookingForSquad(false)
- //a finalization? what does this do?
- sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
- squad_supplement_id = 0
- squadUpdateCounter = 0
- updateSquad = NoSquadUpdates
- chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
- case _ =>
- //remove each member's entry
- GiveSquadColorsToMembers(
- positionsToUpdate.map {
- case (member, index) =>
- sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
- squadUI.remove(member)
- member
- },
- value = 0
- )
- }
-
- case SquadResponse.AssignMember(squad, from_index, to_index) =>
- //we've already swapped position internally; now we swap the cards
- SwapSquadUIElements(squad, from_index, to_index)
-
- case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
- if (promotedPlayer != player.CharId) {
- //demoted from leader; no longer lfsm
- if (player.avatar.lookingForSquad) {
- avatarActor ! AvatarActor.SetLookingForSquad(false)
- }
- }
- sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
- //the players have already been swapped in the backend object
- PromoteSquadUIElements(squad, from_index)
-
- case SquadResponse.UpdateMembers(_, positions) =>
- val pairedEntries = positions.collect {
- case entry if squadUI.contains(entry.char_id) =>
- (entry, squadUI(entry.char_id))
- }
- //prune entries
- val updatedEntries = pairedEntries
- .collect({
- case (entry, element) if entry.zone_number != element.zone =>
- //zone gets updated for these entries
- sendResponse(
- SquadMemberEvent.UpdateZone(squad_supplement_id, entry.char_id, element.index, entry.zone_number)
- )
- squadUI(entry.char_id) =
- SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
- entry
- case (entry, element)
- if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
- //other elements that need to be updated
- squadUI(entry.char_id) =
- SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
- entry
- })
- .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend
- if (updatedEntries.nonEmpty) {
- sendResponse(
- SquadState(
- PlanetSideGUID(squad_supplement_id),
- updatedEntries.map { entry =>
- SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
- }
- )
- )
- }
-
- case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
- sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
-
- case SquadResponse.SquadSearchResults(results) =>
- //TODO positive squad search results message?
-// if(results.nonEmpty) {
-// results.foreach { guid =>
-// sendResponse(SquadDefinitionActionMessage(
-// guid,
-// 0,
-// SquadAction.SquadListDecorator(SquadListDecoration.SearchResult))
-// )
-// }
-// } else {
-// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults()))
-// }
-// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch()))
-
- case SquadResponse.InitWaypoints(char_id, waypoints) =>
- waypoints.foreach {
- case (waypoint_type, info, unk) =>
- sendResponse(
- SquadWaypointEvent.Add(
- squad_supplement_id,
- char_id,
- waypoint_type,
- WaypointEvent(info.zone_number, info.pos, unk)
- )
- )
- }
-
- case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
- sendResponse(
- SquadWaypointEvent.Add(
- squad_supplement_id,
- char_id,
- waypoint_type,
- WaypointEvent(info.zone_number, info.pos, unk)
- )
- )
-
- case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
- sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type))
-
- case _ => ;
- }
- }
- }
/**
* These messages are dispatched when first starting up the client and connecting to the server for the first time.
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 30333abe3..21cc9f9d6 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala
@@ -2,181 +2,42 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
-import net.psforever.objects.sourcing.AmenitySource
-import net.psforever.objects.vital.TerminalUsedActivity
+import net.psforever.objects.guid.GUIDTask
+import net.psforever.packet.game.FavoritesRequest
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
//
import net.psforever.actors.session.AvatarActor
-import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory}
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
-import net.psforever.objects.guid.{StraightforwardTask, TaskBundle, TaskWorkflow}
+import net.psforever.objects.guid.{StraightforwardTask, TaskBundle}
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.equipment.EffectTarget
import net.psforever.objects.serverobject.CommonMessages
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
import net.psforever.objects.serverobject.terminals.{ProximityDefinition, ProximityUnit, Terminal}
-import net.psforever.packet.game.{ItemTransactionMessage, ItemTransactionResultMessage,ProximityTerminalUseMessage, UnuseItemMessage}
-import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
+import net.psforever.packet.game.{ItemTransactionMessage,ProximityTerminalUseMessage}
+import net.psforever.types.PlanetSideGUID
+
+trait TerminalHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: SessionTerminalHandlers
+
+ def handleItemTransaction(pkt: ItemTransactionMessage): Unit
+
+ def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit
+
+ def handleFavoritesRequest(pkt: FavoritesRequest): Unit
+
+ def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit
+}
class SessionTerminalHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
- private[support] var lastTerminalOrderFulfillment: Boolean = true
- private[support] var usingMedicalTerminal: Option[PlanetSideGUID] = None
-
- /* packets */
-
- def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
- val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
- continent.GUID(terminalGuid) match {
- case Some(term: Terminal) if lastTerminalOrderFulfillment =>
- val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
- log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
- lastTerminalOrderFulfillment = false
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
- term.Actor ! Terminal.Request(player, pkt)
- case Some(_: Terminal) =>
- log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}")
- case Some(obj) =>
- log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}")
- case _ =>
- log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}")
- }
- }
-
- def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
- val ProximityTerminalUseMessage(_, objectGuid, _) = pkt
- continent.GUID(objectGuid) match {
- case Some(obj: Terminal with ProximityUnit) =>
- HandleProximityTerminalUse(obj)
- case Some(obj) =>
- log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects")
- case None =>
- log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}")
- }
- }
-
- /* response handler */
-
- /**
- * na
- * @param tplayer na
- * @param msg na
- * @param order na
- */
- def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
- order match {
- case Terminal.BuyEquipment(item)
- if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
- sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
- lastTerminalOrderFulfillment = true
-
- case Terminal.BuyEquipment(item) =>
- avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
- TaskWorkflow.execute(BuyNewEquipmentPutInInventory(
- continent.GUID(tplayer.VehicleSeated) match {
- case Some(v: Vehicle) => v
- case _ => player
- },
- tplayer,
- msg.terminal_guid
- )(item))
-
- case Terminal.SellEquipment() =>
- SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
-
- case Terminal.LearnCertification(cert) =>
- avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert)
- lastTerminalOrderFulfillment = true
-
- case Terminal.SellCertification(cert) =>
- avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert)
- lastTerminalOrderFulfillment = true
-
- case Terminal.LearnImplant(implant) =>
- avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant)
- lastTerminalOrderFulfillment = true
-
- case Terminal.SellImplant(implant) =>
- avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant)
- lastTerminalOrderFulfillment = true
-
- case Terminal.BuyVehicle(vehicle, _, _)
- if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || tplayer.spectator =>
- sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
- lastTerminalOrderFulfillment = true
-
- case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
- continent.map.terminalToSpawnPad
- .find { case (termid, _) => termid == msg.terminal_guid.guid }
- .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) }
- .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
- avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition)
- vehicle.Faction = tplayer.Faction
- vehicle.Position = pad.Position
- vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
- //default loadout, weapons
- val vWeapons = vehicle.Weapons
- weapons.foreach { entry =>
- vWeapons.get(entry.start) match {
- case Some(slot) =>
- entry.obj.Faction = tplayer.Faction
- slot.Equipment = None
- slot.Equipment = entry.obj
- case None =>
- log.warn(
- s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}"
- )
- }
- }
- //default loadout, trunk
- val vTrunk = vehicle.Trunk
- vTrunk.Clear()
- trunk.foreach { entry =>
- entry.obj.Faction = tplayer.Faction
- vTrunk.InsertQuickly(entry.start, entry.obj)
- }
- TaskWorkflow.execute(registerVehicleFromSpawnPad(vehicle, pad, term))
- sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true))
- if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
- sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
- }
- player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
- }
- .orElse {
- log.error(
- s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
- )
- sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
- None
- }
- lastTerminalOrderFulfillment = true
-
- case Terminal.NoDeal() if msg != null =>
- val transaction = msg.transaction_type
- log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order")
- sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false))
- lastTerminalOrderFulfillment = true
-
- case _ =>
- val terminal = msg.terminal_guid.guid
- continent.GUID(terminal) match {
- case Some(term: Terminal) =>
- log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order")
- case Some(_) =>
- log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal")
- case None =>
- log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal")
- }
- lastTerminalOrderFulfillment = true
- }
- }
-
- /* support */
+ private[session] var lastTerminalOrderFulfillment: Boolean = true
+ private[session] var usingMedicalTerminal: Option[PlanetSideGUID] = None
/**
* Construct tasking that adds a completed and registered vehicle into the scene.
@@ -188,7 +49,7 @@ class SessionTerminalHandlers(
* @see `RegisterVehicle`
* @return a `TaskBundle` message
*/
- private[session] def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = {
+ def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = {
TaskBundle(
new StraightforwardTask() {
private val localVehicle = vehicle
@@ -203,7 +64,7 @@ class SessionTerminalHandlers(
Future(true)
}
},
- List(sessionData.registerVehicle(vehicle))
+ List(registerVehicle(vehicle))
)
}
@@ -293,7 +154,7 @@ class SessionTerminalHandlers(
/**
* Cease all current interactions with proximity-based units.
- * Pair with `PlayerActionsToCancel`, except when logging out (stopping).
+ * Pair with `actionsToCancel`, except when logging out (stopping).
* This operations may invoke callback messages.
* @see `postStop`
*/
@@ -303,7 +164,7 @@ class SessionTerminalHandlers(
/**
* Cease all current interactions with proximity-based units.
- * Pair with `PlayerActionsToCancel`, except when logging out (stopping).
+ * Pair with `actionsToCancel`, except when logging out (stopping).
* This operations may invoke callback messages.
* @param guid globally unique identifier for a proximity terminal
* @see `postStop`
@@ -326,4 +187,29 @@ class SessionTerminalHandlers(
usingMedicalTerminal = None
}
}
+
+ /**
+ * Construct tasking that adds a completed and registered vehicle into the scene.
+ * Use this function to renew the globally unique identifiers on a vehicle that has already been added to the scene once.
+ * @param vehicle the `Vehicle` object
+ * @see `RegisterVehicleFromSpawnPad`
+ * @return a `TaskBundle` message
+ */
+ def registerVehicle(vehicle: Vehicle): TaskBundle = {
+ TaskBundle(
+ new StraightforwardTask() {
+ private val localVehicle = vehicle
+
+ override def description(): String = s"register a ${localVehicle.Definition.Name}"
+
+ def action(): Future[Any] = Future(true)
+ },
+ List(GUIDTask.registerVehicle(continent.GUID, vehicle))
+ )
+ }
+
+ override protected[session] def actionsToCancel(): Unit = {
+ lastTerminalOrderFulfillment = true
+ usingMedicalTerminal = None
+ }
}
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala
index 915390ee5..5389b6bb9 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala
@@ -3,390 +3,18 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.AvatarActor
-import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
-import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
-import net.psforever.objects.serverobject.mount.Mountable
-import net.psforever.objects.serverobject.pad.VehicleSpawnPad
-import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
-import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
-import net.psforever.packet.game._
-import net.psforever.services.Service
-import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
-import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
+import net.psforever.services.vehicle.VehicleResponse
+import net.psforever.types.PlanetSideGUID
-import scala.concurrent.duration._
+trait VehicleHandlerFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: SessionVehicleHandlers
+
+ def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit
+}
class SessionVehicleHandlers(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- galaxyService: ActorRef,
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ val galaxyService: ActorRef,
implicit val context: ActorContext
- ) extends CommonSessionInterfacingFunctionality {
- /**
- * na
- *
- * @param toChannel na
- * @param guid na
- * @param reply na
- */
- def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = {
- val resolvedPlayerGuid = if (player.HasGUID) {
- player.GUID
- } else {
- PlanetSideGUID(-1)
- }
- val isNotSameTarget = resolvedPlayerGuid != guid
- reply match {
- case VehicleResponse.VehicleState(
- vehicleGuid,
- unk1,
- pos,
- orient,
- vel,
- unk2,
- unk3,
- unk4,
- wheelDirection,
- unk5,
- unk6
- ) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) =>
- //player who is also in the vehicle (not driver)
- sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
- player.Position = pos
- player.Orientation = orient
- player.Velocity = vel
- sessionData.updateLocalBlockMap(pos)
-
- case VehicleResponse.VehicleState(
- vehicleGuid,
- unk1,
- pos,
- ang,
- vel,
- unk2,
- unk3,
- unk4,
- wheelDirection,
- unk5,
- unk6
- ) if isNotSameTarget =>
- //player who is watching the vehicle from the outside
- sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, ang, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
-
- case VehicleResponse.ChildObjectState(objectGuid, pitch, yaw) if isNotSameTarget =>
- sendResponse(ChildObjectStateMessage(objectGuid, pitch, yaw))
-
- case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)
- if isNotSameTarget =>
- sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA))
-
- case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget =>
- sendResponse(ChangeFireStateMessage_Start(weaponGuid))
-
- case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget =>
- sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
-
- case VehicleResponse.Reload(itemGuid) if isNotSameTarget =>
- sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
-
- case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget =>
- sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
- //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0))
- sendResponse(
- ObjectCreateMessage(
- ammo_id,
- ammo_guid,
- ObjectCreateMessageParent(weapon_guid, weapon_slot),
- ammo_data
- )
- )
- sendResponse(ChangeAmmoMessage(weapon_guid, 1))
-
- case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget =>
- continent.GUID(weaponGuid).collect {
- case tool: Tool if tool.Magazine == 0 =>
- // check that the magazine is still empty before sending WeaponDryFireMessage
- // if it has been reloaded since then, other clients will not see it firing
- sendResponse(WeaponDryFireMessage(weaponGuid))
- }
-
- case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget =>
- sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver))
-
- case VehicleResponse.MountVehicle(vehicleGuid, seat) if isNotSameTarget =>
- sendResponse(ObjectAttachMessage(vehicleGuid, guid, seat))
-
- case VehicleResponse.DeployRequest(objectGuid, state, unk1, unk2, pos) if isNotSameTarget =>
- sendResponse(DeployRequestMessage(guid, objectGuid, state, unk1, unk2, pos))
-
- case VehicleResponse.SendResponse(msg) =>
- sendResponse(msg)
-
- case VehicleResponse.AttachToRails(vehicleGuid, padGuid) =>
- sendResponse(ObjectAttachMessage(padGuid, vehicleGuid, slot=3))
-
- case VehicleResponse.ConcealPlayer(playerGuid) =>
- sendResponse(GenericObjectActionMessage(playerGuid, code=9))
-
- case VehicleResponse.DetachFromRails(vehicleGuid, padGuid, padPosition, padOrientationZ) =>
- val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad].Definition
- sendResponse(
- ObjectDetachMessage(
- padGuid,
- vehicleGuid,
- padPosition + Vector3.z(pad.VehicleCreationZOffset),
- padOrientationZ + pad.VehicleCreationZOrientOffset
- )
- )
-
- case VehicleResponse.EquipmentInSlot(pkt) if isNotSameTarget =>
- sendResponse(pkt)
-
- case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget =>
- sendResponse(GenericObjectActionMessage(objectGuid, action))
-
- case VehicleResponse.HitHint(sourceGuid) if player.isAlive =>
- sendResponse(HitHint(sourceGuid, player.GUID))
-
- case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget =>
- //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
- val objGuid = obj.GUID
- sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
- sendResponse(ObjectCreateDetailedMessage(
- obj.Definition.ObjectId,
- objGuid,
- ObjectCreateMessageParent(parentGuid, start),
- conData
- ))
-
- case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicleGuid) if resolvedPlayerGuid == guid =>
- //seat number (first field) seems to be correct if passenger is kicked manually by driver
- //but always seems to return 4 if user is kicked by mount permissions changing
- sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
- val typeOfRide = continent.GUID(vehicleGuid) match {
- case Some(obj: Vehicle) =>
- sessionData.unaccessContainer(obj)
- s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}"
- case _ =>
- s"${player.Sex.possessive} ride"
- }
- log.info(s"${player.Name} has been kicked from $typeOfRide!")
-
- case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) =>
- //seat number (first field) seems to be correct if passenger is kicked manually by driver
- //but always seems to return 4 if user is kicked by mount permissions changing
- sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
-
- case VehicleResponse.InventoryState2(objGuid, parentGuid, value) if isNotSameTarget =>
- sendResponse(InventoryStateMessage(objGuid, unk=0, parentGuid, value))
-
- case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) if isNotSameTarget =>
- //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible)
- sendResponse(ObjectCreateMessage(vtype, vguid, vdata))
- Vehicles.ReloadAccessPermissions(vehicle, player.Name)
-
- case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget =>
- sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
-
- case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid =>
- //Only the player that owns this vehicle needs the ownership packet
- avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))
- sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid))
-
- case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget =>
- sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue))
-
- case VehicleResponse.ResetSpawnPad(padGuid) =>
- sendResponse(GenericObjectActionMessage(padGuid, code=23))
-
- case VehicleResponse.RevealPlayer(playerGuid) =>
- sendResponse(GenericObjectActionMessage(playerGuid, code=10))
-
- case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget =>
- sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission))
-
- case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget =>
- //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
- sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData))
-
- case VehicleResponse.UnloadVehicle(_, vehicleGuid) =>
- sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0))
-
- case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget =>
- //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
- sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
-
- case VehicleResponse.UpdateAmsSpawnPoint(list) =>
- sessionData.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
- sessionData.zoning.spawn.DrawCurrentAmsSpawnPoint()
-
- case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget =>
- sessionData.zoning.interstellarFerry = Some(vehicle)
- sessionData.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete)
- continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
- galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel
- log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating")
-
- case VehicleResponse.KickCargo(vehicle, speed, delay)
- if player.VehicleSeated.nonEmpty && sessionData.zoning.spawn.deadState == DeadState.Alive && speed > 0 =>
- val strafe = 1 + Vehicles.CargoOrientation(vehicle)
- val reverseSpeed = if (strafe > 1) { 0 } else { speed }
- //strafe or reverse, not both
- sessionData.vehicles.ServerVehicleOverrideWithPacket(
- vehicle,
- ServerVehicleOverrideMsg(
- lock_accelerator=true,
- lock_wheel=true,
- reverse=true,
- unk4=false,
- lock_vthrust=0,
- strafe,
- reverseSpeed,
- unk8=Some(0)
- )
- )
- import scala.concurrent.ExecutionContext.Implicits.global
- context.system.scheduler.scheduleOnce(
- delay milliseconds,
- context.self,
- VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay))
- )
-
- case VehicleResponse.KickCargo(cargo, _, _)
- if player.VehicleSeated.nonEmpty && sessionData.zoning.spawn.deadState == DeadState.Alive =>
- sessionData.vehicles.TotalDriverVehicleControl(cargo)
-
- case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _)
- if player.VisibleSlots.contains(player.DrawnSlot) =>
- player.DrawnSlot = Player.HandsDownSlot
- startPlayerSeatedInVehicle(vehicle)
-
- case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
- startPlayerSeatedInVehicle(vehicle)
-
- case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
- Vehicles.ReloadAccessPermissions(vehicle, player.Name)
- sessionData.vehicles.ServerVehicleOverrideWithPacket(
- vehicle,
- ServerVehicleOverrideMsg(
- lock_accelerator=true,
- lock_wheel=true,
- reverse=true,
- unk4=false,
- lock_vthrust=1,
- lock_strafe=0,
- movement_speed=0,
- unk8=Some(0)
- )
- )
- sessionData.vehicles.serverVehicleControlVelocity = Some(0)
-
- case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
- val vdef = vehicle.Definition
- sessionData.vehicles.ServerVehicleOverrideWithPacket(
- vehicle,
- ServerVehicleOverrideMsg(
- lock_accelerator=true,
- lock_wheel=true,
- reverse=false,
- unk4=false,
- lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 },
- lock_strafe=0,
- movement_speed=vdef.AutoPilotSpeed1,
- unk8=Some(0)
- )
- )
-
- case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
- sessionData.vehicles.ServerVehicleOverrideStop(vehicle)
-
- case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
- sendResponse(ChatMsg(
- ChatMessageType.CMT_OPEN,
- wideContents=true,
- recipient="",
- s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
- note=None
- ))
-
- case VehicleResponse.PeriodicReminder(_, data) =>
- val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match {
- case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg)
- case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg)
- case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.")
- }
- sendResponse(ChatMsg(isType, flag, recipient="", msg, None))
-
- case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory)
- if player.avatar.vehicle.contains(target) =>
- //TODO when vehicle weapons can be changed without visual glitches, rewrite this
- continent.GUID(target).collect { case vehicle: Vehicle =>
- import net.psforever.login.WorldSession.boolToInt
- //owner: must unregister old equipment, and register and install new equipment
- (oldWeapons ++ oldInventory).foreach {
- case (obj, eguid) =>
- sendResponse(ObjectDeleteMessage(eguid, unk1=0))
- TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
- }
- sessionData.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory)
- //jammer or unjamm new weapons based on vehicle status
- val vehicleJammered = vehicle.Jammed
- addedWeapons
- .map { _.obj }
- .collect {
- case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered =>
- jamItem.Jammed = vehicleJammered
- JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered)
- }
- changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
- }
-
- case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _)
- if sessionData.accessedContainer.map { _.GUID }.contains(target) =>
- //TODO when vehicle weapons can be changed without visual glitches, rewrite this
- continent.GUID(target).collect { case vehicle: Vehicle =>
- //external participant: observe changes to equipment
- (oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
- changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
- }
-
- case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) =>
- //TODO when vehicle weapons can be changed without visual glitches, rewrite this
- continent.GUID(target).collect { case vehicle: Vehicle =>
- changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
- }
-
- case _ => ()
- }
- }
-
- private def changeLoadoutDeleteOldEquipment(
- vehicle: Vehicle,
- oldWeapons: Iterable[(Equipment, PlanetSideGUID)],
- oldInventory: Iterable[(Equipment, PlanetSideGUID)]
- ): Unit = {
- vehicle.PassengerInSeat(player) match {
- case Some(seatNum) =>
- //participant: observe changes to equipment
- (oldWeapons ++ oldInventory).foreach {
- case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0))
- }
- sessionData.updateWeaponAtSeatPosition(vehicle, seatNum)
- case None =>
- //observer: observe changes to external equipment
- oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
- }
- }
-
- private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = {
- val vehicle_guid = vehicle.GUID
- sessionData.playerActionsToCancel()
- sessionData.terminals.CancelAllProximityUnits()
- sessionData.vehicles.serverVehicleControlVelocity = Some(0)
- sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off
- sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership
- vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect {
- case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
- }
- }
-}
+ ) extends CommonSessionInterfacingFunctionality
diff --git a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala
index a3a91f61f..d56107506 100644
--- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala
@@ -5,466 +5,39 @@ import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable
-import net.psforever.objects.vehicles.control.BfrFlight
-import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior}
import net.psforever.objects.zones.Zone
import net.psforever.objects._
-import net.psforever.objects.serverobject.PlanetSideServerObject
-import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, MountVehicleCargoMsg, MountVehicleMsg, VehicleSubStateMessage, _}
-import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
-import net.psforever.types.{BailType, DriveState, Vector3}
+import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _}
+import net.psforever.types.DriveState
-class VehicleOperations(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- implicit val context: ActorContext
- ) extends CommonSessionInterfacingFunctionality {
- private[support] var serverVehicleControlVelocity: Option[Int] = None
+trait VehicleFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: VehicleOperations
- /* packets */
+ def handleVehicleState(pkt: VehicleStateMessage): Unit
- def handleVehicleState(pkt: VehicleStateMessage): Unit = {
- val VehicleStateMessage(
- vehicle_guid,
- unk1,
- pos,
- ang,
- vel,
- is_flying,
- unk6,
- unk7,
- wheels,
- is_decelerating,
- is_cloaked
- ) = pkt
- GetVehicleAndSeat() match {
- case (Some(obj), Some(0)) =>
- //we're driving the vehicle
- sessionData.persist()
- sessionData.turnCounterFunc(player.GUID)
- sessionData.fallHeightTracker(pos.z)
- if (obj.MountedIn.isEmpty) {
- sessionData.updateBlockMap(obj, pos)
- }
- player.Position = pos //convenient
- if (obj.WeaponControlledFromSeat(0).isEmpty) {
- player.Orientation = Vector3.z(ang.z) //convenient
- }
- obj.Position = pos
- obj.Orientation = ang
- if (obj.MountedIn.isEmpty) {
- if (obj.DeploymentState != DriveState.Deployed) {
- obj.Velocity = vel
- } else {
- obj.Velocity = Some(Vector3.Zero)
- }
- if (obj.Definition.CanFly) {
- obj.Flying = is_flying //usually Some(7)
- }
- obj.Cloaked = obj.Definition.CanCloak && is_cloaked
- } else {
- obj.Velocity = None
- obj.Flying = None
- }
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.VehicleState(
- player.GUID,
- vehicle_guid,
- unk1,
- obj.Position,
- ang,
- obj.Velocity,
- if (obj.isFlying) {
- is_flying
- } else {
- None
- },
- unk6,
- unk7,
- wheels,
- is_decelerating,
- obj.Cloaked
- )
- )
- sessionData.squad.updateSquad()
- obj.zoneInteractions()
- case (None, _) =>
- //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
- //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
- case (_, Some(index)) =>
- log.error(
- s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
- )
- case _ => ;
- }
- if (player.death_by == -1) {
- sessionData.kickedByAdministration()
- }
- }
+ def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit
- def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
- val FrameVehicleStateMessage(
- vehicle_guid,
- unk1,
- pos,
- ang,
- vel,
- unk2,
- unk3,
- unk4,
- is_crouched,
- is_airborne,
- ascending_flight,
- flight_time,
- unk9,
- unkA
- ) = pkt
- GetVehicleAndSeat() match {
- case (Some(obj), Some(0)) =>
- //we're driving the vehicle
- sessionData.persist()
- sessionData.turnCounterFunc(player.GUID)
- val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
- case Some(v: Vehicle) =>
- sessionData.updateBlockMap(obj, pos)
- (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
- case _ =>
- (pos, ang, vel, true)
- }
- player.Position = position //convenient
- if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
- player.Orientation = Vector3.z(ang.z) //convenient
- }
- obj.Position = position
- obj.Orientation = angle
- obj.Velocity = velocity
- // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
- // //dev stuff goes here
- // }
- // else
- // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
- // //dev stuff goes here
- // }
- obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
- if (notMountedState) {
- if (obj.DeploymentState != DriveState.Kneeling) {
- if (is_airborne) {
- val flight = if (ascending_flight) flight_time else -flight_time
- obj.Flying = Some(flight)
- obj.Actor ! BfrFlight.Soaring(flight)
- } else if (obj.Flying.nonEmpty) {
- obj.Flying = None
- obj.Actor ! BfrFlight.Landed
- }
- } else {
- obj.Velocity = None
- obj.Flying = None
- }
- obj.zoneInteractions()
- } else {
- obj.Velocity = None
- obj.Flying = None
- }
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.FrameVehicleState(
- player.GUID,
- vehicle_guid,
- unk1,
- position,
- angle,
- velocity,
- unk2,
- unk3,
- unk4,
- is_crouched,
- is_airborne,
- ascending_flight,
- flight_time,
- unk9,
- unkA
- )
- )
- sessionData.squad.updateSquad()
- case (None, _) =>
- //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
- //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
- case (_, Some(index)) =>
- log.error(
- s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
- )
- case _ => ;
- }
- if (player.death_by == -1) {
- sessionData.kickedByAdministration()
- }
- }
+ def handleChildObjectState(pkt: ChildObjectStateMessage): Unit
- def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
- val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
- val (o, tools) = sessionData.shooting.FindContainedWeapon
- //is COSM our primary upstream packet?
- (o match {
- case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
- case _ => (None, None)
- }) match {
- case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ;
- case _ =>
- sessionData.persist()
- sessionData.turnCounterFunc(player.GUID)
- }
- //the majority of the following check retrieves information to determine if we are in control of the child
- tools.find { _.GUID == object_guid } match {
- case None =>
- //todo: old warning; this state is problematic, but can trigger in otherwise valid instances
- //log.warn(
- // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
- //)
- case Some(_) =>
- //TODO set tool orientation?
- player.Orientation = Vector3(0f, pitch, yaw)
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
- )
- }
- //TODO status condition of "playing getting out of vehicle to allow for late packets without warning
- if (player.death_by == -1) {
- sessionData.kickedByAdministration()
- }
- }
+ def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit
- def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
- val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
- sessionData.validObject(vehicle_guid, decorator = "VehicleSubState") match {
- case Some(obj: Vehicle) =>
- import net.psforever.login.WorldSession.boolToInt
- obj.Position = pos
- obj.Orientation = ang
- obj.Velocity = vel
- sessionData.updateBlockMap(obj, pos)
- obj.zoneInteractions()
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.VehicleState(
- player.GUID,
- vehicle_guid,
- unk1,
- pos,
- ang,
- obj.Velocity,
- obj.Flying,
- 0,
- 0,
- 15,
- unk5 = false,
- obj.Cloaked
- )
- )
- case _ => ;
- }
- }
-
- def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
- val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
- sessionData.validObject(mountable_guid, decorator = "MountVehicle").collect {
- case obj: Mountable =>
- obj.Actor ! Mountable.TryMount(player, entry_point)
- case _ =>
- log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
- }
- }
-
- def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
- val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt
- val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
- //TODO optimize this later
- //common warning for this section
- if (player.GUID == player_guid) {
- //normally disembarking from a mount
- (sessionData.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
- case out @ Some(obj: Vehicle) =>
- continent.GUID(obj.MountedIn) match {
- case Some(_: Vehicle) => None //cargo vehicle
- case _ => out //arrangement "may" be permissible
- }
- case out @ Some(_: Mountable) =>
- out
- case _ =>
- dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
- None
- }) match {
- case Some(obj: Mountable) =>
- obj.PassengerInSeat(player) match {
- case Some(seat_num) =>
- obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
- //short-circuit the temporary channel for transferring between zones, the player is no longer doing that
- sessionData.zoning.interstellarFerry = None
- // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
- //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
- //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
- //todo: kick cargo passengers out. To be added after PR #216 is merged
- obj match {
- case v: Vehicle
- if bailType == BailType.Bailed &&
- v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) &&
- v.isFlying =>
- v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
- case _ => ;
- }
-
- case None =>
- dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
- }
- case _ =>
- dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
- }
- } else {
- //kicking someone else out of a mount; need to own that mount/mountable
- val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
- player.avatar.vehicle match {
- case Some(obj_guid) =>
- (
- (
- sessionData.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
- sessionData.validObject(player_guid, decorator = "DismountVehicle/Player")
- ) match {
- case (vehicle @ Some(obj: Vehicle), tplayer) =>
- if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
- case (mount @ Some(_: Mountable), tplayer) =>
- (mount, tplayer)
- case _ =>
- (None, None)
- }) match {
- case (Some(obj: Mountable), Some(tplayer: Player)) =>
- obj.PassengerInSeat(tplayer) match {
- case Some(seat_num) =>
- obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
- case None =>
- dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
- }
- case (None, _) =>
- dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
- case (_, None) =>
- dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
- case _ =>
- dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
- }
- case None =>
- dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
- }
- }
- }
-
- private def dismountWarning(
- bailAs: BailType.Value,
- kickedByDriver: Boolean
- )
- (
- note: String,
- player: Player
- ): Unit = {
- log.warn(note)
- player.VehicleSeated = None
- sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
- }
-
- private def dismountError(
- bailAs: BailType.Value,
- kickedByDriver: Boolean
- )
- (
- note: String,
- player: Player
- ): Unit = {
- log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
- player.VehicleSeated = None
- sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
- }
-
- def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
- val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt
- (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
- case (Some(cargo: Vehicle), Some(carrier: Vehicle)) =>
- carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
- case Some((mountPoint, _)) =>
- cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint)
- case _ =>
- log.warn(
- s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold"
- )
- }
- case (None, _) | (Some(_), None) =>
- log.warn(
- s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid"
- )
- case _ => ;
- }
- }
-
- def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
- val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt
- continent.GUID(cargo_guid) match {
- case Some(cargo: Vehicle) =>
- cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
- case _ => ;
- }
- }
-
- def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
- val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
- val vehicle = player.avatar.vehicle
- if (vehicle.contains(vehicle_guid)) {
- if (vehicle == player.VehicleSeated) {
- continent.GUID(vehicle_guid) match {
- case Some(obj: Vehicle) =>
- log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state")
- obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
-
- case _ =>
- log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid")
- avatarActor ! AvatarActor.SetVehicle(None)
- }
- } else {
- log.warn(s"${player.Name} must be mounted to request a deployment change")
- }
- } else {
- log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object")
- }
- }
+ def handleDeployRequest(pkt: DeployRequestMessage): Unit
/* messages */
- def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
- if (state == DriveState.Deploying) {
- log.trace(s"DeployRequest: $obj transitioning to deploy state")
- } else if (state == DriveState.Deployed) {
- log.trace(s"DeployRequest: $obj has been Deployed")
- } else {
- CanNotChangeDeployment(obj, state, "incorrect deploy state")
- }
- }
+ def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit
- def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
- if (state == DriveState.Undeploying) {
- log.trace(s"DeployRequest: $obj transitioning to undeploy state")
- } else if (state == DriveState.Mobile) {
- log.trace(s"DeployRequest: $obj is Mobile")
- } else {
- CanNotChangeDeployment(obj, state, "incorrect undeploy state")
- }
- }
+ def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit
- def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = {
- if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) {
- CanNotChangeDeployment(obj, state, reason = "ground too steep")
- } else {
- CanNotChangeDeployment(obj, state, reason)
- }
- }
+ def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit
+}
- /* support functions */
+class VehicleOperations(
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ implicit val context: ActorContext
+ ) extends CommonSessionInterfacingFunctionality {
+ private[session] var serverVehicleControlVelocity: Option[Int] = None
/**
* If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat.
@@ -508,7 +81,7 @@ class VehicleOperations(
* `(None, None)`, otherwise (even if the vehicle can be determined)
*/
def GetKnownVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
- GetMountableAndSeat(sessionData.zoning.interstellarFerry, player, continent) match {
+ GetMountableAndSeat(sessionLogic.zoning.interstellarFerry, player, continent) match {
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
case _ => (None, None)
}
@@ -604,34 +177,9 @@ class VehicleOperations(
* the client's player who is receiving this packet should be mounted as its driver, but this is not explicitly tested
* @param pkt packet to instigate cancellable control
*/
- def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = {
+ private def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = {
serverVehicleControlVelocity = None
vehicle.DeploymentState = DriveState.Mobile
sendResponse(pkt)
}
-
- /**
- * Common reporting behavior when a `Deployment` object fails to properly transition between states.
- * @param obj the game object that could not
- * @param state the `DriveState` that could not be promoted
- * @param reason a string explaining why the state can not or will not change
- */
- def CanNotChangeDeployment(
- obj: PlanetSideServerObject with Deployment,
- state: DriveState.Value,
- reason: String
- ): Unit = {
- val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) {
- obj.DeploymentState = DriveState.Mobile
- sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero))
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero)
- )
- "; enforcing Mobile deployment state"
- } else {
- ""
- }
- log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift")
- }
}
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 afba349ba..7741fa86f 100644
--- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
@@ -2,584 +2,78 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
-import net.psforever.objects.definition.ProjectileDefinition
-import net.psforever.objects.serverobject.doors.InteriorDoorPassage
-import net.psforever.objects.serverobject.interior.Sidedness
-import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.VanuSentry
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.Future
import scala.concurrent.duration._
//
-import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor}
-import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
+import net.psforever.actors.session.AvatarActor
import net.psforever.objects.avatar.scoring.EquipmentStat
-import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
-import net.psforever.objects.entity.SimpleWorldEntity
-import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch}
-import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow}
+import net.psforever.objects.ballistics.Projectile
+import net.psforever.objects.equipment.EquipmentSize
import net.psforever.objects.inventory.Container
-import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
-import net.psforever.objects.vital.Vitality
-import net.psforever.objects.vital.base.{DamageResolution, DamageType}
-import net.psforever.objects.vital.etc.OicwLilBuddyReason
-import net.psforever.objects.vital.interaction.DamageInteraction
-import net.psforever.objects.vital.projectile.ProjectileReason
-import net.psforever.objects.zones.{Zone, ZoneProjectile}
import net.psforever.objects._
-import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.packet.game._
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
-import net.psforever.types.{ExoSuitType, PlanetSideGUID, Vector3}
-import net.psforever.util.Config
+import net.psforever.types.{ExoSuitType, PlanetSideGUID}
-private[support] class WeaponAndProjectileOperations(
- val sessionData: SessionData,
- avatarActor: typed.ActorRef[AvatarActor.Command],
- chatActor: typed.ActorRef[ChatActor.Command],
+trait WeaponAndProjectileFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: WeaponAndProjectileOperations
+
+ def handleWeaponFire(pkt: WeaponFireMessage): Unit
+
+ def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit
+
+ def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit
+
+ def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit
+
+ def handleUplinkRequest(pkt: UplinkRequest): Unit
+
+ def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit
+
+ def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit
+
+ def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit
+
+ def handleReload(pkt: ReloadMessage): Unit
+
+ def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit
+
+ def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit
+
+ def handleProjectileState(pkt: ProjectileStateMessage): Unit
+
+ def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit
+
+ def handleDirectHit(pkt: HitMessage): Unit
+
+ def handleSplashHit(pkt: SplashHitMessage): Unit
+
+ def handleLashHit(pkt: LashMessage): Unit
+
+ def handleAIDamage(pkt: AIDamage): Unit
+}
+
+class WeaponAndProjectileOperations(
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start
var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start
- private[support] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
- private[support] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
- private val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
- private val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
- private[support] var shotsWhileDead: Int = 0
- private val projectiles: Array[Option[Projectile]] =
+ private[session] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
+ private[session] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]()
+ private[session] val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
+ private[session] val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]()
+ private[session] var shotsWhileDead: Int = 0
+ private[session] val projectiles: Array[Option[Projectile]] = {
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
-
- /* packets */
-
- def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
- val WeaponFireMessage(
- _,
- weapon_guid,
- projectile_guid,
- shot_origin,
- _,
- _,
- _,
- _/*max_distance,*/,
- _,
- _/*projectile_type,*/,
- thrown_projectile_vel
- ) = pkt
- HandleWeaponFireOperations(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten)
- }
-
- def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = {
- val WeaponDelayFireMessage(_, _) = pkt
- log.info(s"${player.Name} - $pkt")
- }
-
- def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = {
- val WeaponDryFireMessage(weapon_guid) = pkt
- val (containerOpt, tools) = FindContainedWeapon
- tools
- .find { _.GUID == weapon_guid }
- .orElse { continent.GUID(weapon_guid) }
- .collect {
- case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) =>
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.WeaponDryFire(player.GUID, weapon_guid)
- )
- case _: Equipment =>
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.WeaponDryFire(player.GUID, weapon_guid)
- )
- }
- .orElse {
- log.warn(
- s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist"
- )
- None
- }
- }
-
- def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = {
- val WeaponLazeTargetPositionMessage(_, _, _) = pkt
- //do not need to handle the progress bar animation/state on the server
- //laze waypoint is requested by client upon completion (see SquadWaypointRequest)
- val purpose = if (sessionData.squad.squad_supplement_id > 0) {
- s" for ${player.Sex.possessive} squad (#${sessionData.squad.squad_supplement_id -1})"
- } else {
- " ..."
- }
- log.info(s"${player.Name} is lazing a position$purpose")
- }
-
- def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = {
- val AvatarGrenadeStateMessage(_, state) = pkt
- //TODO I thought I had this working?
- log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade")
- }
-
- def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start)(implicit context: ActorContext): Unit = {
- val ChangeFireStateMessage_Start(item_guid) = pkt
- if (shooting.isEmpty) {
- sessionData.findEquipment(item_guid) match {
- case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
- fireStateStartWhenPlayer(tool, item_guid)
- case Some(tool: Tool) =>
- fireStateStartWhenMounted(tool, item_guid)
- case Some(_) if player.VehicleSeated.isEmpty =>
- fireStateStartSetup(item_guid)
- fireStateStartPlayerMessages(item_guid)
- case Some(_) =>
- fireStateStartSetup(item_guid)
- fireStateStartMountedMessages(item_guid)
- case None =>
- log.warn(s"ChangeFireState_Start: can not find $item_guid")
- }
- }
- }
-
- def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = {
- val ChangeFireStateMessage_Stop(item_guid) = pkt
- val now = System.currentTimeMillis()
- prefire -= item_guid
- shootingStop += item_guid -> now
- shooting -= item_guid
- sessionData.findEquipment(item_guid) match {
- case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
- fireStateStopWhenPlayer(tool, item_guid)
- case Some(tool: Tool) =>
- fireStateStopWhenMounted(tool, item_guid)
- case Some(trigger: BoomerTrigger) =>
- fireStateStopPlayerMessages(item_guid)
- continent.GUID(trigger.Companion).collect {
- case boomer: BoomerDeployable =>
- boomer.Actor ! CommonMessages.Use(player, Some(trigger))
- }
- case Some(_) if player.VehicleSeated.isEmpty =>
- fireStateStopPlayerMessages(item_guid)
- case Some(_) =>
- fireStateStopMountedMessages(item_guid)
- case _ =>
- log.warn(s"ChangeFireState_Stop: can not find $item_guid")
- }
- sessionData.progressBarUpdate.cancel()
- sessionData.progressBarValue = None
- }
-
- def handleReload(pkt: ReloadMessage): Unit = {
- val ReloadMessage(item_guid, _, unk1) = pkt
- FindContainedWeapon match {
- case (Some(obj: Player), tools) =>
- handleReloadWhenPlayer(item_guid, obj, tools, unk1)
- case (Some(obj: PlanetSideServerObject with Container), tools) =>
- 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")
- }
- }
-
- def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = {
- val ChangeAmmoMessage(item_guid, _) = pkt
- val (thing, equipment) = sessionData.findContainedEquipment()
- if (equipment.isEmpty) {
- log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment")
- } else {
- equipment foreach {
- case obj: ConstructionItem =>
- if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) {
- log.info(
- s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})"
- )
- sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex))
- }
- case tool: Tool =>
- thing match {
- case Some(player: Player) =>
- PerformToolAmmoChange(tool, player, ModifyAmmunition(player))
- case Some(mountable: PlanetSideServerObject with Container) =>
- PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable))
- case _ =>
- log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type")
- }
- case obj =>
- log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition")
- }
- }
- }
-
- def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = {
- val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt
- sessionData.findEquipment(item_guid) match {
- case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) =>
- val originalModeIndex = obj.FireModeIndex
- if (obj match {
- case citem: ConstructionItem =>
- val modeChanged = Deployables.performConstructionItemFireModeChange(
- player.avatar.certifications,
- citem,
- originalModeIndex
- )
- modeChanged
- case _ =>
- obj.NextFireMode
- obj.FireModeIndex != originalModeIndex
- }) {
- val modeIndex = obj.FireModeIndex
- obj match {
- case citem: ConstructionItem =>
- log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)")
- case _ =>
- log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex")
- }
- sendResponse(ChangeFireModeMessage(item_guid, modeIndex))
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex)
- )
- }
- case Some(_) =>
- log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes")
- case None =>
- log.warn(s"ChangeFireMode: can not find $item_guid")
- }
- }
-
- def handleProjectileState(pkt: ProjectileStateMessage): Unit = {
- val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt
- val index = projectile_guid.guid - Projectile.baseUID
- projectiles(index) match {
- case Some(projectile) if projectile.HasGUID =>
- val projectileGlobalUID = projectile.GUID
- projectile.Position = shot_pos
- projectile.Orientation = shot_orient
- projectile.Velocity = shot_vel
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.ProjectileState(
- player.GUID,
- projectileGlobalUID,
- shot_pos,
- shot_vel,
- shot_orient,
- seq,
- end,
- target_guid
- )
- )
- case _ if seq == 0 =>
- /* missing the first packet in the sequence is permissible */
- case _ =>
- log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
- }
- }
-
- def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = {
- val LongRangeProjectileInfoMessage(guid, _, _) = pkt
- FindContainedWeapon match {
- case (Some(_: Vehicle), weapons)
- if weapons.exists { _.GUID == guid } => () //now what?
- case _ => ()
- }
- }
-
- def handleDirectHit(pkt: HitMessage): Unit = {
- val HitMessage(
- _,
- projectile_guid,
- _,
- hit_info,
- _,
- _,
- _
- ) = pkt
- //find defined projectile
- FindProjectileEntry(projectile_guid) match {
- case Some(projectile) =>
- //find target(s)
- (hit_info match {
- case Some(hitInfo) =>
- val hitPos = hitInfo.hit_pos
- sessionData.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match {
- case _ if projectile.profile == GlobalDefinitions.flail_projectile =>
- val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius
- val targets = Zone.findAllTargets(continent, player, hitPos, projectile.profile)
- .filter { target =>
- Vector3.DistanceSquared(target.Position, hitPos) <= radius
- }
- targets.map { target =>
- CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target)
- (target, projectile, hitPos, target.Position)
- }
-
- case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
- CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target)
- List((target, projectile, hitInfo.shot_origin, hitPos))
-
- case None =>
- HandleDamageProxy(projectile, projectile_guid, hitPos)
-
- case _ =>
- Nil
- }
- case None =>
- Nil
- })
- .foreach {
- case (
- target: PlanetSideGameObject with FactionAffinity with Vitality,
- proj: Projectile,
- _: Vector3,
- hitPos: Vector3
- ) =>
- ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
- addShotsLanded(resprojectile.cause.attribution, shots = 1)
- sessionData.handleDealingDamage(target, resprojectile)
- }
- case _ => ()
- }
- case None =>
- log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found")
- }
- }
-
- def handleSplashHit(pkt: SplashHitMessage): Unit = {
- val SplashHitMessage(
- _,
- projectile_guid,
- explosion_pos,
- direct_victim_uid,
- _,
- projectile_vel,
- _,
- targets
- ) = pkt
- FindProjectileEntry(projectile_guid) match {
- case Some(projectile) =>
- val profile = projectile.profile
- projectile.Velocity = projectile_vel
- val (resolution1, resolution2) = profile.Aggravated match {
- case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
- (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
- case _ =>
- (DamageResolution.Splash, DamageResolution.Splash)
- }
- //direct_victim_uid
- sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
- case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
- CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
- ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
- addShotsLanded(resprojectile.cause.attribution, shots = 1)
- sessionData.handleDealingDamage(target, resprojectile)
- }
- case _ => ()
- }
- //other victims
- targets.foreach(elem => {
- sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
- case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
- CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
- ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
- addShotsLanded(resprojectile.cause.attribution, shots = 1)
- sessionData.handleDealingDamage(target, resprojectile)
- }
- case _ => ()
- }
- })
- //...
- HandleDamageProxy(projectile, projectile_guid, explosion_pos)
- if (
- projectile.profile.HasJammedEffectDuration ||
- projectile.profile.JammerProjectile ||
- projectile.profile.SympatheticExplosion
- ) {
- //can also substitute 'projectile.profile' for 'SpecialEmp.emp'
- Zone.serverSideDamage(
- continent,
- player,
- SpecialEmp.emp,
- SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
- SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
- SpecialEmp.findAllBoomers(profile.DamageRadius)
- )
- }
- if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
- //cleanup
- if (projectile.HasGUID) {
- continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
- }
- }
- case None => ()
- }
- }
-
- def handleLashHit(pkt: LashMessage): Unit = {
- val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt
- sessionData.validObject(victim_guid, decorator = "Lash") match {
- case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
- CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target)
- ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach {
- resprojectile =>
- addShotsLanded(resprojectile.cause.attribution, shots = 1)
- sessionData.handleDealingDamage(target, resprojectile)
- }
- case _ => ()
- }
- }
-
- def handleAIDamage(pkt: AIDamage): Unit = {
- val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
- (continent.GUID(player.VehicleSeated) match {
- case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
- if tobj.GUID == targetGuid &&
- tobj.OwnerGuid.contains(player.GUID) =>
- //deployable turrets
- Some(tobj)
- case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
- if tobj.GUID == targetGuid &&
- tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
- //facility turrets, etc.
- Some(tobj)
- case _
- if player.GUID == targetGuid =>
- //player avatars
- Some(player)
- case _ =>
- None
- }).collect {
- case target: AutomatedTurret.Target =>
- sessionData.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
- .collect {
- case turret: AutomatedTurret if turret.Target.isEmpty =>
- turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
- Some(target)
-
- case turret: AutomatedTurret =>
- turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
- HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
- Some(target)
- }
- }
- .orElse {
- //occasionally, something that is not technically a turret's natural target may be attacked
- sessionData.validObject(targetGuid, decorator = "AIDamage/Target")
- .collect {
- case target: PlanetSideServerObject with FactionAffinity with Vitality =>
- sessionData.validObject(attackerGuid, decorator = "AIDamage/Attacker")
- .collect {
- case turret: AutomatedTurret if turret.Target.nonEmpty =>
- //the turret must be shooting at something (else) first
- HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
- }
- Some(target)
- }
- }
- }
-
- /* support code */
-
- def HandleWeaponFireOperations(
- weaponGUID: PlanetSideGUID,
- projectileGUID: PlanetSideGUID,
- shotOrigin: Vector3,
- shotVelocity: Option[Vector3]
- ): Unit = {
- HandleWeaponFireAccountability(weaponGUID, projectileGUID) match {
- case (Some(obj), Some(tool)) =>
- val projectileIndex = projectileGUID.guid - Projectile.baseUID
- val projectilePlace = projectiles(projectileIndex)
- if (
- projectilePlace match {
- case Some(projectile) =>
- !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong
- case None =>
- false
- }
- ) {
- log.debug(
- s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}"
- )
- }
- val (angle, attribution, acceptableDistanceToOwner) = obj match {
- case p: Player =>
- (
- SimpleWorldEntity.validateOrientationEntry(
- p.Orientation + Vector3.z(p.FacingYawUpper)
- ),
- tool.Definition.ObjectId,
- 10f + (if (p.Velocity.nonEmpty) {
- 5f
- } else {
- 0f
- })
- )
- case v: Vehicle if v.Definition.CanFly =>
- (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle
- case _: Vehicle =>
- (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle
- case _ =>
- (obj.Orientation, obj.Definition.ObjectId, 300f)
- }
- val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position)
- if (distanceToOwner <= acceptableDistanceToOwner) {
- val projectile_info = tool.Projectile
- val wguid = weaponGUID.guid
- val mountedIn = (continent.turretToWeapon
- .find { case (guid, _) => guid == wguid } match {
- case Some((_, turretGuid)) => Some((
- turretGuid,
- continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }
- ))
- case _ => None
- }) match {
- case Some((guid, Some(entity))) => Some((guid, entity))
- case _ => None
- }
- val projectile = new Projectile(
- projectile_info,
- tool.Definition,
- tool.FireMode,
- mountedIn,
- PlayerSource(player),
- attribution,
- shotOrigin,
- angle,
- shotVelocity
- )
- val initialQuality = tool.FireMode match {
- case mode: ChargeFireModeDefinition =>
- ProjectileQuality.Modified(
- {
- val timeInterval = projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis())
- timeInterval.toFloat / mode.Time.toFloat
- }
- )
- case _ =>
- ProjectileQuality.Normal
- }
- val qualityprojectile = projectile.quality(initialQuality)
- qualityprojectile.WhichSide = player.WhichSide
- projectiles(projectileIndex) = Some(qualityprojectile)
- if (projectile_info.ExistsOnRemoteClients) {
- log.trace(
- s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile"
- )
- continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile)
- }
- } else {
- log.warn(
- s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect"
- )
- }
-
- case _ => ()
- }
}
def HandleWeaponFireAccountability(
@@ -587,11 +81,11 @@ private[support] class WeaponAndProjectileOperations(
projectileGUID: PlanetSideGUID
): (Option[PlanetSideGameObject with Container], Option[Tool]) = {
if (player.ZoningRequest != Zoning.Method.None) {
- sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_fire")
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_fire")
}
if (player.isShielded) {
// Cancel NC MAX shield if it's active
- sessionData.toggleMaxSpecialState(enable = false)
+ sessionLogic.general.toggleMaxSpecialState(enable = false)
}
val (o, tools) = FindContainedWeapon
val (_, enabledTools) = FindEnabledWeaponsToHandleWeaponFireAccountability(o, tools)
@@ -609,7 +103,8 @@ private[support] class WeaponAndProjectileOperations(
if (tool.Magazine <= 0) { //safety: enforce ammunition depletion
prefire -= weaponGUID
EmptyMagazine(weaponGUID, tool)
- (o, Some(tool))
+ projectiles(projectileGUID.guid - Projectile.baseUID) = None
+ (None, None)
} else if (!player.isAlive) { //proper internal accounting, but no projectile
prefire += weaponGUID
tool.Discharge()
@@ -678,18 +173,6 @@ private[support] class WeaponAndProjectileOperations(
(o, enabledTools)
}
- /**
- * For a certain weapon that cna load ammunition, enforce that its magazine is empty.
- * @param weapon_guid the weapon
- */
- def EmptyMagazine(weapon_guid: PlanetSideGUID): Unit = {
- continent.GUID(weapon_guid) match {
- case Some(tool: Tool) =>
- EmptyMagazine(weapon_guid, tool)
- case _ => ()
- }
- }
-
/**
* For a certain weapon that can load ammunition, enforce that its magazine is empty.
* Punctuate that emptiness with a ceasation of weapons fire and a dry fire sound effect.
@@ -708,209 +191,6 @@ private[support] class WeaponAndProjectileOperations(
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid))
}
- /**
- * 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 =>
- 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 {
- ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue)
- 3
- }
- log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw")
- 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)
- }
- }
-
- /**
- * Given an object that contains a box of amunition in its `Inventory` at a certain location,
- * change the amount of ammunition within that box.
- * @param obj the `Container`
- * @param box an `AmmoBox` to modify
- * @param reloadValue the value to modify the `AmmoBox`;
- * subtracted from the current `Capacity` of `Box`
- */
- def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
- val capacity = box.Capacity - reloadValue
- box.Capacity = capacity
- sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity))
- }
-
- /**
- * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location,
- * change the amount of ammunition within that box.
- * @param obj the `Container`
- * @param box an `AmmoBox` to modify
- * @param reloadValue the value to modify the `AmmoBox`;
- * subtracted from the current `Capacity` of `Box`
- */
- def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
- ModifyAmmunition(obj)(box, reloadValue)
- obj.Find(box).collect { index =>
- continent.VehicleEvents ! VehicleServiceMessage(
- s"${obj.Actor}",
- VehicleAction.InventoryState(
- player.GUID,
- box,
- obj.GUID,
- index,
- box.Definition.Packet.DetailedConstructorData(box).get
- )
- )
- }
- }
-
- /**
- * na
- * @param tool na
- * @param obj na
- */
- def PerformToolAmmoChange(
- tool: Tool,
- obj: PlanetSideServerObject with Container,
- modifyFunc: (AmmoBox, Int) => Unit
- ): Unit = {
- val originalAmmoType = tool.AmmoType
- do {
- val requestedAmmoType = tool.NextAmmoType
- val fullMagazine = tool.MaxMagazine
- if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) {
- FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match {
- case Nil => ()
- case x :: xs =>
- val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj)
- val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj)
-
- xs.foreach(item => {
- obj.Inventory -= item.start
- sendResponse(ObjectDeleteMessage(item.obj.GUID, 0))
- TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj))
- })
-
- //box will be the replacement ammo; give it the discovered magazine and load it into the weapon
- val box = x.obj.asInstanceOf[AmmoBox]
- //previousBox is the current magazine in tool; it will be removed from the weapon
- val previousBox = tool.AmmoSlot.Box
- val originalBoxCapacity = box.Capacity
- val tailReloadValue: Int = if (xs.isEmpty) {
- 0
- } else {
- xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum
- }
- val sumReloadValue: Int = originalBoxCapacity + tailReloadValue
- val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
- val box_guid = box.GUID
- val tool_guid = tool.GUID
- obj.Inventory -= x.start //remove replacement ammo from inventory
- tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool
- sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f))
- sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f))
- sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex))
-
- //announce swapped ammunition box in weapon
- val previous_box_guid = previousBox.GUID
- val boxDef = box.Definition
- sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity))
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.ChangeAmmo(
- player.GUID,
- tool_guid,
- ammoSlotIndex,
- previous_box_guid,
- boxDef.ObjectId,
- box.GUID,
- boxDef.Packet.ConstructorData(box).get
- )
- )
-
- //handle inventory contents
- box.Capacity = if (sumReloadValue <= fullMagazine) {
- sumReloadValue
- } else {
- val splitReloadAmmo: Int = sumReloadValue - fullMagazine
- log.trace(
- s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo"
- )
- val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo)
- TaskWorkflow.execute(stowNewFunc(boxForInventory))
- fullMagazine
- }
- sendResponse(
- InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)
- ) //should work for both players and vehicles
- log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}")
- if (previousBox.Capacity > 0) {
- //divide capacity across other existing and not full boxes of that ammo type
- var capacity = previousBox.Capacity
- val iter = obj.Inventory.Items
- .filter(entry => {
- entry.obj match {
- case item: AmmoBox =>
- item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity
- case _ =>
- false
- }
- })
- .sortBy(_.start)
- .iterator
- while (capacity > 0 && iter.hasNext) {
- val entry = iter.next()
- val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox]
- val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity)
- log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType")
- capacity -= ammoAllocated
- modifyFunc(item, -ammoAllocated)
- }
- previousBox.Capacity = capacity
- }
-
- if (previousBox.Capacity > 0) {
- //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm
- obj.Inventory.Fit(previousBox) match {
- case Some(_) =>
- stowFunc(previousBox)
- case None =>
- sessionData.normalItemDrop(player, continent)(previousBox)
- }
- AmmoBox.Split(previousBox) match {
- case Nil | List(_) => () //done (the former case is technically not possible)
- case _ :: toUpdate =>
- modifyFunc(previousBox, 0) //update to changed capacity value
- toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) })
- }
- } else {
- TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox))
- }
- }
- }
- } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType)
- }
-
/**
* The main purpose of this method is to determine which targets will receive "locked on" warnings from remote projectiles.
* For a given series of globally unique identifiers, indicating targets,
@@ -921,7 +201,7 @@ private[support] class WeaponAndProjectileOperations(
*/
def FindDetectedProjectileTargets(targets: Iterable[PlanetSideGUID]): Iterable[String] = {
targets
- .map { sessionData.validObject(_, decorator="FindDetectedProjectileTargets") }
+ .map { sessionLogic.validObject(_, decorator="FindDetectedProjectileTargets") }
.flatMap {
case Some(obj: Vehicle) if !obj.Cloaked =>
//TODO hint: vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.ProjectileAutoLockAwareness(mode))
@@ -935,20 +215,6 @@ private[support] class WeaponAndProjectileOperations(
}
}
- def CheckForHitPositionDiscrepancy(
- projectile_guid: PlanetSideGUID,
- hitPos: Vector3,
- target: PlanetSideGameObject with FactionAffinity with Vitality
- ): Unit = {
- val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position)
- if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) {
- // If the target position on the server does not match the position where the projectile landed within reason there may be foul play
- log.warn(
- s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect"
- )
- }
- }
-
/**
* Given a globally unique identifier in the 40100 to 40124 range
* (with an optional 25 as buffer),
@@ -966,197 +232,6 @@ private[support] class WeaponAndProjectileOperations(
}
}
- /**
- * Find a projectile with the given globally unique identifier and mark it as a resolved shot.
- * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle.
- * @param projectile_guid the projectile GUID
- * @param resolution the resolution status to promote the projectile
- * @return the projectile
- */
- def ResolveProjectileInteraction(
- projectile_guid: PlanetSideGUID,
- resolution: DamageResolution.Value,
- target: PlanetSideGameObject with FactionAffinity with Vitality,
- pos: Vector3
- ): Option[DamageInteraction] = {
- FindProjectileEntry(projectile_guid) match {
- case Some(projectile) =>
- ResolveProjectileInteraction(projectile, resolution, target, pos)
- case None =>
- log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found")
- None
- }
- }
-
- /**
- * Find a projectile with the given globally unique identifier and mark it as a resolved shot.
- * @param projectile the projectile object
- * @param index where the projectile was found
- * @param resolution the resolution status to promote the projectile
- * @return a copy of the projectile
- */
- def ResolveProjectileInteraction(
- projectile: Projectile,
- index: Int,
- resolution: DamageResolution.Value,
- target: PlanetSideGameObject with FactionAffinity with Vitality,
- pos: Vector3
- ): Option[DamageInteraction] = {
- if (!projectiles(index).contains(projectile)) {
- log.error(s"expected projectile could not be found at $index; can not resolve")
- None
- } else {
- ResolveProjectileInteraction(projectile, resolution, target, pos)
- }
- }
-
- /**
- * na
- * @param projectile the projectile object
- * @param resolution the resolution status to promote the projectile
- * @return a copy of the projectile
- */
- def ResolveProjectileInteraction(
- projectile: Projectile,
- resolution: DamageResolution.Value,
- target: PlanetSideGameObject with FactionAffinity with Vitality,
- pos: Vector3
- ): Option[DamageInteraction] = {
- if (projectile.isMiss) {
- log.warn("expected projectile was already counted as a missed shot; can not resolve any further")
- None
- } else {
- val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player))
- if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) {
- avatarActor ! AvatarActor.ConsumeStamina(10)
- }
- Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos))
- }
- }
-
- /**
- * Take a projectile that was introduced into the game world and
- * determine if it generates a secondary damage projectile or
- * an method of damage causation that requires additional management.
- * @param projectile the projectile
- * @param pguid the client-local projectile identifier
- * @param hitPos the game world position where the projectile is being recorded
- * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location;
- * nothing if no targets were affected
- */
- def HandleDamageProxy(
- projectile: Projectile,
- pguid: PlanetSideGUID,
- hitPos: Vector3
- ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = {
- GlobalDefinitions.getDamageProxy(projectile, hitPos) match {
- case Nil =>
- Nil
- case list if list.isEmpty =>
- Nil
- case list =>
- HandleDamageProxySetupLittleBuddy(list, hitPos)
- UpdateProjectileSidednessAfterHit(projectile, hitPos)
- val projectileSide = projectile.WhichSide
- list.flatMap { proxy =>
- if (proxy.profile.ExistsOnRemoteClients) {
- proxy.Position = hitPos
- proxy.WhichSide = projectileSide
- continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy)
- Nil
- } else if (proxy.tool_def == GlobalDefinitions.maelstrom) {
- //server-side maelstrom grenade target selection
- val radius = proxy.profile.LashRadius * proxy.profile.LashRadius
- val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList })
- .filter { target =>
- Vector3.DistanceSquared(target.Position, hitPos) <= radius
- }
- //chainlash is separated from the actual damage application for convenience
- continent.AvatarEvents ! AvatarServiceMessage(
- continent.id,
- AvatarAction.SendResponse(
- PlanetSideGUID(0),
- ChainLashMessage(
- hitPos,
- projectile.profile.ObjectId,
- targets.map { _.GUID }
- )
- )
- )
- targets.map { target =>
- CheckForHitPositionDiscrepancy(pguid, hitPos, target)
- (target, proxy, hitPos, target.Position)
- }
- } else {
- Nil
- }
- }
- }
- }
-
- def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = {
- val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw }
- val size: Int = listOfLittleBuddies.size
- if (size > 0) {
- val desiredDownwardsProjectiles: Int = 2
- val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down
- val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out
- val z: Float = player.Orientation.z //player's standing direction
- val north: Vector3 = Vector3(0,1,0) //map North
- val speed: Float = 144f //speed (packet discovered)
- val dist: Float = 25 //distance (client defined)
- val downwardsAngle: Float = -85f
- val flaredAngle: Float = -70f
- //angle of separation for downwards, degrees from vertical for flared out
- val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) {
- (360f / firstHalf, downwardsAngle)
- } else {
- (0f, 0f)
- }
- val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) {
- (360f / secondHalf, flaredAngle)
- } else {
- (0f, 0f)
- }
- val smallRotOffset: Float = z + 90f
- val largeRotOffset: Float = z + math.random().toFloat * 45f
- val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat)
- //downwards projectiles
- var i: Int = 0
- listOfLittleBuddies.take(firstHalf).foreach { proxy =>
- val facing = (smallRotOffset + smallStep * i.toFloat) % 360
- val dir = north.Rx(smallAngle).Rz(facing)
- proxy.Position = detonationPosition + dir.xy + verticalCorrection
- proxy.Velocity = dir * speed
- proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing)
- HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
- i += 1
- }
- //flared out projectiles
- i = 0
- listOfLittleBuddies.drop(firstHalf).foreach { proxy =>
- val facing = (largeRotOffset + largeStep * i.toFloat) % 360
- val dir = north.Rx(largeAngle).Rz(facing)
- proxy.Position = detonationPosition + dir
- proxy.Velocity = dir * speed
- proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing)
- HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
- i += 1
- }
- true
- } else {
- false
- }
- }
-
- def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = {
- //explosion
- val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction)
- obj.Position = obj.Position + orientation * distance
- val explosionFunc: ()=>Unit = WeaponAndProjectileOperations.detonateLittleBuddy(continent, obj, proxy, proxy.owner)
- context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() }
- }
-
/**
* Check two locations for a controlled piece of equipment that is associated with the `player`.
* Filter for discovered `Tool`-type `Equipment`.
@@ -1165,7 +240,7 @@ private[support] class WeaponAndProjectileOperations(
* the second value is an `Tool` object in the former
*/
def FindContainedWeapon: (Option[PlanetSideGameObject with Container], Set[Tool]) = {
- sessionData.findContainedEquipment() match {
+ sessionLogic.findContainedEquipment() match {
case (container, equipment) =>
(container, equipment collect { case t: Tool => t })
case _ =>
@@ -1196,104 +271,15 @@ private[support] class WeaponAndProjectileOperations(
*/
def FindWeapon: Set[Tool] = FindContainedWeapon._2
- /*
- used by ChangeFireStateMessage_Start handling
- */
- private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = {
- prefire -= itemGuid
- shooting += itemGuid
- shootingStart += itemGuid -> System.currentTimeMillis()
- }
-
- private def fireStateStartChargeMode(tool: Tool): Unit = {
- //charge ammunition drain
- tool.FireMode match {
- case mode: ChargeFireModeDefinition =>
- sessionData.progressBarValue = Some(0f)
- sessionData.progressBarUpdate = context.system.scheduler.scheduleOnce(
- (mode.Time + mode.DrainInterval) milliseconds,
- context.self,
- SessionActor.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 = {
- sessionData.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 || 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"
- )
- 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 fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = {
+ def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = {
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid)
)
}
- private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = {
- sessionData.findContainedEquipment()._1.collect {
+ def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = {
+ sessionLogic.findContainedEquipment()._1.collect {
case turret: FacilityTurret if continent.map.cavern =>
turret.Actor ! VanuSentry.ChangeFireStop
}
@@ -1303,136 +289,11 @@ private[support] class WeaponAndProjectileOperations(
)
}
- 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)
- fireStateStopPlayerMessages(itemGuid)
- }
-
- private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = {
- fireStateStopUpdateChargeAndCleanup(tool)
- 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 handleReloadProcedure(
- itemGuid: PlanetSideGUID,
- obj: PlanetSideGameObject with Container,
- tools: Set[Tool],
- unk1: Int,
- deleteFunc: Equipment => Future[Any],
- modifyFunc: (AmmoBox, Int) => Unit,
- messageFunc: PlanetSideGUID => Unit
- ): Unit = {
- tools
- .filter { _.GUID == itemGuid }
- .foreach { tool =>
- val currentMagazine : Int = tool.Magazine
- val magazineSize : Int = tool.MaxMagazine
- val reloadValue : Int = magazineSize - currentMagazine
- if (magazineSize > 0 && reloadValue > 0) {
- FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match {
- case Nil => ()
- case x :: xs =>
- xs.foreach { item => deleteFunc(item.obj) }
- val box = x.obj.asInstanceOf[AmmoBox]
- val tailReloadValue : Int = if (xs.isEmpty) {
- 0
- }
- else {
- xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum
- }
- val sumReloadValue : Int = box.Capacity + tailReloadValue
- val actualReloadValue = if (sumReloadValue <= reloadValue) {
- deleteFunc(box)
- sumReloadValue
- }
- else {
- modifyFunc(box, reloadValue - tailReloadValue)
- reloadValue
- }
- val finalReloadValue = actualReloadValue + currentMagazine
- log.info(
- s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}"
- )
- tool.Magazine = finalReloadValue
- sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1))
- messageFunc(itemGuid)
- }
- } else {
- //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it
- sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize))
- }
- }
- }
-
- private def handleReloadWhenPlayer(
- itemGuid: PlanetSideGUID,
- obj: Player,
- tools: Set[Tool],
- unk1: Int
- ): Unit = {
- handleReloadProcedure(
- itemGuid,
- obj,
- tools,
- unk1,
- RemoveOldEquipmentFromInventory(obj)(_),
- ModifyAmmunition(obj)(_, _),
- reloadPlayerMessages
- )
- }
-
- private 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)
}
- //noinspection SameParameterValue
- private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
- addShotsToMap(shotsLanded, weaponId, shots)
- }
-
- private def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = {
+ def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = {
map.put(
weaponId,
map.get(weaponId) match {
@@ -1442,11 +303,11 @@ private[support] class WeaponAndProjectileOperations(
)
}
- private[support] def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
+ private def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
reportOngoingShots(player.CharId, reportFunc)
}
- private[support] def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
+ private def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = {
//only shots that have been reported as fired count
//if somehow shots had reported as landed but never reported as fired, they are ignored
//these are just raw counts; there's only numeric connection between the entries of fired and of landed
@@ -1458,133 +319,22 @@ private[support] class WeaponAndProjectileOperations(
shotsLanded.clear()
}
- //noinspection ScalaUnusedSymbol
- private[support] def reportOngoingShotsToAvatar(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
- avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(weaponId, fired, landed, 0, 0))
- }
-
- private[support] def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
+ private def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = {
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
}
- private def CompileAutomatedTurretDamageData(
- turret: AutomatedTurret,
- owner: SourceEntry,
- projectileTypeId: Long
- ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
- turret.Weapons
- .values
- .flatMap { _.Equipment }
- .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
- .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
- }
-
- private def HandleAIDamage(
- target: PlanetSideServerObject with FactionAffinity with Vitality,
- results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
- ): Unit = {
- results.collect {
- case (obj, tool, owner, projectileInfo) =>
- val angle = Vector3.Unit(target.Position - obj.Position)
- val proj = new Projectile(
- projectileInfo,
- tool.Definition,
- tool.FireMode,
- None,
- owner,
- obj.Definition.ObjectId,
- obj.Position + Vector3.z(value = 1f),
- angle,
- Some(angle * projectileInfo.FinalVelocity)
- )
- val hitPos = target.Position + Vector3.z(value = 1f)
- ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
- addShotsLanded(resprojectile.cause.attribution, shots = 1)
- sessionData.handleDealingDamage(target, resprojectile)
- }
- }
- }
-
- private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = {
- val origin = projectile.Position
- val distance = Vector3.Magnitude(hitPosition - origin)
- continent.blockMap
- .sector(hitPosition, distance)
- .environmentList
- .collect { case o: InteriorDoorPassage =>
- val door = o.door
- val intersectTest = quickLineSphereIntersectionPoints(
- origin,
- hitPosition,
- door.Position,
- door.Definition.UseRadius + 0.1f
- )
- (door, intersectTest)
- }
- .collect { case (door, intersectionTest) if intersectionTest.nonEmpty =>
- (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest)
- }
- .minByOption { case (_, dist, _) => dist }
- .foreach { case (door, _, intersects) =>
- val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) {
- Sidedness.OutsideOf
- } else {
- Sidedness.InsideOf
- }
- projectile.WhichSide = if (intersects.size == 1) {
- Sidedness.InBetweenSides(door, strictly)
- } else {
- strictly
- }
- }
- }
-
- /**
- * Does a line segment line intersect with a sphere?
- * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
- * @param start first point of the line segment
- * @param end second point of the line segment
- * @param center center of the sphere
- * @param radius radius of the sphere
- * @return list of all points of intersection, if any
- * @see `Vector3.DistanceSquared`
- * @see `Vector3.MagnitudeSquared`
- */
- private def quickLineSphereIntersectionPoints(
- start: Vector3,
- end: Vector3,
- center: Vector3,
- radius: Float
- ): Iterable[Vector3] = {
- /*
- Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
- because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
- */
- val Vector3(cx, cy, cz) = center
- val Vector3(sx, sy, sz) = start
- val vector = end - start
- //speed our way through a quadratic equation
- val (a, b) = {
- val Vector3(dx, dy, dz) = vector
- (
- dx * dx + dy * dy + dz * dz,
- 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
+ override protected[session] def actionsToCancel(): Unit = {
+ shootingStart.clear()
+ shootingStop.clear()
+ (prefire ++ shooting).foreach { guid =>
+ sendResponse(ChangeFireStateMessage_Stop(guid))
+ continent.AvatarEvents ! AvatarServiceMessage(
+ continent.id,
+ AvatarAction.ChangeFireState_Stop(player.GUID, guid)
)
}
- val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
- val result = b * b - 4 * a * c
- if (result < 0f) {
- //negative, no intersection
- Seq()
- } else if (result < 0.00001f) {
- //zero-ish, one intersection point
- Seq(start - vector * (b / (2f * a)))
- } else {
- //positive, two intersection points
- val sqrt = math.sqrt(result).toFloat
- val endStart = vector / (2f * a)
- Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
- }.filter(p => Vector3.DistanceSquared(start, p) <= a)
+ prefire.clear()
+ shooting.clear()
}
override protected[session] def stop(): Unit = {
@@ -1599,50 +349,3 @@ private[support] class WeaponAndProjectileOperations(
}
}
}
-
-object WeaponAndProjectileOperations {
- /**
- * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
- * The main difference from "normal" server-side explosion
- * is that the owner of the projectile must be clarified explicitly.
- * @see `Zone::serverSideDamage`
- * @param zone where the explosion is taking place
- * (`source` contains the coordinate location)
- * @param source a game object that represents the source of the explosion
- * @param owner who or what to accredit damage from the explosion to;
- * clarifies a normal `SourceEntry(source)` accreditation
- */
- private def detonateLittleBuddy(
- zone: Zone,
- source: PlanetSideGameObject with FactionAffinity with Vitality,
- proxy: Projectile,
- owner: SourceEntry
- )(): Unit = {
- Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
- }
-
- /**
- * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
- * The main difference from "normal" server-side explosion
- * is that the owner of the projectile must be clarified explicitly.
- * The sub-projectiles will be the product of a normal projectile rather than a standard game object
- * so a custom `source` entity must wrap around it and fulfill the requirements of the field.
- * @see `Zone::explosionDamage`
- * @param owner who or what to accredit damage from the explosion to
- * @param explosionPosition where the explosion will be positioned in the game world
- * @param source a game object that represents the source of the explosion
- * @param target a game object that is affected by the explosion
- * @return a `DamageInteraction` object
- */
- private def littleBuddyExplosionDamage(
- owner: SourceEntry,
- projectileId: Long,
- explosionPosition: Vector3
- )
- (
- source: PlanetSideGameObject with FactionAffinity with Vitality,
- target: PlanetSideGameObject with FactionAffinity with Vitality
- ): DamageInteraction = {
- DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
- }
-}
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 54b00181b..bb0c93584 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -6,6 +6,7 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import akka.pattern.ask
import akka.util.Timeout
+import net.psforever.actors.session.spectator.SpectatorMode
import net.psforever.login.WorldSession
import net.psforever.objects.avatar.BattleRank
import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics}
@@ -17,6 +18,7 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
+import net.psforever.services.chat.DefaultChannel
import scala.collection.mutable
import scala.concurrent.duration._
@@ -73,6 +75,11 @@ import net.psforever.util.{Config, DefinitionUtil}
import net.psforever.zones.Zones
object ZoningOperations {
+ private[session] final case class AvatarAwardMessageBundle(
+ bundle: Iterable[Iterable[PlanetSideGamePacket]],
+ delay: Long
+ )
+
private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20)
def reportProgressionSystem(sessionActor: ActorRef): Unit = {
@@ -101,24 +108,26 @@ object ZoningOperations {
}
class ZoningOperations(
- val sessionData: SessionData,
+ val sessionLogic: SessionData,
avatarActor: typed.ActorRef[AvatarActor.Command],
galaxyService: ActorRef,
cluster: typed.ActorRef[ICS.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
- private var zoningType: Zoning.Method = Zoning.Method.None
- private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT
- private[support] var zoningStatus: Zoning.Status = Zoning.Status.None
- private var zoningCounter: Int = 0
- private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
+ private[session] var zoningStatus: Zoning.Status = Zoning.Status.None
+ /** a flag for the zone having finished loading during zoning
+ * `None` when no zone is loaded
+ * `Some(true)` when a zone has successfully loaded
+ * `Some(false)` when the loading process has failed or was executed but did not complete for some reason
+ */
+ private[session] var zoneLoaded: Option[Boolean] = None
/**
* used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone)
* used during intrazone gate transfers, but not in a way distinct from prior zone transfer procedures
* should only be set during the transient period when moving between one spawn point and the next
* leaving set prior to a subsequent transfers may cause unstable vehicle associations, with memory leak potential
*/
- private[support] var interstellarFerry: Option[Vehicle] = None
+ private[session] var interstellarFerry: Option[Vehicle] = None
/**
* used during zone transfers for cleanup to refer to the vehicle that instigated a transfer
* "top level" is the carrier in a carrier/ferried association or a projected carrier/(ferried carrier)/ferried association
@@ -126,20 +135,18 @@ class ZoningOperations(
* the old-zone unique identifier for the carrier
* no harm should come from leaving the field set to an old unique identifier value after the transfer period
*/
- private[support] var interstellarFerryTopLevelGUID: Option[PlanetSideGUID] = None
- private var loadConfZone: Boolean = false
- /** a flag for the zone having finished loading during zoning
- * `None` when no zone is loaded
- * `Some(true)` when a zone has successfully loaded
- * `Some(false)` when the loading process has failed or was executed but did not complete for some reason
- */
- private[support] var zoneLoaded: Option[Boolean] = None
+ private[session] var interstellarFerryTopLevelGUID: Option[PlanetSideGUID] = None
/** a flag that forces the current zone to reload itself during a zoning operation */
- private[support] var zoneReload: Boolean = false
- private var zoningTimer: Cancellable = Default.Cancellable
-
+ private[session] var zoneReload: Boolean = false
private[session] val spawn: SpawnOperations = new SpawnOperations()
+ private var loadConfZone: Boolean = false
+ private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
+ private var zoningType: Zoning.Method = Zoning.Method.None
+ private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT
+ private var zoningCounter: Int = 0
+ private var zoningTimer: Cancellable = Default.Cancellable
+
/* packets */
def handleWarpgateRequest(pkt: WarpgateRequest): Unit = {
@@ -147,7 +154,7 @@ class ZoningOperations(
CancelZoningProcessWithDescriptiveReason("cancel_use")
if (spawn.deadState != DeadState.RespawnTime) {
continent.Buildings.values.find(_.GUID == building_guid) match {
- case Some(wg: WarpGate) if wg.Active && (sessionData.vehicles.GetKnownVehicleAndSeat() match {
+ case Some(wg: WarpGate) if wg.Active && (sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
case (Some(vehicle), _) =>
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
case _ =>
@@ -176,7 +183,7 @@ class ZoningOperations(
}
}
- def handleDroppodLaunchRequest(pkt: DroppodLaunchRequestMessage)(implicit context: ActorContext): Unit = {
+ def handleDroppodLaunchRequest(pkt: DroppodLaunchRequestMessage): Unit = {
val DroppodLaunchRequestMessage(info, _) = pkt
cluster ! ICS.DroppodLaunchRequest(
info.zone_number,
@@ -202,7 +209,7 @@ class ZoningOperations(
continent.VehicleEvents ! Service.Join(name)
continent.VehicleEvents ! Service.Join(continentId)
continent.VehicleEvents ! Service.Join(factionChannel)
- if (sessionData.connectionState != 100) configZone(continent)
+ if (sessionLogic.connectionState != 100) configZone(continent)
sendResponse(TimeOfDayMessage(1191182336))
//custom
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
@@ -487,7 +494,7 @@ class ZoningOperations(
//the router won't work if it doesn't completely deploy
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deploying, 0, unk3=false, Vector3.Zero))
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, unk3=false, Vector3.Zero))
- sessionData.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
+ sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
}
ServiceManager.serviceManager
.ask(Lookup("hart"))(Timeout(2 seconds))
@@ -656,8 +663,8 @@ class ZoningOperations(
//the following subscriptions last until character switch/logout
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
- sessionData.squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
- sessionData.squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
+ sessionLogic.squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
+ sessionLogic.squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
player.Zone match {
case Zone.Nowhere =>
RandomSanctuarySpawnPosition(player)
@@ -669,7 +676,7 @@ class ZoningOperations(
session = session.copy(zone = zone)
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
zone.AvatarEvents ! Service.Join(player.Name)
- sessionData.persist()
+ sessionLogic.persist()
oldZone.AvatarEvents ! Service.Leave()
oldZone.LocalEvents ! Service.Leave()
oldZone.VehicleEvents ! Service.Leave()
@@ -699,7 +706,7 @@ class ZoningOperations(
loadConfZone = true
val oldZone = session.zone
session = session.copy(zone = foundZone)
- sessionData.persist()
+ sessionLogic.persist()
oldZone.AvatarEvents ! Service.Leave()
oldZone.LocalEvents ! Service.Leave()
oldZone.VehicleEvents ! Service.Leave()
@@ -709,9 +716,9 @@ class ZoningOperations(
player.avatar = avatar
interstellarFerry match {
case Some(vehicle) if vehicle.PassengerInSeat(player).contains(0) =>
- TaskWorkflow.execute(sessionData.registerDrivenVehicle(vehicle, player))
+ TaskWorkflow.execute(registerDrivenVehicle(vehicle, player))
case _ =>
- TaskWorkflow.execute(sessionData.registerNewAvatar(player))
+ TaskWorkflow.execute(registerNewAvatar(player))
}
}
@@ -749,9 +756,9 @@ class ZoningOperations(
}
val previousZoningType = ztype
CancelZoningProcess()
- sessionData.playerActionsToCancel()
- sessionData.terminals.CancelAllProximityUnits()
- sessionData.dropSpecialSlotItem()
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sessionLogic.general.dropSpecialSlotItem()
continent.Population ! Zone.Population.Release(avatar)
spawn.resolveZoningSpawnPointLoad(response, previousZoningType)
}
@@ -859,13 +866,13 @@ class ZoningOperations(
zoningStatus = Zoning.Status.Request
beginZoningCountdown(() => {
log.info(s"Good-bye, ${player.Name}")
- sessionData.immediateDisconnect()
+ sessionLogic.immediateDisconnect()
})
}
def handleSetZone(zoneId: String, position: Vector3): Unit = {
- if (sessionData.vehicles.serverVehicleControlVelocity.isEmpty) {
- sessionData.playerActionsToCancel()
+ if (sessionLogic.vehicles.serverVehicleControlVelocity.isEmpty) {
+ sessionLogic.actionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
@@ -1143,13 +1150,13 @@ class ZoningOperations(
//sync hack state
amenity.Definition match {
case GlobalDefinitions.capture_terminal =>
- sessionData.sendPlanetsideAttributeMessage(
+ sessionLogic.general.sendPlanetsideAttributeMessage(
amenity.GUID,
PlanetsideAttributeEnum.ControlConsoleHackUpdate,
HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false)
)
case _ =>
- sessionData.hackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object
+ sessionLogic.general.hackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object
}
// sync capture flags
@@ -1216,7 +1223,7 @@ class ZoningOperations(
if (!zoneReload && zoneId.equals(continent.id)) {
if (player.isBackpack) { // important! test the actor-wide player ref, not the parameter
// respawning from unregistered player
- TaskWorkflow.execute(sessionData.registerAvatar(targetPlayer))
+ TaskWorkflow.execute(registerAvatar(targetPlayer))
} else {
// move existing player; this is the one case where the original GUID is retained by the player
context.self ! SessionActor.PlayerLoaded(targetPlayer)
@@ -1336,7 +1343,7 @@ class ZoningOperations(
ICS.FindZone(_.id == zoneId, context.self)
))
} else {
- sessionData.unaccessContainer(vehicle)
+ sessionLogic.general.unaccessContainer(vehicle)
LoadZoneCommonTransferActivity()
player.VehicleSeated = vehicle.GUID
player.Continent = zoneId //forward-set the continent id to perform a test
@@ -1354,7 +1361,7 @@ class ZoningOperations(
//unregister vehicle and driver whole + GiveWorld
continent.Transport ! Zone.Vehicle.Despawn(vehicle)
TaskWorkflow.execute(taskThenZoneChange(
- sessionData.unregisterDrivenVehicle(vehicle, player),
+ unregisterDrivenVehicle(vehicle, player),
ICS.FindZone(_.id == zoneId, context.self)
))
}
@@ -1480,9 +1487,9 @@ class ZoningOperations(
// allow AMS, ANT and Router to remain deployed when owner leaves the zone
vehicle.Definition match {
case GlobalDefinitions.ams | GlobalDefinitions.ant | GlobalDefinitions.router
- => sessionData.vehicles.ConditionalDriverVehicleControl(vehicle)
+ => sessionLogic.vehicles.ConditionalDriverVehicleControl(vehicle)
- case _ => sessionData.vehicles.TotalDriverVehicleControl(vehicle)
+ case _ => sessionLogic.vehicles.TotalDriverVehicleControl(vehicle)
}
// remove owner
@@ -1492,12 +1499,12 @@ class ZoningOperations(
}
avatarActor ! AvatarActor.SetVehicle(None)
}
- sessionData.removeBoomerTriggersFromInventory().foreach(obj => {
+ spawn.removeBoomerTriggersFromInventory().foreach(obj => {
TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj))
})
Deployables.Disown(continent, avatar, context.self)
spawn.drawDeloyableIcon = spawn.RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
- sessionData.squad.squadSetup = sessionData.squad.ZoneChangeSquadSetup
+ sessionLogic.squad.squadSetup = sessionLogic.squad.ZoneChangeSquadSetup
}
/**
@@ -1514,7 +1521,7 @@ class ZoningOperations(
if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) {
log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.")
sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary."))
- sessionData.immediateDisconnect()
+ sessionLogic.immediateDisconnect()
} else {
continent.GUID(player.VehicleSeated) match {
case Some(obj: Vehicle) if !obj.Destroyed =>
@@ -1547,8 +1554,8 @@ class ZoningOperations(
def LoadZoneLaunchDroppod(zone: Zone, spawnPosition: Vector3): Unit = {
log.info(s"${player.Name} is launching to ${zone.id} in ${player.Sex.possessive} droppod")
CancelZoningProcess()
- sessionData.playerActionsToCancel()
- sessionData.terminals.CancelAllProximityUnits()
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
//droppod action
val droppod = Vehicle(GlobalDefinitions.droppod)
droppod.GUID = PlanetSideGUID(0) //droppod is not registered, we must jury-rig this
@@ -1596,6 +1603,88 @@ class ZoningOperations(
}
}
+ /**
+ * Construct tasking that registers all aspects of a `Player` avatar
+ * as if that player was already introduced and is just being renewed.
+ * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
+ * @param tplayer the avatar `Player`
+ * @return a `TaskBundle` message
+ */
+ private[session] def registerAvatar(tplayer: Player): TaskBundle = {
+ TaskBundle(
+ new StraightforwardTask() {
+ private val localPlayer = tplayer
+ private val localAnnounce = context.self
+
+ override def description(): String = s"register player avatar ${localPlayer.Name}"
+
+ def action(): Future[Any] = {
+ localAnnounce ! SessionActor.PlayerLoaded(localPlayer)
+ Future(true)
+ }
+ },
+ List(GUIDTask.registerPlayer(continent.GUID, tplayer))
+ )
+ }
+
+ /**
+ * Construct tasking that registers all aspects of a `Player` avatar
+ * as if that player is only just being introduced.
+ * `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
+ * @param tplayer the avatar `Player`
+ * @return a `TaskBundle` message
+ */
+ private[session] def registerNewAvatar(tplayer: Player): TaskBundle = {
+ TaskBundle(
+ new StraightforwardTask() {
+ private val localPlayer = tplayer
+ private val localAnnounce = context.self
+
+ override def description(): String = s"register new player avatar ${localPlayer.Name}"
+
+ def action(): Future[Any] = {
+ localAnnounce ! SessionActor.NewPlayerLoaded(localPlayer)
+ Future(true)
+ }
+ },
+ List(GUIDTask.registerAvatar(continent.GUID, tplayer))
+ )
+ }
+
+ private[session] def registerDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = {
+ TaskBundle(
+ new StraightforwardTask() {
+ private val localVehicle = vehicle
+ private val localDriver = driver
+ private val localAnnounce = context.self
+
+ override def description(): String = s"register a ${localVehicle.Definition.Name} driven by ${localDriver.Name}"
+
+ def action(): Future[Any] = {
+ localDriver.VehicleSeated = localVehicle.GUID
+ Vehicles.Own(localVehicle, localDriver)
+ localAnnounce ! SessionActor.NewPlayerLoaded(localDriver)
+ Future(true)
+ }
+ },
+ List(GUIDTask.registerAvatar(continent.GUID, driver), GUIDTask.registerVehicle(continent.GUID, vehicle))
+ )
+ }
+
+ private[session] def unregisterDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = {
+ TaskBundle(
+ new StraightforwardTask() {
+ private val localVehicle = vehicle
+ private val localDriver = driver
+
+ override def description(): String = s"unregister a ${localVehicle.Definition.Name} driven by ${localDriver.Name}"
+
+ def action(): Future[Any] = Future(true)
+ },
+ List(GUIDTask.unregisterAvatar(continent.GUID, driver), GUIDTask.unregisterVehicle(continent.GUID, vehicle))
+ )
+ }
+
/**
* Use this function to facilitate registering a droppod for a globally unique identifier
* in the event that the user has instigated an instant action event to a destination within the current zone.
@@ -1665,15 +1754,15 @@ class ZoningOperations(
/* nested class - spawn operations */
class SpawnOperations() {
- private[support] var deadState: DeadState.Value = DeadState.Dead
- private[support] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]()
- private[support] var amsSpawnPoints: List[SpawnPoint] = Nil
- private[support] var noSpawnPointHere: Boolean = false
- private[support] var setupAvatarFunc: () => Unit = AvatarCreate
- private[support] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally
- private[support] var nextSpawnPoint: Option[SpawnPoint] = None
- private[support] var interimUngunnedVehicle: Option[PlanetSideGUID] = None
- private[support] var interimUngunnedVehicleSeat: Option[Int] = None
+ private[session] var deadState: DeadState.Value = DeadState.Dead
+ private[session] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]()
+ private[session] var amsSpawnPoints: List[SpawnPoint] = Nil
+ private[session] var noSpawnPointHere: Boolean = false
+ private[session] var setupAvatarFunc: () => Unit = AvatarCreate
+ private[session] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally
+ private[session] var nextSpawnPoint: Option[SpawnPoint] = None
+ private[session] var interimUngunnedVehicle: Option[PlanetSideGUID] = None
+ private[session] var interimUngunnedVehicleSeat: Option[Int] = None
/** Upstream message counter
* Checks for server acknowledgement of the following messages in the following conditions:
* `PlayerStateMessageUpstream` (infantry)
@@ -1682,14 +1771,14 @@ class ZoningOperations(
* `KeepAliveMessage` (any passenger mount that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second
*/
- private[support] var upstreamMessageCount: Int = 0
- private[support] var shiftPosition: Option[Vector3] = None
- private[support] var shiftOrientation: Option[Vector3] = None
- private[support] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
- private[support] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery
- private[support] var setAvatar: Boolean = false
- private[support] var reviveTimer: Cancellable = Default.Cancellable
- private[support] var respawnTimer: Cancellable = Default.Cancellable
+ private[session] var upstreamMessageCount: Int = 0
+ private[session] var shiftPosition: Option[Vector3] = None
+ private[session] var shiftOrientation: Option[Vector3] = None
+ private[session] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
+ private[session] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery
+ private[session] var setAvatar: Boolean = false
+ private[session] var reviveTimer: Cancellable = Default.Cancellable
+ private[session] var respawnTimer: Cancellable = Default.Cancellable
private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields
@@ -1703,7 +1792,7 @@ class ZoningOperations(
HandleReleaseAvatar(player, continent)
}
- def handleSpawnRequest(pkt: SpawnRequestMessage)(implicit context: ActorContext): Unit = {
+ def handleSpawnRequest(pkt: SpawnRequestMessage): Unit = {
val SpawnRequestMessage(_, spawnGroup, _, _, zoneNumber) = pkt
log.info(s"${player.Name} on ${continent.id} wants to respawn in zone #$zoneNumber")
if (deadState != DeadState.RespawnTime) {
@@ -1730,7 +1819,7 @@ class ZoningOperations(
def handleLoginInfoNowhere(name: String, from: ActorRef): Unit = {
log.info(s"LoginInfo: player $name is considered a fresh character")
- sessionData.persistFunc = UpdatePersistence(from)
+ sessionLogic.persistFunc = UpdatePersistence(from)
deadState = DeadState.RespawnTime
val tplayer = new Player(avatar)
session = session.copy(player = tplayer)
@@ -1742,7 +1831,7 @@ class ZoningOperations(
def handleLoginInfoSomewhere(name: String, inZone: Zone, optionalSavedData: Option[Savedplayer], from: ActorRef): Unit = {
log.info(s"LoginInfo: player $name is considered a fresh character")
- sessionData.persistFunc = UpdatePersistence(from)
+ sessionLogic.persistFunc = UpdatePersistence(from)
deadState = DeadState.RespawnTime
session = session.copy(player = new Player(avatar))
player.Zone = inZone
@@ -1836,25 +1925,25 @@ class ZoningOperations(
def handleLoginInfoRestore(name: String, inZone: Zone, pos: Vector3, from: ActorRef): Unit = {
log.info(s"RestoreInfo: player $name is already logged in zone ${inZone.id}; rejoining that character")
- sessionData.persistFunc = UpdatePersistence(from)
+ sessionLogic.persistFunc = UpdatePersistence(from)
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
inZone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.TeardownConnection())
spawn.switchAvatarStatisticsFieldToRefreshAfterRespawn()
//find and reload previous player
(
inZone.Players.find(p => p.name.equals(name)),
- inZone.LivePlayers.find(p => p.Name.equals(name))
+ inZone.AllPlayers.find(p => p.Name.equals(name))
) match {
case (_, Some(p)) if p.death_by == -1 =>
//player is not allowed
- sessionData.kickedByAdministration()
+ sessionLogic.kickedByAdministration()
case (Some(a), Some(p)) if p.isAlive =>
//rejoin current avatar/player
log.info(s"RestoreInfo: player $name is alive")
deadState = DeadState.Alive
session = session.copy(player = p, avatar = a)
- sessionData.persist()
+ sessionLogic.persist()
setupAvatarFunc = AvatarRejoin
dropMedicalApplicators(p)
avatarActor ! AvatarActor.ReplaceAvatar(a)
@@ -1865,7 +1954,7 @@ class ZoningOperations(
log.info(s"RestoreInfo: player $name is dead")
deadState = DeadState.Dead
session = session.copy(player = p, avatar = a)
- sessionData.persist()
+ sessionLogic.persist()
dropMedicalApplicators(p)
HandleReleaseAvatar(p, inZone)
avatarActor ! AvatarActor.ReplaceAvatar(a)
@@ -1900,7 +1989,7 @@ class ZoningOperations(
def handleLoginCanNot(name: String, reason: PlayerToken.DeniedLoginReason.Value): Unit = {
log.warn(s"LoginInfo: $name is denied login for reason - $reason")
reason match {
- case PlayerToken.DeniedLoginReason.Kicked => sessionData.kickedByAdministration()
+ case PlayerToken.DeniedLoginReason.Kicked => sessionLogic.kickedByAdministration()
case _ => sendResponse(DisconnectMessage("You will be logged out."))
}
}
@@ -1939,9 +2028,9 @@ class ZoningOperations(
}
val previousZoningType = ztype
CancelZoningProcess()
- sessionData.playerActionsToCancel()
- sessionData.terminals.CancelAllProximityUnits()
- sessionData.dropSpecialSlotItem()
+ sessionLogic.actionsToCancel()
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sessionLogic.general.dropSpecialSlotItem()
continent.Population ! Zone.Population.Release(avatar)
resolveZoningSpawnPointLoad(response, previousZoningType)
}
@@ -1954,8 +2043,8 @@ class ZoningOperations(
val map = zone.map
val mapName = map.name
log.info(s"${tplayer.Name} has spawned into $id")
- sessionData.oldRefsMap.clear()
- sessionData.persist = UpdatePersistenceAndRefs
+ sessionLogic.oldRefsMap.clear()
+ sessionLogic.persist = UpdatePersistenceAndRefs
tplayer.avatar = avatar
session = session.copy(player = tplayer)
avatarActor ! AvatarActor.CreateImplants()
@@ -1967,7 +2056,7 @@ class ZoningOperations(
//important! the LoadMapMessage must be processed by the client before the avatar is created
setupAvatarFunc()
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
- sessionData.turnCounterFunc = interimUngunnedVehicle match {
+ sessionLogic.turnCounterFunc = interimUngunnedVehicle match {
case Some(_) =>
TurnCounterDuringInterimWhileInPassengerSeat
case None if zoningType == Zoning.Method.Login || zoningType == Zoning.Method.Reset =>
@@ -1975,14 +2064,14 @@ class ZoningOperations(
case None =>
TurnCounterDuringInterim
}
- sessionData.keepAliveFunc = NormalKeepAlive
+ sessionLogic.keepAliveFunc = NormalKeepAlive
if (zoningStatus == Zoning.Status.Deconstructing) {
- sessionData.stopDeconstructing()
+ stopDeconstructing()
}
- sessionData.avatarResponse.lastSeenStreamMessage.clear()
+ sessionLogic.avatarResponse.lastSeenStreamMessage.clear()
upstreamMessageCount = 0
setAvatar = false
- sessionData.persist()
+ sessionLogic.persist()
} else {
//look for different spawn point in same zone
cluster ! ICS.GetNearbySpawnPoint(
@@ -2004,20 +2093,20 @@ class ZoningOperations(
//try this spawn point
setupAvatarFunc()
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
- sessionData.turnCounterFunc = interimUngunnedVehicle match {
+ sessionLogic.turnCounterFunc = interimUngunnedVehicle match {
case Some(_) =>
TurnCounterDuringInterimWhileInPassengerSeat
case None =>
TurnCounterDuringInterim
}
- sessionData.keepAliveFunc = NormalKeepAlive
+ sessionLogic.keepAliveFunc = NormalKeepAlive
if (zoningStatus == Zoning.Status.Deconstructing) {
- sessionData.stopDeconstructing()
+ stopDeconstructing()
}
- sessionData.avatarResponse.lastSeenStreamMessage.clear()
+ sessionLogic.avatarResponse.lastSeenStreamMessage.clear()
upstreamMessageCount = 0
setAvatar = false
- sessionData.persist()
+ sessionLogic.persist()
} else {
//look for different spawn point in same zone
cluster ! ICS.GetNearbySpawnPoint(
@@ -2077,6 +2166,7 @@ class ZoningOperations(
*/
def avatarLoginResponse(avatar: Avatar): Unit = {
session = session.copy(avatar = avatar)
+ sessionLogic.chat.JoinChannel(DefaultChannel)
Deployables.InitializeDeployableQuantities(avatar)
cluster ! ICS.FilterZones(_ => true, context.self)
}
@@ -2087,7 +2177,7 @@ class ZoningOperations(
* @param zone na
*/
def HandleReleaseAvatar(tplayer: Player, zone: Zone): Unit = {
- sessionData.keepAliveFunc = sessionData.keepAlivePersistence
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
tplayer.Release
tplayer.VehicleSeated match {
case None =>
@@ -2101,8 +2191,8 @@ class ZoningOperations(
}
def handleSetPosition(position: Vector3): Unit = {
- if (sessionData.vehicles.serverVehicleControlVelocity.isEmpty) {
- sessionData.playerActionsToCancel()
+ if (sessionLogic.vehicles.serverVehicleControlVelocity.isEmpty) {
+ sessionLogic.actionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
@@ -2147,7 +2237,7 @@ class ZoningOperations(
player.Armor = armor
}
player.death_by = math.min(player.death_by, 0)
- sessionData.vehicles.GetKnownVehicleAndSeat() match {
+ sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
case (Some(vehicle: Vehicle), Some(seat: Int)) =>
//if the vehicle is the cargo of another vehicle in this zone
val carrierInfo = continent.GUID(vehicle.MountedIn) match {
@@ -2254,7 +2344,7 @@ class ZoningOperations(
* Neither the player avatar nor the vehicle should be reconstructed before the next zone load operation
* to avoid damaging the critical setup of this function.
* @see `AccessContainer`
- * @see `UpdateWeaponAtSeatPosition`
+ * @see `SessionMountHandlers.updateWeaponAtSeatPosition`
* @param tplayer the player avatar seated in the vehicle's mount
* @param vehicle the vehicle the player is riding
* @param seat the mount index
@@ -2271,8 +2361,8 @@ class ZoningOperations(
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) {
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
- sessionData.accessContainer(vehicle)
- sessionData.updateWeaponAtSeatPosition(vehicle, seat)
+ sessionLogic.general.accessContainer(vehicle)
+ sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seat)
} else {
interimUngunnedVehicle = Some(vguid)
interimUngunnedVehicleSeat = Some(seat)
@@ -2317,7 +2407,7 @@ class ZoningOperations(
* to avoid damaging the critical setup of this function.
*/
def AvatarRejoin(): Unit = {
- sessionData.vehicles.GetKnownVehicleAndSeat() match {
+ sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
case (Some(vehicle: Vehicle), Some(seat: Int)) =>
//vehicle and driver/passenger
val vdef = vehicle.Definition
@@ -2336,8 +2426,8 @@ class ZoningOperations(
log.debug(s"AvatarRejoin: ${player.Name} - $pguid -> $pdata")
if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) {
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
- sessionData.accessContainer(vehicle)
- sessionData.updateWeaponAtSeatPosition(vehicle, seat)
+ sessionLogic.general.accessContainer(vehicle)
+ sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seat)
} else {
interimUngunnedVehicle = Some(vguid)
interimUngunnedVehicleSeat = Some(seat)
@@ -2406,10 +2496,37 @@ class ZoningOperations(
case Some(_) | None => ;
}
})
- sessionData.removeBoomerTriggersFromInventory().foreach(trigger => { sessionData.normalItemDrop(obj, continent)(trigger) })
+ removeBoomerTriggersFromInventory().foreach(trigger => { sessionLogic.general.normalItemDrop(obj, continent)(trigger) })
}
}
+ /**
+ * Search through the player's holsters and their inventory space
+ * and remove all `BoomerTrigger` objects, both functionally and visually.
+ * @return all discovered `BoomTrigger` objects
+ */
+ def removeBoomerTriggersFromInventory(): List[BoomerTrigger] = {
+ val events = continent.AvatarEvents
+ val zoneId = continent.id
+ (player.Inventory.Items ++ player.HolsterItems())
+ .collect { case InventoryItem(obj: BoomerTrigger, index) =>
+ player.Slot(index).Equipment = None
+ continent.GUID(obj.Companion) match {
+ case Some(mine: BoomerDeployable) => mine.Actor ! Deployable.Ownership(None)
+ case _ => ()
+ }
+ if (player.VisibleSlots.contains(index)) {
+ events ! AvatarServiceMessage(
+ zoneId,
+ AvatarAction.ObjectDelete(Service.defaultPlayerGUID, obj.GUID)
+ )
+ } else {
+ sendResponse(ObjectDeleteMessage(obj.GUID, 0))
+ }
+ obj
+ }
+ }
+
/**
* Creates a player that has the characteristics of a corpse
* so long as the player has items in their knapsack or their holsters.
@@ -2762,10 +2879,10 @@ class ZoningOperations(
SessionActor.SetCurrentAvatar(player, max_attempts, attempt + max_attempts / 3)
)
} else {
- sessionData.keepAliveFunc = sessionData.vehicles.GetMountableAndSeat(None, player, continent) match {
+ sessionLogic.keepAliveFunc = sessionLogic.vehicles.GetMountableAndSeat(None, player, continent) match {
case (Some(v: Vehicle), Some(seatNumber))
if seatNumber > 0 && v.WeaponControlledFromSeat(seatNumber).isEmpty =>
- sessionData.keepAlivePersistence
+ sessionLogic.keepAlivePersistence
case _ =>
NormalKeepAlive
}
@@ -2791,36 +2908,34 @@ class ZoningOperations(
def HandleSetCurrentAvatar(tplayer: Player): Unit = {
log.trace(s"HandleSetCurrentAvatar - ${tplayer.Name}")
session = session.copy(player = tplayer)
+ val tavatar = tplayer.avatar
val guid = tplayer.GUID
- sessionData.updateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar))
+ sessionLogic.general.updateDeployableUIElements(Deployables.InitializeDeployableUIElements(tavatar))
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 75, 0))
sendResponse(SetCurrentAvatarMessage(guid, 0, 0))
sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, wideContents=true, "", "1 on", None)) //CC on //TODO once per respawn?
- val pos = player.Position = shiftPosition.getOrElse(tplayer.Position)
- val orient = player.Orientation = shiftOrientation.getOrElse(tplayer.Orientation)
+ val pos = tplayer.Position = shiftPosition.getOrElse(tplayer.Position)
+ val orient = tplayer.Orientation = shiftOrientation.getOrElse(tplayer.Orientation)
sendResponse(PlayerStateShiftMessage(ShiftState(1, pos, orient.z)))
shiftPosition = None
shiftOrientation = None
- if (player.spectator) {
- sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, wideContents=false, "", "on", None))
- }
- if (player.Jammed) {
+ if (tplayer.Jammed) {
//TODO something better than just canceling?
- player.Actor ! JammableUnit.ClearJammeredStatus()
- player.Actor ! JammableUnit.ClearJammeredSound()
+ tplayer.Actor ! JammableUnit.ClearJammeredStatus()
+ tplayer.Actor ! JammableUnit.ClearJammeredSound()
}
val originalDeadState = deadState
deadState = DeadState.Alive
avatarActor ! AvatarActor.ResetImplants()
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
- initializeShortcutsAndBank(guid)
+ initializeShortcutsAndBank(guid, tavatar.shortcuts)
//Favorites lists
avatarActor ! AvatarActor.InitialRefreshLoadouts()
sendResponse(
SetChatFilterMessage(ChatChannel.Platoon, origin = false, ChatChannel.values.toList)
) //TODO will not always be "on" like this
- sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, unk5 = true))
+ sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, tplayer.Faction, unk5 = true))
//looking for squad (members)
if (tplayer.avatar.lookingForSquad) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
@@ -2851,7 +2966,7 @@ class ZoningOperations(
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
//squad stuff (loadouts, assignment)
- sessionData.squad.squadSetup()
+ sessionLogic.squad.squadSetup()
//MapObjectStateBlockMessage and ObjectCreateMessage?
//TacticsMessage?
//change the owner on our deployables (re-draw the icons for our deployables too)
@@ -2865,7 +2980,7 @@ class ZoningOperations(
drawDeloyableIcon = DontRedrawIcons
//assert or transfer vehicle ownership
- continent.GUID(player.avatar.vehicle) match {
+ continent.GUID(tplayer.avatar.vehicle) match {
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
vehicle.OwnerGuid = guid
continent.VehicleEvents ! VehicleServiceMessage(
@@ -2875,7 +2990,7 @@ class ZoningOperations(
case _ =>
avatarActor ! AvatarActor.SetVehicle(None)
}
- sessionData.vehicles.GetVehicleAndSeat() match {
+ sessionLogic.vehicles.GetVehicleAndSeat() match {
case (Some(vehicle), _) if vehicle.Definition == GlobalDefinitions.droppod =>
//we're falling
sendResponse(
@@ -2919,22 +3034,22 @@ class ZoningOperations(
)
case (Some(vehicle), _) =>
//passenger
- vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(player)
+ vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(tplayer)
case _ => ;
}
interstellarFerryTopLevelGUID = None
- if (loadConfZone && sessionData.connectionState == 100) {
+ if (loadConfZone && sessionLogic.connectionState == 100) {
configZone(continent)
loadConfZone = false
}
if (noSpawnPointHere) {
- RequestSanctuaryZoneSpawn(player, continent.Number)
- } else if (originalDeadState == DeadState.Dead || player.Health == 0) {
+ RequestSanctuaryZoneSpawn(tplayer, continent.Number)
+ } else if (originalDeadState == DeadState.Dead || tplayer.Health == 0) {
//killed during spawn setup or possibly a relog into a corpse (by accident?)
- player.Actor ! Player.Die()
+ tplayer.Actor ! Player.Die()
} else {
- AvatarActor.savePlayerData(player)
- sessionData.displayCharSavedMsgThenRenewTimer(
+ AvatarActor.savePlayerData(tplayer)
+ sessionLogic.general.displayCharSavedMsgThenRenewTimer(
Config.app.game.savedMsg.short.fixed,
Config.app.game.savedMsg.short.variable
)
@@ -2947,17 +3062,20 @@ class ZoningOperations(
}
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
.flatten
- val lastEntryOpt = player.History.lastOption
+ val lastEntryOpt = tplayer.History.lastOption
if (lastEntryOpt.exists { !_.isInstanceOf[IncarnationActivity] }) {
- player.LogActivity({
+ tplayer.LogActivity({
lastEntryOpt match {
case Some(_) =>
- ReconstructionActivity(PlayerSource(player), continent.Number, effortBy)
+ ReconstructionActivity(PlayerSource(tplayer), continent.Number, effortBy)
case None =>
- SpawningActivity(PlayerSource(player), continent.Number, effortBy)
+ SpawningActivity(PlayerSource(tplayer), continent.Number, effortBy)
}
})
}
+ if (!setAvatar && tplayer.spectator) {
+ context.self ! SessionActor.SetMode(SpectatorMode) //should reload spectator status
+ }
}
upstreamMessageCount = 0
setAvatar = true
@@ -2965,9 +3083,9 @@ class ZoningOperations(
!account.gm && /* gm's are excluded */
Config.app.game.promotion.active && /* play versus progress system must be active */
BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */
- avatar.scorecard.Lives.isEmpty && /* first life after login */
- avatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */
- player.History.size == 1 /* did nothing but come into existence */
+ tavatar.scorecard.Lives.isEmpty && /* first life after login */
+ tavatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */
+ tplayer.History.size == 1 /* did nothing but come into existence */
) {
ZoningOperations.reportProgressionSystem(context.self)
}
@@ -3024,7 +3142,13 @@ class ZoningOperations(
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
*/
def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = {
- avatar.shortcuts
+ 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
.collect { case (Some(shortcut), index) =>
sendResponse(CreateShortcutMessage(
@@ -3167,7 +3291,7 @@ class ZoningOperations(
def TurnCounterDuringInterim(guid: PlanetSideGUID): Unit = {
upstreamMessageCount = 0
if (player != null && player.HasGUID && player.GUID == guid && player.Zone == continent) {
- sessionData.turnCounterFunc = NormalTurnCounter
+ sessionLogic.turnCounterFunc = NormalTurnCounter
}
}
@@ -3201,15 +3325,15 @@ class ZoningOperations(
case (Some(vehicle: Vehicle), Some(vguid), Some(seat)) =>
//sit down
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
- sessionData.accessContainer(vehicle)
- sessionData.keepAliveFunc = sessionData.keepAlivePersistence
+ sessionLogic.general.accessContainer(vehicle)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
case _ => ;
//we can't find a vehicle? and we're still here? that's bad
player.VehicleSeated = None
}
interimUngunnedVehicle = None
interimUngunnedVehicleSeat = None
- sessionData.turnCounterFunc = NormalTurnCounter
+ sessionLogic.turnCounterFunc = NormalTurnCounter
}
}
/**
@@ -3226,7 +3350,7 @@ class ZoningOperations(
loginChatMessage.foreach { msg => sendResponse(ChatMsg(zoningChatMessageType, wideContents=false, "", msg, None)) }
loginChatMessage.clear()
CancelZoningProcess()
- sessionData.turnCounterFunc = NormalTurnCounter
+ sessionLogic.turnCounterFunc = NormalTurnCounter
}
/**
@@ -3403,7 +3527,7 @@ class ZoningOperations(
context.system.scheduler.scheduleOnce(
delay.milliseconds,
context.self,
- SessionActor.AvatarAwardMessageBundle(xs, delay)
+ ZoningOperations.AvatarAwardMessageBundle(xs, delay)
)
}
}
@@ -3413,8 +3537,8 @@ class ZoningOperations(
* Set to `persist` when (new) player is loaded.
*/
def UpdatePersistenceAndRefs(): Unit = {
- sessionData.persistFunc()
- sessionData.updateOldRefsMap()
+ sessionLogic.persistFunc()
+ sessionLogic.updateOldRefsMap()
}
/**
@@ -3425,6 +3549,45 @@ class ZoningOperations(
def UpdatePersistence(persistRef: ActorRef)(): Unit = {
persistRef ! AccountPersistenceService.Update(player.Name, continent, player.Position)
}
+
+ def startDeconstructing(obj: SpawnTube): Unit = {
+ log.info(s"${player.Name} is deconstructing at the ${obj.Owner.Definition.Name}'s spawns")
+ avatar.implants.collect {
+ case Some(implant) if implant.active && !implant.definition.Passive =>
+ avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
+ }
+ if (player.ExoSuit != ExoSuitType.MAX) {
+ player.Actor ! PlayerControl.ObjectHeld(Player.HandsDownSlot, updateMyHolsterArm = true)
+ }
+ nextSpawnPoint = Some(obj) //set fallback
+ zoningStatus = Zoning.Status.Deconstructing
+ player.allowInteraction = false
+ if (player.death_by == 0) {
+ player.death_by = 1
+ }
+ GoToDeploymentMap()
+ }
+
+ def stopDeconstructing(): Unit = {
+ zoningStatus = Zoning.Status.None
+ player.death_by = math.min(player.death_by, 0)
+ player.allowInteraction = true
+ nextSpawnPoint.foreach { tube =>
+ sendResponse(PlayerStateShiftMessage(ShiftState(0, tube.Position, tube.Orientation.z)))
+ nextSpawnPoint = None
+ }
+ }
+
+ def randomRespawn(time: FiniteDuration = 300.seconds): Unit = {
+ reviveTimer = context.system.scheduler.scheduleOnce(time) {
+ cluster ! ICS.GetRandomSpawnPoint(
+ Zones.sanctuaryZoneNumber(player.Faction),
+ player.Faction,
+ Seq(SpawnGroup.Sanctuary),
+ context.self
+ )
+ }
+ }
}
override protected[session] def stop(): Unit = {
diff --git a/src/main/scala/net/psforever/objects/BoomerDeployable.scala b/src/main/scala/net/psforever/objects/BoomerDeployable.scala
index 33fb76008..612fbe112 100644
--- a/src/main/scala/net/psforever/objects/BoomerDeployable.scala
+++ b/src/main/scala/net/psforever/objects/BoomerDeployable.scala
@@ -13,6 +13,8 @@ import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.PlanetSideEmpire
+import scala.annotation.unused
+
class BoomerDeployable(cdef: ExplosiveDeployableDefinition)
extends ExplosiveDeployable(cdef) {
private var trigger: Option[BoomerTrigger] = None
@@ -70,8 +72,8 @@ class BoomerDeployableControl(mine: BoomerDeployable)
case _ => ;
}
- override def loseOwnership(faction: PlanetSideEmpire.Value): Unit = {
- super.loseOwnership(PlanetSideEmpire.NEUTRAL)
+ def loseOwnership(@unused faction: PlanetSideEmpire.Value): Unit = {
+ super.loseOwnership(mine, PlanetSideEmpire.NEUTRAL)
val guid = mine.OwnerGuid
mine.AssignOwnership(None)
mine.OwnerGuid = guid
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/Players.scala b/src/main/scala/net/psforever/objects/Players.scala
index ae57870cc..a69c18dfa 100644
--- a/src/main/scala/net/psforever/objects/Players.scala
+++ b/src/main/scala/net/psforever/objects/Players.scala
@@ -4,14 +4,18 @@ package net.psforever.objects
import net.psforever.objects.avatar.Certification
import net.psforever.login.WorldSession.FindEquipmentStock
import net.psforever.objects.avatar.PlayerControl
+import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.ce.Deployable
import net.psforever.objects.definition.ExoSuitDefinition
import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.InfantryLoadout
+import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.PlayerSource
-import net.psforever.objects.vital.RevivingActivity
+import net.psforever.objects.vehicles.MountedWeapons
+import net.psforever.objects.vital.projectile.ProjectileReason
+import net.psforever.objects.vital.{InGameActivity, InGameHistory, RevivingActivity}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
@@ -256,7 +260,7 @@ object Players {
PlayerControl.sendResponse(
zone,
channel,
- ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None)
+ ChatMsg(ChatMessageType.UNK_229, s"@${definition.Descriptor}OldestDestroyed")
)
}
true
@@ -278,7 +282,7 @@ object Players {
PlayerControl.sendResponse(
zone,
channel,
- ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}LimitReached", None)
+ ChatMsg(ChatMessageType.UNK_229, s"@${definition.Descriptor}LimitReached")
)
}
true
@@ -400,7 +404,7 @@ object Players {
val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
- AvatarAction.ObjectDelete(Service.defaultPlayerGUID, tool.GUID, 0)
+ AvatarAction.ObjectDelete(Service.defaultPlayerGUID, tool.GUID)
)
true
} else {
@@ -466,4 +470,22 @@ object Players {
)
}
}
+
+ /**
+ * na
+ * @param zone where the event occurred
+ * @param player person attributed to the event
+ * @param killStat information about the event
+ * @return player-specific historical information and then related information that is inherited from other entities
+ */
+ def produceContributionTranscriptFromKill(zone: Zone, player: Player, killStat: Kill): List[InGameActivity] = {
+ (killStat.info.interaction.cause match {
+ case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
+ case _ => None
+ }).collect {
+ case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
+ player.ContributionFrom(mount)
+ }
+ player.HistoryAndContributions()
+ }
}
diff --git a/src/main/scala/net/psforever/objects/Session.scala b/src/main/scala/net/psforever/objects/Session.scala
index 06b5d6d9a..8dbe39f9b 100644
--- a/src/main/scala/net/psforever/objects/Session.scala
+++ b/src/main/scala/net/psforever/objects/Session.scala
@@ -15,3 +15,7 @@ case class Session(
speed: Float = 1.0f,
flying: Boolean = false
)
+
+trait SessionSource {
+ def session: Session
+}
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/FirstTimeEvents.scala b/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala
index a196bb21d..a527095ea 100644
--- a/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala
+++ b/src/main/scala/net/psforever/objects/avatar/FirstTimeEvents.scala
@@ -463,4 +463,9 @@ object FirstTimeEvents {
"xpe_th_flail",
"xpe_th_bfr"
)
+
+ val All: Set[String] = NC.All ++ TR.All ++ VS.All ++
+ Standard.All ++ Cavern.All ++
+ Maps ++ Monoliths ++ Gingerman ++ Sled ++ Snowman ++ Charlie ++ BattleRanks ++ CommandRanks ++
+ Training ++ OldTraining ++ Generic
}
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/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
index b8ff9be0c..9d86393ce 100644
--- a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala
@@ -69,7 +69,7 @@ trait DeployableBehavior {
if DeployableObject.OwnerGuid.nonEmpty =>
val obj = DeployableObject
if (constructed.contains(true)) {
- loseOwnership(obj.Faction)
+ loseOwnership(obj, obj.Faction)
} else {
obj.OwnerGuid = None
}
@@ -98,35 +98,16 @@ trait DeployableBehavior {
* Losing ownership involves updating map screen UI, to remove management rights from the now-previous owner,
* and may involve concealing the deployable from the map screen for the entirety of the previous owner's faction.
* Displaying the deployable on the map screen of another faction may be required.
+ * @param obj na
* @param toFaction the faction to which to set the deployable to be visualized on the map and in the game world;
* may also affect deployable operation
*/
- def loseOwnership(toFaction: PlanetSideEmpire.Value): Unit = {
- val obj = DeployableObject
- val guid = obj.GUID
- val zone = obj.Zone
- val localEvents = zone.LocalEvents
- val originalFaction = obj.Faction
- val changeFaction = originalFaction != toFaction
- val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, Service.defaultPlayerGUID)
- if (changeFaction) {
- obj.Faction = toFaction
- //visual tells in regards to ownership by faction
- zone.AvatarEvents ! AvatarServiceMessage(
- zone.id,
- AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
- )
- //remove knowledge by the previous owner's faction
- localEvents ! LocalServiceMessage(
- originalFaction.toString,
- LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
- )
- //display to the given faction
- localEvents ! LocalServiceMessage(
- toFaction.toString,
- LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
- )
- }
+ def loseOwnership(obj: Deployable, toFaction: PlanetSideEmpire.Value): Unit = {
+ DeployableBehavior.changeOwership(
+ obj,
+ obj.Faction,
+ DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, Service.defaultPlayerGUID)
+ )
startOwnerlessDecay()
}
@@ -159,27 +140,11 @@ trait DeployableBehavior {
val obj = DeployableObject
obj.AssignOwnership(player)
decay.cancel()
-
- val guid = obj.GUID
- val zone = obj.Zone
- val originalFaction = obj.Faction
- val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.OwnerGuid.get)
- if (originalFaction != toFaction) {
- obj.Faction = toFaction
- val localEvents = zone.LocalEvents
- zone.AvatarEvents ! AvatarServiceMessage(
- zone.id,
- AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
- )
- localEvents ! LocalServiceMessage(
- originalFaction.toString,
- LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
- )
- localEvents ! LocalServiceMessage(
- toFaction.toString,
- LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
- )
- }
+ DeployableBehavior.changeOwership(
+ obj,
+ toFaction,
+ DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, obj.OwnerGuid.get)
+ )
}
/**
@@ -318,4 +283,35 @@ object DeployableBehavior {
/** internal message for progresisng the deconstruction process */
private case class FinalizeElimination()
+
+ /**
+ * na
+ * @param obj na
+ * @param toFaction na
+ * @param info na
+ */
+ def changeOwership(obj: Deployable, toFaction: PlanetSideEmpire.Value, info: DeployableInfo): Unit = {
+ val guid = obj.GUID
+ val zone = obj.Zone
+ val localEvents = zone.LocalEvents
+ val originalFaction = obj.Faction
+ if (originalFaction != toFaction) {
+ obj.Faction = toFaction
+ //visual tells in regards to ownership by faction
+ zone.AvatarEvents ! AvatarServiceMessage(
+ zone.id,
+ AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
+ )
+ //remove knowledge by the previous owner's faction
+ localEvents ! LocalServiceMessage(
+ originalFaction.toString,
+ LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
+ )
+ //display to the given faction
+ localEvents ! LocalServiceMessage(
+ toFaction.toString,
+ LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
+ )
+ }
+ }
}
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 62b3eeff3..31f5f3992 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -69,7 +69,7 @@ object AvatarConverter {
obj.avatar.basic,
CommonFieldData(
obj.Faction,
- bops = false,
+ bops = obj.spectator,
alt_model_flag,
v1 = false,
None,
diff --git a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
index 50a07a9d2..7f19cf8d2 100644
--- a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
@@ -11,6 +11,23 @@ object CommonMessages {
final case class Hack(player: Player, obj: PlanetSideServerObject with Hackable, data: Option[Any] = None)
final case class ClearHack()
+ /**
+ * The message that progresses some form of user-driven activity with a certain eventual outcome
+ * and potential feedback per cycle.
+ * @param delta how much the progress value changes each tick, which will be treated as a percentage;
+ * must be a positive value
+ * @param completionAction a finalizing action performed once the progress reaches 100(%)
+ * @param tickAction an action that is performed for each increase of progress
+ * @param tickTime how long between each `tickAction` (ms);
+ * defaults to 250 milliseconds
+ */
+ final case class ProgressEvent(
+ delta: Float,
+ completionAction: () => Unit,
+ tickAction: Float => Boolean,
+ tickTime: Long = 250L
+ )
+
/**
* The message that initializes a process -
* some form of user-driven activity with a certain eventual outcome and potential feedback per cycle.
diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala
index 22acf7b44..6e6d7aef2 100644
--- a/src/main/scala/net/psforever/objects/zones/Zone.scala
+++ b/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -566,9 +566,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
def Vehicles: List[Vehicle] = vehicles.toList
- def Players: List[Avatar] = players.values.flatten.map(_.avatar).toList
+ def AllPlayers: List[Player] = players.values.flatten.toList
- def LivePlayers: List[Player] = players.values.flatten.toList
+ def Players: List[Avatar] = AllPlayers.map(_.avatar)
+
+ def LivePlayers: List[Player] = AllPlayers.filterNot(_.spectator)
+
+ def Spectator: List[Player] = AllPlayers.filter(_.spectator)
def Corpses: List[Player] = corpses.toList
diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 291928c6c..bea0c62eb 100644
--- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -489,14 +489,14 @@ object GamePacketOpcode extends Enumeration {
case 0x9b => noDecoder(SyncMessage)
case 0x9c => game.DebugDrawMessage.decode
case 0x9d => noDecoder(SoulMarkMessage)
- case 0x9e => noDecoder(UplinkPositionEvent)
+ case 0x9e => game.UplinkPositionEvent.decode
case 0x9f => game.HotSpotUpdateMessage.decode
// OPCODES 0xa0-af
case 0xa0 => game.BuildingInfoUpdateMessage.decode
case 0xa1 => game.FireHintMessage.decode
- case 0xa2 => noDecoder(UplinkRequest)
- case 0xa3 => noDecoder(UplinkResponse)
+ case 0xa2 => game.UplinkRequest.decode
+ case 0xa3 => game.UplinkResponse.decode
case 0xa4 => game.WarpgateRequest.decode
case 0xa5 => noDecoder(WarpgateResponse)
case 0xa6 => game.DamageWithPositionMessage.decode
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/packet/game/UplinkPositionEvent.scala b/src/main/scala/net/psforever/packet/game/UplinkPositionEvent.scala
new file mode 100644
index 000000000..ede79cf4d
--- /dev/null
+++ b/src/main/scala/net/psforever/packet/game/UplinkPositionEvent.scala
@@ -0,0 +1,114 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.GamePacketOpcode.Type
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import shapeless.{::, HNil}
+import net.psforever.types.Vector3
+import scodec.bits.BitVector
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+
+import scala.annotation.switch
+
+trait UplinkEvent {
+ def code: Int
+}
+
+final case class Event0(code: Int) extends UplinkEvent
+
+final case class Event1(code: Int, unk1: Int) extends UplinkEvent
+
+final case class Event2(
+ code: Int,
+ unk1: Vector3,
+ unk2: Int,
+ unk3: Int,
+ unk4: Long,
+ unk5: Long,
+ unk6: Long,
+ unk7: Long,
+ unk8: Option[Boolean]
+ ) extends UplinkEvent
+
+final case class UplinkPositionEvent(
+ code: Int,
+ event: UplinkEvent
+ ) extends PlanetSideGamePacket {
+ type Packet = UplinkPositionEvent
+ def opcode: Type = GamePacketOpcode.UplinkPositionEvent
+ def encode: Attempt[BitVector] = UplinkPositionEvent.encode(this)
+}
+
+object UplinkPositionEvent extends Marshallable[UplinkPositionEvent] {
+ private def event0Codec(code: Int): Codec[Event0] = conditional(included = false, bool).xmap[Event0](
+ _ => Event0(code),
+ {
+ case Event0(_) => None
+ }
+ )
+
+ private def event1Codec(code: Int): Codec[Event1] =
+ ("unk1" | uint8L).hlist.xmap[Event1](
+ {
+ case unk1 :: HNil => Event1(code, unk1)
+ },
+ {
+ case Event1(_, unk1) => unk1 :: HNil
+ }
+ )
+
+ private def event2NoBoolCodec(code: Int): Codec[Event2] = (
+ ("unk1" | Vector3.codec_pos) ::
+ ("unk2" | uint8) ::
+ ("unk3" | uint16L) ::
+ ("unk4" | uint32L) ::
+ ("unk5" | uint32L) ::
+ ("unk6" | uint32L) ::
+ ("unk7" | uint32L)
+ ).xmap[Event2](
+ {
+ case u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil =>
+ Event2(code, u1, u2, u3, u4, u5, u6, u7, None)
+ },
+ {
+ case Event2(_, u1, u2, u3, u4, u5, u6, u7, _) =>
+ u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil
+ }
+ )
+
+ private def event2WithBoolCodec(code: Int): Codec[Event2] = (
+ ("unk1" | Vector3.codec_pos) ::
+ ("unk2" | uint8) ::
+ ("unk3" | uint16L) ::
+ ("unk4" | uint32L) ::
+ ("unk5" | uint32L) ::
+ ("unk6" | uint32L) ::
+ ("unk7" | uint32L) ::
+ ("unk8" | bool)
+ ).xmap[Event2](
+ {
+ case u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: u8 :: HNil =>
+ Event2(code, u1, u2, u3, u4, u5, u6, u7, Some(u8))
+ },
+ {
+ case Event2(_, u1, u2, u3, u4, u5, u6, u7, Some(u8)) =>
+ u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: u8 :: HNil
+ }
+ )
+
+ private def switchUplinkEvent(code: Int): Codec[UplinkEvent] = {
+ ((code: @switch) match {
+ case 0 => event2NoBoolCodec(code)
+ case 1 | 2 => event2WithBoolCodec(code)
+ case 3 | 4 => event1Codec(code)
+ case _ => event0Codec(code)
+ }).asInstanceOf[Codec[UplinkEvent]]
+ }
+
+ implicit val codec: Codec[UplinkPositionEvent] = (
+ ("code" | uint(bits = 3)) >>:~ { code =>
+ ("event" | switchUplinkEvent(code)).hlist
+ }
+ ).as[UplinkPositionEvent]
+}
diff --git a/src/main/scala/net/psforever/packet/game/UplinkRequest.scala b/src/main/scala/net/psforever/packet/game/UplinkRequest.scala
new file mode 100644
index 000000000..f4a66e591
--- /dev/null
+++ b/src/main/scala/net/psforever/packet/game/UplinkRequest.scala
@@ -0,0 +1,81 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.packet.game
+
+import enumeratum.values.{IntEnum, IntEnumEntry}
+import shapeless.{::, HNil}
+import net.psforever.newcodecs.newcodecs
+import net.psforever.packet.GamePacketOpcode.Type
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.bits.BitVector
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+
+sealed abstract class UplinkRequestType(val value: Int) extends IntEnumEntry
+
+object UplinkRequestType extends IntEnum[UplinkRequestType] {
+ val values: IndexedSeq[UplinkRequestType] = findValues
+
+ case object RevealFriendlies extends UplinkRequestType(value = 0)
+
+ case object RevealEnemies extends UplinkRequestType(value = 1)
+
+ case object Unknown2 extends UplinkRequestType(value = 2)
+
+ case object ElectroMagneticPulse extends UplinkRequestType(value = 3)
+
+ case object OrbitalStrike extends UplinkRequestType(value = 4)
+
+ case object Unknown5 extends UplinkRequestType(value = 5)
+
+ case object Function6 extends UplinkRequestType(value = 6)
+
+ case object Function7 extends UplinkRequestType(value = 7)
+
+ case object Function8 extends UplinkRequestType(value = 8)
+
+ case object Unknown9 extends UplinkRequestType(value = 9)
+
+ case object UnknownA extends UplinkRequestType(value = 10)
+
+ case object FunctionB extends UplinkRequestType(value = 11)
+
+ case object FunctionC extends UplinkRequestType(value = 12)
+
+ case object UnknownD extends UplinkRequestType(value = 13)
+
+ case object UnknownE extends UplinkRequestType(value = 14)
+
+ case object FunctionF extends UplinkRequestType(value = 15)
+
+ implicit val codec: Codec[UplinkRequestType] = PacketHelpers.createIntEnumCodec(this, uint4)
+}
+
+final case class UplinkRequest(
+ uplinkType: UplinkRequestType,
+ pos: Option[Vector3],
+ unk: Boolean
+ ) extends PlanetSideGamePacket {
+ type Packet = UplinkRequest
+ def opcode: Type = GamePacketOpcode.UplinkRequest
+ def encode: Attempt[BitVector] = UplinkRequest.encode(this)
+}
+
+object UplinkRequest extends Marshallable[UplinkRequest] {
+ private val xyToVector3: Codec[Vector3] =
+ (newcodecs.q_float(0.0, 8192.0, 20) ::
+ newcodecs.q_float(0.0, 8192.0, 20)).xmap[Vector3](
+ {
+ case x :: y :: HNil => Vector3(x, y, 0f)
+ },
+ {
+ case Vector3(x, y, _) => x :: y :: HNil
+ }
+ )
+
+ implicit val codec: Codec[UplinkRequest] = (
+ ("uplinkType" | UplinkRequestType.codec) >>:~ { uplinkType =>
+ conditional(uplinkType == UplinkRequestType.OrbitalStrike, xyToVector3) ::
+ ("unk" | bool)
+ }).as[UplinkRequest]
+}
diff --git a/src/main/scala/net/psforever/packet/game/UplinkResponse.scala b/src/main/scala/net/psforever/packet/game/UplinkResponse.scala
new file mode 100644
index 000000000..b1aef366f
--- /dev/null
+++ b/src/main/scala/net/psforever/packet/game/UplinkResponse.scala
@@ -0,0 +1,24 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.GamePacketOpcode.Type
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.bits.BitVector
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+
+final case class UplinkResponse(
+ unk1: Int,
+ unk2: Int
+ ) extends PlanetSideGamePacket {
+ type Packet = UplinkResponse
+ def opcode: Type = GamePacketOpcode.UplinkResponse
+ def encode: Attempt[BitVector] = UplinkResponse.encode(this)
+}
+
+object UplinkResponse extends Marshallable[UplinkResponse] {
+ implicit val codec: Codec[UplinkResponse] = (
+ ("unk1" | uint(bits = 3)) ::
+ ("unk2" | uint4)
+ ).as[UplinkResponse]
+}
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/main/scala/net/psforever/services/account/AccountPersistenceService.scala b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala
index 2576bfedf..1d2fe0f4f 100644
--- a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala
+++ b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala
@@ -363,7 +363,7 @@ class PersistenceMonitor(
* but should be uncommon.
*/
def PerformLogout(): Unit = {
- (inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match {
+ (inZone.Players.find(p => p.name == name), inZone.AllPlayers.find(p => p.Name == name)) match {
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
//in case the player is holding the llu and disconnects
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DropSpecialItem())
diff --git a/src/main/scala/net/psforever/services/chat/ChatChannel.scala b/src/main/scala/net/psforever/services/chat/ChatChannel.scala
new file mode 100644
index 000000000..94515f0a3
--- /dev/null
+++ b/src/main/scala/net/psforever/services/chat/ChatChannel.scala
@@ -0,0 +1,12 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.services.chat
+
+import net.psforever.types.PlanetSideGUID
+
+trait ChatChannel
+
+case object DefaultChannel extends ChatChannel
+
+final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel
+
+case object SpectatorChannel extends ChatChannel
diff --git a/src/main/scala/net/psforever/services/chat/ChatService.scala b/src/main/scala/net/psforever/services/chat/ChatService.scala
index 0aa8ff2cf..8803c3667 100644
--- a/src/main/scala/net/psforever/services/chat/ChatService.scala
+++ b/src/main/scala/net/psforever/services/chat/ChatService.scala
@@ -4,9 +4,9 @@ package net.psforever.services.chat
import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
-import net.psforever.objects.Session
+import net.psforever.objects.{Session, SessionSource}
import net.psforever.packet.game.ChatMsg
-import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID}
+import net.psforever.types.{ChatMessageType, PlanetSideEmpire}
object ChatService {
val ChatServiceKey: ServiceKey[Command] = ServiceKey[ChatService.Command]("chatService")
@@ -19,20 +19,12 @@ object ChatService {
sealed trait Command
- final case class JoinChannel(actor: ActorRef[MessageResponse], session: Session, channel: ChatChannel) extends Command
+ final case class JoinChannel(actor: ActorRef[MessageResponse], sessionSource: SessionSource, channel: ChatChannel) extends Command
final case class LeaveChannel(actor: ActorRef[MessageResponse], channel: ChatChannel) extends Command
final case class LeaveAllChannels(actor: ActorRef[MessageResponse]) extends Command
final case class Message(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
final case class MessageResponse(session: Session, message: ChatMsg, channel: ChatChannel)
-
- trait ChatChannel
- object ChatChannel {
- // one of the default channels that the player is always subscribed to (local, broadcast, command...)
- final case class Default() extends ChatChannel
- final case class Squad(guid: PlanetSideGUID) extends ChatChannel
- }
-
}
class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBehavior[ChatService.Command](context) {
@@ -63,9 +55,10 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case Message(session, message, channel) =>
(channel, message.messageType) match {
- case (ChatChannel.Squad(_), CMT_SQUAD) => ()
- case (ChatChannel.Squad(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
- case (ChatChannel.Default(), messageType) if messageType != CMT_SQUAD => ()
+ case (SquadChannel(_), CMT_SQUAD) => ()
+ case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
+ case (DefaultChannel, messageType) if messageType != CMT_SQUAD => ()
+ case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => ()
case _ =>
log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
return this
@@ -78,8 +71,8 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
val recipientName = message.recipient
val recipientNameLower = recipientName.toLowerCase()
(
- subs.find(_.session.player.Name.toLowerCase().equals(playerNameLower)),
- subs.find(_.session.player.Name.toLowerCase().equals(recipientNameLower))
+ subs.find(_.sessionSource.session.player.Name.toLowerCase().equals(playerNameLower)),
+ subs.find(_.sessionSource.session.player.Name.toLowerCase().equals(recipientNameLower))
) match {
case (Some(JoinChannel(sender, _, _)), Some(JoinChannel(receiver, _, _))) =>
val replyType = if (mtype == CMT_TELL) { U_CMT_TELLFROM } else { U_CMT_GMTELLFROM }
@@ -122,14 +115,14 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case _ => (None, None, None)
}
- val sender = subs.find(_.session.player.Name == session.player.Name)
+ val sender = subs.find(_.sessionSource.session.player.Name == session.player.Name)
(sender, name, time, error) match {
case (Some(sender), Some(name), Some(_), None) =>
- val recipient = subs.find(_.session.player.Name == name)
+ val recipient = subs.find(_.sessionSource.session.player.Name == name)
recipient match {
case Some(recipient) =>
- if (recipient.session.player.silenced) {
+ if (recipient.sessionSource.session.player.silenced) {
sender.actor ! MessageResponse(
session,
ChatMsg(UNK_229, wideContents = true, "", "@silence_disabled_ack", None),
@@ -167,7 +160,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case CMT_NOTE =>
subs
- .filter(_.session.player.Name == message.recipient)
+ .filter(_.sessionSource.session.player.Name == message.recipient)
.foreach(
_.actor ! MessageResponse(session, message.copy(recipient = session.player.Name), channel)
)
@@ -175,23 +168,23 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
// faction commands
case CMT_OPEN | CMT_PLATOON | CMT_COMMAND =>
subs
- .filter(_.session.player.Faction == session.player.Faction)
+ .filter(_.sessionSource.session.player.Faction == session.player.Faction)
.foreach(
_.actor ! MessageResponse(session, message, channel)
)
case CMT_GMBROADCAST_NC =>
- subs.filter(_.session.player.Faction == PlanetSideEmpire.NC).foreach {
+ subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.NC).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
case CMT_GMBROADCAST_TR =>
- subs.filter(_.session.player.Faction == PlanetSideEmpire.TR).foreach {
+ subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.TR).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
case CMT_GMBROADCAST_VS =>
- subs.filter(_.session.player.Faction == PlanetSideEmpire.VS).foreach {
+ subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.VS).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
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