PSF-BotServer/pslogin/src/main/scala/WorldSessionActor.scala

9399 lines
430 KiB
Scala

// Copyright (c) 2017 PSForever
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import net.psforever.packet._
import net.psforever.packet.control._
import net.psforever.packet.game._
import scodec.Attempt.{Failure, Successful}
import scodec.bits._
import org.log4s.{Logger, MDC}
import MDCContextAware.Implicits._
import csr.{CSRWarp, CSRZone, Traveler}
import net.psforever.objects.GlobalDefinitions._
import services.ServiceManager.Lookup
import net.psforever.objects._
import net.psforever.objects.avatar.{Certification, DeployableToolbox}
import net.psforever.objects.ballistics._
import net.psforever.objects.ce._
import net.psforever.objects.definition._
import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter}
import net.psforever.objects.equipment.{CItem, _}
import net.psforever.objects.loadouts._
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech
import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.mblocker.Locker
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.serverobject.pad.process.{AutoDriveControls, VehicleSpawnControlGuided}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals._
import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret}
import net.psforever.objects.teamwork.Squad
import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _}
import net.psforever.objects.vital._
import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector}
import net.psforever.packet.game.objectcreate._
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo}
import net.psforever.types._
import services.{RemoverActor, vehicle, _}
import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
import services.galaxy.{GalaxyResponse, GalaxyServiceResponse}
import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
import services.vehicle.support.TurretUpgrader
import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import services.teamwork.{SquadAction => SquadServiceAction, SquadServiceMessage, SquadServiceResponse, SquadResponse, SquadService}
import scala.collection.mutable.LongMap
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.annotation.tailrec
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
import scala.util.Success
import akka.pattern.ask
import net.psforever.objects.vehicles.Utility.InternalTelepad
import services.local.support.{HackCaptureActor, RouterTelepadActivation}
import services.support.SupportActor
class WorldSessionActor extends Actor with MDCContextAware {
import WorldSessionActor._
private[this] val log = org.log4s.getLogger
private[this] val damageLog = org.log4s.getLogger("DamageResolution")
var sessionId : Long = 0
var leftRef : ActorRef = ActorRef.noSender
var rightRef : ActorRef = ActorRef.noSender
var avatarService : ActorRef = ActorRef.noSender
var localService : ActorRef = ActorRef.noSender
var vehicleService : ActorRef = ActorRef.noSender
var galaxyService : ActorRef = ActorRef.noSender
var squadService : ActorRef = ActorRef.noSender
var taskResolver : ActorRef = Actor.noSender
var cluster : ActorRef = Actor.noSender
var continent : Zone = Zone.Nowhere
var player : Player = null
var avatar : Avatar = null
var progressBarValue : Option[Float] = None
var shooting : Option[PlanetSideGUID] = None //ChangeFireStateMessage_Start
var prefire : Option[PlanetSideGUID] = None //if WeaponFireMessage precedes ChangeFireStateMessage_Start
var accessedContainer : Option[PlanetSideGameObject with Container] = None
var flying : Boolean = false
var speed : Float = 1.0f
var spectator : Boolean = false
var admin : Boolean = false
var usingMedicalTerminal : Option[PlanetSideGUID] = None
var controlled : Option[Int] = None
//keep track of avatar's ServerVehicleOverride state
var traveler : Traveler = null
var deadState : DeadState.Value = DeadState.Dead
var whenUsedLastKit : Long = 0
val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None)
var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
var recentTeleportAttempt : Long = 0
var lastTerminalOrderFulfillment : Boolean = true /**
* 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
*/
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
* inherited from parent (carrier) to child (ferried) through the `TransferPassenger` message
* 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
*/
var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
/**
* `AvatarConverter` can only rely on the `Avatar`-local Looking For Squad variable.
* When joining or creating a squad, the original state of the avatar's LFS variable is stored here.
* Upon leaving or disbanding a squad, this value is restored to the avatar's LFS variable.
*/
var lfs : Boolean = false
var amsSpawnPoints : List[SpawnPoint] = Nil
var clientKeepAlive : Cancellable = DefaultCancellable.obj
var progressBarUpdate : Cancellable = DefaultCancellable.obj
var reviveTimer : Cancellable = DefaultCancellable.obj
var respawnTimer : Cancellable = DefaultCancellable.obj
var cargoMountTimer : Cancellable = DefaultCancellable.obj
var cargoDismountTimer : Cancellable = DefaultCancellable.obj
var antChargingTick : Cancellable = DefaultCancellable.obj
var antDischargingTick : Cancellable = DefaultCancellable.obj
/**
* Convert a boolean value into an integer value.
* Use: `true:Int` or `false:Int`
* @param b `true` or `false` (or `null`)
* @return 1 for `true`; 0 for `false`
*/
implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0
override def postStop() : Unit = {
//TODO normally, player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
clientKeepAlive.cancel
reviveTimer.cancel
respawnTimer.cancel
PlayerActionsToCancel()
localService ! Service.Leave()
vehicleService ! Service.Leave()
avatarService ! Service.Leave()
galaxyService ! Service.Leave()
LivePlayerList.Remove(sessionId)
if(player != null && player.HasGUID) {
squadService ! Service.Leave(Some(player.CharId.toString))
val player_guid = player.GUID
//handle orphaned deployables
DisownDeployables()
//clean up boomer triggers and telepads
val equipment = (
(player.Holsters()
.zipWithIndex
.map({ case ((slot, index)) => (index, slot.Equipment) })
.collect { case ((index, Some(obj))) => InventoryItem(obj, index) }
) ++ player.Inventory.Items)
.filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] || obj.isInstanceOf[Telepad] })
//put any temporary value back into the avatar
if(squadUI.nonEmpty) {
avatar.LFS = lfs
}
//TODO final character save before doing any of this (use equipment)
continent.Population ! Zone.Population.Release(avatar)
if(player.isAlive) {
//actually being alive or manually deconstructing
player.Position = Vector3.Zero
//if seated, dismount
player.VehicleSeated match {
case Some(_) =>
//quickly and briefly kill player to avoid disembark animation?
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0))
DismountVehicleOnLogOut()
case _ => ;
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid))
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
//TODO normally, the actual player avatar persists a minute or so after the user disconnects
}
else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) {
//player disconnected while waiting for a revive, maybe
//similar to handling ReleaseAvatarRequestMessage
player.Release
player.VehicleSeated match {
case None =>
FriskCorpse(player) //TODO eliminate dead letters
if(!WellLootedCorpse(player)) {
continent.Population ! Zone.Corpse.Add(player)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent))
taskResolver ! GUIDTask.UnregisterLocker(player.Locker)(continent.GUID) //rest of player will be cleaned up with corpses
}
else {
//no items in inventory; leave no corpse
val player_guid = player.GUID
player.Position = Vector3.Zero //save character before doing this
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid))
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
}
case Some(_) =>
val player_guid = player.GUID
player.Position = Vector3.Zero //save character before doing this
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid))
taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID)
DismountVehicleOnLogOut()
}
}
//disassociate and start the deconstruction timer for any currently owned vehicle
SpecialCaseDisownVehicle()
continent.Population ! Zone.Population.Leave(avatar)
}
}
/**
* Vehicle cleanup that is specific to log out behavior.
*/
def DismountVehicleOnLogOut() : Unit = {
(continent.GUID(player.VehicleSeated) match {
case Some(obj : Mountable) =>
(Some(obj), obj.PassengerInSeat(player))
case _ =>
(None, None)
}) match {
case (Some(mountObj), Some(seatIndex)) =>
mountObj.Seats(seatIndex).Occupant = None
case _ => ;
}
}
/**
* If a vehicle is owned by a character, disassociate the vehicle, then schedule it for deconstruction.
* @see `DisownVehicle()`
* @return the vehicle previously owned, if any
*/
def SpecialCaseDisownVehicle() : Option[Vehicle] = {
DisownVehicle() match {
case out @ Some(vehicle) =>
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent,
if(vehicle.Flying) {
Some(0 seconds) //immediate deconstruction
}
else {
vehicle.Definition.DeconstructionTime //normal deconstruction
}
))
out
case None =>
None
}
}
def receive = Initializing
def Initializing : Receive = {
case HelloFriend(inSessionId, pipe) =>
this.sessionId = inSessionId
leftRef = sender()
if(pipe.hasNext) {
rightRef = pipe.next
rightRef !> HelloFriend(sessionId, pipe)
}
else {
rightRef = sender()
}
context.become(Started)
ServiceManager.serviceManager ! Lookup("avatar")
ServiceManager.serviceManager ! Lookup("local")
ServiceManager.serviceManager ! Lookup("vehicle")
ServiceManager.serviceManager ! Lookup("taskResolver")
ServiceManager.serviceManager ! Lookup("cluster")
ServiceManager.serviceManager ! Lookup("galaxy")
ServiceManager.serviceManager ! Lookup("squad")
case _ =>
log.error("Unknown message")
context.stop(self)
}
def Started : Receive = {
case ServiceManager.LookupResult("avatar", endpoint) =>
avatarService = endpoint
log.info("ID: " + sessionId + " Got avatar service " + endpoint)
case ServiceManager.LookupResult("local", endpoint) =>
localService = endpoint
log.info("ID: " + sessionId + " Got local service " + endpoint)
case ServiceManager.LookupResult("vehicle", endpoint) =>
vehicleService = endpoint
log.info("ID: " + sessionId + " Got vehicle service " + endpoint)
case ServiceManager.LookupResult("taskResolver", endpoint) =>
taskResolver = endpoint
log.info("ID: " + sessionId + " Got task resolver service " + endpoint)
case ServiceManager.LookupResult("galaxy", endpoint) =>
galaxyService = endpoint
log.info("ID: " + sessionId + " Got galaxy service " + endpoint)
case ServiceManager.LookupResult("cluster", endpoint) =>
cluster = endpoint
log.info("ID: " + sessionId + " Got cluster service " + endpoint)
case ServiceManager.LookupResult("squad", endpoint) =>
squadService = endpoint
log.info("ID: " + sessionId + " Got squad service " + endpoint)
case ControlPacket(_, ctrl) =>
handleControlPkt(ctrl)
case GamePacket(_, _, pkt) =>
handleGamePkt(pkt)
// temporary hack to keep the client from disconnecting
//it's been a "temporary hack" since 2016 :P
case PokeClient() =>
sendResponse(KeepAliveMessage())
case AvatarServiceResponse(toChannel, guid, reply) =>
HandleAvatarServiceResponse(toChannel, guid, reply)
case Door.DoorMessage(tplayer, msg, order) =>
HandleDoorMessage(tplayer, msg, order)
case GalaxyServiceResponse(_, reply) =>
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 LocalServiceResponse(toChannel, guid, reply) =>
HandleLocalServiceResponse(toChannel, guid, reply)
case Mountable.MountMessages(tplayer, reply) =>
HandleMountMessages(tplayer, reply)
case Terminal.TerminalMessage(tplayer, msg, order) =>
HandleTerminalMessage(tplayer, msg, order)
case ProximityUnit.Action(term, target) =>
SelectProximityUnitBehavior(term, target)
case VehicleServiceResponse(toChannel, guid, reply) =>
HandleVehicleServiceResponse(toChannel, guid, reply)
case SquadServiceResponse(toChannel, response) =>
response match {
case SquadResponse.ListSquadFavorite(line, task) =>
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
case SquadResponse.InitList(infos) if infos.nonEmpty =>
sendResponse(ReplicationStreamMessage(infos))
case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
val o = ReplicationStreamMessage(6, None,
infos.map { case (index, squadInfo) =>
SquadListing(index, squadInfo)
}.toVector
)
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.Detail(guid, detail) =>
sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
case SquadResponse.AssociateWithSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad()))
case SquadResponse.SetListSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
case SquadResponse.Unknown17(squad, char_id) =>
sendResponse(
SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
)
case SquadResponse.Membership(request_type, unk1, unk2, char_id, opt_char_id, player_name, unk5, unk6) =>
val name = request_type match {
case SquadResponseType.Invite if unk5 =>
//player_name is our name; the name of the player indicated by unk3 is needed
LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match {
case Some(player) =>
player.name
case None =>
player_name
}
case _ =>
player_name
}
sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6))
case SquadResponse.Invite(from_char_id, to_char_id, name) =>
sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
case SquadResponse.WantsSquadPosition(name : String) =>
sendResponse(
ChatMsg(
ChatMessageType.CMT_TELL, true, "",
s"\\#6[SQUAD] \\#3$name\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
None
)
)
case SquadResponse.Join(squad, positionsToUpdate) =>
val leader = squad.Leader
val id = 11
val membershipPositions = squad.Membership
.zipWithIndex
.filter { case (_, index ) => positionsToUpdate.contains(index) }
membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
case Some((ourMember, ourIndex)) =>
//we are joining the squad
//load each member's entry (our own too)
membershipPositions.foreach { case(member, index) =>
sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
}
//initialization
sendResponse(SquadMemberEvent.Add(id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
//lfs state management (always OFF)
if(avatar.LFS) {
lfs = avatar.LFS
avatar.LFS = false
sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0))
}
case _ =>
//other player is joining our squad
//load each member's entry
membershipPositions.foreach { case(member, index) =>
sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
}
}
//send an initial dummy update for map icon(s)
sendResponse(SquadState(PlanetSideGUID(id),
membershipPositions
.filterNot { case (member, _) => member.CharId == avatar.CharId }
.map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) }
.toList
))
log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
case SquadResponse.Leave(_, positionsToUpdate) =>
val id = 11
positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
case Some((ourMember, ourIndex)) =>
//we are leaving the squad
//remove each member's entry (our own too)
positionsToUpdate.foreach { case(member, index) =>
sendResponse(SquadMemberEvent.Remove(id, member, index))
squadUI.remove(member)
}
//uninitialize
sendResponse(SquadMemberEvent.Remove(id, ourMember, ourIndex)) //repeat of our entry
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
//lfs state management (maybe ON)
if(lfs) {
avatar.LFS = lfs
sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 1))
lfs = false
}
case _ =>
//remove each member's entry
positionsToUpdate.foreach { case(member, index) =>
sendResponse(SquadMemberEvent.Remove(id, member, index))
squadUI.remove(member)
}
log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
}
case SquadResponse.UpdateMembers(squad, positions) =>
import services.teamwork.SquadAction.{Update => SAUpdate}
val id = 11
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(11, entry.char_id, element.index, entry.zone_number))
squadUI(entry.char_id) = SquadUIElement(element.name, 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.index, entry.zone_number, entry.health, entry.armor, entry.pos)
entry
})
.filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend
if(updatedEntries.nonEmpty) {
sendResponse(
SquadState(
PlanetSideGUID(id),
updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
)
)
}
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)
log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) =>
//promotion will swap cards visually, but we must fix the backend
val id = 11
sendResponse(SquadMemberEvent.Promote(id, char_id))
SwapSquadUIElements(squad, from_index, to_index)
log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
case SquadResponse.SquadSearchResults() =>
//I don't actually know how to return search results
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
case SquadResponse.InitWaypoints(char_id, waypoints) =>
val id = 11
StartBundlingPackets()
waypoints.foreach { case (waypoint_type, info, unk) =>
sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
}
StopBundlingPackets()
case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
val id = 11
sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
val id = 11
sendResponse(SquadWaypointEvent.Remove(id, char_id, waypoint_type))
case _ => ;
}
case Deployment.CanDeploy(obj, state) =>
val vehicle_guid = obj.GUID
//TODO remove this arbitrary allowance angle when no longer helpful
if(obj.Orientation.x > 30 && obj.Orientation.x < 330) {
obj.DeploymentState = DriveState.Mobile
CanNotChangeDeployment(obj, state, "ground too steep")
}
else if(state == DriveState.Deploying) {
log.info(s"DeployRequest: $obj transitioning to deploy state")
obj.Velocity = Some(Vector3.Zero) //no velocity
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj)
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(obj.DeployTime milliseconds, obj.Actor, Deployment.TryDeploy(DriveState.Deployed))
}
else if(state == DriveState.Deployed) {
log.info(s"DeployRequest: $obj has been Deployed")
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj)
//...
}
else {
CanNotChangeDeployment(obj, state, "incorrect deploy state")
}
case Deployment.CanUndeploy(obj, state) =>
val vehicle_guid = obj.GUID
if(state == DriveState.Undeploying) {
log.info(s"DeployRequest: $obj transitioning to undeploy state")
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj)
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(obj.UndeployTime milliseconds, obj.Actor, Deployment.TryUndeploy(DriveState.Mobile))
}
else if(state == DriveState.Mobile) {
log.info(s"DeployRequest: $obj is Mobile")
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
DeploymentActivities(obj)
//...
}
else {
CanNotChangeDeployment(obj, state, "incorrect undeploy state")
}
case Deployment.CanNotChangeDeployment(obj, state, reason) =>
CanNotChangeDeployment(obj, state, reason)
case ResourceSilo.ResourceSiloMessage(tplayer, msg, order) =>
val vehicle_guid = msg.avatar_guid
val silo_guid = msg.object_guid
order match {
case ResourceSilo.ChargeEvent() =>
antChargingTick.cancel() // If an ANT is refilling an NTU silo it isn't in a warpgate, so disable NTU regeneration
antDischargingTick.cancel()
antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle], silo_guid))
}
case VehicleSpawnPad.StartPlayerSeatedInVehicle(vehicle, pad) =>
val vehicle_guid = vehicle.GUID
PlayerActionsToCancel()
if(player.VisibleSlots.contains(player.DrawnSlot)) {
player.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot))
}
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off
sendResponse(PlanetsideAttributeMessage(player.GUID, 21, vehicle_guid)) //ownership
case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) =>
val vehicle_guid = vehicle.GUID
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on
ReloadVehicleAccessPermissions(vehicle)
ServerVehicleLock(vehicle)
case VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) =>
val vdef = vehicle.Definition
if(vehicle.Seats(0).isOccupied) {
// todo: shouldn't the vehicle already be detached by this point? I'm going to disable the below for now as it's causing duplicate ODM packets
// sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3.z(0.5f), pad.Orientation.z))
}
ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef) : Int)
case VehicleSpawnControlGuided.GuidedControl(cmd, vehicle, data) =>
cmd match {
case AutoDriveControls.State.Drive =>
val speed : Int = data.getOrElse({
vehicle.Definition.AutoPilotSpeed1
}).asInstanceOf[Int]
ServerVehicleOverride(vehicle, speed)
case AutoDriveControls.State.Climb =>
ServerVehicleOverride(vehicle, controlled.getOrElse(0), GlobalDefinitions.isFlightVehicle(vehicle.Definition) : Int)
case AutoDriveControls.State.Turn =>
//TODO how to turn hovering/flying vehicle?
val direction = data.getOrElse(15).asInstanceOf[Int]
sendResponse(VehicleStateMessage(vehicle.GUID, 0, vehicle.Position, vehicle.Orientation, vehicle.Velocity, None, 0, 0, direction, false, false))
case AutoDriveControls.State.Stop =>
ServerVehicleOverride(vehicle, 0)
case _ => ;
}
case VehicleSpawnPad.ServerVehicleOverrideEnd(vehicle, pad) =>
sendResponse(GenericObjectActionMessage(pad.GUID, 92)) //reset spawn pad
DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2)
case VehicleSpawnPad.PeriodicReminder(cause, data) =>
val msg : String = (cause match {
case VehicleSpawnPad.Reminders.Blocked =>
s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}"
case VehicleSpawnPad.Reminders.Queue =>
s"Your position in the vehicle spawn queue is ${data.getOrElse("last")}."
case VehicleSpawnPad.Reminders.Cancelled =>
"Your vehicle order has been cancelled."
})
sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None))
case CheckCargoDismount(cargo_guid, carrier_guid, mountPoint, iteration) =>
HandleCheckCargoDismounting(cargo_guid, carrier_guid, mountPoint, iteration)
case CheckCargoMounting(cargo_guid, carrier_guid, mountPoint, iteration) =>
HandleCheckCargoMounting(cargo_guid, carrier_guid, mountPoint, iteration)
case ListAccountCharacters =>
import net.psforever.objects.definition.converter.CharacterSelectConverter
val gen : AtomicInteger = new AtomicInteger(1)
val converter : CharacterSelectConverter = new CharacterSelectConverter
//load characters
SetCharacterSelectScreenGUID(player, gen)
val health = player.Health
val stamina = player.Stamina
val armor = player.Armor
player.Spawn
sendResponse(
ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, converter.DetailedConstructorData(player).get)
)
if(health > 0) {
//player can not be dead; stay spawned as alive
player.Health = health
player.Stamina = stamina
player.Armor = armor
}
sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), avatar.CharId, player.GUID, false, 6404428))
RemoveCharacterSelectScreenGUID(player)
sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
case VehicleLoaded(_ /*vehicle*/) => ;
//currently being handled by VehicleSpawnPad.LoadVehicle during testing phase
case Zone.ClientInitialization(zone) =>
val continentNumber = zone.Number
val poplist = zone.Players
val popBO = 0
//TODO black ops test (partition)
val popTR = poplist.count(_.faction == PlanetSideEmpire.TR)
val popNC = poplist.count(_.faction == PlanetSideEmpire.NC)
val popVS = poplist.count(_.faction == PlanetSideEmpire.VS)
// StopBundlingPackets() is called on ClientInitializationComplete
StartBundlingPackets()
zone.Buildings.foreach({ case (id, building) => initBuilding(continentNumber, building.MapId, building) })
sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NEUTRAL))
//CaptureFlagUpdateMessage()
//VanuModuleUpdateMessage()
//ModuleLimitsMessage()
sendResponse(ZoneInfoMessage(continentNumber, true, 0))
sendResponse(ZoneLockInfoMessage(continentNumber, false, true))
sendResponse(ZoneForcedCavernConnectionsMessage(continentNumber, 0))
sendResponse(HotSpotUpdateMessage(
continentNumber,
1,
ZoneHotSpotProjector.SpecificHotSpotInfo(player.Faction, zone.HotSpots)
.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
)) //normally set for all zones in bulk; should be fine manually updating per zone like this
case Zone.Population.PlayerHasLeft(zone, None) =>
log.info(s"$avatar does not have a body on ${zone.Id}")
case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
if(tplayer.isAlive) {
log.info(s"$tplayer has left zone ${zone.Id}")
}
case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
log.warn(s"$tplayer can not spawn in zone ${zone.Id}; why?")
case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
log.warn(s"$tplayer is already spawned on zone ${zone.Id}; a clerical error?")
case Zone.Lattice.SpawnPoint(zone_id, spawn_tube) =>
var (pos, ori) = spawn_tube.SpecificPoint(continent.GUID(player.VehicleSeated) match {
case Some(obj : Vehicle) if obj.Health > 0 =>
obj
case _ =>
player
})
spawn_tube.Owner match {
case building : Building =>
log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id in building ${building.MapId} selected")
case vehicle : Vehicle =>
log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id at ams ${vehicle.GUID.guid} selected")
case owner =>
log.warn(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id at ${spawn_tube.Position} has unexpected owner $owner")
}
LoadZonePhysicalSpawnPoint(zone_id, pos, ori, CountSpawnDelay(zone_id, spawn_tube, continent.Id))
case Zone.Lattice.NoValidSpawnPoint(zone_number, None) =>
log.warn(s"Zone.Lattice.SpawnPoint: zone $zone_number could not be accessed as requested")
reviveTimer.cancel
RequestSanctuaryZoneSpawn(player, zone_number)
case Zone.Lattice.NoValidSpawnPoint(zone_number, Some(spawn_group)) =>
log.warn(s"Zone.Lattice.SpawnPoint: zone $zone_number has no available ${player.Faction} targets in spawn group $spawn_group")
reviveTimer.cancel
if(spawn_group == 2) {
sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "No friendly AMS is deployed in this region.", None))
cluster ! Zone.Lattice.RequestSpawnPoint(zone_number, player, 0)
}
else {
RequestSanctuaryZoneSpawn(player, zone_number)
}
case msg @ Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
log.warn(s"$msg")
case msg @ Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
log.warn(s"$msg")
case Zone.Ground.ItemOnGround(item : BoomerTrigger, pos, orient) =>
//dropped the trigger, no longer own the boomer; make certain whole faction is aware of that
val playerGUID = player.GUID
continent.GUID(item.Companion) match {
case Some(obj : BoomerDeployable) =>
val guid = obj.GUID
val factionOnContinentChannel = s"${continent.Id}/${player.Faction}"
obj.AssignOwnership(None)
avatar.Deployables.Remove(obj)
UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent))
sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL))
avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.SetEmpire(playerGUID, guid, PlanetSideEmpire.NEUTRAL))
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, PlanetSideGUID(0))
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Dismiss, info))
localService ! LocalServiceMessage(factionOnContinentChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Dismiss, info))
PutItemOnGround(item, pos, orient)
case Some(_) | None =>
//pointless trigger
val guid = item.GUID
continent.Ground ! Zone.Ground.RemoveItem(guid) //undo; no callback
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), guid))
taskResolver ! GUIDTask.UnregisterObjectTask(item)(continent.GUID)
}
case Zone.Ground.ItemOnGround(item : ConstructionItem, pos, orient) =>
//defensively, reset CItem configuration
item.FireModeIndex = 0
item.AmmoTypeIndex = 0
PutItemOnGround(item, pos, orient)
case Zone.Ground.ItemOnGround(item : PlanetSideGameObject, pos, orient) =>
PutItemOnGround(item, pos, orient)
case Zone.Ground.CanNotDropItem(zone, item, reason) =>
log.warn(s"DropItem: $player tried to drop a $item on the ground, but $reason")
case Zone.Ground.ItemInHand(item : BoomerTrigger) =>
if(PutItemInHand(item)) {
//pick up the trigger, own the boomer; make certain whole faction is aware of that
continent.GUID(item.Companion) match {
case Some(obj : BoomerDeployable) =>
val guid = obj.GUID
val playerGUID = player.GUID
val faction = player.Faction
val factionOnContinentChannel = s"${continent.Id}/${faction}"
obj.AssignOwnership(player)
obj.Faction = faction
avatar.Deployables.Add(obj)
UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item))
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent))
sendResponse(SetEmpireMessage(guid, faction))
avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.SetEmpire(playerGUID, guid, faction))
val info = DeployableInfo(obj.GUID, DeployableIcon.Boomer, obj.Position, obj.Owner.get)
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, info))
localService ! LocalServiceMessage(factionOnContinentChannel, LocalAction.DeployableMapIcon(playerGUID, DeploymentAction.Build, info))
case Some(_) | None => ; //pointless trigger; see Zone.Ground.ItemOnGround(BoomerTrigger, ...)
}
}
case Zone.Ground.ItemInHand(item : Equipment) =>
PutItemInHand(item)
case Zone.Ground.CanNotPickupItem(zone, item_guid, _) =>
zone.GUID(item_guid) match {
case Some(item) =>
log.warn(s"DropItem: finding a $item on the ground was suggested, but $player can not reach it")
case None =>
log.warn(s"DropItem: finding an item ($item_guid) on the ground was suggested, but $player can not see it")
}
case Zone.Deployable.DeployableIsBuilt(obj, tool) =>
val index = player.Find(tool) match {
case Some(x) =>
x
case None =>
player.LastDrawnSlot
}
if(avatar.Deployables.Accept(obj) || (avatar.Deployables.Valid(obj) && !avatar.Deployables.Contains(obj))) {
tool.Definition match {
case GlobalDefinitions.ace =>
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffectLocation(player.GUID, "spawn_object_effect", obj.Position, obj.Orientation))
case GlobalDefinitions.advanced_ace =>
sendResponse(GenericObjectActionMessage(player.GUID, 212)) //put fdu down; it will be removed from the client's holster
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PutDownFDU(player.GUID))
case GlobalDefinitions.router_telepad => ;
case _ =>
log.warn(s"Zone.Deployable.DeployableIsBuilt: not sure what kind of construction item to animate - ${tool.Definition}")
}
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(
obj.Definition.DeployTime milliseconds,
self,
WorldSessionActor.FinalizeDeployable(obj, tool, index)
)
}
else {
TryDropConstructionTool(tool, index, obj.Position)
sendResponse(ObjectDeployedMessage.Failure(obj.Definition.Name))
obj.Position = Vector3.Zero
obj.AssignOwnership(None)
continent.Deployables ! Zone.Deployable.Dismiss(obj)
}
case WorldSessionActor.FinalizeDeployable(obj : TurretDeployable, tool, index) =>
//spitfires and deployable field turrets
StartBundlingPackets()
DeployableBuildActivity(obj)
CommonDestroyConstructionItem(tool, index)
FindReplacementConstructionItem(tool, index)
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : ComplexDeployable, tool, index) =>
//deployable_shield_generator
StartBundlingPackets()
DeployableBuildActivity(obj)
CommonDestroyConstructionItem(tool, index)
FindReplacementConstructionItem(tool, index)
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : BoomerDeployable, tool, index) =>
//boomers
StartBundlingPackets()
DeployableBuildActivity(obj)
//TODO sufficiently delete the tool
sendResponse(ObjectDeleteMessage(tool.GUID, 0))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, tool.GUID))
taskResolver ! GUIDTask.UnregisterEquipment(tool)(continent.GUID)
val trigger = new BoomerTrigger
trigger.Companion = obj.GUID
obj.Trigger = trigger
val holster = player.Slot(index)
if(holster.Equipment.contains(tool)) {
holster.Equipment = None
taskResolver ! DelayedObjectHeld(player, index, List(PutEquipmentInSlot(player, trigger, index)))
}
else {
//don't know where boomer trigger should go; drop it on the ground
taskResolver ! NewItemDrop(player, continent, avatarService)(trigger)
}
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : ExplosiveDeployable, tool, index) =>
//mines
StartBundlingPackets()
DeployableBuildActivity(obj)
CommonDestroyConstructionItem(tool, index)
FindReplacementConstructionItem(tool, index)
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : SensorDeployable, tool, index) =>
//motion alarm sensor and sensor disruptor
StartBundlingPackets()
DeployableBuildActivity(obj)
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffectInfo(player.GUID, "on", obj.GUID, true, 1000))
CommonDestroyConstructionItem(tool, index)
FindReplacementConstructionItem(tool, index)
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : TelepadDeployable, tool, index) =>
StartBundlingPackets()
if(obj.Health > 0) {
val guid = obj.GUID
//router telepad deployable
val router = tool.asInstanceOf[Telepad].Router
//router must exist and be deployed
continent.GUID(router) match {
case Some(vehicle : Vehicle) =>
val routerGUID = router.get
if(vehicle.Health == 0) {
//the Telepad was successfully deployed; but, before it could configure, its Router was destroyed
sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@Telepad_NoDeploy_RouterLost", None))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
}
else {
log.info(s"FinalizeDeployable: setup for telepad #${guid.guid} in zone ${continent.Id}")
obj.Router = routerGUID //necessary; forwards link to the router
DeployableBuildActivity(obj)
CommonDestroyConstructionItem(tool, index)
StopBundlingPackets()
//it takes 60s for the telepad to become properly active
localService ! LocalServiceMessage.Telepads(RouterTelepadActivation.AddTask(obj, continent))
}
case _ =>
//the Telepad was successfully deployed; but, before it could configure, its Router was deconstructed
sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@Telepad_NoDeploy_RouterLost", None))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
}
}
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : SimpleDeployable, tool, index) =>
//tank_trap
StartBundlingPackets()
DeployableBuildActivity(obj)
CommonDestroyConstructionItem(tool, index)
FindReplacementConstructionItem(tool, index)
StopBundlingPackets()
case WorldSessionActor.FinalizeDeployable(obj : PlanetSideGameObject with Deployable, tool, index) =>
val guid = obj.GUID
val definition = obj.Definition
StartBundlingPackets()
sendResponse(GenericObjectActionMessage(guid, 84)) //reset build cooldown
sendResponse(ObjectDeployedMessage.Failure(definition.Name))
log.warn(s"FinalizeDeployable: deployable ${definition.asInstanceOf[DeployableDefinition].Item}@$guid not handled by specific case")
log.warn(s"FinalizeDeployable: deployable will be cleaned up, but may not get unregistered properly")
TryDropConstructionTool(tool, index, obj.Position)
obj.Position = Vector3.Zero
continent.Deployables ! Zone.Deployable.Dismiss(obj)
StopBundlingPackets()
//!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced
case Zone.Deployable.DeployableIsDismissed(obj : TurretDeployable) =>
taskResolver ! GUIDTask.UnregisterDeployableTurret(obj)(continent.GUID)
//!!only dispatch Zone.Deployable.Dismiss from WSA as cleanup if the target deployable was never fully introduced
case Zone.Deployable.DeployableIsDismissed(obj) =>
taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID)
case InterstellarCluster.ClientInitializationComplete() =>
StopBundlingPackets()
LivePlayerList.Add(sessionId, avatar)
traveler = new Traveler(self, continent.Id)
//PropertyOverrideMessage
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
avatarService ! Service.Join(avatar.name) //channel will be player.Name
localService ! Service.Join(avatar.name) //channel will be player.Name
vehicleService ! Service.Join(avatar.name) //channel will be player.Name
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
squadService ! Service.Join(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
cluster ! InterstellarCluster.GetWorld("home3")
case InterstellarCluster.GiveWorld(zoneId, zone) =>
log.info(s"Zone $zoneId will now load")
val continentId = continent.Id
val factionOnContinentChannel = s"$continentId/${avatar.faction}"
avatarService ! Service.Leave(Some(continentId))
avatarService ! Service.Leave(Some(factionOnContinentChannel))
localService ! Service.Leave(Some(continentId))
localService ! Service.Leave(Some(factionOnContinentChannel))
vehicleService ! Service.Leave(Some(continentId))
vehicleService ! Service.Leave(Some(factionOnContinentChannel))
player.Continent = zoneId
continent = zone
continent.Population ! Zone.Population.Join(avatar)
interstellarFerry match {
case Some(vehicle) if vehicle.PassengerInSeat(player).contains(0) =>
taskResolver ! RegisterDrivenVehicle(vehicle, player)
case _ =>
taskResolver ! RegisterNewAvatar(player)
}
case NewPlayerLoaded(tplayer) =>
//new zone
log.info(s"Player ${tplayer.Name} has been loaded")
player = tplayer
//LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar
sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100, 25, true, 3770441820L))
AvatarCreate() //important! the LoadMapMessage must be processed by the client before the avatar is created
case PlayerLoaded(tplayer) =>
//same zone
log.info(s"Player ${tplayer.Name} will respawn")
player = tplayer
AvatarCreate()
self ! SetCurrentAvatar(tplayer)
case PlayerFailedToLoad(tplayer) =>
player.Continent match {
case _ =>
failWithError(s"${tplayer.Name} failed to load anywhere")
}
case UnregisterCorpseOnVehicleDisembark(corpse) =>
if(!corpse.isAlive && corpse.HasGUID) {
corpse.VehicleSeated match {
case Some(_) =>
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(corpse))
case None =>
taskResolver ! GUIDTask.UnregisterPlayer(corpse)(continent.GUID)
}
}
case SetCurrentAvatar(tplayer) =>
HandleSetCurrentAvatar(tplayer)
case NtuCharging(tplayer, vehicle) =>
HandleNtuCharging(tplayer, vehicle)
case NtuDischarging(tplayer, vehicle, silo_guid) =>
HandleNtuDischarging(tplayer, vehicle, silo_guid)
case HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction, tickAction) =>
HandleHackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction, tickAction)
case Vitality.DamageResolution(target : Vehicle) =>
HandleVehicleDamageResolution(target)
case Vitality.DamageResolution(target : TrapDeployable) =>
//tank_traps
val guid = target.GUID
val health = target.Health
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health))
if(health <= 0) {
AnnounceDestroyDeployable(target, None)
}
case Vitality.DamageResolution(target : SensorDeployable) =>
//sensors
val guid = target.GUID
val health = target.Health
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health))
if(health <= 0) {
AnnounceDestroyDeployable(target, Some(0 seconds))
}
case Vitality.DamageResolution(target : SimpleDeployable) =>
//boomers, mines
if(target.Health <= 0) {
//update if destroyed
val guid = target.GUID
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, guid))
AnnounceDestroyDeployable(target, Some(0 seconds))
}
case Vitality.DamageResolution(target : TurretDeployable) =>
HandleTurretDeployableDamageResolution(target)
case Vitality.DamageResolution(target : ComplexDeployable) =>
//shield_generators
val health = target.Health
val guid = target.GUID
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 0, health))
if(health <= 0) {
AnnounceDestroyDeployable(target, None)
}
case Vitality.DamageResolution(target : FacilityTurret) =>
HandleFacilityTurretDamageResolution(target)
case Vitality.DamageResolution(target : PlanetSideGameObject) =>
log.warn(s"Vital target ${target.Definition.Name} damage resolution not supported using this method")
case Vehicle.UpdateShieldsCharge(vehicle) =>
vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), vehicle.GUID, 68, vehicle.Shields))
case ResponseToSelf(pkt) =>
log.info(s"Received a direct message: $pkt")
sendResponse(pkt)
case default =>
log.warn(s"Invalid packet class received: $default from $sender")
}
/**
* na
* @param toChannel na
* @param guid na
* @param reply na
*/
def HandleAvatarServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : AvatarResponse.Response) : Unit = {
val tplayer_guid = if(player.HasGUID) player.GUID
else PlanetSideGUID(0)
reply match {
case AvatarResponse.SendResponse(msg) =>
sendResponse(msg)
case AvatarResponse.ArmorChanged(suit, subtype) =>
if(tplayer_guid != guid) {
sendResponse(ArmorChangedMessage(guid, suit, subtype))
}
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) =>
if(tplayer_guid != guid) {
sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
sendResponse(
ObjectCreateMessage(
ammo_id,
ammo_guid,
ObjectCreateMessageParent(weapon_guid, weapon_slot),
ammo_data
)
)
sendResponse(ChangeAmmoMessage(weapon_guid, 1))
}
case AvatarResponse.ChangeFireMode(item_guid, mode) =>
if(tplayer_guid != guid) {
sendResponse(ChangeFireModeMessage(item_guid, mode))
}
case AvatarResponse.ChangeFireState_Start(weapon_guid) =>
if(tplayer_guid != guid) {
sendResponse(ChangeFireStateMessage_Start(weapon_guid))
}
case AvatarResponse.ChangeFireState_Stop(weapon_guid) =>
if(tplayer_guid != guid) {
sendResponse(ChangeFireStateMessage_Stop(weapon_guid))
}
case AvatarResponse.ConcealPlayer() =>
if(tplayer_guid != guid) {
sendResponse(GenericObjectActionMessage(guid, 36))
}
case AvatarResponse.DamageResolution(target, resolution_function) =>
if(player.isAlive) {
val originalHealth = player.Health
val originalArmor = player.Armor
resolution_function(target)
val health = player.Health
val armor = player.Armor
val damageToHealth = originalHealth - health
val damageToArmor = originalArmor - armor
damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor, AFTER=$health/$armor, CHANGE=$damageToHealth/$damageToArmor")
if(damageToHealth != 0 || damageToArmor != 0) {
val playerGUID = player.GUID
sendResponse(PlanetsideAttributeMessage(playerGUID, 0, health))
sendResponse(PlanetsideAttributeMessage(playerGUID, 4, armor))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 0, health))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(playerGUID, 4, armor))
if(health == 0 && player.isAlive) {
KillPlayer(player)
}
else {
//first damage entry -> most recent damage source -> killing blow
target.History.find(p => p.isInstanceOf[DamagingActivity]) match {
case Some(data : DamageFromProjectile) =>
val owner = data.data.projectile.owner
owner match {
case pSource : PlayerSource =>
continent.LivePlayers.find(_.Name == pSource.Name) match {
case Some(tplayer) =>
sendResponse(HitHint(tplayer.GUID, player.GUID))
case None => ;
}
case vSource : SourceEntry =>
sendResponse(DamageWithPositionMessage(damageToHealth + damageToArmor, vSource.Position))
case _ => ;
}
continent.Activity ! Zone.HotSpot.Activity(owner, data.Target, target.Position)
case _ => ;
}
}
}
}
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.DropItem(pkt) =>
if(tplayer_guid != guid) {
sendResponse(pkt)
}
case AvatarResponse.EquipmentInHand(pkt) =>
if(tplayer_guid != guid) {
sendResponse(pkt)
}
case AvatarResponse.HitHint(source_guid) =>
if(player.isAlive) {
sendResponse(HitHint(source_guid, guid))
}
case AvatarResponse.KilledWhileInVehicle() =>
if(player.isAlive && player.VehicleSeated.nonEmpty) {
(continent.GUID(player.VehicleSeated) match {
case Some(obj : Vehicle) =>
if(obj.Health == 0) Some(obj)
else None
case Some(obj : TurretDeployable) =>
if(obj.Health == 0) Some(obj)
else None
case Some(obj : FacilityTurret) =>
if(obj.Health == 1) Some(obj) //TODO proper turret death at 0 health
else None
case _ =>
None
}) match {
case Some(obj : PlanetSideGameObject with Vitality) =>
obj.LastShot match {
case Some(cause) =>
player.History(cause)
KillPlayer(player)
case None => ;
}
case _ =>
log.warn(s"${player.Name} was seated in a vehicle and should have been killed, but was not; suicidal fallback")
Suicide(player)
}
}
case AvatarResponse.LoadPlayer(pkt) =>
if(tplayer_guid != guid) {
sendResponse(pkt)
}
case AvatarResponse.ObjectDelete(item_guid, unk) =>
if(tplayer_guid != guid) {
sendResponse(ObjectDeleteMessage(item_guid, unk))
}
case AvatarResponse.ObjectHeld(slot) =>
if(tplayer_guid != guid) {
sendResponse(ObjectHeldMessage(guid, slot, false))
}
case AvatarResponse.PlanetsideAttribute(attribute_type, attribute_value) =>
if(tplayer_guid != guid) {
sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value))
}
case AvatarResponse.PlayerState(msg, spectating, weaponInHand) =>
if(tplayer_guid != guid) {
val now = System.currentTimeMillis()
val (location, time, distanceSq) : (Vector3, Long, Float) = if(spectating) {
(Vector3(2, 2, 2), 0L, 0f)
}
else {
val before = player.lastSeenStreamMessage(guid.guid)
val dist = Vector3.DistanceSquared(player.Position, msg.pos)
(msg.pos, now - before, dist)
}
if(spectating ||
((distanceSq < 900 || weaponInHand) && time > 200) ||
(distanceSq < 10000 && time > 500) ||
(distanceSq < 160000 && (
(msg.is_jumping || time < 200)) ||
((msg.vel.isEmpty || Vector3.MagnitudeSquared(msg.vel.get).toInt == 0) && time > 2000) ||
(time > 1000)) ||
(distanceSq > 160000 && time > 5000)) {
sendResponse(
PlayerStateMessage(
guid,
location,
msg.vel,
msg.facingYaw,
msg.facingPitch,
msg.facingYawUpper,
timestamp = 0,
msg.is_crouching,
msg.is_jumping,
msg.jump_thrust,
msg.is_cloaked
)
)
player.lastSeenStreamMessage(guid.guid) = now
}
}
case AvatarResponse.PutDownFDU(target) =>
if(tplayer_guid != guid) {
sendResponse(GenericObjectActionMessage(target, 212))
}
case AvatarResponse.Release(tplayer) =>
if(tplayer_guid != guid) {
TurnPlayerIntoCorpse(tplayer)
}
case AvatarResponse.Reload(item_guid) =>
if(tplayer_guid != guid) {
sendResponse(ReloadMessage(item_guid, 1, 0))
}
case AvatarResponse.SetEmpire(object_guid, faction) =>
if(tplayer_guid != guid) {
sendResponse(SetEmpireMessage(object_guid, faction))
}
case AvatarResponse.StowEquipment(target, slot, item) =>
if(tplayer_guid != guid) {
val definition = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(target, slot),
definition.Packet.DetailedConstructorData(item).get
)
)
}
case AvatarResponse.WeaponDryFire(weapon_guid) =>
if(tplayer_guid != guid) {
sendResponse(WeaponDryFireMessage(weapon_guid))
}
case _ => ;
}
}
/**
* na
* @param tplayer na
* @param msg na
* @param order na
*/
def HandleDoorMessage(tplayer : Player, msg : UseItemMessage, order : Door.Exchange) : Unit = {
val door_guid = msg.object_guid
order match {
case Door.OpenEvent() =>
continent.GUID(door_guid) match {
case Some(door : Door) =>
sendResponse(GenericObjectStateMsg(door_guid, 16))
localService ! LocalServiceMessage(continent.Id, LocalAction.DoorOpens(tplayer.GUID, continent, door))
case _ =>
log.warn(s"door $door_guid wanted to be opened but could not be found")
}
case Door.CloseEvent() =>
sendResponse(GenericObjectStateMsg(door_guid, 17))
localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid))
case Door.NoEvent() => ;
}
}
/**
* na
* @param toChannel na
* @param guid na
* @param reply na
*/
def HandleLocalServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : LocalResponse.Response) : Unit = {
val tplayer_guid = if(player.HasGUID) player.GUID
else PlanetSideGUID(0)
reply match {
case LocalResponse.AlertDestroyDeployable(obj) =>
//the (former) owner (obj.OwnerName) should process this message
avatar.Deployables.Remove(obj)
UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item))
case LocalResponse.DeployableMapIcon(behavior, deployInfo) =>
if(tplayer_guid != guid) {
sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
}
case LocalResponse.DoorOpens(door_guid) =>
if(tplayer_guid != guid) {
sendResponse(GenericObjectStateMsg(door_guid, 16))
}
case LocalResponse.DoorCloses(door_guid) => //door closes for everyone
sendResponse(GenericObjectStateMsg(door_guid, 17))
case LocalResponse.EliminateDeployable(obj : TurretDeployable, guid, pos) =>
if(obj.Health == 0) {
DeconstructDeployable(obj, guid, pos)
}
else {
DeconstructDeployable(obj, guid, pos, obj.Orientation, if(obj.MountPoints.isEmpty) 2 else 1)
}
case LocalResponse.EliminateDeployable(obj : ComplexDeployable, guid, pos) =>
if(obj.Health == 0) {
DeconstructDeployable(obj, guid, pos)
}
else {
DeconstructDeployable(obj, guid, pos, obj.Orientation, 1)
}
case LocalResponse.EliminateDeployable(obj : ExplosiveDeployable, guid, pos) =>
if(obj.Exploded || obj.Health == 0) {
DeconstructDeployable(obj, guid, pos)
}
else {
DeconstructDeployable(obj, guid, pos, obj.Orientation, 2)
}
case LocalResponse.EliminateDeployable(obj : TelepadDeployable, guid, pos) =>
//if active, deactivate
if(obj.Active) {
obj.Active = false
sendResponse(GenericObjectActionMessage(guid, 116))
sendResponse(GenericObjectActionMessage(guid, 120))
}
//determine if no replacement teleport system exists
continent.GUID(obj.Router) match {
case Some(router : Vehicle) =>
//if the telepad was replaced, the new system is physically in place but not yet functional
if(router.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(internalTelepad : Utility.InternalTelepad) => internalTelepad.Telepad.contains(guid) //same telepad
case _ => true
}) {
//there is no replacement telepad; shut down the system
ToggleTeleportSystem(router, None)
}
case _ => ;
}
//standard deployable elimination behavior
if(obj.Health == 0) {
DeconstructDeployable(obj, guid, pos)
}
else {
DeconstructDeployable(obj, guid, pos, obj.Orientation, 2)
}
case LocalResponse.EliminateDeployable(obj, guid, pos) =>
if(obj.Health == 0) {
DeconstructDeployable(obj, guid, pos)
}
else {
DeconstructDeployable(obj, guid, pos, obj.Orientation, 2)
}
case LocalResponse.HackClear(target_guid, unk1, unk2) =>
log.trace(s"Clearing hack for ${target_guid}")
// Reset hack state for all players
sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2))
case LocalResponse.HackObject(target_guid, unk1, unk2) =>
sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2))
case LocalResponse.HackCaptureTerminal(target_guid, unk1, unk2, isResecured) =>
var value = 0L
if(isResecured) {
value = 17039360L
}
else {
continent.GUID(target_guid) match {
case Some(capture_terminal: Hackable) =>
capture_terminal.HackedBy match {
case Some(Hackable.HackInfo(_, _, hfaction, _, start, length)) =>
val hack_time_remaining_ms = TimeUnit.MILLISECONDS.convert(math.max(0, start + length - System.nanoTime), TimeUnit.NANOSECONDS)
val deciseconds_remaining = (hack_time_remaining_ms / 100)
// See PlanetSideAttributeMessage #20 documentation for an explanation of how the timer is calculated
val start_num = hfaction match {
case PlanetSideEmpire.TR => 65536L
case PlanetSideEmpire.NC => 131072L
case PlanetSideEmpire.VS => 196608L
}
value = start_num + deciseconds_remaining
sendResponse(PlanetsideAttributeMessage(target_guid, 20, value))
case _ => log.warn("LocalResponse.HackCaptureTerminal: HackedBy not defined")
}
case _ => log.warn(s"LocalResponse.HackCaptureTerminal: Couldn't find capture terminal with GUID ${target_guid} in zone ${continent.Id}")
}
}
case LocalResponse.ObjectDelete(object_guid, unk) =>
if(tplayer_guid != guid) {
sendResponse(ObjectDeleteMessage(object_guid, unk))
}
case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, true))
case LocalResponse.ProximityTerminalEffect(object_guid, false) =>
sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, false))
ForgetAllProximityTerminals(object_guid)
case LocalResponse.RouterTelepadMessage(msg) =>
sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", msg, None))
case LocalResponse.RouterTelepadTransport(passenger_guid, src_guid, dest_guid) =>
StartBundlingPackets()
UseRouterTelepadEffect(passenger_guid, src_guid, dest_guid)
StopBundlingPackets()
case LocalResponse.SetEmpire(object_guid, empire) =>
sendResponse(SetEmpireMessage(object_guid, empire))
case LocalResponse.ToggleTeleportSystem(router, system_plan) =>
ToggleTeleportSystem(router, system_plan)
case LocalResponse.TriggerEffect(target_guid, effect, effectInfo, triggerLocation) =>
sendResponse(TriggerEffectMessage(target_guid, effect, effectInfo, triggerLocation))
case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
case _ => ;
}
}
/**
* na
* @param tplayer na
* @param reply na
*/
def HandleMountMessages(tplayer : Player, reply : Mountable.Exchange) : Unit = {
reply match {
case Mountable.CanMount(obj : ImplantTerminalMech, seat_num) =>
MountingAction(tplayer, obj, seat_num)
sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, 1000L)) //health of mech
case Mountable.CanMount(obj : Vehicle, seat_num) =>
val obj_guid : PlanetSideGUID = obj.GUID
val player_guid : PlanetSideGUID = tplayer.GUID
log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num")
PlayerActionsToCancel()
sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health))
sendResponse(PlanetsideAttributeMessage(obj_guid, 68, 0)) //shield health
sendResponse(PlanetsideAttributeMessage(obj_guid, 113, 0)) //capacitor
if(seat_num == 0) {
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer
//simplistic vehicle ownership management
obj.Owner match {
case Some(owner_guid) =>
continent.GUID(owner_guid) match {
case Some(previous_owner : Player) =>
if(previous_owner.VehicleOwned.contains(obj_guid)) {
previous_owner.VehicleOwned = None //simplistic ownership management, player loses vehicle ownership
}
case _ => ;
}
case None => ;
}
OwnVehicle(obj, tplayer)
}
AccessContents(obj)
UpdateWeaponAtSeatPosition(obj, seat_num)
MountingAction(tplayer, obj, seat_num)
case Mountable.CanMount(obj : PlanetSideGameObject with WeaponTurret, seat_num) =>
sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health))
UpdateWeaponAtSeatPosition(obj, seat_num)
MountingAction(tplayer, obj, seat_num)
case Mountable.CanMount(obj : Mountable, _) =>
log.warn(s"MountVehicleMsg: $obj is some generic mountable object and nothing will happen")
case Mountable.CanDismount(obj : ImplantTerminalMech, seat_num) =>
DismountAction(tplayer, obj, seat_num)
case Mountable.CanDismount(obj : Vehicle, seat_num) =>
val player_guid : PlanetSideGUID = tplayer.GUID
if(player_guid == player.GUID) {
//disembarking self
TotalDriverVehicleControl(obj)
UnAccessContents(obj)
DismountAction(tplayer, obj, seat_num)
}
else {
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID))
}
case Mountable.CanDismount(obj : PlanetSideGameObject with WeaponTurret, seat_num) =>
DismountAction(tplayer, obj, seat_num)
case Mountable.CanDismount(obj : Mountable, _) =>
log.warn(s"DismountVehicleMsg: $obj is some generic mountable object and nothing will happen")
case Mountable.CanNotMount(obj : Vehicle, seat_num) =>
log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed")
if(obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) {
sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None))
}
case Mountable.CanNotMount(obj : Mountable, seat_num) =>
log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed")
case Mountable.CanNotDismount(obj, seat_num) =>
log.warn(s"DismountVehicleMsg: $tplayer attempted to dismount $obj's seat $seat_num, but was not allowed")
}
}
/**
* na
* @param tplayer na
* @param msg na
* @param order na
*/
def HandleTerminalMessage(tplayer : Player, msg : ItemTransactionMessage, order : Terminal.Exchange) : Unit = {
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
//TODO check exo-suit permissions
val originalSuit = tplayer.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(tplayer)
if(originalSuit != exosuit || originalSubtype != subtype) {
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
//prepare lists of valid objects
val beforeInventory = tplayer.Inventory.Clear()
val beforeHolsters = clearHolsters(tplayer.Holsters().iterator)
//change suit (clear inventory and change holster sizes; holsters must be empty before this point)
val originalArmor = tplayer.Armor
tplayer.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
val toMaxArmor = tplayer.MaxArmor
if(originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) {
tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), exosuit))
tplayer.Armor = toMaxArmor
sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, toMaxArmor))
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, toMaxArmor))
}
else {
tplayer.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if(tplayer.DrawnSlot != Player.HandsDownSlot) {
tplayer.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true))
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, tplayer.LastDrawnSlot))
}
//delete everything not dropped
(beforeHolsters ++ beforeInventory).foreach({ elem =>
sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0))
})
beforeHolsters.foreach({ elem =>
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID))
})
//report change
sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype))
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ArmorChanged(tplayer.GUID, exosuit, subtype))
//sterilize holsters
val normalHolsters = if(originalSuit == ExoSuitType.MAX) {
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
maxWeapons.foreach(entry => { taskResolver ! GUIDTask.UnregisterEquipment(entry.obj)(continent.GUID) })
normalWeapons
}
else {
beforeHolsters
}
//populate holsters
val finalInventory = if(exosuit == ExoSuitType.MAX) {
taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, Tool(GlobalDefinitions.MAXArms(subtype, tplayer.Faction)), 0)))
fillEmptyHolsters(List(tplayer.Slot(4)).iterator, normalHolsters) ++ beforeInventory
}
else if(originalSuit == exosuit) { //note - this will rarely be the situation
fillEmptyHolsters(tplayer.Holsters().iterator, normalHolsters)
}
else {
val (afterHolsters, toInventory) = normalHolsters.partition(elem => elem.obj.Size == tplayer.Slot(elem.start).Size)
afterHolsters.foreach({ elem => tplayer.Slot(elem.start).Equipment = elem.obj })
fillEmptyHolsters(tplayer.Holsters().iterator, toInventory ++ beforeInventory)
}
//draw holsters
tplayer.VisibleSlots.foreach({ index =>
tplayer.Slot(index).Equipment match {
case Some(obj) =>
val definition = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
obj.GUID,
ObjectCreateMessageParent(tplayer.GUID, index),
definition.Packet.DetailedConstructorData(obj).get
)
)
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentInHand(player.GUID, player.GUID, index, obj))
case None => ;
}
})
//re-draw equipment held in free hand
tplayer.FreeHand.Equipment match {
case Some(item) =>
val definition = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
item.GUID,
ObjectCreateMessageParent(tplayer.GUID, Player.FreeHandSlot),
definition.Packet.DetailedConstructorData(item).get
)
)
case None => ;
}
//put items back into inventory
val (stow, drop) = if(originalSuit == exosuit) {
(finalInventory, Nil)
}
else {
GridInventory.recoverInventory(finalInventory, tplayer.Inventory)
}
stow.foreach(elem => {
tplayer.Inventory.Insert(elem.start, elem.obj)
val obj = elem.obj
val definition = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
obj.GUID,
ObjectCreateMessageParent(tplayer.GUID, elem.start),
definition.Packet.DetailedConstructorData(obj).get
)
)
})
val (finalDroppedItems, retiredItems) = drop.map(item => InventoryItem(item, -1)).partition(DropPredicate(tplayer))
//drop special items on ground
val pos = tplayer.Position
val orient = Vector3.z(tplayer.Orientation.z)
finalDroppedItems.foreach(entry => {
//TODO make a sound when dropping stuff
continent.Ground ! Zone.Ground.DropItem(entry.obj, pos, orient)
})
//deconstruct normal items
retiredItems.foreach({ entry =>
taskResolver ! GUIDTask.UnregisterEquipment(entry.obj)(continent.GUID)
})
}
else {
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false))
}
lastTerminalOrderFulfillment = true
case Terminal.BuyEquipment(item) =>
continent.GUID(tplayer.VehicleSeated) match {
//vehicle trunk
case Some(vehicle : Vehicle) =>
vehicle.Fit(item) match {
case Some(index) =>
item.Faction = tplayer.Faction
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
taskResolver ! StowNewEquipmentInVehicle(vehicle)(index, item)
case None => //player free hand?
tplayer.FreeHand.Equipment match {
case None =>
item.Faction = tplayer.Faction
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
taskResolver ! PutEquipmentInSlot(tplayer, item, Player.FreeHandSlot)
case Some(_) =>
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false))
}
}
//player backpack or free hand
case _ =>
tplayer.Fit(item) match {
case Some(index) =>
item.Faction = tplayer.Faction
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
taskResolver ! PutEquipmentInSlot(tplayer, item, index)
case None =>
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false))
}
}
lastTerminalOrderFulfillment = true
case Terminal.SellEquipment() =>
tplayer.FreeHand.Equipment match {
case Some(item) =>
if(item.GUID == msg.item_guid) {
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true))
taskResolver ! RemoveEquipmentFromSlot(tplayer, item, Player.FreeHandSlot)
}
case None =>
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, false))
}
lastTerminalOrderFulfillment = true
case Terminal.InfantryLoadout(exosuit, subtype, holsters, inventory) =>
log.info(s"$tplayer wants to change equipment loadout to their option #${msg.unk1 + 1}")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true))
//prepare lists of valid objects
val beforeFreeHand = tplayer.FreeHand.Equipment
val dropPred = DropPredicate(tplayer)
val (dropHolsters, beforeHolsters) = clearHolsters(tplayer.Holsters().iterator).partition(dropPred)
val (dropInventory, beforeInventory) = tplayer.Inventory.Clear().partition(dropPred)
tplayer.FreeHand.Equipment = None //terminal and inventory will close, so prematurely dropping should be fine
//sanitize exo-suit for change
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(tplayer)
val fallbackSuit = ExoSuitType.Standard
val fallbackSubtype = 0
//a loadout with a prohibited exo-suit type will result in a fallback exo-suit type
val (nextSuit : ExoSuitType.Value, nextSubtype : Int) =
if(ExoSuitDefinition.Select(exosuit).Permissions match {
case Nil =>
true
case permissions if subtype != 0 =>
val certs = tplayer.Certifications
certs.intersect(permissions.toSet).nonEmpty &&
certs.intersect(InfantryLoadout.DetermineSubtypeC(subtype)).nonEmpty
case permissions =>
tplayer.Certifications.intersect(permissions.toSet).nonEmpty
}) {
(exosuit, subtype)
}
else {
log.warn(s"$tplayer no longer has permission to wear the exo-suit type $exosuit; will wear $fallbackSuit instead")
(fallbackSuit, fallbackSubtype)
}
//update suit interally (holsters must be empty before this point)
val originalArmor = player.Armor
tplayer.ExoSuit = nextSuit
val toMaxArmor = tplayer.MaxArmor
if(originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) {
tplayer.History(HealFromExoSuitChange(PlayerSource(tplayer), nextSuit))
tplayer.Armor = toMaxArmor
sendResponse(PlanetsideAttributeMessage(tplayer.GUID, 4, toMaxArmor))
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(tplayer.GUID, 4, toMaxArmor))
}
else {
tplayer.Armor = originalArmor
}
//ensure arm is down, even if it needs to go back up
if(tplayer.DrawnSlot != Player.HandsDownSlot) {
tplayer.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(tplayer.GUID, Player.HandsDownSlot, true))
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectHeld(tplayer.GUID, tplayer.LastDrawnSlot))
}
//a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped
//dropped items can be forgotten safely
val (afterHolsters, afterInventory) = if(nextSuit == exosuit) {
(
holsters.filterNot(dropPred),
inventory.filterNot(dropPred)
)
}
else {
val newSuitDef = ExoSuitDefinition.Select(nextSuit)
val (afterInventory, extra) = GridInventory.recoverInventory(
inventory.filterNot(dropPred),
tplayer.Inventory
)
val afterHolsters = {
val preservedHolsters = if(exosuit == ExoSuitType.MAX) {
holsters.filter(_.start == 4) //melee slot perservation
}
else {
holsters
.filterNot(dropPred)
.collect {
case item @ InventoryItem(obj, index) if newSuitDef.Holster(index) == obj.Size => item
}
}
val size = newSuitDef.Holsters.size
val indexMap = preservedHolsters.map { entry => entry.start }
preservedHolsters ++ (extra.map { obj =>
tplayer.Fit(obj) match {
case Some(index : Int) if index < size && !indexMap.contains(index) =>
InventoryItem(obj, index)
case _ =>
InventoryItem(obj, -1)
}
}).filterNot(entry => entry.start == -1)
}
(afterHolsters, afterInventory)
}
//delete everything (not dropped)
beforeHolsters.foreach({ elem =>
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID))
})
(beforeHolsters ++ beforeInventory).foreach({ elem =>
sendResponse(ObjectDeleteMessage(elem.obj.GUID, 0))
taskResolver ! GUIDTask.UnregisterEquipment(elem.obj)(continent.GUID)
})
//report change
sendResponse(ArmorChangedMessage(tplayer.GUID, nextSuit, nextSubtype))
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, nextSuit, nextSubtype))
if(nextSuit == ExoSuitType.MAX) {
val (maxWeapons, otherWeapons) = afterHolsters.partition(entry => { entry.obj.Size == EquipmentSize.Max })
taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, maxWeapons.head.obj, 0)))
otherWeapons
}
else {
afterHolsters
}.foreach(entry => {
entry.obj.Faction = tplayer.Faction
taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start)
})
//put items into inventory
afterInventory.foreach(entry => {
entry.obj.Faction = tplayer.Faction
taskResolver ! PutEquipmentInSlot(tplayer, entry.obj, entry.start)
})
//drop stuff on ground
val pos = tplayer.Position
val orient = Vector3.z(tplayer.Orientation.z)
((beforeFreeHand match {
case Some(item) => List(InventoryItem(item, -1)) //add the item previously in free hand, if any
case None => Nil
}) ++ dropHolsters ++ dropInventory).foreach(entry => {
entry.obj.Faction = PlanetSideEmpire.NEUTRAL
continent.Ground ! Zone.Ground.DropItem(entry.obj, pos, orient)
})
lastTerminalOrderFulfillment = true
case Terminal.VehicleLoadout(definition, weapons, inventory) =>
log.info(s"$tplayer wants to change their vehicle equipment loadout to their option #${msg.unk1 + 1}")
FindLocalVehicle match {
case Some(vehicle) =>
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, true))
val (_, afterInventory) = inventory.partition(DropPredicate(tplayer))
//dropped items are lost
//remove old inventory
val deleteEquipment : (Int, Equipment) => Unit = DeleteEquipmentFromVehicle(vehicle)
vehicle.Inventory.Clear().foreach({ case InventoryItem(obj, index) => deleteEquipment(index, obj) })
val stowEquipment : (Int, Equipment) => TaskResolver.GiveTask = StowNewEquipmentInVehicle(vehicle)
(if(vehicle.Definition == definition) {
//vehicles are the same type; transfer over weapon ammo
//TODO ammo switching? no vehicle weapon does that currently but ...
//TODO want to completely swap weapons, but holster icon vanishes temporarily after swap
//TODO BFR arms must be swapped properly
val channel = s"${vehicle.Actor}"
weapons.foreach({ case InventoryItem(obj, index) =>
val savedWeapon = obj.asInstanceOf[Tool]
val existingWeapon = vehicle.Weapons(index).Equipment.get.asInstanceOf[Tool]
(0 until existingWeapon.MaxAmmoSlot).foreach({ index =>
val existingBox = existingWeapon.AmmoSlots(index).Box
existingBox.Capacity = savedWeapon.AmmoSlots(index).Box.Capacity
//use VehicleAction.InventoryState2; VehicleAction.InventoryState temporarily glitches ammo count in ui
vehicleService ! VehicleServiceMessage(channel, VehicleAction.InventoryState2(PlanetSideGUID(0), existingBox.GUID, existingWeapon.GUID, existingBox.Capacity))
})
})
afterInventory
}
else {
//do not transfer over weapon ammo
if(vehicle.Definition.TrunkSize == definition.TrunkSize && vehicle.Definition.TrunkOffset == definition.TrunkOffset) {
afterInventory
}
else {
//accommodate as much of inventory as possible
val (stow, _) = GridInventory.recoverInventory(afterInventory, vehicle.Inventory) //dropped items can be forgotten
stow
}
}).foreach({ case InventoryItem(obj, index) =>
obj.Faction = tplayer.Faction
taskResolver ! stowEquipment(index, obj)
})
case None =>
log.error(s"can not apply the loadout - can not find a vehicle")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Loadout, false))
}
lastTerminalOrderFulfillment = true
case Terminal.LearnCertification(cert) =>
val name = tplayer.Name
if(!tplayer.Certifications.contains(cert)) {
val guid = tplayer.GUID
log.info(s"$name is learning the $cert certification for ${Certification.Cost.Of(cert)} points")
avatar.Certifications += cert
StartBundlingPackets()
AddToDeployableQuantities(cert, player.Certifications)
sendResponse(PlanetsideAttributeMessage(guid, 24, cert.id))
tplayer.Certifications.intersect(Certification.Dependencies.Like(cert)).foreach(entry => {
log.info(s"$cert replaces the learned certification $entry that cost ${Certification.Cost.Of(entry)} points")
avatar.Certifications -= entry
sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id))
})
StopBundlingPackets()
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true))
}
else {
log.warn(s"$name already knows the $cert certification, so he can't learn it")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false))
}
lastTerminalOrderFulfillment = true
case Terminal.SellCertification(cert) =>
val name = tplayer.Name
if(tplayer.Certifications.contains(cert)) {
val guid = tplayer.GUID
log.info(s"$name is forgetting the $cert certification for ${Certification.Cost.Of(cert)} points")
avatar.Certifications -= cert
StartBundlingPackets()
RemoveFromDeployablesQuantities(cert, player.Certifications)
sendResponse(PlanetsideAttributeMessage(guid, 25, cert.id))
tplayer.Certifications.intersect(Certification.Dependencies.FromAll(cert)).foreach(entry => {
log.info(s"$name is also forgetting the ${Certification.Cost.Of(entry)}-point $entry certification which depends on $cert")
avatar.Certifications -= entry
RemoveFromDeployablesQuantities(entry, player.Certifications)
sendResponse(PlanetsideAttributeMessage(guid, 25, entry.id))
})
StopBundlingPackets()
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true))
}
else {
log.warn(s"$name doesn't know what a $cert certification is, so he can't forget it")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false))
}
lastTerminalOrderFulfillment = true
case Terminal.LearnImplant(implant) =>
val terminal_guid = msg.terminal_guid
val implant_type = implant.Type
val message = s"Implants: $tplayer wants to learn $implant_type"
val (interface, slotNumber) = tplayer.VehicleSeated match {
case Some(mech_guid) =>
(
continent.Map.TerminalToInterface.get(mech_guid.guid),
if(!avatar.Implants.exists({ slot => slot.Implant == implant_type })) {
//no duplicates
avatar.InstallImplant(implant)
}
else {
None
}
)
case _ =>
(None, None)
}
if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"$message - put in slot $slot")
sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Add, slot, implant_type.id))
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, true))
}
else {
if(interface.isEmpty) {
log.warn(s"$message - not interacting with a terminal")
}
else if(!interface.contains(terminal_guid.guid)) {
log.warn(s"$message - interacting with the wrong terminal, ${interface.get}")
}
else if(slotNumber.isEmpty) {
log.warn(s"$message - already knows that implant")
}
else {
log.warn(s"$message - forgot to sit at a terminal")
}
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, false))
}
lastTerminalOrderFulfillment = true
case Terminal.SellImplant(implant) =>
val terminal_guid = msg.terminal_guid
val implant_type = implant.Type
val (interface, slotNumber) = tplayer.VehicleSeated match {
case Some(mech_guid) =>
(
continent.Map.TerminalToInterface.get(mech_guid.guid),
avatar.UninstallImplant(implant_type)
)
case None =>
(None, None)
}
if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"$tplayer is selling $implant_type - take from slot $slot")
sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Remove, slot, 0))
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, true))
}
else {
val message = s"$tplayer can not sell $implant_type"
if(interface.isEmpty) {
log.warn(s"$message - not interacting with a terminal")
}
else if(!interface.contains(terminal_guid.guid)) {
log.warn(s"$message - interacting with the wrong terminal, ${interface.get}")
}
else if(slotNumber.isEmpty) {
log.warn(s"$message - does not know that implant")
}
else {
log.warn(s"$message - forgot to sit at a terminal")
}
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, false))
}
lastTerminalOrderFulfillment = true
case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match {
case Some(pad_guid) =>
val toFaction = tplayer.Faction
val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad]
vehicle.Faction = toFaction
vehicle.Continent = continent.Id
vehicle.Position = pad.Position
vehicle.Orientation = pad.Orientation
//default loadout, weapons
val vWeapons = vehicle.Weapons
weapons.foreach(entry => {
val index = entry.start
vWeapons.get(index) match {
case Some(slot) =>
entry.obj.Faction = toFaction
slot.Equipment = None
slot.Equipment = entry.obj
case None =>
log.warn(s"applying default loadout to $vehicle on spawn, but can not find a mounted weapon @ $index")
}
})
//default loadout, trunk
val vTrunk = vehicle.Trunk
vTrunk.Clear()
trunk.foreach(entry => {
entry.obj.Faction = toFaction
vTrunk += entry.start -> entry.obj
})
taskResolver ! RegisterNewVehicle(vehicle, pad)
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
case None =>
log.error(s"$tplayer wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it")
}
lastTerminalOrderFulfillment = true
case _ =>
val order : String = if(msg == null) {
s"order $msg"
}
else {
"missing order"
}
log.warn(s"${tplayer.Name} made a request but the terminal rejected the $order")
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false))
lastTerminalOrderFulfillment = true
}
}
/**
* na
* @param toChannel na
* @param guid na
* @param reply na
*/
def HandleVehicleServiceResponse(toChannel : String, guid : PlanetSideGUID, reply : VehicleResponse.Response) : Unit = {
val tplayer_guid = if(player.HasGUID) player.GUID else PlanetSideGUID(0)
reply match {
case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) =>
sendResponse(ObjectAttachMessage(pad_guid, vehicle_guid, 3))
case VehicleResponse.ChildObjectState(object_guid, pitch, yaw) =>
if(tplayer_guid != guid) {
sendResponse(ChildObjectStateMessage(object_guid, pitch, yaw))
}
case VehicleResponse.ConcealPlayer(player_guid) =>
//TODO this is the correct message; but, I don't know how to undo the effects of it
//sendResponse(GenericObjectActionMessage(player_guid, 36))
sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1))
case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) =>
if(tplayer_guid != guid) {
sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver))
}
case VehicleResponse.DeployRequest(object_guid, state, unk1, unk2, pos) =>
if(tplayer_guid != guid) {
sendResponse(DeployRequestMessage(guid, object_guid, state, unk1, unk2, pos))
}
case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) =>
val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad].Definition
sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0, 0, pad.VehicleCreationZOffset), pad_orientation_z + pad.VehicleCreationZOrientOffset))
case VehicleResponse.EquipmentInSlot(pkt) =>
if(tplayer_guid != guid) {
sendResponse(pkt)
}
case VehicleResponse.HitHint(source_guid) =>
if(player.isAlive) {
sendResponse(HitHint(source_guid, player.GUID))
}
case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) =>
if(tplayer_guid != guid) {
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
val obj_guid = obj.GUID
sendResponse(ObjectDeleteMessage(obj_guid, 0))
sendResponse(
ObjectCreateDetailedMessage(
obj.Definition.ObjectId,
obj_guid,
ObjectCreateMessageParent(parent_guid, start),
con_data
)
)
}
case msg@VehicleResponse.KickPassenger(seat_num, wasKickedByDriver, vehicle_guid) =>
// seat_num seems to be correct if passenger is kicked manually by driver, but always seems to return 4 if user is kicked by seat permissions
log.info(s"$msg")
sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
if(tplayer_guid == guid) {
continent.GUID(vehicle_guid) match {
case Some(obj : Vehicle) =>
UnAccessContents(obj)
case _ => ;
}
}
case VehicleResponse.InventoryState2(obj_guid, parent_guid, value) =>
if(tplayer_guid != guid) {
sendResponse(InventoryStateMessage(obj_guid, 0, parent_guid, value))
}
case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) =>
//this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible)
if(tplayer_guid != guid) {
sendResponse(ObjectCreateMessage(vtype, vguid, vdata))
ReloadVehicleAccessPermissions(vehicle)
}
case VehicleResponse.MountVehicle(vehicle_guid, seat) =>
if(tplayer_guid != guid) {
sendResponse(ObjectAttachMessage(vehicle_guid, guid, seat))
}
case VehicleResponse.Ownership(vehicle_guid) =>
if(tplayer_guid == guid) { // Only the player that owns this vehicle needs the ownership packet
sendResponse(PlanetsideAttributeMessage(tplayer_guid, 21, vehicle_guid))
}
case VehicleResponse.PlanetsideAttribute(vehicle_guid, attribute_type, attribute_value) =>
if(tplayer_guid != guid) {
player.VehicleOwned = Some(vehicle_guid)
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type, attribute_value))
}
case VehicleResponse.ResetSpawnPad(pad_guid) =>
sendResponse(GenericObjectActionMessage(pad_guid, 92))
case VehicleResponse.RevealPlayer(player_guid) =>
//TODO see note in ConcealPlayer
sendResponse(PlanetsideAttributeMessage(player_guid, 29, 0))
case VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission) =>
if(tplayer_guid != guid) {
sendResponse(PlanetsideAttributeMessage(vehicle_guid, seat_group, permission))
}
case VehicleResponse.StowEquipment(vehicle_guid, slot, item_type, item_guid, item_data) =>
if(tplayer_guid != guid) {
//TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
sendResponse(
ObjectCreateDetailedMessage(item_type, item_guid, ObjectCreateMessageParent(vehicle_guid, slot), item_data)
)
}
case VehicleResponse.UnloadVehicle(vehicle, vehicle_guid) =>
BeforeUnloadVehicle(vehicle)
sendResponse(ObjectDeleteMessage(vehicle_guid, 0))
case VehicleResponse.UnstowEquipment(item_guid) =>
if(tplayer_guid != guid) {
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
sendResponse(ObjectDeleteMessage(item_guid, 0))
}
case VehicleResponse.VehicleState(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6) =>
if(tplayer_guid != guid) {
sendResponse(VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6))
if(player.VehicleSeated.contains(vehicle_guid)) {
player.Position = pos
}
}
case VehicleResponse.SendResponse(msg) =>
sendResponse(msg)
case VehicleResponse.UpdateAmsSpawnPoint(list) =>
amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
DrawCurrentAmsSpawnPoint()
case VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle) =>
if(tplayer_guid != guid) {
interstellarFerry = Some(vehicle)
vehicleService ! Service.Leave(Some(old_channel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
vehicleService ! Service.Join(temp_channel) //temporary vehicle-specific channel (driver name, plus extra)
}
case VehicleResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete) =>
vehicle.PassengerInSeat(player) match {
case Some(_) =>
vehicleService ! Service.Leave(Some(temp_channel)) //temporary vehicle-specific channel (see above)
deadState = DeadState.Release
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true))
interstellarFerry = Some(vehicle) //on the other continent and registered to that continent's GUID system
interstellarFerryTopLevelGUID = Some(vehicle_to_delete) //vehicle.GUID, or previously a higher level parent
LoadZonePhysicalSpawnPoint(vehicle.Continent, vehicle.Position, vehicle.Orientation, 1)
case None =>
interstellarFerry match {
case None =>
vehicleService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones
case Some(_) => ;
//wait patiently
}
}
case VehicleResponse.ForceDismountVehicleCargo(cargo_guid, bailed, requestedByPassenger, kicked) =>
DismountVehicleCargo(tplayer_guid, cargo_guid, bailed, requestedByPassenger, kicked)
case VehicleResponse.KickCargo(vehicle, speed, delay) =>
if(player.VehicleSeated.nonEmpty && deadState == DeadState.Alive) {
if(speed > 0) {
val strafe = if(CargoOrientation(vehicle) == 1) 2 else 1
val reverseSpeed = if(strafe > 1) 0 else speed
//strafe or reverse, not both
controlled = Some(reverseSpeed)
sendResponse(ServerVehicleOverrideMsg(true, true, true, false, 0, strafe, reverseSpeed, Some(0)))
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(delay milliseconds, self, VehicleServiceResponse(toChannel, tplayer_guid, VehicleResponse.KickCargo(vehicle, 0, delay)))
}
else {
controlled = None
sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None))
}
}
case _ => ;
}
}
/**
* na
* @param decorator custom text for these messages in the log
* @param target an optional the target object
* @param targetGUID the expected globally unique identifier of the target object
*/
def LogCargoEventMissingVehicleError(decorator : String, target : Option[PlanetSideGameObject], targetGUID : PlanetSideGUID) : Unit = {
target match {
case Some(_ : Vehicle) => ;
case Some(_) => log.error(s"$decorator target $targetGUID no longer identifies as a vehicle")
case None => log.error(s"$decorator target $targetGUID has gone missing")
}
}
/**
* na
* @param cargoGUID na
* @param carrierGUID na
* @param mountPoint na
* @param iteration na
*/
def HandleCheckCargoDismounting(cargoGUID : PlanetSideGUID, carrierGUID : PlanetSideGUID, mountPoint : Int, iteration : Int) : Unit = {
(continent.GUID(cargoGUID), continent.GUID(carrierGUID)) match {
case ((Some(vehicle : Vehicle), Some(cargo_vehicle : Vehicle))) =>
HandleCheckCargoDismounting(cargoGUID, vehicle, carrierGUID, cargo_vehicle, mountPoint, iteration)
case (cargo, carrier) if iteration > 0 =>
log.error(s"HandleCheckCargoDismounting: participant vehicles changed in the middle of a mounting event")
LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: cargo", cargo, cargoGUID)
LogCargoEventMissingVehicleError("HandleCheckCargoDismounting: carrier", carrier, carrierGUID)
case _ =>
}
}
/**
* na
* @param cargoGUID na
* @param cargo na
* @param carrierGUID na
* @param carrier na
* @param mountPoint na
* @param iteration na
*/
def HandleCheckCargoDismounting(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, mountPoint : Int, iteration : Int) : Unit = {
carrier.CargoHold(mountPoint) match {
case Some(hold) if !hold.isOccupied =>
val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position)
log.debug(s"HandleCheckCargoDismounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=225")
if(distance > 225) {
//cargo vehicle has moved far enough away; close the carrier's hold door
log.info(s"HandleCheckCargoDismounting: dismount of cargo vehicle from carrier complete at distance of $distance")
vehicleService ! VehicleServiceMessage(
continent.Id,
VehicleAction.SendResponse(
player.GUID,
CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0)
)
)
//sending packet to the cargo vehicle's client results in player locking himself in his vehicle
//player gets stuck as "always trying to remount the cargo hold"
//obviously, don't do this
}
else if(iteration > 40) {
//cargo vehicle has spent too long not getting far enough away; restore the cargo's mount in the carrier hold
cargo.MountedIn = carrierGUID
hold.Occupant = cargo
StartBundlingPackets()
CargoMountBehaviorForAll(carrier, cargo, mountPoint)
StopBundlingPackets()
}
else {
//cargo vehicle did not move far away enough yet and there is more time to wait; reschedule check
import scala.concurrent.ExecutionContext.Implicits.global
cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(cargoGUID, carrierGUID, mountPoint, iteration + 1))
}
case None =>
log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint")
case _ =>
if(iteration == 0) {
log.warn(s"HandleCheckCargoDismounting: carrier vehicle $carrier will not discharge the cargo of hold #$mountPoint; this operation was initiated incorrectly")
}
else {
log.error(s"HandleCheckCargoDismounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40")
}
}
}
/**
* na
* @param cargoGUID the vehicle being ferried as cargo
* @param carrierGUID the ferrying carrier vehicle
* @param mountPoint the cargo hold to which the cargo vehicle is stowed
* @param iteration number of times a proper mounting for this combination has been queried
*/
def HandleCheckCargoMounting(cargoGUID : PlanetSideGUID, carrierGUID : PlanetSideGUID, mountPoint : Int, iteration : Int) : Unit = {
(continent.GUID(cargoGUID), continent.GUID(carrierGUID)) match {
case ((Some(cargo : Vehicle), Some(carrier : Vehicle))) =>
HandleCheckCargoMounting(cargoGUID, cargo, carrierGUID, carrier, mountPoint, iteration)
case (cargo, carrier) if iteration > 0 =>
log.error(s"HandleCheckCargoMounting: participant vehicles changed in the middle of a mounting event")
LogCargoEventMissingVehicleError("HandleCheckCargoMounting: cargo", cargo, cargoGUID)
LogCargoEventMissingVehicleError("HandleCheckCargoMounting: carrier", carrier, carrierGUID)
case _ => ;
}
}
/**
* na
* @param cargoGUID the vehicle being ferried as cargo
* @param cargo the vehicle being ferried as cargo
* @param carrierGUID the ferrying carrier vehicle
* @param carrier the ferrying carrier vehicle
* @param mountPoint the cargo hold to which the cargo vehicle is stowed
* @param iteration number of times a proper mounting for this combination has been queried
*/
def HandleCheckCargoMounting(cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, mountPoint : Int, iteration : Int) : Unit = {
val distance = Vector3.DistanceSquared(cargo.Position, carrier.Position)
carrier.CargoHold(mountPoint) match {
case Some(hold) if !hold.isOccupied =>
log.debug(s"HandleCheckCargoMounting: mount distance between $cargoGUID and $carrierGUID - actual=$distance, target=64")
if(distance <= 64) {
//cargo vehicle is close enough to assume to be physically within the carrier's hold; mount it
log.info(s"HandleCheckCargoMounting: mounting cargo vehicle in carrier at distance of $distance")
cargo.MountedIn = carrierGUID
hold.Occupant = cargo
cargo.Velocity = None
vehicleService ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)))
vehicleService ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)))
StartBundlingPackets()
val (attachMsg, mountPointMsg) = CargoMountBehaviorForAll(carrier, cargo, mountPoint)
StopBundlingPackets()
log.info(s"HandleCheckCargoMounting: $attachMsg")
log.info(s"HandleCheckCargoMounting: $mountPointMsg")
}
else if(distance > 625 || iteration >= 40) {
//vehicles moved too far away or took too long to get into proper position; abort mounting
log.info("HandleCheckCargoMounting: cargo vehicle is too far away or didn't mount within allocated time - aborting")
vehicleService ! VehicleServiceMessage(
continent.Id,
VehicleAction.SendResponse(
player.GUID,
CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0)
)
)
//sending packet to the cargo vehicle's client results in player locking himself in his vehicle
//player gets stuck as "always trying to remount the cargo hold"
//obviously, don't do this
}
else {
//cargo vehicle still not in position but there is more time to wait; reschedule check
import scala.concurrent.ExecutionContext.Implicits.global
cargoMountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoMounting(cargoGUID, carrierGUID, mountPoint, iteration = iteration + 1))
}
case None => ;
log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier does not have a cargo hold #$mountPoint")
case _ =>
if(iteration == 0) {
log.warn(s"HandleCheckCargoMounting: carrier vehicle $carrier already possesses cargo in hold #$mountPoint; this operation was initiated incorrectly")
}
else {
log.error(s"HandleCheckCargoMounting: something has attached to the carrier vehicle $carrier cargo of hold #$mountPoint while a cargo dismount event was ongoing; stopped at iteration $iteration / 40")
}
}
}
/**
* Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
* that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle.
* @see `CargoMountPointStatusMessage`
* @see `CargoOrientation`
* @see `ObjectAttachMessage`
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached;
* also known as a "cargo hold"
* @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
*/
def CargoMountMessages(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = {
CargoMountMessages(carrier.GUID, cargo.GUID, mountPoint, CargoOrientation(cargo))
}
/**
* Produce an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
* that will set up a realized parent-child association between a ferrying vehicle and a ferried vehicle.
* @see `CargoMountPointStatusMessage`
* @see `ObjectAttachMessage`
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached
* @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
*/
def CargoMountMessages(carrierGUID : PlanetSideGUID, cargoGUID : PlanetSideGUID, mountPoint : Int, orientation : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = {
(
ObjectAttachMessage(carrierGUID, cargoGUID, mountPoint),
CargoMountPointStatusMessage(carrierGUID, cargoGUID, cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.Occupied, orientation)
)
}
/**
* The orientation of a cargo vehicle as it is being loaded into and contained by a carrier vehicle.
* The type of carrier is not an important consideration in determining the orientation, oddly enough.
* @param vehicle the cargo vehicle
* @return the orientation as an `Integer` value;
* `0` for almost all cases
*/
def CargoOrientation(vehicle : Vehicle) : Int = {
if(vehicle.Definition == GlobalDefinitions.router) {
1
}
else {
0
}
}
/**
* Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet only to this client.
* @see `CargoMountPointStatusMessage`
* @see `ObjectAttachMessage`
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached
* @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
*/
def CargoMountBehaviorForUs(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = {
val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint)
CargoMountMessagesForUs(attachMessage, mountPointStatusMessage)
msgs
}
/**
* Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet only to this client.
* @see `CargoMountPointStatusMessage`
* @see `ObjectAttachMessage`
* @param attachMessage an `ObjectAttachMessage` packet suitable for initializing cargo operations
* @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations
*/
def CargoMountMessagesForUs(attachMessage : ObjectAttachMessage, mountPointStatusMessage : CargoMountPointStatusMessage) : Unit = {
sendResponse(attachMessage)
sendResponse(mountPointStatusMessage)
}
/**
* Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one.
* @see `CargoMountPointStatusMessage`
* @see `ObjectAttachMessage`
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached
* @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
*/
def CargoMountBehaviorForOthers(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = {
val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint)
CargoMountMessagesForOthers(attachMessage, mountPointStatusMessage)
msgs
}
/**
* Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to all other clients, not this one.
* @see `CargoMountPointStatusMessage`
* @see `ObjectAttachMessage`
* @param attachMessage an `ObjectAttachMessage` packet suitable for initializing cargo operations
* @param mountPointStatusMessage a `CargoMountPointStatusMessage` packet suitable for initializing cargo operations
*/
def CargoMountMessagesForOthers(attachMessage : ObjectAttachMessage, mountPointStatusMessage : CargoMountPointStatusMessage) : Unit = {
val pguid = player.GUID
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(pguid, attachMessage))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(pguid, mountPointStatusMessage))
}
/**
* Dispatch an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet to everyone.
* @see `CargoMountPointStatusMessage`
* @see `ObjectAttachMessage`
* @param carrier the ferrying vehicle
* @param cargo the ferried vehicle
* @param mountPoint the point on the ferryoing vehicle where the ferried vehicle is attached
* @return a tuple composed of an `ObjectAttachMessage` packet and a `CargoMountPointStatusMessage` packet
*/
def CargoMountBehaviorForAll(carrier : Vehicle, cargo : Vehicle, mountPoint : Int) : (ObjectAttachMessage, CargoMountPointStatusMessage) = {
val msgs @ (attachMessage, mountPointStatusMessage) = CargoMountMessages(carrier, cargo, mountPoint)
CargoMountMessagesForUs(attachMessage, mountPointStatusMessage)
CargoMountMessagesForOthers(attachMessage, mountPointStatusMessage)
msgs
}
/**
* na
* @param target na
*/
def HandleVehicleDamageResolution(target : Vehicle) : Unit = {
val targetGUID = target.GUID
val playerGUID = player.GUID
val players = target.Seats.values.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
})
target.LastShot match { //TODO: collision damage from/in history
case Some(shot) =>
if(target.Health > 0) {
//activity on map
continent.Activity ! Zone.HotSpot.Activity(shot.target, shot.projectile.owner, shot.hit_pos)
//alert occupants to damage source
HandleVehicleDamageAwareness(target, playerGUID, shot)
}
else {
//alert to vehicle death (hence, occupants' deaths)
HandleVehicleDestructionAwareness(target, shot)
}
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health))
vehicleService ! VehicleServiceMessage(s"${target.Actor}", VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 68, target.Shields))
case None => ;
}
}
/**
* na
* @param target na
* @param attribution na
* @param lastShot na
*/
def HandleVehicleDamageAwareness(target : Vehicle, attribution : PlanetSideGUID, lastShot : ResolvedProjectile) : Unit = {
//alert occupants to damage source
target.Seats.values.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
}).foreach(seat => {
val tplayer = seat.Occupant.get
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(attribution, tplayer.GUID))
})
//alert cargo occupants to damage source
target.CargoHolds.values.foreach(hold => {
hold.Occupant match {
case Some(cargo) =>
cargo.Health = 0
cargo.Shields = 0
cargo.History(lastShot)
HandleVehicleDamageAwareness(cargo, attribution, lastShot)
case None => ;
}
})
}
/**
* na
* @param target na
* @param attribution na
* @param lastShot na
*/
def HandleVehicleDestructionAwareness(target : Vehicle, lastShot : ResolvedProjectile) : Unit = {
val playerGUID = player.GUID
val continentId = continent.Id
//alert to vehicle death (hence, occupants' deaths)
target.Seats.values.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
}).foreach(seat => {
val tplayer = seat.Occupant.get
val tplayerGUID = tplayer.GUID
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID))
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self
})
//vehicle wreckage has no weapons
target.Weapons.values
.filter {
_.Equipment.nonEmpty
}
.foreach(slot => {
val wep = slot.Equipment.get
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID))
})
target.CargoHolds.values.foreach(hold => {
hold.Occupant match {
case Some(cargo) =>
cargo.Health = 0
cargo.Shields = 0
cargo.Position += Vector3.z(1)
cargo.History(lastShot) //necessary to kill cargo vehicle occupants //TODO: collision damage
HandleVehicleDestructionAwareness(cargo, lastShot) //might cause redundant packets
case None => ;
}
})
target.Definition match {
case GlobalDefinitions.ams =>
target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
case GlobalDefinitions.router =>
target.Actor ! Deployment.TryDeploymentChange(DriveState.Undeploying)
BeforeUnloadVehicle(target)
localService ! LocalServiceMessage(continent.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), target, None))
case _ => ;
}
avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(target.GUID, playerGUID, playerGUID, target.Position))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(1 minute)))
}
/**
* na
* @param target na
*/
def HandleTurretDeployableDamageResolution(target : TurretDeployable) : Unit = {
//spitfires and field turrets
val health = target.Health
val guid = target.GUID
val continentId = continent.Id
if(health <= 0) {
//if occupants, kill them
target.Seats.values
.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
})
.foreach(seat => {
val tplayer = seat.Occupant.get
val tplayerGUID = tplayer.GUID
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID))
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self
})
//destroy weapons
target.Weapons.values
.map(slot => slot.Equipment)
.collect { case Some(weapon) =>
val wguid = weapon.GUID
sendResponse(ObjectDeleteMessage(wguid, 0))
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(player.GUID, wguid))
}
AnnounceDestroyDeployable(target, None)
}
avatarService ! AvatarServiceMessage(continentId, AvatarAction.PlanetsideAttribute(guid, 0, health))
}
def HandleFacilityTurretDamageResolution(target : FacilityTurret) : Unit = {
val targetGUID = target.GUID
val playerGUID = player.GUID
val continentId = continent.Id
val players = target.Seats.values.filter(seat => {
seat.isOccupied && seat.Occupant.get.isAlive
})
if(target.Health > 1) { //TODO turret "death" at 0, as is proper
//alert occupants to damage source
players.foreach(seat => {
val tplayer = seat.Occupant.get
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.HitHint(playerGUID, tplayer.GUID))
})
}
else {
//alert to vehicle death (hence, occupants' deaths)
players.foreach(seat => {
val tplayer = seat.Occupant.get
val tplayerGUID = tplayer.GUID
avatarService ! AvatarServiceMessage(tplayer.Name, AvatarAction.KilledWhileInVehicle(tplayerGUID))
avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(tplayerGUID, tplayerGUID)) //dead player still sees self
})
//turret wreckage has no weapons
// target.Weapons.values
// .filter {
// _.Equipment.nonEmpty
// }
// .foreach(slot => {
// val wep = slot.Equipment.get
// avatarService ! AvatarServiceMessage(continentId, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, wep.GUID))
// })
// avatarService ! AvatarServiceMessage(continentId, AvatarAction.Destroy(targetGUID, playerGUID, playerGUID, player.Position))
target.Health = 1
vehicleService ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.MaxHealth)) //TODO not necessary
if(target.Upgrade != TurretUpgrade.None) {
vehicleService ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(target), continent))
vehicleService ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(target, continent, TurretUpgrade.None))
}
}
vehicleService ! VehicleServiceMessage(continentId, VehicleAction.PlanetsideAttribute(Service.defaultPlayerGUID, targetGUID, 0, target.Health))
}
/**
* na
* @param tplayer na
* @param vehicle na
*/
def HandleNtuCharging(tplayer : Player, vehicle : Vehicle) : Unit = {
log.trace(s"NtuCharging: Vehicle ${vehicle.GUID} is charging NTU capacitor.")
if(vehicle.Capacitor < vehicle.Definition.MaximumCapacitor) {
// Charging
vehicle.Capacitor += 100
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.ceil((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 1L)) // orb particle effect on
antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) // Repeat until fully charged
}
else {
// Fully charged
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.ceil((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI
// Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side
context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying))
}
}
/**
* na
* @param tplayer na
* @param vehicle na
* @param silo_guid na
*/
def HandleNtuDischarging(tplayer : Player, vehicle : Vehicle, silo_guid : PlanetSideGUID) : Unit = {
log.trace(s"NtuDischarging: Vehicle ${vehicle.GUID} is discharging NTU into silo $silo_guid")
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 0L)) // orb particle effect off
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on
var silo = continent.GUID(silo_guid).get.asInstanceOf[ResourceSilo]
// Check vehicle is still deployed before continuing. User can undeploy manually or vehicle may not longer be present.
if(vehicle.DeploymentState == DriveState.Deployed) {
if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) {
// Make sure we don't exceed the silo maximum charge or remove much NTU from ANT if maximum is reached, or try to make ANT go below 0 NTU
var chargeToDeposit = Math.min(Math.min(vehicle.Capacitor, 100), (silo.MaximumCharge - silo.ChargeLevel))
vehicle.Capacitor -= chargeToDeposit
silo.Actor ! ResourceSilo.UpdateChargeLevel(chargeToDeposit)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 1L)) // panel glow on & orb particles on
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.ceil((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI
//todo: grant BEP to user
//todo: grant BEP to squad in range
//todo: notify map service to update ntu % on map for all users
//todo: handle silo orb / panel glow properly if more than one person is refilling silo and one player stops. effects should stay on until all players stop
if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) {
log.trace(s"NtuDischarging: ANT not empty and Silo not full. Scheduling another discharge")
// Silo still not full and ant still has charge left - keep rescheduling ticks
antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, vehicle, silo_guid))
}
else {
log.trace(s"NtuDischarging: ANT NTU empty or Silo NTU full.")
// Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side
context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off
antDischargingTick.cancel()
}
}
else {
// This shouldn't normally be run, only if the client thinks the ANT has capacitor charge when it doesn't, or thinks the silo isn't full when it is.
log.warn(s"NtuDischarging: Invalid discharge state. ANT Capacitor: ${vehicle.Capacitor} Silo Capacitor: ${silo.ChargeLevel}")
// Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side
context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off
antDischargingTick.cancel()
}
}
else {
log.trace(s"NtuDischarging: Vehicle is no longer deployed. Removing effects")
// Vehicle has changed from deployed and this should be the last timer tick sent
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off
antDischargingTick.cancel()
}
}
/**
* na
* @param progressType na
* @param tplayer na
* @param target na
* @param tool_guid na
* @param delta na
* @param completeAction na
* @param tickAction na
*/
def HandleHackingProgress(progressType : Int, tplayer : Player, target : PlanetSideServerObject, tool_guid : PlanetSideGUID, delta : Float, completeAction : ()=>Unit, tickAction : Option[()=>Unit]) : Unit = {
progressBarUpdate.cancel
if(progressBarValue.isDefined) {
val progressBarVal : Float = progressBarValue.get + delta
val vis = if(progressBarVal == 0L) {
//hack state for progress bar visibility
HackState.Start
}
else if(progressBarVal > 100L) {
HackState.Finished
}
else {
HackState.Ongoing
}
sendResponse(HackMessage(progressType, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L))
if(progressBarVal > 100) {
//done
progressBarValue = None
// sendResponse(HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L))
completeAction()
}
else {
//continue next tick
tickAction.getOrElse(() => Unit)()
progressBarValue = Some(progressBarVal)
import scala.concurrent.ExecutionContext.Implicits.global
progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, HackingProgress(progressType, tplayer, target, tool_guid, delta, completeAction))
}
}
}
/**
* na
* @param tplayer na
*/
def HandleSetCurrentAvatar(tplayer : Player) : Unit = {
player = tplayer
val guid = tplayer.GUID
StartBundlingPackets()
InitializeDeployableUIElements(avatar)
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 75, 0))
sendResponse(SetCurrentAvatarMessage(guid, 0, 0))
sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on //TODO once per respawn?
sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z)))
if(spectator) {
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None))
}
(0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => {
sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot
sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant
//TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
})
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
//TODO if Medkit does not have shortcut, add to a free slot or write over slot 64
sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))
sendResponse(ChangeShortcutBankMessage(guid, 0))
//Favorites lists
val (inf, veh) = avatar.EquipmentLoadouts.Loadouts.partition { case (index, _) => index < 10 }
inf.foreach {
case (index, loadout : InfantryLoadout) =>
sendResponse(FavoritesMessage(LoadoutType.Infantry, guid, index, loadout.label, InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)))
}
veh.foreach {
case (index, loadout : VehicleLoadout) =>
sendResponse(FavoritesMessage(LoadoutType.Vehicle, guid, index - 10, loadout.label))
}
sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this
deadState = DeadState.Alive
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
sendResponse(PlanetsideAttributeMessage(guid, 53, tplayer.LFS))
sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0)))
(1 to 73).foreach(i => {
// not all GUID's are set, and not all of the set ones will always be zero; what does this section do?
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0))
})
(0 to 30).foreach(i => {
//TODO 30 for a new character only?
sendResponse(AvatarStatisticsMessage(2, Statistics(0L)))
})
//AvatarAwardMessage
//DisplayAwardMessage
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
(0 to 9).foreach(line => {
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite("")))
})
sendResponse(SquadDetailDefinitionUpdateMessage.Init)
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.AssociateWithSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18)))
//MapObjectStateBlockMessage and ObjectCreateMessage?
//TacticsMessage?
//change the owner on our deployables (re-draw the icons for our deployables too)
val name = tplayer.Name
val faction = tplayer.Faction
continent.DeployableList
.filter(_.OwnerName.contains(name))
.foreach(obj => {
obj.Owner = guid
drawDeloyableIcon(obj)
})
StopBundlingPackets()
drawDeloyableIcon = DontRedrawIcons
//assert or transfer vehicle ownership
continent.GUID(player.VehicleOwned) match {
case Some(vehicle : Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
vehicle.Owner = guid
vehicleService ! VehicleServiceMessage(s"${continent.Id}/${tplayer.Faction}", VehicleAction.Ownership(guid, vehicle.GUID))
case _ =>
player.VehicleOwned = None
}
//if driver of a vehicle, summon any passengers and cargo vehicles left behind on previous continent
GetVehicleAndSeat() match {
case (Some(vehicle), Some(0)) =>
LoadZoneTransferPassengerMessages(
guid,
continent.Id,
TransportVehicleChannelName(vehicle),
vehicle,
interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID)
)
interstellarFerryTopLevelGUID = None
case _ => ;
}
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
}
def handleControlPkt(pkt : PlanetSideControlPacket) = {
pkt match {
case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) =>
log.trace(s"SYNC: $sync")
val serverTick = Math.abs(System.nanoTime().toInt) // limit the size to prevent encoding error
sendResponse(ControlSyncResp(diff, serverTick, fa, fb, fb, fa))
case TeardownConnection(_) =>
log.info("Good bye")
case default =>
log.warn(s"Unhandled ControlPacket $default")
}
}
def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match {
case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) =>
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
log.info(s"New world login to $server with Token:$token. $clientVersion")
//TODO begin temp player character auto-loading; remove later
import net.psforever.objects.GlobalDefinitions._
import net.psforever.types.CertificationType._
val faction = PlanetSideEmpire.VS
val avatar = new Avatar(41605313L+sessionId, s"TestCharacter$sessionId", faction, CharacterGender.Female, 41, CharacterVoice.Voice1)
avatar.Certifications += StandardAssault
avatar.Certifications += MediumAssault
avatar.Certifications += StandardExoSuit
avatar.Certifications += AgileExoSuit
avatar.Certifications += ReinforcedExoSuit
avatar.Certifications += ATV
avatar.Certifications += Harasser
//
avatar.Certifications += InfiltrationSuit
avatar.Certifications += Sniping
avatar.Certifications += AntiVehicular
avatar.Certifications += HeavyAssault
avatar.Certifications += SpecialAssault
avatar.Certifications += EliteAssault
avatar.Certifications += GroundSupport
avatar.Certifications += GroundTransport
avatar.Certifications += Flail
avatar.Certifications += Switchblade
avatar.Certifications += AssaultBuggy
avatar.Certifications += ArmoredAssault1
avatar.Certifications += ArmoredAssault2
avatar.Certifications += AirCavalryScout
avatar.Certifications += AirCavalryAssault
avatar.Certifications += AirCavalryInterceptor
avatar.Certifications += AirSupport
avatar.Certifications += GalaxyGunship
avatar.Certifications += Phantasm
avatar.Certifications += UniMAX
avatar.Certifications += Engineering
avatar.Certifications += CombatEngineering
avatar.Certifications += FortificationEngineering
avatar.Certifications += AssaultEngineering
avatar.Certifications += Hacking
avatar.Certifications += AdvancedHacking
avatar.CEP = 6000001
this.avatar = avatar
InitializeDeployableQuantities(avatar) //set deployables ui elements
AwardBattleExperiencePoints(avatar, 1000000L)
player = new Player(avatar)
player.Position = Vector3(3561.0f, 2854.0f, 90.859375f) //home3, HART C
// player.Position = Vector3(3940.3984f, 4343.625f, 266.45312f) //z6, Anguta
// player.Position = Vector3(3571.2266f, 3278.0938f, 119.0f) //ce test
player.Orientation = Vector3(0f, 0f, 90f)
//player.Position = Vector3(4262.211f ,4067.0625f ,262.35938f) //z6, Akna.tower
//player.Orientation = Vector3(0f, 0f, 132.1875f)
// player.ExoSuit = ExoSuitType.MAX //TODO strange issue; divide number above by 10 when uncommenting
player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(player.Faction))
player.Slot(2).Equipment = Tool(suppressor)
player.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(player.Faction))
player.Slot(6).Equipment = AmmoBox(bullet_9mm)
player.Slot(9).Equipment = AmmoBox(bullet_9mm)
player.Slot(12).Equipment = AmmoBox(bullet_9mm)
player.Slot(33).Equipment = AmmoBox(bullet_9mm_AP)
player.Slot(36).Equipment = AmmoBox(GlobalDefinitions.StandardPistolAmmo(player.Faction))
player.Slot(39).Equipment = SimpleItem(remote_electronics_kit)
player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit)
player.Inventory.Items.foreach { _.obj.Faction = faction }
//TODO end temp player character auto-loading
self ! ListAccountCharacters
import scala.concurrent.ExecutionContext.Implicits.global
clientKeepAlive.cancel
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
case msg @ MountVehicleCargoMsg(player_guid, vehicle_guid, cargo_vehicle_guid, unk4) =>
log.info(msg.toString)
(continent.GUID(vehicle_guid), continent.GUID(cargo_vehicle_guid)) match {
case (Some(_ : Vehicle), Some(carrier : Vehicle)) =>
carrier.Definition.Cargo.headOption match {
case Some((mountPoint, _)) => //begin the mount process - open the cargo door
val reply = CargoMountPointStatusMessage(cargo_vehicle_guid, PlanetSideGUID(0), vehicle_guid, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0)
log.debug(reply.toString)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player.GUID, reply))
sendResponse(reply)
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
// Start timer to check every second if the vehicle is close enough to mount, or far enough away to cancel the mounting
cargoMountTimer.cancel
cargoMountTimer = context.system.scheduler.scheduleOnce(1 second, self, CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, mountPoint, iteration = 0))
case None =>
log.warn(s"MountVehicleCargoMsg: target carrier vehicle (${carrier.Definition.Name}) does not have a cargo hold")
}
case (None, _) | (Some(_), None) =>
log.warn(s"MountVehicleCargoMsg: one or more of the target vehicles do not exist - $cargo_vehicle_guid or $vehicle_guid")
case _ => ;
}
case msg @ DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) =>
log.info(msg.toString)
if(!requestedByPassenger) {
DismountVehicleCargo(player_guid, cargo_guid, bailed, requestedByPassenger, kicked)
}
case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
log.info("Handling " + msg)
sendResponse(ActionResultMessage.Pass)
self ! ListAccountCharacters
case msg @ CharacterRequestMessage(charId, action) =>
log.info("Handling " + msg)
action match {
case CharacterRequestAction.Delete =>
sendResponse(ActionResultMessage.Fail(1))
case CharacterRequestAction.Select =>
//TODO check if can spawn on last continent/location from player?
//TODO if yes, get continent guid accessors
//TODO if no, get sanctuary guid accessors and reset the player's expectations
cluster ! InterstellarCluster.RequestClientInitialization()
case default =>
log.error("Unsupported " + default + " in " + msg)
}
case KeepAliveMessage(code) =>
sendResponse(KeepAliveMessage())
case msg @ BeginZoningMessage() =>
log.info("Reticulating splines ...")
val continentId = continent.Id
traveler.zone = continentId
val faction = player.Faction
val factionOnContinentChannel = s"$continentId/$faction"
avatarService ! Service.Join(continentId)
avatarService ! Service.Join(factionOnContinentChannel)
localService ! Service.Join(continentId)
localService ! Service.Join(factionOnContinentChannel)
vehicleService ! Service.Join(continentId)
vehicleService ! Service.Join(factionOnContinentChannel)
configZone(continent)
sendResponse(TimeOfDayMessage(1191182336))
//custom
sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary."
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
//(0 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) })
//find and reclaim own deployables, if any
val guid = player.GUID
val foundDeployables = continent.DeployableList.filter(obj => obj.OwnerName.contains(player.Name) && obj.Health > 0)
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(foundDeployables, continent))
foundDeployables.foreach(obj => {
if(avatar.Deployables.Add(obj)) {
obj.Owner = guid
log.info(s"Found a ${obj.Definition.Name} of ours while loading the zone")
}
})
//render deployable objects
val (turrets, normal) = continent.DeployableList.partition(obj =>
DeployableToolbox.UnifiedType(obj.Definition.Item) == DeployedItem.portable_manned_turret
)
normal.foreach(obj => {
val definition = obj.Definition
sendResponse(
ObjectCreateMessage(
definition.ObjectId,
obj.GUID,
definition.Packet.ConstructorData(obj).get
)
)
})
turrets.foreach(obj => {
val objGUID = obj.GUID
val definition = obj.Definition
sendResponse(
ObjectCreateMessage(
definition.ObjectId,
objGUID,
definition.Packet.ConstructorData(obj).get
)
)
//seated players
obj.asInstanceOf[Mountable].Seats.values
.map(_.Occupant)
.collect {
case Some(occupant) =>
if(occupant.isAlive) {
val tdefintion = occupant.Definition
sendResponse(
ObjectCreateMessage(
tdefintion.ObjectId,
occupant.GUID,
ObjectCreateMessageParent(objGUID, 0),
tdefintion.Packet.ConstructorData(occupant).get
)
)
}
}
})
normal
.filter(_.Definition.DeployCategory == DeployableCategory.Sensors)
.foreach(obj => { sendResponse(TriggerEffectMessage(obj.GUID, "on", true, 1000)) })
//draw our faction's deployables on the map
continent.DeployableList
.filter(obj => obj.Faction == faction && obj.Health > 0)
.foreach(obj => {
val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0)))
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
})
//render Equipment that was dropped into zone before the player arrived
continent.EquipmentOnGround.foreach(item => {
val definition = item.Definition
sendResponse(
ObjectCreateMessage(
definition.ObjectId,
item.GUID,
DroppedItemData(PlacementData(item.Position, item.Orientation), definition.Packet.ConstructorData(item).get)
)
)
})
//load active players in zone (excepting players who are seated or players who are us)
val live = continent.LivePlayers
live.filterNot(tplayer => { tplayer.GUID == player.GUID || tplayer.VehicleSeated.nonEmpty })
.foreach(char => {
val tdefintion = char.Definition
sendResponse(ObjectCreateMessage(tdefintion.ObjectId, char.GUID, char.Definition.Packet.ConstructorData(char).get))
if(char.UsingSpecial == SpecialExoSuitDefinition.Mode.Anchored) {
sendResponse(PlanetsideAttributeMessage(char.GUID, 19, 1))
}
})
//load corpses in zone
continent.Corpses.foreach {
TurnPlayerIntoCorpse
}
//load vehicles in zone (put separate the one we may be using)
val (wreckages, (vehicles, usedVehicle)) = {
val (a, b) = continent.Vehicles.partition(vehicle => { vehicle.Health == 0 && vehicle.Definition.DestroyedModel.nonEmpty })
(a, (continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.PassengerInSeat(player).isDefined =>
b.partition { _.GUID != vehicle.GUID }
case None =>
(b, List.empty[Vehicle])
case _ =>
//throw error since VehicleSeated didn't point to a vehicle?
player.VehicleSeated = None
(b, List.empty[Vehicle])
}))
}
//active vehicles (and some wreckage)
vehicles.foreach(vehicle => {
val vguid = vehicle.GUID
val vdefinition = vehicle.Definition
sendResponse(ObjectCreateMessage(vdefinition.ObjectId, vguid, vdefinition.Packet.ConstructorData(vehicle).get))
//occupants other than driver
vehicle.Seats
.filter({ case(index, seat) => seat.isOccupied && live.contains(seat.Occupant.get) && index > 0 })
.foreach({ case(index, seat) =>
val tplayer = seat.Occupant.get
val tdefintion = tplayer.Definition
sendResponse(
ObjectCreateMessage(
tdefintion.ObjectId,
tplayer.GUID,
ObjectCreateMessageParent(vguid, index),
tdefintion.Packet.ConstructorData(tplayer).get
)
)
})
ReloadVehicleAccessPermissions(vehicle)
})
//our vehicle would have already been loaded; see NewPlayerLoaded/AvatarCreate
usedVehicle.headOption match {
case Some(vehicle) if !vehicle.PassengerInSeat(player).contains(0) =>
//if passenger, attempt to depict any other passengers already in this zone
val vguid = vehicle.GUID
vehicle.Seats
.filter({ case(index, seat) => seat.isOccupied && !seat.Occupant.contains(player) && live.contains(seat.Occupant.get) && index > 0 })
.foreach({ case(index, seat) =>
val tplayer = seat.Occupant.get
val tdefintion = tplayer.Definition
sendResponse(
ObjectCreateMessage(
tdefintion.ObjectId,
tplayer.GUID,
ObjectCreateMessageParent(vguid, index),
tdefintion.Packet.ConstructorData(tplayer).get
)
)
})
case _ => ; //driver, or no vehicle
}
//vehicle wreckages
wreckages.foreach(vehicle => {
sendResponse(
ObjectCreateMessage(
vehicle.Definition.DestroyedModel.get.id,
vehicle.GUID,
DestroyedVehicleConverter.converter.ConstructorData(vehicle).get
)
)
})
//cargo occupants (including our own vehicle as cargo)
vehicles.collect { case vehicle if vehicle.CargoHolds.nonEmpty =>
vehicle.CargoHolds.collect({ case (index, hold) if hold.isOccupied => {
CargoMountBehaviorForAll(vehicle, hold.Occupant.get, index) //CargoMountBehaviorForUs can fail to attach the cargo vehicle on some clients
}})
}
//special deploy states
val deployedVehicles = vehicles.filter(_.DeploymentState == DriveState.Deployed)
deployedVehicles.filter(_.Definition == GlobalDefinitions.ams).foreach(obj => {
sendResponse(PlanetsideAttributeMessage(obj.GUID, 81, 1))
})
deployedVehicles.filter(_.Definition == GlobalDefinitions.router).foreach(obj => {
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deploying, 0, false, Vector3.Zero))
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, false, Vector3.Zero))
ToggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
})
//implant terminals
continent.Map.TerminalToInterface.foreach({ case ((terminal_guid, interface_guid)) =>
val parent_guid = PlanetSideGUID(terminal_guid)
continent.GUID(interface_guid) match {
case Some(obj : Terminal) =>
val objDef = obj.Definition
sendResponse(
ObjectCreateMessage(
objDef.ObjectId,
PlanetSideGUID(interface_guid),
ObjectCreateMessageParent(parent_guid, 1),
objDef.Packet.ConstructorData(obj).get
)
)
case _ => ;
}
//seat terminal occupants
continent.GUID(terminal_guid) match {
case Some(obj : Mountable) =>
obj.Seats(0).Occupant match {
case Some(tplayer) =>
val tdefintion = tplayer.Definition
sendResponse(
ObjectCreateMessage(
tdefintion.ObjectId,
tplayer.GUID,
ObjectCreateMessageParent(parent_guid, 0),
tdefintion.Packet.ConstructorData(tplayer).get
)
)
case None => ;
}
case _ => ;
}
})
//base turrets
continent.Map.TurretToWeapon.foreach({ case((turret_guid, weapon_guid)) =>
val parent_guid = PlanetSideGUID(turret_guid)
continent.GUID(turret_guid) match {
case Some(turret : FacilityTurret) =>
//attached weapon
turret.ControlledWeapon(1) match {
case Some(obj : Tool) =>
val objDef = obj.Definition
sendResponse(
ObjectCreateMessage(
objDef.ObjectId,
obj.GUID,
ObjectCreateMessageParent(parent_guid, 1),
objDef.Packet.ConstructorData(obj).get
)
)
case _ => ;
}
//reserved ammunition?
//TODO need to register if it exists
//seat turret occupant
turret.Seats(0).Occupant match {
case Some(tplayer) =>
val tdefintion = tplayer.Definition
sendResponse(
ObjectCreateMessage(
tdefintion.ObjectId,
tplayer.GUID,
ObjectCreateMessageParent(parent_guid, 0),
tdefintion.Packet.ConstructorData(tplayer).get
)
)
case None => ;
}
case _ => ;
}
})
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.UpdateAmsSpawnPoint(continent))
self ! SetCurrentAvatar(player)
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
if(deadState == DeadState.Alive) {
if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
sendResponse(
CharacterKnowledgeMessage(
41577140L,
CharacterKnowledgeInfo(
"Degrado",
Set(
CertificationType.StandardAssault,
CertificationType.AgileExoSuit,
CertificationType.StandardExoSuit
),
37,
5,
PlanetSideGUID(7)
)
)
)
sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 4, 41577140L, "Degrado"))
}
player.Position = pos
player.Velocity = vel
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
player.FacingYawUpper = yaw_upper
player.Crouching = is_crouching
player.Jumping = is_jumping
if(vel.isDefined && usingMedicalTerminal.isDefined) {
continent.GUID(usingMedicalTerminal) match {
case Some(term : Terminal with ProximityUnit) =>
StopUsingProximityUnit(term)
case _ => ;
}
}
accessedContainer match {
case Some(veh : Vehicle) =>
if(vel.isDefined || Vector3.DistanceSquared(player.Position, veh.Position) > 100) {
val guid = player.GUID
sendResponse(UnuseItemMessage(guid, veh.GUID))
sendResponse(UnuseItemMessage(guid, guid))
veh.AccessingTrunk = None
UnAccessContents(veh)
accessedContainer = None
}
case Some(container) => //just in case
if(vel.isDefined) {
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))
accessedContainer = None
}
case None => ;
}
val wepInHand : Boolean = player.Slot(player.DrawnSlot).Equipment match {
case Some(item) => item.Definition == GlobalDefinitions.bolt_driver
case None => false
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand))
//squadService ! SquadServiceMessage(tplayer, continent, SquadAction.Update(tplayer.CharId, tplayer.Health, tplayer.MaxHealth, tplayer.Armor, tplayer.MaxArmor, pos, zone.Number))
}
case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
//the majority of the following check retrieves information to determine if we are in control of the child
FindContainedWeapon match {
case (Some(_), Some(tool)) =>
if(tool.GUID == object_guid) {
//TODO set tool orientation?
player.Orientation = Vector3(0f, pitch, yaw)
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw))
}
else {
log.warn(s"ChildObjectState: ${player.Name} is using a different controllable agent than #${object_guid.guid}")
}
case (Some(obj), None) =>
log.warn(s"ChildObjectState: ${player.Name} can not find any controllable agent, let alone #${object_guid.guid}")
case (None, _) => ;
//TODO status condition of "playing getting out of vehicle to allow for late packets without warning
//log.warn(s"ChildObjectState: player $player not related to anything with a controllable agent")
}
case msg @ VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA) =>
if(deadState == DeadState.Alive) {
GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
val seat = obj.Seats(0)
//we're driving the vehicle
player.Position = pos //convenient
if(seat.ControlledWeapon.isEmpty) {
player.Orientation = Vector3.z(ang.z) //convenient
}
obj.Position = pos
obj.Orientation = ang
if(obj.MountedIn.isEmpty) {
obj.Velocity = vel
if(obj.Definition.CanFly) {
obj.Flying = flight.nonEmpty //usually Some(7)
}
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
}
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 should not be dispatching this kind of packet from vehicle#$vehicle_guid when not the driver ($index)")
case _ => ;
}
}
//log.info(s"VehicleState: $msg")
case msg @ VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) =>
//log.info(s"VehicleSubState: $vehicle_guid, $player_guid, $vehicle_pos, $vehicle_ang, $vel, $unk1, $unk2")
case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vector, unk1, unk2, unk3, unk4, time_alive) =>
//log.info("ProjectileState: " + msg)
case msg @ ReleaseAvatarRequestMessage() =>
log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released")
reviveTimer.cancel
continent.Population ! Zone.Population.Release(avatar)
GoToDeploymentMap()
player.VehicleSeated match {
case None =>
FriskCorpse(player)
if(!WellLootedCorpse(player)) {
TurnPlayerIntoCorpse(player)
continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent))
}
else {
//no items in inventory; leave no corpse
val player_guid = player.GUID
sendResponse(ObjectDeleteMessage(player_guid, 0))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0))
taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID)
}
case Some(_) =>
val player_guid = player.GUID
sendResponse(ObjectDeleteMessage(player_guid, 0))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0))
self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, BailType.Normal, true)) //let vehicle try to clean up its fields
import scala.concurrent.ExecutionContext.Implicits.global
context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player))
}
case msg @ SpawnRequestMessage(u1, spawn_type, u3, u4, zone_number) =>
log.info(s"SpawnRequestMessage: $msg")
if(deadState != DeadState.RespawnTime) {
deadState = DeadState.RespawnTime
cluster ! Zone.Lattice.RequestSpawnPoint(zone_number.toInt, player, spawn_type.id.toInt)
}
else {
log.warn("SpawnRequestMessage: request consumed; already respawning ...")
}
case msg @ SetChatFilterMessage(send_channel, origin, whitelist) =>
//log.info("SetChatFilters: " + msg)
case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
var makeReply : Boolean = true
var echoContents : String = contents
val trimContents = contents.trim
//TODO messy on/off strings may work
if(messagetype == ChatMessageType.CMT_FLY) {
if(trimContents.equals("on")) {
flying = true
}
else if(trimContents.equals("off")) {
flying = false
}
}
else if(messagetype == ChatMessageType.CMT_SPEED) {
speed = {
try {
trimContents.toFloat
}
catch {
case _ : Exception =>
echoContents = "1.000"
1f
}
}
}
else if(messagetype == ChatMessageType.CMT_TOGGLESPECTATORMODE) {
if(trimContents.equals("on")) {
spectator = true
}
else if(contents.trim.equals("off")) {
spectator = false
}
}
CSRZone.read(traveler, msg) match {
case (true, zone, pos) if player.isAlive =>
deadState = DeadState.Release //cancel movement updates
PlayerActionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
case Some(0) =>
vehicle.Position = pos
LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0)
case _ => //not seated as the driver, in which case we can't move
deadState = DeadState.Alive
}
case None =>
player.Position = pos
//avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID))
LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0)
case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move
deadState = DeadState.Alive
}
case (_, _, _) => ;
}
CSRWarp.read(traveler, msg) match {
case (true, pos) if player.isAlive =>
deadState = DeadState.Release //cancel movement updates
PlayerActionsToCancel()
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) if vehicle.MountedIn.isEmpty =>
vehicle.PassengerInSeat(player) match {
case Some(0) =>
vehicle.Position = pos
LoadZonePhysicalSpawnPoint(continent.Id, pos, Vector3.z(vehicle.Orientation.z), 0)
case _ => //not seated as the driver, in which case we can't move
deadState = DeadState.Alive
}
case None =>
player.Position = pos
sendResponse(PlayerStateShiftMessage(ShiftState(0, pos, player.Orientation.z, None)))
deadState = DeadState.Alive //must be set here
case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move
deadState = DeadState.Alive
}
case (_, _) => ;
}
// TODO: Prevents log spam, but should be handled correctly
if(messagetype != ChatMessageType.CMT_TOGGLE_GM) {
log.info("Chat: " + msg)
}
else {
makeReply = false
}
if(messagetype == ChatMessageType.CMT_SUICIDE) {
if(player.isAlive && deadState != DeadState.Release) {
Suicide(player)
}
}
if(messagetype == ChatMessageType.CMT_DESTROY) {
val guid = contents.toInt
continent.Map.TerminalToSpawnPad.get(guid) match {
case Some(padGUID) =>
continent.GUID(padGUID).get.asInstanceOf[VehicleSpawnPad].Actor ! VehicleSpawnControl.ProcessControl.Flush
case None =>
self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid)))
}
}
if(messagetype == ChatMessageType.CMT_VOICE) {
sendResponse(ChatMsg(ChatMessageType.CMT_VOICE, false, player.Name, contents, None))
}
// TODO: handle this appropriately
if(messagetype == ChatMessageType.CMT_QUIT) {
sendResponse(DropCryptoSession())
sendResponse(DropSession(sessionId, "user quit"))
}
//dev hack; consider bang-commands to complement slash-commands in future
if(trimContents.equals("!loc")) {
echoContents = s"zone=${continent.Id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
log.info(echoContents)
}
else if(trimContents.equals("!ams")) {
makeReply = false
if(deadState == DeadState.Release) { //player is on deployment screen (either dead or deconstructed)
cluster ! Zone.Lattice.RequestSpawnPoint(continent.Number, player, 2)
}
}
// TODO: Depending on messagetype, may need to prepend sender's name to contents with proper spacing
// TODO: Just replays the packet straight back to sender; actually needs to be routed to recipients!
if(makeReply) {
sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, echoContents, note_contents))
}
case msg @ VoiceHostRequest(unk, PlanetSideGUID(player_guid), data) =>
log.info("Player "+player_guid+" requested in-game voice chat.")
sendResponse(VoiceHostKill())
case msg @ VoiceHostInfo(player_guid, data) =>
sendResponse(VoiceHostKill())
case msg @ ChangeAmmoMessage(item_guid, unk1) =>
log.info("ChangeAmmo: " + msg)
FindContainedEquipment match {
case(Some(_), Some(obj : ConstructionItem)) =>
PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex)
case (Some(obj), Some(tool : Tool)) =>
PerformToolAmmoChange(tool, obj)
case (_, Some(obj)) =>
log.error(s"ChangeAmmo: the object ${obj.Definition.Name} is not a valid type")
case (_, None) =>
log.error(s"ChangeAmmo: can not find $item_guid")
}
case msg @ ChangeFireModeMessage(item_guid, fire_mode) =>
log.info("ChangeFireMode: " + msg)
FindEquipment match {
case Some(obj : PlanetSideGameObject with FireModeSwitch[_]) =>
val originalModeIndex = obj.FireModeIndex
obj match {
case cItem : ConstructionItem =>
NextConstructionItemFireMode(cItem, originalModeIndex)
case _ =>
obj.NextFireMode
}
val modeIndex = obj.FireModeIndex
val tool_guid = obj.GUID
if(originalModeIndex == modeIndex) {
obj.FireModeIndex = originalModeIndex
sendResponse(ChangeFireModeMessage(tool_guid, originalModeIndex)) //reinforcement
}
else {
log.info(s"ChangeFireMode: changing $tool_guid to fire mode $modeIndex")
sendResponse(ChangeFireModeMessage(tool_guid, modeIndex))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireMode(player.GUID, tool_guid, modeIndex))
}
case Some(_) =>
log.error(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes")
case None =>
log.error(s"ChangeFireMode: can not find $item_guid")
}
case msg @ ChangeFireStateMessage_Start(item_guid) =>
log.info("ChangeFireState_Start: " + msg)
if(shooting.isEmpty) {
FindEquipment match {
case Some(tool : Tool) =>
if(tool.Magazine > 0 || prefire.contains(item_guid)) {
prefire = None
shooting = Some(item_guid)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(player.GUID, item_guid))
}
else {
log.warn(s"ChangeFireState_Start: ${tool.Definition.Name} magazine is empty before trying to shoot bullet")
EmptyMagazine(item_guid, tool)
}
case Some(_) => //permissible, for now
prefire = None
shooting = Some(item_guid)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(player.GUID, item_guid))
case None =>
log.error(s"ChangeFireState_Start: can not find $item_guid")
}
}
case msg @ ChangeFireStateMessage_Stop(item_guid) =>
log.info("ChangeFireState_Stop: " + msg)
prefire = None
val weapon : Option[Equipment] = if(shooting.contains(item_guid)) {
shooting = None
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, item_guid))
FindEquipment
}
else {
//some weapons, e.g., the decimator, do not send a ChangeFireState_Start on the last shot
FindEquipment match {
case Some(tool) =>
if(tool.Definition == GlobalDefinitions.phoenix) {
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(player.GUID, item_guid))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, item_guid))
}
Some(tool)
case _ =>
log.warn(s"ChangeFireState_Stop: received an unexpected message about $item_guid")
None
}
}
weapon match {
case Some(tool : Tool) =>
if(tool.Magazine == 0) {
FireCycleCleanup(tool)
}
case Some(trigger : BoomerTrigger) =>
val playerGUID = player.GUID
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(playerGUID, item_guid))
continent.GUID(trigger.Companion) match {
case Some(boomer : BoomerDeployable) =>
val boomerGUID = boomer.GUID
boomer.Exploded = true
sendResponse(TriggerEffectMessage(boomerGUID, "detonate_boomer"))
sendResponse(PlanetsideAttributeMessage(boomerGUID, 29, 1))
sendResponse(ObjectDeleteMessage(boomerGUID, 0))
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffect(playerGUID, "detonate_boomer", boomerGUID))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(boomerGUID, 29, 1))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(playerGUID, boomerGUID))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, continent, Some(0 seconds)))
case Some(_) | None => ;
}
FindEquipmentToDelete(item_guid, trigger)
trigger.Companion = None
case _ => ;
}
progressBarUpdate.cancel //TODO independent action?
case msg @ EmoteMsg(avatar_guid, emote) =>
log.info("Emote: " + msg)
sendResponse(EmoteMsg(avatar_guid, emote))
case msg @ DropItemMessage(item_guid) =>
log.info(s"DropItem: $msg")
continent.GUID(item_guid) match {
case Some(item : Equipment) =>
player.FreeHand.Equipment match {
case Some(_) =>
if(item.GUID == item_guid) {
continent.Ground ! Zone.Ground.DropItem(item, player.Position, player.Orientation)
}
case None =>
log.warn(s"DropItem: $player wanted to drop a $item, but it wasn't at hand")
}
case Some(obj) => //TODO LLU
log.warn(s"DropItem: $player wanted to drop a $obj, but that isn't possible")
case None =>
log.warn(s"DropItem: $player wanted to drop an item ($item_guid), but it was nowhere to be found")
}
case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) =>
log.info(s"PickupItem: $msg")
continent.GUID(item_guid) match {
case Some(item : Equipment) =>
player.Fit(item) match {
case Some(_) =>
continent.Ground ! Zone.Ground.PickupItem(item_guid)
case None => //skip
sendResponse(ActionResultMessage.Fail(16)) //error code?
}
case _ =>
log.warn(s"PickupItem: $player requested an item that doesn't exist in this zone; assume client-side garbage data")
sendResponse(ObjectDeleteMessage(item_guid, 0))
}
case msg @ ReloadMessage(item_guid, ammo_clip, unk1) =>
log.info("Reload: " + msg)
FindContainedWeapon match {
case (Some(obj), Some(tool : 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 =>
log.warn(s"ReloadMessage: no ammunition could be found for $item_guid")
case x :: xs =>
val (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match {
case (veh : Vehicle) =>
(DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh))
case _ =>
(DeleteEquipment(obj), ModifyAmmunition(obj))
}
xs.foreach(item => {
deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox])
})
val box = x.obj.asInstanceOf[AmmoBox]
val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) }
val sumReloadValue : Int = box.Capacity + tailReloadValue
val actualReloadValue = (if(sumReloadValue <= reloadValue) {
deleteFunc(x.start, box)
sumReloadValue
}
else {
modifyFunc(box, reloadValue - tailReloadValue)
reloadValue
}) + currentMagazine
log.info(s"ReloadMessage: success, $tool <- $actualReloadValue ${tool.AmmoType}")
tool.Magazine = actualReloadValue
sendResponse(ReloadMessage(item_guid, actualReloadValue, unk1))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Reload(player.GUID, item_guid))
}
}
else {
log.warn(s"ReloadMessage: item $item_guid can not reload (full=$magazineSize, want=$reloadValue)")
}
case (_, Some(_)) =>
log.error(s"ReloadMessage: the object that was found for $item_guid was not a Tool")
case (_, None) =>
log.error(s"ReloadMessage: can not find $item_guid")
}
case msg @ ObjectHeldMessage(avatar_guid, held_holsters, unk1) =>
log.info(s"ObjectHeld: $msg")
val before = player.DrawnSlot
if(before != held_holsters) {
if(player.ExoSuit == ExoSuitType.MAX && held_holsters != 0) {
log.info(s"ObjectHeld: $player is denied changing hands to $held_holsters as a MAX")
player.DrawnSlot = 0
sendResponse(ObjectHeldMessage(avatar_guid, 0, true))
}
else if((player.DrawnSlot = held_holsters) != before) {
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot))
// Ignore non-equipment holsters
//todo: check current suit holster slots?
if(held_holsters >= 0 && held_holsters < 5) {
player.Holsters()(held_holsters).Equipment match {
case Some(unholsteredItem : Equipment) =>
if(unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) {
// Player has ulholstered a REK - we need to set an atttribute on the REK itself to change the beam/icon colour to the correct one for the player's hack level
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, GetPlayerHackLevel()))
}
case None => ;
}
}
// Stop using proximity terminals if player unholsters a weapon (which should re-trigger the proximity effect and re-holster the weapon)
if(player.VisibleSlots.contains(held_holsters)) {
continent.GUID(usingMedicalTerminal) match {
case Some(term : Terminal with ProximityUnit) =>
StopUsingProximityUnit(term)
case _ => ;
}
}
}
}
case msg @ AvatarJumpMessage(state) =>
//log.info("AvatarJump: " + msg)
case msg @ ZipLineMessage(player_guid,origin_side,action,id,pos) =>
log.info("ZipLineMessage: " + msg)
if (!origin_side && action == 0) {
//doing this lets you use the zip line in one direction, cant come back
sendResponse(ZipLineMessage(player_guid, origin_side, action, id, pos))
}
else if (!origin_side && action == 1) {
//disembark from zipline at destination !
sendResponse(ZipLineMessage(player_guid, origin_side, action, 0, pos))
}
else if (!origin_side && action == 2) {
//get off by force
sendResponse(ZipLineMessage(player_guid, origin_side, action, 0, pos))
}
else if (origin_side && action == 0) {
// for teleporters & the other zipline direction
}
case msg @ RequestDestroyMessage(object_guid) =>
// TODO: Make sure this is the correct response for all cases
continent.GUID(object_guid) match {
case Some(vehicle : Vehicle) =>
if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID))
|| (player.Faction == vehicle.Faction
&& ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) {
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds)))
log.info(s"RequestDestroy: vehicle $vehicle")
}
else {
log.info(s"RequestDestroy: must own vehicle in order to deconstruct it")
}
case Some(obj : BoomerTrigger) =>
if(FindEquipmentToDelete(object_guid, obj)) {
continent.GUID(obj.Companion) match {
case Some(boomer : BoomerDeployable) =>
boomer.Trigger = None
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(boomer, continent, Some(0 seconds)))
//continent.Deployables ! Zone.Deployable.Dismiss(boomer)
case Some(thing) =>
log.info(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing")
case None => ;
}
}
case Some(obj : Equipment) =>
FindEquipmentToDelete(object_guid, obj)
case Some(_ : LocalProjectile) =>
FindProjectileEntry(object_guid) match {
case Some(projectile) =>
if(projectile.isResolved) {
log.warn(s"RequestDestroy: tried to clean up projectile ${object_guid.guid} but it was already resolved")
}
else {
projectile.Miss()
}
case None =>
log.warn(s"RequestDestroy: projectile ${object_guid.guid} has never been fired")
}
case Some(obj : BoomerDeployable) =>
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
obj.Trigger match {
case Some(trigger) =>
obj.Trigger = None
val guid = trigger.GUID
Zone.EquipmentIs.Where(trigger, guid, continent) match {
case Some(Zone.EquipmentIs.InContainer(container, index)) =>
container.Slot(index).Equipment = None
case Some(Zone.EquipmentIs.OnGround()) =>
continent.Ground ! Zone.Ground.RemoveItem(guid)
case Some(Zone.EquipmentIs.Orphaned()) =>
log.warn(s"RequestDestroy: boomer_trigger@$guid has been found but it seems to be orphaned")
case _ => ;
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), guid))
GUIDTask.UnregisterObjectTask(trigger)(continent.GUID)
case None => ;
}
case Some(obj : TelepadDeployable) =>
localService ! LocalServiceMessage.Telepads(SupportActor.ClearSpecific(List(obj), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
case Some(obj : PlanetSideGameObject with Deployable) =>
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
case Some(thing) =>
log.warn(s"RequestDestroy: not allowed to delete object $thing")
case None =>
log.warn(s"RequestDestroy: object ${object_guid.guid} not found")
}
case msg @ ObjectDeleteMessage(object_guid, unk1) =>
sendResponse(ObjectDeleteMessage(object_guid, 0))
log.info("ObjectDelete: " + msg)
case msg @ MoveItemMessage(item_guid, source_guid, destination_guid, dest, _) =>
log.info(s"MoveItem: $msg")
(continent.GUID(source_guid), continent.GUID(destination_guid), continent.GUID(item_guid)) match {
case (Some(source : Container), Some(destination : Container), Some(item : Equipment)) =>
source.Find(item_guid) match {
case Some(index) =>
val indexSlot = source.Slot(index)
val tile = item.Definition.Tile
val destinationCollisionTest = destination.Collisions(dest, tile.Width, tile.Height)
val destItemEntry = destinationCollisionTest match {
case Success(entry :: Nil) =>
Some(entry)
case _ =>
None
}
if( {
destinationCollisionTest match {
case Success(Nil) | Success(_ :: Nil) =>
true //no item or one item to swap
case _ =>
false //abort when too many items at destination or other failure case
}
} && indexSlot.Equipment.contains(item)) {
if(PermitEquipmentStow(item, destination)) {
PerformMoveItem(item, source, index, destination, dest, destItemEntry)
}
else {
log.error(s"MoveItem: $item disallowed storage in $destination")
}
}
else if(!indexSlot.Equipment.contains(item)) {
log.error(s"MoveItem: wanted to move $item_guid, but found unexpected ${indexSlot.Equipment.get} at source location")
}
else {
destinationCollisionTest match {
case Success(_) =>
log.error(s"MoveItem: wanted to move $item_guid, but multiple unexpected items at destination blocked progress")
case scala.util.Failure(err) =>
log.error(s"MoveItem: wanted to move $item_guid, but $err")
}
}
case _ =>
log.error(s"MoveItem: wanted to move $item_guid, but could not find it")
}
case (None, _, _) =>
log.error(s"MoveItem: wanted to move $item_guid from $source_guid, but could not find source object")
case (_, None, _) =>
log.error(s"MoveItem: wanted to move $item_guid to $destination_guid, but could not find destination object")
case (_, _, None) =>
log.error(s"MoveItem: wanted to move $item_guid, but could not find it")
case _ =>
log.error(s"MoveItem: wanted to move $item_guid from $source_guid to $destination_guid, but multiple problems were encountered")
}
case msg @ LootItemMessage(item_guid, target_guid) =>
log.info(s"LootItem: $msg")
(continent.GUID(item_guid), continent.GUID(target_guid)) match {
case (Some(item : Equipment), Some(target : Container)) =>
//figure out the source
(
{
val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] = FindInLocalContainer(item_guid)
findFunc(player.Locker)
.orElse(findFunc(player))
.orElse(accessedContainer match {
case Some(parent) =>
findFunc(parent)
case None =>
None
}
)
}, target.Fit(item)) match {
case (Some((source, Some(index))), Some(dest)) =>
if(PermitEquipmentStow(item, target)) {
PerformMoveItem(item, source, index, target, dest, None)
}
else {
log.error(s"LootItem: $item disallowed storage in $target")
}
case (None, _) =>
log.error(s"LootItem: can not find where $item is put currently")
case (_, None) =>
log.error(s"LootItem: can not find somwhere to put $item in $target")
case _ =>
log.error(s"LootItem: wanted to move $item_guid to $target_guid, but multiple problems were encountered")
}
case (Some(obj), _) =>
log.warn(s"LootItem: item $obj is (probably) not lootable")
case (None, _) =>
log.warn(s"LootItem: can not find $item_guid")
case (_, None) =>
log.warn(s"LootItem: can not find where to put $item_guid")
}
case msg @ AvatarImplantMessage(_, _, _, _) => //(player_guid, unk1, unk2, implant) =>
log.info("AvatarImplantMessage: " + msg)
case msg @ UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) =>
log.info("UseItem: " + msg)
// TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok)
// TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg)
continent.GUID(object_guid) match {
case Some(door : Door) =>
if(player.Faction == door.Faction || (continent.Map.DoorToLock.get(object_guid.guid) match {
case Some(lock_guid) =>
val lock = continent.GUID(lock_guid).get.asInstanceOf[IFFLock]
val playerIsOnInside = Vector3.ScalarProjection(lock.Outwards, player.Position - door.Position) < 0f
// If an IFF lock exists and the IFF lock faction doesn't match the current player and one of the following conditions are met open the door:
// A base is neutral
// A base is hacked
// The lock is hacked
// The player is on the inside of the door, determined by the lock orientation
lock.HackedBy.isDefined || lock.Owner.asInstanceOf[Building].CaptureConsoleIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL || playerIsOnInside
case None => !door.isOpen // If there's no linked IFF lock just open the door if it's closed.
})) {
door.Actor ! Door.Use(player, msg)
}
else if(door.isOpen) {
//the door is open globally ... except on our screen
sendResponse(GenericObjectStateMsg(object_guid, 16))
}
case Some(resourceSilo : ResourceSilo) =>
log.info(s"UseItem: Vehicle $avatar_guid is refilling resource silo $object_guid")
val vehicle = continent.GUID(avatar_guid).get.asInstanceOf[Vehicle]
if(resourceSilo.Faction == PlanetSideEmpire.NEUTRAL || player.Faction == resourceSilo.Faction) {
if(vehicle.Seat(0).get.Occupant.contains(player)) {
log.trace("UseItem: Player matches vehicle driver. Calling ResourceSilo.Use")
resourceSilo.Actor ! ResourceSilo.Use(player, msg)
}
} else {
log.warn(s"Player ${player.GUID} - ${player.Faction} tried to refill silo ${resourceSilo.GUID} - ${resourceSilo.Faction} belonging to another empire")
}
case Some(panel : IFFLock) =>
if((panel.Faction != player.Faction && panel.HackedBy.isEmpty) || (panel.Faction == player.Faction && panel.HackedBy.isDefined)) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool : SimpleItem) =>
if(tool.Definition == GlobalDefinitions.remote_electronics_kit) {
val hackSpeed = GetPlayerHackSpeed(panel)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
if(panel.Faction != player.Faction) {
// Enemy faction is hacking this IFF lock
self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishHacking(panel, 1114636288L))
log.info("Hacking an IFF lock")
} else {
// IFF Lock is being resecured by it's owner faction
self ! WorldSessionActor.HackingProgress(progressType = 1, player, panel, tool.GUID, hackSpeed, FinishResecuringIFFLock(panel))
log.info("Resecuring an IFF lock")
}
}
}
case _ => ;
}
} else {
log.warn("IFF lock is being hacked, but don't know how to handle this state")
log.warn(s"Lock - HackedBy.isDefined: ${panel.HackedBy.isDefined} Faction: ${panel.Faction} HackedBy.isEmpty: ${panel.HackedBy.isEmpty}")
log.warn(s"Hacking player - Faction: ${player.Faction}")
}
case Some(obj : Player) =>
if(obj.isBackpack) {
log.info(s"UseItem: $player looting the corpse of $obj")
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
accessedContainer = Some(obj)
}
else if(!unk3) { //potential kit use
continent.GUID(item_used_guid) match {
case Some(kit : Kit) =>
player.Find(kit) match {
case Some(index) =>
if(kit.Definition == GlobalDefinitions.medkit) {
if(player.Health == player.MaxHealth) {
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "@HealComplete", None))
}
else if(System.currentTimeMillis - whenUsedLastKit < 5000) {
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${5 - (System.currentTimeMillis - whenUsedLastKit) / 1000}~", None))
}
else {
player.Find(kit) match {
case Some(index) =>
whenUsedLastKit = System.currentTimeMillis
player.Slot(index).Equipment = None //remove from slot immediately; must exist on client for next packet
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
sendResponse(ObjectDeleteMessage(kit.GUID, 0))
taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID)
player.History(HealFromKit(PlayerSource(player), 25, kit.Definition))
player.Health = player.Health + 25
sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health))
case None =>
log.error(s"UseItem: anticipated a $kit, but can't find it")
}
}
}
else {
log.warn(s"UseItem: $kit behavior not supported")
}
case None =>
log.error(s"UseItem: anticipated a $kit, but can't find it")
}
case Some(item) =>
log.warn(s"UseItem: looking for Kit to use, but found $item instead")
case None =>
log.warn(s"UseItem: anticipated a Kit $item_used_guid, but can't find it")
}
}
case Some(locker : Locker) =>
if(locker.Faction != player.Faction && locker.HackedBy.isEmpty) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
val hackSpeed = GetPlayerHackSpeed(locker)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, locker, tool.GUID, hackSpeed, FinishHacking(locker, 3212836864L))
log.info("Hacking a locker")
}
}
case _ => ;
}
} else if(player.Faction == locker.Faction || !locker.HackedBy.isEmpty) {
log.info(s"UseItem: $player accessing a locker")
val container = player.Locker
accessedContainer = Some(container)
sendResponse(UseItemMessage(avatar_guid, item_used_guid, container.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, 456))
}
else {
log.info(s"UseItem: not $player's locker")
}
case Some(implant_terminal : ImplantTerminalMech) =>
if(implant_terminal.Faction != player.Faction && implant_terminal.HackedBy.isEmpty) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
val hackSpeed = GetPlayerHackSpeed(implant_terminal)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, implant_terminal, tool.GUID, hackSpeed, FinishHacking(implant_terminal, 3212836864L))
log.info("Hacking an implant terminal")
}
}
case _ => ;
}
}
case Some(captureTerminal : CaptureTerminal) =>
val hackedByCurrentFaction = (captureTerminal.Faction != player.Faction && !captureTerminal.HackedBy.isEmpty && captureTerminal.HackedBy.get.hackerFaction == player.Faction)
val ownedByPlayerFactionAndHackedByEnemyFaction = (captureTerminal.Faction == player.Faction && !captureTerminal.HackedBy.isEmpty)
if(!hackedByCurrentFaction || ownedByPlayerFactionAndHackedByEnemyFaction) {
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
val hackSpeed = GetPlayerHackSpeed(captureTerminal)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, captureTerminal, tool.GUID, hackSpeed, FinishHacking(captureTerminal, 3212836864L))
log.info("Hacking a capture terminal")
}
}
case _ => ;
}
}
case Some(obj : FacilityTurret) =>
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool : Tool) =>
if(tool.Definition == GlobalDefinitions.nano_dispenser && tool.Magazine > 0) {
val ammo = tool.AmmoType
if(ammo == Ammo.upgrade_canister && obj.Seats.values.count(_.isOccupied) == 0) {
progressBarValue = Some(-1.25f)
self ! WorldSessionActor.HackingProgress(
progressType = 2,
player,
obj,
tool.GUID,
delta = 1.25f,
FinishUpgradingMannedTurret(obj, tool, TurretUpgrade(unk2.toInt))
)
}
else if(ammo == Ammo.armor_canister && obj.Health < obj.MaxHealth) {
//repair turret
}
}
else if(tool.Definition == GlobalDefinitions.trek) {
//infect turret with virus
}
case _ => ;
}
case Some(obj : Vehicle) =>
val equipment = player.Slot(player.DrawnSlot).Equipment
if(player.Faction == obj.Faction) {
if(equipment match {
case Some(tool : Tool) =>
tool.Definition match {
case GlobalDefinitions.nano_dispenser => false
case _ => true
}
case _ => true
}) {
//access to trunk
if(obj.AccessingTrunk.isEmpty &&
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner.contains(player.GUID))) {
obj.AccessingTrunk = player.GUID
accessedContainer = Some(obj)
AccessContents(obj)
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
}
else {
log.info(s"UseItem: $obj's trunk is not currently accessible for $player")
}
}
else if(equipment.isDefined) {
equipment.get.Definition match {
case GlobalDefinitions.nano_dispenser =>
//TODO repairing behavior
case _ => ;
}
}
}
//enemy player interactions
else if(equipment.isDefined) {
equipment.get.Definition match {
case GlobalDefinitions.remote_electronics_kit =>
val hackSpeed = GetPlayerHackSpeed(obj)
if(hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, obj, equipment.get.GUID, hackSpeed, FinishHackingVehicle(obj, 3212836864L))
log.info("Hacking a vehicle")
}
case _ => ;
}
}
case Some(terminal : Terminal) =>
val tdef = terminal.Definition
// If the base this terminal belongs to has been hacked the owning faction needs to be able to hack it to gain access
val ownerIsHacked = terminal.Owner match {
case b: Building => b.CaptureConsoleIsHacked
case _ => false
}
var playerIsHacking = false
player.Slot(player.DrawnSlot).Equipment match {
case Some(tool: SimpleItem) =>
if (tool.Definition == GlobalDefinitions.remote_electronics_kit) {
if (!terminal.HackedBy.isEmpty) {
log.warn("Player tried to hack a terminal that is already hacked")
log.warn(s"Player faction ${player.Faction} terminal faction: ${terminal.Faction} terminal hacked: ${terminal.HackedBy.isDefined} owner hacked: ${ownerIsHacked}")
}
else if (terminal.Faction != player.Faction || ownerIsHacked) {
val hackSpeed = GetPlayerHackSpeed(terminal)
if (hackSpeed > 0) {
progressBarValue = Some(-hackSpeed)
self ! WorldSessionActor.HackingProgress(progressType = 1, player, terminal, tool.GUID, hackSpeed, FinishHacking(terminal, 3212836864L))
playerIsHacking = true
log.info("Hacking a terminal")
}
}
}
case _ => ;
}
if(!playerIsHacking) {
if (terminal.Faction == player.Faction) {
if (tdef.isInstanceOf[MatrixTerminalDefinition]) {
//TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks)
sendResponse(BindPlayerMessage(BindStatus.Bind, "", true, 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) =>
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
sendResponse(UseItemMessage(avatar_guid, item_used_guid, vehicle.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, vehicle.Definition.ObjectId))
case None =>
log.error("UseItem: expected seated vehicle, but found none")
}
}
else if (tdef == GlobalDefinitions.teleportpad_terminal) {
//explicit request
terminal.Actor ! Terminal.Request(
player,
ItemTransactionMessage(object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0))
)
}
else if (!ownerIsHacked || (ownerIsHacked && terminal.HackedBy.isDefined)) {
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
}
else {
log.warn("Tried to use a terminal, but can't handle this case")
log.warn(s"Terminal - isHacked ${terminal.HackedBy.isDefined} ownerIsHacked ${ownerIsHacked}")
}
}
else if (terminal.HackedBy.isDefined) {
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
} else {
log.warn("Tried to use a terminal that doesn't belong to this faction and isn't hacked")
log.warn(s"Player faction ${player.Faction} terminal faction: ${terminal.Faction} terminal hacked: ${terminal.HackedBy.isDefined} owner hacked: ${ownerIsHacked}")
}
}
case Some(obj : SpawnTube) =>
//deconstruction
PlayerActionsToCancel()
CancelAllProximityUnits()
continent.Population ! Zone.Population.Release(avatar)
GoToDeploymentMap()
case Some(obj : TelepadDeployable) =>
continent.GUID(obj.Router) match {
case Some(vehicle : Vehicle) =>
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util : Utility.InternalTelepad) =>
UseRouterTelepadSystem(router = vehicle, internalTelepad = util, remoteTelepad = obj, src = obj, dest = util)
case _ =>
log.error(s"telepad@${object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}@${obj.Router.get.guid}")
}
case Some(o) =>
log.error(s"telepad@${object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}@${obj.Router.get.guid}")
case None => ;
}
case Some(obj : Utility.InternalTelepad) =>
continent.GUID(obj.Telepad) match {
case Some(pad : TelepadDeployable) =>
UseRouterTelepadSystem(router = obj.Owner.asInstanceOf[Vehicle], internalTelepad = obj, remoteTelepad = pad, src = obj, dest = pad)
case Some(o) =>
log.error(s"internal telepad@${object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}")
case None => ;
}
case Some(obj) =>
log.warn(s"UseItem: don't know how to handle $obj; taking a shot in the dark")
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
case None =>
log.error(s"UseItem: can not find object $object_guid")
}
case msg @ ProximityTerminalUseMessage(player_guid, object_guid, _) =>
log.info(s"ProximityTerminalUse: $msg")
continent.GUID(object_guid) match {
case Some(obj : Terminal with ProximityUnit) =>
HandleProximityTerminalUse(obj)
case Some(obj) => ;
log.warn(s"ProximityTerminalUse: object does not have proximity effects - $obj")
case None =>
log.warn(s"ProximityTerminalUse: no object with guid $object_guid found")
}
case msg @ UnuseItemMessage(player_guid, object_guid) =>
log.info(s"UnuseItem: $msg")
//TODO check for existing accessedContainer value?
continent.GUID(object_guid) match {
case Some(obj : Vehicle) =>
if(obj.AccessingTrunk.contains(player.GUID)) {
obj.AccessingTrunk = None
UnAccessContents(obj)
}
case Some(obj : Player) =>
TryDisposeOfLootedCorpse(obj)
case _ =>;
}
accessedContainer = None
case msg @ DeployObjectMessage(guid, unk1, pos, orient, unk2) =>
log.info(s"DeployObject: $msg")
//the hand with the construction item is no longer drawn
//TODO consider player.Slot(player.LastDrawnSlot)
(player.Holsters.find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid) match {
case Some(slot) =>
slot.Equipment
case None =>
None
}) match {
case Some(obj : ConstructionItem) =>
val ammoType = obj.AmmoType match {
case DeployedItem.portable_manned_turret =>
GlobalDefinitions.PortableMannedTurret(player.Faction).Item //faction-specific turret
case turret =>
turret
}
log.info(s"DeployObject: Constructing a ${ammoType}")
val dObj : PlanetSideGameObject with Deployable = Deployables.Make(ammoType)()
dObj.Position = pos
dObj.Orientation = orient
dObj.Faction = player.Faction
dObj.AssignOwnership(player)
val tasking : TaskResolver.GiveTask = dObj match {
case turret : TurretDeployable =>
GUIDTask.RegisterDeployableTurret(turret)(continent.GUID)
case _ =>
GUIDTask.RegisterObjectTask(dObj)(continent.GUID)
}
taskResolver ! CallBackForTask(tasking, continent.Deployables, Zone.Deployable.Build(dObj, obj))
case Some(obj) =>
log.warn(s"DeployObject: $obj is something?")
case None =>
log.warn("DeployObject: nothing?")
}
case msg @ GenericObjectStateMsg(object_guid, unk1) =>
log.info("GenericObjectState: " + msg)
case msg @ GenericActionMessage(action) =>
log.info(s"GenericAction: $msg")
val (toolOpt, definition) = player.Slot(0).Equipment match {
case Some(tool : Tool) =>
(Some(tool), tool.Definition)
case _ =>
(None, GlobalDefinitions.bullet_9mm)
}
if(action == 15) { //max deployment
log.info(s"GenericObject: $player is anchored")
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
avatarService ! 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.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
}
}
else if(action == 16) { //max deployment
log.info(s"GenericObject: $player has released the anchors")
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
avatarService ! 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.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
}
}
else if(action == 37) { //Looking For Squad OFF
if(squadUI.nonEmpty) {
lfs = false
}
else if(avatar.LFS) {
avatar.LFS = false
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
log.info(s"GenericObject: ${player.Name} is no longer looking for a squad to join")
}
}
else if(action == 36) { //Looking For Squad ON
if(squadUI.nonEmpty) {
lfs = true
}
else if(!avatar.LFS) {
avatar.LFS = true
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
log.info(s"GenericObject: ${player.Name} has made himself available to join a squad")
}
}
case msg @ ItemTransactionMessage(terminal_guid, transaction_type, _, _, _, _) =>
log.info("ItemTransaction: " + msg)
continent.GUID(terminal_guid) match {
case Some(term : Terminal) =>
log.info(s"ItemTransaction: ${term.Definition.Name} found")
if(lastTerminalOrderFulfillment) {
lastTerminalOrderFulfillment = false
term.Actor ! Terminal.Request(player, msg)
}
case Some(obj : PlanetSideGameObject) =>
log.error(s"ItemTransaction: $obj is not a terminal")
case _ =>
log.error(s"ItemTransaction: $terminal_guid does not exist")
}
case msg @ FavoritesRequest(player_guid, list, action, line, label) =>
log.info(s"FavoritesRequest: $msg")
if(player.GUID == player_guid) {
val lineno = if(list == LoadoutType.Vehicle) { line + 10 } else { line }
val name = label.getOrElse(s"missing_loadout_${line+1}")
action match {
case FavoritesAction.Save =>
(if(list == LoadoutType.Infantry) {
Some(player)
}
else if(list == LoadoutType.Vehicle) {
player.VehicleSeated match {
case Some(vehicle_guid) =>
continent.GUID(vehicle_guid)
case None =>
None
}
}
else {
None
}) match {
case Some(owner : Player) => //InfantryLoadout
avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
import InfantryLoadout._
sendResponse(FavoritesMessage(list, player_guid, line, name, DetermineSubtypeB(player.ExoSuit, DetermineSubtype(player))))
case Some(owner : Vehicle) => //VehicleLoadout
avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
sendResponse(FavoritesMessage(list, player_guid, line, name))
case Some(_) | None =>
log.error("FavoritesRequest: unexpected owner for favorites")
}
case FavoritesAction.Delete =>
avatar.EquipmentLoadouts.DeleteLoadout(lineno)
sendResponse(FavoritesMessage(list, player_guid, line, ""))
case FavoritesAction.Unknown =>
log.warn("FavoritesRequest: unknown favorites action")
}
}
case msg @ WeaponDelayFireMessage(seq_time, weapon_guid) =>
log.info("WeaponDelayFire: " + msg)
case msg @ WeaponDryFireMessage(weapon_guid) =>
log.info("WeaponDryFireMessage: "+msg)
FindWeapon match {
case Some(tool : Tool) =>
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid))
case _ => ;
}
case msg @ WeaponFireMessage(seq_time, weapon_guid, projectile_guid, shot_origin, unk1, unk2, unk3, unk4, unk5, unk6, unk7) =>
log.info("WeaponFire: " + msg)
FindContainedWeapon match {
case (Some(obj), Some(tool : Tool)) =>
if(tool.Magazine <= 0) { //safety: enforce ammunition depletion
prefire = None
EmptyMagazine(weapon_guid, tool)
}
else { //shooting
prefire = shooting.orElse(Some(weapon_guid))
tool.Discharge
val projectileIndex = projectile_guid.guid - Projectile.BaseUID
val projectilePlace = projectiles(projectileIndex)
if(projectilePlace match {
case Some(projectile) => !projectile.isResolved
case None => false
}) {
log.trace(s"WeaponFireMessage: overwriting unresolved projectile ${projectile_guid.guid}")
}
val (angle, attribution, acceptableDistanceToOwner) = obj match {
case p : Player =>
(p.Orientation, tool.Definition.ObjectId, 10f) //TODO upper body facing
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(shot_origin, player.Position)
if(distanceToOwner <= acceptableDistanceToOwner) {
projectiles(projectileIndex) =
Some(Projectile(tool.Projectile, tool.Definition, tool.FireMode, player, attribution, shot_origin, angle))
}
else {
log.warn(s"WeaponFireMessage: $player's ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect")
}
}
case _ => ;
}
case msg @ WeaponLazeTargetPositionMessage(weapon, pos1, pos2) =>
log.info("Lazing position: " + pos2.toString)
case msg @ HitMessage(seq_time, projectile_guid, unk1, hit_info, unk2, unk3, unk4) =>
log.info(s"Hit: $msg")
(hit_info match {
case Some(hitInfo) =>
continent.GUID(hitInfo.hitobject_guid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
Some((target, hitInfo.shot_origin, hitInfo.hit_pos))
case _ =>
None
}
case None => ;
None
}) match {
case Some((target, shotOrigin, hitPos)) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Hit, target, hitPos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case None => ;
}
case msg @ SplashHitMessage(seq_time, projectile_guid, explosion_pos, direct_victim_uid, unk3, projectile_vel, unk4, targets) =>
log.info(s"Splash: $msg")
continent.GUID(direct_victim_uid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Splash, target, target.Position) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case _ => ;
}
targets.foreach(elem => {
continent.GUID(elem.uid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Splash, target, explosion_pos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case _ => ;
}
})
case msg @ LashMessage(seq_time, killer_guid, victim_guid, projectile_guid, pos, unk1) =>
log.info(s"Lash: $msg")
continent.GUID(victim_guid) match {
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
ResolveProjectileEntry(projectile_guid, ProjectileResolution.Lash, target, pos) match {
case Some(projectile) =>
HandleDealingDamage(target, projectile)
case None => ;
}
case _ => ;
}
case msg @ AvatarFirstTimeEventMessage(avatar_guid, object_guid, unk1, event_name) =>
log.info("AvatarFirstTimeEvent: " + msg)
case msg @ WarpgateRequest(continent_guid, building_guid, dest_building_guid, dest_continent_guid, unk1, unk2) =>
log.info(s"WarpgateRequest: $msg")
if(deadState != DeadState.RespawnTime) {
deadState = DeadState.RespawnTime
continent.Buildings.values.find(building => building.GUID == building_guid) match {
case Some(wg : WarpGate) if (wg.Active && (GetKnownVehicleAndSeat() match {
case (Some(vehicle), _) =>
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
case _ =>
true
})) =>
cluster ! Zone.Lattice.RequestSpecificSpawnPoint(dest_continent_guid.guid, player, dest_building_guid)
case _ =>
RequestSanctuaryZoneSpawn(player, continent.Number)
}
}
else {
log.warn("WarpgateRequest: request consumed; already respawning ...")
}
case msg @ MountVehicleMsg(player_guid, mountable_guid, entry_point) =>
log.info("MountVehicleMsg: "+msg)
continent.GUID(mountable_guid) match {
case Some(obj : Mountable) =>
obj.GetSeatFromMountPoint(entry_point) match {
case Some(seat_num) =>
obj.Actor ! Mountable.TryMount(player, seat_num)
case None =>
log.warn(s"MountVehicleMsg: attempted to board mountable $mountable_guid's seat $entry_point, but no seat exists there")
}
case None | Some(_) =>
log.warn(s"MountVehicleMsg: not a mountable thing")
}
case msg @ DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) =>
//TODO optimize this later
log.info(s"DismountVehicleMsg: $msg")
//common warning for this section
def dismountWarning(msg : String) : Unit = {
log.warn(s"$msg; some vehicle might not know that a player is no longer sitting in it")
}
if(player.GUID == player_guid) {
//normally disembarking from a seat
player.VehicleSeated match {
case Some(obj_guid) =>
interstellarFerry.orElse(continent.GUID(obj_guid)) match {
case Some(obj : Mountable) =>
obj.PassengerInSeat(player) match {
case Some(0) if controlled.nonEmpty =>
log.warn(s"DismountVehicleMsg: can not dismount from vehicle as driver while server has asserted control; please wait ...")
case Some(seat_num : Int) =>
obj.Actor ! Mountable.TryDismount(player, seat_num)
if(interstellarFerry.isDefined) {
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
//see above in VehicleResponse.TransferPassenger case
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 && seat_num == 0 && v.Flying =>
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, Some(0 seconds))) // Immediately deconstruct vehicle
case _ => ;
}
case None =>
dismountWarning(s"DismountVehicleMsg: can not find where player $player_guid is seated in mountable $obj_guid")
}
case _ =>
dismountWarning(s"DismountVehicleMsg: can not find mountable entity $obj_guid")
}
case None =>
dismountWarning(s"DismountVehicleMsg: player $player_guid not considered seated in a mountable entity")
}
}
else {
//kicking someone else out of a seat; need to own that seat/mountable
player.VehicleOwned match {
case Some(obj_guid) =>
(continent.GUID(obj_guid), continent.GUID(player_guid)) match {
case (Some(obj : Mountable), Some(tplayer : Player)) =>
obj.PassengerInSeat(tplayer) match {
case Some(seat_num : Int) =>
obj.Actor ! Mountable.TryDismount(tplayer, seat_num)
case None =>
dismountWarning(s"DismountVehicleMsg: can not find where other player $player_guid is seated in mountable $obj_guid")
}
case (None, _) => ;
log.warn(s"DismountVehicleMsg: $player can not find his vehicle")
case (_, None) => ;
log.warn(s"DismountVehicleMsg: player $player_guid could not be found to kick")
case _ =>
log.warn(s"DismountVehicleMsg: object is either not a Mountable or not a Player")
}
case None =>
log.warn(s"DismountVehicleMsg: $player does not own a vehicle")
}
}
case msg @ DeployRequestMessage(player_guid, vehicle_guid, deploy_state, unk2, unk3, pos) =>
log.info(s"DeployRequest: $msg")
if(player.VehicleOwned.contains(vehicle_guid) && player.VehicleOwned == player.VehicleSeated) {
continent.GUID(vehicle_guid) match {
case Some(obj : Vehicle) =>
obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
case _ =>
log.error(s"DeployRequest: can not find $vehicle_guid in scope")
player.VehicleOwned = None
}
}
else {
log.warn(s"DeployRequest: $player does not own the deploying $vehicle_guid object")
}
case msg @ AvatarGrenadeStateMessage(player_guid, state) =>
log.info("AvatarGrenadeStateMessage: " + msg)
case msg @ SquadDefinitionActionMessage(u1, u2, action) =>
log.info(s"SquadDefinitionAction: $msg")
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
log.info(s"$msg")
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
case msg @ SquadWaypointRequest(request, _, wtype, unk, info) =>
log.info(s"Waypoint Request: $msg")
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
log.info("Ouch! " + msg)
case msg @ BugReportMessage(version_major,version_minor,version_date,bug_type,repeatable,location,zone,pos,summary,desc) =>
log.info("BugReportMessage: " + msg)
case msg @ BindPlayerMessage(action, bindDesc, unk1, logging, unk2, unk3, unk4, pos) =>
log.info("BindPlayerMessage: " + msg)
case msg @ PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value) =>
log.info("PlanetsideAttributeMessage: "+msg)
continent.GUID(object_guid) match {
case Some(vehicle : Vehicle) =>
if(player.VehicleOwned.contains(vehicle.GUID)) {
if(9 < attribute_type && attribute_type < 14) {
vehicle.PermissionGroup(attribute_type, attribute_value) match {
case Some(allow) =>
val group = AccessPermissionGroup(attribute_type - 10)
log.info(s"Vehicle attributes: vehicle ${vehicle.GUID} access permission $group changed to $allow")
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SeatPermissions(player.GUID, vehicle.GUID, attribute_type, attribute_value))
//kick players who should not be seated in the vehicle due to permission changes
if(allow == VehicleLockState.Locked) { //TODO only important permission atm
vehicle.Definition.MountPoints.values.foreach(mountpoint_num => {
vehicle.Seat(mountpoint_num) match {
case Some(seat) =>
seat.Occupant match {
case Some(tplayer) =>
if(vehicle.SeatPermissionGroup(mountpoint_num).contains(group) && tplayer != player) { //can not kick self
seat.Occupant = None
tplayer.VehicleSeated = None
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid))
}
case None => ; // No player seated
}
case None => ; // Not a seat mounting point
}
vehicle.CargoHold(mountpoint_num) match {
case Some(cargo) =>
cargo.Occupant match {
case Some(vehicle) =>
if(vehicle.SeatPermissionGroup(mountpoint_num).contains(group)) {
//todo: this probably doesn't work for passengers within the cargo vehicle
// Instruct client to start bail dismount procedure
self ! DismountVehicleCargoMsg(player.GUID, vehicle.GUID, true, false, false)
}
case None => ; // No vehicle in cargo
}
case None => ; // Not a cargo mounting point
}
})
}
case None => ;
}
}
else {
log.warn(s"Vehicle attributes: unsupported change on vehicle $object_guid - $attribute_type")
}
}
else {
log.warn(s"Vehicle attributes: $player does not own vehicle ${vehicle.GUID} and can not change it")
}
case _ =>
log.warn(s"echo unknown attributes behavior")
sendResponse(PlanetsideAttributeMessage(object_guid, attribute_type, attribute_value))
}
case msg @ FacilityBenefitShieldChargeRequestMessage(guid) =>
player.VehicleSeated match {
case Some(vehicleGUID) =>
continent.GUID(vehicleGUID) match {
case Some(obj : Vehicle) =>
if(obj.Health > 0) { //vehicle will try to charge even if destroyed
obj.Actor ! Vehicle.ChargeShields(15)
}
case _ =>
log.warn(s"FacilityBenefitShieldChargeRequest: can not find vehicle ${vehicleGUID.guid} in zone ${continent.Id}")
}
case None =>
log.warn(s"FacilityBenefitShieldChargeRequest: player ${player.Name} is not seated in a vehicle")
}
case msg @ BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
log.info("Battleplan: "+msg)
case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) =>
log.info("CreateShortcutMessage: "+msg)
case msg @ FriendsRequest(action, friend) =>
log.info("FriendsRequest: "+msg)
case msg @ HitHint(source_guid, player_guid) =>
log.trace(s"HitHint: $msg") //HitHint is manually distributed for proper operation
case msg @ TargetingImplantRequest(list) =>
log.info("TargetingImplantRequest: "+msg)
case msg @ ActionCancelMessage(u1, u2, u3) =>
log.info("Cancelled: "+msg)
case default => log.error(s"Unhandled GamePacket $pkt")
}
/**
* Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
* Remove any encountered items and add them to an output `List`.
* @param iter the `Iterator` of `EquipmentSlot`s
* @param index a number that equals the "current" holster slot (`EquipmentSlot`)
* @param list a persistent `List` of `Equipment` in the holster slots
* @return a `List` of `Equipment` in the holster slots
*/
@tailrec private def clearHolsters(iter : Iterator[EquipmentSlot], index : Int = 0, list : List[InventoryItem] = Nil) : List[InventoryItem] = {
if(!iter.hasNext) {
list
}
else {
val slot = iter.next
slot.Equipment match {
case Some(equipment) =>
slot.Equipment = None
clearHolsters(iter, index + 1, InventoryItem(equipment, index) +: list)
case None =>
clearHolsters(iter, index + 1, list)
}
}
}
/**
* Iterate over a group of `EquipmentSlot`s, some of which may be occupied with an item.
* For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot.
* Add that item to the slot and remove it from the list.
* @param iter the `Iterator` of `EquipmentSlot`s
* @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot
* @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot
*/
@tailrec private def fillEmptyHolsters(iter : Iterator[EquipmentSlot], list : List[InventoryItem]) : List[InventoryItem] = {
if(!iter.hasNext) {
list
}
else {
val slot = iter.next
if(slot.Equipment.isEmpty) {
list.find(item => item.obj.Size == slot.Size) match {
case Some(obj) =>
val index = list.indexOf(obj)
slot.Equipment = obj.obj
fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1))
case None =>
fillEmptyHolsters(iter, list)
}
}
else {
fillEmptyHolsters(iter, list)
}
}
}
/**
* Construct tasking that coordinates the following:<br>
* 1) Accept a new piece of `Equipment` and register it with a globally unique identifier.<br>
* 2) Once it is registered, give the `Equipment` to `target`.
* @param target what object will accept the new `Equipment`
* @param obj the new `Equipment`
* @param index the slot where the new `Equipment` will be placed
* @see `GUIDTask.RegisterEquipment`
* @see `PutInSlot`
* @return a `TaskResolver.GiveTask` message
*/
private def PutEquipmentInSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = {
val regTask = GUIDTask.RegisterEquipment(obj)(continent.GUID)
obj match {
case tool : Tool =>
val linearToolTask = TaskResolver.GiveTask(regTask.task) +: regTask.subs
TaskResolver.GiveTask(PutInSlot(target, tool, index).task, linearToolTask)
case _ =>
TaskResolver.GiveTask(PutInSlot(target, obj, index).task, List(regTask))
}
}
/**
* Construct tasking that coordinates the following:<br>
* 1) Remove a new piece of `Equipment` from where it is currently stored.<br>
* 2) Once it is removed, un-register the `Equipment`'s globally unique identifier.
* @param target the object that currently possesses the `Equipment`
* @param obj the `Equipment`
* @param index the slot from where the `Equipment` will be removed
* @see `GUIDTask.UnregisterEquipment`
* @see `RemoveFromSlot`
* @return a `TaskResolver.GiveTask` message
*/
private def RemoveEquipmentFromSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = {
val regTask = GUIDTask.UnregisterEquipment(obj)(continent.GUID)
//to avoid an error from a GUID-less object from being searchable, it is removed from the inventory first
obj match {
case _ : Tool =>
TaskResolver.GiveTask(regTask.task, RemoveFromSlot(target, obj, index) +: regTask.subs)
case _ =>
TaskResolver.GiveTask(regTask.task, List(RemoveFromSlot(target, obj, index)))
}
}
/**
* Construct tasking that gives the `Equipment` to `target`.
* @param target what object will accept the new `Equipment`
* @param obj the new `Equipment`
* @param index the slot where the new `Equipment` will be placed
* @return a `TaskResolver.GiveTask` message
*/
private def PutInSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localTarget = target
private val localIndex = index
private val localObject = obj
private val localAnnounce = self
private val localService = avatarService
override def isComplete : Task.Resolution.Value = {
if(localTarget.Slot(localIndex).Equipment.contains(localObject)) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
localTarget.Slot(localIndex).Equipment = localObject
resolver ! scala.util.Success(this)
}
override def onSuccess() : Unit = {
val definition = localObject.Definition
localAnnounce ! ResponseToSelf(
ObjectCreateDetailedMessage(
definition.ObjectId,
localObject.GUID,
ObjectCreateMessageParent(localTarget.GUID, localIndex),
definition.Packet.DetailedConstructorData(localObject).get
)
)
if(localTarget.VisibleSlots.contains(localIndex)) {
localService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(localTarget.GUID, localTarget.GUID, localIndex, localObject))
}
}
})
}
/**
* Construct tasking that registers all aspects of a `Player` avatar.
* `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
* @param tplayer the avatar `Player`
* @return a `TaskResolver.GiveTask` message
*/
private def RegisterNewAvatar(tplayer : Player) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localPlayer = tplayer
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = {
if(localPlayer.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
log.info(s"Player $localPlayer is registered")
resolver ! scala.util.Success(this)
localAnnounce ! NewPlayerLoaded(localPlayer) //alerts WSA
}
override def onFailure(ex : Throwable) : Unit = {
localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA
}
}, List(GUIDTask.RegisterAvatar(tplayer)(continent.GUID))
)
}
/**
* Construct tasking that registers all aspects of a `Player` avatar.
* `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
* @param tplayer the avatar `Player`
* @return a `TaskResolver.GiveTask` message
*/
private def RegisterAvatar(tplayer : Player) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localPlayer = tplayer
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = {
if(localPlayer.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
log.info(s"Player $localPlayer is registered")
resolver ! scala.util.Success(this)
localAnnounce ! PlayerLoaded(localPlayer) //alerts WSA
}
override def onFailure(ex : Throwable) : Unit = {
localAnnounce ! PlayerFailedToLoad(localPlayer) //alerts WSA
}
}, List(GUIDTask.RegisterPlayer(tplayer)(continent.GUID))
)
}
/**
* Construct tasking that adds a completed and registered vehicle into the scene.
* Use this function to renew the globally unique identifiers on a vehicle that has already been added to the scene once.
* @param vehicle the `Vehicle` object
* @see `RegisterNewVehicle`
* @return a `TaskResolver.GiveTask` message
*/
def RegisterVehicle(vehicle : Vehicle) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localVehicle = vehicle
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = {
if(localVehicle.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
log.info(s"Vehicle $localVehicle is registered")
resolver ! scala.util.Success(this)
localAnnounce ! VehicleLoaded(localVehicle) //alerts WSA
}
}, List(GUIDTask.RegisterVehicle(vehicle)(continent.GUID))
)
}
/**
* Construct tasking that adds a completed and registered vehicle into the scene.
* The major difference between `RegisterVehicle` and `RegisterNewVehicle` is the assumption that this vehicle lacks an internal `Actor`.
* Before being finished, that vehicle is supplied an `Actor` such that it may function properly.
* This function wraps around `RegisterVehicle` and is used in case, prior to this event,
* the vehicle is being brought into existence from scratch and was never a member of any `Zone`.
* @param obj the `Vehicle` object
* @see `RegisterVehicle`
* @return a `TaskResolver.GiveTask` message
*/
def RegisterNewVehicle(obj : Vehicle, pad : VehicleSpawnPad) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localVehicle = obj
private val localPad = pad.Actor
private val localSession : String = sessionId.toString
private val localPlayer = player
private val localVehicleService = vehicleService
private val localZone = continent
override def isComplete : Task.Resolution.Value = {
if(localVehicle.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
localPad ! VehicleSpawnPad.VehicleOrder(localPlayer, localVehicle)
resolver ! scala.util.Success(this)
}
}, List(RegisterVehicle(obj)))
}
def RegisterDrivenVehicle(obj : Vehicle, driver : Player) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localVehicle = obj
private val localDriver = driver
private val localAnnounce = self
override def isComplete : Task.Resolution.Value = {
if(localVehicle.HasGUID && localDriver.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
localDriver.VehicleSeated = localVehicle.GUID
OwnVehicle(localVehicle, localDriver)
localAnnounce ! NewPlayerLoaded(localDriver) //alerts WSA
resolver ! scala.util.Success(this)
}
override def onFailure(ex : Throwable) : Unit = {
localAnnounce ! PlayerFailedToLoad(localDriver) //alerts WSA
}
}, List(GUIDTask.RegisterAvatar(driver)(continent.GUID), GUIDTask.RegisterVehicle(obj)(continent.GUID)))
}
def UnregisterDrivenVehicle(obj : Vehicle, driver : Player) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localVehicle = obj
private val localDriver = driver
override def isComplete : Task.Resolution.Value = {
if(!localVehicle.HasGUID && !localDriver.HasGUID) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
resolver ! scala.util.Success(this)
}
}, List(GUIDTask.UnregisterAvatar(driver)(continent.GUID), GUIDTask.UnregisterVehicle(obj)(continent.GUID)))
}
/**
* Construct tasking that removes the `Equipment` to `target`.
* @param target what object that contains the `Equipment`
* @param obj the `Equipment`
* @param index the slot where the `Equipment` is stored
* @return a `TaskResolver.GiveTask` message
*/
private def RemoveFromSlot(target : PlanetSideGameObject with Container, obj : Equipment, index : Int) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localTarget = target
private val localIndex = index
private val localObject = obj
private val localObjectGUID = obj.GUID
private val localAnnounce = self //self may not be the same when it executes
private val localService = avatarService
private val localContinent = continent.Id
override def isComplete : Task.Resolution.Value = {
if(localTarget.Slot(localIndex).Equipment.contains(localObject)) {
Task.Resolution.Incomplete
}
else {
Task.Resolution.Success
}
}
def Execute(resolver : ActorRef) : Unit = {
localTarget.Slot(localIndex).Equipment = None
resolver ! scala.util.Success(this)
}
override def onSuccess() : Unit = {
localAnnounce ! ResponseToSelf( ObjectDeleteMessage(localObjectGUID, 0))
if(localTarget.VisibleSlots.contains(localIndex)) {
localService ! AvatarServiceMessage(localContinent, AvatarAction.ObjectDelete(localTarget.GUID, localObjectGUID))
}
}
}
)
}
/**
* After some subtasking is completed, draw a particular slot, as if an `ObjectHeldMessage` packet was sent/received.<br>
* <br>
* The resulting `Task` is most useful for sequencing MAX weaponry when combined with the proper subtasks.
* @param player the player
* @param index the slot to be drawn
* @param priorTasking subtasks that needs to be accomplished first
* @return a `TaskResolver.GiveTask` message
*/
private def DelayedObjectHeld(player : Player, index : Int, priorTasking : List[TaskResolver.GiveTask]) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localPlayer = player
private val localSlot = index
private val localAnnounce = self
private val localService = avatarService
override def isComplete : Task.Resolution.Value = {
if(localPlayer.DrawnSlot == localSlot) {
Task.Resolution.Success
}
else {
Task.Resolution.Incomplete
}
}
def Execute(resolver : ActorRef) : Unit = {
localPlayer.DrawnSlot = localSlot
resolver ! scala.util.Success(this)
}
override def onSuccess() : Unit = {
localAnnounce ! ResponseToSelf( ObjectHeldMessage(localPlayer.GUID, localSlot, true))
localService ! AvatarServiceMessage(localPlayer.Continent, AvatarAction.ObjectHeld(localPlayer.GUID, localSlot))
}
}, priorTasking
)
}
/**
* Before calling `Interstellar.GetWorld` to change zones, perform the following task (which can be a nesting of subtasks).
* @param priorTask the tasks to perform
* @param zoneId the zone to load afterwards
* @return a `TaskResolver.GiveTask` message
*/
def TaskBeforeZoneChange(priorTask : TaskResolver.GiveTask, zoneId : String) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localService = cluster
private val localMsg = InterstellarCluster.GetWorld(zoneId)
override def isComplete : Task.Resolution.Value = priorTask.task.isComplete
def Execute(resolver : ActorRef) : Unit = {
localService ! localMsg
resolver ! scala.util.Success(this)
}
}, List(priorTask)
)
}
def CallBackForTask(task : TaskResolver.GiveTask, sendTo : ActorRef, pass : Any) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val destination = sendTo
private val passMsg = pass
def Execute(resolver : ActorRef) : Unit = {
destination ! passMsg
resolver ! scala.util.Success(this)
}
}, List(task)
)
}
/**
* After a client has connected to the server, their account is used to generate a list of characters.
* On the character selection screen, each of these characters is made to exist temporarily when one is selected.
* This "character select screen" is an isolated portion of the client, so it does not have any external constraints.
* Temporary global unique identifiers are assigned to the underlying `Player` objects so that they can be turned into packets.
* @param tplayer the `Player` object
* @param gen a constant source of incremental unique numbers
*/
private def SetCharacterSelectScreenGUID(tplayer : Player, gen : AtomicInteger) : Unit = {
tplayer.Holsters().foreach(holster => {
SetCharacterSelectScreenGUID_SelectEquipment(holster.Equipment, gen)
})
tplayer.GUID = PlanetSideGUID(gen.getAndIncrement)
}
/**
* Assists in assigning temporary global unique identifiers.
* If the item is a `Tool`, handle the embedded `AmmoBox` objects in each ammunition slot.
* Whether or not, give the object itself a GUID as well.
* @param item the piece of `Equipment`
* @param gen a constant source of incremental unique numbers
*/
private def SetCharacterSelectScreenGUID_SelectEquipment(item : Option[Equipment], gen : AtomicInteger) : Unit = {
item match {
case Some(tool : Tool) =>
tool.AmmoSlots.foreach(slot => { slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement) })
tool.GUID = PlanetSideGUID(gen.getAndIncrement)
case Some(item : Equipment) =>
item.GUID = PlanetSideGUID(gen.getAndIncrement)
case None => ;
}
}
/**
* After the user has selected a character to load from the "character select screen,"
* the temporary global unique identifiers used for that screen are stripped from the underlying `Player` object that was selected.
* Characters that were not selected may be destroyed along with their temporary GUIDs.
* @param tplayer the `Player` object
*/
private def RemoveCharacterSelectScreenGUID(tplayer : Player) : Unit = {
tplayer.Holsters().foreach(holster => {
RemoveCharacterSelectScreenGUID_SelectEquipment(holster.Equipment)
})
tplayer.Invalidate()
}
/**
* Assists in stripping temporary global unique identifiers.
* If the item is a `Tool`, handle the embedded `AmmoBox` objects in each ammunition slot.
* Whether or not, remove the GUID from the object itself.
* @param item the piece of `Equipment`
*/
private def RemoveCharacterSelectScreenGUID_SelectEquipment(item : Option[Equipment]) : Unit = {
item match {
case Some(item : Tool) =>
item.AmmoSlots.foreach(slot => { slot.Box.Invalidate() })
item.Invalidate()
case Some(item : Equipment) =>
item.Invalidate()
case None => ;
}
}
/**
* The process of hacking an object is completed.
* Pass the message onto the hackable object and onto the local events system.
* @param target the `Hackable` object that has been hacked
* @param unk na;
* used by `HackMessage` as `unk5`
* @see `HackMessage`
*/
//TODO add params here depending on which params in HackMessage are important
private def FinishHacking(target : PlanetSideServerObject with Hackable, unk : Long)() : Unit = {
log.info(s"Hacked a $target")
// Wait for the target actor to set the HackedBy property, otherwise LocalAction.HackTemporarily will not complete properly
import scala.concurrent.ExecutionContext.Implicits.global
ask(target.Actor, CommonMessages.Hack(player))(1 second).mapTo[Boolean].onComplete {
case Success(_) =>
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, target.HackSound, player.Position, 30, 0.49803925f))
target match {
case term: CaptureTerminal =>
val isResecured = player.Faction == target.Faction
localService ! LocalServiceMessage(continent.Id, LocalAction.HackCaptureTerminal(player.GUID, continent, term, unk, 8L, isResecured))
case _ => localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk, target.HackEffectDuration(GetPlayerHackLevel())))
}
case scala.util.Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
}
/**
* The process of hacking/jacking a vehicle is complete.
* Change the faction of the vehicle to the hacker's faction and remove all occupants.
*
* @param target The `Vehicle` object that has been hacked/jacked
* @param unk na; used by HackMessage` as `unk5`
*/
private def FinishHackingVehicle(target : Vehicle, unk : Long)(): Unit = {
log.info(s"Vehicle guid: ${target.GUID} has been jacked")
// Forcefully dismount any cargo
target.CargoHolds.values.foreach(cargoHold => {
cargoHold.Occupant match {
case Some(cargo : Vehicle) => {
cargo.Seats(0).Occupant match {
case Some(cargoDriver: Player) =>
DismountVehicleCargo(cargoDriver.GUID, cargo.GUID, bailed = target.Flying, requestedByPassenger = false, kicked = true )
case None =>
log.error("FinishHackingVehicle: vehicle in cargo hold missing driver")
HandleDismountVehicleCargo(player.GUID, cargo.GUID, cargo, target.GUID, target, false, false, true)
}
}
case None => ;
}
})
// Forcefully dismount all seated occupants from the vehicle
target.Seats.values.foreach(seat => {
seat.Occupant match {
case Some(tplayer) =>
seat.Occupant = None
tplayer.VehicleSeated = None
if(tplayer.HasGUID) {
vehicleService ! VehicleServiceMessage(tplayer.Continent, VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, target.GUID))
}
case None => ;
}
})
// If the vehicle can fly and is flying deconstruct it, and well played to whomever managed to hack a plane in mid air. I'm impressed.
if(target.Definition.CanFly && target.Flying) {
// todo: Should this force the vehicle to land in the same way as when a pilot bails with passengers on board?
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(target), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(target, continent, Some(0 seconds)))
} else { // Otherwise handle ownership transfer as normal
// Remove ownership of our current vehicle, if we have one
player.VehicleOwned match {
case Some(guid : PlanetSideGUID) =>
continent.GUID(guid) match {
case Some(vehicle: Vehicle) =>
DisownVehicle(player, vehicle)
case _ => ;
}
case _ => ;
}
target.Owner match {
case Some(previousOwnerGuid: PlanetSideGUID) =>
// Remove ownership of the vehicle from the previous player
continent.GUID(previousOwnerGuid) match {
case Some(player: Player) =>
DisownVehicle(player, target)
case _ => ; // Vehicle already has no owner
}
case _ => ;
}
// Now take ownership of the jacked vehicle
target.Faction = player.Faction
OwnVehicle(target, player)
//todo: Send HackMessage -> HackCleared to vehicle? can be found in packet captures. Not sure if necessary.
// And broadcast the faction change to other clients
sendResponse(SetEmpireMessage(target.GUID, player.Faction))
avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.SetEmpire(player.GUID, target.GUID, player.Faction))
}
localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackVehicle, target.Position, 30, 0.49803925f))
// Clean up after specific vehicles, e.g. remove router telepads
// If AMS is deployed, swap it to the new faction
target.Definition match {
case GlobalDefinitions.router =>
log.info("FinishHackingVehicle: cleaning up after a router ...")
RemoveTelepads(target)
case GlobalDefinitions.ams
if(target.DeploymentState == DriveState.Deployed) =>
vehicleService ! VehicleServiceMessage.AMSDeploymentChange(continent)
case _ => ;
}
}
/**
* The process of resecuring an IFF lock is finished
* Clear the hack state and send to clients
* @param lock the `IFFLock` object that has been resecured
*/
private def FinishResecuringIFFLock(lock: IFFLock)() : Unit = {
localService ! LocalServiceMessage(continent.Id, LocalAction.ClearTemporaryHack(player.GUID, lock))
}
/**
* The process of upgrading a turret's weapon(s) is completed.
* Pass the message onto the turret and onto the vehicle events system.
* Additionally, force-deplete the ammunition count of the nano-dispenser used to perform the upgrade.
* @param target the turret
* @param tool the nano-dispenser that was used to perform this upgrade
* @param upgrade the new upgrade state
*/
private def FinishUpgradingMannedTurret(target : FacilityTurret, tool : Tool, upgrade : TurretUpgrade.Value)() : Unit = {
log.info(s"Converting manned wall turret weapon to $upgrade")
tool.Magazine = 0
sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, tool.GUID, 0))
vehicleService ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.ClearSpecific(List(target), continent))
vehicleService ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(target, continent, upgrade))
}
/**
* Temporary function that iterates over vehicle permissions and turns them into `PlanetsideAttributeMessage` packets.<br>
* <br>
* 2 November 2017:<br>
* Unexpected behavior causes seat mount points to become blocked when a new driver claims the vehicle.
* For the purposes of ensuring that other players are always aware of the proper permission state of the trunk and seats,
* packets are intentionally dispatched to the current client to update the states.
* Perform this action just after any instance where the client would initially gain awareness of the vehicle.
* The most important examples include either the player or the vehicle itself spawning in for the first time.<br>
* <br>
* 20 February 2018:<br>
* Occasionally, during deployment, local(?) vehicle seat access permissions may change.
* This results in players being locked into their own vehicle.
* Reloading vehicle permissions supposedly ensures the seats will be properly available.
* This is considered a client issue; but, somehow, it also impacts server operation somehow.<br>
* <br>
* 22 June 2018:<br>
* I think vehicle ownership works properly now.
* @param vehicle the `Vehicle`
*/
def ReloadVehicleAccessPermissions(vehicle : Vehicle) : Unit = {
if(vehicle.Faction == player.Faction) {
val vehicle_guid = vehicle.GUID
(0 to 3).foreach(group => {
sendResponse(
PlanetsideAttributeMessage(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id)
)
})
}
}
/**
* na
* @param vehicle na
* @param tplayer na
* @return na
*/
def OwnVehicle(vehicle : Vehicle, tplayer : Player) : Option[Vehicle] = OwnVehicle(vehicle, Some(tplayer))
/**
* na
* @param vehicle na
* @param playerOpt na
* @return na
*/
def OwnVehicle(vehicle : Vehicle, playerOpt : Option[Player]) : Option[Vehicle] = {
playerOpt match {
case Some(tplayer) =>
tplayer.VehicleOwned = vehicle.GUID
vehicle.AssignOwnership(playerOpt)
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.Ownership(player.GUID, vehicle.GUID))
ReloadVehicleAccessPermissions(vehicle)
Some(vehicle)
case None =>
None
}
}
/**
* Disassociate this client's player (oneself) from a vehicle that he owns.
*/
def DisownVehicle() : Option[Vehicle] = DisownVehicle(player)
/**
* Disassociate a player from a vehicle that he owns.
* The vehicle must exist in the game world on the current continent.
* This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat.
* This is the player side of vehicle ownership removal.
* @param tplayer the player
*/
def DisownVehicle(tplayer : Player) : Option[Vehicle] = {
tplayer.VehicleOwned match {
case Some(vehicle_guid) =>
tplayer.VehicleOwned = None
continent.GUID(vehicle_guid) match {
case Some(vehicle : Vehicle) =>
DisownVehicle(tplayer, vehicle)
case _ =>
None
}
case None =>
None
}
}
/**
* Disassociate a player from a vehicle that he owns without associating a different player as the owner.
* Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire,"
* then reload them for all clients.
* This is the vehicle side of vehicle ownership removal.
* @param tplayer the player
*/
private def DisownVehicle(tplayer : Player, vehicle : Vehicle) : Option[Vehicle] = {
val pguid = tplayer.GUID
if(vehicle.Owner.contains(pguid)) {
vehicle.AssignOwnership(None)
val factionOnContinent = s"${continent.Id}/${vehicle.Faction}"
vehicleService ! VehicleServiceMessage(factionOnContinent, VehicleAction.Ownership(pguid, PlanetSideGUID(0)))
val vguid = vehicle.GUID
val empire = VehicleLockState.Empire.id
(0 to 2).foreach(group => {
vehicle.PermissionGroup(group, empire)
vehicleService ! VehicleServiceMessage(factionOnContinent, VehicleAction.SeatPermissions(pguid, vguid, group, empire))
})
ReloadVehicleAccessPermissions(vehicle)
Some(vehicle)
}
else {
None
}
}
/**
* Gives a target player positive battle experience points only.
* If the player has access to more implant slots as a result of changing battle experience points, unlock those slots.
* @param avatar the player
* @param bep the change in experience points, positive by assertion
* @return the player's current battle experience points
*/
def AwardBattleExperiencePoints(avatar : Avatar, bep : Long) : Long = {
val oldBep = avatar.BEP
if(bep <= 0) {
log.error(s"trying to set $bep battle experience points on $avatar; value can not be negative")
oldBep
}
else {
val oldSlots = DetailedCharacterData.numberOfImplantSlots(oldBep)
val newBep = oldBep + bep
val newSlots = DetailedCharacterData.numberOfImplantSlots(newBep)
avatar.BEP = newBep
if(newSlots > oldSlots) {
(oldSlots until newSlots).foreach(slotNumber => {
avatar.Implants(slotNumber).Unlocked = true
log.info(s"unlocking implant slot $slotNumber for $avatar")
})
}
newBep
}
}
/**
* Common preparation for interfacing with a vehicle.
* Join a vehicle-specific group for shared updates.
* Construct every object in the vehicle's inventory fpr shared manipulation updates.
* @param vehicle the vehicle
*/
def AccessContents(vehicle : Vehicle) : Unit = {
vehicleService ! Service.Join(s"${vehicle.Actor}")
val parent_guid = vehicle.GUID
vehicle.Trunk.Items.foreach(entry => {
val obj = entry.obj
val objDef = obj.Definition
sendResponse(
ObjectCreateDetailedMessage(
objDef.ObjectId,
obj.GUID,
ObjectCreateMessageParent(parent_guid, entry.start),
objDef.Packet.DetailedConstructorData(obj).get
)
)
})
}
/**
* Common preparation for disengaging from a vehicle.
* Leave the vehicle-specific group that was used for shared updates.
* Deconstruct every object in the vehicle's inventory.
* @param vehicle the vehicle
*/
def UnAccessContents(vehicle : Vehicle) : Unit = {
vehicleService ! Service.Leave(Some(s"${vehicle.Actor}"))
vehicle.Trunk.Items.foreach(entry =>{
sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
})
}
/**
* Check two locations for a controlled piece of equipment that is associated with the `player`.<br>
* <br>
* The first location is dependent on whether the avatar is in a vehicle.
* Some vehicle seats may have a "controlled weapon" which counts as the first location to be checked.
* The second location is dependent on whether the avatar has a raised hand.
* That is only possible if the player has something in their hand at the moment, hence the second location.
* Players do have a concept called a "last drawn slot" (hand) but that former location is not eligible.<br>
* <br>
* Along with any discovered item, a containing object such that the statement:<br>
* `container.Find(object) = Some(slot)`<br>
* ... will return a proper result.
* For a seat controlled weapon, the vehicle is returned.
* For the player's hand, the player is returned.
* @return a `Tuple` of the returned values;
* the first value is a `Container` object;
* the second value is an `Equipment` object in the former
*/
def FindContainedEquipment : (Option[PlanetSideGameObject with Container], Option[Equipment]) = {
player.VehicleSeated match {
case Some(vehicle_guid) => //weapon is vehicle turret?
continent.GUID(vehicle_guid) match {
case Some(vehicle : Mountable with MountedWeapons with Container) =>
vehicle.PassengerInSeat(player) match {
case Some(seat_num) =>
(Some(vehicle), vehicle.WeaponControlledFromSeat(seat_num))
case None => ;
(None, None)
}
case _ => ;
(None, None)
}
case None => //not in vehicle; weapon in hand?
(Some(player), player.Slot(player.DrawnSlot).Equipment)
}
}
/**
* Runs `FindContainedEquipment` but ignores the `Container` object output.
* @return an `Equipment` object
*/
def FindEquipment : Option[Equipment] = FindContainedEquipment._2
/**
* Check two locations for a controlled piece of equipment that is associated with the `player`.
* Filter for discovered `Tool`-type `Equipment`.
* @return a `Tuple` of the returned values;
* the first value is a `Container` object;
* the second value is an `Tool` object in the former
*/
def FindContainedWeapon : (Option[PlanetSideGameObject with Container], Option[Tool]) = {
FindContainedEquipment match {
case (container, Some(tool : Tool)) =>
(container, Some(tool))
case _ =>
(None, None)
}
}
/**
* Runs `FindContainedWeapon` but ignores the `Container` object output.
* @return a `Tool` object
*/
def FindWeapon : Option[Tool] = FindContainedWeapon._2
/**
* Within a specified `Container`, find the smallest number of `Equipment` objects of a certain qualifying type
* whose sum count is greater than, or equal to, a `desiredAmount` based on an accumulator method.<br>
* <br>
* In an occupied `List` of returned `Inventory` entries, all but the last entry is typically considered "emptied."
* For objects with contained quantities, the last entry may require having that quantity be set to a non-zero number.
* @param obj the `Container` to search
* @param filterTest test used to determine inclusivity of `Equipment` collection
* @param desiredAmount how much is requested
* @param counting test used to determine value of found `Equipment`;
* defaults to one per entry
* @return a `List` of all discovered entries totaling approximately the amount requested
*/
def FindEquipmentStock(obj : Container,
filterTest : (Equipment)=>Boolean,
desiredAmount : Int,
counting : (Equipment)=>Int = DefaultCount) : List[InventoryItem] = {
var currentAmount : Int = 0
obj.Inventory.Items
.filter(item => filterTest(item.obj))
.toList
.sortBy(_.start)
.takeWhile(entry => {
val previousAmount = currentAmount
currentAmount += counting(entry.obj)
previousAmount < desiredAmount
})
}
/**
* The default counting function for an item.
* Counts the number of item(s).
* @param e the `Equipment` object
* @return the quantity;
* always one
*/
def DefaultCount(e : Equipment) : Int = 1
/**
* The counting function for an item of `AmmoBox`.
* Counts the `Capacity` of the ammunition.
* @param e the `Equipment` object
* @return the quantity
*/
def CountAmmunition(e : Equipment) : Int = {
e match {
case a : AmmoBox =>
a.Capacity
case _ =>
0
}
}
/**
* The counting function for an item of `Tool` where the item is also a grenade.
* Counts the number of grenades.
* @see `GlobalDefinitions.isGrenade`
* @param e the `Equipment` object
* @return the quantity
*/
def CountGrenades(e : Equipment) : Int = {
e match {
case t : Tool =>
(GlobalDefinitions.isGrenade(t.Definition):Int) * t.Magazine
case _ =>
0
}
}
/**
* Flag an `AmmoBox` object that matches for the given ammunition type.
* @param ammo the type of `Ammo` to check
* @param e the `Equipment` object
* @return `true`, if the object is an `AmmoBox` of the correct ammunition type; `false`, otherwise
*/
def FindAmmoBoxThatUses(ammo : Ammo.Value)(e : Equipment) : Boolean = {
e match {
case t : AmmoBox =>
t.AmmoType == ammo
case _ =>
false
}
}
/**
* Flag a `Tool` object that matches for loading the given ammunition type.
* @param ammo the type of `Ammo` to check
* @param e the `Equipment` object
* @return `true`, if the object is a `Tool` that loads the correct ammunition type; `false`, otherwise
*/
def FindToolThatUses(ammo : Ammo.Value)(e : Equipment) : Boolean = {
e match {
case t : Tool =>
t.Definition.AmmoTypes.map { _.AmmoType }.contains(ammo)
case _ =>
false
}
}
/**
* 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] = {
player.VehicleSeated match {
case Some(vehicle_guid) =>
continent.GUID(vehicle_guid) match {
case Some(obj : Vehicle) =>
Some(obj)
case _ =>
None
}
case None =>
None
}
}
/**
* Given an object that contains an item (`Equipment`) in its `Inventory` at a certain location,
* remove it permanently.
* @param obj the `Container`
* @param start where the item can be found
* @param item an object to unregister;
* not explicitly checked
*/
private def DeleteEquipment(obj : PlanetSideGameObject with Container)(start : Int, item : Equipment) : Unit = {
val item_guid = item.GUID
obj.Slot(start).Equipment = None
//obj.Inventory -= start
taskResolver ! GUIDTask.UnregisterEquipment(item)(continent.GUID)
sendResponse(ObjectDeleteMessage(item_guid, 0))
}
/**
* Given a vehicle that contains an item (`Equipment`) in its `Trunk` at a certain location,
* remove it permanently.
* @see `DeleteEquipment`
* @param obj the `Vehicle`
* @param start where the item can be found
* @param item an object to unregister;
* not explicitly checked
*/
private def DeleteEquipmentFromVehicle(obj : Vehicle)(start : Int, item : Equipment) : Unit = {
val item_guid = item.GUID
DeleteEquipment(obj)(start, item)
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player.GUID, item_guid))
}
/**
* 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 amunition 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 ModifyAmmunitionInVehicle(obj : Vehicle)(box : AmmoBox, reloadValue : Int) : Unit = {
val capacity = ModifyAmmunition(obj)(box, reloadValue)
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.InventoryState(player.GUID, box, obj.GUID, obj.Find(box).get, box.Definition.Packet.DetailedConstructorData(box).get))
}
/**
* Announce that an already-registered `AmmoBox` object exists in a given position in some `Container` object's inventory.
* @see `StowEquipmentInVehicles`
* @see `ChangeAmmoMessage`
* @param obj the `Container` object
* @param index an index in `obj`'s inventory
* @param item an `AmmoBox`
*/
def StowEquipment(obj : PlanetSideGameObject with Container)(index : Int, item : AmmoBox) : Unit = {
obj.Inventory += index -> item
sendResponse(ObjectAttachMessage(obj.GUID, item.GUID, index))
}
/**
* Announce that an already-registered `AmmoBox` object exists in a given position in some vehicle's inventory.
* @see `StowEquipment`
* @see `ChangeAmmoMessage`
* @param obj the `Vehicle` object
* @param index an index in `obj`'s inventory
* @param item an `AmmoBox`
*/
def StowEquipmentInVehicles(obj : Vehicle)(index : Int, item : AmmoBox) : Unit = {
StowEquipment(obj)(index, item)
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player.GUID, obj.GUID, index, item))
}
/**
* Prepare tasking that registers an `AmmoBox` object
* and announces that it exists in a given position in some `Container` object's inventory.
* `PutEquipmentInSlot` is the fastest way to achieve these goals.
* @see `StowNewEquipmentInVehicle`
* @see `ChangeAmmoMessage`
* @param obj the `Container` object
* @param index an index in `obj`'s inventory
* @param item the `Equipment` item
* @return a `TaskResolver.GiveTask` chain that executes the action
*/
def StowNewEquipment(obj : PlanetSideGameObject with Container)(index : Int, item : Equipment) : TaskResolver.GiveTask = {
PutEquipmentInSlot(obj, item, index)
}
/**
* Prepare tasking that registers an `AmmoBox` object
* and announces that it exists in a given position in some vehicle's inventory.
* `PutEquipmentInSlot` is the fastest way to achieve these goals.
* @see `StowNewEquipment`
* @see `ChangeAmmoMessage`
* @param obj the `Container` object
* @param index an index in `obj`'s inventory
* @param item the `Equipment` item
* @return a `TaskResolver.GiveTask` chain that executes the action
*/
def StowNewEquipmentInVehicle(obj : Vehicle)(index : Int, item : Equipment) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localService = vehicleService
private val localPlayer = player
private val localVehicle = obj
private val localIndex = index
private val localItem = item
override def isComplete : Task.Resolution.Value = Task.Resolution.Success
def Execute(resolver : ActorRef) : Unit = {
localService ! VehicleServiceMessage(
s"${localVehicle.Actor}",
VehicleAction.StowEquipment(localPlayer.GUID, localVehicle.GUID, localIndex, localItem)
)
resolver ! scala.util.Success(this)
}
},
List(StowNewEquipment(obj)(index, item))
)
}
/**
* Given an item, and two places, one where the item currently is and one where the item will be moved,
* perform a controlled transfer of the item.
* If something exists at the `destination` side of the transfer in the position that `item` will occupy,
* resolve its location as well by swapping it with where `item` originally was positioned.<br>
* <br>
* Parameter checks will not be performed.
* Do perform checks before sending data to this function.
* Do not call with incorrect or unverified data, e.g., `item` not actually being at `source` @ `index`.
* @param item the item being moved
* @param source the container in which `item` is currently located
* @param index the index position in `source` where `item` is currently located
* @param destination the container where `item` is being moved
* @param dest the index position in `destination` where `item` is being moved
* @param destinationCollisionEntry information about the contents in an area of `destination` starting at index `dest`
*/
private def PerformMoveItem(item : Equipment,
source : PlanetSideGameObject with Container,
index : Int,
destination : PlanetSideGameObject with Container,
dest : Int,
destinationCollisionEntry : Option[InventoryItem]) : Unit = {
val item_guid = item.GUID
val source_guid = source.GUID
val destination_guid = destination.GUID
val player_guid = player.GUID
val indexSlot = source.Slot(index)
val sourceIsNotDestination : Boolean = source != destination //if source is destination, explicit OCDM is not required
if(sourceIsNotDestination) {
log.info(s"MoveItem: $item moved from $source @ $index to $destination @ $dest")
}
else {
log.info(s"MoveItem: $item moved from $index to $dest in $source")
}
//remove item from source
indexSlot.Equipment = None
source match {
case obj : Vehicle =>
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item_guid))
case obj : Player =>
if(obj.isBackpack || source.VisibleSlots.contains(index)) { //corpse being looted, or item was in hands
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, item_guid))
}
case _ => ;
}
destinationCollisionEntry match { //do we have a swap item in the destination slot?
case Some(InventoryItem(item2, destIndex)) => //yes, swap
//cleanly shuffle items around to avoid losing icons
//the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap
sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f))
val item2_guid = item2.GUID
destination.Slot(destIndex).Equipment = None //remove the swap item from destination
(indexSlot.Equipment = item2) match {
case Some(_) => //item and item2 swapped places successfully
log.info(s"MoveItem: $item2 swapped to $source @ $index")
//remove item2 from destination
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f))
destination match {
case obj : Vehicle =>
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid))
case obj : Player =>
if(obj.isBackpack || destination.VisibleSlots.contains(dest)) { //corpse being looted, or item was accessible
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, item2_guid))
//put hand down locally
if(dest == player.DrawnSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
}
case _ => ;
}
//display item2 in source
if(sourceIsNotDestination && player == source) {
val objDef = item2.Definition
sendResponse(
ObjectCreateDetailedMessage(
objDef.ObjectId,
item2_guid,
ObjectCreateMessageParent(source_guid, index),
objDef.Packet.DetailedConstructorData(item2).get
)
)
}
else {
sendResponse(ObjectAttachMessage(source_guid, item2_guid, index))
}
source match {
case obj : Vehicle =>
item2.Faction = PlanetSideEmpire.NEUTRAL
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, source_guid, index, item2))
case obj : Player =>
item2.Faction = obj.Faction
if(source.VisibleSlots.contains(index)) { //item is put in hands
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, source_guid, index, item2))
}
else if(obj.isBackpack) { //corpse being given item
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.StowEquipment(player_guid, source_guid, index, item2))
}
case _ =>
item2.Faction = PlanetSideEmpire.NEUTRAL
}
case None => //item2 does not fit; drop on ground
log.info(s"MoveItem: $item2 can not fit in swap location; dropping on ground @ ${source.Position}")
val pos = source.Position
val sourceOrientZ = source.Orientation.z
val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ)
continent.Ground ! Zone.Ground.DropItem(item2, pos, orient)
sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, sourceOrientZ)) //ground
val objDef = item2.Definition
destination match {
case obj : Vehicle =>
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid))
case _ => ;
//Player does not require special case; the act of dropping forces the item and icon to change
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(player_guid, item2, continent))
}
case None => ;
}
//move item into destination slot
destination.Slot(dest).Equipment = item
if(sourceIsNotDestination && player == destination) {
val objDef = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
objDef.ObjectId,
item_guid,
ObjectCreateMessageParent(destination_guid, dest),
objDef.Packet.DetailedConstructorData(item).get
)
)
}
else {
sendResponse(ObjectAttachMessage(destination_guid, item_guid, dest))
}
destination match {
case obj : Vehicle =>
item.Faction = PlanetSideEmpire.NEUTRAL
vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.StowEquipment(player_guid, destination_guid, dest, item))
case obj : Player =>
if(destination.VisibleSlots.contains(dest)) { //item is put in hands
item.Faction = obj.Faction
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, destination_guid, dest, item))
}
else if(obj.isBackpack) { //corpse being given item
item.Faction = PlanetSideEmpire.NEUTRAL
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.StowEquipment(player_guid, destination_guid, dest, item))
}
case _ =>
item.Faction = PlanetSideEmpire.NEUTRAL
}
}
/**
* na
* @param equipment na
* @param obj na
* @return `true`, if the object is allowed to contain the type of equipment object
*/
def PermitEquipmentStow(equipment : Equipment, obj : PlanetSideGameObject with Container) : Boolean = {
equipment match {
case _ : BoomerTrigger =>
obj.isInstanceOf[Player] //a BoomerTrigger can only be stowed in a player's holsters or inventory
case _ =>
true
}
}
/**
* na
* @param tool na
* @param obj na
*/
def PerformToolAmmoChange(tool : Tool, obj : PlanetSideGameObject with Container) : 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 (deleteFunc, modifyFunc) : ((Int, AmmoBox)=>Unit, (AmmoBox, Int)=>Unit) = obj match {
case (veh : Vehicle) =>
(DeleteEquipmentFromVehicle(veh), ModifyAmmunitionInVehicle(veh))
case _ =>
(DeleteEquipment(obj), ModifyAmmunition(obj))
}
val (stowFuncTask, stowFunc) : ((Int, AmmoBox)=>TaskResolver.GiveTask, (Int, AmmoBox)=>Unit) = obj match {
case (veh : Vehicle) =>
(StowNewEquipmentInVehicle(veh), StowEquipmentInVehicles(veh))
case _ =>
(StowNewEquipment(obj), StowEquipment(obj))
}
xs.foreach(item => {
obj.Inventory -= x.start
deleteFunc(item.start, item.obj.asInstanceOf[AmmoBox])
})
//box will be the replacement ammo; give it the discovered magazine and load it into the weapon @ 0
val box = x.obj.asInstanceOf[AmmoBox]
val originalBoxCapacity = box.Capacity
val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) }
val sumReloadValue : Int = originalBoxCapacity + tailReloadValue
val previousBox = tool.AmmoSlot.Box //current magazine in tool
sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f))
sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f))
obj.Inventory -= x.start //remove replacement ammo from inventory
val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool
sendResponse(ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex))
//announce swapped ammunition box in weapon
val previous_box_guid = previousBox.GUID
val boxDef = box.Definition
val box_guid = box.GUID
val tool_guid = tool.GUID
sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity))
avatarService ! 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.info(s"ChangeAmmo: taking ${originalBoxCapacity - splitReloadAmmo} from a box of ${originalBoxCapacity} $requestedAmmoType")
val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo)
obj.Inventory += x.start -> boxForInventory //block early; assumption warning: swappable ammo types have the same icon size
taskResolver ! stowFuncTask(x.start, boxForInventory)
fullMagazine
})
sendResponse(InventoryStateMessage(box.GUID, tool.GUID, box.Capacity)) //should work for both players and vehicles
log.info(s"ChangeAmmo: loading ${box.Capacity} $requestedAmmoType into ${tool.GUID} @ $ammoSlotIndex")
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
}
})
.toList
.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"ChangeAmmo: putting $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(index) =>
stowFunc(index, previousBox)
case None =>
NormalItemDrop(player, continent, avatarService)(previousBox)
}
val dropFunc : (Equipment)=>TaskResolver.GiveTask = NewItemDrop(player, continent, avatarService)
AmmoBox.Split(previousBox) match {
case Nil | _ :: Nil => ; //done (the former case is technically not possible)
case _ :: xs =>
modifyFunc(previousBox, 0) //update to changed capacity value
xs.foreach(box => {
obj.Inventory.Fit(box) match {
case Some(index) =>
obj.Inventory += index -> box //block early, for purposes of Fit
taskResolver ! stowFuncTask(index, box)
case None =>
taskResolver ! dropFunc(box)
}
})
}
}
else {
taskResolver ! GUIDTask.UnregisterObjectTask(previousBox)(continent.GUID)
}
}
}
}
while(tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType)
}
/**
* Drop an `Equipment` item onto the ground.
* Specifically, instruct the item where it will appear,
* add it to the list of items that are visible to multiple users,
* and then inform others that the item has been dropped.
* @param obj a `Container` object that represents where the item will be dropped;
* curried for callback
* @param zone the continent in which the item is being dropped;
* curried for callback
* @param service a reference to the event system that announces that the item has been dropped on the ground;
* "AvatarService";
* curried for callback
* @param item the item
*/
def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = {
continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z))
}
/**
* Register an `Equipment` item and then drop it on the ground.
* @see `NormalItemDrop`
* @param obj a `Container` object that represents where the item will be dropped;
* curried for callback
* @param zone the continent in which the item is being dropped;
* curried for callback
* @param service a reference to the event system that announces that the item has been dropped on the ground;
* "AvatarService";
* curried for callback
* @param item the item
*/
def NewItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : TaskResolver.GiveTask = {
TaskResolver.GiveTask(
new Task() {
private val localItem = item
private val localFunc : (Equipment)=>Unit = NormalItemDrop(obj, zone, service)
def Execute(resolver : ActorRef) : Unit = {
localFunc(localItem)
resolver ! scala.util.Success(this)
}
}, List(GUIDTask.RegisterEquipment(item)(zone.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 this is temporary and will be 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"no more $ammoType grenades")
taskResolver ! RemoveEquipmentFromSlot(player, tool, player.Find(tool).get)
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).reduce(_ + _) }
val sumReloadValue : Int = box.Magazine + tailReloadValue
val actualReloadValue = (if(sumReloadValue <= 3) {
taskResolver ! RemoveEquipmentFromSlot(player, x.obj, x.start)
sumReloadValue
}
else {
ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue)
3
})
log.info(s"found $actualReloadValue more $ammoType grenades to throw")
ModifyAmmunition(player)(tool.AmmoSlot.Box, -actualReloadValue) //grenade item already in holster (negative because empty)
xs.foreach(item => {
taskResolver ! RemoveEquipmentFromSlot(player, item.obj, item.start)
})
}
}
else if(tdef == GlobalDefinitions.phoenix) {
taskResolver ! RemoveEquipmentFromSlot(player, tool, player.Find(tool).get)
}
}
/**
* A predicate used to determine if an `InventoryItem` object contains `Equipment` that should be dropped.
* Used to filter through lists of object data before it is placed into a player's inventory.
* Drop the item if:<br>
* - the item is cavern equipment<br>
* - the item is a `BoomerTrigger` type object<br>
* - the item is a `router_telepad` type object<br>
* - the item is another faction's exclusive equipment
* @param tplayer the player
* @return true if the item is to be dropped; false, otherwise
*/
def DropPredicate(tplayer : Player) : (InventoryItem => Boolean) = entry => {
val objDef = entry.obj.Definition
val faction = GlobalDefinitions.isFactionEquipment(objDef)
GlobalDefinitions.isCavernEquipment(objDef) ||
objDef == GlobalDefinitions.router_telepad ||
entry.obj.isInstanceOf[BoomerTrigger] ||
(faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)
}
/**
* Given an object globally unique identifier, search in a given location for it.
* @param object_guid the object
* @param parent a `Container` object wherein to search
* @return an optional tuple that contains two values;
* the first value is the container that matched correctly with the object's GUID;
* the second value is the slot position of the object
*/
def FindInLocalContainer(object_guid : PlanetSideGUID)(parent : PlanetSideGameObject with Container) : Option[(PlanetSideGameObject with Container, Option[Int])] = {
val slot : Option[Int] = parent.Find(object_guid)
slot match {
case place @ Some(_) =>
Some(parent, slot)
case None =>
None
}
}
/**
* Perform specific operations depending on the target of deployment.
* @param obj the object that has had its deployment state changed
*/
def DeploymentActivities(obj : Deployment.DeploymentObject) : Unit = {
DeploymentActivities(obj, obj.DeploymentState)
}
/**
* Perform specific operations depending on the target of deployment.
* @param obj the object that has had its deployment state changed
* @param state the new deployment state
*/
def DeploymentActivities(obj : Deployment.DeploymentObject, state : DriveState.Value) : Unit = {
obj match {
case vehicle : Vehicle =>
ReloadVehicleAccessPermissions(vehicle) //TODO we should not have to do this imho
//ams
if(vehicle.Definition == GlobalDefinitions.ams) {
state match {
case DriveState.Deployed =>
vehicleService ! VehicleServiceMessage.AMSDeploymentChange(continent)
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 81, 1))
case DriveState.Undeploying =>
vehicleService ! VehicleServiceMessage.AMSDeploymentChange(continent)
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 81, 0))
case DriveState.Mobile | DriveState.State7 =>
case _ => ;
}
}
//ant
else if(vehicle.Definition == GlobalDefinitions.ant) {
state match {
case DriveState.Deployed =>
// We only want this WSA (not other player's WSA) to manage timers
if(vehicle.Seat(0).get.Occupant.contains(player)){
// Start ntu regeneration
// If vehicle sends UseItemMessage with silo as target NTU regeneration will be disabled and orb particles will be disabled
antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle))
}
case DriveState.Undeploying =>
// We only want this WSA (not other player's WSA) to manage timers
if(vehicle.Seat(0).get.Occupant.contains(player)){
antChargingTick.cancel() // Stop charging NTU if charging
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 0L)) // orb particles off
case DriveState.Mobile | DriveState.State7 | DriveState.Deploying =>
case _ => ;
}
}
//router
else if(vehicle.Definition == GlobalDefinitions.router) {
state match {
case DriveState.Deploying =>
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util : Utility.InternalTelepad) =>
util.Active = true
case _ =>
log.warn(s"DeploymentActivities: could not find internal telepad in router@${vehicle.GUID.guid} while $state")
}
case DriveState.Deployed =>
//let the timer do all the work
localService ! LocalServiceMessage(continent.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), vehicle, TelepadLike.AppraiseTeleportationSystem(vehicle, continent)))
case DriveState.Undeploying =>
//deactivate internal router before trying to reset the system
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util : Utility.InternalTelepad) =>
//any telepads linked with internal mechanism must be deconstructed
continent.GUID(util.Telepad) match {
case Some(telepad : TelepadDeployable) =>
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, continent, Some(0 milliseconds)))
case Some(_) | None => ;
}
util.Active = false
localService ! LocalServiceMessage(continent.Id, LocalAction.ToggleTeleportSystem(PlanetSideGUID(0), vehicle, None))
case _ =>
log.warn(s"DeploymentActivities: could not find internal telepad in router@${vehicle.GUID.guid} while $state")
}
case _ => ;
}
}
case _ => ;
}
}
/**
* 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, false, Vector3.Zero))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, false, Vector3.Zero))
"; enforcing Mobile deployment state"
}
else {
""
}
log.error(s"DeployRequest: $obj can not transition to $state - $reason$mobileShift")
}
/**
* For a given continental structure, determine the method of generating server-join client configuration packets.
* @param continentNumber the zone id
* @param buildingNumber the building id
* @param building the building object
*/
def initBuilding(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = {
building.BuildingType match {
case StructureType.WarpGate =>
initGate(continentNumber, buildingNumber, building)
case _ =>
initFacility(continentNumber, buildingNumber, building)
}
}
/**
* For a given facility structure, configure a client by dispatching the appropriate packets.
* Pay special attention to the details of `BuildingInfoUpdateMessage` when preparing this packet.<br>
* <br>
* 24 Janurtay 2019:<br>
* Manual `BIUM` construction to alleviate player login.
* @see `BuildingInfoUpdateMessage`
* @see `DensityLevelUpdateMessage`
* @param continentNumber the zone id
* @param buildingNumber the building id
* @param building the building object
*/
def initFacility(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = {
val (
ntuLevel,
isHacked, empireHack, hackTimeRemaining, controllingEmpire,
unk1, unk1x,
generatorState, spawnTubesNormal, forceDomeActive,
latticeBenefit, cavernBenefit,
unk4, unk5, unk6,
unk7, unk7x,
boostSpawnPain, boostGeneratorPain
) = building.Info
sendResponse(
BuildingInfoUpdateMessage(
continentNumber,
buildingNumber,
ntuLevel,
isHacked, empireHack, hackTimeRemaining, controllingEmpire,
unk1, unk1x,
generatorState, spawnTubesNormal, forceDomeActive,
latticeBenefit, cavernBenefit,
unk4, unk5, unk6,
unk7, unk7x,
boostSpawnPain, boostGeneratorPain
)
)
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0,0, 0,0, 0,0, 0,0)))
}
/**
* For a given lattice warp gate structure, configure a client by dispatching the appropriate packets.
* Unlike other facilities, gates do not have complicated `BuildingInfoUpdateMessage` packets.
* Also unlike facilities, gates have an additional packet.
* @see `BuildingInfoUpdateMessage`
* @see `DensityLevelUpdateMessage`
* @see `BroadcastWarpgateUpdateMessage`
* @param continentNumber the zone id
* @param buildingNumber the building id
* @param building the building object
*/
def initGate(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = {
building match {
case wg : WarpGate =>
sendResponse(
BuildingInfoUpdateMessage(
continentNumber,
buildingNumber,
ntu_level = 0,
is_hacked = false,
empire_hack = PlanetSideEmpire.NEUTRAL,
hack_time_remaining = 0,
building.Faction,
unk1 = 0,
unk1x = None,
PlanetSideGeneratorState.Normal,
spawn_tubes_normal = true,
force_dome_active = false,
lattice_benefit = 0,
cavern_benefit = 0,
unk4 = Nil,
unk5 = 0,
unk6 = false,
unk7 = 8,
unk7x = None,
boost_spawn_pain = false,
boost_generator_pain = false
)
)
sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0,0, 0,0, 0,0, 0,0)))
//TODO one faction knows which gates are broadcast for another faction?
sendResponse(
BroadcastWarpgateUpdateMessage(
continentNumber,
buildingNumber,
wg.Broadcast(PlanetSideEmpire.TR),
wg.Broadcast(PlanetSideEmpire.NC),
wg.Broadcast(PlanetSideEmpire.VS)
)
)
case _ => ;
}
}
/**
* Configure the buildings and each specific amenity for that building in a given zone by sending the client packets.
* These actions are performed during the loading of a zone.
* @see `SetEmpireMessage`<br>
* `PlanetsideAttributeMessage`<br>
* `HackMessage`
* @param zone the zone being loaded
*/
def configZone(zone : Zone) : Unit = {
zone.Buildings.values.foreach(building => {
sendResponse(SetEmpireMessage(building.GUID, building.Faction))
building.Amenities.foreach(amenity => {
val amenityId = amenity.GUID
sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0))
sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0))
amenity.Definition match {
case GlobalDefinitions.resource_silo =>
// Synchronise warning light & silo capacity
val silo = amenity.asInstanceOf[ResourceSilo]
sendResponse(PlanetsideAttributeMessage(amenityId, 45, silo.CapacitorDisplay))
sendResponse(PlanetsideAttributeMessage(amenityId, 47, if(silo.LowNtuWarningOn) 1 else 0))
if(silo.ChargeLevel == 0) {
// temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it.
// sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(silo.Owner.asInstanceOf[Building].ModelId), 48, 1))
}
case _ => ;
}
// Synchronise hack states to clients joining the zone.
// We'll have to fake LocalServiceResponse messages to self, otherwise it means duplicating the same hack handling code twice
if(amenity.isInstanceOf[Hackable]) {
val hackable = amenity.asInstanceOf[Hackable]
if(hackable.HackedBy.isDefined) {
amenity.Definition match {
case GlobalDefinitions.capture_terminal =>
self ! LocalServiceResponse("", PlanetSideGUID(0), LocalResponse.HackCaptureTerminal(amenity.GUID, 0L, 0L, false))
case _ =>
// Generic hackable object
self ! LocalServiceResponse("", PlanetSideGUID(0), LocalResponse.HackObject(amenity.GUID, 1114636288L, 8L))
}
}
}
})
// sendResponse(HackMessage(3, PlanetSideGUID(building.ModelId), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8))
})
}
/**
* The player has lost the will to live and must be killed.
* @see `Vitality`<br>
* `PlayerSuicide`
* @param tplayer the player to be killed
*/
def Suicide(tplayer : Player) : Unit = {
tplayer.History(PlayerSuicide(PlayerSource(tplayer)))
KillPlayer(tplayer)
}
/**
* The player has lost all his vitality and must be killed.<br>
* <br>
* Shift directly into a state of being dead on the client by setting health to zero points,
* whereupon the player will perform a dramatic death animation.
* Stamina is also set to zero points.
* If the player was in a vehicle at the time of demise, special conditions apply and
* the model must be manipulated so it behaves correctly.
* Do not move or completely destroy the `Player` object as its coordinates of death will be important.<br>
* <br>
* A maximum revive waiting timer is started.
* When this timer reaches zero, the avatar will attempt to spawn back on its faction-specific sanctuary continent.
* @param tplayer the player to be killed
*/
def KillPlayer(tplayer : Player) : Unit = {
val player_guid = tplayer.GUID
val pos = tplayer.Position
val respawnTimer = 300000 //milliseconds
tplayer.Die
deadState = DeadState.Dead
sendResponse(PlanetsideAttributeMessage(player_guid, 0, 0))
sendResponse(PlanetsideAttributeMessage(player_guid, 2, 0))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0))
sendResponse(DestroyMessage(player_guid, player_guid, PlanetSideGUID(0), pos)) //how many players get this message?
sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, player.Faction, true))
if(tplayer.VehicleSeated.nonEmpty) {
continent.GUID(tplayer.VehicleSeated.get) match {
case Some(obj : Vehicle) =>
TotalDriverVehicleControl(obj)
UnAccessContents(obj)
case _ => ;
}
//make player invisible (if not, the cadaver sticks out the side in a seated position)
sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 29, 1))
}
PlayerActionsToCancel()
//TODO other methods of death?
val pentry = PlayerSource(tplayer)
(tplayer.History.find({p => p.isInstanceOf[PlayerSuicide]}) match {
case Some(PlayerSuicide(_)) =>
None
case _ =>
tplayer.LastShot match {
case Some(shot) =>
if(System.nanoTime - shot.hit_time < (10 seconds).toNanos) {
Some(shot)
}
else {
None //suicide
}
case None =>
None //suicide
}
}) match {
case Some(shot) =>
continent.Activity ! Zone.HotSpot.Activity(pentry, shot.projectile.owner, shot.hit_pos)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DestroyDisplay(shot.projectile.owner, pentry, shot.projectile.attribute_to))
case None =>
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DestroyDisplay(pentry, pentry, 0))
}
import scala.concurrent.ExecutionContext.Implicits.global
reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7))
}
/**
* An event has occurred that would cause the player character to stop certain stateful activities.
* These activities include shooting, the weapon being drawn, hacking, accessing (a container), flying, and running.
* Other players in the same zone must be made aware that the player has stopped as well.<br>
* <br>
* Things whose configuration should not be changed:<br>
* - if the player is seated<br>
* - if the player is anchored<br>
* This is not a complete list but, for the purpose of enforcement, some pointers will be documented here.
*/
def PlayerActionsToCancel() : Unit = {
progressBarUpdate.cancel
progressBarValue = None
lastTerminalOrderFulfillment = true
accessedContainer match {
case Some(obj : Vehicle) =>
if(obj.AccessingTrunk.contains(player.GUID)) {
obj.AccessingTrunk = None
UnAccessContents(obj)
}
accessedContainer = None
case Some(_) =>
accessedContainer = None
case None => ;
}
shooting match {
case Some(guid) =>
sendResponse(ChangeFireStateMessage_Stop(guid))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, guid))
prefire = None
shooting = None
case None => ;
}
if(flying) {
sendResponse(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None))
flying = false
}
if(speed > 1) {
sendResponse(ChatMsg(ChatMessageType.CMT_SPEED, false, "", "1.000", None))
speed = 1f
}
}
/**
* A part of the process of spawning the player into the game world.
* The function should work regardless of whether the player is alive or dead - it will make them alive.
* It adds the `WSA`-current `Player` to the current zone and sends out the expected packets.<br>
* <br>
* If that player is the driver of a vehicle, it will construct the vehicle.
* If that player is the occupant of a vehicle, it will place them inside that vehicle.
* These two previous statements operate through similar though distinct mechanisms and actually imply different conditions.
* The vehicle will not be created unless the player is a living driver;
* but, the second statement will always trigger so long as the player is in a vehicle.
* The first produces a version of the player more suitable to be "another player in the game," and not "the avatar."
* The second would write over the product of the first to produce "the avatar."
* The vehicle should only be constructed once as, if it created a second time, that distinction will become lost.
* @see `BeginZoningMessage`
* @see `CargoMountBehaviorForOthers`
* @see `AvatarCreateInVehicle`
* @see `GetKnownVehicleAndSeat`
* @see `LoadZoneTransferPassengerMessages`
* @see `Player.Spawn`
* @see `ReloadVehicleAccessPermissions`
* @see `TransportVehicleChannelName`
*/
def AvatarCreate() : Unit = {
val health = player.Health
val armor = player.Armor
val stamina = player.Stamina
player.Spawn
if(health != 0) {
player.Health = health
player.Armor = armor
player.Stamina = stamina
}
GetKnownVehicleAndSeat() match {
case (Some(vehicle : Vehicle), Some(seat : Int)) =>
//vehicle and driver/passenger
interstellarFerry = None
val vdef = vehicle.Definition
val data = vdef.Packet.ConstructorData(vehicle).get
val guid = vehicle.GUID
sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, guid, data))
ReloadVehicleAccessPermissions(vehicle)
//if the vehicle is the cargo of another vehicle in this zone
val carrierInfo = continent.GUID(vehicle.MountedIn) match {
case Some(carrier : Vehicle) =>
(Some(carrier), carrier.CargoHolds.find({ case (index, hold) => hold.Occupant.contains(vehicle)}))
case _ =>
(None, None)
}
player.VehicleSeated = guid
if(seat == 0) {
//if driver
OwnVehicle(vehicle, player)
vehicle.CargoHolds.values
.collect { case hold if hold.isOccupied => hold.Occupant.get }
.foreach { _.MountedIn = guid }
continent.Transport ! Zone.Vehicle.Spawn(vehicle)
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, guid, data))
carrierInfo match {
case (Some(carrier), Some((index, _))) =>
CargoMountBehaviorForOthers(carrier, vehicle, index)
case _ =>
vehicle.MountedIn = None
}
}
else {
//if passenger;
//meant for same-zone warping; when player changes zones, redundant information will be sent
carrierInfo match {
case (Some(carrier), Some((index, _))) =>
CargoMountBehaviorForUs(carrier, vehicle, index)
case _ => ;
}
}
log.info(s"AvatarCreate (vehicle): $guid -> $data")
//player, passenger
AvatarCreateInVehicle(player, vehicle, seat)
case _ =>
player.VehicleSeated = None
val packet = player.Definition.Packet
val data = packet.DetailedConstructorData(player).get
val guid = player.GUID
sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.LoadPlayer(guid, ObjectClass.avatar, guid, packet.ConstructorData(player).get, None))
log.info(s"AvatarCreate: $guid -> $data")
}
continent.Population ! Zone.Population.Spawn(avatar, player)
//cautious redundancy
deadState = DeadState.Alive
}
/**
* If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat.<br>
* <br>
* 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 `interstellarFerry`
* @return a tuple consisting of a vehicle reference and a seat 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]) =
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle : Vehicle) =>
vehicle.PassengerInSeat(player) match {
case index @ Some(_) =>
(Some(vehicle), index)
case None =>
(None, None)
}
case _ =>
(None, None)
}
/**
* If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat.
* @return a tuple consisting of a vehicle reference and a seat 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]) =
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) =>
vehicle.PassengerInSeat(player) match {
case index @ Some(_) =>
(Some(vehicle), index)
case None =>
(None, None)
}
case _ =>
(None, None)
}
/**
* Create an avatar character as if that avatar's player is mounted in a vehicle object's seat.<br>
* <br>
* This is a very specific configuration of the player character that is not visited very often.
* The value of `player.VehicleSeated` should be set to accommodate `Packet.DetailedConstructorData` and,
* though not explicitly checked,
* should be the same as the globally unique identifier that is assigned to the `vehicle` parameter for the current zone.
* The priority of this function is consider "initial" so it introduces the avatar to the game world in this state
* and is permitted to introduce the avatar to the vehicle's internal settings in a similar way.
* @see `AccessContents`
* @see `UpdateWeaponAtSeatPosition`
* @param tplayer the player avatar seated in the vehicle's seat
* @param vehicle the vehicle the player is driving
* @param seat the seat index
*/
def AvatarCreateInVehicle(tplayer : Player, vehicle : Vehicle, seat : Int) : Unit = {
val pdef = tplayer.Definition
val guid = player.GUID
val parent = ObjectCreateMessageParent(vehicle.GUID, seat)
val data = pdef.Packet.DetailedConstructorData(player).get
sendResponse(
ObjectCreateDetailedMessage(
pdef.ObjectId,
guid,
parent,
data
)
)
avatarService ! AvatarServiceMessage(vehicle.Continent, AvatarAction.LoadPlayer(guid, pdef.ObjectId, guid, pdef.Packet.ConstructorData(player).get, Some(parent)))
AccessContents(vehicle)
UpdateWeaponAtSeatPosition(vehicle, seat)
log.info(s"AvatarCreateInVehicle: $guid -> $data")
}
/**
* Produce a clone of the player that is equipped with the default infantry loadout.
* The loadout is hardcoded.
* The player is expected to be in a Standard Exo-Suit.
* @param tplayer the original player
* @return the duplication of the player, in Standard Exo-Suit and with default equipment loadout
*/
def RespawnClone(tplayer : Player) : Player = {
val faction = tplayer.Faction
val obj = Player.Respawn(tplayer)
obj.Slot(0).Equipment = Tool(StandardPistol(faction))
obj.Slot(2).Equipment = Tool(suppressor)
obj.Slot(4).Equipment = Tool(StandardMelee(faction))
obj.Slot(6).Equipment = AmmoBox(bullet_9mm)
obj.Slot(9).Equipment = AmmoBox(bullet_9mm)
obj.Slot(12).Equipment = AmmoBox(bullet_9mm)
obj.Slot(33).Equipment = AmmoBox(bullet_9mm_AP)
obj.Slot(36).Equipment = AmmoBox(StandardPistolAmmo(faction))
obj.Slot(39).Equipment = SimpleItem(remote_electronics_kit)
obj.Inventory.Items.foreach { _.obj.Faction = faction }
obj
}
/**
* Remove items from a deceased player that are not expected to be found on a corpse.
* Most all players have their melee slot knife (which can not be un-equipped normally) removed.
* MAX's have their primary weapon in the designated slot removed.
* @param obj the player to be turned into a corpse
*/
def FriskCorpse(obj : Player) : Unit = {
if(obj.isBackpack) {
obj.Slot(4).Equipment match {
case None => ;
case Some(knife) =>
obj.Slot(4).Equipment = None
taskResolver ! RemoveEquipmentFromSlot(obj, knife, 4)
}
obj.Slot(0).Equipment match {
case Some(arms : Tool) =>
if(GlobalDefinitions.isMaxArms(arms.Definition)) {
obj.Slot(0).Equipment = None
taskResolver ! RemoveEquipmentFromSlot(obj, arms, 0)
}
case _ => ;
}
//disown boomers and drop triggers
val boomers = avatar.Deployables.ClearDeployable(DeployedItem.boomer)
boomers.foreach(boomer => {
continent.GUID(boomer) match {
case Some(obj : BoomerDeployable) =>
obj.OwnerName = None
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent))
case Some(_) | None => ;
}
})
val triggers = RemoveBoomerTriggersFromInventory()
triggers.foreach(trigger => { NormalItemDrop(obj, continent, avatarService)(trigger) })
}
}
/**
* Creates a player that has the characteristics of a corpse.
* To the game, that is a backpack (or some pastry, festive graphical modification allowing).
* @see `CorpseConverter.converter`
* @param tplayer the player
*/
def TurnPlayerIntoCorpse(tplayer : Player) : Unit = {
val guid = tplayer.GUID
sendResponse(
ObjectCreateDetailedMessage(ObjectClass.avatar, guid, CorpseConverter.converter.DetailedConstructorData(tplayer).get)
)
}
/**
* If the corpse has been well-looted, it has no items in its primary holsters nor any items in its inventory.
* @param obj the corpse
* @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack;
* `false`, otherwise
*/
def WellLootedCorpse(obj : Player) : Boolean = {
obj.isBackpack && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0
}
/**
* If the corpse has been well-looted, remove it from the ground.
* @param obj the corpse
* @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack;
* `false`, otherwise
*/
def TryDisposeOfLootedCorpse(obj : Player) : Boolean = {
if(WellLootedCorpse(obj)) {
avatarService ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent))
true
}
else {
false
}
}
/**
* Attempt to tranfer to the player's faction-specific sanctuary continent.
* If the server thinks the player is already on his sanctuary continent, and dead,
* it will disconnect the player under the assumption that an error has occurred.
* Eventually, this functionality should support better error-handling before it jumps to the conclusion:
* "Disconnecting the client is the safest option."
* @see `Zones.SanctuaryZoneNumber`
* @param tplayer the player
* @param currentZone the current zone number
*/
def RequestSanctuaryZoneSpawn(tplayer : Player, currentZone : Int) : Unit = {
val sanctNumber = Zones.SanctuaryZoneNumber(tplayer.Faction)
if(currentZone == sanctNumber) {
if(!player.isAlive) {
sendResponse(DisconnectMessage("Player failed to load on faction's sanctuary continent. Please relog."))
}
//we are already on sanctuary, alive; what more is there to do?
}
else {
continent.GUID(player.VehicleSeated) match {
case Some(obj : Vehicle) if obj.Health > 0 =>
cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 12) //warp gates for functioning vehicles
case None =>
cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) //player character spawns
case _ =>
}
}
}
/**
* na
* @param terminal na
*/
def HandleProximityTerminalUse(terminal : Terminal with ProximityUnit) : Unit = {
val term_guid = terminal.GUID
val targets = FindProximityUnitTargetsInScope(terminal)
val currentTargets = terminal.Targets
targets.foreach(target => {
if(!currentTargets.contains(target)) {
StartUsingProximityUnit(terminal, target)
}
else if(targets.isEmpty) {
log.warn(s"HandleProximityTerminalUse: ${player.Name} could not find valid targets to give to proximity unit ${terminal.Definition.Name}@${term_guid.guid}")
}
})
}
/**
* na
* @param terminal na
* @return na
*/
def FindProximityUnitTargetsInScope(terminal : Terminal with ProximityUnit) : Seq[PlanetSideGameObject] = {
terminal.Definition.asInstanceOf[ProximityDefinition].TargetValidation.keySet collect {
case ProximityTarget.Player => Some(player)
case ProximityTarget.Vehicle | ProximityTarget.Aircraft => continent.GUID(player.VehicleSeated)
} collect {
case Some(a) => a
} toSeq
}
/**
* Queue a proximity-base service.
* @param terminal the proximity-based unit
* @param target the entity that is being considered for terminal operation
*/
def StartUsingProximityUnit(terminal : Terminal with ProximityUnit, target : PlanetSideGameObject) : Unit = {
val term_guid = terminal.GUID
if(player.isAlive) {
log.info(s"StartUsingProximityUnit: ${player.Name} wants to use ${terminal.Definition.Name}@${term_guid.guid} on $target")
target match {
case _ : Player =>
terminal.Actor ! CommonMessages.Use(player, Some(target))
case _ : Vehicle =>
terminal.Actor ! CommonMessages.Use(player, Some((target, vehicleService)))
case _ =>
log.error(s"StartUsingProximityUnit: can not deal with target $target")
}
terminal.Definition match {
case GlobalDefinitions.adv_med_terminal | GlobalDefinitions.medical_terminal =>
usingMedicalTerminal = Some(term_guid)
case _ => ;
}
}
}
/**
* Determine which functionality to pursue by a generic proximity-functional unit given the target for its activity.
* @see `VehicleService:receive, ProximityUnit.Action`
* @param terminal the proximity-based unit
* @param target the object being affected by the unit
*/
def SelectProximityUnitBehavior(terminal : Terminal with ProximityUnit, target : PlanetSideGameObject) : Unit = {
target match {
case o : Player =>
HealthAndArmorTerminal(terminal, o)
case _ => ;
}
}
/**
* Stop using a proximity-base service.
* Special note is warranted when determining the identity of the proximity terminal.
* Medical terminals of both varieties can be cancelled by movement.
* Other sorts of proximity-based units are put on a timer.
* @param terminal the proximity-based unit
*/
def StopUsingProximityUnit(terminal : Terminal with ProximityUnit) : Unit = {
val term_guid = terminal.GUID
log.info(s"StopUsingProximityUnit: attempting to stop using proximity unit ${terminal.Definition.Name}@${term_guid.guid}")
val targets = FindProximityUnitTargetsInScope(terminal)
if(targets.nonEmpty) {
if(usingMedicalTerminal.contains(term_guid)) {
usingMedicalTerminal = None
}
targets.foreach(target =>
terminal.Actor ! CommonMessages.Unuse(player, Some(target))
)
}
else {
log.warn(s"StopUsingProximityUnit: ${player.Name} could not find valid targets for proximity unit ${terminal.Definition.Name}@${term_guid.guid}")
}
}
/**
* na
*/
def ForgetAllProximityTerminals(term_guid : PlanetSideGUID) : Unit = {
if(usingMedicalTerminal.contains(term_guid)) {
usingMedicalTerminal = None
}
}
/**
* Cease all current interactions with proximity-based units.
* Pair with `PlayerActionsToCancel`, except when logging out (stopping).
* This operations may invoke callback messages.
* @see `postStop`
*/
def CancelAllProximityUnits() : Unit = {
continent.GUID(usingMedicalTerminal) match {
case Some(terminal : Terminal with ProximityUnit) =>
FindProximityUnitTargetsInScope(terminal).foreach(target =>
terminal.Actor ! CommonMessages.Unuse(player, Some(target))
)
ForgetAllProximityTerminals(usingMedicalTerminal.get)
case _ => ;
}
}
/**
* When standing on the platform of a(n advanced) medical terminal,
* resotre the player's health and armor points (when they need their health and armor points restored).
* If the player is both fully healed and fully repaired, stop using the terminal.
* @param unit the medical terminal
* @param target the player being healed
*/
def HealthAndArmorTerminal(unit : Terminal with ProximityUnit, target : Player) : Unit = {
val medDef = unit.Definition.asInstanceOf[MedicalTerminalDefinition]
val healAmount = medDef.HealAmount
val healthFull : Boolean = if(healAmount != 0 && target.Health < target.MaxHealth) {
target.History(HealFromTerm(PlayerSource(target), healAmount, 0, medDef))
HealAction(target, healAmount)
}
else {
true
}
val repairAmount = medDef.ArmorAmount
val armorFull : Boolean = if(repairAmount != 0 && target.Armor < target.MaxArmor) {
target.History(HealFromTerm(PlayerSource(target), 0, repairAmount, medDef))
ArmorRepairAction(target, repairAmount)
}
else {
true
}
if(healthFull && armorFull) {
StopUsingProximityUnit(unit)
}
}
/**
* Restore, at most, a specific amount of health points on a player.
* Send messages to connected client and to events system.
* @param tplayer the player
* @param healValue the amount to heal;
* 10 by default
* @return whether the player can be repaired for any more health points
*/
def HealAction(tplayer : Player, healValue : Int = 10) : Boolean = {
val player_guid = tplayer.GUID
tplayer.Health = tplayer.Health + healValue
sendResponse(PlanetsideAttributeMessage(player_guid, 0, tplayer.Health))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, tplayer.Health))
tplayer.Health == tplayer.MaxHealth
}
/**
* Restore, at most, a specific amount of personal armor points on a player.
* Send messages to connected client and to events system.
* @param tplayer the player
* @param repairValue the amount to repair;
* 10 by default
* @return whether the player can be repaired for any more armor points
*/
def ArmorRepairAction(tplayer : Player, repairValue : Int = 10) : Boolean = {
val player_guid = tplayer.GUID
tplayer.Armor = tplayer.Armor + repairValue
sendResponse(PlanetsideAttributeMessage(player_guid, 4, tplayer.Armor))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 4, tplayer.Armor))
tplayer.Armor == tplayer.MaxArmor
}
/**
* This function is applied to vehicles that are leaving a cargo vehicle's cargo hold to auto reverse them out
* Lock all applicable controls of the current vehicle
* Set the vehicle to move in reverse
*/
def ServerVehicleLockReverse() : Unit = {
controlled = Some(0)
sendResponse(ServerVehicleOverrideMsg(lock_accelerator = true, lock_wheel = true, reverse = true, unk4 = true, lock_vthrust = 0, lock_strafe = 1, movement_speed = 2, unk8 = Some(0)))
}
/**
* This function is applied to vehicles that are leaving a cargo vehicle's cargo hold to strafe right out of the cargo hold for vehicles that are mounted sideways e.g. router/BFR
* Lock all applicable controls of the current vehicle
* Set the vehicle to strafe right
*/
def ServerVehicleLockStrafeRight() : Unit = {
controlled = Some(0)
sendResponse(ServerVehicleOverrideMsg(lock_accelerator = true, lock_wheel = true, reverse = false, unk4 = true, lock_vthrust = 0, lock_strafe = 3, movement_speed = 0, unk8 = Some(0)))
}
/**
* This function is applied to vehicles that are leaving a cargo vehicle's cargo hold to strafe left out of the cargo hold for vehicles that are mounted sideways e.g. router/BFR
* Lock all applicable controls of the current vehicle
* Set the vehicle to strafe left
*/
def ServerVehicleLockStrafeLeft() : Unit = {
controlled = Some(0)
sendResponse(ServerVehicleOverrideMsg(lock_accelerator = true, lock_wheel = true, reverse = false, unk4 = true, lock_vthrust = 0, lock_strafe = 2, movement_speed = 0, unk8 = Some(0)))
}
/**
* Lock all applicable controls of the current vehicle.
* This includes forward motion, turning, and, if applicable, strafing.
* @param vehicle the vehicle being controlled
*/
def ServerVehicleLock(vehicle : Vehicle) : Unit = {
controlled = Some(0)
sendResponse(ServerVehicleOverrideMsg(true, true, false, false, 0, 1, 0, Some(0)))
}
/**
* Place the current vehicle under the control of the server's commands.
* @param vehicle the vehicle
* @param speed how fast the vehicle is moving forward
* @param flight whether the vehicle is ascending or not, if the vehicle is an applicable type
*/
def ServerVehicleOverride(vehicle : Vehicle, speed : Int = 0, flight : Int = 0) : Unit = {
controlled = Some(speed)
sendResponse(ServerVehicleOverrideMsg(true, true, false, false, flight, 0, speed, Some(0)))
}
/**
* Place the current vehicle under the control of the driver's commands,
* but leave it in a cancellable auto-drive.
* @param vehicle the vehicle
* @param speed how fast the vehicle is moving forward
* @param flight whether the vehicle is ascending or not, if the vehicle is an applicable type
*/
def DriverVehicleControl(vehicle : Vehicle, speed : Int = 0, flight : Int = 0) : Unit = {
if(controlled.nonEmpty) {
controlled = None
sendResponse(ServerVehicleOverrideMsg(false, false, false, true, flight, 0, speed, None))
}
}
/**
* Place the current vehicle under the control of the driver's commands,
* but leave it in a cancellable auto-drive.
* Stop all movement entirely.
* @param vehicle the vehicle
*/
def TotalDriverVehicleControl(vehicle : Vehicle) : Unit = {
if(controlled.nonEmpty) {
controlled = None
sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None))
}
}
/**
* Given a globally unique identifier in the 40100 to 40124 range
* (with an optional 25 as buffer),
* find a projectile.
* @param projectile_guid the projectile's GUID
* @return the discovered projectile
*/
def FindProjectileEntry(projectile_guid : PlanetSideGUID) : Option[Projectile] = {
val index = projectile_guid.guid - Projectile.BaseUID
if(0 <= index && index < projectiles.length) {
projectiles(index)
}
else {
log.warn(s"ResolveProjectile: 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.
* 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 ResolveProjectileEntry(projectile_guid : PlanetSideGUID, resolution : ProjectileResolution.Value, target : PlanetSideGameObject with FactionAffinity with Vitality, pos : Vector3) : Option[ResolvedProjectile] = {
FindProjectileEntry(projectile_guid) match {
case Some(projectile) =>
val index = projectile_guid.guid - Projectile.BaseUID
ResolveProjectileEntry(projectile, index, resolution, target, pos)
case None =>
log.warn(s"ResolveProjectile: 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.
* A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle.
* The internal copy of the projectile is retained as merely `Resolved`
* while the observed projectile is promoted to the suggested resolution status.
* @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 ResolveProjectileEntry(projectile : Projectile, index : Int, resolution : ProjectileResolution.Value, target : PlanetSideGameObject with FactionAffinity with Vitality, pos : Vector3) : Option[ResolvedProjectile] = {
if(!projectiles(index).contains(projectile)) {
log.error(s"expected projectile could not be found at $index; can not resolve")
None
}
else if(projectile.isMiss) {
log.error(s"expected projectile at $index was already counted as a missed shot; can not resolve any further")
None
}
else {
projectile.Resolve()
Some(ResolvedProjectile(resolution, projectile, SourceEntry(target), target.DamageModel, pos))
}
}
/**
* Common activities/procedure when a player mounts a valid object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the seat into which the player is mounting
*/
def MountingAction(tplayer : Player, obj : PlanetSideGameObject with Mountable, seatNum : Int) : Unit = {
val player_guid : PlanetSideGUID = tplayer.GUID
val obj_guid : PlanetSideGUID = obj.GUID
PlayerActionsToCancel()
log.info(s"MountVehicleMsg: $player_guid mounts $obj @ $seatNum")
sendResponse(ObjectAttachMessage(obj_guid, player_guid, seatNum))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.MountVehicle(player_guid, obj_guid, seatNum))
}
/**
* Common activities/procedure when a player dismounts a valid object.
* @param tplayer the player
* @param obj the mountable object
* @param seatNum the seat out of which which the player is disembarking
*/
def DismountAction(tplayer : Player, obj : PlanetSideGameObject with Mountable, seatNum : Int) : Unit = {
val player_guid : PlanetSideGUID = tplayer.GUID
log.info(s"DismountVehicleMsg: ${tplayer.Name} dismounts $obj from $seatNum")
sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, false))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DismountVehicle(player_guid, BailType.Normal, false))
}
/**
* Calculate the amount of damage to be dealt to an active `target`
* using the information reconstructed from a `Resolvedprojectile`
* and affect the `target` in a synchronized manner.
* The active `target` and the target of the `ResolvedProjectile` do not have be the same.
* @see `DamageResistanceModel`<br>
* `Vitality`
* @param target a valid game object that is known to the server
* @param data a projectile that will affect the target
*/
def HandleDealingDamage(target : PlanetSideGameObject with Vitality, data : ResolvedProjectile) : Unit = {
val func = data.damage_model.Calculate(data)
target match {
case obj : Player =>
//damage is synchronized on the target player's `WSA` (results distributed from there)
avatarService ! AvatarServiceMessage(obj.Name, AvatarAction.Damage(player.GUID, obj, func))
case obj : Vehicle =>
//damage is synchronized on the vehicle actor (results returned to and distributed from this `WSA`)
obj.Actor ! Vitality.Damage(func)
case obj : Deployable =>
//damage is synchronized on `LSA` (results returned to and distributed from this `WSA`)
localService ! Vitality.DamageOn(obj, func)
case obj : FacilityTurret =>
//damage is synchronized on the turret actor (results returned to and distributed from this `WSA`)
obj.Actor ! Vitality.Damage(func)
case _ => ;
}
}
/**
* Properly format a `DestroyDisplayMessage` packet
* given sufficient information about a target (victim) and an actor (killer).
* For the packet, the `charId` field is important for determining distinction between players.
* @param killer the killer's entry
* @param victim the victim's entry
* @param method the manner of death
* @param unk na;
* defaults to 121, the object id of `avatar`
* @return a `DestroyDisplayMessage` packet that is properly formatted
*/
def DestroyDisplayMessage(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) : DestroyDisplayMessage = {
val killer_seated = killer match {
case obj : PlayerSource => obj.Seated
case _ => false
}
val victim_seated = victim match {
case obj : PlayerSource => obj.Seated
case _ => false
}
new DestroyDisplayMessage(
killer.Name, killer.CharId, killer.Faction, killer_seated,
unk, method,
victim.Name, victim.CharId, victim.Faction, victim_seated
)
}
/**
* Initialize the deployables backend information.
* @param avatar the player's core
*/
def InitializeDeployableQuantities(avatar : Avatar) : Unit = {
log.info("Setting up combat engineering ...")
avatar.Deployables.Initialize(avatar.Certifications.toSet)
}
/**
* Initialize the UI elements for deployables.
* @param avatar the player's core
*/
def InitializeDeployableUIElements(avatar : Avatar) : Unit = {
log.info("Setting up combat engineering UI ...")
UpdateDeployableUIElements(avatar.Deployables.UpdateUI())
}
/**
* The player learned a new certification.
* Update the deployables user interface elements if it was an "Engineering" certification.
* The certification "Advanced Hacking" also relates to an element.
* @param certification the certification that was added
* @param certificationSet all applicable certifications
*/
def AddToDeployableQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = {
avatar.Deployables.AddToDeployableQuantities(certification, certificationSet)
UpdateDeployableUIElements(avatar.Deployables.UpdateUI(certification))
}
/**
* The player forgot a certification he previously knew.
* Update the deployables user interface elements if it was an "Engineering" certification.
* The certification "Advanced Hacking" also relates to an element.
* @param certification the certification that was added
* @param certificationSet all applicable certifications
*/
def RemoveFromDeployablesQuantities(certification : CertificationType.Value, certificationSet : Set[CertificationType.Value]) : Unit = {
avatar.Deployables.RemoveFromDeployableQuantities(certification, certificationSet)
UpdateDeployableUIElements(avatar.Deployables.UpdateUI(certification))
}
/**
* Initialize the deployables user interface elements.<br>
* <br>
* All element initializations require both the maximum deployable amount and the current deployables active counts.
* Until initialized, all elements will be RED 0/0 as if the cooresponding certification were not `learn`ed.
* The respective element will become a pair of numbers, the second always being non-zero, when properly initialized.
* The numbers will appear GREEN when more deployables of that type can be placed.
* The numbers will appear RED if the player can not place any more of that type of deployable.
* The numbers will appear YELLOW if the current deployable count is greater than the maximum count of that type
* such as may be the case when a player `forget`s a certification.
* @param list a tuple of each UI element with four numbers;
* even numbers are attribute ids;
* odd numbers are quantities;
* first pair is current quantity;
* second pair is maximum quantity
*/
def UpdateDeployableUIElements(list : List[(Int,Int,Int,Int)]) : Unit = {
val guid = PlanetSideGUID(0)
list.foreach({ case((currElem, curr, maxElem, max)) =>
//fields must update in ordered pairs: max, curr
sendResponse(PlanetsideAttributeMessage(guid, maxElem, max))
sendResponse(PlanetsideAttributeMessage(guid, currElem, curr))
})
}
/**
* Draw the icon for this deployable object.<br>
* <br>
* When a client first joins a zone, all deployables are drawn on the continent map once.
* Should the player place any deployables, those deployables belong to that player.
* Ownership causes icon to be drawn in yellow to the player (as opposed to a white icon)
* and that signifies a certain level of control over the deployable, at least the ability to quietly deconstruct it.
* Under normal death/respawn cycles while the player is in a given zone,
* the map icons for owned deployables ramin manipulable to that given user.
* They do not havwe to be redrawn to stay accurate.
* Upon leaving a zone, where the icons are erased, and returning back to the zone, where they are drawn again,
* the deployables that a player owned should be restored in terms of their map icon visibility.
* This control can not be recovered, however, until they are updated with the player's globally unique identifier.
* Since the player does not need to redraw his own deployable icons each time he respawns,
* but will not possess a valid GUID for that zone until he spawns in it at least once,
* this function is swapped with another after the first spawn in any given zone.
* This function is restored upon transferring zones.
* @see `SetCurrentAvatar`<br>
* `DontRedrawIcons`
* @param obj a `Deployable` object
*/
def RedrawDeployableIcons(obj : PlanetSideGameObject with Deployable) : Unit = {
val deployInfo = DeployableInfo(obj.GUID, Deployable.Icon(obj.Definition.Item), obj.Position, obj.Owner.get)
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
}
/**
* Do not draw any icon for this deployable object.<br>
* <br>
* When a client first joins a zone, all deployables are drawn on the continent map once.
* Should the player place any deployables, those deployables belong to that player.
* Ownership causes icon to be drawn in yellow to the player (as opposed to a white icon)
* and that signifies a certain level of control over the deployable, at least the ability to quietly deconstruct it.
* Under normal death/respawn cycles while the player is in a given zone,
* the map icons for owned deployables remain manipulable by that given user.
* They do not have to be redrawn to stay accurate.
* Upon leaving a zone, where the icons are erased, and returning back to the zone, where they are drawn again,
* the deployables that a player owned should be restored in terms of their map icon visibility.
* This control can not be recovered, however, until they are updated with the player's globally unique identifier.
* Since the player does not need to redraw his own deployable icons each time he respawns,
* but will not possess a valid GUID for that zone until he spawns in it at least once,
* this function swaps out with another after the first spawn in any given zone.
* It stays swapped in until the player changes zones.
* @see `SetCurrentAvatar`<br>
* `RedrawDeployableIcons`
* @param obj a `Deployable` object
*/
def DontRedrawIcons(obj : PlanetSideGameObject with Deployable) : Unit = { }
/**
* The custom behavior responding to the message `ChangeFireModeMessage` for `ConstructionItem` game objects.
* Each fire mode has sub-modes corresponding to a type of "deployable" as ammunition
* and each of these sub-modes have certification requirements that must be met before they can be used.
* Additional effort is exerted to ensure that the requirements for the given mode and given sub-mode are satisfied.
* If no satisfactory combination is achieved, the original state will be restored.
* @see `PerformConstructionItemAmmoChange`<br>
* `FireModeSwitch.NextFireMode`
* @param obj the `ConstructionItem` object
* @param originalModeIndex the starting point fire mode index
* @return the changed fire mode
*/
def NextConstructionItemFireMode(obj : ConstructionItem, originalModeIndex : Int) : ConstructionFireMode = {
val certifications = player.Certifications
do {
obj.NextFireMode
if(!ConstructionItemPermissionComparison(certifications, obj.ModePermissions)) {
PerformConstructionItemAmmoChange(obj, obj.AmmoTypeIndex)
}
sendResponse(ChangeFireModeMessage(obj.GUID, obj.FireModeIndex))
}
while(!ConstructionItemPermissionComparison(certifications, obj.ModePermissions) && originalModeIndex != obj.FireModeIndex)
obj.FireMode
}
/**
* The custom behavior responding to the message `ChangeAmmoMessage` for `ConstructionItem` game objects.
* Iterate through sub-modes corresponding to a type of "deployable" as ammunition for this fire mode
* and check each of these sub-modes for their certification requirements to be met before they can be used.
* Additional effort is exerted to ensure that the requirements for the given ammunition are satisfied.
* If no satisfactory combination is achieved, the original state will be restored.
* @param obj the `ConstructionItem` object
* @param originalModeIndex the starting point ammunition type mode index
*/
def PerformConstructionItemAmmoChange(obj : ConstructionItem, originalAmmoIndex : Int) : Unit = {
val certifications = player.Certifications
do {
obj.NextAmmoType
}
while(!ConstructionItemPermissionComparison(certifications, obj.ModePermissions) && originalAmmoIndex != obj.AmmoTypeIndex)
log.info(s"ChangeFireMode: construction object ${obj.Definition.Name} changed to ${obj.AmmoType} (mode ${obj.FireModeIndex})")
sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex))
}
/**
* Compare sets of certifications to determine if
* the requested `Engineering`-like certification requirements of the one group can be found in a another group.
* @see `CertificationType`
* @param sample the certifications to be compared against
* @param test the desired certifications
* @return `true`, if the desired certification requirements are met; `false`, otherwise
*/
def ConstructionItemPermissionComparison(sample : Set[CertificationType.Value], test : Set[CertificationType.Value]) : Boolean = {
import CertificationType._
val engineeringCerts : Set[CertificationType.Value] = Set(AssaultEngineering, FortificationEngineering)
val testDiff : Set[CertificationType.Value] = test diff (engineeringCerts ++ Set(AdvancedEngineering))
//substitute `AssaultEngineering` and `FortificationEngineering` for `AdvancedEngineering`
val sampleIntersect = if(sample contains AdvancedEngineering) {
engineeringCerts
}
else {
sample intersect engineeringCerts
}
val testIntersect = if(test contains AdvancedEngineering) {
engineeringCerts
}
else {
test intersect engineeringCerts
}
(sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect)
}
/**
* Common actions related to constructing a new `Deployable` object in the game environment.<br>
* <br>
* Besides the standard `ObjectCreateMessage` packet that produces the model and game object on the client,
* two messages are dispatched in accordance with enforced deployable limits.
* The first limit of note is the actual number of a specific type of deployable can be placed.
* The second limit of note is the actual number of a specific group (category) of deployables that can be placed.
* For example, the player can place 25 mines but that count adds up all types of mines;
* specific mines have individual limits such as 25 and 5 and only that many of that type can be placed at once.
* Depending on which limit is encountered, an "oldest entry" is struck from the list to make space.
* This generates the first message - "@*OldestDestroyed."
* The other message is generated if the number of that specific type of deployable
* or the number of deployables available in its category
* matches against the maximum count allowed.
* This generates the second message - "@*LimitReached."
* These messages are mutually exclusive, with "@*OldestDestroyed" taking priority over "@*LimitReached."<br>
* <br>
* The map icon for the deployable just introduced is also created on the clients of all faction-affiliated players.
* This icon is important as, short of destroying it,
* the owner has no other means of controlling the created object that it is associated with.
* @param obj the `Deployable` object to be built
*/
def DeployableBuildActivity(obj : PlanetSideGameObject with Deployable) : Unit = {
val guid = obj.GUID
val definition = obj.Definition
val item = definition.Item
val deployables = avatar.Deployables
val (curr, max) = deployables.CountDeployable(item)
log.info(s"DeployableBuildActivity: ${definition.Name}")
//two potential messages related to numerical limitations of deployables
if(!avatar.Deployables.Available(obj)) {
val (removed, msg) = {
if(curr == max) { //too many of a specific type of deployable
(deployables.DisplaceFirst(obj), max > 1)
}
else { //make room by eliminating a different type of deployable
(deployables.DisplaceFirst(obj, { d => d.Definition.Item != item }), true)
}
}
removed match {
case Some(telepad : TelepadDeployable) =>
telepad.AssignOwnership(None)
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, continent, Some(0 seconds))) //normal decay
case Some(old) =>
old.AssignOwnership(None)
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(old), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(old, continent, Some(0 seconds)))
if(msg) { //max test
sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None))
}
case None => ; //should be an invalid case
log.warn(s"DeployableBuildActivity: how awkward: we probably shouldn't be allowed to build this deployable right now")
}
}
else if(obj.isInstanceOf[TelepadDeployable]) {
//always treat the telepad we are putting down as the first and only one
sendResponse(ObjectDeployedMessage.Success(definition.Name, 1, 1))
}
else {
sendResponse(ObjectDeployedMessage.Success(definition.Name, curr + 1, max))
val (catCurr, catMax) = deployables.CountCategory(item)
if((max > 1 && curr + 1 == max) || (catMax > 1 && catCurr + 1 == catMax)) {
sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}LimitReached", None))
}
}
avatar.Deployables.Add(obj)
UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(item))
sendResponse(GenericObjectActionMessage(guid, 84)) //reset build cooldown
sendResponse(ObjectCreateMessage(definition.ObjectId, guid, definition.Packet.ConstructorData(obj).get))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DeployItem(player.GUID, obj))
//map icon
val deployInfo = DeployableInfo(guid, Deployable.Icon(item), obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0)))
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, deployInfo))
localService ! LocalServiceMessage(s"${continent.Id}/${player.Faction}", LocalAction.DeployableMapIcon(player.GUID, DeploymentAction.Build, deployInfo))
}
/**
* If the tool is a form of field deployment unit (FDU, also called an `advanced_ace`),
* completely remove the object from its current position and place it on the ground.
* In the case of a botched deployable construction, dropping the FDU is visually consistent
* as it should already be depicted as on the ground as a part of its animation cycle.
* @param tool the `ConstructionItem` object currently in the slot (checked)
* @param index the slot index
* @param pos where to drop the object in the game world
*/
def TryDropConstructionTool(tool : ConstructionItem, index : Int, pos : Vector3) : Unit = {
if(tool.Definition == GlobalDefinitions.advanced_ace &&
SafelyRemoveConstructionItemFromSlot(tool, index, "TryDropConstructionTool")) {
continent.Ground ! Zone.Ground.DropItem(tool, pos, Vector3.Zero)
}
}
/**
* Destroy a `ConstructionItem` object that can be found in the indexed slot.
* @see `Player.Find`
* @param tool the `ConstructionItem` object currently in the slot (checked)
* @param index the slot index
*/
def CommonDestroyConstructionItem(tool : ConstructionItem, index : Int) : Unit = {
if(SafelyRemoveConstructionItemFromSlot(tool, index, "CommonDestroyConstructionItem")) {
taskResolver ! GUIDTask.UnregisterEquipment(tool)(continent.GUID)
}
}
/**
* Find the target `ConstructionTool` object, either at the suggested slot or wherever it is on the `player`,
* and remove it from the game world visually.<br>
* <br>
* Not finding the target object at its intended slot is an entirely recoverable situation
* as long as the target object is discovered to be somewhere else in the player's holsters or inventory space.
* If found after a more thorough search, merely log the discrepancy as a warning.
* If the discrepancy becomes common, the developer messed up the function call
* or he should not be using this function.
* @param tool the `ConstructionItem` object currently in the slot (checked)
* @param index the slot index
* @param logDecorator what kind of designation to give any log entires originating from this function;
* defaults to its own function name
* @return `true`, if the target object was found and removed;
* `false`, otherwise
*/
def SafelyRemoveConstructionItemFromSlot(tool : ConstructionItem, index : Int, logDecorator : String = "SafelyRemoveConstructionItemFromSlot") : Boolean = {
if({
val holster = player.Slot(index)
if(holster.Equipment.contains(tool)) {
holster.Equipment = None
true
}
else {
player.Find(tool) match {
case Some(newIndex) =>
log.warn(s"$logDecorator: looking for item in $index, but item was found at $newIndex instead")
player.Slot(newIndex).Equipment = None
true
case None =>
log.error(s"$logDecorator: could not find the target ${tool.Definition.Name}")
false
}
}
}) {
sendResponse(ObjectDeleteMessage(tool.GUID, 0))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, tool.GUID))
true
}
else {
false
}
}
/**
* Find a `ConstructionItem` object in player's inventory
* that is the same type as a target `ConstructionItem` object and
* transfer it into the designated slot index, usually a holster.
* Draw that holster.
* After being transferred, the replacement should be reconfigured to match the fire mode of the original.
* The primary use of this operation is following the successful manifestation of a deployable in the game world.<br>
* <br>
* As this function should be used in response to some other action such as actually placing a deployable,
* do not instigate bundling from within the function's scope.
* @see `WorldSessionActor.FinalizeDeployable`<br>
* `FindEquipmentStock`
* @param tool the `ConstructionItem` object to match
* @param index where to put the discovered replacement
*/
def FindReplacementConstructionItem(tool : ConstructionItem, index : Int) : Unit = {
val fireMode = tool.FireModeIndex
val ammoType = tool.AmmoTypeIndex
val definition = tool.Definition
if(player.Slot(index).Equipment.isEmpty) {
FindEquipmentStock(player, { (e) => e.Definition == definition }, 1) match {
case x :: _ =>
val guid = player.GUID
val obj = x.obj.asInstanceOf[ConstructionItem]
if((player.Slot(index).Equipment = obj).contains(obj)) {
player.Inventory -= x.start
sendResponse(ObjectAttachMessage(guid, obj.GUID, index))
if(obj.FireModeIndex != fireMode) {
obj.FireModeIndex = fireMode
sendResponse(ChangeFireModeMessage(obj.GUID, fireMode))
}
if(obj.AmmoTypeIndex != ammoType) {
obj.AmmoTypeIndex = ammoType
sendResponse(ChangeAmmoMessage(obj.GUID, ammoType))
}
if(player.VisibleSlots.contains(index)) {
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(guid, guid, index, obj))
if(player.DrawnSlot == Player.HandsDownSlot) {
player.DrawnSlot = index
sendResponse(ObjectHeldMessage(guid, index, false))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(guid, index))
}
}
}
case Nil => ; //no replacements found
}
}
else {
log.warn(s"FindReplacementConstructionItem: slot $index needs to be empty before a replacement ${definition.Name} can be installed")
}
}
/**
* 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 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`<br>
* `Zone.ItemIs.Where`
* @param object_guid the target object's globally unique identifier;
* it is not expected that the object will be unregistered, but it is also not gauranteed
* @param obj the target object
* @return `true`, if the target object was discovered and removed;
* `false`, otherwise
*/
def FindEquipmentToDelete(object_guid : PlanetSideGUID, obj : Equipment) : Boolean = {
val findFunc : PlanetSideGameObject with Container => Option[(PlanetSideGameObject with Container, Option[Int])] =
FindInLocalContainer(object_guid)
findFunc(player.Locker)
.orElse(findFunc(player))
.orElse(accessedContainer match {
case Some(parent) =>
findFunc(parent)
case None =>
None
})
.orElse(FindLocalVehicle match {
case Some(parent) =>
findFunc(parent)
case None =>
None
})
match {
case Some((parent, Some(slot))) =>
obj.Position = Vector3.Zero
taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot)
log.info(s"RequestDestroy: equipment $obj")
true
case _ =>
if(continent.EquipmentOnGround.contains(obj)) {
obj.Position = Vector3.Zero
continent.Ground ! Zone.Ground.RemoveItem(object_guid)
avatarService ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), object_guid))
log.info(s"RequestDestroy: equipment $obj on ground")
true
}
else {
log.warn(s"RequestDestroy: equipment $obj exists, but can not be reached")
false
}
}
}
/**
* Common behavior for deconstructing expended explosive 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
*/
def DeconstructDeployable(obj : PlanetSideGameObject with Deployable, guid : PlanetSideGUID, pos : Vector3) : Unit = {
StartBundlingPackets()
sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle
sendResponse(ObjectDeleteMessage(guid, 0))
if(player.Faction == obj.Faction) {
sendResponse(
DeployableObjectsInfoMessage(
DeploymentAction.Dismiss,
DeployableInfo(guid, Deployable.Icon(obj.Definition.Item), pos, obj.Owner.getOrElse(PlanetSideGUID(0)))
)
)
}
StopBundlingPackets()
}
/**
* 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 : PlanetSideGameObject with Deployable, guid : PlanetSideGUID, pos : Vector3, orient : Vector3, deletionType : Int) : Unit = {
StartBundlingPackets()
sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL)) //for some, removes the green marker circle
sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
sendResponse(ObjectDeleteMessage(guid, deletionType))
if(player.Faction == obj.Faction) {
sendResponse(
DeployableObjectsInfoMessage(
DeploymentAction.Dismiss,
DeployableInfo(guid, Deployable.Icon(obj.Definition.Item), pos, obj.Owner.getOrElse(PlanetSideGUID(0)))
)
)
}
StopBundlingPackets()
}
/**
* Distribute information that a deployable has been destroyed.
* The deployable may not have yet been eliminated from the game world (client or server),
* but its health is zero and it has entered the conditions where it is nearly irrelevant.<br>
* <br>
* The typical use case of this function involves destruction via weapon fire, attributed to a particular player.
* Contrast this to simply destroying a deployable by being the deployable's owner and using the map icon controls.
* This function eventually invokes the same routine
* but mainly goes into effect when the deployable has been destroyed
* and may still leave a physical component in the game world to be cleaned up later.
* That is the task `EliminateDeployable` performs.
* Additionally, since the player who destroyed the deployable isn't necessarily the owner,
* and the real owner will still be aware of the existence of the deployable,
* that player must be informed of the loss of the deployable directly.
* @see `DeployableRemover`
* @see `Vitality.DamageResolution`
* @see `LocalResponse.EliminateDeployable`
* @see `DeconstructDeployable`
* @param target the deployable that is destroyed
* @param time length of time that the deployable is allowed to exist in the game world;
* `None` indicates the normal un-owned existence time (180 seconds)
*/
def AnnounceDestroyDeployable(target : PlanetSideGameObject with Deployable, time : Option[FiniteDuration]) : Unit = {
target.OwnerName match {
case Some(owner) =>
target.OwnerName = None
localService ! LocalServiceMessage(owner, LocalAction.AlertDestroyDeployable(PlanetSideGUID(0), target))
case None => ;
}
localService ! LocalServiceMessage(s"${continent.Id}/${target.Faction}", LocalAction.DeployableMapIcon(
PlanetSideGUID(0),
DeploymentAction.Dismiss,
DeployableInfo(target.GUID, Deployable.Icon(target.Definition.Item), target.Position, PlanetSideGUID(0)))
)
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, continent, time))
}
/**
* Search through the player's holsters and their inventory space
* and remove all `BoomerTrigger` objects, both functionally and visually.
* @return all discovered `BoomTrigger` objects
*/
def RemoveBoomerTriggersFromInventory() : List[BoomerTrigger] = {
val player_guid = player.GUID
val holstersWithIndex = player.Holsters().zipWithIndex
((player.Inventory.Items.collect({ case InventoryItem(obj : BoomerTrigger, index) => (obj, index) })) ++
(holstersWithIndex
.map({ case ((slot, index)) => (slot.Equipment, index) })
.collect { case ((Some(obj : BoomerTrigger), index)) => (obj, index) }
)
)
.map({ case ((obj, index)) =>
player.Slot(index).Equipment = None
sendResponse(ObjectDeleteMessage(obj.GUID, 0))
if(player.VisibleSlots.contains(index)) {
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, obj.GUID))
}
obj
})
}
/**
* Collect all deployables previously owned by the player,
* dissociate the avatar's globally unique identifier to remove turnover ownership,
* and, on top of performing the above manipulations, dispose of any boomers discovered.
* (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation
* if they had not already been handled by the time this function is executed.)
* @return all previously-owned deployables after they have been processed;
* boomers are listed before all other deployable types
*/
def DisownDeployables() : List[PlanetSideGameObject with Deployable] = {
val (boomers, deployables) =
avatar.Deployables.Clear()
.map(continent.GUID(_))
.collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] }
.partition(_.isInstanceOf[BoomerDeployable])
//do not change the OwnerName field at this time
boomers.collect({ case obj : BoomerDeployable =>
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds))) //near-instant
obj.Owner = None
obj.Trigger = None
})
deployables.foreach(obj => {
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) //normal decay
obj.Owner = None
})
boomers ++ deployables
}
/**
* The starting point of behavior for a player who:
* is dead and is respawning;
* is deconstructing at a spawn tube and is respawning;
* is using a warp gate; or,
* any or none of the previous conditions, but the final result involves changing what zone the player occupies.
* This route is not taken when first spawning in the game world, unless special conditions need to be satisfied.
* The visible result will be apparent by the respawn timer being displayed to the client over the deployment map.<br>
* <br>
* Two choices must be independently made to complete this part of the process.
* The first choice ivolves the state of the player who is spawning
* as the known entry state involve either being alive or being dead.
* A dead player (technically, a "corpse" that can no longer be revived) is embodied
* in a completely new player with a new globally unique identifier and a whole new inventory.
* A player who is transferring continents also satisfies the requirements
* for obtaining a completely new globally unique identifier,
* though the new identifier belongs to the new zone rather than the previous (still current) one.
* The second choice is satisfied by respawning in the same zone while still in a state of still being alive.
* In this singular case, the player retains his previous globally unique identifier.
* In all other cases, as indicated, a new globally unique identifier is selected.<br>
* <br>
* If the player is alive and mounted in a vehicle, a different can of worms is produced.
* The ramifications of these conditions are not fully satisfied until the player loads into the new zone.
* Even then, the conclusion becomes delayed while a slightly lagged mechanism hoists players between zones.
* @see `AvatarDeadStateMessage`
* @see `interstellarFerry`
* @see `LoadZoneAsPlayer`
* @see `LoadZoneInVehicle`
* @param zone_id the zone in which the player will be placed
* @param pos the game world coordinates where the player will be positioned
* @param ori the direction in which the player will be oriented
* @param respawnTime the character downtime spent respawning, as clocked on the redeployment screen;
* does not factor in any time required for loading zone or game objects
*/
def LoadZonePhysicalSpawnPoint(zone_id : String, pos : Vector3, ori : Vector3, respawnTime : Long) : Unit = {
log.info(s"Load in zone $zone_id at position $pos in $respawnTime seconds")
respawnTimer.cancel
reviveTimer.cancel
val backpack = player.isBackpack
val respawnTimeMillis = respawnTime * 1000 //ms
deadState = DeadState.RespawnTime
sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, respawnTimeMillis, respawnTimeMillis, Vector3.Zero, player.Faction, true))
val (target, msg) = if(backpack) { //if the player is dead, he is handled as dead infantry, even if he died in a vehicle
//new player is spawning
val newPlayer = RespawnClone(player)
newPlayer.Position = pos
newPlayer.Orientation = ori
LoadZoneAsPlayer(newPlayer, zone_id)
}
else {
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle : Vehicle) => //driver or passenger in vehicle using a warp gate
LoadZoneInVehicle(vehicle, pos, ori, zone_id)
case _ => //player is deconstructing self
val player_guid = player.GUID
sendResponse(ObjectDeleteMessage(player_guid, 4))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4))
player.Position = pos
player.Orientation = ori
LoadZoneAsPlayer(player, zone_id)
}
}
import scala.concurrent.ExecutionContext.Implicits.global
respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg)
}
/**
* Deal with a target player as free-standing infantry in the course of a redeployment action to a target continent
* whether that action is the result of a deconstruction (reconstruction), a death (respawning),
* or other position shifting action handled directly by the server.<br>
* <br>
* The two important vectors are still whether the zone being transported to is the same or is different
* and whether the target player is alive or released (note: not just "dead" ...).
* @see `LoadZoneCommonTransferActivity`
* @see `GUIDTask.UnregisterAvatar`
* @see `GUIDTask.UnregisterLocker`
* @see `PlayerLoaded`
* @see `Player.isBackpack`
* @see `RegisterAvatar`
* @see `TaskBeforeZoneChange`
* @param tplayer the target player being moved around;
* not necessarily the same player as the `WorldSessionActor`-global `player`
* @param zone_id the zone in which the player will be placed
* @return a tuple composed of an `ActorRef` destination and a message to send to that destination
*/
def LoadZoneAsPlayer(tplayer : Player, zone_id : String) : (ActorRef, Any) = {
if(zone_id == continent.Id) {
if(player.isBackpack) { //important! test the actor-wide player ref, not the parameter
//respawning from unregistered player
(taskResolver, RegisterAvatar(tplayer))
}
else {
//move existing player; this is the one case where the original GUID is retained by the player
(self, PlayerLoaded(tplayer))
}
}
else {
LoadZoneCommonTransferActivity()
val original = player
if(player.isBackpack) {
//unregister avatar locker + GiveWorld
player = tplayer
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterLocker(original.Locker)(continent.GUID), zone_id))
}
else {
//unregister avatar whole + GiveWorld
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id))
}
}
}
/**
* Deal with a target player as a vehicle occupant in the course of a redeployment action to a target continent
* whether that action is the result of a deconstruction (reconstruction)
* or other position shifting action handled directly by the server.<br>
* <br>
* The original target player must be alive and the only consideration is in what position the player is mounted in the vehicle.
* Any seated position that isn't the driver is a passenger.
* The most important role performed in this function is to declare a reference to the vehicle itsself
* since no other connection from the player to the vehicle is guaranteed to persist in a meaningful way during the transfer.
* @see `interstellarFerry`
* @see `LoadZoneInVehicleAsDriver`
* @see `LoadZoneInVehicleAsPassenger`
* @see `Vehicle.PassengerInSeat`
* @param vehicle the target vehicle being moved around;
* WILL necessarily be the same vehicles as is controlled by the `WorldSessionActor`-global `player`
* @param pos the game world coordinates where the vehicle will be positioned
* @param ori the direction in which the vehicle will be oriented
* @param zone_id the zone in which the vehicle and driver will be placed,
* or in which the vehicle has already been placed
* @return a tuple composed of an ActorRef` destination and a message to send to that destination
**/
def LoadZoneInVehicle(vehicle : Vehicle, pos : Vector3, ori : Vector3, zone_id : String) : (ActorRef, Any) = {
interstellarFerry = Some(vehicle)
if(vehicle.PassengerInSeat(player).contains(0)) {
vehicle.Position = pos
vehicle.Orientation = ori
LoadZoneInVehicleAsDriver(vehicle, zone_id)
}
else {
LoadZoneInVehicleAsPassenger(vehicle, zone_id)
}
}
/**
* Deal with a target player as a vehicle driver in the course of a redeployment action to a target continent
* whether that action is the result of a deconstruction (reconstruction)
* or other position shifting action handled directly by the server.<br>
* <br>
* During a vehicle transfer, whether to the same zone or to a different zone,
* the driver has the important task of ensuring the certain safety of his passengers during transport.
* The driver must modify the conditions of the vehicle's passengers common communication channel
* originally determined entirely by the vehicle's soon-to-be blanked internal `Actor` object.
* Any cargo vehicles under the control of the target vehicle must also be made aware of the current state of the process.
* In the case of a series of ferrying vehicles and cargo vehicles,
* the vehicle to be deleted might not be the one immediately mounted.
* A reference to the top-level ferrying vehicle's former globally unique identifier has been retained for this purpose.
* This vehicle can be deleted for everyone if no more work can be detected.
* @see `interstellarFerryTopLevelGUID`
* @see `LoadZoneCommonTransferActivity`
* @see `PlayerLoaded`
* @see `TaskBeforeZoneChange`
* @see `UnAccessContents`
* @see `UnregisterDrivenVehicle`
* @param vehicle the target vehicle being moved around;
* WILL necessarily be the same vehicles as is controlled by the `WorldSessionActor`-global `player`
* @param zone_id the zone in which the vehicle and driver will be placed,
* or in which the vehicle has already been placed
* @return a tuple composed of an ActorRef` destination and a message to send to that destination
**/
def LoadZoneInVehicleAsDriver(vehicle : Vehicle, zone_id : String) : (ActorRef, Any) = {
log.info(s"LoadZoneInVehicleAsDriver: ${player.Name} is driving a ${vehicle.Definition.Name}")
val pguid = player.GUID
val toChannel = TransportVehicleChannelName(vehicle)
//standard passengers
vehicleService ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.TransferPassengerChannel(pguid, s"${vehicle.Actor}", toChannel, vehicle))
//cargo
val occupiedCargoHolds = vehicle.CargoHolds.values.collect {
case hold if hold.isOccupied =>
hold.Occupant.get
}
occupiedCargoHolds.foreach{ cargo =>
cargo.Seats(0).Occupant match {
case Some(occupant) =>
vehicleService ! VehicleServiceMessage(s"${occupant.Name}", VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo))
case _ =>
log.error("LoadZoneInVehicleAsDriver: abort; vehicle in cargo hold missing driver")
HandleDismountVehicleCargo(player.GUID, cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true)
}
}
//
if(zone_id == continent.Id) {
//transferring a vehicle between spawn points (warp gates) in the same zone
//LoadZoneTransferPassengerMessages(pguid, zone_id, toChannel, vehicle, PlanetSideGUID(0))
(self, PlayerLoaded(player))
}
else {
UnAccessContents(vehicle)
LoadZoneCommonTransferActivity()
player.VehicleSeated = vehicle.GUID
player.Continent = zone_id //forward-set the continent id to perform a test
interstellarFerryTopLevelGUID = (if(vehicle.Seats.values.count(_.isOccupied) == 1 && occupiedCargoHolds.size == 0) {
//do not delete if vehicle has passengers or cargo
val vehicleToDelete = interstellarFerryTopLevelGUID.orElse(player.VehicleSeated).getOrElse(PlanetSideGUID(0))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.UnloadVehicle(pguid, continent, vehicle, vehicleToDelete))
None
}
else {
interstellarFerryTopLevelGUID.orElse(Some(vehicle.GUID))
})
//unregister vehicle and driver whole + GiveWorld
continent.Transport ! Zone.Vehicle.Despawn(vehicle)
(taskResolver, TaskBeforeZoneChange(UnregisterDrivenVehicle(vehicle, player), zone_id))
}
}
/**
* The channel name for summoning passengers to the vehicle
* after it has been loaded to a new location or to a new zone.
* This channel name should be unique to the vehicle for at least the duration of the transition.
* The vehicle-specific channel with which all passengers are coordinated back to the original vehicle.
* @param vehicle the vehicle being moved (or having been moved)
* @return the channel name
*/
def TransportVehicleChannelName(vehicle : Vehicle) : String = {
s"transport-vehicle-channel-${interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID).guid}"
}
/**
* Deal with a target player as a vehicle passenger in the course of a redeployment action to a target continent
* whether that action is the result of a deconstruction (reconstruction)
* or other position shifting action handled directly by the server.<br>
* <br>
* The way a vehicle is handled in reference to being a passenger
* is very similar to how an infantry player is handled in the same process.
* If this player is the last person who requires a zone change
* which is the concluding zone transfer of what might have been a long chain of vehicle and passengers
* then that player is responsible for deleting the vehicle for other players of the previous zone.
* In the case of a series of ferrying vehicles and cargo vehicles,
* the vehicle to be deleted might not be the one immediately mounted.
* A reference to the top-level ferrying vehicle's former globally unique identifier has been retained for this purpose.
* This vehicle can be deleted for everyone if no more work can be detected.
* @see `GUIDTask.UnregisterAvatar`
* @see `LoadZoneCommonTransferActivity`
* @see `NoVehicleOccupantsInZone`
* @see `PlayerLoaded`
* @see `TaskBeforeZoneChange`
* @see `UnAccessContents`
* @param vehicle the target vehicle being moved around
* @param zone_id the zone in which the vehicle and driver will be placed
* @return a tuple composed of an ActorRef` destination and a message to send to that destination
**/
def LoadZoneInVehicleAsPassenger(vehicle : Vehicle, zone_id : String) : (ActorRef, Any) = {
log.info(s"LoadZoneInVehicleAsPassenger: ${player.Name} is the passenger of a ${vehicle.Definition.Name}")
if(zone_id == continent.Id) {
//transferring a vehicle between spawn points (warp gates) in the same zone
(self, PlayerLoaded(player))
}
else {
LoadZoneCommonTransferActivity()
player.VehicleSeated = vehicle.GUID
player.Continent = zone_id //forward-set the continent id to perform a test
val continentId = continent.Id
if(NoVehicleOccupantsInZone(vehicle, continentId)) {
//do not dispatch delete action if any hierarchical occupant has not gotten this far through the summoning process
val vehicleToDelete = interstellarFerryTopLevelGUID.orElse(player.VehicleSeated).getOrElse(PlanetSideGUID(0))
vehicleService ! VehicleServiceMessage(continentId, VehicleAction.UnloadVehicle(player.GUID, continent, vehicle, vehicleToDelete))
}
interstellarFerryTopLevelGUID = None
//unregister avatar + GiveWorld
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(player)(continent.GUID), zone_id))
}
}
/**
* A recursive test that explores all the seats of a target vehicle
* and all the seats of any discovered cargo vehicles
* and then the same criteria in those cargo vehicles
* to determine if any of their combined passenger roster remains in a given zone.<br>
* <br>
* While it should be possible to recursively explore up a parent-child relationship -
* testing the ferrying vehicle to which of the current tested vehicle in consider a cargo vehicle -
* the relationship expressed is one of globally unique refertences and not one of object references -
* that suggested super-ferrying vehicle may not exist in the zone unless special considerations are imposed.
* For the purpose of these special considerations,
* implemented by enforcing a strictly downwards order of vehicular zone transportation,
* where drivers move vehicles and call passengers and immediate cargo vehicle drivers,
* it becomes unnecessary to test any vehicle that might be ferrying the target vehicle.
* @see `ZoneAware`
* @param vehicle the target vehicle being moved around
* @param zone_id the zone in which the vehicle and its passengers should not be located
* @return `true`, if all passengers of the vehicle, and its cargo vehicles, etc., have reported being moved from the zone;
* `false`, otherwise
*/
def NoVehicleOccupantsInZone(vehicle : Vehicle, zoneId : String) : Boolean = {
(vehicle.Seats.values
.collect { case seat if seat.isOccupied && seat.Occupant.get.Continent == zoneId => true }
.isEmpty) &&
{
vehicle.CargoHolds.values
.collect {
case hold if hold.isOccupied =>
hold.Occupant.get
}
.foldLeft(true)(_ && NoVehicleOccupantsInZone(_, zoneId))
}
}
/**
* Dispatch messages to all target players in immediate passenger and gunner seats
* and to the driver of all vehicles in cargo holds
* that their current ferrying vehicle is being transported from one zone to the next
* and that they should follow after it.
* The messages address the avatar of their recipient `WorldSessionActor` objects.
* @param player_guid the driver of the target vehicle
* @param toZoneId the zone where the target vehicle will be moved
* @param toChannel the vehicle-specific channel with which all passengers are coordinated to the vehicle
* @param vehicle the vehicle (object)
* @param vehicleToDelete the vehicle as it was identified in the zone that it is being moved from
*/
def LoadZoneTransferPassengerMessages(player_guid : PlanetSideGUID, toZoneId : String, toChannel : String, vehicle : Vehicle, vehicleToDelete : PlanetSideGUID) : Unit = {
vehicleService ! VehicleServiceMessage(toChannel, VehicleAction.TransferPassenger(player_guid, toChannel, vehicle, vehicleToDelete))
vehicle.CargoHolds.values
.collect {
case hold if hold.isOccupied =>
val cargo = hold.Occupant.get
cargo.Continent = toZoneId
//point to the cargo vehicle to instigate cargo vehicle driver transportation
vehicleService ! VehicleServiceMessage(toChannel, VehicleAction.TransferPassenger(player_guid, toChannel, cargo, vehicleToDelete))
}
}
/**
* Common behavior when transferring between zones
* encompassing actions that disassociate the player with entities they left (will leave) in the previous zone.
* It also sets up actions for the new zone loading process.
*/
def LoadZoneCommonTransferActivity() : Unit = {
RemoveBoomerTriggersFromInventory().foreach(obj => {
taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID)
})
DisownDeployables()
drawDeloyableIcon = RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
continent.Population ! Zone.Population.Leave(avatar)
}
/**
* Primary functionality for tranferring a piece of equipment from a player's hands or his inventory to the ground.
* Items are always dropped at player's feet because for simplicity's sake
* because, by virtue of already standing there, the stability of the surface has been proven.
* The only exception to this is dropping items while falling.
* @see `Player.Find`<br>
* `ObjectDetachMessage`
* @param item the `Equipment` object in the player's hand
* @param pos the game world coordinates where the object will be dropped
* @param orient a suggested orientation in which the object will settle when on the ground;
* as indicated, the simulation is only concerned with certain angles
*/
def PutItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) : Unit = {
//TODO delay or reverse dropping item when player is falling down
item.Position = pos
item.Orientation = Vector3.z(orient.z)
item.Faction = PlanetSideEmpire.NEUTRAL
//dropped items rotate towards the user's standing direction
val exclusionId = player.Find(item) match {
//if the item is in our hands ...
case Some(slotNum) =>
player.Slot(slotNum).Equipment = None
sendResponse(ObjectDetachMessage(player.GUID, item.GUID, pos, orient.z))
sendResponse(ActionResultMessage.Pass)
player.GUID //we're dropping the item; don't need to see it dropped again
case None =>
PlanetSideGUID(0) //item is being introduced into the world upon drop
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent))
}
/**
* Primary functionality for tranferring a piece of equipment from the ground in a player's hands or his inventory.
* The final destination of the item in terms of slot position is not determined until the attempt is made.
* If it can not be placed in a slot correctly, the item will be returned to the ground in the same place.
* @see `Player.Fit`
* @param item the `Equipment` object on the ground
* @return `true`, if the object was properly picked up;
* `false` if it was returned to the ground
*/
def PutItemInHand(item : Equipment) : Boolean = {
player.Fit(item) match {
case Some(slotNum) =>
item.Faction = player.Faction
val item_guid = item.GUID
val player_guid = player.GUID
player.Slot(slotNum).Equipment = item
val definition = item.Definition
sendResponse(
ObjectCreateDetailedMessage(
definition.ObjectId,
item_guid,
ObjectCreateMessageParent(player_guid, slotNum),
definition.Packet.DetailedConstructorData(item).get
)
)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PickupItem(player_guid, continent, player, slotNum, item))
true
case None =>
continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state
false
}
}
/**
* Attempt to link the router teleport system using the provided terminal information.
* Although additional states are necessary to properly use the teleportation system,
* e.g., deployment state, active state of the endpoints, etc.,
* this decision is not made factoring those other conditions.
* @param router the vehicle that houses one end of the teleportation system (the `InternalTelepad` object)
* @param systemPlan specific object identification of the two endpoints of the teleportation system;
* if absent, the knowable endpoint is deleted from the client reflexively
*/
def ToggleTeleportSystem(router : Vehicle, systemPlan : Option[(Utility.InternalTelepad, TelepadDeployable)]) : Unit = {
StartBundlingPackets()
systemPlan match {
case Some((internalTelepad, remoteTelepad)) =>
LinkRouterToRemoteTelepad(router, internalTelepad, remoteTelepad)
case _ =>
router.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util : Utility.InternalTelepad) =>
sendResponse(ObjectDeleteMessage(util.GUID, 0))
case _ => ;
}
}
StopBundlingPackets()
}
/**
* Link the router teleport system using the provided terminal information.
* The internal telepad is made known of the remote telepad, creating the link.
* @param router the vehicle that houses one end of the teleportation system (the `internalTelepad`)
* @param internalTelepad the endpoint of the teleportation system housed by the router
* @param remoteTelepad the endpoint of the teleportation system that exists in the environment
*/
def LinkRouterToRemoteTelepad(router : Vehicle, internalTelepad : Utility.InternalTelepad, remoteTelepad : TelepadDeployable) : Unit = {
internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad
CreateRouterInternalTelepad(router, internalTelepad)
LinkRemoteTelepad(remoteTelepad.GUID)
}
/**
* Create the mechanism that serves as one endpoint of the linked router teleportation system.<br>
* <br>
* Technically, the mechanism - an `InternalTelepad` object - is always made to exist
* due to how the Router vehicle object is encoded into an `ObjectCreateMessage` packet.
* Regardless, that internal mechanism is created anew each time the system links a new remote telepad.
* @param router the vehicle that houses one end of the teleportation system (the `internalTelepad`)
* @param internalTelepad the endpoint of the teleportation system housed by the router
*/
def CreateRouterInternalTelepad(router : Vehicle, internalTelepad : PlanetSideGameObject with TelepadLike) : Unit = {
//create the interal telepad each time the link is made
val rguid = router.GUID
val uguid = internalTelepad.GUID
val udef = internalTelepad.Definition
/*
the following instantiation and configuration creates the internal Router component
normally dispatched while the Router is transitioned into its Deploying state
it is safe, however, to perform these actions at any time during and after the Deploying state
*/
sendResponse(
ObjectCreateMessage(
udef.ObjectId,
uguid,
ObjectCreateMessageParent(rguid, 2), //TODO stop assuming slot number
udef.Packet.ConstructorData(internalTelepad).get
)
)
sendResponse(GenericObjectActionMessage(uguid, 108))
sendResponse(GenericObjectActionMessage(uguid, 120))
/*
the following configurations create the interactive beam underneath the Deployed Router
normally dispatched after the warm-up timer has completed
*/
sendResponse(GenericObjectActionMessage(uguid, 108))
sendResponse(GenericObjectActionMessage(uguid, 112))
}
/**
* na
* @param telepadGUID na
*/
def LinkRemoteTelepad(telepadGUID: PlanetSideGUID) : Unit = {
sendResponse(GenericObjectActionMessage(telepadGUID, 108))
sendResponse(GenericObjectActionMessage(telepadGUID, 112))
}
/**
* 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) = {
val time = System.nanoTime
if(time - recentTeleportAttempt > (2 seconds).toNanos && router.DeploymentState == DriveState.Deployed && internalTelepad.Active && remoteTelepad.Active) {
val pguid = player.GUID
val sguid = src.GUID
val dguid = dest.GUID
StartBundlingPackets()
sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
UseRouterTelepadEffect(pguid, sguid, dguid)
StopBundlingPackets()
// vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(router), continent))
// vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(router, continent, router.Definition.DeconstructionTime))
localService ! LocalServiceMessage(continent.Id, LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid))
}
else {
log.warn(s"UseRouterTelepadSystem: 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, 124))
sendResponse(GenericObjectActionMessage(destGUID, 128))
}
/**
* Before a vehicle is removed from the game world, the following actions must be performed.
* @param vehicle the vehicle
*/
def BeforeUnloadVehicle(vehicle : Vehicle) : Unit = {
vehicle.Definition match {
case GlobalDefinitions.ams if vehicle.Faction == player.Faction =>
log.info("BeforeUnload: cleaning up after a mobile spawn vehicle ...")
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.UpdateAmsSpawnPoint(continent))
None
case GlobalDefinitions.router =>
//this may repeat for multiple players on the same continent but that's okay(?)
log.info("BeforeUnload: cleaning up after a router ...")
RemoveTelepads(vehicle)
case _ => ;
}
}
def RemoveTelepads(vehicle: Vehicle) : Unit = {
(vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
case Some(util : Utility.InternalTelepad) =>
val telepad = util.Telepad
util.Telepad = None
continent.GUID(telepad)
case _ =>
None
}) match {
case Some(telepad : TelepadDeployable) =>
log.info(s"BeforeUnload: deconstructing telepad $telepad that was linked to router $vehicle ...")
telepad.Active = false
localService ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent))
localService ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, continent, Some(0 seconds)))
case _ => ;
}
}
/**
* 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.
* @param weapon_guid the weapon (GUID)
* @param tool the weapon (object)
*/
def EmptyMagazine(weapon_guid : PlanetSideGUID, tool : Tool) : Unit = {
tool.Magazine = 0
sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, weapon_guid, 0))
sendResponse(ChangeFireStateMessage_Stop(weapon_guid))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, weapon_guid))
sendResponse(WeaponDryFireMessage(weapon_guid))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid))
}
/**
* na
* @param player_guid the target player
* @param cargoGUID the globally unique number for the vehicle being ferried
* @param cargo the vehicle being ferried
* @param carrierGUID the globally unique number for the vehicle doing the ferrying
* @param carrier the vehicle doing the ferrying
* @param bailed the ferried vehicle is bailing from the cargo hold
* @param requestedByPassenger the ferried vehicle is being politely disembarked from the cargo hold
* @param kicked the ferried vehicle is being kicked out of the cargo hold
*/
def HandleDismountVehicleCargo(player_guid : PlanetSideGUID, cargoGUID : PlanetSideGUID, cargo : Vehicle, carrierGUID : PlanetSideGUID, carrier : Vehicle, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = {
carrier.CargoHolds.find({case((_, hold)) => hold.Occupant.contains(cargo)}) match {
case Some((mountPoint, hold)) =>
cargo.MountedIn = None
hold.Occupant = None
val driverOpt = cargo.Seats(0).Occupant
val rotation : Vector3 = if(CargoOrientation(cargo) == 1) { //TODO: BFRs will likely also need this set
//dismount router "sideways" in a lodestar
carrier.Orientation.xy + Vector3.z((carrier.Orientation.z - 90) % 360)
}
else {
carrier.Orientation
}
val cargoHoldPosition : Vector3 = if(carrier.Definition == GlobalDefinitions.dropship) {
//the galaxy cargo bay is offset backwards from the center of the vehicle
carrier.Position + Vector3.Rz(Vector3(0, 7, 0), math.toRadians(carrier.Orientation.z))
}
else {
//the lodestar's cargo hold is almost the center of the vehicle
carrier.Position
}
StartBundlingPackets()
vehicleService ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 0, cargo.Health)))
vehicleService ! VehicleServiceMessage(s"${cargo.Actor}", VehicleAction.SendResponse(PlanetSideGUID(0), PlanetsideAttributeMessage(cargoGUID, 68, cargo.Shields)))
if(carrier.Flying) {
//the carrier vehicle is flying; eject the cargo vehicle
val ejectCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.InProgress, 0)
val detachCargoMsg = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition - Vector3.z(1), rotation)
val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0)
sendResponse(ejectCargoMsg) //dismount vehicle on UI and disable "shield" effect on lodestar
sendResponse(detachCargoMsg)
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(player_guid, ejectCargoMsg))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(player_guid, detachCargoMsg))
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(PlanetSideGUID(0), resetCargoMsg)) //lazy
log.debug(ejectCargoMsg.toString)
log.debug(detachCargoMsg.toString)
if(driverOpt.isEmpty) {
//TODO cargo should drop like a rock like normal; until then, deconstruct it
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, continent, Some(0 seconds)))
}
}
else {
//the carrier vehicle is not flying; just open the door and let the cargo vehicle back out; force it out if necessary
val cargoStatusMessage = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), cargoGUID, PlanetSideGUID(0), mountPoint, CargoStatus.InProgress, 0)
val cargoDetachMessage = ObjectDetachMessage(carrierGUID, cargoGUID, cargoHoldPosition + Vector3.z(1f), rotation)
sendResponse(cargoStatusMessage)
sendResponse(cargoDetachMessage)
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player_guid, cargoStatusMessage))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.SendResponse(player_guid, cargoDetachMessage))
driverOpt match {
case Some(driver) =>
vehicleService ! VehicleServiceMessage(s"${driver.Name}", VehicleAction.KickCargo(player_guid, cargo, cargo.Definition.AutoPilotSpeed2, 2500))
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
//check every quarter second if the vehicle has moved far enough away to be considered dismounted
cargoDismountTimer.cancel
cargoDismountTimer = context.system.scheduler.scheduleOnce(250 milliseconds, self, CheckCargoDismount(cargoGUID, carrierGUID, mountPoint, iteration = 0))
case None =>
val resetCargoMsg = CargoMountPointStatusMessage(carrierGUID, PlanetSideGUID(0), PlanetSideGUID(0), cargoGUID, mountPoint, CargoStatus.Empty, 0)
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SendResponse(PlanetSideGUID(0), resetCargoMsg)) //lazy
//TODO cargo should back out like normal; until then, deconstruct it
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(cargo), continent))
vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(cargo, continent, Some(0 seconds)))
}
}
StopBundlingPackets()
case None =>
log.warn(s"HandleDismountVehicleCargo: can not locate cargo $cargo in any hold of the carrier vehicle $carrier")
}
}
/**
* na
* @param player_guid na
* @param cargo_guid na
* @param bailed na
* @param requestedByPassenger na
* @param kicked na
*/
def DismountVehicleCargo(player_guid : PlanetSideGUID, cargo_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) : Unit = {
continent.GUID(cargo_guid) match {
case Some(cargo : Vehicle) =>
continent.GUID(cargo.MountedIn) match {
case Some(ferry : Vehicle) =>
HandleDismountVehicleCargo(player_guid, cargo_guid, cargo, ferry.GUID, ferry, bailed, requestedByPassenger, kicked)
case _ =>
log.warn(s"DismountVehicleCargo: target ${cargo.Definition.Name}@$cargo_guid does not know what treats it as cargo")
}
case _ =>
log.warn(s"DismountVehicleCargo: target $cargo_guid either is not a vehicle in ${continent.Id} or does not exist")
}
}
def GetPlayerHackSpeed(obj: PlanetSideServerObject): Float = {
val playerHackLevel = GetPlayerHackLevel()
val timeToHack = obj match {
case (hackable: Hackable) => hackable.HackDuration(playerHackLevel)
case (vehicle: Vehicle) => vehicle.JackingDuration(playerHackLevel)
case _ =>
log.warn(s"Player tried to hack an object that has no hack time defined ${obj.GUID} - ${obj.Definition.Name}")
0
}
if(timeToHack == 0) {
log.warn(s"Player ${player.GUID} tried to hack an object ${obj.GUID} - ${obj.Definition.Name} that they don't have the correct hacking level for")
0f
} else {
// 250 ms per tick on the hacking progress bar
val ticks = (timeToHack * 1000) / 250
100f / ticks
}
}
def GetPlayerHackLevel(): Int = {
if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) {
3
} else if(player.Certifications.contains(CertificationType.AdvancedHacking)) {
2
} else if (player.Certifications.contains(CertificationType.Hacking)) {
1
} else {
0
}
}
/**
* Make this client display the deployment map, and all its available destination spawn points.
* @see `AvatarDeadStateMessage`
* @see `DeadState.Release`
* @see `Player.Release`
*/
def GoToDeploymentMap() : Unit = {
player.Release
deadState = DeadState.Release
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true))
DrawCurrentAmsSpawnPoint()
}
/**
* From a seat, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines.
* @param objWithSeat the object that owns seats (and weaponry)
* @param seatNum the seat
*/
def UpdateWeaponAtSeatPosition(objWithSeat : MountedWeapons, seatNum : Int) : Unit = {
objWithSeat.WeaponControlledFromSeat(seatNum) match {
case Some(weapon : Tool) =>
//update mounted weapon belonging to seat
weapon.AmmoSlots.foreach(slot => {
//update the magazine(s) in the weapon, specifically
val magazine = slot.Box
sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong))
})
case _ => ; //no weapons to update
}
}
/**
* Given an origin and a destination, determine how long the process of traveling should take in reconstruction time.
* For most destinations, the unit of receiving ("spawn point") determines the reconstruction time.
* In a special consideration, travel to any sanctuary or sanctuary-special zone should be as immediate as zone loading.
* @param toZoneId the zone where the target is headed
* @param toSpawnPoint the unit the target is using as a destination
* @param fromZoneId the zone where the target current is located
* @return how long in seconds the spawning process will take
*/
def CountSpawnDelay(toZoneId : String, toSpawnPoint : SpawnPoint, fromZoneId : String) : Long = {
val sanctuaryZoneId = Zones.SanctuaryZoneId(player.Faction)
if(sanctuaryZoneId.equals(toZoneId)) { //to sanctuary
0L
}
else if(!player.isAlive) {
toSpawnPoint.Definition.Delay //TODO +cumulative death penalty
}
else {
toSpawnPoint.Definition.Delay
}
}
/**
* In the background, a list of advanced mobile spawn vehicles that are deployed in the zone is being updated constantly.
* Select, from this list, the AMS that is closest to the player's current or last position
* and draw its spawn selection icon onto the deployment map.
* @see `BindPlayerMessage`
* @see `DeadState.Release`
*/
def DrawCurrentAmsSpawnPoint() : Unit = {
if(deadState == DeadState.Release) {
amsSpawnPoints
.sortBy(tube => Vector3.DistanceSquared(tube.Position, player.Position))
.headOption match {
case Some(tube) =>
log.info("DrawCurrentAmsSpawnPoint - new @ams spawn point drawn")
sendResponse(BindPlayerMessage(BindStatus.Available, "@ams", true, false, SpawnGroup.AMS, continent.Number, 5, tube.Position))
case None =>
log.info("DrawCurrentAmsSpawnPoint - no @ams spawn point drawn")
sendResponse(BindPlayerMessage(BindStatus.Unavailable, "@ams", false, false, SpawnGroup.AMS, continent.Number, 0, Vector3.Zero))
}
}
}
def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = {
if(squadUI.nonEmpty) {
val fromMember = squad.Membership(fromIndex)
val fromCharId = fromMember.CharId
val toMember = squad.Membership(toIndex)
val toCharId = toMember.CharId
val id = 11
if(fromCharId > 0) {
//toMember and fromMember have swapped places
val fromElem = squadUI(fromCharId)
val toElem = squadUI(toCharId)
sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex))
sendResponse(SquadMemberEvent.Remove(id, fromCharId, toIndex))
squadUI(toCharId) = SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position)
squadUI(fromCharId) = SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position)
sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0))
sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, unk7 = 0))
sendResponse(
SquadState(
PlanetSideGUID(id),
List(
SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, false, 429, None, None),
SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, false, 429, None, None)
)
)
)
}
else {
//previous fromMember has moved toMember
val elem = squadUI(toCharId)
sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex))
squadUI(toCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position)
sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, elem.name, elem.zone, unk7 = 0))
sendResponse(
SquadState(
PlanetSideGUID(id),
List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
)
)
}
val charId = avatar.CharId
if(toCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
}
else if(fromCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
}
}
}
def failWithError(error : String) = {
log.error(error)
sendResponse(ConnectionClose())
}
/**
* Persistent collector that intercepts `GamePacket` and `ControlPacket` messages that are being sent towards the network.
*/
private val packetBundlingCollector : MultiPacketCollector = new MultiPacketCollector()
/**
* Re-assigned function used to direct/intercept packets being sent towards the network.
* Defaults to directing the packets.
*/
private var packetBundlingFunc : (PlanetSidePacket)=>Option[PlanetSidePacket] = NoBundlingAction
/**
* Start packet bundling by assigning the appropriate function.
* @see `sendResponse(PlanetSidePacket) : Unit`
*/
def StartBundlingPackets() : Unit = {
log.trace("WORLD SEND: STARTED BUNDLING PACKETS")
packetBundlingFunc = PerformBundlingAction
}
/**
* Stop packet bundling by assigning the appropriate function.
* If any bundles are in the collector's buffer, push that bundle out towards the network.
* @see `sendResponse(PlanetSidePacket) : Unit`
*/
def StopBundlingPackets() : Unit = {
log.trace("WORLD SEND: PACKET BUNDLING SUSPENDED")
packetBundlingFunc = NoBundlingAction
packetBundlingCollector.BundleOption match {
case Some(bundle) =>
sendResponse(bundle)
case None => ;
}
}
/**
* Transform the packet into either a `PlanetSideGamePacket` or a `PlanetSideControlPacket` and push it towards the network.
* @param cont the packet
* @return the same packet, to indicate it was sent
*/
private def NoBundlingAction(cont : PlanetSidePacket) : Option[PlanetSidePacket] = {
cont match {
case game : PlanetSideGamePacket =>
sendResponse(PacketCoding.CreateGamePacket(0, game))
case control : PlanetSideControlPacket =>
sendResponse(PacketCoding.CreateControlPacket(control))
case _ => ;
}
Some(cont)
}
/**
* Intercept the packet being sent towards the network and
* add it to a bundle that will eventually be sent to the network itself.
* @param cont the packet
* @return always `None`, to indicate the packet was not sent
*/
private def PerformBundlingAction(cont : PlanetSidePacket) : Option[PlanetSidePacket] = {
log.trace("WORLD SEND, BUNDLED: " + cont)
packetBundlingCollector.Add(cont)
None
}
/**
* Common entry point for transmitting packets to the network.
* Alternately, catch those packets and retain them to send out a bundled message.
* @param cont the packet
*/
def sendResponse(cont : PlanetSidePacket) : Unit = packetBundlingFunc(cont)
/**
* `KeepAliveMessage` is a special `PlanetSideGamePacket` that is excluded from being bundled when it is sent to the network.<br>
* <br>
* The risk of the server getting caught in a state where the packets dispatched to the client are always bundled is posible.
* Starting the bundling functionality but forgetting to transition into a state where it is deactivated can lead to this problem.
* No packets except for `KeepAliveMessage` will ever be sent until the ever-accumulating packets overflow.
* To avoid this state, whenever a `KeepAliveMessage` is sent, the packet collector empties its current contents to the network.
* @see `StartBundlingPackets`
* @see `StopBundlingPackets`
* @see `clientKeepAlive`
* @param cont a `KeepAliveMessage` packet
*/
def sendResponse(cont : KeepAliveMessage) : Unit = {
sendResponse(PacketCoding.CreateGamePacket(0, cont))
packetBundlingCollector.BundleOption match {
case Some(bundle) =>
log.trace("WORLD SEND: INTERMITTENT PACKET BUNDLE")
sendResponse(bundle)
case None => ;
}
}
def sendResponse(cont : PlanetSidePacketContainer) : Unit = {
log.trace("WORLD SEND: " + cont)
sendResponse(cont.asInstanceOf[Any])
}
def sendResponse(cont : MultiPacketBundle) : Unit = {
sendResponse(cont.asInstanceOf[Any])
}
def sendResponse(msg : Any) : Unit = {
MDC("sessionId") = sessionId.toString
rightRef !> msg
}
def sendRawResponse(pkt : ByteVector) = {
log.trace("WORLD SEND RAW: " + pkt)
sendResponse(RawPacket(pkt))
}
}
object WorldSessionActor {
final case class ResponseToSelf(pkt : PlanetSideGamePacket)
private final case class PokeClient()
private final case class ServerLoaded()
private final case class NewPlayerLoaded(tplayer : Player)
private final case class PlayerLoaded(tplayer : Player)
private final case class PlayerFailedToLoad(tplayer : Player)
private final case class ListAccountCharacters()
private final case class SetCurrentAvatar(tplayer : Player)
private final case class VehicleLoaded(vehicle : Vehicle)
private final case class UnregisterCorpseOnVehicleDisembark(corpse : Player)
private final case class CheckCargoMounting(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int)
private final case class CheckCargoDismount(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int)
/**
* A message that indicates the user is using a remote electronics kit to hack some server object.<br>
* <br>
* Each time this message is sent for a given hack attempt counts as a single "tick" of progress.
* The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more.
* To calculate the actual amount of change in the progress `delta`,
* start with 100, divide by the length of time in seconds, then divide once more by 4.
* @param progressType 1 - REK hack
* 2 - Turret upgrade with glue gun + upgrade cannister
* @param tplayer the player
* @param target the object being hacked
* @param tool_guid the REK
* @param delta how much the progress bar value changes each tick
* @param completeAction a custom action performed once the hack is completed
* @param tickAction an optional action is is performed for each tick of progress
*/
private final case class HackingProgress(progressType : Int,
tplayer : Player,
target : PlanetSideServerObject,
tool_guid : PlanetSideGUID,
delta : Float,
completeAction : () => Unit,
tickAction : Option[() => Unit] = None)
protected final case class SquadUIElement(name : String, index : Int, zone : Int, health : Int, armor : Int, position : Vector3)
private final case class NtuCharging(tplayer: Player,
vehicle: Vehicle)
private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID)
private final case class FinalizeDeployable(obj : PlanetSideGameObject with Deployable, tool : ConstructionItem, index : Int)
}