diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 2caeb24db..2e0046e95 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -2,11 +2,13 @@ package net.psforever.actors.session import akka.actor.{Actor, Cancellable, MDCContextAware, typed} -import net.psforever.actors.session.support.NormalUser import org.joda.time.LocalDateTime import org.log4s.MDC +import scala.collection.mutable // import net.psforever.actors.net.MiddlewareActor +import net.psforever.actors.session.normal.NormalMode +import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData} import net.psforever.objects.{Default, Player} import net.psforever.objects.avatar.Avatar import net.psforever.objects.definition.BasicDefinition @@ -14,8 +16,6 @@ import net.psforever.packet.PlanetSidePacket import net.psforever.packet.game.{FriendsResponse, KeepAliveMessage} import net.psforever.types.Vector3 -import scala.collection.mutable - object SessionActor { sealed trait Command @@ -68,29 +68,34 @@ object SessionActor { final case object CharSaved extends Command private[session] case object CharSavedMsg extends Command + + final case class SetMode(mode: PlayerMode) extends Command } class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) - extends Actor + extends Actor with MDCContextAware { MDC("connectionId") = connectionId private var clientKeepAlive: Cancellable = Default.Cancellable private[this] val buffer: mutable.ListBuffer[Any] = new mutable.ListBuffer[Any]() - private[this] val logic = new NormalUser(middlewareActor, context) + private[this] val data = new SessionData(middlewareActor, context) + private[this] var mode: PlayerMode = NormalMode + private[this] var logic: ModeLogic = _ override def postStop(): Unit = { clientKeepAlive.cancel() - logic.stop() + data.stop() } def receive: Receive = startup private def startup: Receive = { - case msg if !logic.assignEventBus(msg) => + case msg if !data.assignEventBus(msg) => buffer.addOne(msg) - case _ if logic.whenAllEventBusesLoaded() => + case _ if data.whenAllEventBusesLoaded() => context.become(inTheGame) + logic = mode.setup(data) startHeartbeat() buffer.foreach { self.tell(_, self) } //we forget the original sender, shouldn't be doing callbacks at this point buffer.clear() @@ -110,10 +115,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } private def inTheGame: Receive = { - /* used for the game's heartbeat*/ + /* used for the game's heartbeat */ case SessionActor.PokeClient() => middlewareActor ! MiddlewareActor.Send(KeepAliveMessage()) + case SessionActor.SetMode(newMode) => + mode = newMode + logic = mode.setup(data) + case packet => logic.parse(sender())(packet) } diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala new file mode 100644 index 000000000..89bdf3f66 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -0,0 +1,569 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.support.AvatarHandlerFunctions + +import scala.concurrent.duration._ +// +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionAvatarHandlers, SessionData} +import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp} +import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} +import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} +import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.serverobject.pad.VehicleSpawnPad +import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} +import net.psforever.objects.vital.etc.ExplodingEntityReason +import net.psforever.objects.zones.Zoning +import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent +import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage} +import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage} +import net.psforever.services.Service +import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3} +import net.psforever.util.Config + +class AvatarHandlerLogic(val ops: SessionAvatarHandlers) extends AvatarHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + /** + * na + * @param toChannel na + * @param guid na + * @param reply na + */ + def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = { + val resolvedPlayerGuid = if (player != null && player.HasGUID) { + player.GUID + } else { + Service.defaultPlayerGUID + } + val isNotSameTarget = resolvedPlayerGuid != guid + val isSameTarget = !isNotSameTarget + reply match { + /* special messages */ + case AvatarResponse.TeardownConnection() => + log.trace(s"ending ${player.Name}'s old session by event system request (relog)") + context.stop(context.self) + + /* really common messages (very frequently, every life) */ + case pstate @ AvatarResponse.PlayerState( + pos, + vel, + yaw, + pitch, + yawUpper, + _, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + isNotRendered, + canSeeReallyFar + ) if isNotSameTarget => + val pstateToSave = pstate.copy(timestamp = 0) + val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = ops.lastSeenStreamMessage.get(guid.guid) match { + case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting) + case _ => (None, 0L, Vector3.Zero, false, None) + } + val drawConfig = Config.app.game.playerDraw //m + val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m + val ourPosition = player.Position //xyz + val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m + val inDrawableRange = currentDistance <= maxRange + val now = System.currentTimeMillis() //ms + if ( + sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing && + !isNotRendered && inDrawableRange + ) { + //conditions where visibility is assured + val durationSince = now - lastTime //ms + lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange + lazy val targetDelay = { + val populationOver = math.max( + 0, + sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold + ) + val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m + val adjustedDistance = currentDistance + distanceAdjustment //sq.m + drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match { + case -1 => 1 + case index => drawConfig.delays(index) + } + } //ms + if (!wasVisible || + !previouslyInDrawableRange || + durationSince > drawConfig.delayMax || + (!lastMsg.contains(pstateToSave) && + (canSeeReallyFar || + currentDistance < drawConfig.rangeMin * drawConfig.rangeMin || + sessionLogic.general.canSeeReallyFar || + durationSince > targetDelay + ) + ) + ) { + //must draw + sendResponse( + PlayerStateMessage( + guid, + pos, + vel, + yaw, + pitch, + yawUpper, + timestamp = 0, //is this okay? + isCrouching, + isJumping, + jumpThrust, + isCloaking + ) + ) + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now)) + } else { + //is visible, but skip reinforcement + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime)) + } + } else { + //conditions where the target is not currently visible + if (wasVisible) { + //the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance + val lat = (1 + ops.hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat + sendResponse( + PlayerStateMessage( + guid, + Vector3(1f, lat, 1f), + vel=None, + facingYaw=0f, + facingPitch=0f, + facingYawUpper=0f, + timestamp=0, //is this okay? + is_cloaked = isCloaking + ) + ) + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now)) + } else { + //skip drawing altogether + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime)) + } + } + + case AvatarResponse.ObjectHeld(slot, _) + if isSameTarget && player.VisibleSlots.contains(slot) => + sendResponse(ObjectHeldMessage(guid, slot, unk1=true)) + //Stop using proximity terminals if player unholsters a weapon + continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect { + case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term) + } + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + sessionLogic.zoning.spawn.stopDeconstructing() + } + + case AvatarResponse.ObjectHeld(slot, _) + if isSameTarget && slot > -1 => + sendResponse(ObjectHeldMessage(guid, slot, unk1=true)) + + case AvatarResponse.ObjectHeld(_, _) + if isSameTarget => () + + case AvatarResponse.ObjectHeld(_, previousSlot) => + sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false)) + + case AvatarResponse.ChangeFireState_Start(weaponGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + val entry = ops.lastSeenStreamMessage(guid.guid) + ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid))) + + case AvatarResponse.ChangeFireState_Start(weaponGuid) + if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + + case AvatarResponse.ChangeFireState_Stop(weaponGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + val entry = ops.lastSeenStreamMessage(guid.guid) + ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None)) + + case AvatarResponse.ChangeFireState_Stop(weaponGuid) + if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + + case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget => + sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + + case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) => + sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + + case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget => + sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + + case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget => + sendResponse(GenericObjectActionMessage(objectGuid, actionCode)) + + case AvatarResponse.HitHint(sourceGuid) if player.isAlive => + sendResponse(HitHint(sourceGuid, guid)) + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + + case AvatarResponse.Destroy(victim, killer, weapon, pos) => + // guid = victim // killer = killer + sendResponse(DestroyMessage(victim, killer, weapon, pos)) + + case AvatarResponse.DestroyDisplay(killer, victim, method, unk) => + sendResponse(ops.destroyDisplayMessage(killer, victim, method, unk)) + + case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) + if result && (action == TransactionType.Buy || action == TransactionType.Loadout) => + sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) + sessionLogic.terminals.lastTerminalOrderFulfillment = true + AvatarActor.savePlayerData(player) + sessionLogic.general.renewCharSavedTimer( + Config.app.game.savedMsg.interruptedByAction.fixed, + Config.app.game.savedMsg.interruptedByAction.variable + ) + + case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) => + sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) + sessionLogic.terminals.lastTerminalOrderFulfillment = true + + case AvatarResponse.ChangeExosuit( + target, + armor, + exosuit, + subtype, + _, + maxhand, + oldHolsters, + holsters, + oldInventory, + inventory, + drop, + delete + ) if resolvedPlayerGuid == target => + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to this player + //cleanup + sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false)) + (oldHolsters ++ oldInventory ++ delete).foreach { + case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + } + //functionally delete + delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } + //redraw + if (maxhand) { + TaskWorkflow.execute(HoldNewEquipmentUp(player)( + Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), + 0 + )) + } + //draw free hand + player.FreeHand.Equipment.foreach { obj => + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(target, Player.FreeHandSlot), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + } + //draw holsters and inventory + (holsters ++ inventory).foreach { + case InventoryItem(obj, index) => + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(target, index), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + } + DropLeftovers(player)(drop) + + case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, _, delete) => + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to some other player + sendResponse(ObjectHeldMessage(target, slot, unk1 = false)) + //cleanup + (oldHolsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) } + //draw holsters + holsters.foreach { + case InventoryItem(obj, index) => + val definition = obj.Definition + sendResponse( + ObjectCreateMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(target, index), + definition.Packet.ConstructorData(obj).get + ) + ) + } + + case AvatarResponse.ChangeLoadout( + target, + armor, + exosuit, + subtype, + _, + maxhand, + oldHolsters, + holsters, + oldInventory, + inventory, + drops + ) if resolvedPlayerGuid == target => + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type = 4, armor)) + //happening to this player + sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true)) + //cleanup + (oldHolsters ++ oldInventory).foreach { + case (obj, objGuid) => + sendResponse(ObjectDeleteMessage(objGuid, unk1=0)) + TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) + } + drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0))) + //redraw + if (maxhand) { + TaskWorkflow.execute(HoldNewEquipmentUp(player)( + Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), + slot = 0 + )) + } + sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) + DropLeftovers(player)(drops) + + case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) => + //redraw handled by callbacks + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to some other player + sendResponse(ObjectHeldMessage(target, slot, unk1=false)) + //cleanup + oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) } + + case AvatarResponse.UseKit(kguid, kObjId) => + sendResponse( + UseItemMessage( + resolvedPlayerGuid, + kguid, + resolvedPlayerGuid, + unk2 = 4294967295L, + unk3 = false, + unk4 = Vector3.Zero, + unk5 = Vector3.Zero, + unk6 = 126, + unk7 = 0, //sequence time? + unk8 = 137, + kObjId + ) + ) + sendResponse(ObjectDeleteMessage(kguid, unk1=0)) + + case AvatarResponse.KitNotUsed(_, "") => + sessionLogic.general.kitToBeUsed = None + + case AvatarResponse.KitNotUsed(_, msg) => + sessionLogic.general.kitToBeUsed = None + sendResponse(ChatMsg(ChatMessageType.UNK_225, msg)) + + case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => + avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda) + + case AvatarResponse.AwardBep(charId, bep, expType) => + //if the target player, always award (some) BEP + if (charId == player.CharId) { + avatarActor ! AvatarActor.AwardBep(bep, expType) + } + + case AvatarResponse.AwardCep(charId, cep) => + //if the target player, always award (some) CEP + if (charId == player.CharId) { + avatarActor ! AvatarActor.AwardCep(cep) + } + + case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) => + ops.facilityCaptureRewards(buildingId, zoneNumber, cep) + + case AvatarResponse.SendResponse(msg) => + sendResponse(msg) + + case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid => + sendResponse(msg) + + /* common messages (maybe once every respawn) */ + case AvatarResponse.Reload(itemGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) + + case AvatarResponse.Killed(mount) => + //log and chat messages + val cause = player.LastDamage.flatMap { damage => + val interaction = damage.interaction + val reason = interaction.cause + val adversarial = interaction.adversarial.map { _.attacker } + reason match { + case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] => + //also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..." + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate")) + case _ => () + } + adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") } + }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" } + log.info(s"${player.Name} has died, killed by $cause") + if (sessionLogic.shooting.shotsWhileDead > 0) { + log.warn( + s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server" + ) + sessionLogic.shooting.shotsWhileDead = 0 + } + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") + sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) + + //player state changes + AvatarActor.updateToolDischargeFor(avatar) + player.FreeHand.Equipment.foreach { item => + DropEquipmentFromInventory(player)(item) + } + sessionLogic.general.dropSpecialSlotItem() + sessionLogic.general.toggleMaxSpecialState(enable = false) + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + sessionLogic.zoning.zoningStatus = Zoning.Status.None + sessionLogic.zoning.spawn.deadState = DeadState.Dead + continent.GUID(mount).collect { case obj: Vehicle => + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) + } + sessionLogic.actionsToCancel() + sessionLogic.terminals.CancelAllProximityUnits() + AvatarActor.savePlayerLocation(player) + sessionLogic.zoning.spawn.shiftPosition = Some(player.Position) + + //respawn + sessionLogic.zoning.spawn.reviveTimer.cancel() + if (player.death_by == 0) { + sessionLogic.zoning.spawn.randomRespawn(300.seconds) + } else { + sessionLogic.zoning.spawn.HandleReleaseAvatar(player, continent) + } + + case AvatarResponse.Release(tplayer) if isNotSameTarget => + sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer) + + case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid => + log.info(s"No time for rest, ${player.Name}. Back on your feet!") + sessionLogic.zoning.spawn.reviveTimer.cancel() + sessionLogic.zoning.spawn.deadState = DeadState.Alive + player.Revive + val health = player.Health + sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health)) + sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health) + ) + + /* uncommon messages (utility, or once in a while) */ + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) + sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) + if isNotSameTarget => + ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) + + case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget => + sendResponse(ChangeFireModeMessage(itemGuid, mode)) + + case AvatarResponse.ConcealPlayer() => + sendResponse(GenericObjectActionMessage(guid, code=9)) + + case AvatarResponse.EnvironmentalDamage(_, _, _) => + //TODO damage marker? + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + + case AvatarResponse.DropItem(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget => + sendResponse(ObjectDeleteMessage(itemGuid, unk)) + + /* rare messages */ + case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget => + sendResponse(SetEmpireMessage(objectGuid, faction)) + + case AvatarResponse.DropSpecialItem() => + sessionLogic.general.dropSpecialSlotItem() + + case AvatarResponse.OxygenState(player, vehicle) => + sendResponse(OxygenStateMessage( + DrowningTarget(player.guid, player.progress, player.state), + vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) } + )) + + case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget => + sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid)) + + case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) => + sendResponse( + ProjectileStateMessage( + projectileGuid, + projectile.Position, + shot_vel = Vector3.Zero, + projectile.Orientation, + sequence_num=0, + end=true, + hit_target_guid=PlanetSideGUID(0) + ) + ) + sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2)) + + case AvatarResponse.ProjectileAutoLockAwareness(mode) => + sendResponse(GenericActionMessage(mode)) + + case AvatarResponse.PutDownFDU(target) if isNotSameTarget => + sendResponse(GenericObjectActionMessage(target, code=53)) + + case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget => + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(target, slot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + + case AvatarResponse.WeaponDryFire(weaponGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + continent.GUID(weaponGuid).collect { + case tool: Tool if tool.Magazine == 0 => + // check that the magazine is still empty before sending WeaponDryFireMessage + // if it has been reloaded since then, other clients will not see it firing + sendResponse(WeaponDryFireMessage(weaponGuid)) + } + + case _ => () + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala new file mode 100644 index 000000000..69aac1a0d --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/GalaxyHandlerLogic.scala @@ -0,0 +1,81 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData} +import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo} +import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage} +import net.psforever.types.{MemberAction, PlanetSideEmpire} + +class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers) extends GalaxyHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + private val galaxyService: ActorRef = ops.galaxyService + + /* packets */ + + def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = { + sendResponse(pkt) + pkt.friends.foreach { f => + galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name)) + } + } + + /* response handlers */ + + def handle(reply: GalaxyResponse.Response): Unit = { + reply match { + case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => + sendResponse( + HotSpotUpdateMessage( + zone_index, + priority, + hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) } + ) + ) + + case GalaxyResponse.MapUpdate(msg) => + sendResponse(msg) + + case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) => + val faction = player.Faction + val from = fromFactions.contains(faction) + val to = toFactions.contains(faction) + if (from && !to) { + sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL)) + } else if (!from && to) { + sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction)) + } + + case GalaxyResponse.FlagMapUpdate(msg) => + sendResponse(msg) + + case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) => + sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest) + + case GalaxyResponse.LockedZoneUpdate(zone, time) => + sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time)) + + case GalaxyResponse.UnlockedZoneUpdate(zone) => ; + sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L)) + val popBO = 0 + val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR) + val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC) + val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS) + sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) + + case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) => + avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name) + + case GalaxyResponse.SendResponse(msg) => + sendResponse(msg) + + case _ => () + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala new file mode 100644 index 000000000..4592f531d --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -0,0 +1,1565 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.typed.scaladsl.adapter._ +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.{AvatarActor, ChatActor} +import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} +import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, PutLoadoutEquipmentInInventory, RemoveOldEquipmentFromInventory} +import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} +import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry} +import net.psforever.objects.ballistics.Projectile +import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike} +import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} +import net.psforever.objects.entity.WorldEntity +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} +import net.psforever.objects.inventory.{Container, InventoryItem} +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.generator.Generator +import net.psforever.objects.serverobject.llu.CaptureFlag +import net.psforever.objects.serverobject.locks.IFFLock +import net.psforever.objects.serverobject.mblocker.Locker +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.objects.serverobject.structures.{Building, WarpGate} +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityUnit, Terminal} +import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech +import net.psforever.objects.serverobject.tube.SpawnTube +import net.psforever.objects.serverobject.turret.FacilityTurret +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} +import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, UtilityType, VehicleLockState} +import net.psforever.objects.vehicles.Utility.InternalTelepad +import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity, Vitality} +import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} +import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning} +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.objectcreate.ObjectClass +import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, SetChatFilterMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage} +import net.psforever.services.RemoverActor +import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.local.support.CaptureFlagManager +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, DriveState, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, TransactionType, Vector3} +import net.psforever.util.Config + +import scala.concurrent.duration._ + +class GeneralLogic(val ops: GeneralOperations) extends GeneralFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor + + def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { + val ConnectToWorldRequestMessage(_, token, majorVersion, minorVersion, revision, buildDate, _, _) = pkt + log.trace( + s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server" + ) + sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None)) + sessionLogic.accountIntermediary ! RetrieveAccountData(token) + } + + def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { + val CharacterCreateRequestMessage(name, head, voice, gender, empire) = pkt + avatarActor ! AvatarActor.CreateAvatar(name, head, voice, gender, empire) + } + + def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { + val CharacterRequestMessage(charId, action) = pkt + action match { + case CharacterRequestAction.Delete => + avatarActor ! AvatarActor.DeleteAvatar(charId.toInt) + case CharacterRequestAction.Select => + avatarActor ! AvatarActor.SelectAvatar(charId.toInt, context.self) + } + } + + def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = { + val PlayerStateMessageUpstream( + avatarGuid, + pos, + vel, + yaw, + pitch, + yawUpper, + seqTime, + _, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + _, + _ + )= pkt + sessionLogic.persist() + sessionLogic.turnCounterFunc(avatarGuid) + sessionLogic.updateBlockMap(player, pos) + val isMoving = WorldEntity.isMoving(vel) + val isMovingPlus = isMoving || isJumping || jumpThrust + if (isMovingPlus) { + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + sessionLogic.zoning.spawn.stopDeconstructing() + } else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") + } + } + ops.fallHeightTracker(pos.z) + // if (isCrouching && !player.Crouching) { + // //dev stuff goes here + // } + player.Position = pos + player.Velocity = vel + player.Orientation = Vector3(player.Orientation.x, pitch, yaw) + player.FacingYawUpper = yawUpper + player.Crouching = isCrouching + player.Jumping = isJumping + if (isCloaking && !player.Cloaked) { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak") + } + player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking + maxCapacitorTick(jumpThrust) + if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) { + continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match { + case Some(term: Terminal with ProximityUnit) => + sessionLogic.terminals.StopUsingProximityUnit(term) + case _ => () + } + } + ops.accessedContainer match { + // Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle. + // This can happen if PSUM arrives during the mounting process + case Some(veh: Vehicle) if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID => + if (isMoving || veh.isMoving(test = 1) || Vector3.DistanceSquared(player.Position, veh.TrunkLocation) > 9) { + val guid = player.GUID + sendResponse(UnuseItemMessage(guid, veh.GUID)) + sendResponse(UnuseItemMessage(guid, guid)) + ops.unaccessContainer(veh) + } + case Some(container) => //just in case + if (isMovingPlus && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID)) { + // Ensure we don't close the container if the player is seated in it + val guid = player.GUID + // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first. + if (container.HasGUID) { + sendResponse(UnuseItemMessage(guid, container.GUID)) + } + sendResponse(UnuseItemMessage(guid, guid)) + ops.unaccessContainer(container) + } + case None => () + } + val eagleEye: Boolean = ops.canSeeReallyFar + val isNotVisible: Boolean = player.spectator || + sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || + (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlayerState( + avatarGuid, + player.Position, + player.Velocity, + yaw, + pitch, + yawUpper, + seqTime, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + isNotVisible, + eagleEye + ) + ) + sessionLogic.squad.updateSquad() + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + player.zoneInteractions() + } + + def handleChat(pkt: ChatMsg): Unit = { + chatActor ! ChatActor.Message(pkt) + } + + def handleChatFilter(pkt: SetChatFilterMessage): Unit = { + val SetChatFilterMessage(_, _, _) = pkt + } + + def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = { + log.debug(s"$pkt") + sendResponse(VoiceHostKill()) + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None) + ) + } + + def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = { + log.debug(s"$pkt") + sendResponse(VoiceHostKill()) + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None) + ) + } + + def handleEmote(pkt: EmoteMsg): Unit = { + val EmoteMsg(avatarGuid, emote) = pkt + sendResponse(EmoteMsg(avatarGuid, emote)) + } + + def handleDropItem(pkt: DropItemMessage): Unit = { + val DropItemMessage(itemGuid) = pkt + (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { + case (Some(anItem: Equipment), Some(heldItem)) + if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + RemoveOldEquipmentFromInventory(player)(heldItem) + case (Some(anItem: Equipment), Some(heldItem)) + if anItem eq heldItem => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + DropEquipmentFromInventory(player)(heldItem) + case (Some(anItem: Equipment), _) + if continent.GUID(player.VehicleSeated).isEmpty => + //suppress the warning message if in a vehicle + log.warn(s"DropItem: ${player.Name} wanted to drop a ${anItem.Definition.Name}, but it wasn't at hand") + case (Some(obj), _) => + log.warn(s"DropItem: ${player.Name} wanted to drop a ${obj.Definition.Name}, but it was not equipment") + case _ => () + } + } + + def handlePickupItem(pkt: PickupItemMessage): Unit = { + val PickupItemMessage(itemGuid, _, _, _) = pkt + sessionLogic.validObject(itemGuid, decorator = "PickupItem").collect { + case item: Equipment if player.Fit(item).nonEmpty => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + PickUpEquipmentFromGround(player)(item) + case _: Equipment => + sendResponse(ActionResultMessage.Fail(16)) //error code? + } + } + + def handleObjectHeld(pkt: ObjectHeldMessage): Unit = { + val ObjectHeldMessage(_, heldHolsters, _) = pkt + player.Actor ! PlayerControl.ObjectHeld(heldHolsters) + } + + def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { + val AvatarJumpMessage(_) = pkt + avatarActor ! AvatarActor.ConsumeStamina(10) + avatarActor ! AvatarActor.SuspendStaminaRegeneration(2.5 seconds) + } + + def handleZipLine(pkt: ZipLineMessage): Unit = { + val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt + continent.zipLinePaths.find(x => x.PathId == pathId) match { + case Some(path) if path.IsTeleporter => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") + val endPoint = path.ZipLinePoints.last + sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos)) + //todo: send to zone to show teleport animation to all clients + sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None))) + case Some(_) => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") + action match { + case 0 => + //travel along the zipline in the direction specified + sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos)) + case 1 => + //disembark from zipline at destination! + sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) + case 2 => + //get off by force + sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) + case _ => + log.warn( + s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}" + ) + } + case _ => + log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}") + } + } + + def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = { + val RequestDestroyMessage(objectGuid) = pkt + //make sure this is the correct response for all cases + sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match { + case Some(vehicle: Vehicle) => + /* line 1a: player is admin (and overrules other access requirements) */ + /* line 1b: vehicle and player (as the owner) acknowledge each other */ + /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */ + /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ + if ( + (session.account.gm || + (player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) || + (player.Faction == vehicle.Faction && + (vehicle.Definition.CanBeOwned.nonEmpty && + (vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) && + (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied)) + ) { + vehicle.Actor ! Vehicle.Deconstruct() + //log.info(s"RequestDestroy: vehicle $vehicle") + } else { + log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it") + } + + case Some(obj: Projectile) => + if (!obj.isResolved) { + obj.Miss() + } + continent.Projectile ! ZoneProjectile.Remove(objectGuid) + + case Some(obj: BoomerTrigger) => + if (findEquipmentToDelete(objectGuid, obj)) { + continent.GUID(obj.Companion) match { + case Some(boomer: BoomerDeployable) => + boomer.Trigger = None + boomer.Actor ! Deployable.Deconstruct() + case Some(thing) => + log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") + case None => () + } + } + + case Some(obj: Deployable) => + if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) { + obj.Actor ! Deployable.Deconstruct() + } else { + log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it") + } + + case Some(obj: Equipment) => + findEquipmentToDelete(objectGuid, obj) + + case Some(thing) => + log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") + + case None => () + } + } + + def handleMoveItem(pkt: MoveItemMessage): Unit = { + val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt + ( + continent.GUID(sourceGuid), + continent.GUID(destinationGuid), + sessionLogic.validObject(itemGuid, decorator = "MoveItem") + ) match { + case ( + Some(source: PlanetSideServerObject with Container), + Some(destination: PlanetSideServerObject with Container), + Some(item: Equipment) + ) => + ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest)) + case (None, _, _) => + log.error( + s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object" + ) + case (_, None, _) => + log.error( + s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object" + ) + case (_, _, None) => () + case _ => + log.error( + s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered" + ) + } + } + + def handleLootItem(pkt: LootItemMessage): Unit = { + val LootItemMessage(itemGuid, targetGuid) = pkt + (sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match { + case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => + //figure out the source + ( + { + val findFunc: PlanetSideServerObject with Container => Option[ + (PlanetSideServerObject with Container, Option[Int]) + ] = ops.findInLocalContainer(itemGuid) + findFunc(player.avatar.locker) + .orElse(findFunc(player)) + .orElse(ops.accessedContainer match { + case Some(parent: PlanetSideServerObject) => + findFunc(parent) + case _ => + None + }) + }, + destination.Fit(item) + ) match { + case (Some((source, Some(_))), Some(dest)) => + ContainableMoveItem(player.Name, source, destination, item, dest) + case (None, _) => + log.error(s"LootItem: ${player.Name} can not find where $item is put currently") + case (_, None) => + log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination") + case _ => + log.error( + s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered" + ) + } + case (Some(obj), _) => + log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}") + case (None, _) => () + case (_, None) => + log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid") + } + } + + def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = { + val AvatarImplantMessage(_, action, slot, status) = pkt + if (action == ImplantAction.Activation) { + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + //do not activate; play deactivation sound instead + sessionLogic.zoning.spawn.stopDeconstructing() + avatar.implants(slot).collect { + case implant if implant.active => + avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) + case implant => + sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2)) + } + } else { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") + avatar.implants(slot) match { + case Some(implant) => + if (status == 1) { + avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType) + } else { + avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) + } + case _ => + log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot") + } + } + } + } + + def handleUseItem(pkt: UseItemMessage): Unit = { + val equipment = ops.findContainedEquipment(pkt.item_used_guid) match { + case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) => + sessionLogic.shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption + case (Some(_), a) => + a.headOption + case _ => + None + } + sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { + case Some(door: Door) => + handleUseDoor(door, equipment) + case Some(resourceSilo: ResourceSilo) => + handleUseResourceSilo(resourceSilo, equipment) + case Some(panel: IFFLock) => + handleUseGeneralEntity(panel, equipment) + case Some(obj: Player) => + handleUsePlayer(obj, equipment, pkt) + case Some(locker: Locker) => + handleUseLocker(locker, equipment, pkt) + case Some(gen: Generator) => + handleUseGeneralEntity(gen, equipment) + case Some(mech: ImplantTerminalMech) => + handleUseGeneralEntity(mech, equipment) + case Some(captureTerminal: CaptureTerminal) => + handleUseCaptureTerminal(captureTerminal, equipment) + case Some(obj: FacilityTurret) => + handleUseFacilityTurret(obj, equipment, pkt) + case Some(obj: Vehicle) => + handleUseVehicle(obj, equipment, pkt) + case Some(terminal: Terminal) => + handleUseTerminal(terminal, equipment, pkt) + case Some(obj: SpawnTube) => + handleUseSpawnTube(obj, equipment) + case Some(obj: SensorDeployable) => + handleUseGeneralEntity(obj, equipment) + case Some(obj: TurretDeployable) => + handleUseGeneralEntity(obj, equipment) + case Some(obj: TrapDeployable) => + handleUseGeneralEntity(obj, equipment) + case Some(obj: ShieldGeneratorDeployable) => + handleUseGeneralEntity(obj, equipment) + case Some(obj: TelepadDeployable) => + handleUseTelepadDeployable(obj, equipment, pkt) + case Some(obj: Utility.InternalTelepad) => + handleUseInternalTelepad(obj, pkt) + case Some(obj: CaptureFlag) => + handleUseCaptureFlag(obj) + case Some(_: WarpGate) => + handleUseWarpGate(equipment) + case Some(obj) => + handleUseDefaultEntity(obj, equipment) + case None => () + } + } + + def handleUnuseItem(pkt: UnuseItemMessage): Unit = { + val UnuseItemMessage(_, objectGuid) = pkt + sessionLogic.validObject(objectGuid, decorator = "UnuseItem") match { + case Some(obj: Player) => + ops.unaccessContainer(obj) + sessionLogic.zoning.spawn.TryDisposeOfLootedCorpse(obj) + case Some(obj: Container) => + // Make sure we don't unload the contents of the vehicle the player is seated in + // An example scenario of this would be closing the trunk contents when rearming at a landing pad + if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) { + ops.unaccessContainer(obj) + } + case _ => () + } + } + + def handleDeployObject(pkt: DeployObjectMessage): Unit = { + val DeployObjectMessage(guid, _, pos, orient, _) = pkt + player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid).flatMap { slot => slot.Equipment } match { + case Some(obj: ConstructionItem) => + val ammoType = obj.AmmoType match { + case DeployedItem.portable_manned_turret => GlobalDefinitions.PortableMannedTurret(player.Faction).Item + case dtype => dtype + } + log.info(s"${player.Name} is constructing a $ammoType deployable") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + val dObj: Deployable = Deployables.Make(ammoType)() + dObj.Position = pos + dObj.Orientation = orient + dObj.WhichSide = player.WhichSide + dObj.Faction = player.Faction + dObj.AssignOwnership(player) + val tasking: TaskBundle = dObj match { + case turret: TurretDeployable => + GUIDTask.registerDeployableTurret(continent.GUID, turret) + case _ => + GUIDTask.registerObject(continent.GUID, dObj) + } + TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj))) + case Some(obj) => + log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") + case None => + log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!") + } + } + + def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = { + val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt + sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match { + case Some(vehicle: Vehicle) if player.avatar.vehicle.contains(vehicle.GUID) => + vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue) + case Some(vehicle: Vehicle) => + log.warn(s"PlanetsideAttribute: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it") + // Cosmetics options + case Some(_: Player) if attributeType == 106 => + avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue)) + case Some(obj) => + log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}") + case _ => () + } + } + + def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = { + val GenericObjectActionMessage(objectGuid, code) = pkt + sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match { + case Some(vehicle: Vehicle) + if vehicle.OwnerName.contains(player.Name) => + vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID)) + + case Some(tool: Tool) => + if (code == 35 && + (tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser")) + ) { + //maelstrom primary fire mode discharge (no target) + //aphelion_laser discharge (no target) + sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) + } else { + sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match { + case Some(vehicle: Vehicle) + if vehicle.OwnerName.contains(player.Name) => + vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool)) + case _ => + } + } + case _ => + log.info(s"${player.Name} - $pkt") + } + } + + def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = { + val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt + sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match { + case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => + sessionLogic.shooting.FindContainedWeapon match { + case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) => + vehicle.Actor ! SpecialEmp.Burst() + case _ => () + } + case _ => + log.info(s"${player.Name} - $pkt") + } + } + + def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = { + val GenericObjectStateMsg(_, _) = pkt + log.info(s"${player.Name} - $pkt") + } + + def handleGenericAction(pkt: GenericActionMessage): Unit = { + val GenericActionMessage(action) = pkt + if (player == null) { + if (action == GenericAction.AwayFromKeyboard_RCV) { + log.debug("GenericObjectState: AFK state reported during login") + } + } else { + val (toolOpt, definition) = player.Slot(0).Equipment match { + case Some(tool: Tool) => + (Some(tool), tool.Definition) + case _ => + (None, GlobalDefinitions.bullet_9mm) + } + action match { + case GenericAction.DropSpecialItem => + ops.dropSpecialSlotItem() + case GenericAction.MaxAnchorsExtend_RCV => + log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground") + player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlanetsideAttribute(player.GUID, 19, 1) + ) + definition match { + case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster => + val tool = toolOpt.get + tool.ToFireMode = 1 + sendResponse(ChangeFireModeMessage(tool.GUID, 1)) + case GlobalDefinitions.trhev_pounder => + val tool = toolOpt.get + val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 } + else { 4 } + tool.ToFireMode = convertFireModeIndex + sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) + case _ => + log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}") + } + case GenericAction.MaxAnchorsRelease_RCV => + log.info(s"${player.Name} has released the anchors") + player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlanetsideAttribute(player.GUID, 19, 0) + ) + definition match { + case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster => + val tool = toolOpt.get + tool.ToFireMode = 0 + sendResponse(ChangeFireModeMessage(tool.GUID, 0)) + case GlobalDefinitions.trhev_pounder => + val tool = toolOpt.get + val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 } + tool.ToFireMode = convertFireModeIndex + sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) + case _ => + log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}") + } + case GenericAction.MaxSpecialEffect_RCV => + if (player.ExoSuit == ExoSuitType.MAX) { + ops.toggleMaxSpecialState(enable = true) + } else { + log.warn(s"GenericActionMessage: ${player.Name} can't handle MAX special effect") + } + case GenericAction.StopMaxSpecialEffect_RCV => + if (player.ExoSuit == ExoSuitType.MAX) { + player.Faction match { + case PlanetSideEmpire.NC => + ops.toggleMaxSpecialState(enable = false) + case _ => + log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability") + } + } else { + log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect") + } + case GenericAction.AwayFromKeyboard_RCV => + log.info(s"${player.Name} is AFK") + AvatarActor.savePlayerLocation(player) + ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min + player.AwayFromKeyboard = true + case GenericAction.BackInGame_RCV => + log.info(s"${player.Name} is back") + player.AwayFromKeyboard = false + ops.renewCharSavedTimer( + Config.app.game.savedMsg.renewal.fixed, + Config.app.game.savedMsg.renewal.variable + ) + case GenericAction.LookingForSquad_RCV => //Looking For Squad ON + if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { + avatarActor ! AvatarActor.SetLookingForSquad(true) + } + case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF + if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { + avatarActor ! AvatarActor.SetLookingForSquad(false) + } + case _ => + log.warn(s"GenericActionMessage: ${player.Name} can't handle $action") + } + } + } + + def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { + val GenericCollisionMsg(ctype, p, _, ppos, pv, t, _, tpos, tv, _, _, _) = pkt + val fallHeight = { + if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { + if (ops.heightTrend) { + val fall = ops.heightLast - ops.heightHistory + ops.heightHistory = ops.heightLast + fall + } + else { + val fall = ops.heightHistory - ops.heightLast + ops.heightLast = ops.heightHistory + fall + } + } else { + 0f + } + } + val (target1, target2, bailProtectStatus, velocity) = (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { + case (CollisionIs.OfInfantry, out @ Some(user: Player)) + if user == player => + val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection + player.BailProtection = false + val v = if (player.avatar.implants.exists { + case Some(implant) => implant.definition.implantType == ImplantType.Surge && implant.active + case _ => false + }) { + Vector3.Zero + } else { + pv + } + (out, None, bailStatus, v) + case (CollisionIs.OfGroundVehicle, out @ Some(v: Vehicle)) + if v.Seats(0).occupant.contains(player) => + val bailStatus = v.BailProtection + v.BailProtection = false + (out, sessionLogic.validObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv) + case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) + if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => + (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) + case (CollisionIs.BetweenThings, _) => + log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") + (None, None, false, Vector3.Zero) + case _ => + (None, None, false, Vector3.Zero) + } + val curr = System.currentTimeMillis() + (target1, t, target2) match { + case (None, _, _) => () + + case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => + if (updateCollisionHistoryForTarget(us, curr)) { + if (!bailProtectStatus) { + sessionLogic.handleDealingDamage( + us, + DamageInteraction( + SourceEntry(us), + CollisionReason(velocity, fallHeight, us.DamageModel), + ppos + ) + ) + } + } + + case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) => + collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) + + case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty => + collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) + + case ( + Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, + Some(victim: PlanetSideServerObject with Vitality with FactionAffinity) + ) => + if (updateCollisionHistoryForTarget(victim, curr)) { + val usSource = SourceEntry(us) + val victimSource = SourceEntry(victim) + //we take damage from the collision + if (!bailProtectStatus) { + performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv) + } + //get dealt damage from our own collision (no protection) + ops.collisionHistory.put(us.Actor, curr) + performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity) + } + + case _ => () + } + } + + def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { + val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt + avatarActor ! AvatarActor.AddFirstTimeEvent(eventName) + } + + def handleBugReport(pkt: PlanetSideGamePacket): Unit = { + val BugReportMessage( + _/*versionMajor*/, + _/*versionMinor*/, + _/*versionDate*/, + _/*bugType*/, + _/*repeatable*/, + _/*location*/, + _/*zone*/, + _/*pos*/, + _/*summary*/, + _/*desc*/ + ) = pkt + log.warn(s"${player.Name} filed a bug report - it might be something important") + log.debug(s"$pkt") + } + + def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = { + val FacilityBenefitShieldChargeRequestMessage(_) = pkt + val vehicleGuid = player.VehicleSeated + continent + .GUID(vehicleGuid) + .foreach { + case obj: Vehicle if !obj.Destroyed && obj.MountedIn.isEmpty => // vehicle will try to charge even if destroyed & cargo vehicles need to be excluded + obj.Actor ! CommonMessages.ChargeShields( + 15, + Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius)) + ) + case obj: Vehicle if obj.MountedIn.nonEmpty => + false + case _ if vehicleGuid.nonEmpty => + log.warn( + s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find vehicle ${vehicleGuid.get.guid} in zone ${continent.id}" + ) + case _ => + log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in a vehicle") + } + } + + def handleBattleplan(pkt: BattleplanMessage): Unit = { + val BattleplanMessage(_, name, _, _) = pkt + val lament: String = s"$name has a brilliant idea that no one will ever see" + log.info(lament) + log.debug(s"Battleplan: $lament - $pkt") + } + + def handleBindPlayer(pkt: BindPlayerMessage): Unit = { + val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt + } + + def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { + val CreateShortcutMessage(_, slot, shortcutOpt) = pkt + shortcutOpt match { + case Some(shortcut) => + avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut) + case None => + avatarActor ! AvatarActor.RemoveShortcut(slot - 1) + } + } + + def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { + val ChangeShortcutBankMessage(_, _) = pkt + } + + def handleFriendRequest(pkt: FriendsRequest): Unit = { + val FriendsRequest(action, name) = pkt + avatarActor ! AvatarActor.MemberListRequest(action, name) + } + + def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = { + val InvalidTerrainMessage(_, vehicleGuid, alert, _) = pkt + (continent.GUID(vehicleGuid), continent.GUID(player.VehicleSeated)) match { + case (Some(packetVehicle: Vehicle), Some(playerVehicle: Vehicle)) if packetVehicle eq playerVehicle => + if (alert == TerrainCondition.Unsafe) { + log.info(s"${player.Name}'s ${packetVehicle.Definition.Name} is approaching terrain unsuitable for idling") + } + case (Some(packetVehicle: Vehicle), Some(_: Vehicle)) => + if (alert == TerrainCondition.Unsafe) { + log.info(s"${packetVehicle.Definition.Name}@${packetVehicle.GUID} is approaching terrain unsuitable for idling, but is not ${player.Name}'s vehicle") + } + case (Some(_: Vehicle), _) => + log.warn(s"InvalidTerrain: ${player.Name} is not seated in a(ny) vehicle near unsuitable terrain") + case (Some(packetThing), _) => + log.warn(s"InvalidTerrain: ${player.Name} thinks that ${packetThing.Definition.Name}@${packetThing.GUID} is near unsuitable terrain") + case _ => + log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicleGuid that can not be found") + } + } + + def handleActionCancel(pkt: ActionCancelMessage): Unit = { + val ActionCancelMessage(_, _, _) = pkt + ops.progressBarUpdate.cancel() + ops.progressBarValue = None + } + + def handleTrade(pkt: TradeMessage): Unit = { + val TradeMessage(trade) = pkt + log.trace(s"${player.Name} wants to trade for some reason - $trade") + } + + def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { + val DisplayedAwardMessage(_, ribbon, bar) = pkt + log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon") + avatarActor ! AvatarActor.SetRibbon(ribbon, bar) + } + + def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { + val ObjectDetectedMessage(_, _, _, targets) = pkt + sessionLogic.shooting.FindWeapon.foreach { + case weapon if weapon.Projectile.AutoLock => + //projectile with auto-lock instigates a warning on the target + val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets) + val mode = 7 + (if (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) 1 else 0) + detectedTargets.foreach { target => + continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode)) + } + case _ => () + } + } + + def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { + val TargetingImplantRequest(list) = pkt + val targetInfo: List[TargetInfo] = list.flatMap { x => + continent.GUID(x.target_guid) match { + case Some(player: Player) => + val health = player.Health.toFloat / player.MaxHealth + val armor = if (player.MaxArmor > 0) { + player.Armor.toFloat / player.MaxArmor + } else { + 0 + } + Some(TargetInfo(player.GUID, health, armor)) + case _ => + log.warn( + s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player" + ) + None + } + } + sendResponse(TargetingInfoMessage(targetInfo)) + } + + def handleHitHint(pkt: HitHint): Unit = { + val HitHint(_, _) = pkt + } + + /* messages */ + + def handleSetAvatar(avatar: Avatar): Unit = { + session = session.copy(avatar = avatar) + if (session.player != null) { + session.player.avatar = avatar + } + LivePlayerList.Update(avatar.id, avatar) + } + + def handleReceiveAccountData(account: Account): Unit = { + log.trace(s"ReceiveAccountData $account") + session = session.copy(account = account) + avatarActor ! AvatarActor.SetAccount(account) + } + + def handleUseCooldownRenew: BasicDefinition => Unit = { + case _: KitDefinition => ops.kitToBeUsed = None + case _ => () + } + + def handleAvatarResponse(avatar: Avatar): Unit = { + session = session.copy(avatar = avatar) + sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id) + } + + def handleSetSpeed(speed: Float): Unit = { + session = session.copy(speed = speed) + } + + def handleSetFlying(flying: Boolean): Unit = { + session = session.copy(flying = flying) + } + + def handleSetSpectator(spectator: Boolean): Unit = { + session.player.spectator = spectator + } + + def handleKick(player: Player, time: Option[Long]): Unit = { + administrativeKick(player) + sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time) + } + + def handleSilenced(isSilenced: Boolean): Unit = { + player.silenced = isSilenced + } + + /* supporting functions */ + + /** + * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. + * Intended to assist in sanitizing loadout information from the perspective of the player, or target owner. + * The equipment is expected to be unregistered and already fitted to their ultimate slot in the target container. + * @param player the player whose purchasing constraints are to be tested + * @param target the location in which the equipment will be stowed + * @param slots the equipment, in the standard object-slot format container + */ + def applyPurchaseTimersBeforePackingLoadout( + player: Player, + target: PlanetSideServerObject with Container, + slots: List[InventoryItem] + ): Unit = { + slots.foreach { item => + player.avatar.purchaseCooldown(item.obj.Definition) match { + case Some(_) => () + case None if Avatar.purchaseCooldowns.contains(item.obj.Definition) => + avatarActor ! AvatarActor.UpdatePurchaseTime(item.obj.Definition) + TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start)) + case None => + TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start)) + } + } + } + + private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + val distance: Float = math.max( + Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, + door.Definition.initialOpeningDistance + ) + door.Actor ! CommonMessages.Use(player, Some(distance)) + case _ => + door.Actor ! CommonMessages.Use(player) + } + } + + private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + (continent.GUID(player.VehicleSeated), equipment) match { + case (Some(vehicle: Vehicle), Some(item)) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => + resourceSilo.Actor ! CommonMessages.Use(player, equipment) + case _ => + resourceSilo.Actor ! CommonMessages.Use(player) + } + } + + private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + if (obj.isBackpack) { + if (equipment.isEmpty) { + log.info(s"${player.Name} is looting the corpse of ${obj.Name}") + sendResponse(msg) + ops.accessContainer(obj) + } + } else if (!msg.unk3 && player.isAlive) { //potential kit use + (continent.GUID(msg.item_used_guid), ops.kitToBeUsed) match { + case (Some(kit: Kit), None) => + ops.kitToBeUsed = Some(msg.item_used_guid) + player.Actor ! CommonMessages.Use(player, Some(kit)) + case (Some(_: Kit), Some(_)) | (None, Some(_)) => + //a kit is already queued to be used; ignore this request + sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None)) + case (Some(item), _) => + log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") + case (None, None) => + log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") } + } else if (msg.object_id == ObjectClass.avatar && msg.unk3) { + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => + obj.Actor ! CommonMessages.Use(player, equipment) + + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + obj.Actor ! CommonMessages.Use(player, equipment) + case _ => () + } + } + } + + private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(locker, item) + case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => + log.info(s"${player.Name} is accessing a locker") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + val playerLocker = player.avatar.locker + sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) + ops.accessContainer(playerLocker) + case _ => () + } + } + + private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(captureTerminal, item) + case _ if ops.specialItemSlotGuid.nonEmpty => + continent.GUID(ops.specialItemSlotGuid) match { + case Some(llu: CaptureFlag) => + if (llu.Target.GUID == captureTerminal.Owner.GUID) { + continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) + } else { + log.info( + s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}" + ) + } + case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") + } + case _ => () + } + } + + private def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment.foreach { item => + sendUseGeneralEntityMessage(obj, item) + obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path + } + } + + private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(obj, item) + case None if player.Faction == obj.Faction => + //access to trunk + if ( + obj.AccessingTrunk.isEmpty && + (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid + .contains(player.GUID)) + ) { + log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + obj.AccessingTrunk = player.GUID + ops.accessContainer(obj) + sendResponse(msg) + } + case _ => () + } + } + + private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(terminal, item) + case None + if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || + terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => + val tdef = terminal.Definition + if (tdef.isInstanceOf[MatrixTerminalDefinition]) { + //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sendResponse( + BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) + ) + } else if ( + tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || + tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal + ) { + findLocalVehicle match { + case Some(vehicle) => + log.info( + s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}" + ) + sendResponse(msg) + sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId)) + case None => + log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") + } + } else if (tdef == GlobalDefinitions.teleportpad_terminal) { + //explicit request + log.info(s"${player.Name} is purchasing a router telepad") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + terminal.Actor ! Terminal.Request( + player, + ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) + ) + } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { + //explicit request + log.info(s"${player.Name} is purchasing a targeting laser") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + terminal.Actor ! Terminal.Request( + player, + ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0)) + ) + } else { + log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sendResponse(msg) + } + case _ => () + } + } + + private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(obj, item) + case None if player.Faction == obj.Faction => + //deconstruction + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.actionsToCancel() + sessionLogic.terminals.CancelAllProximityUnits() + sessionLogic.zoning.spawn.startDeconstructing(obj) + case _ => () + } + } + + private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + if (equipment.isEmpty) { + (continent.GUID(obj.Router) match { + case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) + case Some(vehicle) => Some(vehicle, None) + case None => None + }) match { + case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") + player.WhichSide = vehicle.WhichSide + useRouterTelepadSystem( + router = vehicle, + internalTelepad = util, + remoteTelepad = obj, + src = obj, + dest = util + ) + case Some((vehicle: Vehicle, None)) => + log.error( + s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" + ) + case Some((o, _)) => + log.error( + s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" + ) + obj.Actor ! Deployable.Deconstruct() + case _ => () + } + } + } + + private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = { + continent.GUID(obj.Telepad) match { + case Some(pad: TelepadDeployable) => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") + player.WhichSide = pad.WhichSide + useRouterTelepadSystem( + router = obj.Owner.asInstanceOf[Vehicle], + internalTelepad = obj, + remoteTelepad = pad, + src = obj, + dest = pad + ) + case Some(o) => + log.error( + s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" + ) + case None => () + } + } + + private def handleUseCaptureFlag(obj: CaptureFlag): Unit = { + // LLU can normally only be picked up the faction that owns it + ops.specialItemSlotGuid match { + case None if obj.Faction == player.Faction => + ops.specialItemSlotGuid = Some(obj.GUID) + player.Carrying = SpecialCarry.CaptureFlag + continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) + case None => + log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}") + case Some(guid) if guid != obj.GUID => + // Ignore duplicate pickup requests + log.warn( + s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid" + ) + case _ => () + } + } + + private def handleUseWarpGate(equipment: Option[Equipment]): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + (continent.GUID(player.VehicleSeated), equipment) match { + case (Some(vehicle: Vehicle), Some(item)) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => + vehicle.Actor ! CommonMessages.Use(player, equipment) + case _ => () + } + } + + private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { + equipment.foreach { item => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + obj.Actor ! CommonMessages.Use(player, Some(item)) + } + } + + private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + obj.Actor ! CommonMessages.Use(player, Some(equipment)) + } + + private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + equipment match { + case Some(item) + if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => () + case _ => + log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") + } + } + + /** + * Get the current `Vehicle` object that the player is riding/driving. + * The vehicle must be found solely through use of `player.VehicleSeated`. + * @return the vehicle + */ + private def findLocalVehicle: Option[Vehicle] = { + continent.GUID(player.VehicleSeated) match { + case Some(obj: Vehicle) => Some(obj) + case _ => None + } + } + + /** + * A simple object searching algorithm that is limited to containers currently known and accessible by the player. + * If all relatively local containers are checked and the object is not found, + * the player's locker inventory will be checked, and then + * the game environment (items on the ground) will be checked too. + * If the target object is discovered, it is removed from its current location and is completely destroyed. + * @see `RequestDestroyMessage` + * @see `Zone.ItemIs.Where` + * @param objectGuid the target object's globally unique identifier; + * it is not expected that the object will be unregistered, but it is also not gauranteed + * @param obj the target object + * @return `true`, if the target object was discovered and removed; + * `false`, otherwise + */ + private def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = { + val findFunc + : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = + ops.findInLocalContainer(objectGuid) + + findFunc(player) + .orElse(ops.accessedContainer match { + case Some(parent: PlanetSideServerObject) => + findFunc(parent) + case _ => + None + }) + .orElse(findLocalVehicle match { + case Some(parent: PlanetSideServerObject) => + findFunc(parent) + case _ => + None + }) match { + case Some((parent, Some(_))) => + obj.Position = Vector3.Zero + RemoveOldEquipmentFromInventory(parent)(obj) + true + case _ if player.avatar.locker.Inventory.Remove(objectGuid) => + sendResponse(ObjectDeleteMessage(objectGuid, 0)) + true + case _ if continent.EquipmentOnGround.contains(obj) => + obj.Position = Vector3.Zero + continent.Ground ! Zone.Ground.RemoveItem(objectGuid) + continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) + true + case _ => + Zone.EquipmentIs.Where(obj, objectGuid, continent) match { + case None => + true + case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID => + TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) + true + case Some(Zone.EquipmentIs.Orphaned()) => + true + case _ => + log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") + false + } + } + } + + /** + * A player uses a fully-linked Router teleportation system. + * @param router the Router vehicle + * @param internalTelepad the internal telepad within the Router vehicle + * @param remoteTelepad the remote telepad that is currently associated with this Router + * @param src the origin of the teleportation (where the player starts) + * @param dest the destination of the teleportation (where the player is going) + */ + private def useRouterTelepadSystem( + router: Vehicle, + internalTelepad: InternalTelepad, + remoteTelepad: TelepadDeployable, + src: PlanetSideGameObject with TelepadLike, + dest: PlanetSideGameObject with TelepadLike + ): Unit = { + val time = System.currentTimeMillis() + if ( + time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && + internalTelepad.Active && + remoteTelepad.Active + ) { + val pguid = player.GUID + val sguid = src.GUID + val dguid = dest.GUID + sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) + ops.useRouterTelepadEffect(pguid, sguid, dguid) + continent.LocalEvents ! LocalServiceMessage( + continent.id, + LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid) + ) + val vSource = VehicleSource(router) + val zoneNumber = continent.Number + player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber)) + player.Position = dest.Position + player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber)) + } else { + log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") + } + ops.recentTeleportAttempt = time + } + + private def maxCapacitorTick(jumpThrust: Boolean): Unit = { + if (player.ExoSuit == ExoSuitType.MAX) { + val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 + player.CapacitorState match { + case CapacitorStateType.Idle => maxCapacitorTickIdle(activate) + case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate) + case CapacitorStateType.ChargeDelay => maxCapacitorTickChargeDelay(activate) + case CapacitorStateType.Charging => maxCapacitorTickCharging(activate) + } + } else if (player.CapacitorState != CapacitorStateType.Idle) { + player.CapacitorState = CapacitorStateType.Idle + } + } + + private def maxCapacitorTickIdle(activate: Boolean): Unit = { + if (activate) { + player.CapacitorState = CapacitorStateType.Discharging + //maxCapacitorTickDischarging(activate) + } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + player.CapacitorState = CapacitorStateType.ChargeDelay + maxCapacitorTickChargeDelay(activate) + } + } + + private def maxCapacitorTickDischarging(activate: Boolean): Unit = { + if (activate) { + val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000 + val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff + player.Capacitor -= drainAmount + sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt)) + } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + if (player.Faction != PlanetSideEmpire.VS) { + ops.toggleMaxSpecialState(enable = false) + } + player.CapacitorState = CapacitorStateType.ChargeDelay + maxCapacitorTickChargeDelay(activate) + } else { + player.CapacitorState = CapacitorStateType.Idle + } + } + + private def maxCapacitorTickChargeDelay(activate: Boolean): Unit = { + if (activate) { + player.CapacitorState = CapacitorStateType.Discharging + //maxCapacitorTickDischarging(activate) + } else if (player.Capacitor == player.ExoSuitDef.MaxCapacitor) { + player.CapacitorState = CapacitorStateType.Idle + } else if (System.currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis) { + player.CapacitorState = CapacitorStateType.Charging + //maxCapacitorTickCharging(activate) + } + } + + private def maxCapacitorTickCharging(activate: Boolean): Unit = { + if (activate) { + player.CapacitorState = CapacitorStateType.Discharging + //maxCapacitorTickDischarging(activate) + } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + val timeDiff = (System.currentTimeMillis() - player.CapacitorLastChargedMillis).toFloat / 1000 + val chargeAmount = player.ExoSuitDef.CapacitorRechargePerSecond * timeDiff + player.Capacitor += chargeAmount + sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt)) + } else { + player.CapacitorState = CapacitorStateType.Idle + } + } + + def administrativeKick(tplayer: Player): Unit = { + log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") + tplayer.death_by = -1 + sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) + //get out of that vehicle + sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match { + case (Some(obj), Some(seatNum)) => + tplayer.VehicleSeated = None + obj.Seats(seatNum).unmount(tplayer) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.KickPassenger(tplayer.GUID, seatNum, unk2=false, obj.GUID) + ) + case _ => () + } + } + + private def updateCollisionHistoryForTarget( + target: PlanetSideServerObject with Vitality with FactionAffinity, + curr: Long + ): Boolean = { + ops.collisionHistory.get(target.Actor) match { + case Some(lastCollision) if curr - lastCollision <= 1000L => + false + case _ => + ops.collisionHistory.put(target.Actor, curr) + true + } + } + + private def collisionBetweenVehicleAndFragileDeployable( + vehicle: Vehicle, + vehiclePosition: Vector3, + smallDeployable: Deployable, + smallDeployablePosition: Vector3, + velocity: Vector3, + fallHeight: Float, + collisionTime: Long + ): Unit = { + if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) { + val smallDeployableSource = SourceEntry(smallDeployable) + //vehicle takes damage from the collision (ignore bail protection in this case) + performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity) + //deployable gets absolutely destroyed + ops.collisionHistory.put(vehicle.Actor, collisionTime) + sessionLogic.handleDealingDamage( + smallDeployable, + DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition) + ) + } + } + + private def performCollisionWithSomethingDamage( + target: PlanetSideServerObject with Vitality with FactionAffinity, + targetSource: SourceEntry, + targetPosition: Vector3, + victimSource: SourceEntry, + fallHeight: Float, + velocity: Vector3 + ): Unit = { + sessionLogic.handleDealingDamage( + target, + DamageInteraction( + targetSource, + CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource), + targetPosition + ) + ) + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala new file mode 100644 index 000000000..04c845b43 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala @@ -0,0 +1,244 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.ActorContext +import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers} +import net.psforever.objects.ce.Deployable +import net.psforever.objects.vehicles.MountableWeapons +import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable} +import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} +import net.psforever.services.Service +import net.psforever.services.local.LocalResponse +import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3} + +class LocalHandlerLogic(val ops: SessionLocalHandlers) extends LocalHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + /* response handlers */ + + /** + * na + * @param toChannel na + * @param guid na + * @param reply na + */ + def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = { + val resolvedPlayerGuid = if (player.HasGUID) { + player.GUID + } else { + Service.defaultPlayerGUID + } + val isNotSameTarget = resolvedPlayerGuid != guid + reply match { + case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget => + sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) + + case LocalResponse.DeployableUIFor(item) => + sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) + + case LocalResponse.Detonate(dguid, _: BoomerDeployable) => + sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) + sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) => + sendResponse(GenericObjectActionMessage(dguid, code=19)) + sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.Detonate(_, obj) => + log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly") + + case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget => + sendResponse(GenericObjectStateMsg(doorGuid, state=16)) + + case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone + sendResponse(GenericObjectStateMsg(doorGuid, state=17)) + + case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed => + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => + obj.Destroyed = true + DeconstructDeployable( + obj, + dguid, + pos, + obj.Orientation, + deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 } + ) + + case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _) + if obj.Destroyed || obj.Jammed || obj.Health == 0 => + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => + obj.Destroyed = true + DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) + + case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed => + //if active, deactivate + obj.Active = false + sendResponse(GenericObjectActionMessage(dguid, code=29)) + sendResponse(GenericObjectActionMessage(dguid, code=30)) + //standard deployable elimination behavior + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active => + //if active, deactivate + obj.Active = false + sendResponse(GenericObjectActionMessage(dguid, code=29)) + sendResponse(GenericObjectActionMessage(dguid, code=30)) + //standard deployable elimination behavior + obj.Destroyed = true + DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) + + case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed => + //standard deployable elimination behavior + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => + //standard deployable elimination behavior + obj.Destroyed = true + DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) + + case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed => + sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + + case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => + obj.Destroyed = true + DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) + + case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) => + sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2)) + + case LocalResponse.HackObject(targetGuid, unk1, unk2) => + sessionLogic.general.hackObject(targetGuid, unk1, unk2) + + case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) => + sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue) + + case LocalResponse.GenericObjectAction(targetGuid, actionNumber) => + sendResponse(GenericObjectActionMessage(targetGuid, actionNumber)) + + case LocalResponse.GenericActionMessage(actionNumber) => + sendResponse(GenericActionMessage(actionNumber)) + + case LocalResponse.ChatMessage(msg) => + sendResponse(msg) + + case LocalResponse.SendPacket(packet) => + sendResponse(packet) + + case LocalResponse.LluSpawned(llu) => + // Create LLU on client + sendResponse(ObjectCreateMessage( + llu.Definition.ObjectId, + llu.GUID, + llu.Definition.Packet.ConstructorData(llu).get + )) + sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f)) + + case LocalResponse.LluDespawned(lluGuid, position) => + sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f)) + sendResponse(ObjectDeleteMessage(lluGuid, unk1=0)) + // If the player was holding the LLU, remove it from their tracked special item slot + sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid => + sessionLogic.general.specialItemSlotGuid = None + player.Carrying = None + } + + case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget => + sendResponse(ObjectDeleteMessage(objectGuid, unk)) + + case LocalResponse.ProximityTerminalEffect(object_guid, true) => + sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true)) + + case LocalResponse.ProximityTerminalEffect(objectGuid, false) => + sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false)) + sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid) + + case LocalResponse.RouterTelepadMessage(msg) => + sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None)) + + case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) => + sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) + + case LocalResponse.SendResponse(msg) => + sendResponse(msg) + + case LocalResponse.SetEmpire(objectGuid, empire) => + sendResponse(SetEmpireMessage(objectGuid, empire)) + + case LocalResponse.ShuttleEvent(ev) => + val msg = OrbitalShuttleTimeMsg( + ev.u1, + ev.u2, + ev.t1, + ev.t2, + ev.t3, + pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) } + ) + sendResponse(msg) + + case LocalResponse.ShuttleDock(pguid, sguid, slot) => + sendResponse(ObjectAttachMessage(pguid, sguid, slot)) + + case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) => + sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient)) + + case LocalResponse.ShuttleState(sguid, pos, orient, state) => + sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false)) + + case LocalResponse.ToggleTeleportSystem(router, systemPlan) => + sessionLogic.general.toggleTeleportSystem(router, systemPlan) + + case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) => + sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation)) + + case LocalResponse.TriggerSound(sound, pos, unk, volume) => + sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) + + case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) => + sendResponse(GenericObjectActionMessage(buildingGuid, 11)) + + case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) => + sendResponse(GenericObjectActionMessage(buildingGuid, 12)) + + case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid => + continent.GUID(vehicleGuid) + .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) } + .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) } + .getOrElse(Set.empty) + .collect { case weapon: Tool if weapon.GUID == weaponGuid => + sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) + } + + case _ => () + } + } + + /* support functions */ + + /** + * Common behavior for deconstructing deployables in the game environment. + * @param obj the deployable + * @param guid the globally unique identifier for the deployable + * @param pos the previous position of the deployable + * @param orient the previous orientation of the deployable + * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation + */ + def DeconstructDeployable( + obj: Deployable, + guid: PlanetSideGUID, + pos: Vector3, + orient: Vector3, + deletionType: Int + ): Unit = { + sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) + sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish + sendResponse(ObjectDeleteMessage(guid, deletionType)) + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala new file mode 100644 index 000000000..6f8bf8d97 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -0,0 +1,516 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers} +import net.psforever.actors.zone.ZoneActor +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles} +import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions +import net.psforever.objects.serverobject.hackable.GenericHackables +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech +import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} +import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} +import net.psforever.objects.vital.InGameHistory +import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} +import net.psforever.services.Service +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} + +import scala.concurrent.duration._ + +class MountHandlerLogic(val ops: SessionMountHandlers) extends MountHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + 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 + */ + 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 + */ + 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 + */ + def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { + val playerGuid: PlanetSideGUID = tplayer.GUID + tplayer.ContributionFrom(obj) + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + val bailType = if (tplayer.BailProtection) { + BailType.Bailed + } else { + BailType.Normal + } + sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false) + ) + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/NormalUser.scala b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala similarity index 76% rename from src/main/scala/net/psforever/actors/session/support/NormalUser.scala rename to src/main/scala/net/psforever/actors/session/normal/NormalMode.scala index fdbb89f83..1fdf22e9b 100644 --- a/src/main/scala/net/psforever/actors/session/support/NormalUser.scala +++ b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala @@ -1,10 +1,11 @@ // Copyright (c) 2024 PSForever -package net.psforever.actors.session.support +package net.psforever.actors.session.normal import akka.actor.Actor.Receive -import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.net.MiddlewareActor +import akka.actor.ActorRef +// import net.psforever.actors.session.{AvatarActor, SessionActor} +import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData, ZoningOperations} import net.psforever.objects.TurretDeployable import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.serverobject.CommonMessages @@ -27,10 +28,18 @@ import net.psforever.services.teamwork.SquadServiceResponse import net.psforever.services.vehicle.VehicleServiceResponse import net.psforever.util.Config -class NormalUser( - val middlewareActor: typed.ActorRef[MiddlewareActor.Command], - implicit val context: ActorContext - ) extends SessionLogic { +class NormalModeLogic(data: SessionData) extends ModeLogic { + val avatarResponse = new AvatarHandlerLogic(data.avatarResponse) + val galaxy = new GalaxyHandlerLogic(data.galaxyResponseHandlers) + val general = new GeneralLogic(data.general) + val local = new LocalHandlerLogic(data.localResponse) + val mountResponse = new MountHandlerLogic(data.mountResponse) + val shooting = new WeaponAndProjectileLogic(data.shooting) + val squad = new SquadHandlerLogic(data.squad) + val terminals = new TerminalHandlerLogic(data.terminals) + val vehicles = new VehicleLogic(data.vehicles) + val vehicleResponse = new VehicleHandlerLogic(data.vehicleResponseOperations) + def parse(sender: ActorRef): Receive = { /* really common messages (very frequently, every life) */ case packet: PlanetSideGamePacket => @@ -40,10 +49,10 @@ class NormalUser( avatarResponse.handle(toChannel, guid, reply) case GalaxyServiceResponse(_, reply) => - galaxyResponseHandlers.handle(reply) + galaxy.handle(reply) case LocalServiceResponse(toChannel, guid, reply) => - localResponse.handle(toChannel, guid, reply) + local.handle(toChannel, guid, reply) case Mountable.MountMessages(tplayer, reply) => mountResponse.handle(tplayer, reply) @@ -55,81 +64,81 @@ class NormalUser( terminals.handle(tplayer, msg, order) case VehicleServiceResponse(toChannel, guid, reply) => - vehicleResponseOperations.handle(toChannel, guid, reply) + vehicleResponse.handle(toChannel, guid, reply) case SessionActor.PokeClient() => - sendResponse(KeepAliveMessage()) + data.sendResponse(KeepAliveMessage()) case SessionActor.SendResponse(packet) => - sendResponse(packet) + data.sendResponse(packet) case SessionActor.CharSaved => - general.renewCharSavedTimer( + general.ops.renewCharSavedTimer( Config.app.game.savedMsg.interruptedByAction.fixed, Config.app.game.savedMsg.interruptedByAction.variable ) case SessionActor.CharSavedMsg => - general.displayCharSavedMsgThenRenewTimer( + general.ops.displayCharSavedMsgThenRenewTimer( Config.app.game.savedMsg.renewal.fixed, Config.app.game.savedMsg.renewal.variable ) /* common messages (maybe once every respawn) */ case ICS.SpawnPointResponse(response) => - zoning.handleSpawnPointResponse(response) + data.zoning.handleSpawnPointResponse(response) case SessionActor.NewPlayerLoaded(tplayer) => - zoning.spawn.handleNewPlayerLoaded(tplayer) + data.zoning.spawn.handleNewPlayerLoaded(tplayer) case SessionActor.PlayerLoaded(tplayer) => - zoning.spawn.handlePlayerLoaded(tplayer) + data.zoning.spawn.handlePlayerLoaded(tplayer) case Zone.Population.PlayerHasLeft(zone, None) => - log.debug(s"PlayerHasLeft: ${player.Name} does not have a body on ${zone.id}") + data.log.debug(s"PlayerHasLeft: ${data.player.Name} does not have a body on ${zone.id}") case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) => if (tplayer.isAlive) { - log.info(s"${tplayer.Name} has left zone ${zone.id}") + data.log.info(s"${tplayer.Name} has left zone ${zone.id}") } case Zone.Population.PlayerCanNotSpawn(zone, tplayer) => - log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?") + data.log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?") case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => - log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?") + data.log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?") case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) => - log.warn( - s"${player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason" + data.log.warn( + s"${data.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason" ) case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) => - log.warn( - s"${player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason" + data.log.warn( + s"${data.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason" ) case ICS.ZoneResponse(Some(zone)) => - zoning.handleZoneResponse(zone) + data.zoning.handleZoneResponse(zone) /* uncommon messages (once a session) */ case ICS.ZonesResponse(zones) => - zoning.handleZonesResponse(zones) + data.zoning.handleZonesResponse(zones) case SessionActor.SetAvatar(avatar) => general.handleSetAvatar(avatar) case PlayerToken.LoginInfo(name, Zone.Nowhere, _) => - zoning.spawn.handleLoginInfoNowhere(name, sender) + data.zoning.spawn.handleLoginInfoNowhere(name, sender) case PlayerToken.LoginInfo(name, inZone, optionalSavedData) => - zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender) + data.zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender) case PlayerToken.RestoreInfo(playerName, inZone, pos) => - zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender) + data.zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender) case PlayerToken.CanNotLogin(playerName, reason) => - zoning.spawn.handleLoginCanNot(playerName, reason) + data.zoning.spawn.handleLoginCanNot(playerName, reason) case ReceiveAccountData(account) => general.handleReceiveAccountData(account) @@ -138,35 +147,35 @@ class NormalUser( general.handleAvatarResponse(avatar) case AvatarActor.AvatarLoginResponse(avatar) => - zoning.spawn.avatarLoginResponse(avatar) + data.zoning.spawn.avatarLoginResponse(avatar) case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) => - zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt) + data.zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt) case SessionActor.SetConnectionState(state) => - connectionState = state + data.connectionState = state case SessionActor.AvatarLoadingSync(state) => - zoning.spawn.handleAvatarLoadingSync(state) + data.zoning.spawn.handleAvatarLoadingSync(state) /* uncommon messages (utility, or once in a while) */ case ZoningOperations.AvatarAwardMessageBundle(pkts, delay) => - zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay) + data.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay) case CommonMessages.ProgressEvent(delta, finishedAction, stepAction, tick) => - general.handleProgressChange(delta, finishedAction, stepAction, tick) + general.ops.handleProgressChange(delta, finishedAction, stepAction, tick) case CommonMessages.Progress(rate, finishedAction, stepAction) => - general.setupProgressChange(rate, finishedAction, stepAction) + general.ops.setupProgressChange(rate, finishedAction, stepAction) case CavernRotationService.CavernRotationServiceKey.Listing(listings) => - listings.head ! SendCavernRotationUpdates(context.self) + listings.head ! SendCavernRotationUpdates(data.context.self) case LookupResult("propertyOverrideManager", endpoint) => - zoning.propertyOverrideManagerLoadOverrides(endpoint) + data.zoning.propertyOverrideManagerLoadOverrides(endpoint) case SessionActor.UpdateIgnoredPlayers(msg) => - galaxyResponseHandlers.handleUpdateIgnoredPlayers(msg) + galaxy.handleUpdateIgnoredPlayers(msg) case SessionActor.UseCooldownRenewed(definition, _) => general.handleUseCooldownRenew(definition) @@ -182,28 +191,28 @@ class NormalUser( /* rare messages */ case ProximityUnit.StopAction(term, _) => - terminals.LocalStopUsingProximityUnit(term) + terminals.ops.LocalStopUsingProximityUnit(term) case SessionActor.Suicide() => - general.suicide(player) + general.ops.suicide(data.player) case SessionActor.Recall() => - zoning.handleRecall() + data.zoning.handleRecall() case SessionActor.InstantAction() => - zoning.handleInstantAction() + data.zoning.handleInstantAction() case SessionActor.Quit() => - zoning.handleQuit() + data.zoning.handleQuit() case ICS.DroppodLaunchDenial(errorCode, _) => - zoning.handleDroppodLaunchDenial(errorCode) + data.zoning.handleDroppodLaunchDenial(errorCode) case ICS.DroppodLaunchConfirmation(zone, position) => - zoning.LoadZoneLaunchDroppod(zone, position) + data.zoning.LoadZoneLaunchDroppod(zone, position) case SessionActor.PlayerFailedToLoad(tplayer) => - failWithError(s"${tplayer.Name} failed to load anywhere") + data.failWithError(s"${tplayer.Name} failed to load anywhere") /* csr only */ case SessionActor.SetSpeed(speed) => @@ -219,10 +228,10 @@ class NormalUser( general.handleKick(player, time) case SessionActor.SetZone(zoneId, position) => - zoning.handleSetZone(zoneId, position) + data.zoning.handleSetZone(zoneId, position) case SessionActor.SetPosition(position) => - zoning.spawn.handleSetPosition(position) + data.zoning.spawn.handleSetPosition(position) case SessionActor.SetSilenced(silenced) => general.handleSilenced(silenced) @@ -235,19 +244,19 @@ class NormalUser( case _: Zone.Vehicle.HasDespawned => ; case Zone.Deployable.IsDismissed(obj: TurretDeployable) => //only if target deployable was never fully introduced - TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(continent.GUID, obj)) + TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(data.continent.GUID, obj)) case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj)) + TaskWorkflow.execute(GUIDTask.unregisterObject(data.continent.GUID, obj)) case msg: Containable.ItemPutInSlot => - log.debug(s"ItemPutInSlot: $msg") + data.log.debug(s"ItemPutInSlot: $msg") case msg: Containable.CanNotPutItemInSlot => - log.debug(s"CanNotPutItemInSlot: $msg") + data.log.debug(s"CanNotPutItemInSlot: $msg") case default => - log.warn(s"Invalid packet class received: $default from $sender") + data.log.warn(s"Invalid packet class received: $default from $sender") } private def handleGamePkt: PlanetSideGamePacket => Unit = { @@ -255,10 +264,10 @@ class NormalUser( general.handleConnectToWorldRequest(packet) case packet: MountVehicleCargoMsg => - vehicles.handleMountVehicleCargo(packet) + mountResponse.handleMountVehicleCargo(packet) case packet: DismountVehicleCargoMsg => - vehicles.handleDismountVehicleCargo(packet) + mountResponse.handleDismountVehicleCargo(packet) case packet: CharacterCreateRequestMessage => general.handleCharacterCreateRequest(packet) @@ -267,10 +276,10 @@ class NormalUser( general.handleCharacterRequest(packet) case _: KeepAliveMessage => - keepAliveFunc() + data.keepAliveFunc() case packet: BeginZoningMessage => - zoning.handleBeginZoning(packet) + data.zoning.handleBeginZoning(packet) case packet: PlayerStateMessageUpstream => general.handlePlayerStateUpstream(packet) @@ -294,10 +303,10 @@ class NormalUser( shooting.handleLongRangeProjectileState(packet) case packet: ReleaseAvatarRequestMessage => - zoning.spawn.handleReleaseAvatarRequest(packet) + data.zoning.spawn.handleReleaseAvatarRequest(packet) case packet: SpawnRequestMessage => - zoning.spawn.handleSpawnRequest(packet) + data.zoning.spawn.handleSpawnRequest(packet) case packet: ChatMsg => general.handleChat(packet) @@ -384,7 +393,7 @@ class NormalUser( terminals.handleItemTransaction(packet) case packet: FavoritesRequest => - general.handleFavoritesRequest(packet) + terminals.handleFavoritesRequest(packet) case packet: WeaponDelayFireMessage => shooting.handleWeaponDelayFire(packet) @@ -414,13 +423,13 @@ class NormalUser( general.handleAvatarFirstTimeEvent(packet) case packet: WarpgateRequest => - zoning.handleWarpgateRequest(packet) + data.zoning.handleWarpgateRequest(packet) case packet: MountVehicleMsg => - vehicles.handleMountVehicle(packet) + mountResponse.handleMountVehicle(packet) case packet: DismountVehicleMsg => - vehicles.handleDismountVehicle(packet) + mountResponse.handleDismountVehicle(packet) case packet: DeployRequestMessage => vehicles.handleDeployRequest(packet) @@ -465,7 +474,7 @@ class NormalUser( general.handleFriendRequest(packet) case packet: DroppodLaunchRequestMessage => - zoning.handleDroppodLaunchRequest(packet) + data.zoning.handleDroppodLaunchRequest(packet) case packet: InvalidTerrainMessage => general.handleInvalidTerrain(packet) @@ -491,6 +500,12 @@ class NormalUser( case _: OutfitRequest => () case pkt => - log.warn(s"Unhandled GamePacket $pkt") + data.log.warn(s"Unhandled GamePacket $pkt") + } +} + +case object NormalMode extends PlayerMode { + def setup(data: SessionData): ModeLogic = { + new NormalModeLogic(data) } } diff --git a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala new file mode 100644 index 000000000..35363ac8e --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -0,0 +1,356 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement +import net.psforever.actors.session.{AvatarActor, ChatActor} +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.ChatService +import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype} + +class SquadHandlerLogic(val ops: SessionSquadHandlers) extends SquadHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor + + 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 + chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(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 + chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID)) + case _ => + //remove each member's entry + ops.GiveSquadColorsToMembers( + positionsToUpdate.map { + case (member, index) => + sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index)) + ops.squadUI.remove(member) + member + }, + value = 0 + ) + } + + case SquadResponse.AssignMember(squad, from_index, to_index) => + //we've already swapped position internally; now we swap the cards + ops.SwapSquadUIElements(squad, from_index, to_index) + + case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) => + if (promotedPlayer != player.CharId) { + //demoted from leader; no longer lfsm + if (player.avatar.lookingForSquad) { + avatarActor ! AvatarActor.SetLookingForSquad(false) + } + } + sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0)) + //the players have already been swapped in the backend object + ops.PromoteSquadUIElements(squad, from_index) + + case SquadResponse.UpdateMembers(_, positions) => + val pairedEntries = positions.collect { + case entry if ops.squadUI.contains(entry.char_id) => + (entry, ops.squadUI(entry.char_id)) + } + //prune entries + val updatedEntries = pairedEntries + .collect({ + case (entry, element) if entry.zone_number != element.zone => + //zone gets updated for these entries + sendResponse( + SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) + ) + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + case (entry, element) + if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => + //other elements that need to be updated + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend + if (updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + updatedEntries.map { entry => + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) + } + ) + ) + } + + case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => + sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) + + case SquadResponse.SquadSearchResults(_/*results*/) => + //TODO positive squad search results message? + // if(results.nonEmpty) { + // results.foreach { guid => + // sendResponse(SquadDefinitionActionMessage( + // guid, + // 0, + // SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + // ) + // } + // } else { + // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + // } + // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) + + case SquadResponse.InitWaypoints(char_id, waypoints) => + waypoints.foreach { + case (waypoint_type, info, unk) => + sendResponse( + SquadWaypointEvent.Add( + ops.squad_supplement_id, + char_id, + waypoint_type, + WaypointEvent(info.zone_number, info.pos, unk) + ) + ) + } + + case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) => + sendResponse( + SquadWaypointEvent.Add( + ops.squad_supplement_id, + char_id, + waypoint_type, + WaypointEvent(info.zone_number, info.pos, unk) + ) + ) + + case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => + sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type)) + + case _ => () + } + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala new file mode 100644 index 000000000..2c53b8d6d --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala @@ -0,0 +1,180 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions} +import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory} +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.guid.TaskWorkflow +import net.psforever.objects.serverobject.pad.VehicleSpawnPad +import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} +import net.psforever.objects.sourcing.AmenitySource +import net.psforever.objects.vital.TerminalUsedActivity +import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} +import net.psforever.types.{TransactionType, Vector3} + +class TerminalHandlerLogic(val ops: SessionTerminalHandlers) extends TerminalHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { + val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt + continent.GUID(terminalGuid) match { + case Some(term: Terminal) if ops.lastTerminalOrderFulfillment => + val msg: String = if (itemName.nonEmpty) s" of $itemName" else "" + log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg") + ops.lastTerminalOrderFulfillment = false + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + term.Actor ! Terminal.Request(player, pkt) + case Some(_: Terminal) => + log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}") + case Some(obj) => + log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}") + case _ => + log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}") + } + } + + def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { + val ProximityTerminalUseMessage(_, objectGuid, _) = pkt + continent.GUID(objectGuid) match { + case Some(obj: Terminal with ProximityUnit) => + ops.HandleProximityTerminalUse(obj) + case Some(obj) => + log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects") + case None => + log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}") + } + } + + def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { + val FavoritesRequest(_, loadoutType, action, line, label) = pkt + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + action match { + case FavoritesAction.Save => + avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) + case FavoritesAction.Delete => + avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) + case FavoritesAction.Unknown => + log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action") + } + } + + /** + * na + * @param tplayer na + * @param msg na + * @param order na + */ + def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = { + order match { + case Terminal.BuyEquipment(item) + if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) + ops.lastTerminalOrderFulfillment = true + + case Terminal.BuyEquipment(item) => + avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) + TaskWorkflow.execute(BuyNewEquipmentPutInInventory( + continent.GUID(tplayer.VehicleSeated) match { + case Some(v: Vehicle) => v + case _ => player + }, + tplayer, + msg.terminal_guid + )(item)) + + case Terminal.SellEquipment() => + SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot) + + case Terminal.LearnCertification(cert) => + avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert) + ops.lastTerminalOrderFulfillment = true + + case Terminal.SellCertification(cert) => + avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert) + ops.lastTerminalOrderFulfillment = true + + case Terminal.LearnImplant(implant) => + avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant) + ops.lastTerminalOrderFulfillment = true + + case Terminal.SellImplant(implant) => + avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant) + ops.lastTerminalOrderFulfillment = true + + case Terminal.BuyVehicle(vehicle, _, _) + if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || tplayer.spectator => + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) + ops.lastTerminalOrderFulfillment = true + + case Terminal.BuyVehicle(vehicle, weapons, trunk) => + continent.map.terminalToSpawnPad + .find { case (termid, _) => termid == msg.terminal_guid.guid } + .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) } + .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) => + avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) + vehicle.Faction = tplayer.Faction + vehicle.Position = pad.Position + vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset) + //default loadout, weapons + val vWeapons = vehicle.Weapons + weapons.foreach { entry => + vWeapons.get(entry.start) match { + case Some(slot) => + entry.obj.Faction = tplayer.Faction + slot.Equipment = None + slot.Equipment = entry.obj + case None => + log.warn( + s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}" + ) + } + } + //default loadout, trunk + val vTrunk = vehicle.Trunk + vTrunk.Clear() + trunk.foreach { entry => + entry.obj.Faction = tplayer.Faction + vTrunk.InsertQuickly(entry.start, entry.obj) + } + TaskWorkflow.execute(ops.registerVehicleFromSpawnPad(vehicle, pad, term)) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true)) + if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { + sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) + } + player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type)) + } + .orElse { + log.error( + s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it" + ) + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) + None + } + ops.lastTerminalOrderFulfillment = true + + case Terminal.NoDeal() if msg != null => + val transaction = msg.transaction_type + log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false)) + ops.lastTerminalOrderFulfillment = true + + case _ => + val terminal = msg.terminal_guid.guid + continent.GUID(terminal) match { + case Some(term: Terminal) => + log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order") + case Some(_) => + log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal") + case None => + log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal") + } + ops.lastTerminalOrderFulfillment = true + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala new file mode 100644 index 000000000..58b466f54 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala @@ -0,0 +1,395 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions} +import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} +import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} +import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.pad.VehicleSpawnPad +import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent +import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage} +import net.psforever.services.Service +import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse} +import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} + +class VehicleHandlerLogic(val ops: SessionVehicleHandlers) extends VehicleHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + private val galaxyService: ActorRef = ops.galaxyService + + /** + * na + * + * @param toChannel na + * @param guid na + * @param reply na + */ + def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = { + val resolvedPlayerGuid = if (player.HasGUID) { + player.GUID + } else { + PlanetSideGUID(-1) + } + val isNotSameTarget = resolvedPlayerGuid != guid + reply match { + case VehicleResponse.VehicleState( + vehicleGuid, + unk1, + pos, + orient, + vel, + unk2, + unk3, + unk4, + wheelDirection, + unk5, + unk6 + ) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) => + //player who is also in the vehicle (not driver) + sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6)) + player.Position = pos + player.Orientation = orient + player.Velocity = vel + sessionLogic.updateLocalBlockMap(pos) + + case VehicleResponse.VehicleState( + vehicleGuid, + unk1, + pos, + ang, + vel, + unk2, + unk3, + unk4, + wheelDirection, + unk5, + unk6 + ) if isNotSameTarget => + //player who is watching the vehicle from the outside + sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, ang, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6)) + + case VehicleResponse.ChildObjectState(objectGuid, pitch, yaw) if isNotSameTarget => + sendResponse(ChildObjectStateMessage(objectGuid, pitch, yaw)) + + case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA) + if isNotSameTarget => + sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)) + + case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + + case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + + case VehicleResponse.Reload(itemGuid) if isNotSameTarget => + sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) + + case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget => + sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) + //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0)) + sendResponse( + ObjectCreateMessage( + ammo_id, + ammo_guid, + ObjectCreateMessageParent(weapon_guid, weapon_slot), + ammo_data + ) + ) + sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + + case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget => + continent.GUID(weaponGuid).collect { + case tool: Tool if tool.Magazine == 0 => + // check that the magazine is still empty before sending WeaponDryFireMessage + // if it has been reloaded since then, other clients will not see it firing + sendResponse(WeaponDryFireMessage(weaponGuid)) + } + + case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget => + sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver)) + + case VehicleResponse.MountVehicle(vehicleGuid, seat) if isNotSameTarget => + sendResponse(ObjectAttachMessage(vehicleGuid, guid, seat)) + + case VehicleResponse.DeployRequest(objectGuid, state, unk1, unk2, pos) if isNotSameTarget => + sendResponse(DeployRequestMessage(guid, objectGuid, state, unk1, unk2, pos)) + + case VehicleResponse.SendResponse(msg) => + sendResponse(msg) + + case VehicleResponse.AttachToRails(vehicleGuid, padGuid) => + sendResponse(ObjectAttachMessage(padGuid, vehicleGuid, slot=3)) + + case VehicleResponse.ConcealPlayer(playerGuid) => + sendResponse(GenericObjectActionMessage(playerGuid, code=9)) + + case VehicleResponse.DetachFromRails(vehicleGuid, padGuid, padPosition, padOrientationZ) => + val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad].Definition + sendResponse( + ObjectDetachMessage( + padGuid, + vehicleGuid, + padPosition + Vector3.z(pad.VehicleCreationZOffset), + padOrientationZ + pad.VehicleCreationZOrientOffset + ) + ) + + case VehicleResponse.EquipmentInSlot(pkt) if isNotSameTarget => + sendResponse(pkt) + + case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget => + sendResponse(GenericObjectActionMessage(objectGuid, action)) + + case VehicleResponse.HitHint(sourceGuid) if player.isAlive => + sendResponse(HitHint(sourceGuid, player.GUID)) + + case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget => + //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? + val objGuid = obj.GUID + sendResponse(ObjectDeleteMessage(objGuid, unk1=0)) + sendResponse(ObjectCreateDetailedMessage( + obj.Definition.ObjectId, + objGuid, + ObjectCreateMessageParent(parentGuid, start), + conData + )) + + case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicleGuid) if resolvedPlayerGuid == guid => + //seat number (first field) seems to be correct if passenger is kicked manually by driver + //but always seems to return 4 if user is kicked by mount permissions changing + sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) + val typeOfRide = continent.GUID(vehicleGuid) match { + case Some(obj: Vehicle) => + sessionLogic.general.unaccessContainer(obj) + s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}" + case _ => + s"${player.Sex.possessive} ride" + } + log.info(s"${player.Name} has been kicked from $typeOfRide!") + + case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) => + //seat number (first field) seems to be correct if passenger is kicked manually by driver + //but always seems to return 4 if user is kicked by mount permissions changing + sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) + + case VehicleResponse.InventoryState2(objGuid, parentGuid, value) if isNotSameTarget => + sendResponse(InventoryStateMessage(objGuid, unk=0, parentGuid, value)) + + case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) if isNotSameTarget => + //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible) + sendResponse(ObjectCreateMessage(vtype, vguid, vdata)) + Vehicles.ReloadAccessPermissions(vehicle, player.Name) + + case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget => + sendResponse(ObjectDeleteMessage(itemGuid, unk1=0)) + + case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid => + //Only the player that owns this vehicle needs the ownership packet + avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid)) + sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid)) + + case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget => + sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue)) + + case VehicleResponse.ResetSpawnPad(padGuid) => + sendResponse(GenericObjectActionMessage(padGuid, code=23)) + + case VehicleResponse.RevealPlayer(playerGuid) => + sendResponse(GenericObjectActionMessage(playerGuid, code=10)) + + case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget => + sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission)) + + case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget => + //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly? + sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData)) + + case VehicleResponse.UnloadVehicle(_, vehicleGuid) => + sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) + + case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget => + //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? + sendResponse(ObjectDeleteMessage(itemGuid, unk1=0)) + + case VehicleResponse.UpdateAmsSpawnPoint(list) => + sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction) + sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint() + + case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget => + sessionLogic.zoning.interstellarFerry = Some(vehicle) + sessionLogic.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete) + continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}") + galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel + log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating") + + case VehicleResponse.KickCargo(vehicle, speed, delay) + if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 => + val strafe = 1 + Vehicles.CargoOrientation(vehicle) + val reverseSpeed = if (strafe > 1) { 0 } else { speed } + //strafe or reverse, not both + sessionLogic.vehicles.ServerVehicleOverrideWithPacket( + vehicle, + ServerVehicleOverrideMsg( + lock_accelerator=true, + lock_wheel=true, + reverse=true, + unk4=false, + lock_vthrust=0, + strafe, + reverseSpeed, + unk8=Some(0) + ) + ) + import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.duration._ + context.system.scheduler.scheduleOnce( + delay milliseconds, + context.self, + VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay)) + ) + + case VehicleResponse.KickCargo(cargo, _, _) + if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive => + sessionLogic.vehicles.TotalDriverVehicleControl(cargo) + + case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) + if player.VisibleSlots.contains(player.DrawnSlot) => + player.DrawnSlot = Player.HandsDownSlot + startPlayerSeatedInVehicle(vehicle) + + case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) => + startPlayerSeatedInVehicle(vehicle) + + case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) => + Vehicles.ReloadAccessPermissions(vehicle, player.Name) + sessionLogic.vehicles.ServerVehicleOverrideWithPacket( + vehicle, + ServerVehicleOverrideMsg( + lock_accelerator=true, + lock_wheel=true, + reverse=true, + unk4=false, + lock_vthrust=1, + lock_strafe=0, + movement_speed=0, + unk8=Some(0) + ) + ) + sessionLogic.vehicles.serverVehicleControlVelocity = Some(0) + + case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) => + val vdef = vehicle.Definition + sessionLogic.vehicles.ServerVehicleOverrideWithPacket( + vehicle, + ServerVehicleOverrideMsg( + lock_accelerator=true, + lock_wheel=true, + reverse=false, + unk4=false, + lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 }, + lock_strafe=0, + movement_speed=vdef.AutoPilotSpeed1, + unk8=Some(0) + ) + ) + + case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) => + sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle) + + case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) => + sendResponse(ChatMsg( + ChatMessageType.CMT_OPEN, + wideContents=true, + recipient="", + s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}", + note=None + )) + + case VehicleResponse.PeriodicReminder(_, data) => + val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match { + case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg) + case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg) + case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.") + } + sendResponse(ChatMsg(isType, flag, recipient="", msg, None)) + + case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory) + if player.avatar.vehicle.contains(target) => + //TODO when vehicle weapons can be changed without visual glitches, rewrite this + continent.GUID(target).collect { case vehicle: Vehicle => + import net.psforever.login.WorldSession.boolToInt + //owner: must unregister old equipment, and register and install new equipment + (oldWeapons ++ oldInventory).foreach { + case (obj, eguid) => + sendResponse(ObjectDeleteMessage(eguid, unk1=0)) + TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) + } + sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory) + //jammer or unjamm new weapons based on vehicle status + val vehicleJammered = vehicle.Jammed + addedWeapons + .map { _.obj } + .collect { + case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered => + jamItem.Jammed = vehicleJammered + JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered) + } + changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory) + } + + case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) + if sessionLogic.general.accessedContainer.map(_.GUID).contains(target) => + //TODO when vehicle weapons can be changed without visual glitches, rewrite this + continent.GUID(target).collect { case vehicle: Vehicle => + //external participant: observe changes to equipment + (oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } + changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory) + } + + case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) => + //TODO when vehicle weapons can be changed without visual glitches, rewrite this + continent.GUID(target).collect { case vehicle: Vehicle => + changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory) + } + + case _ => () + } + } + + private def changeLoadoutDeleteOldEquipment( + vehicle: Vehicle, + oldWeapons: Iterable[(Equipment, PlanetSideGUID)], + oldInventory: Iterable[(Equipment, PlanetSideGUID)] + ): Unit = { + vehicle.PassengerInSeat(player) match { + case Some(seatNum) => + //participant: observe changes to equipment + (oldWeapons ++ oldInventory).foreach { + case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) + } + sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seatNum) + case None => + //observer: observe changes to external equipment + oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } + } + } + + private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = { + val vehicle_guid = vehicle.GUID + sessionLogic.actionsToCancel() + sessionLogic.terminals.CancelAllProximityUnits() + sessionLogic.vehicles.serverVehicleControlVelocity = Some(0) + sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off + sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership + vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect { + case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint) + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala new file mode 100644 index 000000000..a7efd72cd --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -0,0 +1,407 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles} +import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.vehicles.control.BfrFlight +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{DriveState, Vector3} + +class VehicleLogic(val ops: VehicleOperations) extends VehicleFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + /* packets */ + + def handleVehicleState(pkt: VehicleStateMessage): Unit = { + val VehicleStateMessage( + vehicle_guid, + unk1, + pos, + ang, + vel, + is_flying, + unk6, + unk7, + wheels, + is_decelerating, + is_cloaked + ) = pkt + GetVehicleAndSeat() match { + case (Some(obj), Some(0)) => + //we're driving the vehicle + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + sessionLogic.general.fallHeightTracker(pos.z) + if (obj.MountedIn.isEmpty) { + sessionLogic.updateBlockMap(obj, pos) + } + player.Position = pos //convenient + if (obj.WeaponControlledFromSeat(0).isEmpty) { + player.Orientation = Vector3.z(ang.z) //convenient + } + obj.Position = pos + obj.Orientation = ang + if (obj.MountedIn.isEmpty) { + if (obj.DeploymentState != DriveState.Deployed) { + obj.Velocity = vel + } else { + obj.Velocity = Some(Vector3.Zero) + } + if (obj.Definition.CanFly) { + obj.Flying = is_flying //usually Some(7) + } + obj.Cloaked = obj.Definition.CanCloak && is_cloaked + } else { + obj.Velocity = None + obj.Flying = None + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.VehicleState( + player.GUID, + vehicle_guid, + unk1, + obj.Position, + ang, + obj.Velocity, + if (obj.isFlying) { + is_flying + } else { + None + }, + unk6, + unk7, + wheels, + is_decelerating, + obj.Cloaked + ) + ) + sessionLogic.squad.updateSquad() + obj.zoneInteractions() + case (None, _) => + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle + case (_, Some(index)) => + log.error( + s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" + ) + case _ => ; + } + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + } + + def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { + val FrameVehicleStateMessage( + vehicle_guid, + unk1, + pos, + ang, + vel, + unk2, + unk3, + unk4, + is_crouched, + is_airborne, + ascending_flight, + flight_time, + unk9, + unkA + ) = pkt + GetVehicleAndSeat() match { + case (Some(obj), Some(0)) => + //we're driving the vehicle + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { + case Some(v: Vehicle) => + sessionLogic.updateBlockMap(obj, pos) + (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) + case _ => + (pos, ang, vel, true) + } + player.Position = position //convenient + if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) { + player.Orientation = Vector3.z(ang.z) //convenient + } + obj.Position = position + obj.Orientation = angle + obj.Velocity = velocity + // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) { + // //dev stuff goes here + // } + // else + // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) { + // //dev stuff goes here + // } + obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile + if (notMountedState) { + if (obj.DeploymentState != DriveState.Kneeling) { + if (is_airborne) { + val flight = if (ascending_flight) flight_time else -flight_time + obj.Flying = Some(flight) + obj.Actor ! BfrFlight.Soaring(flight) + } else if (obj.Flying.nonEmpty) { + obj.Flying = None + obj.Actor ! BfrFlight.Landed + } + } else { + obj.Velocity = None + obj.Flying = None + } + obj.zoneInteractions() + } else { + obj.Velocity = None + obj.Flying = None + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.FrameVehicleState( + player.GUID, + vehicle_guid, + unk1, + position, + angle, + velocity, + unk2, + unk3, + unk4, + is_crouched, + is_airborne, + ascending_flight, + flight_time, + unk9, + unkA + ) + ) + sessionLogic.squad.updateSquad() + case (None, _) => + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle + case (_, Some(index)) => + log.error( + s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" + ) + case _ => ; + } + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + } + + def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { + val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt + val (o, tools) = sessionLogic.shooting.FindContainedWeapon + //is COSM our primary upstream packet? + (o match { + case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) + case _ => (None, None) + }) match { + case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ; + case _ => + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + } + //the majority of the following check retrieves information to determine if we are in control of the child + tools.find { _.GUID == object_guid } match { + case None => + //todo: old warning; this state is problematic, but can trigger in otherwise valid instances + //log.warn( + // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" + //) + case Some(_) => + //TODO set tool orientation? + player.Orientation = Vector3(0f, pitch, yaw) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) + ) + } + //TODO status condition of "playing getting out of vehicle to allow for late packets without warning + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + } + + def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = { + val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt + sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState") match { + case Some(obj: Vehicle) => + import net.psforever.login.WorldSession.boolToInt + obj.Position = pos + obj.Orientation = ang + obj.Velocity = vel + sessionLogic.updateBlockMap(obj, pos) + obj.zoneInteractions() + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.VehicleState( + player.GUID, + vehicle_guid, + unk1, + pos, + ang, + obj.Velocity, + obj.Flying, + 0, + 0, + 15, + unk5 = false, + obj.Cloaked + ) + ) + case _ => () + } + } + + def handleDeployRequest(pkt: DeployRequestMessage): Unit = { + val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt + val vehicle = player.avatar.vehicle + if (vehicle.contains(vehicle_guid)) { + if (vehicle == player.VehicleSeated) { + continent.GUID(vehicle_guid) match { + case Some(obj: Vehicle) => + log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state") + obj.Actor ! Deployment.TryDeploymentChange(deploy_state) + + case _ => + log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid") + avatarActor ! AvatarActor.SetVehicle(None) + } + } else { + log.warn(s"${player.Name} must be mounted to request a deployment change") + } + } else { + log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object") + } + } + + /* messages */ + + def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { + if (state == DriveState.Deploying) { + log.trace(s"DeployRequest: $obj transitioning to deploy state") + } else if (state == DriveState.Deployed) { + log.trace(s"DeployRequest: $obj has been Deployed") + } else { + CanNotChangeDeployment(obj, state, "incorrect deploy state") + } + } + + def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { + if (state == DriveState.Undeploying) { + log.trace(s"DeployRequest: $obj transitioning to undeploy state") + } else if (state == DriveState.Mobile) { + log.trace(s"DeployRequest: $obj is Mobile") + } else { + CanNotChangeDeployment(obj, state, "incorrect undeploy state") + } + } + + def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = { + if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) { + CanNotChangeDeployment(obj, state, reason = "ground too steep") + } else { + CanNotChangeDeployment(obj, state, reason) + } + } + + /* support functions */ + + /** + * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat. + * The priority of object confirmation is `direct` then `occupant.VehicleSeated`. + * Once an object is found, the remainder are ignored. + * @param direct a game object in which the player may be sat + * @param occupant the player who is sat and may have specified the game object in which mounted + * @return a tuple consisting of a vehicle reference and a mount index + * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; + * `(None, None)`, otherwise (even if the vehicle can be determined) + */ + def GetMountableAndSeat( + direct: Option[PlanetSideGameObject with Mountable], + occupant: Player, + zone: Zone + ): (Option[PlanetSideGameObject with Mountable], Option[Int]) = + direct.orElse(zone.GUID(occupant.VehicleSeated)) match { + case Some(obj: PlanetSideGameObject with Mountable) => + obj.PassengerInSeat(occupant) match { + case index @ Some(_) => + (Some(obj), index) + case None => + (None, None) + } + case _ => + (None, None) + } + + /** + * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
+ *
+ * For special purposes involved in zone transfers, + * where the vehicle may or may not exist in either of the zones (yet), + * the value of `interstellarFerry` is also polled. + * Making certain this field is blanked after the transfer is completed is important + * to avoid inspecting the wrong vehicle and failing simple vehicle checks where this function may be employed. + * @see `GetMountableAndSeat` + * @see `interstellarFerry` + * @return a tuple consisting of a vehicle reference and a mount index + * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; + * `(None, None)`, otherwise (even if the vehicle can be determined) + */ + def GetKnownVehicleAndSeat(): (Option[Vehicle], Option[Int]) = + GetMountableAndSeat(sessionLogic.zoning.interstellarFerry, player, continent) match { + case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat)) + case _ => (None, None) + } + + /** + * If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat. + * @see `GetMountableAndSeat` + * @return a tuple consisting of a vehicle reference and a mount index + * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; + * `(None, None)`, otherwise (even if the vehicle can be determined) + */ + def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) = + GetMountableAndSeat(None, player, continent) match { + case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat)) + case _ => (None, None) + } + + /** + * Common reporting behavior when a `Deployment` object fails to properly transition between states. + * @param obj the game object that could not + * @param state the `DriveState` that could not be promoted + * @param reason a string explaining why the state can not or will not change + */ + private def CanNotChangeDeployment( + obj: PlanetSideServerObject with Deployment, + state: DriveState.Value, + reason: String + ): Unit = { + val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) { + obj.DeploymentState = DriveState.Mobile + sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero) + ) + "; enforcing Mobile deployment state" + } else { + "" + } + log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift") + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala new file mode 100644 index 000000000..e27926e20 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala @@ -0,0 +1,1338 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.normal + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.{AvatarActor, ChatActor} +import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} +import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} +import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} +import net.psforever.objects.definition.ProjectileDefinition +import net.psforever.objects.entity.SimpleWorldEntity +import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch} +import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} +import net.psforever.objects.inventory.Container +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.doors.InteriorDoorPassage +import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, Player, SpecialEmp, Tool, Tools, Vehicle} +import net.psforever.objects.serverobject.interior.Sidedness +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.turret.{FacilityTurret, VanuSentry} +import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.{DamageResolution, DamageType} +import net.psforever.objects.vital.etc.OicwLilBuddyReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.projectile.ProjectileReason +import net.psforever.objects.zones.{Zone, ZoneProjectile} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ObjectAttachMessage, ObjectDeleteMessage, ObjectDetachMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{PlanetSideGUID, Vector3} +import net.psforever.util.Config + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.concurrent.duration._ + +class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations) extends WeaponAndProjectileFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + implicit val context: ActorContext = ops.context + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor + + /* packets */ + + def handleWeaponFire(pkt: WeaponFireMessage): Unit = { + val WeaponFireMessage( + _, + weapon_guid, + projectile_guid, + shot_origin, + _, + _, + _, + _/*max_distance,*/, + _, + _/*projectile_type,*/, + thrown_projectile_vel + ) = pkt + HandleWeaponFireOperations(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten) + } + + def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { + val WeaponDelayFireMessage(_, _) = pkt + log.info(s"${player.Name} - $pkt") + } + + def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { + val WeaponDryFireMessage(weapon_guid) = pkt + val (containerOpt, tools) = ops.FindContainedWeapon + tools + .find { _.GUID == weapon_guid } + .orElse { continent.GUID(weapon_guid) } + .collect { + case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) => + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.WeaponDryFire(player.GUID, weapon_guid) + ) + case _: Equipment => + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.WeaponDryFire(player.GUID, weapon_guid) + ) + } + .orElse { + log.warn( + s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist" + ) + None + } + } + + def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { + val WeaponLazeTargetPositionMessage(_, _, _) = pkt + //do not need to handle the progress bar animation/state on the server + //laze waypoint is requested by client upon completion (see SquadWaypointRequest) + val purpose = if (sessionLogic.squad.squad_supplement_id > 0) { + s" for ${player.Sex.possessive} squad (#${sessionLogic.squad.squad_supplement_id -1})" + } else { + " ..." + } + log.info(s"${player.Name} is lazing a position$purpose") + } + + def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { + val AvatarGrenadeStateMessage(_, state) = pkt + //TODO I thought I had this working? + log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade") + } + + def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = { + val ChangeFireStateMessage_Start(item_guid) = pkt + if (ops.shooting.isEmpty) { + sessionLogic.findEquipment(item_guid) match { + case Some(tool: Tool) if player.VehicleSeated.isEmpty => + fireStateStartWhenPlayer(tool, item_guid) + case Some(tool: Tool) => + fireStateStartWhenMounted(tool, item_guid) + case Some(_) if player.VehicleSeated.isEmpty => + fireStateStartSetup(item_guid) + fireStateStartPlayerMessages(item_guid) + case Some(_) => + fireStateStartSetup(item_guid) + fireStateStartMountedMessages(item_guid) + case None => + log.warn(s"ChangeFireState_Start: can not find $item_guid") + } + } + } + + def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = { + val ChangeFireStateMessage_Stop(item_guid) = pkt + val now = System.currentTimeMillis() + ops.prefire -= item_guid + ops.shootingStop += item_guid -> now + ops.shooting -= item_guid + sessionLogic.findEquipment(item_guid) match { + case Some(tool: Tool) if player.VehicleSeated.isEmpty => + fireStateStopWhenPlayer(tool, item_guid) + case Some(tool: Tool) => + fireStateStopWhenMounted(tool, item_guid) + case Some(trigger: BoomerTrigger) => + ops.fireStateStopPlayerMessages(item_guid) + continent.GUID(trigger.Companion).collect { + case boomer: BoomerDeployable => + boomer.Actor ! CommonMessages.Use(player, Some(trigger)) + } + case Some(_) if player.VehicleSeated.isEmpty => + ops.fireStateStopPlayerMessages(item_guid) + case Some(_) => + ops.fireStateStopMountedMessages(item_guid) + case _ => + log.warn(s"ChangeFireState_Stop: can not find $item_guid") + } + sessionLogic.general.progressBarUpdate.cancel() + sessionLogic.general.progressBarValue = None + } + + def handleReload(pkt: ReloadMessage): Unit = { + val ReloadMessage(item_guid, _, unk1) = pkt + ops.FindContainedWeapon match { + case (Some(obj: Player), tools) => + handleReloadWhenPlayer(item_guid, obj, tools, unk1) + case (Some(obj: PlanetSideServerObject with Container), tools) => + handleReloadWhenMountable(item_guid, obj, tools, unk1) + case (_, _) => + log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") + } + } + + def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { + val ChangeAmmoMessage(item_guid, _) = pkt + val (thing, equipment) = sessionLogic.findContainedEquipment() + if (equipment.isEmpty) { + log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment") + } else { + equipment foreach { + case obj: ConstructionItem => + if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { + log.info( + s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" + ) + sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) + } + case tool: Tool => + thing match { + case Some(player: Player) => + PerformToolAmmoChange(tool, player, ModifyAmmunition(player)) + case Some(mountable: PlanetSideServerObject with Container) => + PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable)) + case _ => + log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") + } + case obj => + log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") + } + } + } + + def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { + val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt + sessionLogic.findEquipment(item_guid) match { + case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => + val originalModeIndex = obj.FireModeIndex + if (obj match { + case citem: ConstructionItem => + val modeChanged = Deployables.performConstructionItemFireModeChange( + player.avatar.certifications, + citem, + originalModeIndex + ) + modeChanged + case _ => + obj.NextFireMode + obj.FireModeIndex != originalModeIndex + }) { + val modeIndex = obj.FireModeIndex + obj match { + case citem: ConstructionItem => + log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)") + case _ => + log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex") + } + sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) + ) + } + case Some(_) => + log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") + case None => + log.warn(s"ChangeFireMode: can not find $item_guid") + } + } + + def handleProjectileState(pkt: ProjectileStateMessage): Unit = { + val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt + val index = projectile_guid.guid - Projectile.baseUID + ops.projectiles(index) match { + case Some(projectile) if projectile.HasGUID => + val projectileGlobalUID = projectile.GUID + projectile.Position = shot_pos + projectile.Orientation = shot_orient + projectile.Velocity = shot_vel + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ProjectileState( + player.GUID, + projectileGlobalUID, + shot_pos, + shot_vel, + shot_orient, + seq, + end, + target_guid + ) + ) + case _ if seq == 0 => + /* missing the first packet in the sequence is permissible */ + case _ => + log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") + } + } + + def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { + val LongRangeProjectileInfoMessage(guid, _, _) = pkt + ops.FindContainedWeapon match { + case (Some(_: Vehicle), weapons) + if weapons.exists { _.GUID == guid } => () //now what? + case _ => () + } + } + + def handleDirectHit(pkt: HitMessage): Unit = { + val HitMessage( + _, + projectile_guid, + _, + hit_info, + _, + _, + _ + ) = pkt + //find defined projectile + ops.FindProjectileEntry(projectile_guid) match { + case Some(projectile) => + //find target(s) + (hit_info match { + case Some(hitInfo) => + val hitPos = hitInfo.hit_pos + sessionLogic.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match { + case _ if projectile.profile == GlobalDefinitions.flail_projectile => + val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius + val targets = Zone.findAllTargets(continent, player, hitPos, projectile.profile) + .filter { target => + Vector3.DistanceSquared(target.Position, hitPos) <= radius + } + targets.map { target => + CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) + (target, projectile, hitPos, target.Position) + } + + case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => + CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) + List((target, projectile, hitInfo.shot_origin, hitPos)) + + case None => + HandleDamageProxy(projectile, projectile_guid, hitPos) + + case _ => + Nil + } + case None => + Nil + }) + .foreach { + case ( + target: PlanetSideGameObject with FactionAffinity with Vitality, + proj: Projectile, + _: Vector3, + hitPos: Vector3 + ) => + ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionLogic.handleDealingDamage(target, resprojectile) + } + case _ => () + } + case None => + log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + } + } + + def handleSplashHit(pkt: SplashHitMessage): Unit = { + val SplashHitMessage( + _, + projectile_guid, + explosion_pos, + direct_victim_uid, + _, + projectile_vel, + _, + targets + ) = pkt + ops.FindProjectileEntry(projectile_guid) match { + case Some(projectile) => + val profile = projectile.profile + projectile.Velocity = projectile_vel + val (resolution1, resolution2) = profile.Aggravated match { + case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) => + (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash) + case _ => + (DamageResolution.Splash, DamageResolution.Splash) + } + //direct_victim_uid + sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { + case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => + CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) + ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionLogic.handleDealingDamage(target, resprojectile) + } + case _ => () + } + //other victims + targets.foreach(elem => { + sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match { + case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => + CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) + ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionLogic.handleDealingDamage(target, resprojectile) + } + case _ => () + } + }) + //... + HandleDamageProxy(projectile, projectile_guid, explosion_pos) + if ( + projectile.profile.HasJammedEffectDuration || + projectile.profile.JammerProjectile || + projectile.profile.SympatheticExplosion + ) { + //can also substitute 'projectile.profile' for 'SpecialEmp.emp' + Zone.serverSideDamage( + continent, + player, + SpecialEmp.emp, + SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos), + SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction), + SpecialEmp.findAllBoomers(profile.DamageRadius) + ) + } + if (profile.ExistsOnRemoteClients && projectile.HasGUID) { + //cleanup + if (projectile.HasGUID) { + continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) + } + } + case None => () + } + } + + def handleLashHit(pkt: LashMessage): Unit = { + val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt + sessionLogic.validObject(victim_guid, decorator = "Lash") match { + case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => + CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) + ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach { + resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionLogic.handleDealingDamage(target, resprojectile) + } + case _ => () + } + } + + def handleAIDamage(pkt: AIDamage): Unit = { + val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt + (continent.GUID(player.VehicleSeated) match { + case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer) + if tobj.GUID == targetGuid && + tobj.OwnerGuid.contains(player.GUID) => + //deployable turrets + Some(tobj) + case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable) + if tobj.GUID == targetGuid && + tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) => + //facility turrets, etc. + Some(tobj) + case _ + if player.GUID == targetGuid => + //player avatars + Some(player) + case _ => + None + }).collect { + case target: AutomatedTurret.Target => + sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") + .collect { + case turret: AutomatedTurret if turret.Target.isEmpty => + turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) + Some(target) + + case turret: AutomatedTurret => + turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) + HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)) + Some(target) + } + } + .orElse { + //occasionally, something that is not technically a turret's natural target may be attacked + sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target") + .collect { + case target: PlanetSideServerObject with FactionAffinity with Vitality => + sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") + .collect { + case turret: AutomatedTurret if turret.Target.nonEmpty => + //the turret must be shooting at something (else) first + HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)) + } + Some(target) + } + } + } + + /* support code */ + + private def HandleWeaponFireOperations( + weaponGUID: PlanetSideGUID, + projectileGUID: PlanetSideGUID, + shotOrigin: Vector3, + shotVelocity: Option[Vector3] + ): Unit = { + ops.HandleWeaponFireAccountability(weaponGUID, projectileGUID) match { + case (Some(obj), Some(tool)) => + val projectileIndex = projectileGUID.guid - Projectile.baseUID + val projectilePlace = ops.projectiles(projectileIndex) + if ( + projectilePlace match { + case Some(projectile) => + !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong + case None => + false + } + ) { + log.debug( + s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}" + ) + } + val (angle, attribution, acceptableDistanceToOwner) = obj match { + case p: Player => + ( + SimpleWorldEntity.validateOrientationEntry( + p.Orientation + Vector3.z(p.FacingYawUpper) + ), + tool.Definition.ObjectId, + 10f + (if (p.Velocity.nonEmpty) { + 5f + } else { + 0f + }) + ) + case v: Vehicle if v.Definition.CanFly => + (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle + case _: Vehicle => + (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle + case _ => + (obj.Orientation, obj.Definition.ObjectId, 300f) + } + val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position) + if (distanceToOwner <= acceptableDistanceToOwner) { + val projectile_info = tool.Projectile + val wguid = weaponGUID.guid + val mountedIn = (continent.turretToWeapon + .find { case (guid, _) => guid == wguid } match { + case Some((_, turretGuid)) => Some(( + turretGuid, + continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } + )) + case _ => None + }) match { + case Some((guid, Some(entity))) => Some((guid, entity)) + case _ => None + } + val projectile = new Projectile( + projectile_info, + tool.Definition, + tool.FireMode, + mountedIn, + PlayerSource(player), + attribution, + shotOrigin, + angle, + shotVelocity + ) + val initialQuality = tool.FireMode match { + case mode: ChargeFireModeDefinition => + ProjectileQuality.Modified( + { + val timeInterval = projectile.fire_time - ops.shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) + timeInterval.toFloat / mode.Time.toFloat + } + ) + case _ => + ProjectileQuality.Normal + } + val qualityprojectile = projectile.quality(initialQuality) + qualityprojectile.WhichSide = player.WhichSide + ops.projectiles(projectileIndex) = Some(qualityprojectile) + if (projectile_info.ExistsOnRemoteClients) { + log.trace( + s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" + ) + continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) + } + } else { + log.warn( + s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect" + ) + } + + case _ => () + } + } + + /** + * After a weapon has finished shooting, determine if it needs to be sorted in a special way. + * @param tool a weapon + */ + private def FireCycleCleanup(tool: Tool): Unit = { + //TODO replaced by more appropriate functionality in the future + val tdef = tool.Definition + if (GlobalDefinitions.isGrenade(tdef)) { + val ammoType = tool.AmmoType + FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters + case Nil => + log.info(s"${player.Name} has no more $ammoType grenades to throw") + RemoveOldEquipmentFromInventory(player)(tool) + + case x :: xs => //this is similar to ReloadMessage + val box = x.obj.asInstanceOf[Tool] + val tailReloadValue: Int = if (xs.isEmpty) { 0 } + else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } + val sumReloadValue: Int = box.Magazine + tailReloadValue + val actualReloadValue = if (sumReloadValue <= 3) { + RemoveOldEquipmentFromInventory(player)(x.obj) + sumReloadValue + } else { + ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + 3 + } + log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") + ModifyAmmunition(player)( + tool.AmmoSlot.Box, + -actualReloadValue + ) //grenade item already in holster (negative because empty) + xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) + } + } else if (tdef == GlobalDefinitions.phoenix) { + RemoveOldEquipmentFromInventory(player)(tool) + } + } + + /** + * Given an object that contains a box of amunition in its `Inventory` at a certain location, + * change the amount of ammunition within that box. + * @param obj the `Container` + * @param box an `AmmoBox` to modify + * @param reloadValue the value to modify the `AmmoBox`; + * subtracted from the current `Capacity` of `Box` + */ + private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { + val capacity = box.Capacity - reloadValue + box.Capacity = capacity + sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) + } + + /** + * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location, + * change the amount of ammunition within that box. + * @param obj the `Container` + * @param box an `AmmoBox` to modify + * @param reloadValue the value to modify the `AmmoBox`; + * subtracted from the current `Capacity` of `Box` + */ + private def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { + ModifyAmmunition(obj)(box, reloadValue) + obj.Find(box).collect { index => + continent.VehicleEvents ! VehicleServiceMessage( + s"${obj.Actor}", + VehicleAction.InventoryState( + player.GUID, + box, + obj.GUID, + index, + box.Definition.Packet.DetailedConstructorData(box).get + ) + ) + } + } + + /** + * na + * @param tool na + * @param obj na + */ + private def PerformToolAmmoChange( + tool: Tool, + obj: PlanetSideServerObject with Container, + modifyFunc: (AmmoBox, Int) => Unit + ): Unit = { + val originalAmmoType = tool.AmmoType + do { + val requestedAmmoType = tool.NextAmmoType + val fullMagazine = tool.MaxMagazine + if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { + case Nil => () + case x :: xs => + val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj) + val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) + + xs.foreach(item => { + obj.Inventory -= item.start + sendResponse(ObjectDeleteMessage(item.obj.GUID, 0)) + TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj)) + }) + + //box will be the replacement ammo; give it the discovered magazine and load it into the weapon + val box = x.obj.asInstanceOf[AmmoBox] + //previousBox is the current magazine in tool; it will be removed from the weapon + val previousBox = tool.AmmoSlot.Box + val originalBoxCapacity = box.Capacity + val tailReloadValue: Int = if (xs.isEmpty) { + 0 + } else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } + val sumReloadValue: Int = originalBoxCapacity + tailReloadValue + val ammoSlotIndex = tool.FireMode.AmmoSlotIndex + val box_guid = box.GUID + val tool_guid = tool.GUID + obj.Inventory -= x.start //remove replacement ammo from inventory + tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool + sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f)) + sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f)) + sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex)) + + //announce swapped ammunition box in weapon + val previous_box_guid = previousBox.GUID + val boxDef = box.Definition + sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeAmmo( + player.GUID, + tool_guid, + ammoSlotIndex, + previous_box_guid, + boxDef.ObjectId, + box.GUID, + boxDef.Packet.ConstructorData(box).get + ) + ) + + //handle inventory contents + box.Capacity = if (sumReloadValue <= fullMagazine) { + sumReloadValue + } else { + val splitReloadAmmo: Int = sumReloadValue - fullMagazine + log.trace( + s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo" + ) + val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) + TaskWorkflow.execute(stowNewFunc(boxForInventory)) + fullMagazine + } + sendResponse( + InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) + ) //should work for both players and vehicles + log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}") + if (previousBox.Capacity > 0) { + //divide capacity across other existing and not full boxes of that ammo type + var capacity = previousBox.Capacity + val iter = obj.Inventory.Items + .filter(entry => { + entry.obj match { + case item: AmmoBox => + item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity + case _ => + false + } + }) + .sortBy(_.start) + .iterator + while (capacity > 0 && iter.hasNext) { + val entry = iter.next() + val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox] + val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) + log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") + capacity -= ammoAllocated + modifyFunc(item, -ammoAllocated) + } + previousBox.Capacity = capacity + } + + if (previousBox.Capacity > 0) { + //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm + obj.Inventory.Fit(previousBox) match { + case Some(_) => + stowFunc(previousBox) + case None => + sessionLogic.general.normalItemDrop(player, continent)(previousBox) + } + AmmoBox.Split(previousBox) match { + case Nil | List(_) => () //done (the former case is technically not possible) + case _ :: toUpdate => + modifyFunc(previousBox, 0) //update to changed capacity value + toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) + } + } else { + TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox)) + } + } + } + } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) + } + + private def CheckForHitPositionDiscrepancy( + projectile_guid: PlanetSideGUID, + hitPos: Vector3, + target: PlanetSideGameObject with FactionAffinity with Vitality + ): Unit = { + val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position) + if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { + // If the target position on the server does not match the position where the projectile landed within reason there may be foul play + log.warn( + s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" + ) + } + } + + /** + * Find a projectile with the given globally unique identifier and mark it as a resolved shot. + * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle. + * @param projectile_guid the projectile GUID + * @param resolution the resolution status to promote the projectile + * @return the projectile + */ + private def ResolveProjectileInteraction( + projectile_guid: PlanetSideGUID, + resolution: DamageResolution.Value, + target: PlanetSideGameObject with FactionAffinity with Vitality, + pos: Vector3 + ): Option[DamageInteraction] = { + ops.FindProjectileEntry(projectile_guid) match { + case Some(projectile) => + ResolveProjectileInteraction(projectile, resolution, target, pos) + case None => + log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found") + None + } + } + + /** + * na + * @param projectile the projectile object + * @param resolution the resolution status to promote the projectile + * @return a copy of the projectile + */ + private def ResolveProjectileInteraction( + projectile: Projectile, + resolution: DamageResolution.Value, + target: PlanetSideGameObject with FactionAffinity with Vitality, + pos: Vector3 + ): Option[DamageInteraction] = { + if (projectile.isMiss) { + log.warn("expected projectile was already counted as a missed shot; can not resolve any further") + None + } else { + val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player)) + if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) { + avatarActor ! AvatarActor.ConsumeStamina(10) + } + Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos)) + } + } + + /** + * Take a projectile that was introduced into the game world and + * determine if it generates a secondary damage projectile or + * an method of damage causation that requires additional management. + * @param projectile the projectile + * @param pguid the client-local projectile identifier + * @param hitPos the game world position where the projectile is being recorded + * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location; + * nothing if no targets were affected + */ + private def HandleDamageProxy( + projectile: Projectile, + pguid: PlanetSideGUID, + hitPos: Vector3 + ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + GlobalDefinitions.getDamageProxy(projectile, hitPos) match { + case Nil => + Nil + case list if list.isEmpty => + Nil + case list => + HandleDamageProxySetupLittleBuddy(list, hitPos) + UpdateProjectileSidednessAfterHit(projectile, hitPos) + val projectileSide = projectile.WhichSide + list.flatMap { proxy => + if (proxy.profile.ExistsOnRemoteClients) { + proxy.Position = hitPos + proxy.WhichSide = projectileSide + continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy) + Nil + } else if (proxy.tool_def == GlobalDefinitions.maelstrom) { + //server-side maelstrom grenade target selection + val radius = proxy.profile.LashRadius * proxy.profile.LashRadius + val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList }) + .filter { target => + Vector3.DistanceSquared(target.Position, hitPos) <= radius + } + //chainlash is separated from the actual damage application for convenience + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.SendResponse( + PlanetSideGUID(0), + ChainLashMessage( + hitPos, + projectile.profile.ObjectId, + targets.map { _.GUID } + ) + ) + ) + targets.map { target => + CheckForHitPositionDiscrepancy(pguid, hitPos, target) + (target, proxy, hitPos, target.Position) + } + } else { + Nil + } + } + } + } + + private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = { + val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw } + val size: Int = listOfLittleBuddies.size + if (size > 0) { + val desiredDownwardsProjectiles: Int = 2 + val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down + val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out + val z: Float = player.Orientation.z //player's standing direction + val north: Vector3 = Vector3(0,1,0) //map North + val speed: Float = 144f //speed (packet discovered) + val dist: Float = 25 //distance (client defined) + val downwardsAngle: Float = -85f + val flaredAngle: Float = -70f + //angle of separation for downwards, degrees from vertical for flared out + val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) { + (360f / firstHalf, downwardsAngle) + } else { + (0f, 0f) + } + val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) { + (360f / secondHalf, flaredAngle) + } else { + (0f, 0f) + } + val smallRotOffset: Float = z + 90f + val largeRotOffset: Float = z + math.random().toFloat * 45f + val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat) + //downwards projectiles + var i: Int = 0 + listOfLittleBuddies.take(firstHalf).foreach { proxy => + val facing = (smallRotOffset + smallStep * i.toFloat) % 360 + val dir = north.Rx(smallAngle).Rz(facing) + proxy.Position = detonationPosition + dir.xy + verticalCorrection + proxy.Velocity = dir * speed + proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing) + HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) + i += 1 + } + //flared out projectiles + i = 0 + listOfLittleBuddies.drop(firstHalf).foreach { proxy => + val facing = (largeRotOffset + largeStep * i.toFloat) % 360 + val dir = north.Rx(largeAngle).Rz(facing) + proxy.Position = detonationPosition + dir + proxy.Velocity = dir * speed + proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing) + HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) + i += 1 + } + true + } else { + false + } + } + + private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = { + //explosion + val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction) + obj.Position = obj.Position + orientation * distance + val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner) + context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() } + } + + /* + used by ChangeFireStateMessage_Start handling + */ + private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { + ops.prefire -= itemGuid + ops.shooting += itemGuid + ops.shootingStart += itemGuid -> System.currentTimeMillis() + } + + private def fireStateStartChargeMode(tool: Tool): Unit = { + //charge ammunition drain + tool.FireMode match { + case mode: ChargeFireModeDefinition => + sessionLogic.general.progressBarValue = Some(0f) + sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( + (mode.Time + mode.DrainInterval) milliseconds, + context.self, + CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) + ) + case _ => () + } + } + + private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { + sessionLogic.findContainedEquipment()._1.collect { + case turret: FacilityTurret if continent.map.cavern => + turret.Actor ! VanuSentry.ChangeFireStart + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + private def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { + tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || ops.prefire.contains(itemGuid) + } + + private def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + log.warn( + s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" + ) + ops.EmptyMagazine(itemGuid, tool) + } + + private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + //special case - suppress the decimator's alternate fire mode, by projectile + if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) + } + } + + private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + fireStateStartMountedMessages(itemGuid) + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) + } + } + + /* + used by ChangeFireStateMessage_Stop handling + */ + private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { + tool.FireMode match { + case _: ChargeFireModeDefinition => + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) + case _ => () + } + if (tool.Magazine == 0) { + FireCycleCleanup(tool) + } + } + + private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why + //suppress the decimator's alternate fire mode, however + if ( + tool.Definition == GlobalDefinitions.phoenix && + tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile + ) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStopUpdateChargeAndCleanup(tool) + ops.fireStateStopPlayerMessages(itemGuid) + } + + private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + fireStateStopUpdateChargeAndCleanup(tool) + ops.fireStateStopMountedMessages(itemGuid) + } + + /* + used by ReloadMessage handling + */ + private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.Reload(player.GUID, itemGuid) + ) + } + + private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.Reload(player.GUID, itemGuid) + ) + } + + private def handleReloadProcedure( + itemGuid: PlanetSideGUID, + obj: PlanetSideGameObject with Container, + tools: Set[Tool], + unk1: Int, + deleteFunc: Equipment => Future[Any], + modifyFunc: (AmmoBox, Int) => Unit, + messageFunc: PlanetSideGUID => Unit + ): Unit = { + tools + .filter { _.GUID == itemGuid } + .foreach { tool => + val currentMagazine : Int = tool.Magazine + val magazineSize : Int = tool.MaxMagazine + val reloadValue : Int = magazineSize - currentMagazine + if (magazineSize > 0 && reloadValue > 0) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { + case Nil => () + case x :: xs => + xs.foreach { item => deleteFunc(item.obj) } + val box = x.obj.asInstanceOf[AmmoBox] + val tailReloadValue : Int = if (xs.isEmpty) { + 0 + } + else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } + val sumReloadValue : Int = box.Capacity + tailReloadValue + val actualReloadValue = if (sumReloadValue <= reloadValue) { + deleteFunc(box) + sumReloadValue + } + else { + modifyFunc(box, reloadValue - tailReloadValue) + reloadValue + } + val finalReloadValue = actualReloadValue + currentMagazine + log.info( + s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" + ) + tool.Magazine = finalReloadValue + sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1)) + messageFunc(itemGuid) + } + } else { + //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) + } + } + } + + private def handleReloadWhenPlayer( + itemGuid: PlanetSideGUID, + obj: Player, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + ModifyAmmunition(obj)(_, _), + reloadPlayerMessages + ) + } + + private def handleReloadWhenMountable( + itemGuid: PlanetSideGUID, + obj: PlanetSideServerObject with Container, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + ModifyAmmunitionInMountable(obj)(_, _), + reloadVehicleMessages + ) + } + + //noinspection SameParameterValue + private def addShotsLanded(weaponId: Int, shots: Int): Unit = { + ops.addShotsToMap(ops.shotsLanded, weaponId, shots) + } + + private def CompileAutomatedTurretDamageData( + turret: AutomatedTurret, + owner: SourceEntry, + projectileTypeId: Long + ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { + turret.Weapons + .values + .flatMap { _.Equipment } + .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) } + .find { case (_, _, _, p) => p.ObjectId == projectileTypeId } + } + + private def HandleAIDamage( + target: PlanetSideServerObject with FactionAffinity with Vitality, + results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] + ): Unit = { + results.collect { + case (obj, tool, owner, projectileInfo) => + val angle = Vector3.Unit(target.Position - obj.Position) + val proj = new Projectile( + projectileInfo, + tool.Definition, + tool.FireMode, + None, + owner, + obj.Definition.ObjectId, + obj.Position + Vector3.z(value = 1f), + angle, + Some(angle * projectileInfo.FinalVelocity) + ) + val hitPos = target.Position + Vector3.z(value = 1f) + ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => + addShotsLanded(resprojectile.cause.attribution, shots = 1) + sessionLogic.handleDealingDamage(target, resprojectile) + } + } + } + + private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = { + val origin = projectile.Position + val distance = Vector3.Magnitude(hitPosition - origin) + continent.blockMap + .sector(hitPosition, distance) + .environmentList + .collect { case o: InteriorDoorPassage => + val door = o.door + val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints( + origin, + hitPosition, + door.Position, + door.Definition.UseRadius + 0.1f + ) + (door, intersectTest) + } + .collect { case (door, intersectionTest) if intersectionTest.nonEmpty => + (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest) + } + .minByOption { case (_, dist, _) => dist } + .foreach { case (door, _, intersects) => + val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) { + Sidedness.OutsideOf + } else { + Sidedness.InsideOf + } + projectile.WhichSide = if (intersects.size == 1) { + Sidedness.InBetweenSides(door, strictly) + } else { + strictly + } + } + } +} + +object WeaponAndProjectileLogic { + /** + * Does a line segment line intersect with a sphere?
+ * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package. + * @param start first point of the line segment + * @param end second point of the line segment + * @param center center of the sphere + * @param radius radius of the sphere + * @return list of all points of intersection, if any + * @see `Vector3.DistanceSquared` + * @see `Vector3.MagnitudeSquared` + */ + private def quickLineSphereIntersectionPoints( + start: Vector3, + end: Vector3, + center: Vector3, + radius: Float + ): Iterable[Vector3] = { + /* + Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere, + because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation. + */ + val Vector3(cx, cy, cz) = center + val Vector3(sx, sy, sz) = start + val vector = end - start + //speed our way through a quadratic equation + val (a, b) = { + val Vector3(dx, dy, dz) = vector + ( + dx * dx + dy * dy + dz * dz, + 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz)) + ) + } + val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius + val result = b * b - 4 * a * c + if (result < 0f) { + //negative, no intersection + Seq() + } else if (result < 0.00001f) { + //zero-ish, one intersection point + Seq(start - vector * (b / (2f * a))) + } else { + //positive, two intersection points + val sqrt = math.sqrt(result).toFloat + val endStart = vector / (2f * a) + Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f) + }.filter(p => Vector3.DistanceSquared(start, p) <= a) + } + /** + * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. + * The main difference from "normal" server-side explosion + * is that the owner of the projectile must be clarified explicitly. + * @see `Zone::serverSideDamage` + * @param zone where the explosion is taking place + * (`source` contains the coordinate location) + * @param source a game object that represents the source of the explosion + * @param owner who or what to accredit damage from the explosion to; + * clarifies a normal `SourceEntry(source)` accreditation + */ + private def detonateLittleBuddy( + zone: Zone, + source: PlanetSideGameObject with FactionAffinity with Vitality, + proxy: Projectile, + owner: SourceEntry + )(): Unit = { + Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position)) + } + + /** + * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. + * The main difference from "normal" server-side explosion + * is that the owner of the projectile must be clarified explicitly. + * The sub-projectiles will be the product of a normal projectile rather than a standard game object + * so a custom `source` entity must wrap around it and fulfill the requirements of the field. + * @see `Zone::explosionDamage` + * @param owner who or what to accredit damage from the explosion to + * @param explosionPosition where the explosion will be positioned in the game world + * @param source a game object that represents the source of the explosion + * @param target a game object that is affected by the explosion + * @return a `DamageInteraction` object + */ + private def littleBuddyExplosionDamage( + owner: SourceEntry, + projectileId: Long, + explosionPosition: Vector3 + ) + ( + source: PlanetSideGameObject with FactionAffinity with Vitality, + target: PlanetSideGameObject with FactionAffinity with Vitality + ): DamageInteraction = { + DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition) + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala index 187f8e8ab..e05f5ec5b 100644 --- a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala +++ b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala @@ -23,7 +23,7 @@ trait CommonSessionInterfacingFunctionality { protected def context: ActorContext - protected def sessionLogic: SessionLogic + protected def sessionLogic: SessionData protected def session: Session = sessionLogic.session diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index 4130003c1..8a2342951 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -1,10 +1,8 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.support -import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} -import net.psforever.objects.vital.etc.SuicideReason +import net.psforever.objects.sourcing.PlayerSource import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global @@ -14,1021 +12,157 @@ import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor} import net.psforever.login.WorldSession._ import net.psforever.objects._ import net.psforever.objects.avatar._ -import net.psforever.objects.ballistics._ import net.psforever.objects.ce._ import net.psforever.objects.definition._ -import net.psforever.objects.entity.WorldEntity import net.psforever.objects.equipment._ import net.psforever.objects.guid._ import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.locker.LockerContainer -import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.llu.CaptureFlag -import net.psforever.objects.serverobject.locks.IFFLock -import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.structures.{Building, WarpGate} -import net.psforever.objects.serverobject.terminals._ -import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal -import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech -import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.serverobject.turret.FacilityTurret -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} -import net.psforever.objects.vehicles.Utility.InternalTelepad +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.vehicles._ import net.psforever.objects.vital._ -import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} -import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones._ import net.psforever.packet._ -import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, SetChatFilterMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage} +import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, SetChatFilterMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage} import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game._ -import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} +import net.psforever.services.account.AccountPersistenceService import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.support.CaptureFlagManager -import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.services.{RemoverActor, Service} +import net.psforever.services.Service import net.psforever.types._ import net.psforever.util.Config -class GeneralOperations( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], - chatActor: typed.ActorRef[ChatActor.Command], - implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality { - private[support] var progressBarValue: Option[Float] = None - private[support] var accessedContainer: Option[PlanetSideGameObject with Container] = None - private[support] var recentTeleportAttempt: Long = 0 - private[support] var kitToBeUsed: Option[PlanetSideGUID] = None - // If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped. - private[support] var specialItemSlotGuid: Option[PlanetSideGUID] = None - private[support] val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap() - private var heightLast: Float = 0f - private var heightTrend: Boolean = false //up = true, down = false - private var heightHistory: Float = 0f - private[support] var progressBarUpdate: Cancellable = Default.Cancellable - private var charSavedTimer: Cancellable = Default.Cancellable +trait GeneralFunctions extends CommonSessionInterfacingFunctionality { + def ops: GeneralOperations - /* packets */ + def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit - def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage)(implicit context: ActorContext): Unit = { - val ConnectToWorldRequestMessage(_, token, majorVersion, minorVersion, revision, buildDate, _, _) = pkt - log.trace( - s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server" - ) - sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None)) - sessionLogic.accountIntermediary ! RetrieveAccountData(token) - } + def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit - def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { - val CharacterCreateRequestMessage(name, head, voice, gender, empire) = pkt - avatarActor ! AvatarActor.CreateAvatar(name, head, voice, gender, empire) - } + def handleCharacterRequest(pkt: CharacterRequestMessage): Unit - def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { - val CharacterRequestMessage(charId, action) = pkt - action match { - case CharacterRequestAction.Delete => - avatarActor ! AvatarActor.DeleteAvatar(charId.toInt) - case CharacterRequestAction.Select => - avatarActor ! AvatarActor.SelectAvatar(charId.toInt, context.self) - } - } + def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit - def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = { - val PlayerStateMessageUpstream( - avatarGuid, - pos, - vel, - yaw, - pitch, - yawUpper, - seqTime, - _, - isCrouching, - isJumping, - jumpThrust, - isCloaking, - _, - _ - )= pkt - sessionLogic.persist() - sessionLogic.turnCounterFunc(avatarGuid) - sessionLogic.updateBlockMap(player, pos) - val isMoving = WorldEntity.isMoving(vel) - val isMovingPlus = isMoving || isJumping || jumpThrust - if (isMovingPlus) { - if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { - sessionLogic.zoning.spawn.stopDeconstructing() - } else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") - } - } - fallHeightTracker(pos.z) - // if (isCrouching && !player.Crouching) { - // //dev stuff goes here - // } - player.Position = pos - player.Velocity = vel - player.Orientation = Vector3(player.Orientation.x, pitch, yaw) - player.FacingYawUpper = yawUpper - player.Crouching = isCrouching - player.Jumping = isJumping - if (isCloaking && !player.Cloaked) { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak") - } - player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking - maxCapacitorTick(jumpThrust) - if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) { - continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match { - case Some(term: Terminal with ProximityUnit) => - sessionLogic.terminals.StopUsingProximityUnit(term) - case _ => () - } - } - accessedContainer match { - // Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle. - // This can happen if PSUM arrives during the mounting process - case Some(veh: Vehicle) if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID => - if (isMoving || veh.isMoving(test = 1) || Vector3.DistanceSquared(player.Position, veh.TrunkLocation) > 9) { - val guid = player.GUID - sendResponse(UnuseItemMessage(guid, veh.GUID)) - sendResponse(UnuseItemMessage(guid, guid)) - unaccessContainer(veh) - } - case Some(container) => //just in case - if (isMovingPlus && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID)) { - // Ensure we don't close the container if the player is seated in it - val guid = player.GUID - // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first. - if (container.HasGUID) { - sendResponse(UnuseItemMessage(guid, container.GUID)) - } - sendResponse(UnuseItemMessage(guid, guid)) - unaccessContainer(container) - } - case None => () - } - val eagleEye: Boolean = canSeeReallyFar - val isNotVisible: Boolean = player.spectator || - sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || - (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlayerState( - avatarGuid, - player.Position, - player.Velocity, - yaw, - pitch, - yawUpper, - seqTime, - isCrouching, - isJumping, - jumpThrust, - isCloaking, - isNotVisible, - eagleEye - ) - ) - sessionLogic.squad.updateSquad() - if (player.death_by == -1) { - sessionLogic.kickedByAdministration() - } - player.zoneInteractions() - } + def handleChat(pkt: ChatMsg): Unit - def handleChat(pkt: ChatMsg): Unit = { - chatActor ! ChatActor.Message(pkt) - } + def handleChatFilter(pkt: SetChatFilterMessage): Unit - def handleChatFilter(pkt: SetChatFilterMessage): Unit = { - val SetChatFilterMessage(_, _, _) = pkt - } + def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit - def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = { - log.debug(s"$pkt") - sendResponse(VoiceHostKill()) - sendResponse( - ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None) - ) - } + def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit - def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = { - log.debug(s"$pkt") - sendResponse(VoiceHostKill()) - sendResponse( - ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None) - ) - } + def handleEmote(pkt: EmoteMsg): Unit - def handleEmote(pkt: EmoteMsg): Unit = { - val EmoteMsg(avatarGuid, emote) = pkt - sendResponse(EmoteMsg(avatarGuid, emote)) - } + def handleDropItem(pkt: DropItemMessage): Unit - def handleDropItem(pkt: DropItemMessage): Unit = { - val DropItemMessage(itemGuid) = pkt - (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { - case (Some(anItem: Equipment), Some(heldItem)) - if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - RemoveOldEquipmentFromInventory(player)(heldItem) - case (Some(anItem: Equipment), Some(heldItem)) - if anItem eq heldItem => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - DropEquipmentFromInventory(player)(heldItem) - case (Some(anItem: Equipment), _) - if continent.GUID(player.VehicleSeated).isEmpty => - //suppress the warning message if in a vehicle - log.warn(s"DropItem: ${player.Name} wanted to drop a ${anItem.Definition.Name}, but it wasn't at hand") - case (Some(obj), _) => - log.warn(s"DropItem: ${player.Name} wanted to drop a ${obj.Definition.Name}, but it was not equipment") - case _ => () - } - } + def handlePickupItem(pkt: PickupItemMessage): Unit - def handlePickupItem(pkt: PickupItemMessage): Unit = { - val PickupItemMessage(itemGuid, _, _, _) = pkt - sessionLogic.validObject(itemGuid, decorator = "PickupItem").collect { - case item: Equipment if player.Fit(item).nonEmpty => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - PickUpEquipmentFromGround(player)(item) - case _: Equipment => - sendResponse(ActionResultMessage.Fail(16)) //error code? - } - } + def handleObjectHeld(pkt: ObjectHeldMessage): Unit - def handleObjectHeld(pkt: ObjectHeldMessage): Unit = { - val ObjectHeldMessage(_, heldHolsters, _) = pkt - player.Actor ! PlayerControl.ObjectHeld(heldHolsters) - } + def handleAvatarJump(pkt: AvatarJumpMessage): Unit - def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { - val AvatarJumpMessage(_) = pkt - avatarActor ! AvatarActor.ConsumeStamina(10) - avatarActor ! AvatarActor.SuspendStaminaRegeneration(2.5 seconds) - } + def handleZipLine(pkt: ZipLineMessage): Unit - def handleZipLine(pkt: ZipLineMessage): Unit = { - val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt - continent.zipLinePaths.find(x => x.PathId == pathId) match { - case Some(path) if path.IsTeleporter => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - val endPoint = path.ZipLinePoints.last - sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos)) - //todo: send to zone to show teleport animation to all clients - sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None))) - case Some(_) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") - action match { - case 0 => - //travel along the zipline in the direction specified - sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos)) - case 1 => - //disembark from zipline at destination! - sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) - case 2 => - //get off by force - sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) - case _ => - log.warn( - s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}" - ) - } - case _ => - log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}") - } - } + def handleRequestDestroy(pkt: RequestDestroyMessage): Unit - def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = { - val RequestDestroyMessage(objectGuid) = pkt - //make sure this is the correct response for all cases - sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match { - case Some(vehicle: Vehicle) => - /* line 1a: player is admin (and overrules other access requirements) */ - /* line 1b: vehicle and player (as the owner) acknowledge each other */ - /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */ - /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ - if ( - (session.account.gm || - (player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) || - (player.Faction == vehicle.Faction && - (vehicle.Definition.CanBeOwned.nonEmpty && - (vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) && - (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied)) - ) { - vehicle.Actor ! Vehicle.Deconstruct() - //log.info(s"RequestDestroy: vehicle $vehicle") - } else { - log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it") - } + def handleMoveItem(pkt: MoveItemMessage): Unit - case Some(obj: Projectile) => - if (!obj.isResolved) { - obj.Miss() - } - continent.Projectile ! ZoneProjectile.Remove(objectGuid) + def handleLootItem(pkt: LootItemMessage): Unit - case Some(obj: BoomerTrigger) => - if (findEquipmentToDelete(objectGuid, obj)) { - continent.GUID(obj.Companion) match { - case Some(boomer: BoomerDeployable) => - boomer.Trigger = None - boomer.Actor ! Deployable.Deconstruct() - case Some(thing) => - log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") - case None => () - } - } + def handleAvatarImplant(pkt: AvatarImplantMessage): Unit - case Some(obj: Deployable) => - if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) { - obj.Actor ! Deployable.Deconstruct() - } else { - log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it") - } + def handleUseItem(pkt: UseItemMessage): Unit - case Some(obj: Equipment) => - findEquipmentToDelete(objectGuid, obj) + def handleUnuseItem(pkt: UnuseItemMessage): Unit - case Some(thing) => - log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") + def handleDeployObject(pkt: DeployObjectMessage): Unit - case None => () - } - } + def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit - def handleMoveItem(pkt: MoveItemMessage): Unit = { - val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt - ( - continent.GUID(sourceGuid), - continent.GUID(destinationGuid), - sessionLogic.validObject(itemGuid, decorator = "MoveItem") - ) match { - case ( - Some(source: PlanetSideServerObject with Container), - Some(destination: PlanetSideServerObject with Container), - Some(item: Equipment) - ) => - ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest)) - case (None, _, _) => - log.error( - s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object" - ) - case (_, None, _) => - log.error( - s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object" - ) - case (_, _, None) => () - case _ => - log.error( - s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered" - ) - } - } + def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit - def handleLootItem(pkt: LootItemMessage): Unit = { - val LootItemMessage(itemGuid, targetGuid) = pkt - (sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match { - case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => - //figure out the source - ( - { - val findFunc: PlanetSideServerObject with Container => Option[ - (PlanetSideServerObject with Container, Option[Int]) - ] = findInLocalContainer(itemGuid) - findFunc(player.avatar.locker) - .orElse(findFunc(player)) - .orElse(accessedContainer match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) - }, - destination.Fit(item) - ) match { - case (Some((source, Some(_))), Some(dest)) => - ContainableMoveItem(player.Name, source, destination, item, dest) - case (None, _) => - log.error(s"LootItem: ${player.Name} can not find where $item is put currently") - case (_, None) => - log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination") - case _ => - log.error( - s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered" - ) - } - case (Some(obj), _) => - log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}") - case (None, _) => () - case (_, None) => - log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid") - } - } + def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit - def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = { - val AvatarImplantMessage(_, action, slot, status) = pkt - if (action == ImplantAction.Activation) { - if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { - //do not activate; play deactivation sound instead - sessionLogic.zoning.spawn.stopDeconstructing() - avatar.implants(slot).collect { - case implant if implant.active => - avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) - case implant => - sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2)) - } - } else { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") - avatar.implants(slot) match { - case Some(implant) => - if (status == 1) { - avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType) - } else { - avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) - } - case _ => - log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot") - } - } - } - } + def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit - def handleUseItem(pkt: UseItemMessage): Unit = { - val equipment = findContainedEquipment(pkt.item_used_guid) match { - case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) => - sessionLogic.shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption - case (Some(_), a) => - a.headOption - case _ => - None - } - sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { - case Some(door: Door) => - handleUseDoor(door, equipment) - case Some(resourceSilo: ResourceSilo) => - handleUseResourceSilo(resourceSilo, equipment) - case Some(panel: IFFLock) => - handleUseGeneralEntity(panel, equipment) - case Some(obj: Player) => - handleUsePlayer(obj, equipment, pkt) - case Some(locker: Locker) => - handleUseLocker(locker, equipment, pkt) - case Some(gen: Generator) => - handleUseGeneralEntity(gen, equipment) - case Some(mech: ImplantTerminalMech) => - handleUseGeneralEntity(mech, equipment) - case Some(captureTerminal: CaptureTerminal) => - handleUseCaptureTerminal(captureTerminal, equipment) - case Some(obj: FacilityTurret) => - handleUseFacilityTurret(obj, equipment, pkt) - case Some(obj: Vehicle) => - handleUseVehicle(obj, equipment, pkt) - case Some(terminal: Terminal) => - handleUseTerminal(terminal, equipment, pkt) - case Some(obj: SpawnTube) => - handleUseSpawnTube(obj, equipment) - case Some(obj: SensorDeployable) => - handleUseGeneralEntity(obj, equipment) - case Some(obj: TurretDeployable) => - handleUseGeneralEntity(obj, equipment) - case Some(obj: TrapDeployable) => - handleUseGeneralEntity(obj, equipment) - case Some(obj: ShieldGeneratorDeployable) => - handleUseGeneralEntity(obj, equipment) - case Some(obj: TelepadDeployable) => - handleUseTelepadDeployable(obj, equipment, pkt) - case Some(obj: Utility.InternalTelepad) => - handleUseInternalTelepad(obj, pkt) - case Some(obj: CaptureFlag) => - handleUseCaptureFlag(obj) - case Some(_: WarpGate) => - handleUseWarpGate(equipment) - case Some(obj) => - handleUseDefaultEntity(obj, equipment) - case None => () - } - } + def handleGenericAction(pkt: GenericActionMessage): Unit - def handleUnuseItem(pkt: UnuseItemMessage): Unit = { - val UnuseItemMessage(_, objectGuid) = pkt - sessionLogic.validObject(objectGuid, decorator = "UnuseItem") match { - case Some(obj: Player) => - unaccessContainer(obj) - sessionLogic.zoning.spawn.TryDisposeOfLootedCorpse(obj) - case Some(obj: Container) => - // Make sure we don't unload the contents of the vehicle the player is seated in - // An example scenario of this would be closing the trunk contents when rearming at a landing pad - if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) { - unaccessContainer(obj) - } - case _ => () - } - } + def handleGenericCollision(pkt: GenericCollisionMsg): Unit - def handleDeployObject(pkt: DeployObjectMessage): Unit = { - val DeployObjectMessage(guid, _, pos, orient, _) = pkt - player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid).flatMap { slot => slot.Equipment } match { - case Some(obj: ConstructionItem) => - val ammoType = obj.AmmoType match { - case DeployedItem.portable_manned_turret => GlobalDefinitions.PortableMannedTurret(player.Faction).Item - case dtype => dtype - } - log.info(s"${player.Name} is constructing a $ammoType deployable") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val dObj: Deployable = Deployables.Make(ammoType)() - dObj.Position = pos - dObj.Orientation = orient - dObj.WhichSide = player.WhichSide - dObj.Faction = player.Faction - dObj.AssignOwnership(player) - val tasking: TaskBundle = dObj match { - case turret: TurretDeployable => - GUIDTask.registerDeployableTurret(continent.GUID, turret) - case _ => - GUIDTask.registerObject(continent.GUID, dObj) - } - TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj))) - case Some(obj) => - log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") - case None => - log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!") - } - } + def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit - def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = { - val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt - sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match { - case Some(vehicle: Vehicle) if player.avatar.vehicle.contains(vehicle.GUID) => - vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue) - case Some(vehicle: Vehicle) => - log.warn(s"PlanetsideAttribute: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it") - // Cosmetics options - case Some(_: Player) if attributeType == 106 => - avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue)) - case Some(obj) => - log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}") - case _ => () - } - } + def handleBugReport(pkt: PlanetSideGamePacket): Unit - def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = { - val GenericObjectActionMessage(objectGuid, code) = pkt - sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match { - case Some(vehicle: Vehicle) - if vehicle.OwnerName.contains(player.Name) => - vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID)) + def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit - case Some(tool: Tool) => - if (code == 35 && - (tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser")) - ) { - //maelstrom primary fire mode discharge (no target) - //aphelion_laser discharge (no target) - sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) - } else { - sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match { - case Some(vehicle: Vehicle) - if vehicle.OwnerName.contains(player.Name) => - vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool)) - case _ => - } - } - case _ => - log.info(s"${player.Name} - $pkt") - } - } + def handleBattleplan(pkt: BattleplanMessage): Unit - def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = { - val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt - sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match { - case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => - sessionLogic.shooting.FindContainedWeapon match { - case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) => - vehicle.Actor ! SpecialEmp.Burst() - case _ => () - } - case _ => - log.info(s"${player.Name} - $pkt") - } - } + def handleBindPlayer(pkt: BindPlayerMessage): Unit - def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = { - val GenericObjectStateMsg(_, _) = pkt - log.info(s"${player.Name} - $pkt") - } + def handleCreateShortcut(pkt: CreateShortcutMessage): Unit - def handleGenericAction(pkt: GenericActionMessage): Unit = { - val GenericActionMessage(action) = pkt - if (player == null) { - if (action == GenericAction.AwayFromKeyboard_RCV) { - log.debug("GenericObjectState: AFK state reported during login") - } - } else { - val (toolOpt, definition) = player.Slot(0).Equipment match { - case Some(tool: Tool) => - (Some(tool), tool.Definition) - case _ => - (None, GlobalDefinitions.bullet_9mm) - } - action match { - case GenericAction.DropSpecialItem => - dropSpecialSlotItem() - case GenericAction.MaxAnchorsExtend_RCV => - log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground") - player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlanetsideAttribute(player.GUID, 19, 1) - ) - definition match { - case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster => - val tool = toolOpt.get - tool.ToFireMode = 1 - sendResponse(ChangeFireModeMessage(tool.GUID, 1)) - case GlobalDefinitions.trhev_pounder => - val tool = toolOpt.get - val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 } - else { 4 } - tool.ToFireMode = convertFireModeIndex - sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) - case _ => - log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}") - } - case GenericAction.MaxAnchorsRelease_RCV => - log.info(s"${player.Name} has released the anchors") - player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlanetsideAttribute(player.GUID, 19, 0) - ) - definition match { - case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster => - val tool = toolOpt.get - tool.ToFireMode = 0 - sendResponse(ChangeFireModeMessage(tool.GUID, 0)) - case GlobalDefinitions.trhev_pounder => - val tool = toolOpt.get - val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 } - tool.ToFireMode = convertFireModeIndex - sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) - case _ => - log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}") - } - case GenericAction.MaxSpecialEffect_RCV => - if (player.ExoSuit == ExoSuitType.MAX) { - toggleMaxSpecialState(enable = true) - } else { - log.warn(s"GenericActionMessage: ${player.Name} can't handle MAX special effect") - } - case GenericAction.StopMaxSpecialEffect_RCV => - if (player.ExoSuit == ExoSuitType.MAX) { - player.Faction match { - case PlanetSideEmpire.NC => - toggleMaxSpecialState(enable = false) - case _ => - log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability") - } - } else { - log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect") - } - case GenericAction.AwayFromKeyboard_RCV => - log.info(s"${player.Name} is AFK") - AvatarActor.savePlayerLocation(player) - displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min - player.AwayFromKeyboard = true - case GenericAction.BackInGame_RCV => - log.info(s"${player.Name} is back") - player.AwayFromKeyboard = false - renewCharSavedTimer( - Config.app.game.savedMsg.renewal.fixed, - Config.app.game.savedMsg.renewal.variable - ) - case GenericAction.LookingForSquad_RCV => //Looking For Squad ON - if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { - avatarActor ! AvatarActor.SetLookingForSquad(true) - } - case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF - if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { - avatarActor ! AvatarActor.SetLookingForSquad(false) - } - case _ => - log.warn(s"GenericActionMessage: ${player.Name} can't handle $action") - } - } - } + def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit - 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") - } - } + def handleFriendRequest(pkt: FriendsRequest): Unit - def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { - val GenericCollisionMsg(ctype, p, _, ppos, pv, t, _, tpos, tv, _, _, _) = pkt - val fallHeight = { - if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { - if (heightTrend) { - val fall = heightLast - heightHistory - heightHistory = heightLast - fall - } - else { - val fall = heightHistory - heightLast - heightLast = heightHistory - fall - } - } else { - 0f - } - } - val (target1, target2, bailProtectStatus, velocity) = (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { - case (CollisionIs.OfInfantry, out @ Some(user: Player)) - if user == player => - val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection - player.BailProtection = false - val v = if (player.avatar.implants.exists { - case Some(implant) => implant.definition.implantType == ImplantType.Surge && implant.active - case _ => false - }) { - Vector3.Zero - } else { - pv - } - (out, None, bailStatus, v) - case (CollisionIs.OfGroundVehicle, out @ Some(v: Vehicle)) - if v.Seats(0).occupant.contains(player) => - val bailStatus = v.BailProtection - v.BailProtection = false - (out, sessionLogic.validObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv) - case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) - if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => - (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) - case (CollisionIs.BetweenThings, _) => - log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") - (None, None, false, Vector3.Zero) - case _ => - (None, None, false, Vector3.Zero) - } - val curr = System.currentTimeMillis() - (target1, t, target2) match { - case (None, _, _) => () + def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit - case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => - if (updateCollisionHistoryForTarget(us, curr)) { - if (!bailProtectStatus) { - sessionLogic.handleDealingDamage( - us, - DamageInteraction( - SourceEntry(us), - CollisionReason(velocity, fallHeight, us.DamageModel), - ppos - ) - ) - } - } + def handleActionCancel(pkt: ActionCancelMessage): Unit - case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) => - collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) + def handleTrade(pkt: TradeMessage): Unit - case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty => - collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) + def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit - case ( - Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, - Some(victim: PlanetSideServerObject with Vitality with FactionAffinity) - ) => - if (updateCollisionHistoryForTarget(victim, curr)) { - val usSource = SourceEntry(us) - val victimSource = SourceEntry(victim) - //we take damage from the collision - if (!bailProtectStatus) { - performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv) - } - //get dealt damage from our own collision (no protection) - collisionHistory.put(us.Actor, curr) - performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity) - } + def handleObjectDetected(pkt: ObjectDetectedMessage): Unit - case _ => () - } - } + def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit - def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { - val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt - avatarActor ! AvatarActor.AddFirstTimeEvent(eventName) - } - - def handleBugReport(pkt: PlanetSideGamePacket): Unit = { - val BugReportMessage( - _/*versionMajor*/, - _/*versionMinor*/, - _/*versionDate*/, - _/*bugType*/, - _/*repeatable*/, - _/*location*/, - _/*zone*/, - _/*pos*/, - _/*summary*/, - _/*desc*/ - ) = pkt - log.warn(s"${player.Name} filed a bug report - it might be something important") - log.debug(s"$pkt") - } - - def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = { - val FacilityBenefitShieldChargeRequestMessage(_) = pkt - val vehicleGuid = player.VehicleSeated - continent - .GUID(vehicleGuid) - .foreach { - case obj: Vehicle if !obj.Destroyed && obj.MountedIn.isEmpty => // vehicle will try to charge even if destroyed & cargo vehicles need to be excluded - obj.Actor ! CommonMessages.ChargeShields( - 15, - Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius)) - ) - case obj: Vehicle if obj.MountedIn.nonEmpty => - false - case _ if vehicleGuid.nonEmpty => - log.warn( - s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find vehicle ${vehicleGuid.get.guid} in zone ${continent.id}" - ) - case _ => - log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in a vehicle") - } - } - - def handleBattleplan(pkt: BattleplanMessage): Unit = { - val BattleplanMessage(_, name, _, _) = pkt - val lament: String = s"$name has a brilliant idea that no one will ever see" - log.info(lament) - log.debug(s"Battleplan: $lament - $pkt") - } - - def handleBindPlayer(pkt: BindPlayerMessage): Unit = { - val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt - } - - def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { - val CreateShortcutMessage(_, slot, shortcutOpt) = pkt - shortcutOpt match { - case Some(shortcut) => - avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut) - case None => - avatarActor ! AvatarActor.RemoveShortcut(slot - 1) - } - } - - def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { - val ChangeShortcutBankMessage(_, _) = pkt - } - - def handleFriendRequest(pkt: FriendsRequest): Unit = { - val FriendsRequest(action, name) = pkt - avatarActor ! AvatarActor.MemberListRequest(action, name) - } - - def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = { - val InvalidTerrainMessage(_, vehicleGuid, alert, _) = pkt - (continent.GUID(vehicleGuid), continent.GUID(player.VehicleSeated)) match { - case (Some(packetVehicle: Vehicle), Some(playerVehicle: Vehicle)) if packetVehicle eq playerVehicle => - if (alert == TerrainCondition.Unsafe) { - log.info(s"${player.Name}'s ${packetVehicle.Definition.Name} is approaching terrain unsuitable for idling") - } - case (Some(packetVehicle: Vehicle), Some(_: Vehicle)) => - if (alert == TerrainCondition.Unsafe) { - log.info(s"${packetVehicle.Definition.Name}@${packetVehicle.GUID} is approaching terrain unsuitable for idling, but is not ${player.Name}'s vehicle") - } - case (Some(_: Vehicle), _) => - log.warn(s"InvalidTerrain: ${player.Name} is not seated in a(ny) vehicle near unsuitable terrain") - case (Some(packetThing), _) => - log.warn(s"InvalidTerrain: ${player.Name} thinks that ${packetThing.Definition.Name}@${packetThing.GUID} is near unsuitable terrain") - case _ => - log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicleGuid that can not be found") - } - } - - def handleActionCancel(pkt: ActionCancelMessage): Unit = { - val ActionCancelMessage(_, _, _) = pkt - progressBarUpdate.cancel() - progressBarValue = None - } - - def handleTrade(pkt: TradeMessage): Unit = { - val TradeMessage(trade) = pkt - log.trace(s"${player.Name} wants to trade for some reason - $trade") - } - - def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { - val DisplayedAwardMessage(_, ribbon, bar) = pkt - log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon") - avatarActor ! AvatarActor.SetRibbon(ribbon, bar) - } - - def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { - val ObjectDetectedMessage(_, _, _, targets) = pkt - sessionLogic.shooting.FindWeapon.foreach { - case weapon if weapon.Projectile.AutoLock => - //projectile with auto-lock instigates a warning on the target - val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets) - val mode = 7 + (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) - detectedTargets.foreach { target => - continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode)) - } - case _ => () - } - } - - def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { - val TargetingImplantRequest(list) = pkt - val targetInfo: List[TargetInfo] = list.flatMap { x => - continent.GUID(x.target_guid) match { - case Some(player: Player) => - val health = player.Health.toFloat / player.MaxHealth - val armor = if (player.MaxArmor > 0) { - player.Armor.toFloat / player.MaxArmor - } else { - 0 - } - Some(TargetInfo(player.GUID, health, armor)) - case _ => - log.warn( - s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player" - ) - None - } - } - sendResponse(TargetingInfoMessage(targetInfo)) - } - - def handleHitHint(pkt: HitHint): Unit = { - val HitHint(_, _) = pkt - } + def handleHitHint(pkt: HitHint): Unit /* messages */ - def handleSetAvatar(avatar: Avatar): Unit = { - session = session.copy(avatar = avatar) - if (session.player != null) { - session.player.avatar = avatar - } - LivePlayerList.Update(avatar.id, avatar) - } + def handleSetAvatar(avatar: Avatar): Unit - def handleReceiveAccountData(account: Account): Unit = { - log.trace(s"ReceiveAccountData $account") - session = session.copy(account = account) - avatarActor ! AvatarActor.SetAccount(account) - } + def handleReceiveAccountData(account: Account): Unit - def handleUseCooldownRenew: BasicDefinition => Unit = { - case _: KitDefinition => kitToBeUsed = None - case _ => () - } + def handleUseCooldownRenew: BasicDefinition => Unit - def handleAvatarResponse(avatar: Avatar): Unit = { - session = session.copy(avatar = avatar) - sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id) - } + def handleAvatarResponse(avatar: Avatar): Unit - def handleSetSpeed(speed: Float): Unit = { - session = session.copy(speed = speed) - } + def handleSetSpeed(speed: Float): Unit - def handleSetFlying(flying: Boolean): Unit = { - session = session.copy(flying = flying) - } + def handleSetFlying(flying: Boolean): Unit - def handleSetSpectator(spectator: Boolean): Unit = { - session.player.spectator = spectator - } + def handleSetSpectator(spectator: Boolean): Unit - def handleKick(player: Player, time: Option[Long]): Unit = { - administrativeKick(player) - sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time) - } + def handleKick(player: Player, time: Option[Long]): Unit - def handleSilenced(isSilenced: Boolean): Unit = { - player.silenced = isSilenced - } + def handleSilenced(isSilenced: Boolean): Unit +} - /* supporting functions */ +class GeneralOperations( + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], + val chatActor: typed.ActorRef[ChatActor.Command], + implicit val context: ActorContext + ) extends CommonSessionInterfacingFunctionality { + private[session] var progressBarValue: Option[Float] = None + private[session] var accessedContainer: Option[PlanetSideGameObject with Container] = None + private[session] var recentTeleportAttempt: Long = 0 + private[session] var kitToBeUsed: Option[PlanetSideGUID] = None + // If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped. + private[session] var specialItemSlotGuid: Option[PlanetSideGUID] = None + private[session] val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap() + private[session] var heightLast: Float = 0f + private[session] var heightTrend: Boolean = false //up = true, down = false + private[session] var heightHistory: Float = 0f + private[session] var progressBarUpdate: Cancellable = Default.Cancellable + private var charSavedTimer: Cancellable = Default.Cancellable /** * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. @@ -1055,295 +189,6 @@ class GeneralOperations( } } - private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - val distance: Float = math.max( - Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, - door.Definition.initialOpeningDistance - ) - door.Actor ! CommonMessages.Use(player, Some(distance)) - case _ => - door.Actor ! CommonMessages.Use(player) - } - } - - private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - (continent.GUID(player.VehicleSeated), equipment) match { - case (Some(vehicle: Vehicle), Some(item)) - if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => - resourceSilo.Actor ! CommonMessages.Use(player, equipment) - case _ => - resourceSilo.Actor ! CommonMessages.Use(player) - } - } - - private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - if (obj.isBackpack) { - if (equipment.isEmpty) { - log.info(s"${player.Name} is looting the corpse of ${obj.Name}") - sendResponse(msg) - accessContainer(obj) - } - } else if (!msg.unk3 && player.isAlive) { //potential kit use - (continent.GUID(msg.item_used_guid), kitToBeUsed) match { - case (Some(kit: Kit), None) => - kitToBeUsed = Some(msg.item_used_guid) - player.Actor ! CommonMessages.Use(player, Some(kit)) - case (Some(_: Kit), Some(_)) | (None, Some(_)) => - //a kit is already queued to be used; ignore this request - sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None)) - case (Some(item), _) => - log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") - case (None, None) => - log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") } - } else if (msg.object_id == ObjectClass.avatar && msg.unk3) { - equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => - obj.Actor ! CommonMessages.Use(player, equipment) - - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - obj.Actor ! CommonMessages.Use(player, equipment) - case _ => () - } - } - } - - private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(locker, item) - case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => - log.info(s"${player.Name} is accessing a locker") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val playerLocker = player.avatar.locker - sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) - accessContainer(playerLocker) - case _ => () - } - } - - private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(captureTerminal, item) - case _ if specialItemSlotGuid.nonEmpty => - continent.GUID(specialItemSlotGuid) match { - case Some(llu: CaptureFlag) => - if (llu.Target.GUID == captureTerminal.Owner.GUID) { - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) - } else { - log.info( - s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}" - ) - } - case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") - } - case _ => () - } - } - - private def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment.foreach { item => - sendUseGeneralEntityMessage(obj, item) - obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path - } - } - - private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(obj, item) - case None if player.Faction == obj.Faction => - //access to trunk - if ( - obj.AccessingTrunk.isEmpty && - (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid - .contains(player.GUID)) - ) { - log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.AccessingTrunk = player.GUID - accessContainer(obj) - sendResponse(msg) - } - case _ => () - } - } - - private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(terminal, item) - case None - if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || - terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => - val tdef = terminal.Definition - if (tdef.isInstanceOf[MatrixTerminalDefinition]) { - //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sendResponse( - BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) - ) - } else if ( - tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || - tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal - ) { - findLocalVehicle match { - case Some(vehicle) => - log.info( - s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}" - ) - sendResponse(msg) - sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId)) - case None => - log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") - } - } else if (tdef == GlobalDefinitions.teleportpad_terminal) { - //explicit request - log.info(s"${player.Name} is purchasing a router telepad") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - terminal.Actor ! Terminal.Request( - player, - ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) - ) - } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { - //explicit request - log.info(s"${player.Name} is purchasing a targeting laser") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - terminal.Actor ! Terminal.Request( - player, - ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0)) - ) - } else { - log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sendResponse(msg) - } - case _ => () - } - } - - private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(obj, item) - case None if player.Faction == obj.Faction => - //deconstruction - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sessionLogic.actionsToCancel() - sessionLogic.terminals.CancelAllProximityUnits() - sessionLogic.zoning.spawn.startDeconstructing(obj) - case _ => () - } - } - - private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - if (equipment.isEmpty) { - (continent.GUID(obj.Router) match { - case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) - case Some(vehicle) => Some(vehicle, None) - case None => None - }) match { - case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - player.WhichSide = vehicle.WhichSide - useRouterTelepadSystem( - router = vehicle, - internalTelepad = util, - remoteTelepad = obj, - src = obj, - dest = util - ) - case Some((vehicle: Vehicle, None)) => - log.error( - s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" - ) - case Some((o, _)) => - log.error( - s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" - ) - obj.Actor ! Deployable.Deconstruct() - case _ => () - } - } - } - - private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = { - continent.GUID(obj.Telepad) match { - case Some(pad: TelepadDeployable) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - player.WhichSide = pad.WhichSide - useRouterTelepadSystem( - router = obj.Owner.asInstanceOf[Vehicle], - internalTelepad = obj, - remoteTelepad = pad, - src = obj, - dest = pad - ) - case Some(o) => - log.error( - s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" - ) - case None => () - } - } - - private def handleUseCaptureFlag(obj: CaptureFlag): Unit = { - // LLU can normally only be picked up the faction that owns it - specialItemSlotGuid match { - case None if obj.Faction == player.Faction => - specialItemSlotGuid = Some(obj.GUID) - player.Carrying = SpecialCarry.CaptureFlag - continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) - case None => - log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}") - case Some(guid) if guid != obj.GUID => - // Ignore duplicate pickup requests - log.warn( - s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid" - ) - case _ => () - } - } - - private def handleUseWarpGate(equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - (continent.GUID(player.VehicleSeated), equipment) match { - case (Some(vehicle: Vehicle), Some(item)) - if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => - vehicle.Actor ! CommonMessages.Use(player, equipment) - case _ => () - } - } - - private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { - equipment.foreach { item => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(item)) - } - } - - private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(equipment)) - } - - private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - equipment match { - case Some(item) - if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => () - case _ => - log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") - } - } - def dropSpecialSlotItem(): Unit = { specialItemSlotGuid.foreach { guid => specialItemSlotGuid = None @@ -1456,6 +301,10 @@ class GeneralOperations( } } + /** + * For whatever container the character considers itself trying to access, + * initiate protocol to "access" it. + */ def accessContainer(container: Container): Unit = { container match { case v: Vehicle => @@ -1470,6 +319,9 @@ class GeneralOperations( } } + /** + * For the target container, initiate protocol to "access" it. + */ private def accessGenericContainer(container: PlanetSideServerObject with Container): Unit = { accessedContainer = Some(container) displayContainerContents(container.GUID, container.Inventory.Items) @@ -1539,7 +391,7 @@ class GeneralOperations( } /** - * For whatever conatiner the character considers itself accessing, + * For whatever container the character considers itself accessing, * initiate protocol to release it from "access". */ def unaccessContainer(): Unit = { @@ -1669,31 +521,6 @@ class GeneralOperations( } } - /** - * Runs `FindContainedEquipment` but ignores the `Container` object output. - * @return an `Equipment` object - */ - def findEquipment(): Set[Equipment] = findContainedEquipment()._2 - - /** - * Runs `FindContainedEquipment` but ignores the `Container` object output - * and only discovers `Equipment` with the specified global unique identifier number. - * @return an `Equipment` object - */ - def findEquipment(guid: PlanetSideGUID): Option[Equipment] = findEquipment().find { _.GUID == guid } - - /** - * Get the current `Vehicle` object that the player is riding/driving. - * The vehicle must be found solely through use of `player.VehicleSeated`. - * @return the vehicle - */ - def findLocalVehicle: Option[Vehicle] = { - continent.GUID(player.VehicleSeated) match { - case Some(obj: Vehicle) => Some(obj) - case _ => None - } - } - /** * Drop an `Equipment` item onto the ground. * Specifically, instruct the item where it will appear, @@ -1785,63 +612,16 @@ class GeneralOperations( } /** - * A simple object searching algorithm that is limited to containers currently known and accessible by the player. - * If all relatively local containers are checked and the object is not found, - * the player's locker inventory will be checked, and then - * the game environment (items on the ground) will be checked too. - * If the target object is discovered, it is removed from its current location and is completely destroyed. - * @see `RequestDestroyMessage` - * @see `Zone.ItemIs.Where` - * @param objectGuid the target object's globally unique identifier; - * it is not expected that the object will be unregistered, but it is also not gauranteed - * @param obj the target object - * @return `true`, if the target object was discovered and removed; - * `false`, otherwise + * Animate(?) a player using a fully-linked Router teleportation system. + * In reality, this seems to do nothing visually? + * @param playerGUID the player being teleported + * @param srcGUID the origin of the teleportation + * @param destGUID the destination of the teleportation */ - def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = { - val findFunc - : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = - findInLocalContainer(objectGuid) - - findFunc(player) - .orElse(accessedContainer match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) - .orElse(findLocalVehicle match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) match { - case Some((parent, Some(_))) => - obj.Position = Vector3.Zero - RemoveOldEquipmentFromInventory(parent)(obj) - true - case _ if player.avatar.locker.Inventory.Remove(objectGuid) => - sendResponse(ObjectDeleteMessage(objectGuid, 0)) - true - case _ if continent.EquipmentOnGround.contains(obj) => - obj.Position = Vector3.Zero - continent.Ground ! Zone.Ground.RemoveItem(objectGuid) - continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) - true - case _ => - Zone.EquipmentIs.Where(obj, objectGuid, continent) match { - case None => - true - case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID => - TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) - true - case Some(Zone.EquipmentIs.Orphaned()) => - true - case _ => - log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") - false - } - } + def useRouterTelepadEffect(playerGUID: PlanetSideGUID, srcGUID: PlanetSideGUID, destGUID: PlanetSideGUID): Unit = { + sendResponse(PlanetsideAttributeMessage(playerGUID, 64, 1)) //what does this do? + sendResponse(GenericObjectActionMessage(srcGUID, 31)) + sendResponse(GenericObjectActionMessage(destGUID, 32)) } /** @@ -1868,127 +648,6 @@ class GeneralOperations( } } - /** - * A player uses a fully-linked Router teleportation system. - * @param router the Router vehicle - * @param internalTelepad the internal telepad within the Router vehicle - * @param remoteTelepad the remote telepad that is currently associated with this Router - * @param src the origin of the teleportation (where the player starts) - * @param dest the destination of the teleportation (where the player is going) - */ - def useRouterTelepadSystem( - router: Vehicle, - internalTelepad: InternalTelepad, - remoteTelepad: TelepadDeployable, - src: PlanetSideGameObject with TelepadLike, - dest: PlanetSideGameObject with TelepadLike - ): Unit = { - val time = System.currentTimeMillis() - if ( - time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && - internalTelepad.Active && - remoteTelepad.Active - ) { - val pguid = player.GUID - val sguid = src.GUID - val dguid = dest.GUID - sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) - useRouterTelepadEffect(pguid, sguid, dguid) - continent.LocalEvents ! LocalServiceMessage( - continent.id, - LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid) - ) - val vSource = VehicleSource(router) - val zoneNumber = continent.Number - player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber)) - player.Position = dest.Position - player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber)) - } else { - log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") - } - recentTeleportAttempt = time - } - - /** - * Animate(?) a player using a fully-linked Router teleportation system. - * In reality, this seems to do nothing visually? - * @param playerGUID the player being teleported - * @param srcGUID the origin of the teleportation - * @param destGUID the destination of the teleportation - */ - def useRouterTelepadEffect(playerGUID: PlanetSideGUID, srcGUID: PlanetSideGUID, destGUID: PlanetSideGUID): Unit = { - sendResponse(PlanetsideAttributeMessage(playerGUID, 64, 1)) //what does this do? - sendResponse(GenericObjectActionMessage(srcGUID, 31)) - sendResponse(GenericObjectActionMessage(destGUID, 32)) - } - - def maxCapacitorTick(jumpThrust: Boolean): Unit = { - if (player.ExoSuit == ExoSuitType.MAX) { - val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 - player.CapacitorState match { - case CapacitorStateType.Idle => maxCapacitorTickIdle(activate) - case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate) - case CapacitorStateType.ChargeDelay => maxCapacitorTickChargeDelay(activate) - case CapacitorStateType.Charging => maxCapacitorTickCharging(activate) - } - } else if (player.CapacitorState != CapacitorStateType.Idle) { - player.CapacitorState = CapacitorStateType.Idle - } - } - - private def maxCapacitorTickIdle(activate: Boolean): Unit = { - if (activate) { - player.CapacitorState = CapacitorStateType.Discharging - //maxCapacitorTickDischarging(activate) - } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { - player.CapacitorState = CapacitorStateType.ChargeDelay - maxCapacitorTickChargeDelay(activate) - } - } - - private def maxCapacitorTickDischarging(activate: Boolean): Unit = { - if (activate) { - val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000 - val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff - player.Capacitor -= drainAmount - sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt)) - } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { - if (player.Faction != PlanetSideEmpire.VS) { - toggleMaxSpecialState(enable = false) - } - player.CapacitorState = CapacitorStateType.ChargeDelay - maxCapacitorTickChargeDelay(activate) - } else { - player.CapacitorState = CapacitorStateType.Idle - } - } - - private def maxCapacitorTickChargeDelay(activate: Boolean): Unit = { - if (activate) { - player.CapacitorState = CapacitorStateType.Discharging - //maxCapacitorTickDischarging(activate) - } else if (player.Capacitor == player.ExoSuitDef.MaxCapacitor) { - player.CapacitorState = CapacitorStateType.Idle - } else if (System.currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis) { - player.CapacitorState = CapacitorStateType.Charging - //maxCapacitorTickCharging(activate) - } - } - - private def maxCapacitorTickCharging(activate: Boolean): Unit = { - if (activate) { - player.CapacitorState = CapacitorStateType.Discharging - //maxCapacitorTickDischarging(activate) - } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { - val timeDiff = (System.currentTimeMillis() - player.CapacitorLastChargedMillis).toFloat / 1000 - val chargeAmount = player.ExoSuitDef.CapacitorRechargePerSecond * timeDiff - player.Capacitor += chargeAmount - sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt)) - } else { - player.CapacitorState = CapacitorStateType.Idle - } - } - def toggleMaxSpecialState(enable: Boolean): Unit = { if (player.ExoSuit == ExoSuitType.MAX) { if (enable && player.UsingSpecial == SpecialExoSuitDefinition.Mode.Normal) { @@ -2087,59 +746,6 @@ class GeneralOperations( sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None)) } - private def updateCollisionHistoryForTarget( - target: PlanetSideServerObject with Vitality with FactionAffinity, - curr: Long - ): Boolean = { - collisionHistory.get(target.Actor) match { - case Some(lastCollision) if curr - lastCollision <= 1000L => - false - case _ => - collisionHistory.put(target.Actor, curr) - true - } - } - - private def collisionBetweenVehicleAndFragileDeployable( - vehicle: Vehicle, - vehiclePosition: Vector3, - smallDeployable: Deployable, - smallDeployablePosition: Vector3, - velocity: Vector3, - fallHeight: Float, - collisionTime: Long - ): Unit = { - if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) { - val smallDeployableSource = SourceEntry(smallDeployable) - //vehicle takes damage from the collision (ignore bail protection in this case) - performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity) - //deployable gets absolutely destroyed - collisionHistory.put(vehicle.Actor, collisionTime) - sessionLogic.handleDealingDamage( - smallDeployable, - DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition) - ) - } - } - - private def performCollisionWithSomethingDamage( - target: PlanetSideServerObject with Vitality with FactionAffinity, - targetSource: SourceEntry, - targetPosition: Vector3, - victimSource: SourceEntry, - fallHeight: Float, - velocity: Vector3 - ): Unit = { - sessionLogic.handleDealingDamage( - target, - DamageInteraction( - targetSource, - CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource), - targetPosition - ) - ) - } - override protected[support] def actionsToCancel(): Unit = { progressBarValue = None kitToBeUsed = None @@ -2169,7 +775,7 @@ class GeneralOperations( } } - override protected[support] def stop(): Unit = { + override protected[session] def stop(): Unit = { progressBarUpdate.cancel() charSavedTimer.cancel() } diff --git a/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala b/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala new file mode 100644 index 000000000..6eed2b2a8 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala @@ -0,0 +1,24 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.support + +import akka.actor.Actor.Receive +import akka.actor.ActorRef + +trait ModeLogic { + def avatarResponse: AvatarHandlerFunctions + def galaxy: GalaxyHandlerFunctions + def general: GeneralFunctions + def local: LocalHandlerFunctions + def mountResponse: MountHandlerFunctions + def squad: SquadHandlerFunctions + def shooting: WeaponAndProjectileFunctions + def terminals: TerminalHandlerFunctions + def vehicles: VehicleFunctions + def vehicleResponse: VehicleHandlerFunctions + + def parse(sender: ActorRef): Receive +} + +trait PlayerMode { + def setup(data: SessionData): ModeLogic +} diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index f92ea3425..64df47cd0 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -1,591 +1,38 @@ // Copyright (c) 2023 PSForever package net.psforever.actors.session.support -import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, typed} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.services.Service import net.psforever.objects.zones.exp import scala.collection.mutable -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ // import net.psforever.actors.session.{AvatarActor, ChatActor} -import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp} -import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} -import net.psforever.objects.inventory.InventoryItem -import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} -import net.psforever.objects.vital.etc.ExplodingEntityReason -import net.psforever.objects.zones.Zoning -import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game._ -import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage} -import net.psforever.services.{InterstellarClusterService => ICS} +import net.psforever.services.avatar.AvatarResponse import net.psforever.types._ import net.psforever.util.Config -import net.psforever.zones.Zones + +trait AvatarHandlerFunctions extends CommonSessionInterfacingFunctionality { + val ops: SessionAvatarHandlers + + def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit +} class SessionAvatarHandlers( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], chatActor: typed.ActorRef[ChatActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { //TODO player characters only exist within a certain range of GUIDs for a given zone; this is overkill - private[support] var lastSeenStreamMessage: mutable.LongMap[SessionAvatarHandlers.LastUpstream] = + private[session] var lastSeenStreamMessage: mutable.LongMap[SessionAvatarHandlers.LastUpstream] = mutable.LongMap[SessionAvatarHandlers.LastUpstream]() - private[this] val hidingPlayerRandomizer = new scala.util.Random + private[session] val hidingPlayerRandomizer = new scala.util.Random - /** - * na - * @param toChannel na - * @param guid na - * @param reply na - */ - def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = { - val resolvedPlayerGuid = if (player != null && player.HasGUID) { - player.GUID - } else { - Service.defaultPlayerGUID - } - val isNotSameTarget = resolvedPlayerGuid != guid - val isSameTarget = !isNotSameTarget - reply match { - /* special messages */ - case AvatarResponse.TeardownConnection() => - log.trace(s"ending ${player.Name}'s old session by event system request (relog)") - context.stop(context.self) - - /* really common messages (very frequently, every life) */ - case pstate @ AvatarResponse.PlayerState( - pos, - vel, - yaw, - pitch, - yawUpper, - _, - isCrouching, - isJumping, - jumpThrust, - isCloaking, - isNotRendered, - canSeeReallyFar - ) if isNotSameTarget => - val pstateToSave = pstate.copy(timestamp = 0) - val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = lastSeenStreamMessage.get(guid.guid) match { - case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting) - case _ => (None, 0L, Vector3.Zero, false, None) - } - val drawConfig = Config.app.game.playerDraw //m - val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m - val ourPosition = player.Position //xyz - val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m - val inDrawableRange = currentDistance <= maxRange - val now = System.currentTimeMillis() //ms - if ( - 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 - ) - ) - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now)) - } else { - //is visible, but skip reinforcement - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime)) - } - } else { - //conditions where the target is not currently visible - if (wasVisible) { - //the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance - val lat = (1 + hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat - sendResponse( - PlayerStateMessage( - guid, - Vector3(1f, lat, 1f), - vel=None, - facingYaw=0f, - facingPitch=0f, - facingYawUpper=0f, - timestamp=0, //is this okay? - is_cloaked = isCloaking - ) - ) - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now)) - } else { - //skip drawing altogether - lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime)) - } - } - - case AvatarResponse.ObjectHeld(slot, _) - if isSameTarget && player.VisibleSlots.contains(slot) => - sendResponse(ObjectHeldMessage(guid, slot, unk1=true)) - //Stop using proximity terminals if player unholsters a weapon - continent.GUID(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 && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => - sendResponse(ChangeFireStateMessage_Start(weaponGuid)) - val entry = lastSeenStreamMessage(guid.guid) - lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid))) - - case AvatarResponse.ChangeFireState_Start(weaponGuid) - if isNotSameTarget => - sendResponse(ChangeFireStateMessage_Start(weaponGuid)) - - case AvatarResponse.ChangeFireState_Stop(weaponGuid) - if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } => - sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) - val entry = lastSeenStreamMessage(guid.guid) - lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None)) - - case AvatarResponse.ChangeFireState_Stop(weaponGuid) - if isNotSameTarget => - sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) - - case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget => - sendResponse(pkt) - - case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget => - sendResponse(pkt) - - case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget => - sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) - - case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) => - sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) - - case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget => - sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) - - case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget => - sendResponse(GenericObjectActionMessage(objectGuid, actionCode)) - - case AvatarResponse.HitHint(sourceGuid) if player.isAlive => - sendResponse(HitHint(sourceGuid, guid)) - 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(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) => - facilityCaptureRewards(buildingId, zoneNumber, cep) - - case AvatarResponse.SendResponse(msg) => - sendResponse(msg) - - case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid => - sendResponse(msg) - - /* common messages (maybe once every respawn) */ - case AvatarResponse.Reload(itemGuid) - if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => - sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - - case AvatarResponse.Killed(mount) => - //log and chat messages - val cause = player.LastDamage.flatMap { damage => - val interaction = damage.interaction - val reason = interaction.cause - val adversarial = interaction.adversarial.map { _.attacker } - reason match { - case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] => - //also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..." - sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate")) - case _ => () - } - adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") } - }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" } - log.info(s"${player.Name} has died, killed by $cause") - if (sessionLogic.shooting.shotsWhileDead > 0) { - log.warn( - s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server" - ) - sessionLogic.shooting.shotsWhileDead = 0 - } - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") - sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) - - //player state changes - AvatarActor.updateToolDischargeFor(avatar) - player.FreeHand.Equipment.foreach { item => - DropEquipmentFromInventory(player)(item) - } - sessionLogic.general.dropSpecialSlotItem() - sessionLogic.general.toggleMaxSpecialState(enable = false) - sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive - sessionLogic.zoning.zoningStatus = Zoning.Status.None - sessionLogic.zoning.spawn.deadState = DeadState.Dead - continent.GUID(mount).collect { case obj: Vehicle => - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) - } - sessionLogic.actionsToCancel() - sessionLogic.terminals.CancelAllProximityUnits() - AvatarActor.savePlayerLocation(player) - sessionLogic.zoning.spawn.shiftPosition = Some(player.Position) - - //respawn - val respawnTimer = 300.seconds - sessionLogic.zoning.spawn.reviveTimer.cancel() - if (player.death_by == 0) { - sessionLogic.zoning.spawn.reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) { - sessionLogic.cluster ! ICS.GetRandomSpawnPoint( - Zones.sanctuaryZoneNumber(player.Faction), - player.Faction, - Seq(SpawnGroup.Sanctuary), - context.self - ) - } - } else { - sessionLogic.zoning.spawn.HandleReleaseAvatar(player, continent) - } - - case AvatarResponse.Release(tplayer) if isNotSameTarget => - sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer) - - case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid => - log.info(s"No time for rest, ${player.Name}. Back on your feet!") - sessionLogic.zoning.spawn.reviveTimer.cancel() - sessionLogic.zoning.spawn.deadState = DeadState.Alive - player.Revive - val health = player.Health - sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health)) - sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health) - ) - - /* uncommon messages (utility, or once in a while) */ - case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) - if isNotSameTarget && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => - changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) - sendResponse(ChangeAmmoMessage(weapon_guid, 1)) - - case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) - if isNotSameTarget => - changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) - - case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget => - sendResponse(ChangeFireModeMessage(itemGuid, mode)) - - case AvatarResponse.ConcealPlayer() => - sendResponse(GenericObjectActionMessage(guid, code=9)) - - case AvatarResponse.EnvironmentalDamage(_, _, _) => - //TODO damage marker? - 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 && lastSeenStreamMessage.get(guid.guid).exists { _.visible } => - continent.GUID(weaponGuid).collect { - case tool: Tool if tool.Magazine == 0 => - // check that the magazine is still empty before sending WeaponDryFireMessage - // if it has been reloaded since then, other clients will not see it firing - sendResponse(WeaponDryFireMessage(weaponGuid)) - } - - case _ => () - } - } - - private def changeAmmoProcedures( + def changeAmmoProcedures( weaponGuid: PlanetSideGUID, previousAmmoGuid: PlanetSideGUID, ammoTypeId: Int, @@ -605,7 +52,7 @@ class SessionAvatarHandlers( ) } - private def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = { + def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = { //TODO squad services deactivated, participation trophy rewards for now - 11-20-2023 //must be in a squad to earn experience val charId = player.CharId @@ -711,7 +158,7 @@ class SessionAvatarHandlers( } object SessionAvatarHandlers { - private[support] case class LastUpstream( + private[session] case class LastUpstream( msg: Option[AvatarResponse.PlayerState], visible: Boolean, shooting: Option[PlanetSideGUID], diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLogic.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala similarity index 92% rename from src/main/scala/net/psforever/actors/session/support/SessionLogic.scala rename to src/main/scala/net/psforever/actors/session/support/SessionData.scala index b07ea88e5..401bd7850 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionLogic.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -1,14 +1,9 @@ // Copyright (c) 2023 PSForever package net.psforever.actors.session.support -import akka.actor.Actor.Receive import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation} -import net.psforever.services.ServiceManager -import net.psforever.services.ServiceManager.Lookup - import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -27,7 +22,9 @@ import net.psforever.objects.vehicles._ import net.psforever.objects.vital._ import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones._ -import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity} +import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity, SectorGroup, SectorPopulation} +import net.psforever.services.ServiceManager +import net.psforever.services.ServiceManager.Lookup import net.psforever.packet._ import net.psforever.packet.game._ import net.psforever.services.account.AccountPersistenceService @@ -37,7 +34,7 @@ import net.psforever.services.{Service, InterstellarClusterService => ICS} import net.psforever.types._ import net.psforever.util.Config -object SessionLogic { +object SessionData { //noinspection ScalaUnusedSymbol private def NoTurnCounterYet(guid: PlanetSideGUID): Unit = { } @@ -60,11 +57,10 @@ object SessionLogic { } } -trait SessionLogic { - def middlewareActor: typed.ActorRef[MiddlewareActor.Command] - - implicit def context: ActorContext - +class SessionData( + val middlewareActor: typed.ActorRef[MiddlewareActor.Command], + implicit val context: ActorContext + ) { /** * Hardwire an implicit `sender` to be the same as `context.self` of the `SessionActor` actor class * for which this support class was initialized. @@ -80,19 +76,19 @@ trait SessionLogic { private val avatarActor: typed.ActorRef[AvatarActor.Command] = context.spawnAnonymous(AvatarActor(context.self)) private val chatActor: typed.ActorRef[ChatActor.Command] = context.spawnAnonymous(ChatActor(context.self, avatarActor)) - private[support] val log = org.log4s.getLogger - private[support] var theSession: Session = Session() - private[support] var accountIntermediary: ActorRef = Default.Actor - private[support] var accountPersistence: ActorRef = Default.Actor - private[support] var galaxyService: ActorRef = Default.Actor - private[support] var squadService: ActorRef = Default.Actor - private[support] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor + private[session] val log = org.log4s.getLogger + private[session] var theSession: Session = Session() + private[session] var accountIntermediary: ActorRef = Default.Actor + private[session] var accountPersistence: ActorRef = Default.Actor + private[session] var galaxyService: ActorRef = Default.Actor + private[session] var squadService: ActorRef = Default.Actor + private[session] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor private[session] var connectionState: Int = 25 - private[support] var persistFunc: () => Unit = noPersistence - private[support] var persist: () => Unit = updatePersistenceOnly + private[session] var persistFunc: () => Unit = noPersistence + private[session] var persist: () => Unit = updatePersistenceOnly private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial - private[support] var turnCounterFunc: PlanetSideGUID => Unit = SessionLogic.NoTurnCounterYet - private[support] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() + private[session] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet + private[session] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0) val general: GeneralOperations = @@ -128,7 +124,7 @@ trait SessionLogic { * updated when an upstream packet arrives; * allow to be a little stale for a short while */ - private[support] var localSector: SectorPopulation = SectorGroup(Nil) + private[session] var localSector: SectorPopulation = SectorGroup(Nil) def session: Session = theSession @@ -159,7 +155,7 @@ trait SessionLogic { case LookupResult("galaxy", endpoint) => galaxyService = endpoint buildDependentOperationsForGalaxy(endpoint) - buildDependentOperations(endpoint, cluster) + buildDependentOperationsForZoning(endpoint, cluster) true case LookupResult("squad", endpoint) => squadService = endpoint @@ -167,7 +163,7 @@ trait SessionLogic { true case ICS.InterstellarClusterServiceKey.Listing(listings) => cluster = listings.head - buildDependentOperations(galaxyService, cluster) + buildDependentOperationsForZoning(galaxyService, cluster) true case _ => @@ -182,7 +178,7 @@ trait SessionLogic { } } - def buildDependentOperations(galaxyActor: ActorRef, clusterActor: typed.ActorRef[ICS.Command]): Unit = { + def buildDependentOperationsForZoning(galaxyActor: ActorRef, clusterActor: typed.ActorRef[ICS.Command]): Unit = { if (zoningOpt.isEmpty && galaxyActor != Default.Actor && clusterActor != Default.typed.Actor) { zoningOpt = Some(new ZoningOperations(sessionLogic=this, avatarActor, galaxyActor, clusterActor, context)) } @@ -203,10 +199,6 @@ trait SessionLogic { zoningOpt.nonEmpty } - /* message processing */ - - def parse(sender: ActorRef): Receive - /* support functions */ def validObject(id: Int): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator = "") @@ -487,21 +479,21 @@ trait SessionLogic { (continent.GUID(player.VehicleSeated) match { case Some(v : Vehicle) => v.Weapons.toList.collect { - case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => SessionLogic.updateOldRefsMap(slot.Equipment.get) + case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => SessionData.updateOldRefsMap(slot.Equipment.get) }.flatten ++ - SessionLogic.updateOldRefsMap(v.Inventory) + SessionData.updateOldRefsMap(v.Inventory) case _ => Map.empty[PlanetSideGUID, String] }) ++ (general.accessedContainer match { - case Some(cont) => SessionLogic.updateOldRefsMap(cont.Inventory) + case Some(cont) => SessionData.updateOldRefsMap(cont.Inventory) case None => Map.empty[PlanetSideGUID, String] }) ++ player.Holsters().toList.collect { - case slot if slot.Equipment.nonEmpty => SessionLogic.updateOldRefsMap(slot.Equipment.get) + case slot if slot.Equipment.nonEmpty => SessionData.updateOldRefsMap(slot.Equipment.get) }.flatten ++ - SessionLogic.updateOldRefsMap(player.Inventory) ++ - SessionLogic.updateOldRefsMap(player.avatar.locker.Inventory) + SessionData.updateOldRefsMap(player.Inventory) ++ + SessionData.updateOldRefsMap(player.avatar.locker.Inventory) ) } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala index c2cabd167..ad7614296 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala @@ -2,190 +2,22 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.FriendsResponse -import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} // import net.psforever.actors.session.AvatarActor -import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage} import net.psforever.services.galaxy.GalaxyResponse -import net.psforever.types.{MemberAction, PlanetSideEmpire} -class SessionGalaxyHandlers( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], - galaxyService: ActorRef, - implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality { - /* packets */ +trait GalaxyHandlerFunctions extends CommonSessionInterfacingFunctionality { + def ops: SessionGalaxyHandlers - def handleUpdateIgnoredPlayers: PlanetSideGamePacket => Unit = { - case msg: FriendsResponse => - sendResponse(msg) - msg.friends.foreach { f => - galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name)) - } - case _ => () - } + def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit - /* response handlers */ - - def handle(reply: GalaxyResponse.Response): Unit = { - reply match { - case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => - sendResponse( - HotSpotUpdateMessage( - zone_index, - priority, - hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) } - ) - ) - - case GalaxyResponse.MapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) => - val faction = player.Faction - val from = fromFactions.contains(faction) - val to = toFactions.contains(faction) - if (from && !to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL)) - } else if (!from && to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction)) - } - - case GalaxyResponse.FlagMapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) => - sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest) - - case GalaxyResponse.LockedZoneUpdate(zone, time) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time)) - - case GalaxyResponse.UnlockedZoneUpdate(zone) => ; - sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L)) - val popBO = 0 - val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR) - val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC) - val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS) - sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) - - case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) => - avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name) - - case GalaxyResponse.SendResponse(msg) => - sendResponse(msg) - - case _ => () - } - } + def handle(reply: GalaxyResponse.Response): Unit } -/*package net.psforever.actors.session.support - -import akka.actor.{ActorContext, ActorRef, typed} -import scala.concurrent.duration._ -// -import net.psforever.actors.session.AvatarActor -import net.psforever.objects.Vehicle -import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, DeadState, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage} -import net.psforever.services.Service -import net.psforever.services.galaxy.GalaxyResponse -import net.psforever.types.{MemberAction, PlanetSideEmpire} - class SessionGalaxyHandlers( - val sessionData: SessionData, - avatarActor: typed.ActorRef[AvatarActor.Command], - galaxyService: ActorRef, + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], + val galaxyService: ActorRef, implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality { - def handle(reply: GalaxyResponse.Response): Unit = { - reply match { - case GalaxyResponse.HotSpotUpdate(zoneIndex, priority, hotSpotInfo) => - sendResponse( - HotSpotUpdateMessage( - zoneIndex, - priority, - hotSpotInfo.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) } - ) - ) - - case GalaxyResponse.MapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) => - val faction = player.Faction - val from = fromFactions.contains(faction) - val to = toFactions.contains(faction) - if (from && !to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL)) - } else if (!from && to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction)) - } - - case GalaxyResponse.FlagMapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.TransferPassenger(tempChannel, vehicle, _, manifest) => - val playerName = player.Name - log.debug(s"TransferPassenger: $playerName received the summons to transfer to ${vehicle.Zone.id} ...") - manifest.passengers - .find { _.name.equals(playerName) } - .collect { - case entry if vehicle.Seats(entry.mount).occupant.isEmpty => - player.VehicleSeated = None - vehicle.Seats(entry.mount).mount(player) - player.VehicleSeated = vehicle.GUID - Some(vehicle) - case entry if vehicle.Seats(entry.mount).occupant.contains(player) => - Some(vehicle) - case entry => - log.warn( - s"TransferPassenger: $playerName tried to mount seat ${entry.mount} during summoning, but it was already occupied, and ${player.Sex.pronounSubject} was rebuked" - ) - None - }.orElse { - manifest.cargo.find { _.name.equals(playerName) }.flatMap { entry => - vehicle.CargoHolds(entry.mount).occupant.collect { - case cargo if cargo.Seats(0).occupants.exists(_.Name.equals(playerName)) => cargo - } - } - } match { - case Some(v: Vehicle) => - galaxyService ! Service.Leave(Some(tempChannel)) //temporary vehicle-specific channel (see above) - sessionData.zoning.spawn.deadState = DeadState.Release - sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true)) - sessionData.zoning.interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system - sessionData.zoning.spawn.LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None) - case _ => - sessionData.zoning.interstellarFerry match { - case None => - galaxyService ! Service.Leave(Some(tempChannel)) //no longer being transferred between zones - sessionData.zoning.interstellarFerryTopLevelGUID = None - case Some(_) => ; - //wait patiently - } - } - - case GalaxyResponse.LockedZoneUpdate(zone, time) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time)) - - case GalaxyResponse.UnlockedZoneUpdate(zone) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L)) - val popBO = 0 - val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR) - val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC) - val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS) - sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) - - case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists { _.name.equals(name) } => - avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name) - - case GalaxyResponse.SendResponse(msg) => - sendResponse(msg) - - case _ => () - } - } -}*/ + ) extends CommonSessionInterfacingFunctionality diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala index 6d80ff682..df53c650e 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala @@ -2,237 +2,16 @@ package net.psforever.actors.session.support import akka.actor.ActorContext -import net.psforever.objects.ce.Deployable -import net.psforever.objects.vehicles.MountableWeapons -import net.psforever.objects._ -import net.psforever.packet.game._ -import net.psforever.services.Service import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3} +import net.psforever.types.PlanetSideGUID + +trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality { + def ops: SessionLocalHandlers + + def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit +} class SessionLocalHandlers( - val sessionLogic: SessionLogic, + val sessionLogic: SessionData, implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality { - /** - * na - * @param toChannel na - * @param guid na - * @param reply na - */ - def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = { - val resolvedPlayerGuid = if (player.HasGUID) { - player.GUID - } else { - Service.defaultPlayerGUID - } - val isNotSameTarget = resolvedPlayerGuid != guid - reply match { - case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget => - sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) - - case LocalResponse.DeployableUIFor(item) => - sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) - - case LocalResponse.Detonate(dguid, _: BoomerDeployable) => - sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) - sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) => - sendResponse(GenericObjectActionMessage(dguid, code=19)) - sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.Detonate(_, obj) => - log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly") - - case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget => - sendResponse(GenericObjectStateMsg(doorGuid, state=16)) - - case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone - sendResponse(GenericObjectStateMsg(doorGuid, state=17)) - - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => - obj.Destroyed = true - DeconstructDeployable( - obj, - dguid, - pos, - obj.Orientation, - deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 } - ) - - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _) - if obj.Destroyed || obj.Jammed || obj.Health == 0 => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed => - //if active, deactivate - obj.Active = false - sendResponse(GenericObjectActionMessage(dguid, code=29)) - sendResponse(GenericObjectActionMessage(dguid, code=30)) - //standard deployable elimination behavior - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active => - //if active, deactivate - obj.Active = false - sendResponse(GenericObjectActionMessage(dguid, code=29)) - sendResponse(GenericObjectActionMessage(dguid, code=30)) - //standard deployable elimination behavior - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed => - //standard deployable elimination behavior - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => - //standard deployable elimination behavior - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) - - case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) - - case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) => - sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2)) - - case LocalResponse.HackObject(targetGuid, unk1, unk2) => - sessionLogic.general.hackObject(targetGuid, unk1, unk2) - - case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) => - sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue) - - case LocalResponse.GenericObjectAction(targetGuid, actionNumber) => - sendResponse(GenericObjectActionMessage(targetGuid, actionNumber)) - - case LocalResponse.GenericActionMessage(actionNumber) => - sendResponse(GenericActionMessage(actionNumber)) - - case LocalResponse.ChatMessage(msg) => - sendResponse(msg) - - case LocalResponse.SendPacket(packet) => - sendResponse(packet) - - case LocalResponse.LluSpawned(llu) => - // Create LLU on client - sendResponse(ObjectCreateMessage( - llu.Definition.ObjectId, - llu.GUID, - llu.Definition.Packet.ConstructorData(llu).get - )) - sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f)) - - case LocalResponse.LluDespawned(lluGuid, position) => - sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f)) - sendResponse(ObjectDeleteMessage(lluGuid, unk1=0)) - // If the player was holding the LLU, remove it from their tracked special item slot - sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid => - sessionLogic.general.specialItemSlotGuid = None - player.Carrying = None - } - - case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget => - sendResponse(ObjectDeleteMessage(objectGuid, unk)) - - case LocalResponse.ProximityTerminalEffect(object_guid, true) => - sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true)) - - case LocalResponse.ProximityTerminalEffect(objectGuid, false) => - sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false)) - sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid) - - case LocalResponse.RouterTelepadMessage(msg) => - sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None)) - - case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) => - sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) - - case LocalResponse.SendResponse(msg) => - sendResponse(msg) - - case LocalResponse.SetEmpire(objectGuid, empire) => - sendResponse(SetEmpireMessage(objectGuid, empire)) - - case LocalResponse.ShuttleEvent(ev) => - val msg = OrbitalShuttleTimeMsg( - ev.u1, - ev.u2, - ev.t1, - ev.t2, - ev.t3, - pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) } - ) - sendResponse(msg) - - case LocalResponse.ShuttleDock(pguid, sguid, slot) => - sendResponse(ObjectAttachMessage(pguid, sguid, slot)) - - case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) => - sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient)) - - case LocalResponse.ShuttleState(sguid, pos, orient, state) => - sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false)) - - case LocalResponse.ToggleTeleportSystem(router, systemPlan) => - sessionLogic.general.toggleTeleportSystem(router, systemPlan) - - case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) => - sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation)) - - case LocalResponse.TriggerSound(sound, pos, unk, volume) => - sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) - - case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) => - sendResponse(GenericObjectActionMessage(buildingGuid, 11)) - - case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) => - sendResponse(GenericObjectActionMessage(buildingGuid, 12)) - - case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid => - continent.GUID(vehicleGuid) - .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) } - .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) } - .getOrElse(Set.empty) - .collect { case weapon: Tool if weapon.GUID == weaponGuid => - sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) - } - - case _ => () - } - } - - /** - * 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)) - } -} + ) extends CommonSessionInterfacingFunctionality diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala index 8d04ae652..c124bd182 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -3,366 +3,33 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.Tool -import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions import net.psforever.objects.vehicles.MountableWeapons -import net.psforever.objects.vital.InGameHistory -import net.psforever.packet.game.InventoryStateMessage - -import scala.concurrent.duration._ +import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg} // import net.psforever.actors.session.AvatarActor -import net.psforever.actors.zone.ZoneActor -import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles} -import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition} -import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime +import net.psforever.objects.Player import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech -import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} -import net.psforever.objects.vehicles.AccessPermissionGroup -import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleMsg, GenericObjectActionMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} -import net.psforever.services.Service -import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} +import net.psforever.packet.game.DismountVehicleMsg + +trait MountHandlerFunctions extends CommonSessionInterfacingFunctionality { + val ops: SessionMountHandlers + + def handleMountVehicle(pkt: MountVehicleMsg): Unit + + def handleDismountVehicle(pkt: DismountVehicleMsg): Unit + + def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit + + def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit + + def handle(tplayer: Player, reply: Mountable.Exchange): Unit +} class SessionMountHandlers( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { - /** - * na - * - * @param tplayer na - * @param reply na - */ - 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) - 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) - 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) - 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) - 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)) - updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) - - case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) - if !obj.isUpgrading || System.currentTimeMillis() - 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)) - 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)) - 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") - } - } - - /** - * Common activities/procedure when a player mounts a valid object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount into which the player is mounting - */ - def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - val playerGuid: PlanetSideGUID = tplayer.GUID - val objGuid: PlanetSideGUID = obj.GUID - 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 - */ - 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 - */ - 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) - ) - } - /** * From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines. * @param objWithSeat the object that owns seats (and weaponry) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala index 14cf91710..891443561 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala @@ -5,39 +5,48 @@ import akka.actor.{ActorContext, ActorRef, typed} import scala.collection.mutable // import net.psforever.actors.session.{AvatarActor, ChatActor} -import net.psforever.objects.avatar.Avatar import net.psforever.objects.teamwork.Squad -import net.psforever.objects.{Default, LivePlayerList, Player} +import net.psforever.objects.{Default, Player} import net.psforever.packet.game._ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.chat.ChatService import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction} -import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, SquadListDecoration, SquadResponseType, Vector3, WaypointSubtype} +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} + +trait SquadHandlerFunctions extends CommonSessionInterfacingFunctionality { + val ops: SessionSquadHandlers + + def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit + + def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit + + def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit + + def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit +} object SessionSquadHandlers { - protected final case class SquadUIElement( - name: String, - outfit: Long, - index: Int, - zone: Int, - health: Int, - armor: Int, - position: Vector3 - ) + final case class SquadUIElement( + name: String, + outfit: Long, + index: Int, + zone: Int, + health: Int, + armor: Int, + position: Vector3 + ) } class SessionSquadHandlers( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], - chatActor: typed.ActorRef[ChatActor.Command], - squadService: ActorRef, + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], + val chatActor: typed.ActorRef[ChatActor.Command], + val squadService: ActorRef, implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { import SessionSquadHandlers._ - private var waypointCooldown: Long = 0L - val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]() - var squad_supplement_id: Int = 0 + private[session] val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]() + private[session] var squad_supplement_id: Int = 0 /** * When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked. * This `WorldSessionActor`-local variable is then used to indicate the ongoing state of the LFS UI component, @@ -46,340 +55,11 @@ class SessionSquadHandlers( * Upon leaving or disbanding a squad, this value is made false. * Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated. */ - private[support] var squadSetup: () => Unit = FirstTimeSquadSetup - private var squadUpdateCounter: Int = 0 + private[session] var squadSetup: () => Unit = FirstTimeSquadSetup + private[session] var squadUpdateCounter: Int = 0 + private[session] var updateSquad: () => Unit = NoSquadUpdates + private[session] var updateSquadRef: ActorRef = Default.Actor private val queuedSquadActions: Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates) - private[support] var updateSquad: () => Unit = NoSquadUpdates - private var updateSquadRef: ActorRef = Default.Actor - - /* packet */ - - def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { - val SquadDefinitionActionMessage(u1, u2, action) = pkt - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) - } - - def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { - val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt - squadService ! SquadServiceMessage( - player, - continent, - SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5) - ) - } - - def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = { - val SquadWaypointRequest(request, _, wtype, unk, info) = pkt - val time = System.currentTimeMillis() - val subtype = wtype.subtype - if(subtype == WaypointSubtype.Squad) { - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) - } else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) { - //guarding against duplicating laze waypoints - waypointCooldown = time - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) - } - } - - /* response handlers */ - - def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - if (!excluded.exists(_ == avatar.id)) { - response match { - case SquadResponse.ListSquadFavorite(line, task) => - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task))) - - case SquadResponse.InitList(infos) => - sendResponse(ReplicationStreamMessage(infos)) - - case SquadResponse.UpdateList(infos) if infos.nonEmpty => - sendResponse( - ReplicationStreamMessage( - 6, - None, - infos.map { - case (index, squadInfo) => - SquadListing(index, squadInfo) - }.toVector - ) - ) - - case SquadResponse.RemoveFromList(infos) if infos.nonEmpty => - sendResponse( - ReplicationStreamMessage( - 1, - None, - infos.map { index => - SquadListing(index, None) - }.toVector - ) - ) - - case SquadResponse.SquadDecoration(guid, squad) => - val decoration = if ( - squadUI.nonEmpty || - squad.Size == squad.Capacity || - { - val offer = avatar.certifications - !squad.Membership.exists { _.isAvailable(offer) } - } - ) { - SquadListDecoration.NotAvailable - } else { - SquadListDecoration.Available - } - sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration))) - - case SquadResponse.Detail(guid, detail) => - sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - - case SquadResponse.IdentifyAsSquadLeader(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) - - case SquadResponse.SetListSquad(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) - - case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) => - val name = request_type match { - case SquadResponseType.Invite if unk5 => - //the name of the player indicated by unk3 is needed - LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match { - case Some(player) => - player.name - case None => - player_name - } - case _ => - player_name - } - sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6)) - - case SquadResponse.WantsSquadPosition(_, name) => - sendResponse( - ChatMsg( - ChatMessageType.CMT_SQUAD, - wideContents=true, - name, - s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)", - None - ) - ) - - case SquadResponse.Join(squad, positionsToUpdate, _, ref) => - val avatarId = avatar.id - val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex) - .filter { case (mem, index) => - mem.CharId > 0 && positionsToUpdate.contains(index) - } - membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match { - case Some((ourMember, ourIndex)) => - //we are joining the squad - //load each member's entry (our own too) - squad_supplement_id = squad.GUID.guid + 1 - membershipPositions.foreach { - case (member, index) => - sendResponse( - SquadMemberEvent.Add( - squad_supplement_id, - member.CharId, - index, - member.Name, - member.ZoneId, - outfit_id = 0 - ) - ) - squadUI(member.CharId) = - SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position) - } - //repeat our entry - sendResponse( - SquadMemberEvent.Add( - squad_supplement_id, - ourMember.CharId, - ourIndex, - ourMember.Name, - ourMember.ZoneId, - outfit_id = 0 - ) - ) - //turn lfs off - if (avatar.lookingForSquad) { - avatarActor ! AvatarActor.SetLookingForSquad(false) - } - val playerGuid = player.GUID - val factionChannel = s"${player.Faction}" - //squad colors - GiveSquadColorsToMembers() - GiveSquadColorsForOthers(playerGuid, factionChannel, squad_supplement_id) - //associate with member position in squad - sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex)) - //a finalization? what does this do? - sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18))) - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration()) - updateSquadRef = ref - updateSquad = PeriodicUpdatesWhenEnrolledInSquad - chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID)) - case _ => - //other player is joining our squad - //load each member's entry - GiveSquadColorsToMembers( - membershipPositions.map { - case (member, index) => - val charId = member.CharId - sendResponse( - SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0) - ) - squadUI(charId) = - SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position) - charId - } - ) - } - //send an initial dummy update for map icon(s) - sendResponse( - SquadState( - PlanetSideGUID(squad_supplement_id), - membershipPositions.map { case (member, _) => - SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position) - } - ) - ) - - case SquadResponse.Leave(squad, positionsToUpdate) => - positionsToUpdate.find({ case (member, _) => member == avatar.id }) match { - case Some((ourMember, ourIndex)) => - //we are leaving the squad - //remove each member's entry (our own too) - updateSquadRef = Default.Actor - positionsToUpdate.foreach { - case (member, index) => - sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) - squadUI.remove(member) - } - //uninitialize - val playerGuid = player.GUID - sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry - GiveSquadColorsToSelf(value = 0) - sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad? - sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated? - avatarActor ! AvatarActor.SetLookingForSquad(false) - //a finalization? what does this do? - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) - squad_supplement_id = 0 - squadUpdateCounter = 0 - updateSquad = NoSquadUpdates - chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID)) - case _ => - //remove each member's entry - GiveSquadColorsToMembers( - positionsToUpdate.map { - case (member, index) => - sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) - squadUI.remove(member) - member - }, - value = 0 - ) - } - - case SquadResponse.AssignMember(squad, from_index, to_index) => - //we've already swapped position internally; now we swap the cards - SwapSquadUIElements(squad, from_index, to_index) - - case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) => - if (promotedPlayer != player.CharId) { - //demoted from leader; no longer lfsm - if (player.avatar.lookingForSquad) { - avatarActor ! AvatarActor.SetLookingForSquad(false) - } - } - sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0)) - //the players have already been swapped in the backend object - PromoteSquadUIElements(squad, from_index) - - case SquadResponse.UpdateMembers(_, positions) => - val pairedEntries = positions.collect { - case entry if squadUI.contains(entry.char_id) => - (entry, squadUI(entry.char_id)) - } - //prune entries - val updatedEntries = pairedEntries - .collect({ - case (entry, element) if entry.zone_number != element.zone => - //zone gets updated for these entries - sendResponse( - SquadMemberEvent.UpdateZone(squad_supplement_id, entry.char_id, element.index, entry.zone_number) - ) - squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - case (entry, element) - if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => - //other elements that need to be updated - squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - }) - .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend - if (updatedEntries.nonEmpty) { - sendResponse( - SquadState( - PlanetSideGUID(squad_supplement_id), - updatedEntries.map { entry => - SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) - } - ) - ) - } - - case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => - sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) - - case SquadResponse.SquadSearchResults(results) => - //TODO positive squad search results message? -// if(results.nonEmpty) { -// results.foreach { guid => -// sendResponse(SquadDefinitionActionMessage( -// guid, -// 0, -// SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) -// ) -// } -// } else { -// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) -// } -// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) - - case SquadResponse.InitWaypoints(char_id, waypoints) => - waypoints.foreach { - case (waypoint_type, info, unk) => - sendResponse( - SquadWaypointEvent.Add( - squad_supplement_id, - char_id, - waypoint_type, - WaypointEvent(info.zone_number, info.pos, unk) - ) - ) - } - - case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) => - sendResponse( - SquadWaypointEvent.Add( - squad_supplement_id, - char_id, - waypoint_type, - WaypointEvent(info.zone_number, info.pos, unk) - ) - ) - - case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => - sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type)) - - case _ => ; - } - } - } /** * These messages are dispatched when first starting up the client and connecting to the server for the first time. diff --git a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala index b74c5f60d..73254d6fe 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala @@ -3,181 +3,41 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.guid.GUIDTask -import net.psforever.objects.sourcing.AmenitySource -import net.psforever.objects.vital.TerminalUsedActivity +import net.psforever.packet.game.FavoritesRequest import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future // import net.psforever.actors.session.AvatarActor -import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory} import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} -import net.psforever.objects.guid.{StraightforwardTask, TaskBundle, TaskWorkflow} +import net.psforever.objects.guid.{StraightforwardTask, TaskBundle} import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.equipment.EffectTarget import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.terminals.{ProximityDefinition, ProximityUnit, Terminal} -import net.psforever.packet.game.{ItemTransactionMessage, ItemTransactionResultMessage,ProximityTerminalUseMessage, UnuseItemMessage} -import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3} +import net.psforever.packet.game.{ItemTransactionMessage,ProximityTerminalUseMessage} +import net.psforever.types.PlanetSideGUID + +trait TerminalHandlerFunctions extends CommonSessionInterfacingFunctionality { + def ops: SessionTerminalHandlers + + def handleItemTransaction(pkt: ItemTransactionMessage): Unit + + def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit + + def handleFavoritesRequest(pkt: FavoritesRequest): Unit + + def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit +} class SessionTerminalHandlers( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { - private[support] var lastTerminalOrderFulfillment: Boolean = true - private[support] var usingMedicalTerminal: Option[PlanetSideGUID] = None - - /* packets */ - - def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { - val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt - continent.GUID(terminalGuid) match { - case Some(term: Terminal) if lastTerminalOrderFulfillment => - val msg: String = if (itemName.nonEmpty) s" of $itemName" else "" - log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg") - lastTerminalOrderFulfillment = false - 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) => - HandleProximityTerminalUse(obj) - case Some(obj) => - log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects") - case None => - log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}") - } - } - - /* response handler */ - - /** - * na - * @param tplayer na - * @param msg na - * @param order na - */ - def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = { - order match { - case Terminal.BuyEquipment(item) - if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty => - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) - lastTerminalOrderFulfillment = true - - case Terminal.BuyEquipment(item) => - avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) - TaskWorkflow.execute(BuyNewEquipmentPutInInventory( - continent.GUID(tplayer.VehicleSeated) match { - case Some(v: Vehicle) => v - case _ => player - }, - tplayer, - msg.terminal_guid - )(item)) - - case Terminal.SellEquipment() => - SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot) - - case Terminal.LearnCertification(cert) => - avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert) - lastTerminalOrderFulfillment = true - - case Terminal.SellCertification(cert) => - avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert) - lastTerminalOrderFulfillment = true - - case Terminal.LearnImplant(implant) => - avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant) - lastTerminalOrderFulfillment = true - - case Terminal.SellImplant(implant) => - avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant) - lastTerminalOrderFulfillment = true - - case Terminal.BuyVehicle(vehicle, _, _) - if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || tplayer.spectator => - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) - lastTerminalOrderFulfillment = true - - case Terminal.BuyVehicle(vehicle, weapons, trunk) => - continent.map.terminalToSpawnPad - .find { case (termid, _) => termid == msg.terminal_guid.guid } - .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) } - .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) => - avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) - vehicle.Faction = tplayer.Faction - vehicle.Position = pad.Position - vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset) - //default loadout, weapons - val vWeapons = vehicle.Weapons - weapons.foreach { entry => - vWeapons.get(entry.start) match { - case Some(slot) => - entry.obj.Faction = tplayer.Faction - slot.Equipment = None - slot.Equipment = entry.obj - case None => - log.warn( - s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}" - ) - } - } - //default loadout, trunk - val vTrunk = vehicle.Trunk - vTrunk.Clear() - trunk.foreach { entry => - entry.obj.Faction = tplayer.Faction - vTrunk.InsertQuickly(entry.start, entry.obj) - } - TaskWorkflow.execute(registerVehicleFromSpawnPad(vehicle, pad, term)) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true)) - if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { - sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) - } - player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type)) - } - .orElse { - log.error( - s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it" - ) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) - None - } - lastTerminalOrderFulfillment = true - - case Terminal.NoDeal() if msg != null => - val transaction = msg.transaction_type - log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order") - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false)) - lastTerminalOrderFulfillment = true - - case _ => - val terminal = msg.terminal_guid.guid - continent.GUID(terminal) match { - case Some(term: Terminal) => - log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order") - case Some(_) => - log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal") - case None => - log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal") - } - lastTerminalOrderFulfillment = true - } - } - - /* support */ + private[session] var lastTerminalOrderFulfillment: Boolean = true + private[session] var usingMedicalTerminal: Option[PlanetSideGUID] = None /** * Construct tasking that adds a completed and registered vehicle into the scene. @@ -189,7 +49,7 @@ class SessionTerminalHandlers( * @see `RegisterVehicle` * @return a `TaskBundle` message */ - private[session] def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = { + def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = { TaskBundle( new StraightforwardTask() { private val localVehicle = vehicle @@ -335,7 +195,7 @@ class SessionTerminalHandlers( * @see `RegisterVehicleFromSpawnPad` * @return a `TaskBundle` message */ - private[session] def registerVehicle(vehicle: Vehicle): TaskBundle = { + def registerVehicle(vehicle: Vehicle): TaskBundle = { TaskBundle( new StraightforwardTask() { private val localVehicle = vehicle @@ -350,5 +210,6 @@ class SessionTerminalHandlers( override protected[support] def actionsToCancel(): Unit = { lastTerminalOrderFulfillment = true + usingMedicalTerminal = None } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala index f1736f233..5389b6bb9 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala @@ -3,390 +3,18 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.AvatarActor -import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} -import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} -import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import net.psforever.packet.game._ -import net.psforever.services.Service -import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse} -import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} +import net.psforever.services.vehicle.VehicleResponse +import net.psforever.types.PlanetSideGUID -import scala.concurrent.duration._ +trait VehicleHandlerFunctions extends CommonSessionInterfacingFunctionality { + def ops: SessionVehicleHandlers + + def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit +} class SessionVehicleHandlers( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], - galaxyService: ActorRef, + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], + val galaxyService: ActorRef, implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality { - /** - * na - * - * @param toChannel na - * @param guid na - * @param reply na - */ - def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = { - val resolvedPlayerGuid = if (player.HasGUID) { - player.GUID - } else { - PlanetSideGUID(-1) - } - val isNotSameTarget = resolvedPlayerGuid != guid - reply match { - case VehicleResponse.VehicleState( - vehicleGuid, - unk1, - pos, - orient, - vel, - unk2, - unk3, - unk4, - wheelDirection, - unk5, - unk6 - ) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) => - //player who is also in the vehicle (not driver) - sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6)) - player.Position = pos - player.Orientation = orient - player.Velocity = vel - 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 - 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) - } - } -} + ) extends CommonSessionInterfacingFunctionality diff --git a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala index 7a753f8b5..d56107506 100644 --- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala @@ -5,466 +5,39 @@ import akka.actor.{ActorContext, typed} import net.psforever.actors.session.AvatarActor import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.control.BfrFlight -import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} import net.psforever.objects.zones.Zone import net.psforever.objects._ -import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, MountVehicleCargoMsg, MountVehicleMsg, VehicleSubStateMessage, _} -import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{BailType, DriveState, Vector3} +import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _} +import net.psforever.types.DriveState -class VehicleOperations( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], - implicit val context: ActorContext - ) extends CommonSessionInterfacingFunctionality { - private[support] var serverVehicleControlVelocity: Option[Int] = None +trait VehicleFunctions extends CommonSessionInterfacingFunctionality { + def ops: VehicleOperations - /* packets */ + def handleVehicleState(pkt: VehicleStateMessage): Unit - def handleVehicleState(pkt: VehicleStateMessage): Unit = { - val VehicleStateMessage( - vehicle_guid, - unk1, - pos, - ang, - vel, - is_flying, - unk6, - unk7, - wheels, - is_decelerating, - is_cloaked - ) = pkt - GetVehicleAndSeat() match { - case (Some(obj), Some(0)) => - //we're driving the vehicle - 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 - def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { - val FrameVehicleStateMessage( - vehicle_guid, - unk1, - pos, - ang, - vel, - unk2, - unk3, - unk4, - is_crouched, - is_airborne, - ascending_flight, - flight_time, - unk9, - unkA - ) = pkt - GetVehicleAndSeat() match { - case (Some(obj), Some(0)) => - //we're driving the vehicle - sessionLogic.persist() - sessionLogic.turnCounterFunc(player.GUID) - val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { - case Some(v: Vehicle) => - sessionLogic.updateBlockMap(obj, pos) - (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) - case _ => - (pos, ang, vel, true) - } - player.Position = position //convenient - if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient - } - obj.Position = position - obj.Orientation = angle - obj.Velocity = velocity - // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) { - // //dev stuff goes here - // } - // else - // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) { - // //dev stuff goes here - // } - obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile - if (notMountedState) { - if (obj.DeploymentState != DriveState.Kneeling) { - if (is_airborne) { - val flight = if (ascending_flight) flight_time else -flight_time - obj.Flying = Some(flight) - obj.Actor ! BfrFlight.Soaring(flight) - } else if (obj.Flying.nonEmpty) { - obj.Flying = None - obj.Actor ! BfrFlight.Landed - } - } else { - obj.Velocity = None - obj.Flying = None - } - obj.zoneInteractions() - } else { - obj.Velocity = None - obj.Flying = None - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.FrameVehicleState( - player.GUID, - vehicle_guid, - unk1, - position, - angle, - velocity, - unk2, - unk3, - unk4, - is_crouched, - is_airborne, - ascending_flight, - flight_time, - unk9, - unkA - ) - ) - sessionLogic.squad.updateSquad() - case (None, _) => - //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") - //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle - case (_, Some(index)) => - log.error( - s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" - ) - case _ => ; - } - if (player.death_by == -1) { - sessionLogic.kickedByAdministration() - } - } + def handleChildObjectState(pkt: ChildObjectStateMessage): Unit - 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 - def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = { - val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt - sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState") match { - case Some(obj: Vehicle) => - import net.psforever.login.WorldSession.boolToInt - obj.Position = pos - obj.Orientation = ang - obj.Velocity = vel - sessionLogic.updateBlockMap(obj, pos) - obj.zoneInteractions() - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.VehicleState( - player.GUID, - vehicle_guid, - unk1, - pos, - ang, - obj.Velocity, - obj.Flying, - 0, - 0, - 15, - unk5 = false, - obj.Cloaked - ) - ) - case _ => ; - } - } - - def 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) - } - } - } - - private def dismountWarning( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.warn(note) - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - private def dismountError( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it") - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { - val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt - (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { - case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => - carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { - case Some((mountPoint, _)) => - cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint) - case _ => - log.warn( - s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold" - ) - } - case (None, _) | (Some(_), None) => - log.warn( - s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid" - ) - case _ => ; - } - } - - def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { - val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt - continent.GUID(cargo_guid) match { - case Some(cargo: Vehicle) => - cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked) - case _ => ; - } - } - - def handleDeployRequest(pkt: DeployRequestMessage): Unit = { - val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt - val vehicle = player.avatar.vehicle - if (vehicle.contains(vehicle_guid)) { - if (vehicle == player.VehicleSeated) { - continent.GUID(vehicle_guid) match { - case Some(obj: Vehicle) => - log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state") - obj.Actor ! Deployment.TryDeploymentChange(deploy_state) - - case _ => - log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid") - avatarActor ! AvatarActor.SetVehicle(None) - } - } else { - log.warn(s"${player.Name} must be mounted to request a deployment change") - } - } else { - log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object") - } - } + def handleDeployRequest(pkt: DeployRequestMessage): Unit /* messages */ - def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { - if (state == DriveState.Deploying) { - log.trace(s"DeployRequest: $obj transitioning to deploy state") - } else if (state == DriveState.Deployed) { - log.trace(s"DeployRequest: $obj has been Deployed") - } else { - CanNotChangeDeployment(obj, state, "incorrect deploy state") - } - } + def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit - def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { - if (state == DriveState.Undeploying) { - log.trace(s"DeployRequest: $obj transitioning to undeploy state") - } else if (state == DriveState.Mobile) { - log.trace(s"DeployRequest: $obj is Mobile") - } else { - CanNotChangeDeployment(obj, state, "incorrect undeploy state") - } - } + def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit - def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = { - if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) { - CanNotChangeDeployment(obj, state, reason = "ground too steep") - } else { - CanNotChangeDeployment(obj, state, reason) - } - } + def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit +} - /* support functions */ +class VehicleOperations( + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], + implicit val context: ActorContext + ) extends CommonSessionInterfacingFunctionality { + private[session] var serverVehicleControlVelocity: Option[Int] = None /** * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat. @@ -604,34 +177,9 @@ class VehicleOperations( * the client's player who is receiving this packet should be mounted as its driver, but this is not explicitly tested * @param pkt packet to instigate cancellable control */ - def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = { + private def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = { serverVehicleControlVelocity = None vehicle.DeploymentState = DriveState.Mobile sendResponse(pkt) } - - /** - * Common reporting behavior when a `Deployment` object fails to properly transition between states. - * @param obj the game object that could not - * @param state the `DriveState` that could not be promoted - * @param reason a string explaining why the state can not or will not change - */ - def CanNotChangeDeployment( - obj: PlanetSideServerObject with Deployment, - state: DriveState.Value, - reason: String - ): Unit = { - val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) { - obj.DeploymentState = DriveState.Mobile - sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero) - ) - "; enforcing Mobile deployment state" - } else { - "" - } - log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift") - } } diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 156fe18bf..fc06890de 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -2,584 +2,77 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} -import net.psforever.objects.definition.ProjectileDefinition -import net.psforever.objects.serverobject.doors.InteriorDoorPassage -import net.psforever.objects.serverobject.interior.Sidedness -import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} import net.psforever.objects.zones.Zoning import net.psforever.objects.serverobject.turret.VanuSentry import net.psforever.objects.zones.exp.ToDatabase import scala.collection.mutable -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future import scala.concurrent.duration._ // import net.psforever.actors.session.{AvatarActor, ChatActor} -import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects.avatar.scoring.EquipmentStat -import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} -import net.psforever.objects.entity.SimpleWorldEntity -import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch} -import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} +import net.psforever.objects.ballistics.Projectile +import net.psforever.objects.equipment.EquipmentSize import net.psforever.objects.inventory.Container -import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.turret.FacilityTurret -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.vital.Vitality -import net.psforever.objects.vital.base.{DamageResolution, DamageType} -import net.psforever.objects.vital.etc.OicwLilBuddyReason -import net.psforever.objects.vital.interaction.DamageInteraction -import net.psforever.objects.vital.projectile.ProjectileReason -import net.psforever.objects.zones.{Zone, ZoneProjectile} import net.psforever.objects._ -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.packet.game._ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{ExoSuitType, PlanetSideGUID, Vector3} -import net.psforever.util.Config +import net.psforever.types.{ExoSuitType, PlanetSideGUID} -private[support] class WeaponAndProjectileOperations( - val sessionLogic: SessionLogic, - avatarActor: typed.ActorRef[AvatarActor.Command], - chatActor: typed.ActorRef[ChatActor.Command], - implicit val context: ActorContext +trait WeaponAndProjectileFunctions extends CommonSessionInterfacingFunctionality { + def ops: WeaponAndProjectileOperations + + def handleWeaponFire(pkt: WeaponFireMessage): Unit + + def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit + + def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit + + def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit + + def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit + + def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit + + def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit + + def handleReload(pkt: ReloadMessage): Unit + + def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit + + def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit + + def handleProjectileState(pkt: ProjectileStateMessage): Unit + + def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit + + def handleDirectHit(pkt: HitMessage): Unit + + def handleSplashHit(pkt: SplashHitMessage): Unit + + def handleLashHit(pkt: LashMessage): Unit + + def handleAIDamage(pkt: AIDamage): Unit +} + +class WeaponAndProjectileOperations( + val sessionLogic: SessionData, + val avatarActor: typed.ActorRef[AvatarActor.Command], + val chatActor: typed.ActorRef[ChatActor.Command], + implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start - private[support] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() - private[support] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() - private val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() - private val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() - private[support] var shotsWhileDead: Int = 0 - private val projectiles: Array[Option[Projectile]] = + private[session] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() + private[session] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() + private[session] val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() + private[session] val shotsLanded: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() + private[session] var shotsWhileDead: Int = 0 + private[session] val projectiles: Array[Option[Projectile]] = { Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) - - /* packets */ - - def handleWeaponFire(pkt: WeaponFireMessage): Unit = { - val WeaponFireMessage( - _, - weapon_guid, - projectile_guid, - shot_origin, - _, - _, - _, - _/*max_distance,*/, - _, - _/*projectile_type,*/, - thrown_projectile_vel - ) = pkt - HandleWeaponFireOperations(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten) - } - - def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { - val WeaponDelayFireMessage(_, _) = pkt - log.info(s"${player.Name} - $pkt") - } - - def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { - val WeaponDryFireMessage(weapon_guid) = pkt - val (containerOpt, tools) = FindContainedWeapon - tools - .find { _.GUID == weapon_guid } - .orElse { continent.GUID(weapon_guid) } - .collect { - case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.WeaponDryFire(player.GUID, weapon_guid) - ) - case _: Equipment => - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.WeaponDryFire(player.GUID, weapon_guid) - ) - } - .orElse { - log.warn( - s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist" - ) - None - } - } - - def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { - val WeaponLazeTargetPositionMessage(_, _, _) = pkt - //do not need to handle the progress bar animation/state on the server - //laze waypoint is requested by client upon completion (see SquadWaypointRequest) - val purpose = if (sessionLogic.squad.squad_supplement_id > 0) { - s" for ${player.Sex.possessive} squad (#${sessionLogic.squad.squad_supplement_id -1})" - } else { - " ..." - } - log.info(s"${player.Name} is lazing a position$purpose") - } - - def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { - val AvatarGrenadeStateMessage(_, state) = pkt - //TODO I thought I had this working? - log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade") - } - - def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start)(implicit context: ActorContext): Unit = { - val ChangeFireStateMessage_Start(item_guid) = pkt - if (shooting.isEmpty) { - sessionLogic.findEquipment(item_guid) match { - case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStartWhenPlayer(tool, item_guid) - case Some(tool: Tool) => - fireStateStartWhenMounted(tool, item_guid) - case Some(_) if player.VehicleSeated.isEmpty => - fireStateStartSetup(item_guid) - fireStateStartPlayerMessages(item_guid) - case Some(_) => - fireStateStartSetup(item_guid) - fireStateStartMountedMessages(item_guid) - case None => - log.warn(s"ChangeFireState_Start: can not find $item_guid") - } - } - } - - def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = { - val ChangeFireStateMessage_Stop(item_guid) = pkt - val now = System.currentTimeMillis() - prefire -= item_guid - shootingStop += item_guid -> now - shooting -= item_guid - sessionLogic.findEquipment(item_guid) match { - case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStopWhenPlayer(tool, item_guid) - case Some(tool: Tool) => - fireStateStopWhenMounted(tool, item_guid) - case Some(trigger: BoomerTrigger) => - fireStateStopPlayerMessages(item_guid) - continent.GUID(trigger.Companion).collect { - case boomer: BoomerDeployable => - boomer.Actor ! CommonMessages.Use(player, Some(trigger)) - } - case Some(_) if player.VehicleSeated.isEmpty => - fireStateStopPlayerMessages(item_guid) - case Some(_) => - fireStateStopMountedMessages(item_guid) - case _ => - log.warn(s"ChangeFireState_Stop: can not find $item_guid") - } - sessionLogic.general.progressBarUpdate.cancel() - sessionLogic.general.progressBarValue = None - } - - def handleReload(pkt: ReloadMessage): Unit = { - val ReloadMessage(item_guid, _, unk1) = pkt - FindContainedWeapon match { - case (Some(obj: Player), tools) => - handleReloadWhenPlayer(item_guid, obj, tools, unk1) - case (Some(obj: PlanetSideServerObject with Container), tools) => - handleReloadWhenMountable(item_guid, obj, tools, unk1) - case (_, _) => - log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") - } - } - - def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { - val ChangeAmmoMessage(item_guid, _) = pkt - val (thing, equipment) = sessionLogic.findContainedEquipment() - if (equipment.isEmpty) { - log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment") - } else { - equipment foreach { - case obj: ConstructionItem => - if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { - log.info( - s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" - ) - sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) - } - case tool: Tool => - thing match { - case Some(player: Player) => - PerformToolAmmoChange(tool, player, ModifyAmmunition(player)) - case Some(mountable: PlanetSideServerObject with Container) => - PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable)) - case _ => - log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") - } - case obj => - log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") - } - } - } - - def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { - val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt - sessionLogic.findEquipment(item_guid) match { - case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => - val originalModeIndex = obj.FireModeIndex - if (obj match { - case citem: ConstructionItem => - val modeChanged = Deployables.performConstructionItemFireModeChange( - player.avatar.certifications, - citem, - originalModeIndex - ) - modeChanged - case _ => - obj.NextFireMode - obj.FireModeIndex != originalModeIndex - }) { - val modeIndex = obj.FireModeIndex - obj match { - case citem: ConstructionItem => - log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)") - case _ => - log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex") - } - sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) - ) - } - case Some(_) => - log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") - case None => - log.warn(s"ChangeFireMode: can not find $item_guid") - } - } - - def handleProjectileState(pkt: ProjectileStateMessage): Unit = { - val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt - val index = projectile_guid.guid - Projectile.baseUID - projectiles(index) match { - case Some(projectile) if projectile.HasGUID => - val projectileGlobalUID = projectile.GUID - projectile.Position = shot_pos - projectile.Orientation = shot_orient - projectile.Velocity = shot_vel - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileState( - player.GUID, - projectileGlobalUID, - shot_pos, - shot_vel, - shot_orient, - seq, - end, - target_guid - ) - ) - case _ if seq == 0 => - /* missing the first packet in the sequence is permissible */ - case _ => - log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") - } - } - - def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { - val LongRangeProjectileInfoMessage(guid, _, _) = pkt - FindContainedWeapon match { - case (Some(_: Vehicle), weapons) - if weapons.exists { _.GUID == guid } => () //now what? - case _ => () - } - } - - def handleDirectHit(pkt: HitMessage): Unit = { - val HitMessage( - _, - projectile_guid, - _, - hit_info, - _, - _, - _ - ) = pkt - //find defined projectile - FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - //find target(s) - (hit_info match { - case Some(hitInfo) => - val hitPos = hitInfo.hit_pos - sessionLogic.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match { - case _ if projectile.profile == GlobalDefinitions.flail_projectile => - val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius - val targets = Zone.findAllTargets(continent, player, hitPos, projectile.profile) - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - targets.map { target => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - (target, projectile, hitPos, target.Position) - } - - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - List((target, projectile, hitInfo.shot_origin, hitPos)) - - case None => - HandleDamageProxy(projectile, projectile_guid, hitPos) - - case _ => - Nil - } - case None => - Nil - }) - .foreach { - case ( - target: PlanetSideGameObject with FactionAffinity with Vitality, - proj: Projectile, - _: Vector3, - hitPos: Vector3 - ) => - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - case None => - log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") - } - } - - def handleSplashHit(pkt: SplashHitMessage): Unit = { - val SplashHitMessage( - _, - projectile_guid, - explosion_pos, - direct_victim_uid, - _, - projectile_vel, - _, - targets - ) = pkt - FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - val profile = projectile.profile - projectile.Velocity = projectile_vel - val (resolution1, resolution2) = profile.Aggravated match { - case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) => - (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash) - case _ => - (DamageResolution.Splash, DamageResolution.Splash) - } - //direct_victim_uid - sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - //other victims - targets.foreach(elem => { - sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - }) - //... - HandleDamageProxy(projectile, projectile_guid, explosion_pos) - if ( - projectile.profile.HasJammedEffectDuration || - projectile.profile.JammerProjectile || - projectile.profile.SympatheticExplosion - ) { - //can also substitute 'projectile.profile' for 'SpecialEmp.emp' - Zone.serverSideDamage( - continent, - player, - SpecialEmp.emp, - SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos), - SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction), - SpecialEmp.findAllBoomers(profile.DamageRadius) - ) - } - if (profile.ExistsOnRemoteClients && projectile.HasGUID) { - //cleanup - if (projectile.HasGUID) { - continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) - } - } - case None => () - } - } - - def handleLashHit(pkt: LashMessage): Unit = { - val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt - sessionLogic.validObject(victim_guid, decorator = "Lash") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) - ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach { - resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - } - - def handleAIDamage(pkt: AIDamage): Unit = { - val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt - (continent.GUID(player.VehicleSeated) match { - case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer) - if tobj.GUID == targetGuid && - tobj.OwnerGuid.contains(player.GUID) => - //deployable turrets - Some(tobj) - case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable) - if tobj.GUID == targetGuid && - tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) => - //facility turrets, etc. - Some(tobj) - case _ - if player.GUID == targetGuid => - //player avatars - Some(player) - case _ => - None - }).collect { - case target: AutomatedTurret.Target => - sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") - .collect { - case turret: AutomatedTurret if turret.Target.isEmpty => - turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) - Some(target) - - case turret: AutomatedTurret => - turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) - HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)) - Some(target) - } - } - .orElse { - //occasionally, something that is not technically a turret's natural target may be attacked - sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target") - .collect { - case target: PlanetSideServerObject with FactionAffinity with Vitality => - sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") - .collect { - case turret: AutomatedTurret if turret.Target.nonEmpty => - //the turret must be shooting at something (else) first - HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)) - } - Some(target) - } - } - } - - /* support code */ - - def HandleWeaponFireOperations( - weaponGUID: PlanetSideGUID, - projectileGUID: PlanetSideGUID, - shotOrigin: Vector3, - shotVelocity: Option[Vector3] - ): Unit = { - HandleWeaponFireAccountability(weaponGUID, projectileGUID) match { - case (Some(obj), Some(tool)) => - val projectileIndex = projectileGUID.guid - Projectile.baseUID - val projectilePlace = projectiles(projectileIndex) - if ( - projectilePlace match { - case Some(projectile) => - !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong - case None => - false - } - ) { - log.debug( - s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}" - ) - } - val (angle, attribution, acceptableDistanceToOwner) = obj match { - case p: Player => - ( - SimpleWorldEntity.validateOrientationEntry( - p.Orientation + Vector3.z(p.FacingYawUpper) - ), - tool.Definition.ObjectId, - 10f + (if (p.Velocity.nonEmpty) { - 5f - } else { - 0f - }) - ) - case v: Vehicle if v.Definition.CanFly => - (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle - case _: Vehicle => - (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle - case _ => - (obj.Orientation, obj.Definition.ObjectId, 300f) - } - val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position) - if (distanceToOwner <= acceptableDistanceToOwner) { - val projectile_info = tool.Projectile - val wguid = weaponGUID.guid - val mountedIn = (continent.turretToWeapon - .find { case (guid, _) => guid == wguid } match { - case Some((_, turretGuid)) => Some(( - turretGuid, - continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } - )) - case _ => None - }) match { - case Some((guid, Some(entity))) => Some((guid, entity)) - case _ => None - } - val projectile = new Projectile( - projectile_info, - tool.Definition, - tool.FireMode, - mountedIn, - PlayerSource(player), - attribution, - shotOrigin, - angle, - shotVelocity - ) - val initialQuality = tool.FireMode match { - case mode: ChargeFireModeDefinition => - ProjectileQuality.Modified( - { - val timeInterval = projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) - timeInterval.toFloat / mode.Time.toFloat - } - ) - case _ => - ProjectileQuality.Normal - } - val qualityprojectile = projectile.quality(initialQuality) - qualityprojectile.WhichSide = player.WhichSide - projectiles(projectileIndex) = Some(qualityprojectile) - if (projectile_info.ExistsOnRemoteClients) { - log.trace( - s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" - ) - continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) - } - } else { - log.warn( - s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect" - ) - } - - case _ => () - } } def HandleWeaponFireAccountability( @@ -676,18 +169,6 @@ private[support] class WeaponAndProjectileOperations( (o, enabledTools) } - /** - * For a certain weapon that cna load ammunition, enforce that its magazine is empty. - * @param weapon_guid the weapon - */ - def EmptyMagazine(weapon_guid: PlanetSideGUID): Unit = { - continent.GUID(weapon_guid) match { - case Some(tool: Tool) => - EmptyMagazine(weapon_guid, tool) - case _ => () - } - } - /** * For a certain weapon that can load ammunition, enforce that its magazine is empty. * Punctuate that emptiness with a ceasation of weapons fire and a dry fire sound effect. @@ -706,209 +187,6 @@ private[support] class WeaponAndProjectileOperations( continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid)) } - /** - * After a weapon has finished shooting, determine if it needs to be sorted in a special way. - * @param tool a weapon - */ - def FireCycleCleanup(tool: Tool): Unit = { - //TODO replaced by more appropriate functionality in the future - val tdef = tool.Definition - if (GlobalDefinitions.isGrenade(tdef)) { - val ammoType = tool.AmmoType - FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters - case Nil => - log.info(s"${player.Name} has no more $ammoType grenades to throw") - RemoveOldEquipmentFromInventory(player)(tool) - - case x :: xs => //this is similar to ReloadMessage - val box = x.obj.asInstanceOf[Tool] - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } - val sumReloadValue: Int = box.Magazine + tailReloadValue - val actualReloadValue = if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player)(x.obj) - sumReloadValue - } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - } - log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ModifyAmmunition(player)( - tool.AmmoSlot.Box, - -actualReloadValue - ) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) - } - } else if (tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player)(tool) - } - } - - /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - val capacity = box.Capacity - reloadValue - box.Capacity = capacity - sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) - } - - /** - * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - ModifyAmmunition(obj)(box, reloadValue) - obj.Find(box).collect { index => - continent.VehicleEvents ! VehicleServiceMessage( - s"${obj.Actor}", - VehicleAction.InventoryState( - player.GUID, - box, - obj.GUID, - index, - box.Definition.Packet.DetailedConstructorData(box).get - ) - ) - } - } - - /** - * na - * @param tool na - * @param obj na - */ - def PerformToolAmmoChange( - tool: Tool, - obj: PlanetSideServerObject with Container, - modifyFunc: (AmmoBox, Int) => Unit - ): Unit = { - val originalAmmoType = tool.AmmoType - do { - val requestedAmmoType = tool.NextAmmoType - val fullMagazine = tool.MaxMagazine - if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { - case Nil => () - case x :: xs => - val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj) - val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) - - xs.foreach(item => { - obj.Inventory -= item.start - sendResponse(ObjectDeleteMessage(item.obj.GUID, 0)) - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj)) - }) - - //box will be the replacement ammo; give it the discovered magazine and load it into the weapon - val box = x.obj.asInstanceOf[AmmoBox] - //previousBox is the current magazine in tool; it will be removed from the weapon - val previousBox = tool.AmmoSlot.Box - val originalBoxCapacity = box.Capacity - val tailReloadValue: Int = if (xs.isEmpty) { - 0 - } else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue: Int = originalBoxCapacity + tailReloadValue - val ammoSlotIndex = tool.FireMode.AmmoSlotIndex - val box_guid = box.GUID - val tool_guid = tool.GUID - obj.Inventory -= x.start //remove replacement ammo from inventory - tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool - sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f)) - sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f)) - sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex)) - - //announce swapped ammunition box in weapon - val previous_box_guid = previousBox.GUID - val boxDef = box.Definition - sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeAmmo( - player.GUID, - tool_guid, - ammoSlotIndex, - previous_box_guid, - boxDef.ObjectId, - box.GUID, - boxDef.Packet.ConstructorData(box).get - ) - ) - - //handle inventory contents - box.Capacity = if (sumReloadValue <= fullMagazine) { - sumReloadValue - } else { - val splitReloadAmmo: Int = sumReloadValue - fullMagazine - log.trace( - s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo" - ) - val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) - TaskWorkflow.execute(stowNewFunc(boxForInventory)) - fullMagazine - } - sendResponse( - InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) - ) //should work for both players and vehicles - log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}") - if (previousBox.Capacity > 0) { - //divide capacity across other existing and not full boxes of that ammo type - var capacity = previousBox.Capacity - val iter = obj.Inventory.Items - .filter(entry => { - entry.obj match { - case item: AmmoBox => - item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity - case _ => - false - } - }) - .sortBy(_.start) - .iterator - while (capacity > 0 && iter.hasNext) { - val entry = iter.next() - val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox] - val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) - log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") - capacity -= ammoAllocated - modifyFunc(item, -ammoAllocated) - } - previousBox.Capacity = capacity - } - - if (previousBox.Capacity > 0) { - //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm - obj.Inventory.Fit(previousBox) match { - case Some(_) => - stowFunc(previousBox) - case None => - sessionLogic.general.normalItemDrop(player, continent)(previousBox) - } - AmmoBox.Split(previousBox) match { - case Nil | List(_) => () //done (the former case is technically not possible) - case _ :: toUpdate => - modifyFunc(previousBox, 0) //update to changed capacity value - toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) - } - } else { - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox)) - } - } - } - } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) - } - /** * The main purpose of this method is to determine which targets will receive "locked on" warnings from remote projectiles. * For a given series of globally unique identifiers, indicating targets, @@ -933,20 +211,6 @@ private[support] class WeaponAndProjectileOperations( } } - def CheckForHitPositionDiscrepancy( - projectile_guid: PlanetSideGUID, - hitPos: Vector3, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): Unit = { - val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position) - if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { - // If the target position on the server does not match the position where the projectile landed within reason there may be foul play - log.warn( - s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" - ) - } - } - /** * Given a globally unique identifier in the 40100 to 40124 range * (with an optional 25 as buffer), @@ -964,197 +228,6 @@ private[support] class WeaponAndProjectileOperations( } } - /** - * Find a projectile with the given globally unique identifier and mark it as a resolved shot. - * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle. - * @param projectile_guid the projectile GUID - * @param resolution the resolution status to promote the projectile - * @return the projectile - */ - def ResolveProjectileInteraction( - projectile_guid: PlanetSideGUID, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - ResolveProjectileInteraction(projectile, resolution, target, pos) - case None => - log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found") - None - } - } - - /** - * Find a projectile with the given globally unique identifier and mark it as a resolved shot. - * @param projectile the projectile object - * @param index where the projectile was found - * @param resolution the resolution status to promote the projectile - * @return a copy of the projectile - */ - def ResolveProjectileInteraction( - projectile: Projectile, - index: Int, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - if (!projectiles(index).contains(projectile)) { - log.error(s"expected projectile could not be found at $index; can not resolve") - None - } else { - ResolveProjectileInteraction(projectile, resolution, target, pos) - } - } - - /** - * na - * @param projectile the projectile object - * @param resolution the resolution status to promote the projectile - * @return a copy of the projectile - */ - def ResolveProjectileInteraction( - projectile: Projectile, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - if (projectile.isMiss) { - log.warn("expected projectile was already counted as a missed shot; can not resolve any further") - None - } else { - val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player)) - if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) { - avatarActor ! AvatarActor.ConsumeStamina(10) - } - Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos)) - } - } - - /** - * Take a projectile that was introduced into the game world and - * determine if it generates a secondary damage projectile or - * an method of damage causation that requires additional management. - * @param projectile the projectile - * @param pguid the client-local projectile identifier - * @param hitPos the game world position where the projectile is being recorded - * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location; - * nothing if no targets were affected - */ - def HandleDamageProxy( - projectile: Projectile, - pguid: PlanetSideGUID, - hitPos: Vector3 - ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { - GlobalDefinitions.getDamageProxy(projectile, hitPos) match { - case Nil => - Nil - case list if list.isEmpty => - Nil - case list => - HandleDamageProxySetupLittleBuddy(list, hitPos) - UpdateProjectileSidednessAfterHit(projectile, hitPos) - val projectileSide = projectile.WhichSide - list.flatMap { proxy => - if (proxy.profile.ExistsOnRemoteClients) { - proxy.Position = hitPos - proxy.WhichSide = projectileSide - continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy) - Nil - } else if (proxy.tool_def == GlobalDefinitions.maelstrom) { - //server-side maelstrom grenade target selection - val radius = proxy.profile.LashRadius * proxy.profile.LashRadius - val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList }) - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - //chainlash is separated from the actual damage application for convenience - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.SendResponse( - PlanetSideGUID(0), - ChainLashMessage( - hitPos, - projectile.profile.ObjectId, - targets.map { _.GUID } - ) - ) - ) - targets.map { target => - CheckForHitPositionDiscrepancy(pguid, hitPos, target) - (target, proxy, hitPos, target.Position) - } - } else { - Nil - } - } - } - } - - def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = { - val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw } - val size: Int = listOfLittleBuddies.size - if (size > 0) { - val desiredDownwardsProjectiles: Int = 2 - val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down - val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out - val z: Float = player.Orientation.z //player's standing direction - val north: Vector3 = Vector3(0,1,0) //map North - val speed: Float = 144f //speed (packet discovered) - val dist: Float = 25 //distance (client defined) - val downwardsAngle: Float = -85f - val flaredAngle: Float = -70f - //angle of separation for downwards, degrees from vertical for flared out - val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) { - (360f / firstHalf, downwardsAngle) - } else { - (0f, 0f) - } - val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) { - (360f / secondHalf, flaredAngle) - } else { - (0f, 0f) - } - val smallRotOffset: Float = z + 90f - val largeRotOffset: Float = z + math.random().toFloat * 45f - val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat) - //downwards projectiles - var i: Int = 0 - listOfLittleBuddies.take(firstHalf).foreach { proxy => - val facing = (smallRotOffset + smallStep * i.toFloat) % 360 - val dir = north.Rx(smallAngle).Rz(facing) - proxy.Position = detonationPosition + dir.xy + verticalCorrection - proxy.Velocity = dir * speed - proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing) - HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) - i += 1 - } - //flared out projectiles - i = 0 - listOfLittleBuddies.drop(firstHalf).foreach { proxy => - val facing = (largeRotOffset + largeStep * i.toFloat) % 360 - val dir = north.Rx(largeAngle).Rz(facing) - proxy.Position = detonationPosition + dir - proxy.Velocity = dir * speed - proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing) - HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) - i += 1 - } - true - } else { - false - } - } - - def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = { - //explosion - val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction) - obj.Position = obj.Position + orientation * distance - val explosionFunc: ()=>Unit = WeaponAndProjectileOperations.detonateLittleBuddy(continent, obj, proxy, proxy.owner) - context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() } - } - /** * Check two locations for a controlled piece of equipment that is associated with the `player`. * Filter for discovered `Tool`-type `Equipment`. @@ -1194,103 +267,14 @@ private[support] class WeaponAndProjectileOperations( */ def FindWeapon: Set[Tool] = FindContainedWeapon._2 - /* - used by ChangeFireStateMessage_Start handling - */ - private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { - prefire -= itemGuid - shooting += itemGuid - shootingStart += itemGuid -> System.currentTimeMillis() - } - - private def fireStateStartChargeMode(tool: Tool): Unit = { - //charge ammunition drain - tool.FireMode match { - case mode: ChargeFireModeDefinition => - sessionLogic.general.progressBarValue = Some(0f) - sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( - (mode.Time + mode.DrainInterval) milliseconds, - context.self, - CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) - ) - case _ => () - } - } - - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionLogic.findContainedEquipment()._1.collect { - case turret: FacilityTurret if continent.map.cavern => - turret.Actor ! VanuSentry.ChangeFireStart - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - private def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { - tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(itemGuid) - } - - private def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - log.warn( - s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" - ) - EmptyMagazine(itemGuid, tool) - } - - private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - //special case - suppress the decimator's alternate fire mode, by projectile - if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - fireStateStartMountedMessages(itemGuid) - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - /* - used by ChangeFireStateMessage_Stop handling - */ - private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => () - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - } - - private def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { continent.AvatarEvents ! AvatarServiceMessage( continent.id, AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) ) } - private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { + def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { sessionLogic.findContainedEquipment()._1.collect { case turret: FacilityTurret if continent.map.cavern => turret.Actor ! VanuSentry.ChangeFireStop @@ -1301,136 +285,11 @@ private[support] class WeaponAndProjectileOperations( ) } - private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - //suppress the decimator's alternate fire mode, however - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStopUpdateChargeAndCleanup(tool) - fireStateStopPlayerMessages(itemGuid) - } - - private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - fireStateStopUpdateChargeAndCleanup(tool) - fireStateStopMountedMessages(itemGuid) - } - - /* - used by ReloadMessage handling - */ - private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, itemGuid) - ) - } - - private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.Reload(player.GUID, itemGuid) - ) - } - - private def handleReloadProcedure( - itemGuid: PlanetSideGUID, - obj: PlanetSideGameObject with Container, - tools: Set[Tool], - unk1: Int, - deleteFunc: Equipment => Future[Any], - modifyFunc: (AmmoBox, Int) => Unit, - messageFunc: PlanetSideGUID => Unit - ): Unit = { - tools - .filter { _.GUID == itemGuid } - .foreach { tool => - val currentMagazine : Int = tool.Magazine - val magazineSize : Int = tool.MaxMagazine - val reloadValue : Int = magazineSize - currentMagazine - if (magazineSize > 0 && reloadValue > 0) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { - case Nil => () - case x :: xs => - xs.foreach { item => deleteFunc(item.obj) } - val box = x.obj.asInstanceOf[AmmoBox] - val tailReloadValue : Int = if (xs.isEmpty) { - 0 - } - else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue : Int = box.Capacity + tailReloadValue - val actualReloadValue = if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } - else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - } - val finalReloadValue = actualReloadValue + currentMagazine - log.info( - s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" - ) - tool.Magazine = finalReloadValue - sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1)) - messageFunc(itemGuid) - } - } else { - //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) - } - } - } - - private def handleReloadWhenPlayer( - itemGuid: PlanetSideGUID, - obj: Player, - tools: Set[Tool], - unk1: Int - ): Unit = { - handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunition(obj)(_, _), - reloadPlayerMessages - ) - } - - private def handleReloadWhenMountable( - itemGuid: PlanetSideGUID, - obj: PlanetSideServerObject with Container, - tools: Set[Tool], - unk1: Int - ): Unit = { - handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunitionInMountable(obj)(_, _), - reloadVehicleMessages - ) - } - private def addShotsFired(weaponId: Int, shots: Int): Unit = { addShotsToMap(shotsFired, weaponId, shots) } - //noinspection SameParameterValue - private def addShotsLanded(weaponId: Int, shots: Int): Unit = { - addShotsToMap(shotsLanded, weaponId, shots) - } - - private def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = { + def addShotsToMap(map: mutable.HashMap[Int, Int], weaponId: Int, shots: Int): Unit = { map.put( weaponId, map.get(weaponId) match { @@ -1440,11 +299,11 @@ private[support] class WeaponAndProjectileOperations( ) } - private[support] def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = { + private def reportOngoingShots(reportFunc: (Long, Int, Int, Int) => Unit): Unit = { reportOngoingShots(player.CharId, reportFunc) } - private[support] def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = { + private def reportOngoingShots(avatarId: Long, reportFunc: (Long, Int, Int, Int) => Unit): Unit = { //only shots that have been reported as fired count //if somehow shots had reported as landed but never reported as fired, they are ignored //these are just raw counts; there's only numeric connection between the entries of fired and of landed @@ -1456,135 +315,10 @@ private[support] class WeaponAndProjectileOperations( shotsLanded.clear() } - //noinspection ScalaUnusedSymbol - private[support] def reportOngoingShotsToAvatar(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = { - avatarActor ! AvatarActor.UpdateToolDischarge(EquipmentStat(weaponId, fired, landed, 0, 0)) - } - - private[support] def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = { + private def reportOngoingShotsToDatabase(avatarId: Long, weaponId: Int, fired: Int, landed: Int): Unit = { ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0)) } - private def CompileAutomatedTurretDamageData( - turret: AutomatedTurret, - owner: SourceEntry, - projectileTypeId: Long - ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { - turret.Weapons - .values - .flatMap { _.Equipment } - .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) } - .find { case (_, _, _, p) => p.ObjectId == projectileTypeId } - } - - private def HandleAIDamage( - target: PlanetSideServerObject with FactionAffinity with Vitality, - results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] - ): Unit = { - results.collect { - case (obj, tool, owner, projectileInfo) => - val angle = Vector3.Unit(target.Position - obj.Position) - val proj = new Projectile( - projectileInfo, - tool.Definition, - tool.FireMode, - None, - owner, - obj.Definition.ObjectId, - obj.Position + Vector3.z(value = 1f), - angle, - Some(angle * projectileInfo.FinalVelocity) - ) - val hitPos = target.Position + Vector3.z(value = 1f) - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - } - } - - private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = { - val origin = projectile.Position - val distance = Vector3.Magnitude(hitPosition - origin) - continent.blockMap - .sector(hitPosition, distance) - .environmentList - .collect { case o: InteriorDoorPassage => - val door = o.door - val intersectTest = quickLineSphereIntersectionPoints( - origin, - hitPosition, - door.Position, - door.Definition.UseRadius + 0.1f - ) - (door, intersectTest) - } - .collect { case (door, intersectionTest) if intersectionTest.nonEmpty => - (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest) - } - .minByOption { case (_, dist, _) => dist } - .foreach { case (door, _, intersects) => - val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) { - Sidedness.OutsideOf - } else { - Sidedness.InsideOf - } - projectile.WhichSide = if (intersects.size == 1) { - Sidedness.InBetweenSides(door, strictly) - } else { - strictly - } - } - } - - /** - * Does a line segment line intersect with a sphere?
- * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package. - * @param start first point of the line segment - * @param end second point of the line segment - * @param center center of the sphere - * @param radius radius of the sphere - * @return list of all points of intersection, if any - * @see `Vector3.DistanceSquared` - * @see `Vector3.MagnitudeSquared` - */ - private def quickLineSphereIntersectionPoints( - start: Vector3, - end: Vector3, - center: Vector3, - radius: Float - ): Iterable[Vector3] = { - /* - Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere, - because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation. - */ - val Vector3(cx, cy, cz) = center - val Vector3(sx, sy, sz) = start - val vector = end - start - //speed our way through a quadratic equation - val (a, b) = { - val Vector3(dx, dy, dz) = vector - ( - dx * dx + dy * dy + dz * dz, - 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz)) - ) - } - val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius - val result = b * b - 4 * a * c - if (result < 0f) { - //negative, no intersection - Seq() - } else if (result < 0.00001f) { - //zero-ish, one intersection point - Seq(start - vector * (b / (2f * a))) - } else { - //positive, two intersection points - val sqrt = math.sqrt(result).toFloat - val endStart = vector / (2f * a) - Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f) - }.filter(p => Vector3.DistanceSquared(start, p) <= a) - } - override protected[support] def actionsToCancel(): Unit = { shootingStart.clear() shootingStop.clear() @@ -1599,7 +333,7 @@ private[support] class WeaponAndProjectileOperations( shooting.clear() } - override protected[support] def stop(): Unit = { + override protected[session] def stop(): Unit = { if (player != null && player.HasGUID) { (prefire ++ shooting).foreach { guid => //do I need to do this? (maybe) @@ -1611,50 +345,3 @@ private[support] class WeaponAndProjectileOperations( } } } - -object WeaponAndProjectileOperations { - /** - * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. - * The main difference from "normal" server-side explosion - * is that the owner of the projectile must be clarified explicitly. - * @see `Zone::serverSideDamage` - * @param zone where the explosion is taking place - * (`source` contains the coordinate location) - * @param source a game object that represents the source of the explosion - * @param owner who or what to accredit damage from the explosion to; - * clarifies a normal `SourceEntry(source)` accreditation - */ - private def detonateLittleBuddy( - zone: Zone, - source: PlanetSideGameObject with FactionAffinity with Vitality, - proxy: Projectile, - owner: SourceEntry - )(): Unit = { - Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position)) - } - - /** - * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. - * The main difference from "normal" server-side explosion - * is that the owner of the projectile must be clarified explicitly. - * The sub-projectiles will be the product of a normal projectile rather than a standard game object - * so a custom `source` entity must wrap around it and fulfill the requirements of the field. - * @see `Zone::explosionDamage` - * @param owner who or what to accredit damage from the explosion to - * @param explosionPosition where the explosion will be positioned in the game world - * @param source a game object that represents the source of the explosion - * @param target a game object that is affected by the explosion - * @return a `DamageInteraction` object - */ - private def littleBuddyExplosionDamage( - owner: SourceEntry, - projectileId: Long, - explosionPosition: Vector3 - ) - ( - source: PlanetSideGameObject with FactionAffinity with Vitality, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): DamageInteraction = { - DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition) - } -} diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index ee4dbf6f0..f40239b12 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -73,7 +73,7 @@ import net.psforever.util.{Config, DefinitionUtil} import net.psforever.zones.Zones object ZoningOperations { - private[support] final case class AvatarAwardMessageBundle( + private[session] final case class AvatarAwardMessageBundle( bundle: Iterable[Iterable[PlanetSideGamePacket]], delay: Long ) @@ -106,24 +106,26 @@ object ZoningOperations { } class ZoningOperations( - val sessionLogic: SessionLogic, + val sessionLogic: SessionData, avatarActor: typed.ActorRef[AvatarActor.Command], galaxyService: ActorRef, cluster: typed.ActorRef[ICS.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { - private var zoningType: Zoning.Method = Zoning.Method.None - private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT - private[support] var zoningStatus: Zoning.Status = Zoning.Status.None - private var zoningCounter: Int = 0 - private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None + private[session] var zoningStatus: Zoning.Status = Zoning.Status.None + /** a flag for the zone having finished loading during zoning + * `None` when no zone is loaded + * `Some(true)` when a zone has successfully loaded + * `Some(false)` when the loading process has failed or was executed but did not complete for some reason + */ + private[session] var zoneLoaded: Option[Boolean] = None /** * used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone) * used during intrazone gate transfers, but not in a way distinct from prior zone transfer procedures * should only be set during the transient period when moving between one spawn point and the next * leaving set prior to a subsequent transfers may cause unstable vehicle associations, with memory leak potential */ - private[support] var interstellarFerry: Option[Vehicle] = None + private[session] var interstellarFerry: Option[Vehicle] = None /** * used during zone transfers for cleanup to refer to the vehicle that instigated a transfer * "top level" is the carrier in a carrier/ferried association or a projected carrier/(ferried carrier)/ferried association @@ -131,20 +133,18 @@ class ZoningOperations( * the old-zone unique identifier for the carrier * no harm should come from leaving the field set to an old unique identifier value after the transfer period */ - private[support] var interstellarFerryTopLevelGUID: Option[PlanetSideGUID] = None - private var loadConfZone: Boolean = false - /** a flag for the zone having finished loading during zoning - * `None` when no zone is loaded - * `Some(true)` when a zone has successfully loaded - * `Some(false)` when the loading process has failed or was executed but did not complete for some reason - */ - private[support] var zoneLoaded: Option[Boolean] = None + private[session] var interstellarFerryTopLevelGUID: Option[PlanetSideGUID] = None /** a flag that forces the current zone to reload itself during a zoning operation */ - private[support] var zoneReload: Boolean = false - private var zoningTimer: Cancellable = Default.Cancellable - + private[session] var zoneReload: Boolean = false private[session] val spawn: SpawnOperations = new SpawnOperations() + private var loadConfZone: Boolean = false + private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None + private var zoningType: Zoning.Method = Zoning.Method.None + private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT + private var zoningCounter: Int = 0 + private var zoningTimer: Cancellable = Default.Cancellable + /* packets */ def handleWarpgateRequest(pkt: WarpgateRequest): Unit = { @@ -181,7 +181,7 @@ class ZoningOperations( } } - def handleDroppodLaunchRequest(pkt: DroppodLaunchRequestMessage)(implicit context: ActorContext): Unit = { + def handleDroppodLaunchRequest(pkt: DroppodLaunchRequestMessage): Unit = { val DroppodLaunchRequestMessage(info, _) = pkt cluster ! ICS.DroppodLaunchRequest( info.zone_number, @@ -1752,15 +1752,15 @@ class ZoningOperations( /* nested class - spawn operations */ class SpawnOperations() { - private[support] var deadState: DeadState.Value = DeadState.Dead - private[support] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]() - private[support] var amsSpawnPoints: List[SpawnPoint] = Nil - private[support] var noSpawnPointHere: Boolean = false - private[support] var setupAvatarFunc: () => Unit = AvatarCreate - private[support] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally - private[support] var nextSpawnPoint: Option[SpawnPoint] = None - private[support] var interimUngunnedVehicle: Option[PlanetSideGUID] = None - private[support] var interimUngunnedVehicleSeat: Option[Int] = None + private[session] var deadState: DeadState.Value = DeadState.Dead + private[session] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]() + private[session] var amsSpawnPoints: List[SpawnPoint] = Nil + private[session] var noSpawnPointHere: Boolean = false + private[session] var setupAvatarFunc: () => Unit = AvatarCreate + private[session] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally + private[session] var nextSpawnPoint: Option[SpawnPoint] = None + private[session] var interimUngunnedVehicle: Option[PlanetSideGUID] = None + private[session] var interimUngunnedVehicleSeat: Option[Int] = None /** Upstream message counter
* Checks for server acknowledgement of the following messages in the following conditions:
* `PlayerStateMessageUpstream` (infantry)
@@ -1769,14 +1769,14 @@ class ZoningOperations( * `KeepAliveMessage` (any passenger mount that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second */ - private[support] var upstreamMessageCount: Int = 0 - private[support] var shiftPosition: Option[Vector3] = None - private[support] var shiftOrientation: Option[Vector3] = None - private[support] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons - private[support] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery - private[support] var setAvatar: Boolean = false - private[support] var reviveTimer: Cancellable = Default.Cancellable - private[support] var respawnTimer: Cancellable = Default.Cancellable + private[session] var upstreamMessageCount: Int = 0 + private[session] var shiftPosition: Option[Vector3] = None + private[session] var shiftOrientation: Option[Vector3] = None + private[session] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons + private[session] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery + private[session] var setAvatar: Boolean = false + private[session] var reviveTimer: Cancellable = Default.Cancellable + private[session] var respawnTimer: Cancellable = Default.Cancellable private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields @@ -1790,7 +1790,7 @@ class ZoningOperations( HandleReleaseAvatar(player, continent) } - def handleSpawnRequest(pkt: SpawnRequestMessage)(implicit context: ActorContext): Unit = { + def handleSpawnRequest(pkt: SpawnRequestMessage): Unit = { val SpawnRequestMessage(_, spawnGroup, _, _, zoneNumber) = pkt log.info(s"${player.Name} on ${continent.id} wants to respawn in zone #$zoneNumber") if (deadState != DeadState.RespawnTime) { @@ -3567,6 +3567,17 @@ class ZoningOperations( nextSpawnPoint = None } } + + def randomRespawn(time: FiniteDuration = 300.seconds): Unit = { + reviveTimer = context.system.scheduler.scheduleOnce(time) { + cluster ! ICS.GetRandomSpawnPoint( + Zones.sanctuaryZoneNumber(player.Faction), + player.Faction, + Seq(SpawnGroup.Sanctuary), + context.self + ) + } + } } override protected[session] def stop(): Unit = {