initial workings for a csr/gm player mode

This commit is contained in:
Fate-JH 2024-09-27 01:02:13 -04:00
parent 360c3264bd
commit 30a4eba646
19 changed files with 6135 additions and 125 deletions

View file

@ -27,7 +27,7 @@ world {
# How the server is displayed in the server browser.
# One of: released beta development
server-type = released
server-type = development
}
# Admin API configuration

View file

@ -1061,17 +1061,19 @@ object AvatarActor {
val result = ctx.run(query[persistence.Avatarmodepermission].filter(_.avatarId == lift(avatarId)))
result.onComplete {
case Success(res) =>
val isDevServer = Config.app.world.serverType == ServerType.Development
res.headOption
.collect {
case perms: persistence.Avatarmodepermission =>
out.completeWith(Future(ModePermissions(perms.canSpectate, perms.canGm)))
out.completeWith(Future(ModePermissions(perms.canSpectate || isDevServer, perms.canGm || isDevServer)))
}
.orElse {
out.completeWith(Future(ModePermissions()))
out.completeWith(Future(ModePermissions(isDevServer, isDevServer)))
None
}
case _ =>
out.completeWith(Future(ModePermissions()))
val isDevServer = Config.app.world.serverType == ServerType.Development
out.completeWith(Future(ModePermissions(isDevServer, isDevServer)))
}
out.future
}

View file

@ -0,0 +1,586 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.support.AvatarHandlerFunctions
import net.psforever.objects.definition.converter.OCM
import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction}
import net.psforever.types.ImplantType
//
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.terminals.{ProximityUnit, Terminal}
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.AvatarImplant(ImplantAction.Add, implant_slot, value)
if value == ImplantType.SecondWind.value =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Add, implant_slot, 7))
//second wind does not normally load its icon into the shortcut hotbar
avatar
.shortcuts
.zipWithIndex
.find { case (s, _) => s.isEmpty}
.foreach { case (_, index) =>
sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, Some(ImplantType.SecondWind.shortcut)))
}
case AvatarResponse.AvatarImplant(ImplantAction.Remove, implant_slot, value)
if value == ImplantType.SecondWind.value =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Remove, implant_slot, value))
//second wind does not normally unload its icon from the shortcut hotbar
val shortcut = {
val imp = ImplantType.SecondWind.shortcut
net.psforever.objects.avatar.Shortcut(imp.code, imp.tile) //case class
}
avatar
.shortcuts
.zipWithIndex
.find { case (s, _) => s.contains(shortcut) }
.foreach { case (_, index) =>
sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, None))
}
case AvatarResponse.AvatarImplant(action, implant_slot, value) =>
sendResponse(AvatarImplantMessage(resolvedPlayerGuid, action, implant_slot, value))
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) =>
//pure logic
sessionLogic.shooting.shotsWhileDead = 0
//player state changes
sessionLogic.zoning.spawn.reviveTimer.cancel()
player.Revive
val health = player.Health
sendResponse(PlanetsideAttributeMessage(player.GUID, 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(player.GUID, attribute_type=0, health)
)
avatarActor ! AvatarActor.InitializeImplants
AvatarActor.updateToolDischargeFor(avatar)
player.FreeHand.Equipment.foreach { item =>
DropEquipmentFromInventory(player)(item)
}
continent.GUID(mount)
.collect {
case obj: Vehicle if obj.Destroyed =>
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
sessionLogic.general.unaccessContainer(obj)
player.VehicleSeated = None
sendResponse(OCM.detailed(player))
case _: Vehicle =>
player.VehicleSeated = None
sendResponse(OCM.detailed(player))
}
sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE"))
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 _ => ()
}
}
}

View file

@ -0,0 +1,238 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.ActorContext
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.DefaultChannel
import net.psforever.types.ChatMessageType
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
ops.SpectatorMode = SpectateAsCustomerServiceRepresentativeMode
def handleChatMsg(message: ChatMsg): Unit = {
import net.psforever.types.ChatMessageType._
val isAlive = if (player != null) player.isAlive else false
(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, _, contents)=>
customCommandModerator(contents)
case (CMT_CULLWATERMARK, _, contents) =>
ops.commandWatermark(contents)
case (CMT_SPEED, _, contents) =>
ops.commandSpeed(message, contents)
case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive =>
commandToggleSpectatorMode(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) =>
ops.commandSetBaseResources(session, contents)
case (CMT_ZONELOCK, _, contents) =>
ops.commandZoneLock(contents)
case (U_CMT_ZONEROTATE, _, _) =>
ops.commandZoneRotate()
case (CMT_CAPTUREBASE, _, contents) =>
ops.commandCaptureBase(session, message, contents)
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) =>
ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_GMTELL, _, _) =>
ops.commandSend(session, message, DefaultChannel)
case (CMT_GMBROADCASTPOPUP, _, _) =>
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, _, _) =>
ops.commandSendToRecipient(session, message, DefaultChannel)
case (CMT_NOTE, _, _) =>
ops.commandSend(session, message, DefaultChannel)
case (CMT_SILENCE, _, _) =>
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) =>
ops.commandZone(message, contents)
case (CMT_WARP, _, contents) =>
ops.commandWarp(session, message, contents)
case (CMT_SETBATTLERANK, _, contents) =>
ops.commandSetBattleRank(session, message, contents)
case (CMT_SETCOMMANDRANK, _, contents) =>
ops.commandSetCommandRank(session, message, contents)
case (CMT_ADDBATTLEEXPERIENCE, _, contents) =>
ops.commandAddBattleExperience(message, contents)
case (CMT_ADDCOMMANDEXPERIENCE, _, contents) =>
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) =>
ops.commandAddCertification(session, message, contents)
case (CMT_KICK, _, contents) =>
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(""))
}
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 "whitetext" => ops.customCommandWhitetext(session, params)
case "list" => ops.customCommandList(session, params, message)
case "ntu" => ops.customCommandNtu(session, params)
case "zonerotate" => ops.customCommandZonerotate(params)
case "nearby" => ops.customCommandNearby(session)
case "csr" | "gm" | "op" => customCommandModerator(params.headOption.getOrElse(""))
case _ =>
// command was not handled
sendResponse(
ChatMsg(
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
false
}
} else {
false
}
}
def commandToggleSpectatorMode(contents: String): Unit = {
// val currentSpectatorActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canSpectate
// contents.toLowerCase() match {
// case "on" | "o" | "" if !currentSpectatorActivation =>
// context.self ! SessionActor.SetMode(SessionSpectatorMode)
// case "off" | "of" if currentSpectatorActivation =>
// context.self ! SessionActor.SetMode(SessionCustomerServiceRepresentativeMode)
// case _ => ()
// }
}
def customCommandModerator(contents : String): Boolean = {
if (player.spectator) {
sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE"))
sendResponse(ChatMsg(ChatMessageType.UNK_227, "Disable spectator mode first."))
} else {
ops.customCommandModerator(contents)
}
true
}
}

View file

@ -0,0 +1,46 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.Session
import net.psforever.packet.PlanetSidePacket
import net.psforever.packet.game.ChatMsg
import net.psforever.types.ChatMessageType
class CustomerServiceRepresentativeMode(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)
override def switchTo(session: Session): Unit = {
val player = session.player
val continent = session.zone
val sendResponse: PlanetSidePacket=>Unit = data.sendResponse
//
continent.actor ! ZoneActor.RemoveFromBlockMap(player)
sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE ON"))
}
override def switchFrom(session: Session): Unit = {
val player = data.player
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
//
data.continent.actor ! ZoneActor.AddToBlockMap(player, player.Position)
sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE OFF"))
}
}
case object CustomerServiceRepresentativeMode extends PlayerMode {
def setup(data: SessionData): ModeLogic = {
new CustomerServiceRepresentativeMode(data)
}
}

View file

@ -0,0 +1,86 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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 pop = zone.LivePlayers.distinctBy(_.CharId)
val popTR = pop.count(_.Faction == PlanetSideEmpire.TR)
val popNC = pop.count(_.Faction == PlanetSideEmpire.NC)
val popVS = pop.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 _ => ()
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,268 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
import akka.actor.ActorContext
import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers}
import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.doors.Door
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, HackState1, 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
/* messages */
def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = {
ops.handleTurretDeployableIsDismissed(obj)
}
def handleDeployableIsDismissed(obj: Deployable): Unit = {
ops.handleDeployableIsDismissed(obj)
}
/* 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 =>
val pos = player.Position.xy
val range = ops.doorLoadRange()
val foundDoor = continent
.blockMap
.sector(pos, range)
.amenityList
.collect { case door: Door => door }
.find(_.GUID == doorGuid)
val doorExistsInRange: Boolean = foundDoor.nonEmpty
if (doorExistsInRange) {
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
ops.deactivateTelpadDeployableMessages(dguid)
//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
ops.deactivateTelpadDeployableMessages(dguid)
//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(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, 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))
}
}

View file

@ -0,0 +1,520 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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)
)
}
}

View file

@ -0,0 +1,120 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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.serverobject.ServerObject
import net.psforever.objects.{Session, Vehicle}
import net.psforever.packet.PlanetSidePacket
import net.psforever.packet.game.ObjectDeleteMessage
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.chat.SpectatorChannel
import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, SquadRequestType}
//
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData}
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.packet.game.{ChatMsg, UnuseItemMessage}
class SpectatorCSRModeLogic(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)) }
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 _ => ()
}
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)
)
if (player.silenced) {
data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0"))
}
//
player.spectator = true
data.chat.JoinChannel(SpectatorChannel)
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on"))
sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE"))
data.session = session.copy(player = player)
}
override def switchFrom(session: Session): Unit = {
val player = data.player
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
//
data.continent.actor ! ZoneActor.AddToBlockMap(player, player.Position)
data.general.stop()
data.chat.LeaveChannel(SpectatorChannel)
player.spectator = false
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled"))
}
}
case object SpectateAsCustomerServiceRepresentativeMode extends PlayerMode {
def setup(data: SessionData): ModeLogic = {
new SpectatorCSRModeLogic(data)
}
}

View file

@ -0,0 +1,358 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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 _ => ()
}
}
}
}

View file

@ -0,0 +1,184 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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 =>
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
}
}
}

View file

@ -0,0 +1,399 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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)
}
}
}

View file

@ -0,0 +1,355 @@
// Copyright (c) 2024 PSForever
package net.psforever.actors.session.csr
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.{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
ops.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
ops.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")
.collect {
case 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
)
)
}
}
def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
continent.GUID(vehicle_guid)
.collect {
case obj: Vehicle =>
val vehicle = player.avatar.vehicle
if (!vehicle.contains(vehicle_guid)) {
log.warn(s"DeployRequest: ${player.Name} does not own the would-be-deploying ${obj.Definition.Name}")
} else if (vehicle != player.VehicleSeated) {
log.warn(s"${player.Name} must be mounted as the driver to request a deployment change")
} else {
log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state")
continent.Transport ! Zone.Vehicle.TryDeploymentChange(obj, deploy_state)
}
obj
case obj =>
log.error(s"DeployRequest: ${player.Name} expected a vehicle, but found a ${obj.Definition.Name} instead")
obj
}
.orElse {
log.error(s"DeployRequest: ${player.Name} can not find entity $vehicle_guid")
avatarActor ! AvatarActor.SetVehicle(None) //todo is this safe
None
}
}
/* 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 */
/**
* 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")
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,12 @@
package net.psforever.actors.session.normal
import akka.actor.ActorContext
import net.psforever.actors.session.spectator.SpectatorMode
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 = {
@ -19,34 +18,27 @@ object ChatLogic {
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
def sessionLogic: SessionData = ops.sessionLogic
ops.SpectatorMode = SpectatorMode
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_TOGGLE_GM, _, contents) =>
ops.customCommandModerator(contents)
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_TOGGLESPECTATORMODE, _, contents) if isAlive =>
ops.commandToggleSpectatorMode(contents)
case (CMT_RECALL, _, _) =>
ops.commandRecall(session)
@ -63,28 +55,6 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
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)
@ -100,54 +70,21 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
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 (CMT_REPORTUSER, _, contents) =>
ops.commandReportUser(session, message, contents)
case _ =>
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@no_permission"))
}
@ -192,51 +129,28 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
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
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 "csr" | "gm" | "op" => ops.customCommandModerator(params.headOption.getOrElse(""))
case _ =>
// command was not handled
sendResponse(
ChatMsg(
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
message.wideContents,
"Server",
s"Unknown command !$command",
message.note
)
)
)
false
}
result
} else {
false // not a handled command
false
}
}
}

View file

@ -292,8 +292,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
/* 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)) ||
((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))) &&
@ -324,7 +323,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
case Some(obj: Deployable) =>
if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) {
if (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")

View file

@ -7,7 +7,9 @@ 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.session.csr.{CustomerServiceRepresentativeMode => SessionCustomerServiceRepresentativeMode}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.ModePermissions
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.ZoneInfo
import net.psforever.packet.game.SetChatFilterMessage
@ -67,6 +69,8 @@ class ChatOperations(
*/
private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]()
private[session] var SpectatorMode: PlayerMode = SessionSpectatorMode
import akka.actor.typed.scaladsl.adapter._
private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse]
@ -112,8 +116,8 @@ class ChatOperations(
sendResponse(message.copy(contents = f"$speed%.3f"))
}
def commandToggleSpectatorMode(session: Session, contents: String): Unit = {
val currentSpectatorActivation = session.player.spectator
def commandToggleSpectatorMode(contents: String): Unit = {
val currentSpectatorActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canSpectate
contents.toLowerCase() match {
case "on" | "o" | "" if !currentSpectatorActivation =>
context.self ! SessionActor.SetMode(SessionSpectatorMode)
@ -1226,7 +1230,7 @@ class ChatOperations(
params: Seq[String]
): Boolean = {
val ourRank = BattleRank.withExperience(session.avatar.bep).value
if (!session.account.gm &&
if (!avatar.permissions.canGM &&
(ourRank <= Config.app.game.promotion.broadcastBattleRank ||
ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
setBattleRank(session, params, AvatarActor.Progress)
@ -1253,6 +1257,18 @@ class ChatOperations(
true
}
def customCommandModerator(contents: String): Boolean = {
val currentCsrActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canGM
contents.toLowerCase() match {
case "on" | "o" | "" if currentCsrActivation =>
context.self ! SessionActor.SetMode(SessionCustomerServiceRepresentativeMode)
case "off" | "of" if currentCsrActivation =>
context.self ! SessionActor.SetMode(SessionNormalMode)
case _ => ()
}
true
}
def firstParam[T](
session: Session,
buffer: Iterable[String],

View file

@ -3214,10 +3214,10 @@ class ZoningOperations(
upstreamMessageCount = 0
if (tplayer.spectator) {
if (!setAvatar) {
context.self ! SessionActor.SetMode(SpectatorMode) //should reload spectator status
context.self ! SessionActor.SetMode(sessionLogic.chat.SpectatorMode) //should reload spectator status
}
} else if (
!account.gm && /* gm's are excluded */
!avatar.permissions.canGM && /* 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 */
tavatar.scorecard.Lives.isEmpty && /* first life after login */