mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
11252 lines
515 KiB
Scala
11252 lines
515 KiB
Scala
// Copyright (c) 2017-2020 PSForever
|
|
//language imports
|
|
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
|
import com.github.mauricio.async.db.general.ArrayRowData
|
|
import com.github.mauricio.async.db.{Connection, QueryResult}
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.concurrent.atomic.AtomicInteger
|
|
import org.log4s.{Logger, MDC}
|
|
import scala.annotation.{switch, tailrec}
|
|
import scala.collection.mutable.LongMap
|
|
import scala.concurrent.{Await, Future, Promise}
|
|
import scala.concurrent.duration._
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
import scala.util.Success
|
|
import scodec.bits.ByteVector
|
|
//project imports
|
|
import csr.{CSRWarp, CSRZone, Traveler}
|
|
import MDCContextAware.Implicits._
|
|
import net.psforever.objects._
|
|
import net.psforever.objects.avatar.{Certification, DeployableToolbox}
|
|
import net.psforever.objects.ballistics.{PlayerSource, Projectile, ProjectileResolution, ResolvedProjectile, SourceEntry}
|
|
import net.psforever.objects.ce.{ComplexDeployable, Deployable, DeployableCategory, DeployedItem, SimpleDeployable, TelepadLike}
|
|
import net.psforever.objects.definition._
|
|
import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter}
|
|
import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity}
|
|
import net.psforever.objects.equipment.{Ammo, CItem, EffectTarget, Equipment, EquipmentSize, EquipmentSlot, FireModeSwitch, JammableUnit}
|
|
import net.psforever.objects.GlobalDefinitions
|
|
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
|
|
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
|
|
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, SquadLoadout, VehicleLoadout}
|
|
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
|
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
|
import net.psforever.objects.serverobject.damage.Damageable
|
|
import net.psforever.objects.serverobject.deploy.Deployment
|
|
import net.psforever.objects.serverobject.doors.Door
|
|
import net.psforever.objects.serverobject.generator.Generator
|
|
import net.psforever.objects.serverobject.hackable.{Hackable, GenericHackables}
|
|
import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech
|
|
import net.psforever.objects.serverobject.locks.{IFFLock, IFFLocks}
|
|
import net.psforever.objects.serverobject.mblocker.Locker
|
|
import net.psforever.objects.serverobject.mount.Mountable
|
|
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
|
|
import net.psforever.objects.serverobject.painbox.Painbox
|
|
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
|
import net.psforever.objects.serverobject.structures.{Amenity, Building, SphereOfInfluence, StructureType, WarpGate}
|
|
import net.psforever.objects.serverobject.terminals.{CaptureTerminal, CaptureTerminals, MatrixTerminalDefinition, MedicalTerminalDefinition, ProximityDefinition, ProximityTerminal, ProximityUnit, Terminal}
|
|
import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage
|
|
import net.psforever.objects.serverobject.tube.SpawnTube
|
|
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret, WeaponTurrets}
|
|
import net.psforever.objects.serverobject.zipline.ZipLinePath
|
|
import net.psforever.objects.teamwork.Squad
|
|
import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, CargoBehavior, MountedWeapons, Utility, UtilityType, VehicleLockState}
|
|
import net.psforever.objects.vehicles.Utility.InternalTelepad
|
|
import net.psforever.objects.vital.{DamageFromPainbox, HealFromExoSuitChange, HealFromKit, HealFromTerm, PlayerSuicide, RepairFromKit, Vitality, VitalityDefinition}
|
|
import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector, Zoning}
|
|
import net.psforever.packet._
|
|
import net.psforever.packet.control._
|
|
import net.psforever.packet.game._
|
|
import net.psforever.packet.game.objectcreate.{ConstructorData, DetailedCharacterData, DroppedItemData, ObjectClass, ObjectCreateMessageParent, PlacementData}
|
|
import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo}
|
|
import net.psforever.types._
|
|
import services.{RemoverActor, Service, ServiceManager}
|
|
import services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData}
|
|
import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse}
|
|
import services.chat.{ChatAction, ChatResponse, ChatServiceMessage, ChatServiceResponse}
|
|
import services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse}
|
|
import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
|
|
import services.local.support.{HackCaptureActor, RouterTelepadActivation}
|
|
import services.ServiceManager.LookupResult
|
|
import services.support.SupportActor
|
|
import services.teamwork.{SquadResponse, SquadService, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction}
|
|
import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
|
|
|
|
class WorldSessionActor extends Actor
|
|
with MDCContextAware {
|
|
|
|
import WorldSessionActor._
|
|
|
|
private[this] val log = org.log4s.getLogger
|
|
private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel)
|
|
var sessionId : Long = 0
|
|
var leftRef : ActorRef = ActorRef.noSender
|
|
var rightRef : ActorRef = ActorRef.noSender
|
|
var accountIntermediary : ActorRef = ActorRef.noSender
|
|
var accountPersistence : ActorRef = ActorRef.noSender
|
|
var chatService : 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 account : Account = null
|
|
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 shotsWhileDead : Int = 0
|
|
var accessedContainer : Option[PlanetSideGameObject with Container] = None
|
|
var connectionState : Int = 25
|
|
var flying : Boolean = false
|
|
var speed : Float = 1.0f
|
|
var admin : Boolean = false
|
|
var loadConfZone : Boolean = false
|
|
var noSpawnPointHere : 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 whenUsedLastAAMAX : Long = 0
|
|
var whenUsedLastAIMAX : Long = 0
|
|
var whenUsedLastAVMAX : Long = 0
|
|
var whenUsedLastMAX : Array[Long] = Array.fill[Long](4)(0L)
|
|
var whenUsedLastMAXName : Array[String] = Array.fill[String](4)("")
|
|
var whenUsedLastItem : Array[Long] = Array.fill[Long](1020)(0L)
|
|
var whenUsedLastItemName : Array[String] = Array.fill[String](1020)("")
|
|
var whenUsedLastKit : Long = 0
|
|
var whenUsedLastSMKit : Long = 0
|
|
var whenUsedLastSAKit : Long = 0
|
|
var whenUsedLastSSKit : Long = 0
|
|
val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None)
|
|
val projectilesToCleanUp : Array[Boolean] = Array.fill[Boolean](Projectile.RangeUID - Projectile.BaseUID)(false)
|
|
var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
|
|
var updateSquad : () => Unit = NoSquadUpdates
|
|
var recentTeleportAttempt : Long = 0
|
|
var lastTerminalOrderFulfillment : Boolean = true
|
|
var shiftPosition : Option[Vector3] = None
|
|
var shiftOrientation : Option[Vector3] = None
|
|
var setupAvatarFunc : () => Unit = AvatarCreate
|
|
var beginZoningSetCurrentAvatarFunc : (Player) => Unit = SetCurrentAvatarNormally
|
|
var persist : () => Unit = NoPersistence
|
|
/**
|
|
* 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]()
|
|
var squad_supplement_id : Int = 0
|
|
/**
|
|
* When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked.
|
|
* This `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component,
|
|
* now called "Looking for Squad Member."
|
|
* Only the squad leader may toggle the LFSM marquee.
|
|
* Upon leaving or disbanding a squad, this value is made false.
|
|
* Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
|
|
*/
|
|
var lfsm : Boolean = false
|
|
var squadChannel : Option[String] = None
|
|
var squadSetup : () => Unit = FirstTimeSquadSetup
|
|
var squadUpdateCounter : Int = 0
|
|
val queuedSquadActions : Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
|
|
/** Keeps track of the number of PlayerStateMessageUpstream messages received by the client
|
|
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second */
|
|
private var playerStateMessageUpstreamCount = 0
|
|
var zoningType : Zoning.Method.Value = Zoning.Method.None
|
|
var zoningChatMessageType : ChatMessageType.Value = ChatMessageType.CMT_QUIT
|
|
var zoningStatus : Zoning.Status.Value = Zoning.Status.None
|
|
var zoningCounter : Int = 0
|
|
var instantActionFallbackDestination : Option[Zoning.InstantAction.Located] = None
|
|
lazy val unsignedIntMaxValue : Long = Int.MaxValue.toLong * 2L + 1L
|
|
var serverTime : Long = 0
|
|
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
|
|
var zoningTimer : Cancellable = DefaultCancellable.obj
|
|
var zoningReset : 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`
|
|
*/
|
|
|
|
import scala.language.implicitConversions
|
|
|
|
implicit def boolToInt(b : Boolean) : Int = if(b) 1
|
|
else 0
|
|
|
|
override def postStop() : Unit = {
|
|
//normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
|
|
clientKeepAlive.cancel
|
|
progressBarUpdate.cancel
|
|
reviveTimer.cancel
|
|
respawnTimer.cancel
|
|
cargoMountTimer.cancel
|
|
cargoDismountTimer.cancel
|
|
antChargingTick.cancel
|
|
antDischargingTick.cancel
|
|
chatService ! Service.Leave()
|
|
galaxyService ! Service.Leave()
|
|
continent.AvatarEvents ! Service.Leave()
|
|
continent.LocalEvents ! Service.Leave()
|
|
continent.VehicleEvents ! Service.Leave()
|
|
if(avatar != null) {
|
|
//TODO put any temporary values back into the avatar
|
|
squadService ! Service.Leave(Some(s"${avatar.faction}"))
|
|
if(player != null && player.HasGUID) {
|
|
prefire.orElse(shooting) match {
|
|
case Some(guid) =>
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, guid))
|
|
case 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)
|
|
import services.ServiceManager.Lookup
|
|
val serviceManager = ServiceManager.serviceManager
|
|
serviceManager ! Lookup("accountIntermediary")
|
|
serviceManager ! Lookup("accountPersistence")
|
|
serviceManager ! Lookup("chat")
|
|
serviceManager ! Lookup("taskResolver")
|
|
serviceManager ! Lookup("cluster")
|
|
serviceManager ! Lookup("galaxy")
|
|
serviceManager ! Lookup("squad")
|
|
|
|
case _ =>
|
|
log.error("Unknown message")
|
|
context.stop(self)
|
|
}
|
|
|
|
def ValidObject(id : Int) : Option[PlanetSideGameObject] = ValidObject(Some(PlanetSideGUID(id)))
|
|
|
|
def ValidObject(id : PlanetSideGUID) : Option[PlanetSideGameObject] = ValidObject(Some(id))
|
|
|
|
def ValidObject(id : Option[PlanetSideGUID]) : Option[PlanetSideGameObject] = continent.GUID(id) match {
|
|
case out@Some(obj) if obj.HasGUID =>
|
|
out
|
|
case None if id.nonEmpty && id.get != PlanetSideGUID(0) =>
|
|
//delete stale entity reference from client
|
|
log.warn(s"Player ${player.Name} has an invalid reference to GUID ${id.get} in zone ${continent.Id}.")
|
|
//sendResponse(ObjectDeleteMessage(id.get, 0))
|
|
None
|
|
case _ =>
|
|
None
|
|
}
|
|
|
|
def Started : Receive = {
|
|
case LookupResult("accountIntermediary", endpoint) =>
|
|
accountIntermediary = endpoint
|
|
log.info("ID: " + sessionId + " Got account intermediary service " + endpoint)
|
|
case LookupResult("accountPersistence", endpoint) =>
|
|
accountPersistence = endpoint
|
|
log.info("ID: " + sessionId + " Got account persistence service " + endpoint)
|
|
case LookupResult("chat", endpoint) =>
|
|
chatService = endpoint
|
|
log.info("ID: " + sessionId + " Got chat service " + endpoint)
|
|
case LookupResult("taskResolver", endpoint) =>
|
|
taskResolver = endpoint
|
|
log.info("ID: " + sessionId + " Got task resolver service " + endpoint)
|
|
case LookupResult("galaxy", endpoint) =>
|
|
galaxyService = endpoint
|
|
log.info("ID: " + sessionId + " Got galaxy service " + endpoint)
|
|
case LookupResult("cluster", endpoint) =>
|
|
cluster = endpoint
|
|
log.info("ID: " + sessionId + " Got cluster service " + endpoint)
|
|
case LookupResult("squad", endpoint) =>
|
|
squadService = endpoint
|
|
log.info("ID: " + sessionId + " Got squad service " + endpoint)
|
|
|
|
case ControlPacket(_, ctrl) =>
|
|
handleControlPkt(ctrl)
|
|
case GamePacket(_, _, pkt) =>
|
|
handleGamePkt(pkt)
|
|
|
|
case PokeClient() =>
|
|
persist()
|
|
sendResponse(KeepAliveMessage())
|
|
|
|
case AvatarServiceResponse(toChannel, guid, reply) =>
|
|
HandleAvatarServiceResponse(toChannel, guid, reply)
|
|
|
|
case CommonMessages.Progress(rate, finishedAction, stepAction) =>
|
|
if(progressBarValue.isEmpty) {
|
|
progressBarValue = Some(-rate)
|
|
self ! ProgressEvent(rate, finishedAction, stepAction)
|
|
}
|
|
|
|
case ProgressEvent(delta, finishedAction, stepAction) =>
|
|
HandleProgressChange(delta, finishedAction, stepAction)
|
|
|
|
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 GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest) =>
|
|
((manifest.passengers.find { case (name, _) => player.Name.equals(name) } match {
|
|
case Some((name, index)) if vehicle.Seats(index).Occupant.isEmpty =>
|
|
vehicle.Seats(index).Occupant = player
|
|
Some(vehicle)
|
|
case Some((name, index)) =>
|
|
log.warn(s"TransferPassenger: seat $index is already occupied")
|
|
None
|
|
case None =>
|
|
None
|
|
}).orElse(manifest.cargo.find { case (name, _) => player.Name.equals(name) } match {
|
|
case Some((name, index)) =>
|
|
vehicle.CargoHolds(index).Occupant match {
|
|
case Some(cargo) =>
|
|
cargo.Seats(0).Occupant match {
|
|
case Some(driver) if driver.Name.equals(name) =>
|
|
Some(cargo)
|
|
case _ =>
|
|
None
|
|
}
|
|
case None =>
|
|
None
|
|
}
|
|
case None =>
|
|
None
|
|
})
|
|
) match {
|
|
case Some(v) =>
|
|
galaxyService ! 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(v) //on the other continent and registered to that continent's GUID system
|
|
LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1)
|
|
case None =>
|
|
interstellarFerry match {
|
|
case None =>
|
|
galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones
|
|
interstellarFerryTopLevelGUID = None
|
|
case Some(_) => ;
|
|
//wait patiently
|
|
}
|
|
}
|
|
}
|
|
|
|
case LocalServiceResponse(toChannel, guid, reply) =>
|
|
HandleLocalServiceResponse(toChannel, guid, reply)
|
|
|
|
case ChatServiceResponse(toChannel, guid, avatar_name, cont, avatar_pos, avatar_faction, target, reply) =>
|
|
HandleChatServiceResponse(toChannel, guid, avatar_name, cont, avatar_pos, avatar_faction, target, 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(_, excluded, response) =>
|
|
if(!excluded.exists(_ == avatar.CharId)) {
|
|
response match {
|
|
case SquadResponse.ListSquadFavorite(line, task) =>
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
|
|
|
|
case SquadResponse.InitList(infos) =>
|
|
sendResponse(ReplicationStreamMessage(infos))
|
|
|
|
case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
|
|
sendResponse(
|
|
ReplicationStreamMessage(6, None,
|
|
infos.map { case (index, squadInfo) =>
|
|
SquadListing(index, squadInfo)
|
|
}.toVector
|
|
)
|
|
)
|
|
|
|
case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
|
|
sendResponse(
|
|
ReplicationStreamMessage(1, None,
|
|
infos.map { index =>
|
|
SquadListing(index, None)
|
|
}.toVector
|
|
)
|
|
)
|
|
|
|
case SquadResponse.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.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.WantsSquadPosition(_, name) =>
|
|
sendResponse(
|
|
ChatMsg(
|
|
ChatMessageType.CMT_SQUAD, true, name,
|
|
s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
|
|
None
|
|
)
|
|
)
|
|
|
|
case SquadResponse.Join(squad, positionsToUpdate, toChannel) =>
|
|
val leader = squad.Leader
|
|
val membershipPositions = positionsToUpdate map squad.Membership.zipWithIndex
|
|
StartBundlingPackets()
|
|
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)
|
|
squad_supplement_id = squad.GUID.guid + 1
|
|
membershipPositions.foreach { case (member, index) =>
|
|
sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
|
|
squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
|
}
|
|
//repeat our entry
|
|
sendResponse(SquadMemberEvent.Add(squad_supplement_id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
|
|
val playerGuid = player.GUID
|
|
//turn lfs off
|
|
val factionChannel = s"${player.Faction}"
|
|
if(avatar.LFS) {
|
|
avatar.LFS = false
|
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 53, 0))
|
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(playerGuid, 53, 0))
|
|
}
|
|
//squad colors
|
|
GiveSquadColorsInZone()
|
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(playerGuid, 31, squad_supplement_id))
|
|
//associate with member position in squad
|
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
|
|
//a finalization? what does this do?
|
|
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
|
|
updateSquad = PeriodicUpdatesWhenEnrolledInSquad
|
|
squadChannel = Some(toChannel)
|
|
chatService ! Service.Join(squadChannel.get)
|
|
case _ =>
|
|
//other player is joining our squad
|
|
//load each member's entry
|
|
GiveSquadColorsInZone(
|
|
membershipPositions.map { case (member, index) =>
|
|
val charId = member.CharId
|
|
sendResponse(SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, unk7 = 0))
|
|
squadUI(charId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
|
charId
|
|
}
|
|
)
|
|
}
|
|
StopBundlingPackets()
|
|
//send an initial dummy update for map icon(s)
|
|
sendResponse(SquadState(PlanetSideGUID(squad_supplement_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
|
|
))
|
|
|
|
case SquadResponse.Leave(squad, positionsToUpdate) =>
|
|
StartBundlingPackets()
|
|
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(squad_supplement_id, member, index))
|
|
squadUI.remove(member)
|
|
}
|
|
//uninitialize
|
|
val playerGuid = player.GUID
|
|
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
|
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 31, 0)) //disassociate with squad?
|
|
continent.AvatarEvents ! AvatarServiceMessage(s"${player.Faction}", AvatarAction.PlanetsideAttribute(playerGuid, 31, 0))
|
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
|
|
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
|
|
lfsm = false
|
|
//a finalization? what does this do?
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
|
|
squad_supplement_id = 0
|
|
squadUpdateCounter = 0
|
|
updateSquad = NoSquadUpdates
|
|
chatService ! Service.Leave(squadChannel)
|
|
squadChannel = None
|
|
case _ =>
|
|
//remove each member's entry
|
|
GiveSquadColorsInZone(
|
|
positionsToUpdate.map { case (member, index) =>
|
|
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
|
|
squadUI.remove(member)
|
|
member
|
|
},
|
|
value = 0
|
|
)
|
|
}
|
|
StopBundlingPackets()
|
|
|
|
case SquadResponse.AssignMember(squad, from_index, to_index) =>
|
|
//we've already swapped position internally; now we swap the cards
|
|
SwapSquadUIElements(squad, from_index, to_index)
|
|
|
|
case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) =>
|
|
val charId = player.CharId
|
|
val guid = player.GUID
|
|
lazy val factionChannel = s"${player.Faction}"
|
|
//are we being demoted?
|
|
if(squadUI(charId).index == 0) {
|
|
//lfsm -> lfs
|
|
if(lfsm) {
|
|
sendResponse(PlanetsideAttributeMessage(guid, 53, 0))
|
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(guid, 53, 0))
|
|
}
|
|
lfsm = false
|
|
sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad
|
|
}
|
|
//are we being promoted?
|
|
else if(charId == char_id) {
|
|
sendResponse(PlanetsideAttributeMessage(guid, 32, 0)) //associate with member position in squad
|
|
}
|
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id))
|
|
//we must fix the squad cards backend
|
|
SwapSquadUIElements(squad, from_index, to_index)
|
|
|
|
case SquadResponse.UpdateMembers(squad, positions) =>
|
|
val pairedEntries = positions.collect {
|
|
case entry if squadUI.contains(entry.char_id) =>
|
|
(entry, squadUI(entry.char_id))
|
|
}
|
|
//prune entries
|
|
val updatedEntries = pairedEntries
|
|
.collect({
|
|
case (entry, element) if entry.zone_number != element.zone =>
|
|
//zone gets updated for these entries
|
|
sendResponse(SquadMemberEvent.UpdateZone(squad_supplement_id, entry.char_id, element.index, entry.zone_number))
|
|
squadUI(entry.char_id) = SquadUIElement(element.name, element.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(squad_supplement_id),
|
|
updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2, 2, false, 429, None, None) }
|
|
)
|
|
)
|
|
}
|
|
|
|
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) =>
|
|
StartBundlingPackets()
|
|
waypoints.foreach { case (waypoint_type, info, unk) =>
|
|
sendResponse(SquadWaypointEvent.Add(squad_supplement_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) =>
|
|
sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
|
|
|
|
case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
|
|
sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type))
|
|
|
|
case _ => ;
|
|
}
|
|
}
|
|
|
|
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))
|
|
continent.VehicleEvents ! 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")
|
|
obj.Velocity = Some(Vector3.Zero)
|
|
sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero))
|
|
continent.VehicleEvents ! 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))
|
|
continent.VehicleEvents ! 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))
|
|
continent.VehicleEvents ! 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) =>
|
|
continent.GUID(msg.avatar_guid) match {
|
|
case Some(vehicle : Vehicle) =>
|
|
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, vehicle, msg.object_guid))
|
|
case _ => ;
|
|
}
|
|
case _ => ;
|
|
}
|
|
|
|
case CreateCharacter(name, head, voice, gender, empire) =>
|
|
log.info(s"Creating new character $name...")
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
val accountUserName : String = account.Username
|
|
connection.inTransaction {
|
|
c =>
|
|
c.sendPreparedStatement(
|
|
"INSERT INTO characters (name, account_id, faction_id, gender_id, head_id, voice_id) VALUES(?,?,?,?,?,?) RETURNING id",
|
|
Array(name, account.AccountId, empire.id, gender.id, head, voice.id)
|
|
)
|
|
}.onComplete {
|
|
case scala.util.Success(insertResult) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
insertResult match {
|
|
case result : QueryResult =>
|
|
if(result.rows.nonEmpty) {
|
|
log.info(s"CreateCharacter: successfully created new character for $accountUserName")
|
|
sendResponse(ActionResultMessage.Pass)
|
|
self ! ListAccountCharacters()
|
|
}
|
|
else {
|
|
log.error(s"CreateCharacter: new character for $accountUserName was not created")
|
|
sendResponse(ActionResultMessage.Fail(0))
|
|
self ! ListAccountCharacters()
|
|
}
|
|
case e =>
|
|
log.error(s"CreateCharacter: unexpected error while creating new character for $accountUserName")
|
|
sendResponse(ActionResultMessage.Fail(3))
|
|
self ! ListAccountCharacters()
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
failWithError(s"CreateCharacter: query failed - ${e.getMessage}")
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
log.error(s"CreateCharacter: no connection - ${e.getMessage}?")
|
|
}
|
|
|
|
case ListAccountCharacters() =>
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
val accountUserName : String = account.Username
|
|
connection.sendPreparedStatement(
|
|
"SELECT id, name, faction_id, gender_id, head_id, voice_id, deleted, last_login FROM characters where account_id=? ORDER BY last_login", Array(account.AccountId)
|
|
).onComplete {
|
|
case scala.util.Success(result : QueryResult) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
if(result.rows.nonEmpty) {
|
|
import net.psforever.objects.definition.converter.CharacterSelectConverter
|
|
val gen : AtomicInteger = new AtomicInteger(1)
|
|
val converter : CharacterSelectConverter = new CharacterSelectConverter
|
|
result.rows foreach { row =>
|
|
log.trace(s"char list : ${row.toString()}")
|
|
val nowTimeInSeconds = System.currentTimeMillis() / 1000
|
|
var avatarArray : Array[Avatar] = Array.ofDim(row.length)
|
|
var playerArray : Array[Player] = Array.ofDim(row.length)
|
|
row.zipWithIndex.foreach { case (value, i) =>
|
|
val lName : String = value(1).asInstanceOf[String]
|
|
val lFaction : PlanetSideEmpire.Value = PlanetSideEmpire(value(2).asInstanceOf[Int])
|
|
val lGender : CharacterGender.Value = CharacterGender(value(3).asInstanceOf[Int])
|
|
val lHead : Int = value(4).asInstanceOf[Int]
|
|
val lVoice : CharacterVoice.Value = CharacterVoice(value(5).asInstanceOf[Int])
|
|
val lDeleted : Boolean = value(6).asInstanceOf[Boolean]
|
|
val lTime = value(7).asInstanceOf[org.joda.time.LocalDateTime].toDateTime().getMillis() / 1000
|
|
val secondsSinceLastLogin = nowTimeInSeconds - lTime
|
|
if(!lDeleted) {
|
|
avatarArray(i) = new Avatar(value(0).asInstanceOf[Int], lName, lFaction, lGender, lHead, lVoice)
|
|
AwardCharacterSelectBattleExperiencePoints(avatarArray(i), 20000000L)
|
|
avatarArray(i).CEP = 600000
|
|
playerArray(i) = new Player(avatarArray(i))
|
|
playerArray(i).ExoSuit = ExoSuitType.Reinforced
|
|
playerArray(i).Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(playerArray(i).Faction))
|
|
playerArray(i).Slot(1).Equipment = Tool(GlobalDefinitions.MediumPistol(playerArray(i).Faction))
|
|
playerArray(i).Slot(2).Equipment = Tool(GlobalDefinitions.HeavyRifle(playerArray(i).Faction))
|
|
playerArray(i).Slot(3).Equipment = Tool(GlobalDefinitions.AntiVehicularLauncher(playerArray(i).Faction))
|
|
playerArray(i).Slot(4).Equipment = Tool(GlobalDefinitions.katana)
|
|
SetCharacterSelectScreenGUID(playerArray(i), gen)
|
|
val health = playerArray(i).Health
|
|
val stamina = playerArray(i).Stamina
|
|
val armor = playerArray(i).Armor
|
|
playerArray(i).Spawn
|
|
sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, playerArray(i).GUID, converter.DetailedConstructorData(playerArray(i)).get))
|
|
if(health > 0) { //player can not be dead; stay spawned as alive
|
|
playerArray(i).Health = health
|
|
playerArray(i).Stamina = stamina
|
|
playerArray(i).Armor = armor
|
|
}
|
|
sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(4), value(0).asInstanceOf[Int], playerArray(i).GUID, false, secondsSinceLastLogin))
|
|
RemoveCharacterSelectScreenGUID(playerArray(i))
|
|
}
|
|
}
|
|
sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
|
|
}
|
|
}
|
|
Thread.sleep(50)
|
|
|
|
case scala.util.Success(result) =>
|
|
if(connection.isConnected) connection.disconnect //pre-empt failWithError
|
|
failWithError(s"ListAccountCharacters: unexpected query result format - ${result.getClass}")
|
|
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect //pre-empt failWithError
|
|
failWithError(s"ListAccountCharacters: query failed - ${e.getMessage}")
|
|
}
|
|
|
|
case scala.util.Failure(e) =>
|
|
failWithError(s"ListAccountCharacters: no connection - ${e.getMessage}")
|
|
}
|
|
|
|
case VehicleLoaded(_ /*vehicle*/) => ;
|
|
//currently being handled by VehicleSpawnPad.LoadVehicle during testing phase
|
|
|
|
case Zone.ClientInitialization(zone) =>
|
|
Thread.sleep(connectionState)
|
|
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))
|
|
if(continentNumber == 11) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.NC)) // "The NC have captured the NC Sanctuary."
|
|
else if(continentNumber == 12) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.TR)) // "The TR have captured the TR Sanctuary."
|
|
else if(continentNumber == 13) sendResponse(ContinentalLockUpdateMessage(continentNumber, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary."
|
|
else 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
|
|
|
|
StopBundlingPackets()
|
|
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) =>
|
|
CancelZoningProcess()
|
|
var (pos, ori) = spawn_tube.SpecificPoint(continent.GUID(player.VehicleSeated) match {
|
|
case Some(obj : Vehicle) if !obj.Destroyed =>
|
|
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 factionChannel = s"${player.Faction}"
|
|
obj.AssignOwnership(None)
|
|
avatar.Deployables.Remove(obj)
|
|
UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent))
|
|
obj.Faction = PlanetSideEmpire.NEUTRAL
|
|
sendResponse(SetEmpireMessage(guid, PlanetSideEmpire.NEUTRAL))
|
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(playerGUID, guid, PlanetSideEmpire.NEUTRAL))
|
|
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, PlanetSideGUID(0))
|
|
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Dismiss, info))
|
|
continent.LocalEvents ! LocalServiceMessage(factionChannel, 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
|
|
continent.AvatarEvents ! 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")
|
|
if(!item.HasGUID) {
|
|
log.warn(s"DropItem: zone ${continent.Id} contents may be in disarray")
|
|
}
|
|
|
|
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 factionChannel = s"$faction"
|
|
obj.AssignOwnership(player)
|
|
obj.Faction = faction
|
|
avatar.Deployables.Add(obj)
|
|
UpdateDeployableUIElements(avatar.Deployables.UpdateUIElement(obj.Definition.Item))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent))
|
|
sendResponse(SetEmpireMessage(guid, faction))
|
|
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.SetEmpire(playerGUID, guid, faction))
|
|
val info = DeployableInfo(obj.GUID, DeployableIcon.Boomer, obj.Position, obj.Owner.getOrElse(PlanetSideGUID(0)))
|
|
sendResponse(DeployableObjectsInfoMessage(DeploymentAction.Build, info))
|
|
continent.LocalEvents ! LocalServiceMessage(factionChannel, 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")
|
|
sendResponse(ObjectDeleteMessage(item_guid, 0))
|
|
}
|
|
|
|
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 =>
|
|
continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffectLocation(player.GUID, "spawn_object_effect", obj.Position, obj.Orientation))
|
|
case GlobalDefinitions.advanced_ace =>
|
|
sendResponse(GenericObjectActionMessage(player.GUID, 53)) //put fdu down; it will be removed from the client's holster
|
|
continent.AvatarEvents ! 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 : SensorDeployable, tool, index) =>
|
|
//motion alarm sensor and sensor disruptor
|
|
StartBundlingPackets()
|
|
DeployableBuildActivity(obj)
|
|
continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.TriggerEffectInfo(player.GUID, "on", obj.GUID, true, 1000))
|
|
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))
|
|
continent.AvatarEvents ! 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, continent.AvatarEvents)(trigger)
|
|
}
|
|
StopBundlingPackets()
|
|
|
|
case WorldSessionActor.FinalizeDeployable(obj : ExplosiveDeployable, tool, index) =>
|
|
//mines
|
|
StartBundlingPackets()
|
|
DeployableBuildActivity(obj)
|
|
CommonDestroyConstructionItem(tool, index)
|
|
FindReplacementConstructionItem(tool, index)
|
|
StopBundlingPackets()
|
|
|
|
case WorldSessionActor.FinalizeDeployable(obj : ComplexDeployable, tool, index) =>
|
|
//tank_traps, spitfires, deployable field turrets and the deployable_shield_generator
|
|
StartBundlingPackets()
|
|
DeployableBuildActivity(obj)
|
|
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.Destroyed) {
|
|
//the Telepad was successfully deployed; but, before it could configure, its Router was destroyed
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@Telepad_NoDeploy_RouterLost", None))
|
|
continent.LocalEvents ! 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
|
|
continent.LocalEvents ! 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))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
|
|
}
|
|
}
|
|
StopBundlingPackets()
|
|
|
|
case WorldSessionActor.FinalizeDeployable(obj : PlanetSideGameObject with Deployable, tool, index) =>
|
|
val guid = obj.GUID
|
|
val definition = obj.Definition
|
|
StartBundlingPackets()
|
|
sendResponse(GenericObjectActionMessage(guid, 21)) //reset build cooldown
|
|
sendResponse(ObjectDeployedMessage.Failure(definition.Name))
|
|
log.warn(s"FinalizeDeployable: deployable ${definition.asInstanceOf[BaseDeployableDefinition].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() =>
|
|
LivePlayerList.Add(avatar.CharId, avatar)
|
|
traveler = new Traveler(self, continent.Id)
|
|
StartBundlingPackets()
|
|
//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))
|
|
//the following subscriptions last until character switch/logout
|
|
chatService ! Service.Join("local")
|
|
chatService ! Service.Join("squad")
|
|
chatService ! Service.Join("platoon")
|
|
chatService ! Service.Join("voice")
|
|
chatService ! Service.Join("tell")
|
|
chatService ! Service.Join("broadcast")
|
|
chatService ! Service.Join("note")
|
|
chatService ! Service.Join("gm")
|
|
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)
|
|
player.Zone match {
|
|
case Zone.Nowhere =>
|
|
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
|
case zone =>
|
|
log.info(s"Zone ${zone.Id} will now load")
|
|
loadConfZone = true
|
|
val oldZone = continent
|
|
continent = zone
|
|
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
|
|
continent.AvatarEvents ! Service.Join(player.Name)
|
|
persist()
|
|
oldZone.AvatarEvents ! Service.Leave()
|
|
oldZone.LocalEvents ! Service.Leave()
|
|
oldZone.VehicleEvents ! Service.Leave()
|
|
self ! NewPlayerLoaded(player)
|
|
}
|
|
StopBundlingPackets()
|
|
|
|
case InterstellarCluster.GiveWorld(zoneId, zone) =>
|
|
log.info(s"Zone $zoneId will now load")
|
|
loadConfZone = true
|
|
val oldZone = continent
|
|
continent = zone
|
|
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
|
|
continent.AvatarEvents ! Service.Join(player.Name)
|
|
persist()
|
|
oldZone.AvatarEvents ! Service.Leave()
|
|
oldZone.LocalEvents ! Service.Leave()
|
|
oldZone.VehicleEvents ! Service.Leave()
|
|
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 msg@Zoning.InstantAction.Located(zone, _, spawn_point) =>
|
|
//in between subsequent reply messages, it does not matter if the destination changes
|
|
//so long as there is at least one destination at all (including the fallback)
|
|
if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) {
|
|
val (pos, ori) = spawn_point.SpecificPoint(player)
|
|
SpawnThroughZoningProcess(zone, pos, ori)
|
|
}
|
|
else if(zoningStatus != Zoning.Status.None) {
|
|
instantActionFallbackDestination = Some(msg)
|
|
}
|
|
|
|
case Zoning.InstantAction.NotLocated() =>
|
|
instantActionFallbackDestination match {
|
|
case Some(Zoning.InstantAction.Located(zone, _, spawn_point)) if spawn_point.Owner.Faction == player.Faction && !spawn_point.Offline =>
|
|
if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) {
|
|
val (pos, ori) = spawn_point.SpecificPoint(player)
|
|
SpawnThroughZoningProcess(zone, pos, ori)
|
|
}
|
|
else if(zoningCounter == 0) {
|
|
CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable")
|
|
}
|
|
case _ =>
|
|
//no instant action available
|
|
CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable")
|
|
}
|
|
|
|
case Zoning.Recall.Located(zone, spawn_point) =>
|
|
if(ContemplateZoningResponse(Zoning.Recall.Request(player.Faction, zone.Id))) {
|
|
val (pos, ori) = spawn_point.SpecificPoint(player)
|
|
SpawnThroughZoningProcess(zone, pos, ori)
|
|
}
|
|
|
|
case Zoning.Recall.Denied(reason) =>
|
|
CancelZoningProcessWithReason(s"@norecall_sanctuary_$reason", Some(ChatMessageType.CMT_QUIT))
|
|
|
|
case ZoningReset() =>
|
|
CancelZoningProcess()
|
|
|
|
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
|
|
val weaponsEnabled = (continent.Map.Name != "map11" && continent.Map.Name != "map12" && continent.Map.Name != "map13")
|
|
sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100, 25, weaponsEnabled, continent.Map.Checksum))
|
|
setupAvatarFunc() //important! the LoadMapMessage must be processed by the client before the avatar is created
|
|
persist()
|
|
|
|
case PlayerLoaded(tplayer) =>
|
|
//same zone
|
|
log.info(s"Player ${tplayer.Name} will respawn")
|
|
player = tplayer
|
|
setupAvatarFunc()
|
|
persist()
|
|
self ! SetCurrentAvatar(tplayer)
|
|
|
|
case PlayerFailedToLoad(tplayer) =>
|
|
player.Continent match {
|
|
case _ =>
|
|
failWithError(s"${tplayer.Name} failed to load anywhere")
|
|
}
|
|
|
|
case SetCurrentAvatar(tplayer) =>
|
|
if(tplayer.Actor == ActorRef.noSender) {
|
|
respawnTimer = context.system.scheduler.scheduleOnce(100 milliseconds, self, SetCurrentAvatar(tplayer))
|
|
}
|
|
else {
|
|
HandleSetCurrentAvatar(tplayer)
|
|
}
|
|
|
|
case NtuCharging(tplayer, vehicle) =>
|
|
HandleNtuCharging(tplayer, vehicle)
|
|
|
|
case NtuDischarging(tplayer, vehicle, silo_guid) =>
|
|
HandleNtuDischarging(tplayer, vehicle, silo_guid)
|
|
|
|
case Vitality.DamageResolution(target : TelepadDeployable, _) =>
|
|
//telepads
|
|
if(target.Health <= 0) {
|
|
//update if destroyed
|
|
target.Destroyed = true
|
|
val guid = target.GUID
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, guid))
|
|
Deployables.AnnounceDestroyDeployable(target, Some(0 seconds))
|
|
}
|
|
|
|
case Vitality.DamageResolution(target : PlanetSideGameObject, _) =>
|
|
log.warn(s"Vital target ${target.Definition.Name} damage resolution not supported using this method")
|
|
|
|
case ResponseToSelf(pkt) =>
|
|
//log.info(s"Received a direct message: $pkt")
|
|
sendResponse(pkt)
|
|
|
|
case ReceiveAccountData(account) =>
|
|
log.info(s"Retrieved account data for accountId = ${account.AccountId}")
|
|
this.account = account
|
|
admin = account.GM
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
Database.query(connection.sendPreparedStatement(
|
|
"SELECT gm FROM accounts where id=?", Array(account.AccountId)
|
|
)).onComplete {
|
|
case scala.util.Success(queryResult) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
queryResult match {
|
|
case row : ArrayRowData => // If we got a row from the database
|
|
log.info(s"ReceiveAccountData: ready to load character list for ${account.Username}")
|
|
self ! ListAccountCharacters()
|
|
case _ => // If the account didn't exist in the database
|
|
log.error(s"ReceiveAccountData: ${account.Username} data not found, or unexpected query result format - ${queryResult.getClass}")
|
|
Thread.sleep(50)
|
|
sendResponse(DropSession(sessionId, "You should not exist!"))
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
log.error(s"ReceiveAccountData: ${e.getMessage}")
|
|
Thread.sleep(50)
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
log.error(s"RetrieveAccountData: no connection ${e.getMessage}")
|
|
}
|
|
|
|
case LoadedRemoteProjectile(projectile_guid, Some(projectile)) =>
|
|
if(projectile.profile.ExistsOnRemoteClients) {
|
|
//spawn projectile on other clients
|
|
val definition = projectile.Definition
|
|
continent.AvatarEvents ! AvatarServiceMessage(
|
|
continent.Id,
|
|
AvatarAction.LoadProjectile(player.GUID, definition.ObjectId, projectile_guid, definition.Packet.ConstructorData(projectile).get)
|
|
)
|
|
}
|
|
//immediately slated for deletion?
|
|
CleanUpRemoteProjectile(projectile.GUID, projectile)
|
|
|
|
case LoadedRemoteProjectile(projectile_guid, None) =>
|
|
continent.GUID(projectile_guid) match {
|
|
case Some(obj : Projectile) if obj.profile.ExistsOnRemoteClients =>
|
|
//spawn projectile on other clients
|
|
val definition = obj.Definition
|
|
continent.AvatarEvents ! AvatarServiceMessage(
|
|
continent.Id,
|
|
AvatarAction.LoadProjectile(player.GUID, definition.ObjectId, projectile_guid, definition.Packet.ConstructorData(obj).get)
|
|
)
|
|
case _ => ;
|
|
}
|
|
|
|
case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
|
|
log.info(s"LoginInfo: player $name is considered a new character")
|
|
//TODO poll the database for saved zone and coordinates?
|
|
persist = UpdatePersistence(sender)
|
|
//the original standard sim way to load data for this user for the user's avatar and player
|
|
import net.psforever.types.CertificationType._
|
|
val avatar = this.avatar
|
|
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 += UniMAX
|
|
avatar.Certifications += Medical
|
|
avatar.Certifications += AdvancedMedical
|
|
avatar.Certifications += Engineering
|
|
avatar.Certifications += CombatEngineering
|
|
avatar.Certifications += FortificationEngineering
|
|
avatar.Certifications += AssaultEngineering
|
|
avatar.Certifications += Hacking
|
|
avatar.Certifications += AdvancedHacking
|
|
avatar.Certifications += ElectronicsExpert
|
|
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 += BattleFrameRobotics
|
|
// avatar.Certifications += BFRAntiInfantry
|
|
// avatar.Certifications += BFRAntiAircraft
|
|
InitializeDeployableQuantities(avatar) //set deployables ui elements
|
|
AwardBattleExperiencePoints(avatar, 20000000L)
|
|
avatar.CEP = 600000
|
|
avatar.Implants(0).Unlocked = true
|
|
avatar.Implants(0).Implant = GlobalDefinitions.surge
|
|
avatar.Implants(1).Unlocked = true
|
|
avatar.Implants(1).Implant = GlobalDefinitions.audio_amplifier
|
|
avatar.Implants(2).Unlocked = true
|
|
avatar.Implants(2).Implant = GlobalDefinitions.targeting
|
|
|
|
player = new Player(avatar)
|
|
//xy-coordinates indicate sanctuary spawn bias:
|
|
player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match {
|
|
case 0 => Vector3(8192, 8192, 0) //NE
|
|
case 1 => Vector3(8192, 0, 0) //SE
|
|
case 2 => Vector3(0, 0, 0) //SW
|
|
case 3 => Vector3(0, 8192, 0) //NW
|
|
}
|
|
LoadClassicDefault(player)
|
|
LoadDataBaseLoadouts(player).onComplete {
|
|
case _ =>
|
|
UpdateLoginTimeThenDoClientInitialization()
|
|
}
|
|
|
|
case PlayerToken.LoginInfo(playerName, inZone, pos) =>
|
|
log.info(s"LoginInfo: player $playerName is already logged in zone ${inZone.Id}; rejoining that character")
|
|
persist = UpdatePersistence(sender)
|
|
//tell the old WSA to kill itself by using its own subscriptions against itself
|
|
inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection())
|
|
//find and reload previous player
|
|
(inZone.Players.find(p => p.name.equals(playerName)), inZone.LivePlayers.find(p => p.Name.equals(playerName))) match {
|
|
case (Some(a), Some(p)) if p.isAlive =>
|
|
//rejoin current avatar/player
|
|
avatar = a
|
|
player = p
|
|
persist()
|
|
setupAvatarFunc = AvatarRejoin
|
|
UpdateLoginTimeThenDoClientInitialization()
|
|
|
|
case (Some(a), Some(p)) =>
|
|
//convert player to a corpse (unless in vehicle); go to deployment map
|
|
avatar = a
|
|
player = p
|
|
persist()
|
|
player.Zone = inZone
|
|
setupAvatarFunc = AvatarDeploymentPassOver
|
|
beginZoningSetCurrentAvatarFunc = SetCurrentAvatarUponDeployment
|
|
p.Release
|
|
inZone.Population ! Zone.Population.Release(avatar)
|
|
if(p.VehicleSeated.isEmpty) {
|
|
PrepareToTurnPlayerIntoCorpse(p, inZone)
|
|
}
|
|
else {
|
|
inZone.GUID(p.VehicleSeated) match {
|
|
case Some(v : Vehicle) if v.Destroyed =>
|
|
v.Actor ! Vehicle.Deconstruct(
|
|
if(v.Flying) {
|
|
//TODO gravity
|
|
None //immediate deconstruction
|
|
}
|
|
else {
|
|
v.Definition.DeconstructionTime //normal deconstruction
|
|
})
|
|
case _ => ;
|
|
}
|
|
}
|
|
UpdateLoginTimeThenDoClientInitialization()
|
|
|
|
case (Some(a), None) =>
|
|
//respawn avatar as a new player; go to deployment map
|
|
avatar = a
|
|
player = inZone.Corpses.find(c => c.Name == playerName) match {
|
|
case Some(c) =>
|
|
c
|
|
case None =>
|
|
val tplayer = Player(a) //throwaway
|
|
tplayer.Position = pos
|
|
tplayer.Release //for proper respawn
|
|
tplayer.Zone = inZone
|
|
tplayer
|
|
}
|
|
setupAvatarFunc = AvatarDeploymentPassOver
|
|
beginZoningSetCurrentAvatarFunc = SetCurrentAvatarUponDeployment
|
|
UpdateLoginTimeThenDoClientInitialization()
|
|
|
|
case _ =>
|
|
//fall back to sanctuary/prior?
|
|
log.error(s"LoginInfo: player $playerName could not be found in game world")
|
|
self ! PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos)
|
|
}
|
|
|
|
case default =>
|
|
log.warn(s"Invalid packet class received: $default from $sender")
|
|
}
|
|
|
|
/**
|
|
* Update this player avatar for persistence.
|
|
* @param persistRef reference to the persistence monitor
|
|
*/
|
|
def UpdatePersistence(persistRef : ActorRef)() : Unit = {
|
|
persistRef ! AccountPersistenceService.Update(player.Name, continent, player.Position)
|
|
}
|
|
|
|
/**
|
|
* Do not update this player avatar for persistence.
|
|
*/
|
|
def NoPersistence() : Unit = {}
|
|
|
|
/**
|
|
* Common action to perform before starting the transition to client initialization.
|
|
* That the operation completes before client initialization begins is important.
|
|
*/
|
|
def UpdateLoginTimeThenDoClientInitialization() : Unit = {
|
|
UpdateCharacterLoginTime(avatar.CharId).onComplete {
|
|
case _ =>
|
|
cluster ! InterstellarCluster.RequestClientInitialization()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updating the character login time is an important bookkeeping aspect of a player who is (re)joining the server.
|
|
* Logging into the server or relogging from an unexpected connection loss both qualify to update the time.<br>
|
|
* <br>
|
|
* The operation requires a database connection and completion of a database transaction,
|
|
* both of which must completed independently of any subsequent tasking,
|
|
* especially if that future tasking may require database use.
|
|
* @see `Connection.sendPreparedStatement`
|
|
* @see `Database.getConnection`
|
|
* @see `Future`
|
|
* @see `java.sql.Timestamp`
|
|
* @see `Promise`
|
|
* @param charId the character unique identifier number to update in the system
|
|
* @return a `Future` predicated by the "promise" of the task being completed
|
|
*/
|
|
def UpdateCharacterLoginTime(charId : Long) : Future[Any] = {
|
|
val result : Promise[Any] = Promise[Any]()
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
Database.query(connection.sendPreparedStatement(
|
|
"UPDATE characters SET last_login = ? where id=?", Array(new java.sql.Timestamp(System.currentTimeMillis), charId)
|
|
)).onComplete {
|
|
case _ =>
|
|
if(connection.isConnected) connection.disconnect
|
|
result success true
|
|
}
|
|
case _ =>
|
|
val msg = s"UpdateCharacterLoginTime: could not update login time for $charId"
|
|
log.error(msg)
|
|
result failure new Throwable(msg)
|
|
}
|
|
result.future
|
|
}
|
|
|
|
/**
|
|
* A zoning message was received.
|
|
* That doesn't matter.
|
|
* In what stage of the zoning determination process is the client, and what is the next stage.<br>
|
|
* <br>
|
|
* To perform any actions involving zoning, an initial request must have been dispatched and marked as dispatched.
|
|
* When invoked after, the process will switch over to a countdown of time until the zoning actually occurs.
|
|
* The origin will be evaluated based on comparison of faction affinity with the client's player
|
|
* and from that an initial time and a message will be generated.
|
|
* Afterwards, the process will queue another inquiry for another zoning response.
|
|
* Each time 5s of the countdown passes, another message will be sent and received;
|
|
* and, this is another pass of the countdown.<br>
|
|
* <br>
|
|
* Once the countdown reaches 0, the transportation that has been promised by the zoning attempt may begin.
|
|
* @param nextStepMsg send this message to the `InterGalacticCluster` for the next step of the zoning process,
|
|
* if there will be a next step
|
|
* @return `true`, if the zoning transportation process should start;
|
|
* `false`, otherwise
|
|
*/
|
|
def ContemplateZoningResponse(nextStepMsg : Any) : Boolean = {
|
|
val descriptor = zoningType.toString.toLowerCase
|
|
if(zoningStatus == Zoning.Status.Request) {
|
|
DeactivateImplants()
|
|
zoningStatus = Zoning.Status.Countdown
|
|
val (time, origin) = ZoningStartInitialMessageAndTimer()
|
|
zoningCounter = time
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", s"@${descriptor}_$origin", None))
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
zoningReset.cancel
|
|
zoningTimer.cancel
|
|
zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset())
|
|
zoningTimer = context.system.scheduler.scheduleOnce(5 seconds, cluster, nextStepMsg)
|
|
false
|
|
}
|
|
else if(zoningStatus == Zoning.Status.Countdown) {
|
|
zoningCounter -= 5
|
|
zoningReset.cancel
|
|
zoningTimer.cancel
|
|
if(zoningCounter > 0) {
|
|
if(zoningCountdownMessages.contains(zoningCounter)) {
|
|
sendResponse(ChatMsg(zoningChatMessageType, false, "", s"@${descriptor}_$zoningCounter", None))
|
|
}
|
|
//again
|
|
zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset())
|
|
zoningTimer = context.system.scheduler.scheduleOnce(5 seconds, cluster, nextStepMsg)
|
|
false
|
|
}
|
|
else {
|
|
//zoning deployment
|
|
true
|
|
}
|
|
}
|
|
else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The primary method of determination involves the faction affinity of the most favorable available region subset,
|
|
* e.g., in the overlapping sphere of influences of a friendly field tower and an enemy major facility,
|
|
* the time representative of the the tower has priority.
|
|
* When no spheres of influence are being encroached, one is considered "in the wilderness".
|
|
* The messaging is different but the location is normally treated the same as if in a neutral sphere of influence.
|
|
* Being anywhere in one's faction's own sanctuary is a special case.
|
|
* @return a `Tuple` composed of the initial countdown time and the descriptor for message composition
|
|
*/
|
|
def ZoningStartInitialMessageAndTimer() : (Int, String) = {
|
|
val location = (if(Zones.SanctuaryZoneNumber(player.Faction) == continent.Number) {
|
|
Zoning.Time.Sanctuary
|
|
}
|
|
else {
|
|
val playerPosition = player.Position.xy
|
|
(continent.Buildings
|
|
.values
|
|
.filter { building =>
|
|
val radius = building.Definition.SOIRadius
|
|
Vector3.DistanceSquared(building.Position.xy, playerPosition) < radius * radius
|
|
}) match {
|
|
case Nil =>
|
|
Zoning.Time.None
|
|
case List(building) =>
|
|
if(building.Faction == player.Faction) Zoning.Time.Friendly
|
|
else if(building.Faction == PlanetSideEmpire.NEUTRAL) Zoning.Time.Neutral
|
|
else Zoning.Time.Enemy
|
|
case buildings =>
|
|
if(buildings.exists(_.Faction == player.Faction)) Zoning.Time.Friendly
|
|
else if(buildings.exists(_.Faction == PlanetSideEmpire.NEUTRAL)) Zoning.Time.Neutral
|
|
else Zoning.Time.Enemy
|
|
}
|
|
})
|
|
(location.id, location.descriptor.toLowerCase)
|
|
}
|
|
|
|
/**
|
|
* Use the zoning process using some spawnable entity in the destination zone.
|
|
* @param zone the destination zone
|
|
* @param spawnPosition the destination spawn position
|
|
* @param spawnOrientation the destination spawn orientation
|
|
*/
|
|
def SpawnThroughZoningProcess(zone : Zone, spawnPosition : Vector3, spawnOrientation : Vector3) : Unit = {
|
|
CancelZoningProcess()
|
|
PlayerActionsToCancel()
|
|
CancelAllProximityUnits()
|
|
continent.Population ! Zone.Population.Release(avatar)
|
|
val respawnTime : Long = if(zone.Number == continent.Number) {
|
|
//distract the user while he slips through the cracks of reality
|
|
GoToDeploymentMap()
|
|
1L
|
|
}
|
|
else {
|
|
//zone loading will take long enough
|
|
0L
|
|
}
|
|
LoadZonePhysicalSpawnPoint(zone.Id, spawnPosition, spawnOrientation, respawnTime)
|
|
}
|
|
|
|
/**
|
|
* You can't instant action to respond to some activity using a droppod!
|
|
* You can't.
|
|
* You just can't.
|
|
* @param zone the destination zone
|
|
* @param hotspotPosition where is the hotspot that is being addressed
|
|
* @param spawnPosition the destination spawn position (may not be related to a literal `SpawnPoint` entity)
|
|
*/
|
|
def YouCantInstantActionUsingDroppod(zone : Zone, hotspotPosition : Vector3, spawnPosition : Vector3) : Unit = {
|
|
CancelZoningProcess()
|
|
PlayerActionsToCancel()
|
|
CancelAllProximityUnits()
|
|
//find a safe drop point
|
|
var targetBuildings = zone.Buildings.values
|
|
var whereToDroppod = spawnPosition.xy
|
|
while(targetBuildings.nonEmpty) {
|
|
(targetBuildings
|
|
.filter { building =>
|
|
val radius = building.Definition.SOIRadius
|
|
Vector3.DistanceSquared(building.Position.xy, whereToDroppod) < radius * radius
|
|
}) match {
|
|
case Nil =>
|
|
//no soi interference
|
|
targetBuildings = Nil
|
|
case List(building) =>
|
|
//blocked by a single soi; find space just outside of this soi and confirm no new overlap
|
|
val radius = Vector3(0, building.Definition.SOIRadius + 5, 0)
|
|
whereToDroppod = building.Position.xy + Vector3.Rz(radius, math.abs(scala.util.Random.nextInt() % 360))
|
|
case buildings =>
|
|
//probably blocked by a facility and its tower (maximum overlap potential is 2?); find space outside of largest soi
|
|
val largestBuilding = buildings.maxBy(_.Definition.SOIRadius)
|
|
val radius = Vector3(0, largestBuilding.Definition.SOIRadius + 5, 0)
|
|
whereToDroppod = largestBuilding.Position.xy + Vector3.Rz(radius, math.abs(scala.util.Random.nextInt() % 360))
|
|
targetBuildings = buildings
|
|
}
|
|
}
|
|
//droppod action
|
|
val droppod = Vehicle(GlobalDefinitions.droppod)
|
|
droppod.Faction = player.Faction
|
|
droppod.Position = whereToDroppod.xy + Vector3.z(1024)
|
|
droppod.Orientation = Vector3.z(180) //you always seems to land looking south; don't know why
|
|
droppod.Seats(0).Occupant = player
|
|
droppod.GUID = PlanetSideGUID(0) //droppod is not registered, we must jury-rig this
|
|
droppod.Invalidate() //now, we must short-circuit the jury-rig
|
|
interstellarFerry = Some(droppod) //leverage vehicle gating
|
|
player.Position = droppod.Position
|
|
continent.Population ! Zone.Population.Release(avatar)
|
|
LoadZonePhysicalSpawnPoint(zone.Id, droppod.Position, Vector3.Zero, 0L)
|
|
/* Don't even think about it. */
|
|
}
|
|
|
|
/**
|
|
* The user no longer expects to perform a zoning event for this reason.
|
|
* @param msg the message to the user
|
|
*/
|
|
def CancelZoningProcessWithDescriptiveReason(msg : String) : Unit = {
|
|
CancelZoningProcessWithReason(s"@${zoningType.toString.toLowerCase}_$msg", Some(zoningChatMessageType))
|
|
}
|
|
|
|
/**
|
|
* The user no longer expects to perform a zoning event for this reason.
|
|
* @param msg the message to the user
|
|
* @param msgType the type of message, influencing how it is presented to the user;
|
|
* normally, this message uses the same value as `zoningChatMessageType`s
|
|
* defaults to `None`
|
|
*/
|
|
def CancelZoningProcessWithReason(msg : String, msgType : Option[ChatMessageType.Value] = None) : Unit = {
|
|
if(zoningStatus > Zoning.Status.None) {
|
|
sendResponse(ChatMsg(msgType.getOrElse(zoningChatMessageType), false, "", msg, None))
|
|
}
|
|
CancelZoningProcess()
|
|
}
|
|
|
|
/**
|
|
* The user no longer expects to perform a zoning event,
|
|
* or the process is merely resetting its internal state.
|
|
*/
|
|
def CancelZoningProcess() : Unit = {
|
|
zoningTimer.cancel
|
|
zoningReset.cancel
|
|
zoningType = Zoning.Method.None
|
|
zoningStatus = Zoning.Status.None
|
|
zoningCounter = 0
|
|
//instant action exclusive field
|
|
instantActionFallbackDestination = None
|
|
}
|
|
|
|
/**
|
|
* 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.TeardownConnection() =>
|
|
log.info("ending session by event system request (relog)")
|
|
context.stop(self)
|
|
|
|
case AvatarResponse.SendResponse(msg) =>
|
|
sendResponse(msg)
|
|
|
|
case AvatarResponse.SendResponseTargeted(target_guid, msg) =>
|
|
if(tplayer_guid == target_guid) {
|
|
sendResponse(msg)
|
|
}
|
|
|
|
case AvatarResponse.Revive(target_guid) =>
|
|
if(tplayer_guid == target_guid) {
|
|
reviveTimer.cancel
|
|
deadState = DeadState.Alive
|
|
player.Revive
|
|
val health = player.Health
|
|
sendResponse(PlanetsideAttributeMessage(target_guid, 0, health))
|
|
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, player.Position, player.Faction, true))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttributeToAll(target_guid, 0, health))
|
|
}
|
|
|
|
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() =>
|
|
sendResponse(GenericObjectActionMessage(guid, 9))
|
|
|
|
case AvatarResponse.EnvironmentalDamage(target, source, amount) =>
|
|
if(player.isAlive && amount > 0) {
|
|
val playerGUID = player.GUID
|
|
val armor = player.Armor
|
|
val capacitor = player.Capacitor
|
|
val originalHealth = player.Health
|
|
//history
|
|
continent.GUID(source) match {
|
|
case Some(obj : Painbox) =>
|
|
player.History(DamageFromPainbox(PlayerSource(player), obj, amount))
|
|
case _ => ;
|
|
}
|
|
CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
|
player.Health = originalHealth - amount
|
|
sendResponse(PlanetsideAttributeMessage(target, 0, player.Health))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(target, 0, player.Health))
|
|
damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$armor/$capacitor, AFTER=${player.Health}/$armor/$capacitor, CHANGE=$amount/0/0")
|
|
if(player.Health == 0 && player.isAlive) {
|
|
player.Actor ! Player.Die()
|
|
}
|
|
}
|
|
|
|
case AvatarResponse.DeactivateImplantSlot(slot) =>
|
|
sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid), ImplantAction.Activation, slot, 0))
|
|
|
|
case AvatarResponse.ActivateImplantSlot(slot) =>
|
|
sendResponse(AvatarImplantMessage(PlanetSideGUID(player.GUID.guid), ImplantAction.Activation, slot, 1))
|
|
|
|
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.GenericObjectAction(object_guid, action_code) =>
|
|
if(tplayer_guid != guid) {
|
|
sendResponse(GenericObjectActionMessage(object_guid, action_code))
|
|
}
|
|
|
|
case AvatarResponse.HitHint(source_guid) =>
|
|
if(player.isAlive) {
|
|
sendResponse(HitHint(source_guid, guid))
|
|
CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
|
}
|
|
|
|
case AvatarResponse.Killed() =>
|
|
val respawnTimer = 300000 //milliseconds
|
|
ToggleMaxSpecialState(enable = false)
|
|
zoningStatus = Zoning.Status.None
|
|
deadState = DeadState.Dead
|
|
continent.GUID(player.VehicleSeated) match {
|
|
case Some(obj : Vehicle) =>
|
|
TotalDriverVehicleControl(obj)
|
|
UnAccessContents(obj)
|
|
case _ => ;
|
|
}
|
|
PlayerActionsToCancel()
|
|
CancelAllProximityUnits()
|
|
CancelZoningProcessWithDescriptiveReason("cancel")
|
|
if(shotsWhileDead > 0) {
|
|
log.warn(s"KillPlayer/SHOTS_WHILE_DEAD: client of ${avatar.name} fired $shotsWhileDead rounds while character was dead on server")
|
|
shotsWhileDead = 0
|
|
}
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(player.Faction), player, 7))
|
|
|
|
case AvatarResponse.LoadPlayer(pkt) =>
|
|
if(tplayer_guid != guid) {
|
|
sendResponse(pkt)
|
|
}
|
|
|
|
case AvatarResponse.LoadProjectile(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.PlanetsideAttributeToAll(attribute_type, attribute_value) =>
|
|
sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value))
|
|
|
|
case AvatarResponse.PlanetsideAttributeSelf(attribute_type, attribute_value) =>
|
|
if(tplayer_guid == guid) {
|
|
sendResponse(PlanetsideAttributeMessage(guid, attribute_type, attribute_value))
|
|
}
|
|
|
|
case AvatarResponse.PlayerState(pos, vel, yaw, pitch, yaw_upper, seq_time, is_crouching, is_jumping, jump_thrust, is_cloaking, spectating, weaponInHand) =>
|
|
if(tplayer_guid != guid) {
|
|
val now = System.currentTimeMillis()
|
|
val (location, time, distanceSq) : (Vector3, Long, Float) = if(spectating) {
|
|
val r = new scala.util.Random
|
|
val r1 = 2 + r.nextInt(30)
|
|
val r2 = 2 + r.nextInt(4000)
|
|
(Vector3(r2, r2, r1), 0L, 0f)
|
|
}
|
|
else {
|
|
val before = player.lastSeenStreamMessage(guid.guid)
|
|
val dist = Vector3.DistanceSquared(player.Position, pos)
|
|
(pos, now - before, dist)
|
|
}
|
|
if(spectating ||
|
|
((distanceSq < 900 || weaponInHand) && time > 200) ||
|
|
(distanceSq < 10000 && time > 500) ||
|
|
(distanceSq < 160000 && (
|
|
(is_jumping || time < 200)) ||
|
|
(!WorldEntity.isMoving(vel) && time > 2000) ||
|
|
(time > 1000)) ||
|
|
(distanceSq > 160000 && time > 5000)) {
|
|
sendResponse(
|
|
PlayerStateMessage(
|
|
guid,
|
|
location,
|
|
vel,
|
|
yaw,
|
|
pitch,
|
|
yaw_upper,
|
|
timestamp = 0,
|
|
is_crouching,
|
|
is_jumping,
|
|
jump_thrust,
|
|
is_cloaking
|
|
)
|
|
)
|
|
player.lastSeenStreamMessage(guid.guid) = now
|
|
}
|
|
}
|
|
|
|
case AvatarResponse.ProjectileExplodes(projectile_guid, projectile) =>
|
|
sendResponse(ProjectileStateMessage(projectile_guid, projectile.Position, Vector3.Zero, projectile.Orientation, 0, true, PlanetSideGUID(0)))
|
|
sendResponse(ObjectDeleteMessage(projectile_guid, 2))
|
|
|
|
case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
|
|
sendResponse(GenericActionMessage(mode))
|
|
|
|
case AvatarResponse.ProjectileState(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) =>
|
|
if(tplayer_guid != guid) {
|
|
sendResponse(ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid))
|
|
}
|
|
|
|
case AvatarResponse.PutDownFDU(target) =>
|
|
if(tplayer_guid != guid) {
|
|
sendResponse(GenericObjectActionMessage(target, 53))
|
|
}
|
|
|
|
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))
|
|
continent.LocalEvents ! 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))
|
|
continent.LocalEvents ! 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.Detonate(guid, obj : BoomerDeployable) =>
|
|
sendResponse(TriggerEffectMessage(guid, "detonate_boomer"))
|
|
sendResponse(PlanetsideAttributeMessage(guid, 29, 1))
|
|
sendResponse(ObjectDeleteMessage(guid, 0))
|
|
|
|
case LocalResponse.Detonate(guid, obj : ExplosiveDeployable) =>
|
|
sendResponse(GenericObjectActionMessage(guid, 19))
|
|
sendResponse(PlanetsideAttributeMessage(guid, 29, 1))
|
|
sendResponse(ObjectDeleteMessage(guid, 0))
|
|
|
|
case LocalResponse.Detonate(guid, obj) =>
|
|
log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
|
|
|
|
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.Destroyed) {
|
|
DeconstructDeployable(obj, guid, pos)
|
|
}
|
|
else {
|
|
obj.Destroyed = true
|
|
DeconstructDeployable(obj, guid, pos, obj.Orientation, if(obj.MountPoints.isEmpty) 2
|
|
else 1)
|
|
}
|
|
|
|
case LocalResponse.EliminateDeployable(obj : ExplosiveDeployable, guid, pos) =>
|
|
if(obj.Destroyed || obj.Jammed || obj.Health == 0) {
|
|
DeconstructDeployable(obj, guid, pos)
|
|
}
|
|
else {
|
|
obj.Destroyed = true
|
|
DeconstructDeployable(obj, guid, pos, obj.Orientation, 2)
|
|
}
|
|
|
|
case LocalResponse.EliminateDeployable(obj : ComplexDeployable, guid, pos) =>
|
|
if(obj.Destroyed) {
|
|
DeconstructDeployable(obj, guid, pos)
|
|
}
|
|
else {
|
|
obj.Destroyed = true
|
|
DeconstructDeployable(obj, guid, pos, obj.Orientation, 1)
|
|
}
|
|
|
|
case LocalResponse.EliminateDeployable(obj : TelepadDeployable, guid, pos) =>
|
|
//if active, deactivate
|
|
if(obj.Active) {
|
|
obj.Active = false
|
|
sendResponse(GenericObjectActionMessage(guid, 29))
|
|
sendResponse(GenericObjectActionMessage(guid, 30))
|
|
}
|
|
//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.Destroyed) {
|
|
DeconstructDeployable(obj, guid, pos)
|
|
}
|
|
else {
|
|
obj.Destroyed = true
|
|
DeconstructDeployable(obj, guid, pos, obj.Orientation, 2)
|
|
}
|
|
|
|
case LocalResponse.EliminateDeployable(obj, guid, pos) =>
|
|
if(obj.Destroyed) {
|
|
DeconstructDeployable(obj, guid, pos)
|
|
}
|
|
else {
|
|
obj.Destroyed = true
|
|
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) =>
|
|
HackObject(target_guid, unk1, unk2)
|
|
|
|
case LocalResponse.HackCaptureTerminal(target_guid, unk1, unk2, isResecured) =>
|
|
HackCaptureTerminal(target_guid, unk1, unk2, isResecured)
|
|
|
|
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 LocalResponse.UpdateForceDomeStatus(building_guid, activated) => {
|
|
if(activated) {
|
|
sendResponse(GenericObjectActionMessage(building_guid, 11))
|
|
}
|
|
else {
|
|
sendResponse(GenericObjectActionMessage(building_guid, 12))
|
|
}
|
|
}
|
|
|
|
case LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) => {
|
|
if(tplayer_guid == guid) {
|
|
continent.GUID(vehicle_guid) match {
|
|
case Some(vehicle : Mountable with MountedWeapons) =>
|
|
vehicle.PassengerInSeat(player) match {
|
|
case Some(seat_num : Int) =>
|
|
vehicle.WeaponControlledFromSeat(seat_num) match {
|
|
case Some(equipment) if equipment.GUID == weapon_guid =>
|
|
val weapon = equipment.asInstanceOf[Tool]
|
|
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
|
|
case _ => ;
|
|
}
|
|
case _ => ;
|
|
}
|
|
case _ => ;
|
|
}
|
|
}
|
|
}
|
|
|
|
case _ => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param toChannel na
|
|
* @param avatar_guid na
|
|
* @param target na
|
|
* @param reply na
|
|
*/
|
|
def HandleChatServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, avatar_name : String, cont : Zone, avatar_pos : Vector3, avatar_faction : PlanetSideEmpire.Value, target : Int, reply : ChatMsg) : Unit = {
|
|
val tplayer_guid = if(player.HasGUID) player.GUID
|
|
else PlanetSideGUID(0)
|
|
target match {
|
|
case 0 => // for other(s) user(s)
|
|
if(player.GUID != avatar_guid) {
|
|
reply.messageType match {
|
|
case ChatMessageType.CMT_TELL =>
|
|
if (player.Name.equalsIgnoreCase(reply.recipient)) {
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, avatar_name, reply.contents, reply.note))
|
|
}
|
|
case ChatMessageType.CMT_SILENCE =>
|
|
val args = avatar_name.split(" ")
|
|
var silence_name : String = ""
|
|
var silence_time : Int = 5
|
|
if(args.length == 1) {
|
|
silence_name = args(0)
|
|
}
|
|
else if(args.length == 2) {
|
|
silence_name = args(0)
|
|
silence_time = args(1).toInt
|
|
}
|
|
if(player.Name == args(0)) {
|
|
if(!player.silenced) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_71, reply.wideContents, reply.recipient, "@silence_on", reply.note))
|
|
player.silenced = true
|
|
context.system.scheduler.scheduleOnce(silence_time minutes, chatService, ChatServiceMessage("gm", ChatAction.GM(PlanetSideGUID(0), player.Name, ChatMsg(ChatMessageType.CMT_SILENCE, true, "", player.Name, None))))
|
|
}
|
|
else {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_71, reply.wideContents, reply.recipient, "@silence_off", reply.note))
|
|
player.silenced = false
|
|
}
|
|
}
|
|
case _ =>
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
}
|
|
case 1 => // for player
|
|
if(player.Name == avatar_name) {
|
|
if((reply.contents.length > 1 && (reply.contents.dropRight(reply.contents.length - 1) != "!" || reply.contents.drop(1).dropRight(reply.contents.length - 2) == "!")) || reply.contents.length == 1) {
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
}
|
|
case 2 => // both case
|
|
if((reply.contents.length > 1 && (reply.contents.dropRight(reply.contents.length - 1) != "!" || reply.contents.drop(1).dropRight(reply.contents.length - 2) == "!")) || reply.contents.length == 1) {
|
|
reply.messageType match {
|
|
case ChatMessageType.CMT_OPEN =>
|
|
if(Vector3.Distance(player.Position, avatar_pos) < 25 && player.Faction == avatar_faction && player.Continent == cont.Id) {
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
case ChatMessageType.CMT_COMMAND =>
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
case ChatMessageType.CMT_SQUAD =>
|
|
if(squadChannel.nonEmpty) {
|
|
if("/Chat/"+squadChannel.get == toChannel) {
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
}
|
|
case ChatMessageType.CMT_PLATOON =>
|
|
if (player.Faction == avatar_faction) {
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
case ChatMessageType.CMT_VOICE =>
|
|
if(Vector3.Distance(player.Position, avatar_pos) < 25 && player.Continent == cont.Id) {
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
case _ =>
|
|
sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param tplayer na
|
|
* @param reply na
|
|
*/
|
|
def HandleMountMessages(tplayer : Player, reply : Mountable.Exchange) : Unit = {
|
|
reply match {
|
|
case Mountable.CanMount(obj : ImplantTerminalMech, seat_num) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
CancelAllProximityUnits()
|
|
MountingAction(tplayer, obj, seat_num)
|
|
|
|
case Mountable.CanMount(obj : Vehicle, seat_num) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
|
val obj_guid : PlanetSideGUID = obj.GUID
|
|
val player_guid : PlanetSideGUID = tplayer.GUID
|
|
log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num")
|
|
CancelAllProximityUnits()
|
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 0, obj.Health))
|
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 68, obj.Shields)) //shield health
|
|
if(obj.Definition.MaxNtuCapacitor > 0) {
|
|
val ntuCapacitor = scala.math.ceil((obj.NtuCapacitor.toFloat / obj.Definition.MaxNtuCapacitor.toFloat) * 10).toInt
|
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 45, ntuCapacitor))
|
|
}
|
|
if(obj.Definition.MaxCapacitor > 0) {
|
|
val capacitor = scala.math.ceil((obj.Capacitor.toFloat / obj.Definition.MaxCapacitor.toFloat) * 10).toInt
|
|
sendResponse(PlanetsideAttributeMessage(obj_guid, 113, capacitor))
|
|
}
|
|
if(seat_num == 0) {
|
|
//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 => ;
|
|
}
|
|
Vehicles.Own(obj, tplayer)
|
|
if(obj.Definition == GlobalDefinitions.quadstealth) {
|
|
//wraith cloak state matches the cloak state of the driver
|
|
//phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks
|
|
obj.Cloaked = tplayer.Cloaked
|
|
}
|
|
}
|
|
AccessContents(obj)
|
|
UpdateWeaponAtSeatPosition(obj, seat_num)
|
|
MountingAction(tplayer, obj, seat_num)
|
|
|
|
case Mountable.CanMount(obj : FacilityTurret, seat_num) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
|
if(!obj.isUpgrading) {
|
|
if(obj.Definition == GlobalDefinitions.vanu_sentry_turret) {
|
|
obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.Id, LocalAction.SetEmpire(obj.GUID, player.Faction))
|
|
}
|
|
sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health))
|
|
UpdateWeaponAtSeatPosition(obj, seat_num)
|
|
MountingAction(tplayer, obj, seat_num)
|
|
}
|
|
else {
|
|
log.warn(s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating")
|
|
}
|
|
|
|
case Mountable.CanMount(obj : PlanetSideGameObject with WeaponTurret, seat_num) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
|
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) if obj.Definition == GlobalDefinitions.droppod =>
|
|
UnAccessContents(obj)
|
|
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 {
|
|
continent.VehicleEvents ! 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)
|
|
val lTime = System.currentTimeMillis
|
|
var changeArmor : Boolean = true
|
|
if(lTime - whenUsedLastMAX(subtype) < 300000) {
|
|
changeArmor = false
|
|
}
|
|
if(changeArmor && exosuit.id == 2) {
|
|
for(i <- 1 to 3) {
|
|
sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, whenUsedLastMAXName(i), 300, true))
|
|
whenUsedLastMAX(i) = lTime
|
|
}
|
|
}
|
|
if(originalSuit != exosuit || originalSubtype != subtype && changeArmor) {
|
|
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))
|
|
continent.AvatarEvents ! 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))
|
|
continent.AvatarEvents ! 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 =>
|
|
continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(tplayer.GUID, elem.obj.GUID))
|
|
})
|
|
//report change
|
|
sendResponse(ArmorChangedMessage(tplayer.GUID, exosuit, subtype))
|
|
continent.AvatarEvents ! 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
|
|
)
|
|
)
|
|
continent.AvatarEvents ! 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))
|
|
//sanitize exo-suit for change
|
|
val originalSuit = player.ExoSuit
|
|
val originalSubtype = Loadout.DetermineSubtype(tplayer)
|
|
//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
|
|
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, player.Faction).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
|
|
}) {
|
|
val lTime = System.currentTimeMillis
|
|
if(lTime - whenUsedLastMAX(subtype) < 300000) { // PTS v3 hack
|
|
(originalSuit, subtype)
|
|
}
|
|
else {
|
|
if(lTime - whenUsedLastMAX(subtype) > 300000 && subtype != 0) {
|
|
for(i <- 1 to 3) {
|
|
sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, whenUsedLastMAXName(i), 300, true))
|
|
whenUsedLastMAX(i) = lTime
|
|
}
|
|
}
|
|
(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))
|
|
continent.AvatarEvents ! 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))
|
|
continent.AvatarEvents ! 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, player.Faction)
|
|
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 =>
|
|
continent.AvatarEvents ! 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))
|
|
continent.AvatarEvents ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ArmorChanged(tplayer.GUID, nextSuit, nextSubtype))
|
|
if(nextSuit == ExoSuitType.MAX) {
|
|
val (maxWeapons, otherWeapons) = afterHolsters.partition(entry => {
|
|
entry.obj.Size == EquipmentSize.Max
|
|
})
|
|
val weapon = maxWeapons.headOption match {
|
|
case Some(mweapon) =>
|
|
mweapon.obj
|
|
case None =>
|
|
Tool(GlobalDefinitions.MAXArms(nextSubtype, tplayer.Faction))
|
|
}
|
|
taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, weapon, 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
|
|
continent.VehicleEvents ! 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))
|
|
player.Actor ! Player.ImplantInitializationStart(slot)
|
|
}
|
|
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")
|
|
player.Actor ! Player.UninitializeImplant(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 lTime = System.currentTimeMillis
|
|
if(lTime - whenUsedLastItem(vehicle.Definition.ObjectId) > 300000) {
|
|
whenUsedLastItem(vehicle.Definition.ObjectId) = lTime
|
|
whenUsedLastItemName(vehicle.Definition.ObjectId) = msg.item_name
|
|
sendResponse(AvatarVehicleTimerMessage(tplayer.GUID, msg.item_name, 300, true))
|
|
val toFaction = tplayer.Faction
|
|
val pad = continent.GUID(pad_guid).get.asInstanceOf[VehicleSpawnPad]
|
|
vehicle.Faction = toFaction
|
|
vehicle.Position = pad.Position
|
|
vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
|
|
//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 ! RegisterVehicleFromSpawnPad(vehicle, pad)
|
|
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, true))
|
|
}
|
|
else {
|
|
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, false))
|
|
}
|
|
|
|
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) =>
|
|
sendResponse(GenericObjectActionMessage(player_guid, 9))
|
|
|
|
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 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
|
|
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))
|
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
|
}
|
|
|
|
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
|
|
player.VehicleOwned = Some(vehicle_guid)
|
|
sendResponse(PlanetsideAttributeMessage(tplayer_guid, 21, vehicle_guid))
|
|
}
|
|
|
|
case VehicleResponse.PlanetsideAttribute(vehicle_guid, attribute_type, attribute_value) =>
|
|
if(tplayer_guid != guid) {
|
|
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type, attribute_value))
|
|
}
|
|
|
|
case VehicleResponse.ResetSpawnPad(pad_guid) =>
|
|
sendResponse(GenericObjectActionMessage(pad_guid, 23))
|
|
|
|
case VehicleResponse.RevealPlayer(player_guid) =>
|
|
sendResponse(GenericObjectActionMessage(player_guid, 10))
|
|
|
|
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) =>
|
|
//if(tplayer_guid != 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, vehicle_to_delete) =>
|
|
if(tplayer_guid != guid) {
|
|
interstellarFerry = Some(vehicle)
|
|
interstellarFerryTopLevelGUID = Some(vehicle_to_delete)
|
|
continent.VehicleEvents ! Service.Leave(Some(old_channel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
|
|
galaxyService ! Service.Join(temp_channel) //temporary vehicle-specific channel
|
|
}
|
|
|
|
case VehicleResponse.KickCargo(vehicle, speed, delay) =>
|
|
if(player.VehicleSeated.nonEmpty && deadState == DeadState.Alive) {
|
|
if(speed > 0) {
|
|
val strafe = if(Vehicles.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, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, 0, delay)))
|
|
}
|
|
else {
|
|
controlled = None
|
|
sendResponse(ServerVehicleOverrideMsg(false, false, false, false, 0, 0, 0, None))
|
|
}
|
|
}
|
|
|
|
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, pad) =>
|
|
val vehicle_guid = vehicle.GUID
|
|
PlayerActionsToCancel()
|
|
CancelAllProximityUnits()
|
|
if(player.VisibleSlots.contains(player.DrawnSlot)) {
|
|
player.DrawnSlot = Player.HandsDownSlot
|
|
sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true))
|
|
continent.AvatarEvents ! 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
|
|
vehicle.Actor ! Mountable.TryMount(player, 0)
|
|
|
|
case VehicleResponse.PlayerSeatedInVehicle(vehicle, pad) =>
|
|
val vehicle_guid = vehicle.GUID
|
|
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on
|
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
|
ServerVehicleLock(vehicle)
|
|
|
|
case VehicleResponse.ServerVehicleOverrideStart(vehicle, pad) =>
|
|
val vdef = vehicle.Definition
|
|
ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef) : Int)
|
|
|
|
case VehicleResponse.ServerVehicleOverrideEnd(vehicle, pad) =>
|
|
DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2)
|
|
|
|
case VehicleResponse.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 _ => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) = CargoBehavior.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)
|
|
}
|
|
|
|
/**
|
|
* 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.NtuCapacitor < vehicle.Definition.MaxNtuCapacitor) {
|
|
// Charging
|
|
vehicle.NtuCapacitor += 100
|
|
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.ceil((vehicle.NtuCapacitor.toFloat / vehicle.Definition.MaxNtuCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on
|
|
continent.AvatarEvents ! 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.NtuCapacitor.toFloat / vehicle.Definition.MaxNtuCapacitor.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")
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 0L)) // orb particle effect off
|
|
continent.AvatarEvents ! 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.NtuCapacitor > 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.NtuCapacitor, 100), (silo.MaximumCharge - silo.ChargeLevel))
|
|
vehicle.NtuCapacitor -= chargeToDeposit
|
|
silo.Actor ! ResourceSilo.UpdateChargeLevel(chargeToDeposit)
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 1L)) // panel glow on & orb particles on
|
|
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.ceil((vehicle.NtuCapacitor.toFloat / vehicle.Definition.MaxNtuCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI
|
|
|
|
//todo: grant BEP to user
|
|
//todo: grant BEP to squad in range
|
|
|
|
//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.NtuCapacitor > 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))
|
|
continent.AvatarEvents ! 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.NtuCapacitor} 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))
|
|
continent.AvatarEvents ! 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
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off
|
|
antDischargingTick.cancel()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the message that indicates the level of completion of a process.
|
|
* The process is any form of user-driven activity with a certain eventual outcome
|
|
* but indeterminate progress feedback per cycle.<br>
|
|
* <br>
|
|
* This task is broken down into the "progression" from its initial state to the eventual outcome
|
|
* as is reported back to the player through some means of messaging window feedback.
|
|
* Though common in practice, this is not a requirement
|
|
* and the progress can accumulate without a user reportable method.
|
|
* To ensure that completion is reported properly,
|
|
* an exception is made that 99% completion is accounted uniquely
|
|
* before the final 100% is achieved.
|
|
* If the background process recording value is never set before running the initial operation
|
|
* or gets unset by failing a `tickAction` check
|
|
* the process is stopped.
|
|
* @see `progressBarUpdate`
|
|
* @see `progressBarValue`
|
|
* @see `WorldSessionActor.Progress`
|
|
* @param delta how much the progress changes each tick
|
|
* @param completeAction a custom action performed once the process is completed
|
|
* @param tickAction an optional action is is performed for each tick of progress;
|
|
* also performs a continuity check to determine if the process has been disrupted
|
|
*/
|
|
def HandleProgressChange(delta : Float, completionAction : () => Unit, tickAction : Float => Boolean) : Unit = {
|
|
progressBarUpdate.cancel
|
|
progressBarValue match {
|
|
case Some(value) =>
|
|
val next = value + delta
|
|
if(value >= 100f) {
|
|
//complete
|
|
progressBarValue = None
|
|
tickAction(100)
|
|
completionAction()
|
|
}
|
|
else if(value < 100f && next >= 100f) {
|
|
if(tickAction(99)) {
|
|
//will complete after this turn
|
|
progressBarValue = Some(next)
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
progressBarUpdate = context.system.scheduler.scheduleOnce(100 milliseconds, self,
|
|
ProgressEvent(delta, completionAction, tickAction)
|
|
)
|
|
}
|
|
else {
|
|
progressBarValue = None
|
|
}
|
|
}
|
|
else {
|
|
if(tickAction(next)) {
|
|
//normal progress activity
|
|
progressBarValue = Some(next)
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self,
|
|
ProgressEvent(delta, completionAction, tickAction)
|
|
)
|
|
}
|
|
else {
|
|
progressBarValue = None
|
|
}
|
|
}
|
|
case None => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instruct the client to treat this player as the avatar.
|
|
* Initialize all client-specific data that is dependent on some player beign decalred the "avatar".
|
|
* @param tplayer the target player
|
|
*/
|
|
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, shiftPosition.getOrElse(tplayer.Position), shiftOrientation.getOrElse(tplayer.Orientation).z)))
|
|
shiftPosition = None
|
|
shiftOrientation = None
|
|
if(player.spectator) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None))
|
|
}
|
|
(0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => {
|
|
val implantSlot = player.ImplantSlot(slot)
|
|
if(implantSlot.Initialized) {
|
|
sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
|
|
}
|
|
else {
|
|
player.Actor ! Player.ImplantInitializationStart(slot)
|
|
}
|
|
//TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
|
|
// for now, just write into slots 2, 3 and 4
|
|
Shortcut.ImplantsMap(implantSlot.Implant) match {
|
|
case Some(shortcut : Shortcut) =>
|
|
sendResponse(CreateShortcutMessage(guid, slot + 2, 0, addShortcut = true, Some(shortcut)))
|
|
case _ => log.warn(s"Could not find shortcut for implant ${implantSlot.Implant.toString()}")
|
|
}
|
|
})
|
|
|
|
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.Platoon, 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))
|
|
//looking for squad (members)
|
|
if(tplayer.LFS || lfsm) {
|
|
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
|
|
}
|
|
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"))
|
|
//squad stuff (loadouts, assignment)
|
|
squadSetup()
|
|
//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
|
|
continent.VehicleEvents ! VehicleServiceMessage(s"${tplayer.Faction}", VehicleAction.Ownership(guid, vehicle.GUID))
|
|
case _ =>
|
|
player.VehicleOwned = None
|
|
}
|
|
GetVehicleAndSeat() match {
|
|
//we're falling
|
|
case (Some(vehicle), _) if vehicle.Definition == GlobalDefinitions.droppod =>
|
|
sendResponse(DroppodFreefallingMessage(
|
|
vehicle.GUID,
|
|
vehicle.Position + Vector3.z(50),
|
|
Vector3.z(-999),
|
|
vehicle.Position + Vector3.z(25),
|
|
Vector3(0, 70.3125f, 90), Vector3(0, 0, 90)
|
|
))
|
|
case (Some(vehicle), Some(0)) =>
|
|
//summon any passengers and cargo vehicles left behind on previous continent
|
|
LoadZoneTransferPassengerMessages(
|
|
guid,
|
|
continent.Id,
|
|
vehicle
|
|
)
|
|
case _ => ;
|
|
}
|
|
interstellarFerryTopLevelGUID = None
|
|
if(loadConfZone && connectionState == 100) {
|
|
configZone(continent)
|
|
loadConfZone = false
|
|
}
|
|
if(noSpawnPointHere) {
|
|
RequestSanctuaryZoneSpawn(player, continent.Number)
|
|
}
|
|
else if(player.Health == 0) {
|
|
//player died during setup; probably a relog
|
|
player.Actor ! Player.Die()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instruct the client to treat this player as the avatar.
|
|
* @see `SetCurrentAvatar`
|
|
* @param tplayer the target player
|
|
*/
|
|
def SetCurrentAvatarNormally(tplayer : Player) : Unit = {
|
|
self ! SetCurrentAvatar(tplayer)
|
|
}
|
|
|
|
/**
|
|
* An interruption of the normal procedure -
|
|
* "instruct the client to treat this player as the avatar" -
|
|
* in order to locate a spawn point for this player.
|
|
* After a spawn point is located, the actual avatar designation will be made.
|
|
* @see `beginZoningSetCurrentAvatarFunc`
|
|
* @see `SetCurrentAvatarNormally`
|
|
* @see `Zone.Lattice.RequestSpawnPoint`
|
|
* @param tplayer the target player
|
|
*/
|
|
def SetCurrentAvatarUponDeployment(tplayer : Player) : Unit = {
|
|
beginZoningSetCurrentAvatarFunc = SetCurrentAvatarNormally
|
|
continent.Actor ! Zone.Lattice.RequestSpawnPoint(continent.Number, tplayer, 0)
|
|
}
|
|
|
|
/**
|
|
* These messages are dispatched when first starting up the client and connecting to the server for the first time.
|
|
* While many of thee messages will be reused for other situations, they appear in this order only during startup.
|
|
*/
|
|
def FirstTimeSquadSetup() : Unit = {
|
|
sendResponse(SquadDetailDefinitionUpdateMessage.Init)
|
|
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
|
|
//only need to load these once - they persist between zone transfers and respawns
|
|
avatar.SquadLoadouts.Loadouts.foreach {
|
|
case (index, loadout : SquadLoadout) =>
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task)))
|
|
}
|
|
//non-squad GUID-0 counts as the settings when not joined with a squad
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.AssociateWithSquad()))
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad()))
|
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
|
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
|
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId())
|
|
squadSetup = RespawnSquadSetup
|
|
}
|
|
|
|
/**
|
|
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
|
|
* By using `squadUI` to maintain relevant information about squad members,
|
|
* especially the unique character identifier number,
|
|
* only the zone-specific squad members will receive the important messages about their squad member's spawn.
|
|
*/
|
|
def RespawnSquadSetup() : Unit = {
|
|
if(squadUI.nonEmpty) {
|
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
|
|
continent.AvatarEvents ! AvatarServiceMessage(s"${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id))
|
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, squadUI(player.CharId).index))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
|
|
* During a zone change,
|
|
* on top of other squad mates in the zone needing to have their knowledge of this player's squad colors changed,
|
|
* the player must also set squad colors for each other squad members.
|
|
* Default respawn functionality may resume afterwards.
|
|
*/
|
|
def ZoneChangeSquadSetup() : Unit = {
|
|
RespawnSquadSetup()
|
|
GiveSquadColorsInZone()
|
|
squadSetup = RespawnSquadSetup
|
|
}
|
|
|
|
/**
|
|
* Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
|
*/
|
|
def GiveSquadColorsInZone() : Unit = {
|
|
GiveSquadColorsInZone(squadUI.keys, squad_supplement_id)
|
|
}
|
|
|
|
/**
|
|
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
|
* @param members members of the squad to target
|
|
*/
|
|
def GiveSquadColorsInZone(members : Iterable[Long]) : Unit = {
|
|
GiveSquadColorsInZone(members, squad_supplement_id)
|
|
}
|
|
|
|
/**
|
|
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
|
|
* @see `PlanetsideAttributeMessage`
|
|
* @param members members of the squad to target
|
|
* @param value the assignment value
|
|
*/
|
|
def GiveSquadColorsInZone(members : Iterable[Long], value : Long) : Unit = {
|
|
SquadMembersInZone(members).foreach {
|
|
members => sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For the listed squad member unique character identifier numbers,
|
|
* find and return all squad members in the current zone.
|
|
* @param members members of the squad to target
|
|
* @return a list of `Player` objects
|
|
*/
|
|
def SquadMembersInZone(members : Iterable[Long]) : Iterable[Player] = {
|
|
val players = continent.LivePlayers
|
|
for {
|
|
charId <- members
|
|
player = players.find {
|
|
_.CharId == charId
|
|
}
|
|
if player.nonEmpty
|
|
} yield player.get
|
|
}
|
|
|
|
def handleControlPkt(pkt : PlanetSideControlPacket) = {
|
|
pkt match {
|
|
case sync@ControlSync(diff, _, _, _, _, _, fa, fb) =>
|
|
log.trace(s"SYNC: $sync")
|
|
val nextDiff = if(diff == 65535) {
|
|
0
|
|
}
|
|
else {
|
|
diff + 1
|
|
}
|
|
val serverTick = ServerTick
|
|
sendResponse(ControlSyncResp(nextDiff, serverTick, fa, fb, fb, fa))
|
|
|
|
case TeardownConnection(_) =>
|
|
log.info("Good bye")
|
|
|
|
case default =>
|
|
log.warn(s"Unhandled ControlPacket $default")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a measure of server time as an unsigned 32-bit integer.
|
|
* The server time started at 0 back at the beginning (POSIX time).
|
|
* The server time will loop around to 0 again to maintain datatype integrity.
|
|
* @see `Int.MaxValue`
|
|
* @see `System.nanoTime`
|
|
* @return a number that indicates server tick time
|
|
*/
|
|
def ServerTick : Long = {
|
|
serverTime = System.currentTimeMillis() & unsignedIntMaxValue
|
|
serverTime
|
|
}
|
|
|
|
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")
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, false, "", "", None))
|
|
Thread.sleep(40)
|
|
import scala.concurrent.ExecutionContext.Implicits.global
|
|
clientKeepAlive.cancel
|
|
clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
|
|
accountIntermediary ! RetrieveAccountData(token)
|
|
|
|
case msg@MountVehicleCargoMsg(player_guid, cargo_guid, carrier_guid, unk4) =>
|
|
log.info(msg.toString)
|
|
(continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
|
|
case (Some(cargo : Vehicle), Some(carrier : Vehicle)) =>
|
|
carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
|
|
case Some((mountPoint, _)) => //try begin the mount process
|
|
cargo.Actor ! CargoBehavior.CheckCargoMounting(carrier_guid, mountPoint, 0)
|
|
case _ =>
|
|
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 - $carrier_guid or $cargo_guid")
|
|
case _ => ;
|
|
}
|
|
|
|
case msg@DismountVehicleCargoMsg(player_guid, cargo_guid, bailed, requestedByPassenger, kicked) =>
|
|
log.info(msg.toString)
|
|
//when kicked by carrier driver, player_guid will be PlanetSideGUID(0)
|
|
//when exiting of the cargo vehicle driver's own accord, player_guid will be the cargo vehicle driver
|
|
continent.GUID(cargo_guid) match {
|
|
case Some(cargo : Vehicle) if !requestedByPassenger =>
|
|
continent.GUID(cargo.MountedIn) match {
|
|
case Some(carrier : Vehicle) =>
|
|
CargoBehavior.HandleVehicleCargoDismount(continent, cargo_guid, bailed, requestedByPassenger, kicked)
|
|
case _ => ;
|
|
}
|
|
case _ => ;
|
|
}
|
|
|
|
case msg@CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
|
|
log.info("Handling " + msg)
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
Database.query(connection.sendPreparedStatement(
|
|
"SELECT account_id FROM characters where name ILIKE ? AND deleted = false", Array(name)
|
|
)).onComplete {
|
|
case scala.util.Success(queryResult) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
queryResult match {
|
|
case row : ArrayRowData => // If we got a row from the database
|
|
if(row(0).asInstanceOf[Int] == account.AccountId) { // create char
|
|
self ! CreateCharacter(name, head, voice, gender, empire)
|
|
sendResponse(ActionResultMessage.Fail(1))
|
|
Thread.sleep(50)
|
|
}
|
|
else { // send "char already exist"
|
|
sendResponse(ActionResultMessage.Fail(1))
|
|
Thread.sleep(50)
|
|
}
|
|
case _ => // If the char name didn't exist in the database, create char
|
|
self ! CreateCharacter(name, head, voice, gender, empire)
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
sendResponse(ActionResultMessage.Fail(4))
|
|
log.error("Returning to character list due to error " + e.getMessage)
|
|
self ! ListAccountCharacters()
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
log.error(s"CharacterCreateRequest: no connection - ${e.getMessage}")
|
|
sendResponse(ActionResultMessage.Fail(5))
|
|
}
|
|
|
|
case msg@CharacterRequestMessage(charId, action) =>
|
|
log.info(s"Handling $msg")
|
|
action match {
|
|
case CharacterRequestAction.Delete =>
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
Database.query(connection.sendPreparedStatement(
|
|
"UPDATE characters SET deleted = true where id=?", Array(charId)
|
|
)).onComplete {
|
|
case scala.util.Success(_) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
log.info(s"CharacterRequest/Delete: character id $charId deleted")
|
|
sendResponse(ActionResultMessage.Pass)
|
|
self ! ListAccountCharacters()
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
log.info(s"CharacterRequest/Delete: character id $charId NOT deleted - ${e.getMessage}")
|
|
sendResponse(ActionResultMessage.Fail(6))
|
|
Thread.sleep(50)
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
log.error(s"CharacterRequest/Delete: no connection - ${e.getMessage}")
|
|
}
|
|
|
|
case CharacterRequestAction.Select =>
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
Database.query(connection.sendPreparedStatement(
|
|
"SELECT id, name, faction_id, gender_id, head_id, voice_id FROM characters where id=?", Array(charId)
|
|
)).onComplete {
|
|
case Success(queryResult) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
queryResult match {
|
|
case row : ArrayRowData =>
|
|
val lName : String = row(1).asInstanceOf[String]
|
|
val lFaction : PlanetSideEmpire.Value = PlanetSideEmpire(row(2).asInstanceOf[Int])
|
|
val lGender : CharacterGender.Value = CharacterGender(row(3).asInstanceOf[Int])
|
|
val lHead : Int = row(4).asInstanceOf[Int]
|
|
val lVoice : CharacterVoice.Value = CharacterVoice(row(5).asInstanceOf[Int])
|
|
log.info(s"CharacterRequest/Select: character $lName found in records")
|
|
avatar = new Avatar(charId, lName, lFaction, lGender, lHead, lVoice)
|
|
var faction : String = lFaction.toString.toLowerCase
|
|
whenUsedLastMAXName(2) = faction + "hev_antipersonnel"
|
|
whenUsedLastMAXName(3) = faction + "hev_antivehicular"
|
|
whenUsedLastMAXName(1) = faction + "hev_antiaircraft"
|
|
accountPersistence ! AccountPersistenceService.Login(lName)
|
|
case _ =>
|
|
log.error(s"CharacterRequest/Select: no character for $charId found")
|
|
}
|
|
case e =>
|
|
if(connection.isConnected) connection.disconnect
|
|
log.error(s"CharacterRequest/Select: toto tata; unexpected query result format - ${e.getClass}")
|
|
}
|
|
|
|
case scala.util.Failure(e) =>
|
|
log.error(s"CharacterRequest/Select: no connection - ${e.getMessage}")
|
|
}
|
|
|
|
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 factionChannel = s"$faction"
|
|
continent.AvatarEvents ! Service.Join(continentId)
|
|
continent.AvatarEvents ! Service.Join(factionChannel)
|
|
continent.LocalEvents ! Service.Join(avatar.name)
|
|
continent.LocalEvents ! Service.Join(continentId)
|
|
continent.LocalEvents ! Service.Join(factionChannel)
|
|
continent.VehicleEvents ! Service.Join(avatar.name)
|
|
continent.VehicleEvents ! Service.Join(continentId)
|
|
continent.VehicleEvents ! Service.Join(factionChannel)
|
|
if(connectionState != 100) configZone(continent)
|
|
sendResponse(TimeOfDayMessage(1191182336))
|
|
//custom
|
|
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
|
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
|
|
|
|
//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)
|
|
continent.LocalEvents ! 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
|
|
)
|
|
)
|
|
}
|
|
}
|
|
})
|
|
//sensor animation
|
|
normal
|
|
.filter(obj =>
|
|
obj.Definition.DeployCategory == DeployableCategory.Sensors &&
|
|
!obj.Destroyed &&
|
|
(obj match {
|
|
case jObj : JammableUnit => !jObj.Jammed;
|
|
case _ => true
|
|
})
|
|
)
|
|
.foreach(obj => {
|
|
sendResponse(TriggerEffectMessage(obj.GUID, "on", true, 1000))
|
|
})
|
|
//update the health of our faction's deployables (if necessary)
|
|
//draw our faction's deployables on the map
|
|
continent.DeployableList
|
|
.filter(obj => obj.Faction == faction && !obj.Destroyed)
|
|
.foreach(obj => {
|
|
if(obj.Health != obj.DefaultHealth) {
|
|
sendResponse(PlanetsideAttributeMessage(obj.GUID, 0, obj.Health))
|
|
}
|
|
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.Destroyed && 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 Some(_) =>
|
|
//vehicle, but we're not seated in it
|
|
player.VehicleSeated = None
|
|
(b, List.empty[Vehicle])
|
|
case None =>
|
|
//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
|
|
)
|
|
)
|
|
})
|
|
})
|
|
vehicles.collect { case vehicle if vehicle.Faction == faction =>
|
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
|
}
|
|
//our vehicle would have already been loaded; see NewPlayerLoaded/AvatarCreate
|
|
usedVehicle.headOption match {
|
|
case Some(vehicle) =>
|
|
//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
|
|
)
|
|
)
|
|
})
|
|
//since we would have only subscribed recently, we need to reload seat access states
|
|
(0 to 3).foreach { group =>
|
|
sendResponse(PlanetsideAttributeMessage(vguid, group + 10, vehicle.PermissionGroup(group).get.id))
|
|
}
|
|
//positive shield strength
|
|
if(vehicle.Shields > 0) {
|
|
sendResponse(PlanetsideAttributeMessage(vguid, 68, vehicle.Shields))
|
|
}
|
|
case _ => ; //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 => {
|
|
CargoBehavior.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
|
|
.map { case ((turret_guid, _)) => continent.GUID(turret_guid) }
|
|
.collect { case Some(turret : FacilityTurret) =>
|
|
val pguid = turret.GUID
|
|
//attached weapon
|
|
if(!turret.isUpgrading) {
|
|
turret.ControlledWeapon(wepNumber = 1) match {
|
|
case Some(obj : Tool) =>
|
|
val objDef = obj.Definition
|
|
sendResponse(
|
|
ObjectCreateMessage(
|
|
objDef.ObjectId,
|
|
obj.GUID,
|
|
ObjectCreateMessageParent(pguid, 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(pguid, 0),
|
|
tdefintion.Packet.ConstructorData(tplayer).get
|
|
)
|
|
)
|
|
case None => ;
|
|
}
|
|
}
|
|
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.UpdateAmsSpawnPoint(continent))
|
|
beginZoningSetCurrentAvatarFunc(player)
|
|
|
|
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, jump_thrust, is_cloaking, unk5, unk6) =>
|
|
if (player.death_by == -1) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
|
|
Thread.sleep(300)
|
|
sendResponse(DropSession(sessionId, "kick by GM"))
|
|
}
|
|
playerStateMessageUpstreamCount += 1
|
|
val isMoving = WorldEntity.isMoving(vel)
|
|
val isMovingPlus = isMoving || is_jumping || jump_thrust
|
|
if(isMovingPlus) {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_motion")
|
|
}
|
|
|
|
if(deadState == DeadState.Alive && playerStateMessageUpstreamCount % 2 == 0) { // Regen stamina roughly every 500ms
|
|
if(player.skipStaminaRegenForTurns > 0) {
|
|
//do not renew stamina for a while
|
|
player.skipStaminaRegenForTurns -= 1
|
|
}
|
|
else if(!isMovingPlus && player.Stamina != player.MaxStamina) {
|
|
player.Stamina += 1
|
|
}
|
|
}
|
|
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(is_cloaking && !player.Cloaked) {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_cloak")
|
|
}
|
|
player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && is_cloaking
|
|
CapacitorTick(jump_thrust)
|
|
if(isMovingPlus && usingMedicalTerminal.isDefined) {
|
|
continent.GUID(usingMedicalTerminal) match {
|
|
case Some(term : Terminal with ProximityUnit) =>
|
|
StopUsingProximityUnit(term)
|
|
case _ => ;
|
|
}
|
|
}
|
|
accessedContainer match {
|
|
case Some(veh : Vehicle) =>
|
|
if(isMoving || 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(isMovingPlus) {
|
|
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
|
|
}
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, player.Position, player.Velocity, yaw, pitch, yaw_upper, seq_time, is_crouching, is_jumping, jump_thrust, is_cloaking, player.spectator, wepInHand))
|
|
updateSquad()
|
|
|
|
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)
|
|
continent.VehicleEvents ! 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")
|
|
}
|
|
if (player.death_by == -1) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
|
|
Thread.sleep(300)
|
|
sendResponse(DropSession(sessionId, "kick by GM"))
|
|
}
|
|
|
|
case msg@VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, flying, unk6, unk7, wheels, is_decelerating, is_cloaked) =>
|
|
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) {
|
|
if(obj.DeploymentState != DriveState.Deployed) {
|
|
obj.Velocity = vel
|
|
} else {
|
|
obj.Velocity = Some(Vector3.Zero)
|
|
}
|
|
if(obj.Definition.CanFly) {
|
|
obj.Flying = flying.nonEmpty //usually Some(7)
|
|
}
|
|
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
|
|
}
|
|
else {
|
|
obj.Velocity = None
|
|
obj.Flying = false
|
|
}
|
|
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, obj.Position, ang, obj.Velocity, if(obj.Flying) {
|
|
flying
|
|
}
|
|
else {
|
|
None
|
|
}, unk6, unk7, wheels, is_decelerating, obj.Cloaked))
|
|
updateSquad()
|
|
case (None, _) =>
|
|
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
|
|
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
|
|
case (_, Some(index)) =>
|
|
log.error(s"VehicleState: player should not be dispatching this kind of packet from vehicle#$vehicle_guid when not the driver ($index)")
|
|
case _ => ;
|
|
}
|
|
}
|
|
//log.info(s"VehicleState: $msg")
|
|
if (player.death_by == -1) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
|
|
Thread.sleep(300)
|
|
sendResponse(DropSession(sessionId, "kick by GM"))
|
|
}
|
|
|
|
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_vel, shot_orient, seq, end, target_guid) =>
|
|
//log.trace(s"ProjectileState: $msg")
|
|
val index = projectile_guid.guid - Projectile.BaseUID
|
|
projectiles(index) match {
|
|
case Some(projectile) if projectile.HasGUID =>
|
|
val projectileGlobalUID = projectile.GUID
|
|
projectile.Position = shot_pos
|
|
projectile.Orientation = shot_orient
|
|
projectile.Velocity = shot_vel
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ProjectileState(player.GUID, projectileGlobalUID, shot_pos, shot_vel, shot_orient, seq, end, target_guid))
|
|
case _ if seq == 0 =>
|
|
/* missing the first packet in the sequence is permissible */
|
|
case _ =>
|
|
log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
|
|
}
|
|
|
|
case msg @ ReleaseAvatarRequestMessage() =>
|
|
log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released")
|
|
reviveTimer.cancel
|
|
GoToDeploymentMap()
|
|
continent.Population ! Zone.Population.Release(avatar)
|
|
player.VehicleSeated match {
|
|
case None =>
|
|
PrepareToTurnPlayerIntoCorpse(player, continent)
|
|
|
|
case Some(_) =>
|
|
val player_guid = player.GUID
|
|
sendResponse(ObjectDeleteMessage(player_guid, 0))
|
|
GetMountableAndSeat(None, player) match {
|
|
case (Some(obj), Some(seatNum)) =>
|
|
obj.Seats(seatNum).Occupant = None
|
|
obj match {
|
|
case v : Vehicle if seatNum == 0 && v.Flying =>
|
|
TotalDriverVehicleControl(v)
|
|
UnAccessContents(v)
|
|
v.Actor ! Vehicle.Deconstruct()
|
|
case _ => ;
|
|
}
|
|
case _ => ; //found no vehicle where one was expected; since we're dead, let's not dwell on it
|
|
}
|
|
taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID)
|
|
}
|
|
|
|
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 = false
|
|
var echoContents : String = contents
|
|
val trimContents = contents.trim
|
|
val trimRecipient = recipient.trim
|
|
//TODO messy on/off strings may work
|
|
if(messagetype == ChatMessageType.CMT_FLY && admin) {
|
|
makeReply = false
|
|
if(!flying) {
|
|
flying = true
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_FLY, msg.wideContents, msg.recipient, "on", msg.note))
|
|
}
|
|
else {
|
|
flying = false
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_FLY, msg.wideContents, msg.recipient, "off", msg.note))
|
|
}
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_SPEED && admin) {
|
|
makeReply = true
|
|
speed = {
|
|
try {
|
|
trimContents.toFloat
|
|
}
|
|
catch {
|
|
case _ : Exception =>
|
|
echoContents = "1.000"
|
|
1f
|
|
}
|
|
}
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_TOGGLESPECTATORMODE && admin) {
|
|
makeReply = false
|
|
if(!player.spectator) {
|
|
player.spectator = true
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, msg.wideContents, msg.recipient, "on", msg.note))
|
|
}
|
|
else {
|
|
player.spectator = false
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, msg.wideContents, msg.recipient, "off", msg.note))
|
|
}
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_RECALL) {
|
|
makeReply = false
|
|
val sanctuary = Zones.SanctuaryZoneId(player.Faction)
|
|
if(zoningType == Zoning.Method.InstantAction) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_instantactionting", None))
|
|
}
|
|
else if(zoningType == Zoning.Method.Recall) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You already requested to recall to your sanctuary continent.", None))
|
|
}
|
|
else if(continent.Id.equals(sanctuary)) {
|
|
//nonstandard message
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall when you are already on your faction's sanctuary continent.", None))
|
|
}
|
|
else if(deadState != DeadState.Alive) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_dead", None))
|
|
}
|
|
else if(player.VehicleSeated.nonEmpty) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_invehicle", None))
|
|
}
|
|
else {
|
|
zoningType = Zoning.Method.Recall
|
|
zoningChatMessageType = messagetype
|
|
zoningStatus = Zoning.Status.Request
|
|
zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset())
|
|
cluster ! Zoning.Recall.Request(player.Faction, sanctuary)
|
|
}
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_INSTANTACTION) {
|
|
makeReply = false
|
|
if(zoningType == Zoning.Method.InstantAction) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_instantactionting", None))
|
|
}
|
|
else if(zoningType == Zoning.Method.Recall) {
|
|
//nonstandard message
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You already requested to recall to your sanctuary continent.", None))
|
|
}
|
|
else if(deadState != DeadState.Alive) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_dead", None))
|
|
}
|
|
else if(player.VehicleSeated.nonEmpty) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_invehicle", None))
|
|
}
|
|
else {
|
|
zoningType = Zoning.Method.InstantAction
|
|
zoningChatMessageType = messagetype
|
|
zoningStatus = Zoning.Status.Request
|
|
zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset())
|
|
cluster ! Zoning.InstantAction.Request(player.Faction)
|
|
}
|
|
}
|
|
CSRZone.read(traveler, msg) match {
|
|
case (true, zone, pos) =>
|
|
if (player.isAlive && zone != player.Continent && (admin || zone == "z8" || zone == "c1" || zone == "c2" || zone == "c3" || zone == "c4" || zone == "c5" || zone == "c6" ||
|
|
zone == "tzshtr" || zone == "tzcotr" || zone == "tzdrtr" ||
|
|
zone == "tzshnc" || zone == "tzconc" || zone == "tzdrnc" ||
|
|
zone == "tzshvs" || zone == "tzcovs" || zone == "tzdrvs")) {
|
|
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
|
|
//continent.AvatarEvents ! 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) =>
|
|
// continent.Id == "c1" || continent.Id == "c2" || continent.Id == "c3" || continent.Id == "c4" || continent.Id == "c5" || continent.Id == "c6" ||
|
|
if (player.isAlive && (admin || continent.Id == "z8" ||
|
|
continent.Id == "tzshtr" || continent.Id == "tzcotr" || continent.Id == "tzdrtr" ||
|
|
continent.Id == "tzshnc" || continent.Id == "tzconc" || continent.Id == "tzdrnc" ||
|
|
continent.Id == "tzshvs" || continent.Id == "tzcovs" || continent.Id == "tzdrvs")) {
|
|
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 {
|
|
log.info("Chat: " + msg)
|
|
makeReply = false
|
|
}
|
|
if(messagetype == ChatMessageType.CMT_SUICIDE) {
|
|
if(player.isAlive && deadState != DeadState.Release) {
|
|
Suicide(player)
|
|
}
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_CULLWATERMARK) {
|
|
if(trimContents.contains("40 80")) connectionState = 100
|
|
else if(trimContents.contains("120 200")) connectionState = 25
|
|
else connectionState = 50
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_DESTROY) {
|
|
makeReply = true
|
|
val guid = contents.toInt
|
|
continent.GUID(continent.Map.TerminalToSpawnPad.getOrElse(guid, guid)) match {
|
|
case Some(pad : VehicleSpawnPad) =>
|
|
pad.Actor ! VehicleSpawnControl.ProcessControl.Flush
|
|
case Some(turret : FacilityTurret) if turret.isUpgrading =>
|
|
WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None)
|
|
case _ =>
|
|
self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid)))
|
|
}
|
|
} else if(messagetype == ChatMessageType.CMT_QUIT) { // TODO: handle this appropriately
|
|
sendResponse(DropCryptoSession())
|
|
sendResponse(DropSession(sessionId, "user quit"))
|
|
}
|
|
//dev hack; consider bang-commands to complement slash-commands in future
|
|
if(trimContents.equals("!loc")) {
|
|
makeReply = true
|
|
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.contains("!list") && admin) {
|
|
// StartBundlingPackets()
|
|
val localString : String = contents.drop(contents.indexOf(" ") + 1)
|
|
|
|
if(localString.equalsIgnoreCase("!list")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID] at PosX PosY PosZ", note_contents))
|
|
continent.LivePlayers.filterNot(_.GUID == player.GUID).sortBy(_.Name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server",
|
|
char.Name + " (" + char.Faction + ") [" + char.CharId + "] at " + char.Position.x.toInt + " " + char.Position.y.toInt + " " + char.Position.z.toInt, note_contents))
|
|
})
|
|
continent.Corpses.filterNot(_.GUID == player.GUID).sortBy(_.Name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server",
|
|
"\\#7" + char.Name + " (" + char.Faction + ") [" + char.CharId + "] at " + char.Position.x.toInt + " " + char.Position.y.toInt + " " + char.Position.z.toInt, note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z1")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z1.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z2")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z2.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z3")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z3.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z4")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z4.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z5")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z5.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z6")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z6.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z7")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z7.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z8")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z8.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z9")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z9.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("z10")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.z10.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("home1")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.home1.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("home2")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.home2.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("home3")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.home3.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("c1")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.c1.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("c2")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.c2.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("c3")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.c3.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("c4")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.c4.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("c5")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.c5.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("c6")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.c6.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("i1")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.i1.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("i2")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.i2.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("i3")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.i3.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else if(localString.equalsIgnoreCase("i4")) {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID]", note_contents))
|
|
Zones.i4.Players.filterNot(_.CharId == player.CharId).sortBy(_.name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", char.name + " (" + char.faction + ") [" + char.CharId + "]", note_contents))
|
|
})
|
|
}
|
|
else {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server", "\\#8Name (Faction) [ID] in Zone at PosX PosY PosZ", note_contents))
|
|
continent.LivePlayers.filter(_.Name.contains(localString)).sortBy(_.Name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server",
|
|
char.Name + " (" + char.Faction + ") [" + char.CharId + "] in " + char.Continent + " at " + char.Position.x.toInt + " " + char.Position.y.toInt + " " + char.Position.z.toInt, note_contents))
|
|
})
|
|
continent.Corpses.filter(_.Name.contains(localString)).sortBy(_.Name).foreach(char => {
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_GMOPEN, has_wide_contents, "Server",
|
|
"\\#7" + char.Name + " (" + char.Faction + ") [" + char.CharId + "] in " + char.Continent + " at " + char.Position.x.toInt + " " + char.Position.y.toInt + " " + char.Position.z.toInt, note_contents))
|
|
})
|
|
}
|
|
// StopBundlingPackets()
|
|
}
|
|
else if (trimContents.contains("!kick") && admin) {
|
|
val CharIDorName : String = contents.drop(contents.indexOf(" ") + 1)
|
|
try {
|
|
val charID : Long = CharIDorName.toLong
|
|
if(charID != player.CharId) {
|
|
var charToKick = continent.LivePlayers.filter(_.CharId == charID)
|
|
if (charToKick.nonEmpty) {
|
|
charToKick.head.death_by = -1
|
|
}
|
|
else {
|
|
charToKick = continent.Corpses.filter(_.CharId == charID)
|
|
if (charToKick.nonEmpty) charToKick.head.death_by = -1
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
case _ : Throwable =>
|
|
{
|
|
val charToKick = continent.LivePlayers.filter(_.Name.equalsIgnoreCase(CharIDorName))
|
|
if(charToKick.nonEmpty) charToKick.head.death_by = -1
|
|
}
|
|
}
|
|
}
|
|
else if(trimRecipient.equals("tr")) {
|
|
sendResponse(ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0))
|
|
}
|
|
else if(trimRecipient.equals("nc")) {
|
|
sendResponse(ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0))
|
|
}
|
|
else if(trimRecipient.equals("vs")) {
|
|
sendResponse(ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0))
|
|
}
|
|
else if(trimRecipient.equals("bo")){
|
|
sendResponse(ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt))
|
|
|
|
// sendResponse(ZoneInfoMessage(contents.toInt, true, 25200000))
|
|
// sendResponse(ZoneInfoMessage(contents.toInt-1, false, 25200000))
|
|
// sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", "@cavern_switched^@c1~^@c5~", None))
|
|
}
|
|
else if(trimContents.startsWith("!ntu") && admin){
|
|
continent.Buildings.values.foreach(building =>
|
|
building.Amenities.foreach(amenity =>
|
|
amenity.Definition match {
|
|
case GlobalDefinitions.resource_silo =>
|
|
val r = new scala.util.Random
|
|
val silo = amenity.asInstanceOf[ResourceSilo]
|
|
val ntu: Int = 900 + r.nextInt(100) - silo.ChargeLevel
|
|
// val ntu: Int = 0 + r.nextInt(100) - silo.ChargeLevel
|
|
silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu)
|
|
|
|
case _ => ;
|
|
}
|
|
)
|
|
)
|
|
}
|
|
else if(trimContents.startsWith("!hack") && admin){
|
|
var hackFaction = PlanetSideEmpire.NEUTRAL
|
|
val args = trimContents.split(" ")
|
|
if (args.length == 3) {
|
|
var bad : Boolean = false
|
|
if(args(2).equalsIgnoreCase("tr")) hackFaction = PlanetSideEmpire.TR
|
|
else if(args(2).equalsIgnoreCase("nc")) hackFaction = PlanetSideEmpire.NC
|
|
else if(args(2).equalsIgnoreCase("vs")) hackFaction = PlanetSideEmpire.VS
|
|
else if(args(2).equalsIgnoreCase("bo")) hackFaction = PlanetSideEmpire.NEUTRAL
|
|
else bad = true
|
|
if(bad) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_229, true, "", "USE !hack tr|vs|nc|bo OR !hack BaseName tr|vs|nc|bo", None))
|
|
}
|
|
else {
|
|
continent.Buildings.foreach({
|
|
case (id, building) =>
|
|
if(!building.Name.isEmpty && args(1).equalsIgnoreCase(building.Name)) {
|
|
log.info(s"Hack Base Name : ${args(1)} to empire : ${args(2)}")
|
|
building.Faction = hackFaction
|
|
continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.SetEmpire(building.GUID, hackFaction))
|
|
}
|
|
})
|
|
}
|
|
} else if (args.length == 2) {
|
|
var bad : Boolean = false
|
|
if(args(1).equalsIgnoreCase("tr")) hackFaction = PlanetSideEmpire.TR
|
|
else if(args(1).equalsIgnoreCase("nc")) hackFaction = PlanetSideEmpire.NC
|
|
else if(args(1).equalsIgnoreCase("vs")) hackFaction = PlanetSideEmpire.VS
|
|
else if(args(1).equalsIgnoreCase("bo")) hackFaction = PlanetSideEmpire.NEUTRAL
|
|
else bad = true
|
|
if(bad) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_229, true, "", "USE !hack tr|vs|nc|bo OR !hack BaseName tr|vs|nc|bo", None))
|
|
}
|
|
else {
|
|
continent.Buildings.foreach({
|
|
case (id, building) =>
|
|
if (!building.Name.isEmpty && !bad
|
|
&& building.BuildingType != StructureType.Bridge
|
|
&& building.BuildingType != StructureType.Bunker
|
|
&& building.BuildingType != StructureType.WarpGate
|
|
) {
|
|
log.info(s"Hack Bases to empire : ${args(1)}")
|
|
building.Faction = hackFaction
|
|
continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.SetEmpire(building.GUID, hackFaction))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
else {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_229, true, "", "USE !hack tr|vs|nc|bo OR !hack BaseName tr|vs|nc|bo", None))
|
|
}
|
|
}
|
|
// 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))
|
|
}
|
|
if(messagetype == ChatMessageType.CMT_OPEN && !player.silenced) {
|
|
chatService ! ChatServiceMessage("local", ChatAction.Local(player.GUID, player.Name, continent, player.Position, player.Faction, msg))
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_VOICE) {
|
|
chatService ! ChatServiceMessage("voice", ChatAction.Voice(player.GUID, player.Name, continent, player.Position, player.Faction, msg))
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_TELL && !player.silenced) {
|
|
chatService ! ChatServiceMessage("tell", ChatAction.Tell(player.GUID, player.Name, msg))
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_BROADCAST && !player.silenced) {
|
|
chatService ! ChatServiceMessage("broadcast", ChatAction.Broadcast(player.GUID, player.Name, continent, player.Position, player.Faction, msg))
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_NOTE) {
|
|
chatService ! ChatServiceMessage("note", ChatAction.Note(player.GUID, player.Name, msg))
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_SILENCE && admin) {
|
|
chatService ! ChatServiceMessage("gm", ChatAction.GM(player.GUID, player.Name, msg))
|
|
}
|
|
else if (messagetype == ChatMessageType.CMT_SQUAD && !player.silenced) {
|
|
if(squadChannel.nonEmpty) {
|
|
chatService ! ChatServiceMessage(squadChannel.get, ChatAction.Squad(player.GUID, player.Name, continent, player.Position, player.Faction, msg))
|
|
}
|
|
}
|
|
else if (messagetype == ChatMessageType.CMT_PLATOON && !player.silenced) {
|
|
chatService ! ChatServiceMessage("platoon", ChatAction.Platoon(player.GUID, player.Name, continent, player.Position, player.Faction, msg))
|
|
}
|
|
else if (messagetype == ChatMessageType.CMT_COMMAND && admin) {
|
|
chatService ! ChatServiceMessage("command", ChatAction.Command(player.GUID, player.Name, continent, player.Position, player.Faction, msg))
|
|
}
|
|
else if(messagetype == ChatMessageType.CMT_WHO || messagetype == ChatMessageType.CMT_WHO_CSR || messagetype == ChatMessageType.CMT_WHO_CR ||
|
|
messagetype == ChatMessageType.CMT_WHO_PLATOONLEADERS || messagetype == ChatMessageType.CMT_WHO_SQUADLEADERS || messagetype == ChatMessageType.CMT_WHO_TEAMS) {
|
|
val poplist = continent.Players
|
|
val popTR = poplist.count(_.faction == PlanetSideEmpire.TR)
|
|
val popNC = poplist.count(_.faction == PlanetSideEmpire.NC)
|
|
val popVS = poplist.count(_.faction == PlanetSideEmpire.VS)
|
|
val contName = continent.Map.Name
|
|
StartBundlingPackets()
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None))
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None))
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None))
|
|
sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None))
|
|
StopBundlingPackets()
|
|
}
|
|
|
|
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))
|
|
continent.AvatarEvents ! 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.trace("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)
|
|
//special case - suppress the decimator's alternate fire mode, by projectile
|
|
if(tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) {
|
|
continent.AvatarEvents ! 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)
|
|
continent.AvatarEvents ! 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.trace("ChangeFireState_Stop: " + msg)
|
|
prefire = None
|
|
val weapon : Option[Equipment] = if(shooting.contains(item_guid)) {
|
|
shooting = None
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, item_guid))
|
|
FindEquipment
|
|
}
|
|
else {
|
|
FindEquipment match {
|
|
case Some(tool : Tool) =>
|
|
//the decimator does not send a ChangeFireState_Start on the last shot
|
|
if(tool.Definition == GlobalDefinitions.phoenix &&
|
|
tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) {
|
|
//suppress the decimator's alternate fire mode, however
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(player.GUID, item_guid))
|
|
}
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, item_guid))
|
|
Some(tool)
|
|
case Some(tool) => //permissible, for now
|
|
continent.AvatarEvents ! 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
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Start(playerGUID, item_guid))
|
|
continent.GUID(trigger.Companion) match {
|
|
case Some(boomer : BoomerDeployable) =>
|
|
boomer.Destroyed = true
|
|
continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.Detonate(boomer.GUID, boomer))
|
|
Deployables.AnnounceDestroyDeployable(boomer, Some(500 milliseconds))
|
|
case Some(_) | None => ;
|
|
}
|
|
FindEquipmentToDelete(item_guid, trigger)
|
|
trigger.Companion = None
|
|
case _ => ;
|
|
}
|
|
progressBarUpdate.cancel
|
|
progressBarValue = None
|
|
|
|
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")
|
|
ValidObject(item_guid) match {
|
|
case Some(anItem : Equipment) =>
|
|
player.FreeHand.Equipment match {
|
|
case Some(item) =>
|
|
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 $anItem, 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 =>
|
|
sendResponse(ObjectDeleteMessage(item_guid, 0)) //this is fine; item doesn't exist to the server anyway
|
|
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")
|
|
ValidObject(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))
|
|
continent.AvatarEvents ! 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) {
|
|
continent.AvatarEvents ! 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 unholstered 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
|
|
continent.AvatarEvents ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, Player.GetHackLevel(player)))
|
|
}
|
|
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)
|
|
player.Stamina = player.Stamina - 10
|
|
player.skipStaminaRegenForTurns = math.max(player.skipStaminaRegenForTurns, 5)
|
|
|
|
case msg@ZipLineMessage(player_guid, forwards, action, path_id, pos) =>
|
|
log.info("ZipLineMessage: " + msg)
|
|
val (isTeleporter : Boolean, path : Option[ZipLinePath]) = continent.ZipLinePaths.find(x => x.PathId == path_id) match {
|
|
case Some(x) => (x.IsTeleporter, Some(x))
|
|
case _ =>
|
|
log.warn(s"Couldn't find zipline path ${path_id} in zone ${continent.Number} / ${continent.Id}")
|
|
(false, None)
|
|
}
|
|
if(isTeleporter) {
|
|
CancelZoningProcessWithDescriptiveReason("cancel")
|
|
val endPoint = path.get.ZipLinePoints.last
|
|
sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, path_id, pos)) // todo: send to zone to show teleport animation to all clients
|
|
sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, player.Orientation.z, None)))
|
|
}
|
|
else {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_motion")
|
|
action match {
|
|
case 0 =>
|
|
// Travel along the zipline in the direction specified
|
|
sendResponse(ZipLineMessage(player_guid, forwards, action, path_id, pos))
|
|
case 1 =>
|
|
//disembark from zipline at destination !
|
|
sendResponse(ZipLineMessage(player_guid, forwards, action, 0, pos))
|
|
case 2 =>
|
|
//get off by force
|
|
sendResponse(ZipLineMessage(player_guid, forwards, action, 0, pos))
|
|
case _ =>
|
|
log.warn(s"Tried to do something with a zipline but can't handle it. forwards: ${forwards} action: ${action} path_id: ${path_id} zone: ${continent.Number} / ${continent.Id}")
|
|
}
|
|
}
|
|
|
|
case msg@RequestDestroyMessage(object_guid) =>
|
|
// TODO: Make sure this is the correct response for all cases
|
|
ValidObject(object_guid) match {
|
|
case Some(vehicle : Vehicle) =>
|
|
/* line 1a: player is admin (and overrules other access requirements) */
|
|
/* line 1b: vehicle and player (as the owner) acknowledge each other */
|
|
/* line 1c: vehicle is the same faction as player and either the owner is absent or the vehicle is destroyed */
|
|
/* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */
|
|
if(
|
|
(admin ||
|
|
(player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) ||
|
|
(player.Faction == vehicle.Faction && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Destroyed))
|
|
) &&
|
|
(vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied))
|
|
) {
|
|
vehicle.Actor ! Vehicle.Deconstruct()
|
|
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
|
|
continent.LocalEvents ! 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()
|
|
if(projectile.profile.ExistsOnRemoteClients && projectile.HasGUID) {
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ProjectileExplodes(player.GUID, projectile.GUID, projectile))
|
|
taskResolver ! UnregisterProjectile(projectile)
|
|
}
|
|
}
|
|
case None =>
|
|
log.warn(s"RequestDestroy: projectile ${object_guid.guid} has never been fired")
|
|
}
|
|
|
|
case Some(obj : BoomerDeployable) =>
|
|
continent.LocalEvents ! 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 _ => ;
|
|
}
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), guid))
|
|
GUIDTask.UnregisterObjectTask(trigger)(continent.GUID)
|
|
|
|
case None => ;
|
|
}
|
|
|
|
case Some(obj : TelepadDeployable) =>
|
|
continent.LocalEvents ! LocalServiceMessage.Telepads(SupportActor.ClearSpecific(List(obj), continent))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds)))
|
|
|
|
case Some(obj : PlanetSideGameObject with Deployable) =>
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(obj), continent))
|
|
continent.LocalEvents ! 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)) {
|
|
StartBundlingPackets()
|
|
PerformMoveItem(item, source, index, destination, dest, destItemEntry)
|
|
StopBundlingPackets()
|
|
}
|
|
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} 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")
|
|
(ValidObject(item_guid), ValidObject(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)) {
|
|
StartBundlingPackets()
|
|
PerformMoveItem(item, source, index, target, dest, None)
|
|
StopBundlingPackets()
|
|
}
|
|
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, action, slot, status) =>
|
|
log.info("AvatarImplantMessage: " + msg)
|
|
if(action == ImplantAction.Activation) {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_implant")
|
|
player.Actor ! Player.ImplantActivation(slot, status)
|
|
}
|
|
|
|
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)
|
|
val equipment = player.Slot(player.DrawnSlot).Equipment match {
|
|
case out@Some(item) if item.GUID == item_used_guid => out
|
|
case _ => None
|
|
}
|
|
ValidObject(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 owner = lock.Owner.asInstanceOf[Building]
|
|
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:
|
|
// The player is on the inside of the door, determined by the lock orientation
|
|
// The lock is hacked
|
|
// A base is hacked
|
|
// A base is neutral
|
|
// todo: A base is out of power (generator down)
|
|
|
|
playerIsOnInside || lock.HackedBy.isDefined || owner.CaptureConsoleIsHacked || lock.Faction == PlanetSideEmpire.NEUTRAL
|
|
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) =>
|
|
resourceSilo.Actor ! ResourceSilo.Use(player, msg)
|
|
|
|
case Some(panel : IFFLock) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
panel.Actor ! CommonMessages.Use(player, Some(item))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : Player) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
if(obj.isBackpack) {
|
|
if(equipment.isEmpty) {
|
|
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 && player.isAlive) { //potential kit use
|
|
ValidObject(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 {
|
|
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))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health))
|
|
}
|
|
}
|
|
else if(kit.Definition == GlobalDefinitions.super_medkit) {
|
|
if(player.Health == player.MaxHealth) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "@HealComplete", None))
|
|
}
|
|
else if(System.currentTimeMillis - whenUsedLastSMKit < 1200000) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${1200 - (System.currentTimeMillis - whenUsedLastSMKit) / 1000}~", None))
|
|
}
|
|
else {
|
|
whenUsedLastSMKit = 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), 100, kit.Definition))
|
|
player.Health = player.Health + 100
|
|
sendResponse(PlanetsideAttributeMessage(avatar_guid, 0, player.Health))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 0, player.Health))
|
|
}
|
|
}
|
|
else if(kit.Definition == GlobalDefinitions.super_armorkit) {
|
|
if(player.Armor == player.MaxArmor) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "Armor at maximum - No repairing required.", None))
|
|
}
|
|
else if(System.currentTimeMillis - whenUsedLastSAKit < 1200000) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${1200 - (System.currentTimeMillis - whenUsedLastSAKit) / 1000}~", None))
|
|
}
|
|
else {
|
|
whenUsedLastSAKit = 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(RepairFromKit(PlayerSource(player), 200, kit.Definition))
|
|
player.Armor = player.Armor + 200
|
|
sendResponse(PlanetsideAttributeMessage(avatar_guid, 4, player.Armor))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(avatar_guid, 4, player.Armor))
|
|
}
|
|
}
|
|
else if(kit.Definition == GlobalDefinitions.super_staminakit) {
|
|
if(player.Stamina == player.MaxStamina) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", "Stamina at maximum - No recharge required.", None))
|
|
}
|
|
else if(System.currentTimeMillis - whenUsedLastSSKit < 1200000) {
|
|
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${300 - (System.currentTimeMillis - whenUsedLastSSKit) / 1200}~", None))
|
|
}
|
|
else {
|
|
whenUsedLastSSKit = 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.Stamina = player.Stamina + 100
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
else if(itemType == ObjectClass.avatar && unk3) {
|
|
equipment match {
|
|
case Some(tool : Tool) if tool.Definition == GlobalDefinitions.bank =>
|
|
obj.Actor ! CommonMessages.Use(player, equipment)
|
|
|
|
case Some(tool : Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
|
|
obj.Actor ! CommonMessages.Use(player, equipment)
|
|
case _ => ;
|
|
}
|
|
}
|
|
|
|
case Some(locker : Locker) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
locker.Actor ! CommonMessages.Use(player, Some(item))
|
|
case None if locker.Faction == player.Faction || !locker.HackedBy.isEmpty =>
|
|
log.trace(s"UseItem: $player accessing a locker")
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
val container = player.Locker
|
|
accessedContainer = Some(container)
|
|
sendResponse(UseItemMessage(avatar_guid, item_used_guid, container.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, 456))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(gen : Generator) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
gen.Actor ! CommonMessages.Use(player, Some(item))
|
|
case None => ;
|
|
}
|
|
|
|
case Some(mech : ImplantTerminalMech) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
mech.Actor ! CommonMessages.Use(player, Some(item))
|
|
case None => ;
|
|
}
|
|
|
|
case Some(captureTerminal : CaptureTerminal) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
captureTerminal.Actor ! CommonMessages.Use(player, Some(item))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : FacilityTurret) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item)) //try generic
|
|
obj.Actor ! CommonMessages.Use(player, Some((item, unk2.toInt))) //try upgrade path
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : Vehicle) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item))
|
|
|
|
case None if player.Faction == obj.Faction =>
|
|
//access to trunk
|
|
if(obj.AccessingTrunk.isEmpty &&
|
|
(!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner.contains(player.GUID))) {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
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))
|
|
}
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(terminal : Terminal) =>
|
|
log.info(s"$msg")
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
terminal.Actor ! CommonMessages.Use(player, Some(item))
|
|
|
|
case None if terminal.Faction == player.Faction || terminal.HackedBy.nonEmpty =>
|
|
val tdef = terminal.Definition
|
|
if(tdef.isInstanceOf[MatrixTerminalDefinition]) {
|
|
//TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks)
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
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
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
terminal.Actor ! Terminal.Request(
|
|
player,
|
|
ItemTransactionMessage(object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0))
|
|
)
|
|
}
|
|
else {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType))
|
|
}
|
|
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : SpawnTube) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item))
|
|
case None if player.Faction == obj.Faction =>
|
|
//deconstruction
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
PlayerActionsToCancel()
|
|
CancelAllProximityUnits()
|
|
continent.Population ! Zone.Population.Release(avatar)
|
|
GoToDeploymentMap()
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : SensorDeployable) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : TurretDeployable) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : TrapDeployable) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : ShieldGeneratorDeployable) =>
|
|
equipment match {
|
|
case Some(item) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
obj.Actor ! CommonMessages.Use(player, Some(item))
|
|
case _ => ;
|
|
}
|
|
|
|
case Some(obj : TelepadDeployable) =>
|
|
if(equipment.isEmpty) {
|
|
continent.GUID(obj.Router) match {
|
|
case Some(vehicle : Vehicle) =>
|
|
vehicle.Utility(UtilityType.internal_router_telepad_deployable) match {
|
|
case Some(util : Utility.InternalTelepad) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel")
|
|
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}")
|
|
}
|
|
case Some(o) =>
|
|
log.error(s"telepad@${object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}")
|
|
case None => ;
|
|
}
|
|
}
|
|
|
|
case Some(obj : Utility.InternalTelepad) =>
|
|
continent.GUID(obj.Telepad) match {
|
|
case Some(pad : TelepadDeployable) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel")
|
|
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) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
log.warn(s"UseItem: don't know how to handle $obj")
|
|
|
|
case None =>
|
|
log.error(s"UseItem: can not find object $object_guid")
|
|
}
|
|
|
|
case msg @ ProximityTerminalUseMessage(player_guid, object_guid, _) =>
|
|
log.trace(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?
|
|
ValidObject(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}")
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
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
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 19, 1))
|
|
definition match {
|
|
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
|
|
val tool = toolOpt.get
|
|
tool.ToFireMode = 1
|
|
sendResponse(ChangeFireModeMessage(tool.GUID, 1))
|
|
case GlobalDefinitions.trhev_pounder =>
|
|
val tool = toolOpt.get
|
|
val convertFireModeIndex = if(tool.FireModeIndex == 0) { 1 } else { 4 }
|
|
tool.ToFireMode = convertFireModeIndex
|
|
sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
|
|
case _ =>
|
|
log.warn(s"GenericObject: $player 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
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 19, 0))
|
|
definition match {
|
|
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
|
|
val tool = toolOpt.get
|
|
tool.ToFireMode = 0
|
|
sendResponse(ChangeFireModeMessage(tool.GUID, 0))
|
|
case GlobalDefinitions.trhev_pounder =>
|
|
val tool = toolOpt.get
|
|
val convertFireModeIndex = if(tool.FireModeIndex == 1) { 0 } else { 3 }
|
|
tool.ToFireMode = convertFireModeIndex
|
|
sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
|
|
case _ =>
|
|
log.warn(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
|
|
}
|
|
}
|
|
else if (action == 20) {
|
|
if(player.ExoSuit == ExoSuitType.MAX) {
|
|
ToggleMaxSpecialState(enable = true)
|
|
} else {
|
|
log.warn("Got GenericActionMessage 20 but can't handle it")
|
|
}
|
|
}
|
|
else if (action == 21) {
|
|
if(player.ExoSuit == ExoSuitType.MAX) {
|
|
player.Faction match {
|
|
case PlanetSideEmpire.NC =>
|
|
ToggleMaxSpecialState(enable = false)
|
|
case _ => log.warn(s"Player ${player.Name} tried to cancel an uncancellable MAX special ability")
|
|
}
|
|
} else {
|
|
log.warn("Got GenericActionMessage 21 but can't handle it")
|
|
}
|
|
}
|
|
else if(action == 36) { //Looking For Squad ON
|
|
if(squadUI.nonEmpty) {
|
|
if(!lfsm && squadUI(player.CharId).index == 0) {
|
|
lfsm = true
|
|
continent.AvatarEvents ! AvatarServiceMessage(s"${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
|
|
}
|
|
}
|
|
else if(!avatar.LFS) {
|
|
avatar.LFS = true
|
|
continent.AvatarEvents ! AvatarServiceMessage(s"${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
|
|
}
|
|
}
|
|
else if(action == 37) { //Looking For Squad OFF
|
|
if(squadUI.nonEmpty) {
|
|
if(lfsm && squadUI(player.CharId).index == 0) {
|
|
lfsm = false
|
|
continent.AvatarEvents ! AvatarServiceMessage(s"${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
|
|
}
|
|
}
|
|
else if(avatar.LFS) {
|
|
avatar.LFS = false
|
|
continent.AvatarEvents ! AvatarServiceMessage(s"${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
|
|
}
|
|
}
|
|
|
|
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
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
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
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
|
|
SaveLoadoutToDB(owner, name, lineno)
|
|
import InfantryLoadout._
|
|
// println(player_guid, line, name, DetermineSubtypeB(player.ExoSuit, DetermineSubtype(player)), player.ExoSuit, DetermineSubtype(player))
|
|
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 =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
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) =>
|
|
continent.AvatarEvents ! 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(s"WeaponFire: $msg")
|
|
CancelZoningProcessWithDescriptiveReason("cancel_fire")
|
|
if(player.isShielded) {
|
|
// Cancel NC MAX shield if it's active
|
|
ToggleMaxSpecialState(enable = false)
|
|
}
|
|
FindContainedWeapon match {
|
|
case (Some(obj), Some(tool : Tool)) =>
|
|
if(tool.Magazine <= 0) { //safety: enforce ammunition depletion
|
|
prefire = None
|
|
EmptyMagazine(weapon_guid, tool)
|
|
}
|
|
else if(!player.isAlive) { //proper internal accounting, but no projectile
|
|
prefire = shooting.orElse(Some(weapon_guid))
|
|
tool.Discharge
|
|
projectiles(projectile_guid.guid - Projectile.BaseUID) = None
|
|
shotsWhileDead += 1
|
|
}
|
|
else { //shooting
|
|
if (tool.FireModeIndex == 1 && (tool.Definition.Name == "anniversary_guna" || tool.Definition.Name == "anniversary_gun" || tool.Definition.Name == "anniversary_gunb")) {
|
|
player.Stamina = 0
|
|
}
|
|
|
|
prefire = shooting.orElse(Some(weapon_guid))
|
|
tool.Discharge //always
|
|
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 =>
|
|
(SimpleWorldEntity.validateOrientationEntry(p.Orientation + Vector3.z(p.FacingYawUpper)), tool.Definition.ObjectId, 10f + (if(p.Velocity.nonEmpty) { 5f } else { 0f }))
|
|
case v : Vehicle if v.Definition.CanFly =>
|
|
(tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle
|
|
case _ : Vehicle =>
|
|
(tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle
|
|
case _ =>
|
|
(obj.Orientation, obj.Definition.ObjectId, 300f)
|
|
}
|
|
val distanceToOwner = Vector3.DistanceSquared(shot_origin, player.Position)
|
|
if(distanceToOwner <= acceptableDistanceToOwner) {
|
|
val projectile_info = tool.Projectile
|
|
val projectile = Projectile(projectile_info, tool.Definition, tool.FireMode, player, attribution, shot_origin, angle)
|
|
projectiles(projectileIndex) = Some(projectile)
|
|
if(projectile_info.ExistsOnRemoteClients) {
|
|
log.trace(s"WeaponFireMessage: ${projectile_info.Name} is a remote projectile")
|
|
taskResolver ! (if(projectile.HasGUID) {
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ProjectileExplodes(player.GUID, projectile.GUID, projectile))
|
|
ReregisterProjectile(projectile)
|
|
}
|
|
else {
|
|
RegisterProjectile(projectile)
|
|
})
|
|
}
|
|
projectilesToCleanUp(projectileIndex) = false
|
|
|
|
obj match {
|
|
case turret : FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
|
turret.Actor ! FacilityTurret.WeaponDischarged()
|
|
case _ => ;
|
|
}
|
|
}
|
|
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 @ ObjectDetectedMessage(guid1, guid2, unk, targets) =>
|
|
//log.info(s"Detection: $msg")
|
|
FindWeapon match {
|
|
case Some(weapon) if weapon.Projectile.AutoLock =>
|
|
//projectile with auto-lock instigates a warning on the target
|
|
val detectedTargets = FindDetectedProjectileTargets(targets)
|
|
if(detectedTargets.nonEmpty) {
|
|
val mode = 7 + (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile)
|
|
detectedTargets.foreach { target =>
|
|
continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode))
|
|
}
|
|
}
|
|
case _ => ;
|
|
}
|
|
|
|
case msg @ HitMessage(seq_time, projectile_guid, unk1, hit_info, unk2, unk3, unk4) =>
|
|
log.info(s"Hit: $msg")
|
|
(hit_info match {
|
|
case Some(hitInfo) =>
|
|
ValidObject(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")
|
|
FindProjectileEntry(projectile_guid) match {
|
|
case Some(projectile) =>
|
|
projectile.Position = explosion_pos
|
|
projectile.Velocity = projectile_vel
|
|
//direct_victim_uid
|
|
ValidObject(direct_victim_uid) match {
|
|
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
|
|
ResolveProjectileEntry(projectile, ProjectileResolution.Splash, target, target.Position) match {
|
|
case Some(projectile) =>
|
|
HandleDealingDamage(target, projectile)
|
|
case None => ;
|
|
}
|
|
case _ => ;
|
|
}
|
|
//other victims
|
|
targets.foreach(elem => {
|
|
ValidObject(elem.uid) match {
|
|
case Some(target : PlanetSideGameObject with FactionAffinity with Vitality) =>
|
|
ResolveProjectileEntry(projectile, ProjectileResolution.Splash, target, explosion_pos) match {
|
|
case Some(projectile) =>
|
|
HandleDealingDamage(target, projectile)
|
|
case None => ;
|
|
}
|
|
case _ => ;
|
|
}
|
|
})
|
|
if(projectile.profile.ExistsOnRemoteClients && projectile.HasGUID) {
|
|
//cleanup
|
|
val localIndex = projectile_guid.guid - Projectile.BaseUID
|
|
if(projectile.HasGUID) {
|
|
CleanUpRemoteProjectile(projectile.GUID, projectile, localIndex)
|
|
}
|
|
else {
|
|
projectilesToCleanUp(localIndex) = true
|
|
}
|
|
}
|
|
case None => ;
|
|
}
|
|
|
|
case msg @ LashMessage(seq_time, killer_guid, victim_guid, projectile_guid, pos, unk1) =>
|
|
log.info(s"Lash: $msg")
|
|
ValidObject(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")
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
if(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
|
|
})) =>
|
|
deadState = DeadState.RespawnTime
|
|
cluster ! Zone.Lattice.RequestSpecificSpawnPoint(dest_continent_guid.guid, player, dest_building_guid)
|
|
|
|
case Some(wg : WarpGate) if(!wg.Active) =>
|
|
log.info(s"WarpgateRequest: inactive WarpGate")
|
|
|
|
case _ =>
|
|
deadState = DeadState.RespawnTime
|
|
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)
|
|
ValidObject(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
|
|
(interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
|
|
case out @ Some(obj : Vehicle) =>
|
|
if(obj.MountedIn.isEmpty) out else None
|
|
case out @ Some(_ : Mountable) =>
|
|
out
|
|
case _ =>
|
|
dismountWarning(s"DismountVehicleMsg: player $player_guid not considered seated in a mountable entity")
|
|
None
|
|
}) 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 =>
|
|
v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
|
|
case _ => ;
|
|
}
|
|
|
|
case None =>
|
|
dismountWarning(s"DismountVehicleMsg: can not find where player $player_guid is seated in mountable ${player.VehicleSeated}")
|
|
}
|
|
case _ =>
|
|
dismountWarning(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}")
|
|
}
|
|
}
|
|
else {
|
|
//kicking someone else out of a seat; need to own that seat/mountable
|
|
player.VehicleOwned match {
|
|
case Some(obj_guid) =>
|
|
((ValidObject(obj_guid), ValidObject(player_guid)) match {
|
|
case (vehicle @ Some(obj : Vehicle), tplayer) =>
|
|
if(obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
|
|
case (mount @ Some(obj : Mountable), tplayer) =>
|
|
(mount, tplayer)
|
|
case _ =>
|
|
(None, None)
|
|
}) 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, char_id, unk3, player_name, unk5) =>
|
|
log.info(s"$msg")
|
|
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Membership(request_type, char_id, 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)
|
|
ValidObject(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")
|
|
continent.VehicleEvents ! 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
|
|
continent.VehicleEvents ! 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.Destroyed) { //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)
|
|
val targetInfo: List[TargetInfo] = list.flatMap(x => {
|
|
continent.GUID(x.target_guid) match {
|
|
case Some(player: Player) =>
|
|
val health = player.Health.toFloat / player.MaxHealth
|
|
val armor = if (player.MaxArmor > 0) {
|
|
player.Armor.toFloat / player.MaxArmor
|
|
} else {
|
|
0
|
|
}
|
|
|
|
Some(TargetInfo(player.GUID, health, armor))
|
|
case _ =>
|
|
log.warn(s"Target info requested for guid ${x.target_guid} but is not a player")
|
|
None
|
|
}
|
|
})
|
|
|
|
sendResponse(TargetingInfoMessage(targetInfo))
|
|
case msg @ ActionCancelMessage(u1, u2, u3) =>
|
|
log.info("Cancelled: "+msg)
|
|
progressBarUpdate.cancel
|
|
progressBarValue = None
|
|
|
|
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 = continent.AvatarEvents
|
|
|
|
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 `RegisterVehicleFromSpawnPad`
|
|
* @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))
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Use this function to facilitate registering a droppod for a globally unique identifier
|
|
* in the event that the user has instigated an instant action event to a destination within the current zone.<br>
|
|
* <br>
|
|
* If going to another zone instead,
|
|
* this is uneccessary as the normal vehicle gating protocol is partially intersected for droppod operation,
|
|
* and will properly register the droppod before introducing it into the new zone without additional concern.
|
|
* The droppod should actually not be completely unregistered.
|
|
* If inquired, it will act like a GUID had already been assigned to it, but it was invalidated.
|
|
* This condition is artificial, but it necessary to pass certain operations related to vehicle gating.
|
|
* Additionally, the driver is only partially associated with the vehicle at this time.
|
|
* `interstellarFerry` is properly keeping track of the vehicle during the transition
|
|
* and the user who is the driver (second param) is properly seated
|
|
* but the said driver does not know about the vehicle through his usual convention - VehicleSeated` - yet.
|
|
* @see `GlobalDefinitions.droppod`
|
|
* @see `GUIDTask.RegisterObjectTask`
|
|
* @see `interstellarFerry`
|
|
* @see `Player.VehicleSeated`
|
|
* @see `PlayerLoaded`
|
|
* @see `TaskResolver.GiveTask`
|
|
* @see `Vehicles.Own`
|
|
* @param vehicle the unregistered droppod
|
|
* @param tplayer the player using the droppod for instant action;
|
|
* should already be the driver of the droppod
|
|
* @return a `TaskResolver.GiveTask` message
|
|
*/
|
|
def RegisterDroppod(vehicle : Vehicle, tplayer : Player) : TaskResolver.GiveTask = {
|
|
TaskResolver.GiveTask(
|
|
new Task() {
|
|
private val localDriver = tplayer
|
|
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")
|
|
localDriver.VehicleSeated = localVehicle.GUID
|
|
Vehicles.Own(localVehicle, localDriver)
|
|
localAnnounce ! PlayerLoaded(localDriver)
|
|
resolver ! scala.util.Success(this)
|
|
}
|
|
}, List(GUIDTask.RegisterObjectTask(vehicle)(continent.GUID))
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Construct tasking that adds a completed and registered vehicle into the scene.
|
|
* The major difference between `RegisterVehicle` and `RegisterVehicleFromSpawnPad` 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 RegisterVehicleFromSpawnPad(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 = continent.VehicleEvents
|
|
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
|
|
Vehicles.Own(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)))
|
|
}
|
|
|
|
/**
|
|
* Construct tasking that adds a completed but unregistered projectile into the scene.
|
|
* After the projectile is registered to the curent zone's global unique identifier system,
|
|
* all connected clients save for the one that registered it will be informed about the projectile's "creation."
|
|
* @param obj the projectile to be registered
|
|
* @return a `TaskResolver.GiveTask` message
|
|
*/
|
|
def RegisterProjectile(obj : Projectile) : TaskResolver.GiveTask = {
|
|
val definition = obj.Definition
|
|
TaskResolver.GiveTask(
|
|
new Task() {
|
|
private val globalProjectile = obj
|
|
private val localAnnounce = self
|
|
|
|
override def isComplete : Task.Resolution.Value = {
|
|
if(globalProjectile.HasGUID) {
|
|
Task.Resolution.Success
|
|
}
|
|
else {
|
|
Task.Resolution.Incomplete
|
|
}
|
|
}
|
|
|
|
def Execute(resolver : ActorRef) : Unit = {
|
|
localAnnounce ! LoadedRemoteProjectile(globalProjectile.GUID, Some(globalProjectile))
|
|
resolver ! scala.util.Success(this)
|
|
}
|
|
}, List(GUIDTask.RegisterObjectTask(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 a formerly complete and currently registered projectile from the scene.
|
|
* After the projectile is unregistered from the curent zone's global unique identifier system,
|
|
* all connected clients save for the one that registered it will be informed about the projectile's "destruction."
|
|
* @param obj the projectile to be unregistered
|
|
* @return a `TaskResolver.GiveTask` message
|
|
*/
|
|
def UnregisterProjectile(obj : Projectile) : TaskResolver.GiveTask = {
|
|
TaskResolver.GiveTask(
|
|
new Task() {
|
|
private val globalProjectile = obj
|
|
private val localAnnounce = continent.AvatarEvents
|
|
private val localMsg = AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, obj.GUID, 2))
|
|
|
|
override def isComplete : Task.Resolution.Value = {
|
|
if(!globalProjectile.HasGUID) {
|
|
Task.Resolution.Success
|
|
}
|
|
else {
|
|
Task.Resolution.Incomplete
|
|
}
|
|
}
|
|
|
|
def Execute(resolver : ActorRef) : Unit = {
|
|
localAnnounce ! localMsg
|
|
resolver ! scala.util.Success(this)
|
|
}
|
|
}, List(GUIDTask.UnregisterObjectTask(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 = continent.AvatarEvents
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* If the projectile object is unregistered, register it.
|
|
* If the projectile object is already registered, unregister it and then register it again.
|
|
* @see `RegisterProjectile(Projectile)`
|
|
* @see `UnregisterProjectile(Projectile)`
|
|
* @param obj the projectile to be registered (a second time?)
|
|
* @return a `TaskResolver.GiveTask` message
|
|
*/
|
|
def ReregisterProjectile(obj : Projectile) : TaskResolver.GiveTask = {
|
|
val reg = RegisterProjectile(obj)
|
|
if(obj.HasGUID) {
|
|
TaskResolver.GiveTask(
|
|
reg.task,
|
|
List(TaskResolver.GiveTask(
|
|
reg.subs(0).task,
|
|
List(UnregisterProjectile(obj))
|
|
))
|
|
)
|
|
}
|
|
else {
|
|
reg
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = continent.AvatarEvents
|
|
|
|
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 localZone = continent
|
|
private val localAvatarMsg = Zone.Population.Leave(avatar)
|
|
private val localService = cluster
|
|
private val localServiceMsg = InterstellarCluster.GetWorld(zoneId)
|
|
|
|
override def isComplete : Task.Resolution.Value = priorTask.task.isComplete
|
|
|
|
def Execute(resolver : ActorRef) : Unit = {
|
|
localZone.Population ! localAvatarMsg
|
|
localService ! localServiceMsg
|
|
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 => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gives a target player positive battle experience points only.
|
|
* This value gets set as the battle experience points
|
|
* rather than be added to any previous total battle experience points.
|
|
* The number of implant slots that are activated is equal to the allowances calculated from on this value.
|
|
* We do this quietly.
|
|
* @param avatar the player
|
|
* @param bep the total amount of experience points, positive by assertion
|
|
* @return the player's current battle experience points
|
|
*/
|
|
def AwardCharacterSelectBattleExperiencePoints(avatar : Avatar, bep : Long) : Long = {
|
|
if(bep <= 0) {
|
|
log.error(s"trying to set $bep battle experience points on $avatar; value can not be negative")
|
|
}
|
|
else {
|
|
avatar.BEP = bep
|
|
val slots = DetailedCharacterData.numberOfImplantSlots(bep)
|
|
(0 until slots).foreach(slotNumber =>
|
|
avatar.Implants(slotNumber).Unlocked = true
|
|
)
|
|
}
|
|
bep
|
|
}
|
|
|
|
/**
|
|
* Common preparation for interfacing with a vehicle.
|
|
* Join a vehicle-specific group for shared updates.
|
|
* Construct every object in the vehicle's inventory for shared manipulation updates.
|
|
* @param vehicle the vehicle
|
|
*/
|
|
def AccessContents(vehicle : Vehicle) : Unit = {
|
|
AccessContentsChannel(vehicle)
|
|
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
|
|
)
|
|
)
|
|
})
|
|
}
|
|
|
|
def AccessContentsChannel(container : PlanetSideServerObject) : Unit = {
|
|
continent.VehicleEvents ! Service.Join(s"${container.Actor}")
|
|
}
|
|
|
|
/**
|
|
* 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 = {
|
|
continent.VehicleEvents ! Service.Leave(Some(s"${vehicle.Actor}"))
|
|
vehicle.Trunk.Items.foreach(entry =>{
|
|
sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
|
|
})
|
|
}
|
|
|
|
|
|
def UnAccessContentsChannel(container : PlanetSideServerObject) : Unit = {
|
|
continent.VehicleEvents ! Service.Leave(Some(s"${container.Actor}"))
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
continent.VehicleEvents ! 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)
|
|
obj.Find(box) match {
|
|
case Some(index) =>
|
|
continent.VehicleEvents ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.InventoryState(player.GUID, box, obj.GUID, index, box.Definition.Packet.DetailedConstructorData(box).get))
|
|
case None => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
continent.VehicleEvents ! 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 = continent.VehicleEvents
|
|
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 =>
|
|
continent.VehicleEvents ! 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
|
|
continent.AvatarEvents ! 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 =>
|
|
continent.VehicleEvents ! 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
|
|
continent.AvatarEvents ! 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
|
|
continent.VehicleEvents ! 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
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, source_guid, index, item2))
|
|
}
|
|
else if(obj.isBackpack) { //corpse being given item
|
|
continent.AvatarEvents ! 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 =>
|
|
continent.VehicleEvents ! 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
|
|
}
|
|
continent.AvatarEvents ! 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
|
|
continent.VehicleEvents ! 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
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(player_guid, destination_guid, dest, item))
|
|
}
|
|
else if(obj.isBackpack) { //corpse being given item
|
|
item.Faction = PlanetSideEmpire.NEUTRAL
|
|
continent.AvatarEvents ! 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))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeAmmo(player.GUID, tool_guid, ammoSlotIndex,previous_box_guid, boxDef.ObjectId, box.GUID, boxDef.Packet.ConstructorData(box).get))
|
|
|
|
//handle inventory contents
|
|
box.Capacity = (if(sumReloadValue <= fullMagazine) {
|
|
sumReloadValue
|
|
}
|
|
else {
|
|
val splitReloadAmmo : Int = sumReloadValue - fullMagazine
|
|
log.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, continent.AvatarEvents)(previousBox)
|
|
}
|
|
val dropFunc : (Equipment)=>TaskResolver.GiveTask = NewItemDrop(player, continent, continent.AvatarEvents)
|
|
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 =>
|
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name) //TODO we should not have to do this imho
|
|
//ams
|
|
if(vehicle.Definition == GlobalDefinitions.ams) {
|
|
state match {
|
|
case DriveState.Deployed =>
|
|
continent.VehicleEvents ! VehicleServiceMessage.AMSDeploymentChange(continent)
|
|
sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 81, 1))
|
|
case DriveState.Undeploying =>
|
|
continent.VehicleEvents ! 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.Seats(0).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.Seats(0).Occupant.contains(player)){
|
|
antChargingTick.cancel() // Stop charging NTU if charging
|
|
}
|
|
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off
|
|
continent.AvatarEvents ! 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
|
|
continent.LocalEvents ! 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) =>
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, continent, Some(0 milliseconds)))
|
|
case Some(_) | None => ;
|
|
}
|
|
util.Active = false
|
|
continent.LocalEvents ! 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))
|
|
continent.VehicleEvents ! 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(
|
|
building.Zone.Number,
|
|
building.MapId,
|
|
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(
|
|
building.Zone.Number,
|
|
building.MapId,
|
|
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))
|
|
|
|
// Synchronise capitol force dome state
|
|
if(building.IsCapitol && building.ForceDomeActive) {
|
|
sendResponse(GenericObjectActionMessage(building.GUID, 13))
|
|
}
|
|
// Synchronise amenities
|
|
building.Amenities.collect {
|
|
case obj if obj.Destroyed => configAmenityAsDestroyed(obj)
|
|
case obj => configAmenityAsWorking(obj)
|
|
}
|
|
Thread.sleep(connectionState)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Configure the specific working amenity by sending the client packets.
|
|
* Amenities that are not `Damageable` are also included.
|
|
* These actions are performed during the loading of a zone.
|
|
* @see `Door`
|
|
* @see `GenericObjectStateMsg`
|
|
* @see `Hackable`
|
|
* @see `HackCaptureTerminal`
|
|
* @see `HackObject`
|
|
* @see `PlanetsideAttributeMessage`
|
|
* @see `ResourceSilo`
|
|
* @see `SetEmpireMessage`
|
|
* @see `VitalityDefinition.Damageable`
|
|
* @param amenity the facility object
|
|
*/
|
|
def configAmenityAsWorking(amenity : Amenity) : Unit = {
|
|
val amenityId = amenity.GUID
|
|
//sync model access state
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0))
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0))
|
|
//sync damageable, if
|
|
val health = amenity.Health
|
|
if(amenity.Definition.Damageable && health < amenity.MaxHealth) {
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 0, health))
|
|
}
|
|
//sync special object type cases
|
|
amenity match {
|
|
case silo : ResourceSilo =>
|
|
//silo capacity
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 45, silo.CapacitorDisplay))
|
|
//warning lights
|
|
sendResponse(PlanetsideAttributeMessage(silo.Owner.GUID, 47, if(silo.LowNtuWarningOn) 1 else 0))
|
|
if(silo.ChargeLevel == 0) {
|
|
sendResponse(PlanetsideAttributeMessage(silo.Owner.GUID, 48, 1))
|
|
}
|
|
case door : Door if door.isOpen =>
|
|
sendResponse(GenericObjectStateMsg(amenityId, 16))
|
|
|
|
case _ => ;
|
|
}
|
|
//sync hack state
|
|
amenity match {
|
|
case obj : Hackable if obj.HackedBy.nonEmpty =>
|
|
amenity.Definition match {
|
|
case GlobalDefinitions.capture_terminal =>
|
|
HackCaptureTerminal(amenity.GUID, 0L, 0L, false)
|
|
case _ =>
|
|
HackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object
|
|
}
|
|
case _ => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure the specific destroyed amenity by sending the client packets.
|
|
* These actions are performed during the loading of a zone.
|
|
* @see `Generator`
|
|
* @see `ImplantTerminalMech`
|
|
* @see `PlanetsideAttributeMessage`
|
|
* @see `PlanetSideGameObject.Destroyed`
|
|
* @param amenity the facility object
|
|
*/
|
|
def configAmenityAsDestroyed(amenity : Amenity) : Unit = {
|
|
val amenityId = amenity.GUID
|
|
val configValue = amenity match {
|
|
case _ : ImplantTerminalMech => 0
|
|
case _ : Generator => 0
|
|
case _ => 1
|
|
}
|
|
//sync model access state
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 50, configValue))
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 51, configValue))
|
|
//sync damageable, if
|
|
if(amenity.Definition.Damageable) {
|
|
sendResponse(PlanetsideAttributeMessage(amenityId, 0, 0))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param target_guid na
|
|
* @param unk1 na
|
|
* @param unk2 na
|
|
*/
|
|
def HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) : Unit = {
|
|
sendResponse(HackMessage(0, target_guid, PlanetSideGUID(0), 100, unk1, HackState.Hacked, unk2))
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param target_guid na
|
|
* @param unk1 na
|
|
* @param unk2 na
|
|
* @param isResecured na
|
|
*/
|
|
def HackCaptureTerminal(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long, isResecured : Boolean) : Unit = {
|
|
var value = 0L
|
|
if(isResecured) {
|
|
value = 17039360L
|
|
sendResponse(PlanetsideAttributeMessage(target_guid, 20, value))
|
|
}
|
|
else {
|
|
continent.GUID(target_guid) match {
|
|
case Some(capture_terminal : Amenity with 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))
|
|
continent.GUID(player.VehicleSeated) match {
|
|
case Some(mountable : Amenity with Mountable) =>
|
|
if(mountable.Owner.GUID == capture_terminal.Owner.GUID) {
|
|
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player.GUID, mountable.Seats.head._1, true, mountable.GUID))
|
|
}
|
|
case _ => ;
|
|
}
|
|
case _ => log.warn("HackCaptureTerminal: hack state monitor not defined")
|
|
}
|
|
case _ => log.warn(s"HackCaptureTerminal: couldn't find capture terminal with GUID ${target_guid} in zone ${continent.Id}")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)))
|
|
tplayer.Actor ! Player.Die()
|
|
}
|
|
|
|
/**
|
|
* An event has occurred that would cause the player character to stop certain stateful activities.
|
|
* These activities include shooting, the weapon being drawn, hacking, accessing (a container), flying, and running.
|
|
* 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
|
|
player.skipStaminaRegenForTurns = 0
|
|
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 => ;
|
|
}
|
|
prefire.orElse(shooting) match {
|
|
case Some(guid) =>
|
|
sendResponse(ChangeFireStateMessage_Stop(guid))
|
|
continent.AvatarEvents ! 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 in a vehicle, it will construct that vehicle.
|
|
* If the player is the driver of the vehicle,
|
|
* they must temporarily be removed from the driver seat in order for the vehicle to be constructed properly.
|
|
* These two previous statements operate through similar though distinct mechanisms and imply different conditions.
|
|
* In reality, they produce the same output but enforce different relationships between the components.
|
|
* The vehicle without a rendered player will always be created if that vehicle exists.
|
|
* The vehicle should only be constructed once.
|
|
* @see `AvatarCreateInVehicle`
|
|
* @see `BeginZoningMessage`
|
|
* @see `CargoBehavior.CargoMountBehaviorForOthers`
|
|
* @see `GetKnownVehicleAndSeat`
|
|
* @see `LoadZoneTransferPassengerMessages`
|
|
* @see `Player.Spawn`
|
|
* @see `ReloadUsedLastCoolDownTimes`
|
|
* @see `Vehicles.Own`
|
|
* @see `Vehicles.ReloadAccessPermissions`
|
|
*/
|
|
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)) =>
|
|
//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)
|
|
}
|
|
//vehicle and driver/passenger
|
|
interstellarFerry = None
|
|
val vdef = vehicle.Definition
|
|
val vguid = vehicle.GUID
|
|
val vdata = if(seat == 0) {
|
|
//driver
|
|
continent.Transport ! Zone.Vehicle.Spawn(vehicle)
|
|
//as the driver, we must temporarily exclude ourselves from being in the vehicle during its creation
|
|
val seat = vehicle.Seats(0)
|
|
seat.Occupant = None
|
|
val data = vdef.Packet.ConstructorData(vehicle).get
|
|
sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, data))
|
|
seat.Occupant = player
|
|
Vehicles.Own(vehicle, player)
|
|
vehicle.CargoHolds.values
|
|
.collect { case hold if hold.isOccupied => hold.Occupant.get }
|
|
.foreach { _.MountedIn = vguid }
|
|
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, vguid, data))
|
|
carrierInfo match {
|
|
case (Some(carrier), Some((index, _))) =>
|
|
CargoBehavior.CargoMountBehaviorForOthers(carrier, vehicle, index, player.GUID)
|
|
case _ =>
|
|
vehicle.MountedIn = None
|
|
}
|
|
data
|
|
}
|
|
else {
|
|
//passenger
|
|
//non-drivers are not rendered in the vehicle at this time
|
|
val data = vdef.Packet.ConstructorData(vehicle).get
|
|
sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, data))
|
|
carrierInfo match {
|
|
case (Some(carrier), Some((index, _))) =>
|
|
CargoMountBehaviorForUs(carrier, vehicle, index)
|
|
case _ => ;
|
|
}
|
|
data
|
|
}
|
|
val originalSeated = player.VehicleSeated
|
|
player.VehicleSeated = vguid
|
|
if(Vehicles.AllGatedOccupantsInSameZone(vehicle)) {
|
|
//do not dispatch delete action if any hierarchical occupant has not gotten this far through the summoning process
|
|
val vehicleToDelete = interstellarFerryTopLevelGUID.orElse(originalSeated).getOrElse(PlanetSideGUID(0))
|
|
val zone = vehicle.PreviousGatingManifest().get.origin
|
|
zone.VehicleEvents ! VehicleServiceMessage(zone.Id, VehicleAction.UnloadVehicle(player.GUID, zone, vehicle, vehicleToDelete))
|
|
log.info(s"AvatarCreate: cleaning up ghost of transitioning vehicle ${vehicle.Definition.Name}@${vehicleToDelete.guid} in zone ${zone.Id}")
|
|
}
|
|
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
|
//log.info(s"AvatarCreate (vehicle): $guid -> $data")
|
|
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))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.LoadPlayer(guid, ObjectClass.avatar, guid, packet.ConstructorData(player).get, None))
|
|
//log.info(s"AvatarCreate: $guid -> $data")
|
|
log.trace(s"AvatarCreate: ${player.Name}")
|
|
}
|
|
continent.Population ! Zone.Population.Spawn(avatar, player)
|
|
//cautious redundancy
|
|
deadState = DeadState.Alive
|
|
ReloadUsedLastCoolDownTimes()
|
|
|
|
val lTime = System.currentTimeMillis // PTS v3
|
|
for (i <- 0 to whenUsedLastItem.length-1) {
|
|
if (lTime - whenUsedLastItem(i) < 300000) {
|
|
sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastItemName(i), 300 - ((lTime - whenUsedLastItem(i)) / 1000 toInt), true))
|
|
}
|
|
}
|
|
for (i <- 1 to 3) {
|
|
if (lTime - whenUsedLastMAX(i) < 300000) {
|
|
sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(i), 300 - ((lTime - whenUsedLastMAX(i)) / 1000 toInt), true))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the player is mounted in some entity, find that entity and get the seat index number at which the player is sat.
|
|
* The priority of object confirmation is `direct` then `occupant.VehicleSeated`.
|
|
* Once an object is found, the remainder are ignored.
|
|
* @param direct a game object in which the player may be sat
|
|
* @param target the player who is sat and may have specified the game object in which mounted
|
|
* @return a tuple consisting of a vehicle reference and a 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 GetMountableAndSeat(direct : Option[PlanetSideGameObject with Mountable], occupant : Player) : (Option[PlanetSideGameObject with Mountable], Option[Int]) =
|
|
direct.orElse(continent.GUID(occupant.VehicleSeated)) match {
|
|
case Some(obj : PlanetSideGameObject with Mountable) =>
|
|
obj.PassengerInSeat(occupant) match {
|
|
case index @ Some(_) =>
|
|
(Some(obj), index)
|
|
case None =>
|
|
(None, None)
|
|
}
|
|
case _ =>
|
|
(None, None)
|
|
}
|
|
|
|
/**
|
|
* If the player is seated in a vehicle, find that vehicle and get the 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 `GetMountableAndSeat`
|
|
* @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]) = GetMountableAndSeat(interstellarFerry, player) match {
|
|
case (Some(v : Vehicle), Some(seat)) => (Some(v), Some(seat))
|
|
case _ => (None, None)
|
|
}
|
|
|
|
/**
|
|
* If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat.
|
|
* @see `GetMountableAndSeat`
|
|
* @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]) = GetMountableAndSeat(None, player) match {
|
|
case (Some(v : Vehicle), Some(seat)) => (Some(v), Some(seat))
|
|
case _ => (None, None)
|
|
}
|
|
|
|
/**
|
|
* Create an avatar character so that avatar's player is mounted in a vehicle's seat.
|
|
* A part of the process of spawning the player into the game world.<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.
|
|
* Neither the player avatar nor the vehicle should be reconstructed before the next zone load operation
|
|
* to avoid damaging the critical setup of this function.
|
|
* @see `AccessContents`
|
|
* @see `UpdateWeaponAtSeatPosition`
|
|
* @param tplayer the player avatar seated in the vehicle's seat
|
|
* @param vehicle the vehicle the player is riding
|
|
* @param seat the seat index
|
|
*/
|
|
def AvatarCreateInVehicle(tplayer : Player, vehicle : Vehicle, seat : Int) : Unit = {
|
|
val pdef = tplayer.Definition
|
|
val pguid = tplayer.GUID
|
|
val vguid = vehicle.GUID
|
|
tplayer.VehicleSeated = None
|
|
val pdata = pdef.Packet.DetailedConstructorData(tplayer).get
|
|
tplayer.VehicleSeated = vguid
|
|
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
|
|
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
|
|
AccessContents(vehicle)
|
|
UpdateWeaponAtSeatPosition(vehicle, seat)
|
|
continent.AvatarEvents ! AvatarServiceMessage(
|
|
continent.Id,
|
|
AvatarAction.LoadPlayer(
|
|
pguid,
|
|
pdef.ObjectId,
|
|
pguid,
|
|
pdef.Packet.ConstructorData(tplayer).get,
|
|
Some(ObjectCreateMessageParent(vguid, seat))
|
|
)
|
|
)
|
|
//log.info(s"AvatarCreateInVehicle: $pguid -> pdata")
|
|
}
|
|
|
|
/**
|
|
* A part of the process of spawning the player into the game world
|
|
* in the case of a restored game connection (relogging).<br>
|
|
* <br>
|
|
* A login protocol that substitutes the first call to `avatarSetupFunc` (replacing `AvatarCreate`)
|
|
* in consideration of a user re-logging into the game
|
|
* before the period of time where an avatar/player instance would decay and be cleaned-up.
|
|
* Large portions of this function operate as a combination of the mechanics
|
|
* for normal `AvatarCreate` and for `AvatarCreateInVehicle`.
|
|
* Unlike either of the previous, this functionlality is disinterested in updating other clients
|
|
* as the target player and potential vehicle already exist as far as other clients are concerned.<br>
|
|
* <br>
|
|
* If that player is in a vehicle, it will construct that vehicle.
|
|
* If the player is the driver of the vehicle,
|
|
* they must temporarily be removed from the driver seat in order for the vehicle to be constructed properly.
|
|
* These two previous statements operate through similar though distinct mechanisms and imply different conditions.
|
|
* In reality, they produce the same output but enforce different relationships between the components.
|
|
* The vehicle without a rendered player will always be created if that vehicle exists.<br>
|
|
* <br>
|
|
* 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.
|
|
* Neither the player avatar nor the vehicle should be reconstructed before the next zone load operation
|
|
* to avoid damaging the critical setup of this function.
|
|
* @see `AccessContents`
|
|
* @see `AccountPersistenceService`
|
|
* @see `avatarSetupFunc`
|
|
* @see `AvatarCreate`
|
|
* @see `GetKnownVehicleAndSeat`
|
|
* @see `ObjectAttachMessage`
|
|
* @see `ObjectCreateMessage`
|
|
* @see `PlayerInfo.LoginInfo`
|
|
* @see `ReloadUsedLastCoolDownTimes`
|
|
* @see `UpdateWeaponAtSeatPosition`
|
|
* @see `Vehicles.ReloadAccessPermissions`
|
|
*/
|
|
def AvatarRejoin() : Unit = {
|
|
GetKnownVehicleAndSeat() match {
|
|
case (Some(vehicle : Vehicle), Some(seat : Int)) =>
|
|
//vehicle and driver/passenger
|
|
val vdef = vehicle.Definition
|
|
val vguid = vehicle.GUID
|
|
if(seat == 0) {
|
|
val seat = vehicle.Seats(0)
|
|
seat.Occupant = None
|
|
val vdata = vdef.Packet.ConstructorData(vehicle).get
|
|
sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata))
|
|
seat.Occupant = player
|
|
}
|
|
else {
|
|
val vdata = vdef.Packet.ConstructorData(vehicle).get
|
|
sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, vguid, vdata))
|
|
}
|
|
Vehicles.ReloadAccessPermissions(vehicle, continent.Id)
|
|
//log.info(s"AvatarCreate (vehicle): $vguid -> $vdata")
|
|
val pdef = player.Definition
|
|
val pguid = player.GUID
|
|
val parent = ObjectCreateMessageParent(vguid, seat)
|
|
player.VehicleSeated = None
|
|
val pdata = pdef.Packet.DetailedConstructorData(player).get
|
|
player.VehicleSeated = vguid
|
|
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
|
|
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
|
|
//log.info(s"AvatarCreateInVehicle: $pguid -> $pdata")
|
|
AccessContents(vehicle)
|
|
UpdateWeaponAtSeatPosition(vehicle, seat)
|
|
//log.trace(s"AvatarCreateInVehicle: ${player.Name} in ${vehicle.Definition.Name}")
|
|
|
|
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))
|
|
//log.info(s"AvatarCreate: $guid -> $data")
|
|
//log.trace(s"AvatarCreate: ${player.Name}")
|
|
}
|
|
//cautious redundancy
|
|
deadState = DeadState.Alive
|
|
ReloadUsedLastCoolDownTimes()
|
|
setupAvatarFunc = AvatarCreate
|
|
}
|
|
|
|
/**
|
|
* A part of the process of spawning the player into the game world
|
|
* in the case of a restored game connection (relogging).
|
|
* Rather than create any avatar here, the process has been skipped for now
|
|
* and will be handled by a different operation
|
|
* and this routine's normal operation when it revisits the same code.
|
|
* @see `avatarSetupFunc`
|
|
* @see `AvatarCreate`
|
|
* @see `ReloadUsedLastCoolDownTimes`
|
|
*/
|
|
def AvatarDeploymentPassOver() : Unit = {
|
|
ReloadUsedLastCoolDownTimes()
|
|
setupAvatarFunc = AvatarCreate
|
|
}
|
|
|
|
/**
|
|
* Sometimes the game stops you from doing something a second time as soon as you would have liked to do it again.
|
|
* This is called "skill".
|
|
* @see `AvatarVehicleTimerMessage`
|
|
*/
|
|
def ReloadUsedLastCoolDownTimes() : Unit = {
|
|
val lTime = System.currentTimeMillis
|
|
for (i <- 0 to whenUsedLastItem.length-1) {
|
|
if (lTime - whenUsedLastItem(i) < 300000) {
|
|
sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastItemName(i), 300 - ((lTime - whenUsedLastItem(i)) / 1000 toInt), true))
|
|
}
|
|
}
|
|
for (i <- 1 to 3) {
|
|
if (lTime - whenUsedLastMAX(i) < 300000) {
|
|
sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(i), 300 - ((lTime - whenUsedLastMAX(i)) / 1000 toInt), true))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.ResetAllImplants()
|
|
LoadClassicDefault(obj)
|
|
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 FriskDeadBody(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
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent))
|
|
case Some(_) | None => ;
|
|
}
|
|
})
|
|
val triggers = RemoveBoomerTriggersFromInventory()
|
|
triggers.foreach(trigger => { NormalItemDrop(obj, continent, continent.AvatarEvents)(trigger) })
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a player that has the characteristics of a corpse
|
|
* so long as the player has items in their knapsack or their holsters.
|
|
* If the player has no items stored, the clean solution is to remove the player from the game.
|
|
* To the game, that is a backpack (or some pastry, festive graphical modification allowing).
|
|
* @see `AvatarAction.ObjectDelete`
|
|
* @see `AvatarAction.Release`
|
|
* @see `AvatarServiceMessage`
|
|
* @see `FriskDeadBody`
|
|
* @see `GUIDTask.UnregisterPlayer`
|
|
* @see `ObjectDeleteMessage`
|
|
* @see `WellLootedDeadBody`
|
|
* @see `Zone.Corpse.Add`
|
|
* @param tplayer the player
|
|
*/
|
|
def PrepareToTurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = {
|
|
FriskDeadBody(tplayer)
|
|
if(!WellLootedDeadBody(tplayer)) {
|
|
TurnPlayerIntoCorpse(tplayer)
|
|
zone.Population ! Zone.Corpse.Add(tplayer)
|
|
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.Release(tplayer, zone))
|
|
}
|
|
else {
|
|
//no items in inventory; leave no corpse
|
|
val pguid = tplayer.GUID
|
|
sendResponse(ObjectDeleteMessage(pguid, 0))
|
|
zone.AvatarEvents ! AvatarServiceMessage(zone.Id, AvatarAction.ObjectDelete(pguid, pguid, 0))
|
|
taskResolver ! GUIDTask.UnregisterPlayer(tplayer)(zone.GUID)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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-lootedP, 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 WellLootedDeadBody(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(WellLootedDeadBody(obj)) {
|
|
continent.AvatarEvents ! 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. Oh no."))
|
|
}
|
|
//we are already on sanctuary, alive; what more is there to do?
|
|
}
|
|
else {
|
|
continent.GUID(player.VehicleSeated) match {
|
|
case Some(obj : Vehicle) if !obj.Destroyed =>
|
|
cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 12) //warp gates for functioning vehicles
|
|
case _ =>
|
|
cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) //player character spawns
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 EffectTarget.Category.Player => Some(player)
|
|
case EffectTarget.Category.Vehicle | EffectTarget.Category.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
|
|
//log.trace(s"StartUsingProximityUnit: ${player.Name} wants to use ${terminal.Definition.Name}@${term_guid.guid} on $target")
|
|
if(player.isAlive) {
|
|
target match {
|
|
case _ : Player =>
|
|
terminal.Actor ! CommonMessages.Use(player, Some(target))
|
|
case _ : Vehicle =>
|
|
terminal.Actor ! CommonMessages.Use(player, Some((target, continent.VehicleEvents)))
|
|
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.trace(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))
|
|
continent.AvatarEvents ! 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))
|
|
continent.AvatarEvents ! 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) =>
|
|
ResolveProjectileEntry(projectile, 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.
|
|
* @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 {
|
|
ResolveProjectileEntry(projectile, resolution, target, pos)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param projectile the projectile object
|
|
* @param resolution the resolution status to promote the projectile
|
|
* @return a copy of the projectile
|
|
*/
|
|
def ResolveProjectileEntry(projectile : Projectile, resolution : ProjectileResolution.Value, target : PlanetSideGameObject with FactionAffinity with Vitality, pos : Vector3) : Option[ResolvedProjectile] = {
|
|
if(projectile.isMiss) {
|
|
log.error("expected projectile 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))
|
|
continent.VehicleEvents ! 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))
|
|
continent.VehicleEvents ! 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.
|
|
* While the "tell" for being able to sustain damage is an entity of type `Vitality`,
|
|
* only specific `Vitality` entity types are being screened for sustaining damage.
|
|
* @see `DamageResistanceModel`
|
|
* @see `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 if obj.CanDamage =>
|
|
if(obj.spectator) {
|
|
player.death_by = -1 // little thing for auto kick
|
|
}
|
|
else {
|
|
obj.Actor ! Vitality.Damage(func)
|
|
}
|
|
case obj : Vehicle if obj.CanDamage => obj.Actor ! Vitality.Damage(func)
|
|
case obj : Amenity if obj.CanDamage => obj.Actor ! Vitality.Damage(func)
|
|
case obj : ComplexDeployable if obj.CanDamage => obj.Actor ! Vitality.Damage(func)
|
|
|
|
case obj : SimpleDeployable if obj.CanDamage =>
|
|
//damage is synchronized on `LSA` (results returned to and distributed from this `WSA`)
|
|
continent.LocalEvents ! Vitality.DamageOn(obj, 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 `DontRedrawIcons`
|
|
* @see `SetCurrentAvatar`
|
|
* @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.getOrElse(PlanetSideGUID(0)))
|
|
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 `RedrawDeployableIcons`
|
|
* @see `SetCurrentAvatar`
|
|
* @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 `FireModeSwitch.NextFireMode`
|
|
* @see `PerformConstructionItemAmmoChange`
|
|
* @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 originalAmmoIndex 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)
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(telepad), continent))
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(telepad, continent, Some(0 seconds))) //normal decay
|
|
case Some(old) =>
|
|
old.AssignOwnership(None)
|
|
continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(old), continent))
|
|
continent.LocalEvents ! 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, 21)) //reset build cooldown
|
|
sendResponse(ObjectCreateMessage(definition.ObjectId, guid, definition.Packet.ConstructorData(obj).get))
|
|
continent.AvatarEvents ! 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))
|
|
continent.LocalEvents ! LocalServiceMessage(s"${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))
|
|
continent.AvatarEvents ! 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)) {
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentInHand(guid, guid, index, obj))
|
|
if(player.DrawnSlot == Player.HandsDownSlot) {
|
|
player.DrawnSlot = index
|
|
sendResponse(ObjectHeldMessage(guid, index, false))
|
|
continent.AvatarEvents ! 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)
|
|
continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
|
|
continent.AvatarEvents ! 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()
|
|
}
|
|
|
|
/**
|
|
* 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 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) && player.HasGUID) {
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, obj.GUID))
|
|
}
|
|
obj
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 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))
|
|
shiftPosition = Some(pos)
|
|
shiftOrientation = Some(ori)
|
|
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, or a droppod
|
|
LoadZoneInVehicle(vehicle, pos, ori, zone_id)
|
|
|
|
case _ if player.HasGUID => //player is deconstructing self
|
|
val player_guid = player.GUID
|
|
sendResponse(ObjectDeleteMessage(player_guid, 4))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 4))
|
|
player.Position = pos
|
|
player.Orientation = ori
|
|
LoadZoneAsPlayer(player, zone_id)
|
|
|
|
case _ => //player is logging in
|
|
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 if(player.HasGUID) {
|
|
//unregister avatar whole + GiveWorld
|
|
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id))
|
|
}
|
|
else {
|
|
//not currently registered; so we'll just GiveWorld
|
|
(cluster, InterstellarCluster.GetWorld(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 manifest = vehicle.PrepareGatingManifest()
|
|
log.info(s"$manifest")
|
|
val pguid = player.GUID
|
|
val toChannel = manifest.file
|
|
val topLevel = interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID)
|
|
continent.VehicleEvents ! VehicleServiceMessage(
|
|
s"${vehicle.Actor}",
|
|
VehicleAction.TransferPassengerChannel(pguid, s"${vehicle.Actor}", toChannel, vehicle, topLevel)
|
|
)
|
|
manifest.cargo.foreach {
|
|
case ("MISSING_DRIVER", index) =>
|
|
val cargo = vehicle.CargoHolds(index).Occupant.get
|
|
log.error(s"LoadZoneInVehicleAsDriver: eject cargo in hold $index; vehicle missing driver")
|
|
CargoBehavior.HandleVehicleCargoDismount(cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true)
|
|
case (name, index) =>
|
|
val cargo = vehicle.CargoHolds(index).Occupant.get
|
|
continent.VehicleEvents ! VehicleServiceMessage(name, VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo, topLevel))
|
|
}
|
|
//
|
|
if(zone_id == continent.Id) {
|
|
if(vehicle.Definition == GlobalDefinitions.droppod) {
|
|
//instant action droppod in the same zone
|
|
(taskResolver, RegisterDroppod(vehicle, player))
|
|
}
|
|
else {
|
|
//transferring a vehicle between spawn points (warp gates) in the same zone
|
|
(self, PlayerLoaded(player))
|
|
}
|
|
}
|
|
else if(vehicle.Definition == GlobalDefinitions.droppod) {
|
|
LoadZoneCommonTransferActivity()
|
|
player.Continent = zone_id //forward-set the continent id to perform a test
|
|
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(player)(continent.GUID), zone_id))
|
|
}
|
|
else {
|
|
UnAccessContents(vehicle)
|
|
LoadZoneCommonTransferActivity()
|
|
player.VehicleSeated = vehicle.GUID
|
|
player.Continent = zone_id //forward-set the continent id to perform a test
|
|
interstellarFerryTopLevelGUID = (if(manifest.passengers.isEmpty && manifest.cargo.count { case (name, _) => !name.equals("MISSING_DRIVER") } == 0) {
|
|
//do not delete if vehicle has passengers or cargo
|
|
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.UnloadVehicle(pguid, continent, vehicle, topLevel))
|
|
None
|
|
}
|
|
else {
|
|
Some(topLevel)
|
|
})
|
|
//unregister vehicle and driver whole + GiveWorld
|
|
continent.Transport ! Zone.Vehicle.Despawn(vehicle)
|
|
(taskResolver, TaskBeforeZoneChange(UnregisterDrivenVehicle(vehicle, player), zone_id))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 `Vehicles.AllGatedOccupantsInSameZone`
|
|
* @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
|
|
interstellarFerryTopLevelGUID = None
|
|
//unregister avatar + GiveWorld
|
|
(taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(player)(continent.GUID), zone_id))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
def LoadZoneTransferPassengerMessages(player_guid : PlanetSideGUID, toZoneId : String, vehicle : Vehicle) : Unit = {
|
|
vehicle.PublishGatingManifest() match {
|
|
case Some(manifest) =>
|
|
val toChannel = manifest.file
|
|
val topLevel = interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID)
|
|
galaxyService ! GalaxyServiceMessage(toChannel, GalaxyAction.TransferPassenger(player_guid, toChannel, vehicle, topLevel, manifest))
|
|
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
|
|
galaxyService ! GalaxyServiceMessage(toChannel, GalaxyAction.TransferPassenger(player_guid, toChannel, vehicle, topLevel, manifest))
|
|
}
|
|
case None =>
|
|
log.error("LoadZoneTransferPassengerMessages: expected a manifest for zone transfer; got nothing")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
})
|
|
Deployables.Disown(continent, avatar, self)
|
|
drawDeloyableIcon = RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
|
|
squadSetup = ZoneChangeSquadSetup
|
|
}
|
|
|
|
/**
|
|
* 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 = {
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
//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
|
|
}
|
|
continent.AvatarEvents ! 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) =>
|
|
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
|
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
|
|
)
|
|
)
|
|
continent.AvatarEvents ! 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, 27))
|
|
sendResponse(GenericObjectActionMessage(uguid, 30))
|
|
/*
|
|
the following configurations create the interactive beam underneath the Deployed Router
|
|
normally dispatched after the warm-up timer has completed
|
|
*/
|
|
sendResponse(GenericObjectActionMessage(uguid, 27))
|
|
sendResponse(GenericObjectActionMessage(uguid, 28))
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param telepadGUID na
|
|
*/
|
|
def LinkRemoteTelepad(telepadGUID: PlanetSideGUID) : Unit = {
|
|
sendResponse(GenericObjectActionMessage(telepadGUID, 27))
|
|
sendResponse(GenericObjectActionMessage(telepadGUID, 28))
|
|
}
|
|
|
|
/**
|
|
* 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()
|
|
continent.LocalEvents ! 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, 31))
|
|
sendResponse(GenericObjectActionMessage(destGUID, 32))
|
|
}
|
|
|
|
/**
|
|
* 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 ...")
|
|
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.UpdateAmsSpawnPoint(continent))
|
|
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 ...")
|
|
Deployables.RemoveTelepad(vehicle)
|
|
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))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, weapon_guid))
|
|
sendResponse(WeaponDryFireMessage(weapon_guid))
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.WeaponDryFire(player.GUID, weapon_guid))
|
|
}
|
|
|
|
def SaveLoadoutToDB(owner : Player, label : String, line : Int) = {
|
|
val charId = owner.CharId
|
|
val exosuitId = owner.ExoSuit.id
|
|
var clob : String = {
|
|
val clobber : StringBuilder = new StringBuilder()
|
|
//encode holsters
|
|
owner.Holsters()
|
|
.zipWithIndex
|
|
.collect { case (slot, index) if slot.Equipment.nonEmpty =>
|
|
clobber.append(EncodeLoadoutCLOBFragment(slot.Equipment.get, index))
|
|
}
|
|
//encode inventory
|
|
owner.Inventory.Items.foreach { case InventoryItem(obj, index) =>
|
|
clobber.append(EncodeLoadoutCLOBFragment(obj, index))
|
|
}
|
|
clobber.mkString
|
|
}
|
|
//database
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
Database.query(connection.sendPreparedStatement(
|
|
"SELECT id, exosuit_id, name, items FROM loadouts where characters_id = ? AND loadout_number = ?", Array(charId, line)
|
|
)).onComplete {
|
|
case scala.util.Success(queryResult) =>
|
|
queryResult match {
|
|
case row: ArrayRowData => // Update
|
|
connection.sendPreparedStatement(
|
|
"UPDATE loadouts SET exosuit_id=?, name=?, items=? where id=?", Array(exosuitId, label, clob.drop(1), row(0))
|
|
).onComplete {
|
|
case _ =>
|
|
if(connection.isConnected) connection.disconnect
|
|
}
|
|
Thread.sleep(50)
|
|
case _ => // Save
|
|
connection.sendPreparedStatement(
|
|
"INSERT INTO loadouts (characters_id, loadout_number, exosuit_id, name, items) VALUES(?,?,?,?,?) RETURNING id",
|
|
Array(charId, line, exosuitId, label, clob.drop(1))
|
|
).onComplete {
|
|
case _ =>
|
|
if(connection.isConnected) connection.disconnect
|
|
}
|
|
Thread.sleep(50)
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
log.error(s"SaveLoadoutToDB: query failed - ${e.getMessage}")
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
log.error(s"SaveLoadoutToDB: no connection ${e.getMessage}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* na
|
|
* @param equipment
|
|
* @param index
|
|
* @return
|
|
*/
|
|
def EncodeLoadoutCLOBFragment(equipment : Equipment, index : Int) : String = {
|
|
val ammoInfo : String = equipment match {
|
|
case tool : Tool =>
|
|
tool.AmmoSlots
|
|
.zipWithIndex
|
|
.collect { case (ammoSlot, index2) if ammoSlot.AmmoTypeIndex != 0 =>
|
|
s"_$index2-${ammoSlot.AmmoTypeIndex}-${ammoSlot.AmmoType.id}"
|
|
}
|
|
.mkString
|
|
case _ =>
|
|
""
|
|
}
|
|
s"/${equipment.getClass.getSimpleName},$index,${equipment.Definition.ObjectId},$ammoInfo"
|
|
}
|
|
|
|
/**
|
|
* A selection of up to ten customized equipment loadouts that are saved externally.
|
|
* The loadouts are encoded through number and text and procedural assembly is required.
|
|
* When loaded properly, these loadouts will become available through an equipment terminal entity
|
|
* and will influence the equipment terminal to open to the equipment loadout selection tab called "Favorites."<br>
|
|
* <br>
|
|
* The operation requires a database connection and completion of a database transaction,
|
|
* both of which must completed independently of any subsequent tasking,
|
|
* especially if that future tasking may require database use.
|
|
* @see `ClearHolstersAndInventory`
|
|
* @see `Connection.sendPreparedStatement`
|
|
* @see `Database.getConnection`
|
|
* @see `ExoSuitType`
|
|
* @see `Future`
|
|
* @see `GetToolDefFromObjectID`
|
|
* @see `Loadout`
|
|
* @see `Player.EquipmentLoadouts`
|
|
* @see `Player.EquipmentLoadouts.SaveLoadout`
|
|
* @see `Promise`
|
|
* @see `QueryResult`
|
|
* @param owner the player who will be stipped of equipment
|
|
* @return a `Future` predicated by the "promise" of the task being completed
|
|
*/
|
|
def LoadDataBaseLoadouts(owner : Player) : Future[Any] = {
|
|
val result : Promise[Any] = Promise[Any]()
|
|
Database.getConnection.connect.onComplete {
|
|
case scala.util.Success(connection) =>
|
|
connection.sendPreparedStatement(
|
|
"SELECT id, loadout_number, exosuit_id, name, items FROM loadouts where characters_id = ?", Array(owner.CharId)
|
|
).onComplete {
|
|
case Success(queryResult) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
queryResult match {
|
|
case result: QueryResult =>
|
|
if (result.rows.nonEmpty) {
|
|
val doll = new Player(Avatar("doll", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) //play dress up
|
|
log.debug(s"LoadDataBaseLoadouts: ${result.rows.size} saved loadout(s) for character with id ${owner.CharId}")
|
|
result.rows foreach{ row =>
|
|
row.zipWithIndex.foreach{ case (value,i) =>
|
|
val lLoadoutNumber : Int = value(1).asInstanceOf[Int]
|
|
val lExosuitId : Int = value(2).asInstanceOf[Int]
|
|
val lName : String = value(3).asInstanceOf[String]
|
|
val lItems : String = value(4).asInstanceOf[String]
|
|
|
|
doll.ExoSuit = ExoSuitType(lExosuitId)
|
|
|
|
val args = lItems.split("/")
|
|
args.indices.foreach(i => {
|
|
val args2 = args(i).split(",")
|
|
val lType = args2(0)
|
|
val lIndex : Int = args2(1).toInt
|
|
val lObjectId : Int = args2(2).toInt
|
|
|
|
lType match {
|
|
case "Tool" =>
|
|
doll.Slot(lIndex).Equipment = Tool(GetToolDefFromObjectID(lObjectId).asInstanceOf[ToolDefinition])
|
|
case "AmmoBox" =>
|
|
doll.Slot(lIndex).Equipment = AmmoBox(GetToolDefFromObjectID(lObjectId).asInstanceOf[AmmoBoxDefinition])
|
|
case "ConstructionItem" =>
|
|
doll.Slot(lIndex).Equipment = ConstructionItem(GetToolDefFromObjectID(lObjectId).asInstanceOf[ConstructionItemDefinition])
|
|
case "SimpleItem" =>
|
|
doll.Slot(lIndex).Equipment = SimpleItem(GetToolDefFromObjectID(lObjectId).asInstanceOf[SimpleItemDefinition])
|
|
case "Kit" =>
|
|
doll.Slot(lIndex).Equipment = Kit(GetToolDefFromObjectID(lObjectId).asInstanceOf[KitDefinition])
|
|
case thing =>
|
|
log.warn(s"LoadDataBaseLoadouts: what's that $thing doing in the loadout?")
|
|
}
|
|
if (args2.length == 4) { //tool ammo info
|
|
val args3 = args2(3).split("_")
|
|
(1 until args3.length).foreach(j => {
|
|
val args4 = args3(j).split("-")
|
|
val lAmmoSlots = args4(0).toInt
|
|
val lAmmoTypeIndex = args4(1).toInt
|
|
val lAmmoBoxDefinition = args4(2).toInt
|
|
doll.Slot(lIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(lAmmoSlots).AmmoTypeIndex = lAmmoTypeIndex
|
|
doll.Slot(lIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(lAmmoSlots).Box = AmmoBox(AmmoBoxDefinition(lAmmoBoxDefinition))
|
|
})
|
|
}
|
|
})
|
|
owner.EquipmentLoadouts.SaveLoadout(doll, lName, lLoadoutNumber)
|
|
ClearHolstersAndInventory(doll)
|
|
}
|
|
// something to do at end of loading ?
|
|
}
|
|
}
|
|
case _ =>
|
|
log.debug(s"LoadDataBaseLoadouts: no saved loadout(s) for character with id ${owner.CharId}")
|
|
}
|
|
result success queryResult
|
|
case scala.util.Failure(e) =>
|
|
if(connection.isConnected) connection.disconnect
|
|
val msg = s"LoadDataBaseLoadouts: unexpected query result - ${e.getMessage}"
|
|
log.error(msg)
|
|
result failure new Throwable(msg)
|
|
}
|
|
case scala.util.Failure(e) =>
|
|
val msg = s"LoadDataBaseLoadouts: no connection - ${e.getMessage}"
|
|
log.error(msg)
|
|
result failure new Throwable(msg)
|
|
}
|
|
result.future
|
|
}
|
|
|
|
/**
|
|
* Remove the equipment from all holsters and from out of the player inventory,
|
|
* no matter how spacious it is.
|
|
* @param target the player who will be stipped of equipment
|
|
*/
|
|
def ClearHolstersAndInventory(target : Player) : Unit = {
|
|
(0 until 4).foreach( index => {
|
|
target.Slot(index).Equipment = None
|
|
})
|
|
target.Inventory.Clear()
|
|
}
|
|
|
|
/**
|
|
* The "default loadout."
|
|
* The selection of equipment that a player respawns in possession of after dying.
|
|
* Excepting the pistol (slot 0) and its ammo and the melee weapon, which are all faction-associated,
|
|
* all equipment is generic.
|
|
* All equipment belongs to the implicit `Standard` certification.
|
|
* @param target the player who will be assigned this selection of equipment
|
|
*/
|
|
def LoadClassicDefault(target : Player) : Unit = {
|
|
val faction = target.Faction
|
|
target.ExoSuit = ExoSuitType.Standard
|
|
target.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(faction))
|
|
target.Slot(2).Equipment = Tool(GlobalDefinitions.suppressor)
|
|
target.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(faction))
|
|
target.Slot(6).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
|
|
target.Slot(9).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
|
|
target.Slot(12).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
|
|
target.Slot(33).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm_AP)
|
|
target.Slot(36).Equipment = AmmoBox(GlobalDefinitions.StandardPistolAmmo(faction))
|
|
target.Slot(39).Equipment = SimpleItem(GlobalDefinitions.remote_electronics_kit)
|
|
target.Inventory.Items.foreach { _.obj.Faction = faction }
|
|
}
|
|
|
|
/**
|
|
* A selection of ten customized equipment loadouts.
|
|
* Currently, unused.
|
|
* @see `ClearHolstersAndInventory`
|
|
* @see `GlobalDefinitions`
|
|
* @see `Player.EquipmentLoadouts`
|
|
* @see `Player.EquipmentLoadouts.SaveLoadout`
|
|
* @see `Player.ExoSuit`
|
|
* @see `Player.Slot`
|
|
* @param target the player who will be assigned these loadouts
|
|
*/
|
|
def LoadDefaultLoadouts(target : Player) : Unit = {
|
|
//cached defaults
|
|
val faction = target.Faction
|
|
val aiMaxAmmo = AmmoBox(GlobalDefinitions.AI_MAXAmmo(faction))
|
|
val avMaxAmmo = AmmoBox(GlobalDefinitions.AV_MAXAmmo(faction))
|
|
val antiVehicularAmmo = AmmoBox(GlobalDefinitions.AntiVehicularAmmo(faction))
|
|
val armorCanister = AmmoBox(GlobalDefinitions.armor_canister)
|
|
val bank = Tool(GlobalDefinitions.bank)
|
|
val bolt = AmmoBox(GlobalDefinitions.bolt)
|
|
val decimator = Tool(GlobalDefinitions.phoenix)
|
|
val fragGrenade = Tool(GlobalDefinitions.frag_grenade)
|
|
val fragCartridge = AmmoBox(GlobalDefinitions.frag_cartridge)
|
|
val healthCanister = AmmoBox(GlobalDefinitions.health_canister)
|
|
val heavyRifle = Tool(GlobalDefinitions.HeavyRifle(faction))
|
|
val heavyRifleAmmo= AmmoBox(GlobalDefinitions.HeavyRifleAmmo(faction))
|
|
val heavyRifleAPAmmo= AmmoBox(GlobalDefinitions.HeavyRifleAPAmmo(faction))
|
|
val jammerGrenade = Tool(GlobalDefinitions.jammer_grenade)
|
|
val medicalApplicator = Tool(GlobalDefinitions.medicalapplicator)
|
|
val mediumRifle = Tool(GlobalDefinitions.MediumRifle(faction))
|
|
val mediumRifleAmmo = AmmoBox(GlobalDefinitions.MediumRifleAmmo(faction))
|
|
val medkit = Kit(GlobalDefinitions.medkit)
|
|
val rek = SimpleItem(GlobalDefinitions.remote_electronics_kit)
|
|
val rocket = AmmoBox(GlobalDefinitions.rocket)
|
|
val shotgunAmmo = AmmoBox(GlobalDefinitions.shotgun_shell)
|
|
//
|
|
val doll = new Player(Avatar("doll", faction, CharacterGender.Male, 0, CharacterVoice.Mute)) //play dress up
|
|
doll.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(faction)) //will not be cleared
|
|
// 1
|
|
doll.ExoSuit = ExoSuitType.Agile
|
|
doll.Slot(0).Equipment = fragGrenade
|
|
doll.Slot(1).Equipment = bank
|
|
doll.Slot(2).Equipment = heavyRifle
|
|
doll.Slot(6).Equipment = medicalApplicator
|
|
doll.Slot(9).Equipment = heavyRifleAmmo
|
|
doll.Slot(12).Equipment = heavyRifleAmmo
|
|
doll.Slot(33).Equipment = decimator
|
|
doll.Slot(60).Equipment = rek
|
|
doll.Slot(72).Equipment = jammerGrenade
|
|
doll.Slot(74).Equipment = medkit
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Agile HA/Deci", 0)
|
|
ClearHolstersAndInventory(doll)
|
|
// 2
|
|
doll.ExoSuit = ExoSuitType.Agile
|
|
doll.Slot(0).Equipment = fragGrenade
|
|
doll.Slot(1).Equipment = fragGrenade
|
|
doll.Slot(2).Equipment = heavyRifle
|
|
doll.Slot(6).Equipment = heavyRifleAmmo
|
|
doll.Slot(9).Equipment = heavyRifleAmmo
|
|
doll.Slot(12).Equipment = rek
|
|
doll.Slot(33).Equipment = medicalApplicator
|
|
doll.Slot(36).Equipment = fragGrenade
|
|
doll.Slot(38).Equipment = medkit
|
|
doll.Slot(54).Equipment = fragGrenade
|
|
doll.Slot(56).Equipment = medkit
|
|
doll.Slot(60).Equipment = bank
|
|
doll.Slot(72).Equipment = jammerGrenade
|
|
doll.Slot(74).Equipment = medkit
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Agile HA", 1)
|
|
ClearHolstersAndInventory(doll)
|
|
// 3
|
|
doll.ExoSuit = ExoSuitType.Reinforced
|
|
doll.Slot(0).Equipment = medicalApplicator
|
|
doll.Slot(1).Equipment = rek
|
|
doll.Slot(2).Equipment = heavyRifle
|
|
doll.Slot(3).Equipment = decimator
|
|
doll.Slot(6).Equipment = heavyRifleAmmo
|
|
doll.Slot(9).Equipment = heavyRifleAmmo
|
|
doll.Slot(12).Equipment = medkit
|
|
doll.Slot(16).Equipment = fragGrenade
|
|
doll.Slot(36).Equipment = medkit
|
|
doll.Slot(40).Equipment = fragGrenade
|
|
doll.Slot(42).Equipment = heavyRifleAmmo
|
|
doll.Slot(45).Equipment = heavyRifleAPAmmo
|
|
doll.Slot(60).Equipment = medkit
|
|
doll.Slot(64).Equipment = jammerGrenade
|
|
doll.Slot(78).Equipment = decimator
|
|
doll.Slot(87).Equipment = bank
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Rexo HA/Deci", 2)
|
|
ClearHolstersAndInventory(doll)
|
|
// 4
|
|
doll.ExoSuit = ExoSuitType.Reinforced
|
|
doll.Slot(0).Equipment = medicalApplicator
|
|
doll.Slot(1).Equipment = rek
|
|
doll.Slot(2).Equipment = mediumRifle
|
|
doll.Slot(3).Equipment = Tool(GlobalDefinitions.AntiVehicularLauncher(faction))
|
|
doll.Slot(6).Equipment = mediumRifleAmmo
|
|
doll.Slot(9).Equipment = mediumRifleAmmo
|
|
doll.Slot(12).Equipment = mediumRifleAmmo
|
|
doll.Slot(15).Equipment = bank
|
|
doll.Slot(42).Equipment = fragGrenade
|
|
doll.Slot(44).Equipment = jammerGrenade
|
|
doll.Slot(46).Equipment = medkit
|
|
doll.Slot(50).Equipment = medkit
|
|
doll.Slot(66).Equipment = antiVehicularAmmo
|
|
doll.Slot(70).Equipment = antiVehicularAmmo
|
|
doll.Slot(74).Equipment = antiVehicularAmmo
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Rexo MA/AV", 3)
|
|
ClearHolstersAndInventory(doll)
|
|
// 5
|
|
doll.ExoSuit = ExoSuitType.Reinforced
|
|
doll.Slot(0).Equipment = medicalApplicator
|
|
doll.Slot(1).Equipment = rek
|
|
doll.Slot(2).Equipment = heavyRifle
|
|
doll.Slot(3).Equipment = Tool(GlobalDefinitions.thumper)
|
|
doll.Slot(6).Equipment = heavyRifleAmmo
|
|
doll.Slot(9).Equipment = heavyRifleAmmo
|
|
doll.Slot(12).Equipment = medkit
|
|
doll.Slot(16).Equipment = fragGrenade
|
|
doll.Slot(36).Equipment = medkit
|
|
doll.Slot(40).Equipment = fragGrenade
|
|
doll.Slot(42).Equipment = heavyRifleAmmo
|
|
doll.Slot(45).Equipment = heavyRifleAPAmmo
|
|
doll.Slot(60).Equipment = medkit
|
|
doll.Slot(64).Equipment = jammerGrenade
|
|
doll.Slot(78).Equipment = bank
|
|
doll.Slot(81).Equipment = fragCartridge
|
|
doll.Slot(84).Equipment = fragCartridge
|
|
doll.Slot(87).Equipment = fragCartridge
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Rexo HA/Thumper", 4)
|
|
ClearHolstersAndInventory(doll)
|
|
// 6
|
|
doll.ExoSuit = ExoSuitType.Reinforced
|
|
doll.Slot(0).Equipment = medicalApplicator
|
|
doll.Slot(1).Equipment = rek
|
|
doll.Slot(2).Equipment = heavyRifle
|
|
doll.Slot(3).Equipment = Tool(GlobalDefinitions.rocklet)
|
|
doll.Slot(6).Equipment = heavyRifleAmmo
|
|
doll.Slot(9).Equipment = heavyRifleAmmo
|
|
doll.Slot(12).Equipment = medkit
|
|
doll.Slot(16).Equipment = fragGrenade
|
|
doll.Slot(36).Equipment = medkit
|
|
doll.Slot(40).Equipment = fragGrenade
|
|
doll.Slot(42).Equipment = heavyRifleAmmo
|
|
doll.Slot(45).Equipment = heavyRifleAPAmmo
|
|
doll.Slot(60).Equipment = medkit
|
|
doll.Slot(64).Equipment = jammerGrenade
|
|
doll.Slot(78).Equipment = bank
|
|
doll.Slot(81).Equipment = rocket
|
|
doll.Slot(84).Equipment = rocket
|
|
doll.Slot(87).Equipment = fragCartridge
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Rexo HA/Rocklet", 5)
|
|
ClearHolstersAndInventory(doll)
|
|
// 7
|
|
doll.ExoSuit = ExoSuitType.Reinforced
|
|
doll.Slot(0).Equipment = medicalApplicator
|
|
doll.Slot(1).Equipment = rek
|
|
doll.Slot(2).Equipment = mediumRifle
|
|
doll.Slot(3).Equipment = Tool(GlobalDefinitions.bolt_driver)
|
|
doll.Slot(6).Equipment = mediumRifleAmmo
|
|
doll.Slot(9).Equipment = mediumRifleAmmo
|
|
doll.Slot(12).Equipment = medkit
|
|
doll.Slot(16).Equipment = fragGrenade
|
|
doll.Slot(36).Equipment = medkit
|
|
doll.Slot(40).Equipment = fragGrenade
|
|
doll.Slot(42).Equipment = mediumRifleAmmo
|
|
doll.Slot(45).Equipment = mediumRifleAmmo
|
|
doll.Slot(60).Equipment = medkit
|
|
doll.Slot(64).Equipment = jammerGrenade
|
|
doll.Slot(78).Equipment = bank
|
|
doll.Slot(81).Equipment = bolt
|
|
doll.Slot(84).Equipment = bolt
|
|
doll.Slot(87).Equipment = bolt
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Rexo MA/Sniper", 6)
|
|
ClearHolstersAndInventory(doll)
|
|
// 8
|
|
doll.ExoSuit = ExoSuitType.Reinforced
|
|
doll.Slot(0).Equipment = medicalApplicator
|
|
doll.Slot(1).Equipment = rek
|
|
doll.Slot(2).Equipment = Tool(GlobalDefinitions.flechette)
|
|
doll.Slot(3).Equipment = decimator
|
|
doll.Slot(6).Equipment = shotgunAmmo
|
|
doll.Slot(9).Equipment = shotgunAmmo
|
|
doll.Slot(12).Equipment = medkit
|
|
doll.Slot(16).Equipment = fragGrenade
|
|
doll.Slot(36).Equipment = medkit
|
|
doll.Slot(40).Equipment = fragGrenade
|
|
doll.Slot(42).Equipment = shotgunAmmo
|
|
doll.Slot(45).Equipment = AmmoBox(GlobalDefinitions.shotgun_shell_AP)
|
|
doll.Slot(60).Equipment = medkit
|
|
doll.Slot(64).Equipment = jammerGrenade
|
|
doll.Slot(78).Equipment = decimator
|
|
doll.Slot(87).Equipment = bank
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "Rexo Sweeper/Deci", 7)
|
|
ClearHolstersAndInventory(doll)
|
|
// 9
|
|
doll.ExoSuit = ExoSuitType.MAX
|
|
doll.Slot(0).Equipment = Tool(GlobalDefinitions.AI_MAX(faction))
|
|
doll.Slot(6).Equipment = aiMaxAmmo
|
|
doll.Slot(10).Equipment = aiMaxAmmo
|
|
doll.Slot(14).Equipment = aiMaxAmmo
|
|
doll.Slot(18).Equipment = aiMaxAmmo
|
|
doll.Slot(70).Equipment = aiMaxAmmo
|
|
doll.Slot(74).Equipment = aiMaxAmmo
|
|
doll.Slot(78).Equipment = medkit
|
|
doll.Slot(98).Equipment = healthCanister
|
|
doll.Slot(100).Equipment = armorCanister
|
|
doll.Slot(110).Equipment = medkit
|
|
doll.Slot(134).Equipment = medkit
|
|
doll.Slot(138).Equipment = medkit
|
|
doll.Slot(142).Equipment = medkit
|
|
doll.Slot(146).Equipment = medkit
|
|
doll.Slot(166).Equipment = medkit
|
|
doll.Slot(170).Equipment = medkit
|
|
doll.Slot(174).Equipment = medkit
|
|
doll.Slot(178).Equipment = medkit
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "AI MAX", 8)
|
|
ClearHolstersAndInventory(doll)
|
|
// 10
|
|
doll.ExoSuit = ExoSuitType.MAX
|
|
doll.Slot(0).Equipment = Tool(GlobalDefinitions.AV_MAX(faction))
|
|
doll.Slot(6).Equipment = avMaxAmmo
|
|
doll.Slot(10).Equipment = avMaxAmmo
|
|
doll.Slot(14).Equipment = avMaxAmmo
|
|
doll.Slot(18).Equipment = avMaxAmmo
|
|
doll.Slot(70).Equipment = avMaxAmmo
|
|
doll.Slot(74).Equipment = avMaxAmmo
|
|
doll.Slot(78).Equipment = medkit
|
|
doll.Slot(98).Equipment = healthCanister
|
|
doll.Slot(100).Equipment = armorCanister
|
|
doll.Slot(110).Equipment = medkit
|
|
doll.Slot(134).Equipment = medkit
|
|
doll.Slot(138).Equipment = medkit
|
|
doll.Slot(142).Equipment = medkit
|
|
doll.Slot(146).Equipment = medkit
|
|
doll.Slot(166).Equipment = medkit
|
|
doll.Slot(170).Equipment = medkit
|
|
doll.Slot(174).Equipment = medkit
|
|
doll.Slot(178).Equipment = medkit
|
|
target.EquipmentLoadouts.SaveLoadout(doll, "AV MAX", 9)
|
|
}
|
|
|
|
def GetToolDefFromObjectID(objectID : Int) : Any = {
|
|
import net.psforever.objects.GlobalDefinitions._
|
|
objectID match {
|
|
//ammunition
|
|
case 0 => bullet_105mm
|
|
case 3 => bullet_12mm
|
|
case 6 => bullet_150mm
|
|
case 9 => bullet_15mm
|
|
case 16 => bullet_20mm
|
|
case 19 => bullet_25mm
|
|
case 21 => bullet_35mm
|
|
case 25 => bullet_75mm
|
|
case 28 => bullet_9mm
|
|
case 29 => bullet_9mm_AP
|
|
case 50 => ancient_ammo_combo
|
|
case 51 => ancient_ammo_vehicle
|
|
case 54 => anniversary_ammo
|
|
case 86 => aphelion_immolation_cannon_ammo
|
|
case 89 => aphelion_laser_ammo
|
|
case 97 => aphelion_plasma_rocket_ammo
|
|
case 101 => aphelion_ppa_ammo
|
|
case 106 => aphelion_starfire_ammo
|
|
case 111 => armor_canister
|
|
case 145 => bolt
|
|
case 154 => burster_ammo
|
|
case 180 => colossus_100mm_cannon_ammo
|
|
case 186 => colossus_burster_ammo
|
|
case 191 => colossus_chaingun_ammo
|
|
case 195 => colossus_cluster_bomb_ammo
|
|
case 205 => colossus_tank_cannon_ammo
|
|
case 209 => comet_ammo
|
|
case 265 => dualcycler_ammo
|
|
case 272 => energy_cell
|
|
case 275 => energy_gun_ammo
|
|
case 285 => falcon_ammo
|
|
case 287 => firebird_missile
|
|
case 300 => flamethrower_ammo
|
|
case 307 => flux_cannon_thresher_battery
|
|
case 310 => fluxpod_ammo
|
|
case 327 => frag_cartridge
|
|
case 331 => frag_grenade_ammo
|
|
case 347 => gauss_cannon_ammo
|
|
case 389 => health_canister
|
|
case 391 => heavy_grenade_mortar
|
|
case 393 => heavy_rail_beam_battery
|
|
case 399 => hellfire_ammo
|
|
case 403 => hunter_seeker_missile
|
|
case 413 => jammer_cartridge
|
|
case 417 => jammer_grenade_ammo
|
|
case 426 => lancer_cartridge
|
|
case 434 => liberator_bomb
|
|
case 463 => maelstrom_ammo
|
|
case 540 => melee_ammo
|
|
case 600 => oicw_ammo
|
|
case 630 => pellet_gun_ammo
|
|
case 637 => peregrine_dual_machine_gun_ammo
|
|
case 645 => peregrine_mechhammer_ammo
|
|
case 653 => peregrine_particle_cannon_ammo
|
|
case 656 => peregrine_rocket_pod_ammo
|
|
case 659 => peregrine_sparrow_ammo
|
|
case 664 => phalanx_ammo
|
|
case 674 => phoenix_missile
|
|
case 677 => plasma_cartridge
|
|
case 681 => plasma_grenade_ammo
|
|
case 693 => pounder_ammo
|
|
case 704 => pulse_battery
|
|
case 712 => quasar_ammo
|
|
case 722 => reaver_rocket
|
|
case 734 => rocket
|
|
case 745 => scattercannon_ammo
|
|
case 755 => shotgun_shell
|
|
case 756 => shotgun_shell_AP
|
|
case 762 => six_shooter_ammo
|
|
case 786 => skyguard_flak_cannon_ammo
|
|
case 791 => sparrow_ammo
|
|
case 820 => spitfire_aa_ammo
|
|
case 823 => spitfire_ammo
|
|
case 830 => starfire_ammo
|
|
case 839 => striker_missile_ammo
|
|
case 877 => trek_ammo
|
|
case 922 => upgrade_canister
|
|
case 998 => wasp_gun_ammo
|
|
case 1000 => wasp_rocket_ammo
|
|
case 1004 => winchester_ammo
|
|
//weapons
|
|
case 14 => cannon_dropship_20mm
|
|
case 40 => advanced_missile_launcher_t
|
|
case 55 => anniversary_gun
|
|
case 56 => anniversary_guna
|
|
case 57 => anniversary_gunb
|
|
case 63 => apc_ballgun_l
|
|
case 64 => apc_ballgun_r
|
|
case 69 => apc_weapon_systema
|
|
case 70 => apc_weapon_systemb
|
|
case 72 => apc_weapon_systemc_nc
|
|
case 73 => apc_weapon_systemc_tr
|
|
case 74 => apc_weapon_systemc_vs
|
|
case 76 => apc_weapon_systemd_nc
|
|
case 77 => apc_weapon_systemd_tr
|
|
case 78 => apc_weapon_systemd_vs
|
|
case 119 => aurora_weapon_systema
|
|
case 120 => aurora_weapon_systemb
|
|
case 136 => battlewagon_weapon_systema
|
|
case 137 => battlewagon_weapon_systemb
|
|
case 138 => battlewagon_weapon_systemc
|
|
case 139 => battlewagon_weapon_systemd
|
|
case 140 => beamer
|
|
case 146 => bolt_driver
|
|
case 175 => chainblade
|
|
case 177 => chaingun_p
|
|
case 233 => cycler
|
|
case 262 => dropship_rear_turret
|
|
case 274 => energy_gun
|
|
case 276 => energy_gun_nc
|
|
case 278 => energy_gun_tr
|
|
case 280 => energy_gun_vs
|
|
case 298 => flail_weapon
|
|
case 299 => flamethrower
|
|
case 304 => flechette
|
|
case 306 => flux_cannon_thresher
|
|
case 324 => forceblade
|
|
case 336 => fury_weapon_systema
|
|
case 339 => galaxy_gunship_cannon
|
|
case 340 => galaxy_gunship_gun
|
|
case 342 => galaxy_gunship_tailgun
|
|
case 345 => gauss
|
|
case 371 => grenade_launcher_marauder
|
|
case 394 => heavy_rail_beam_magrider
|
|
case 396 => heavy_sniper
|
|
case 406 => hunterseeker
|
|
case 407 => ilc9
|
|
case 411 => isp
|
|
case 421 => katana
|
|
case 425 => lancer
|
|
case 429 => lasher
|
|
case 433 => liberator_25mm_cannon
|
|
case 435 => liberator_bomb_bay
|
|
case 440 => liberator_weapon_system
|
|
case 445 => lightgunship_weapon_system
|
|
case 448 => lightning_weapon_system
|
|
case 462 => maelstrom
|
|
case 468 => magcutter
|
|
case 534 => mediumtransport_weapon_systemA
|
|
case 535 => mediumtransport_weapon_systemB
|
|
case 556 => mini_chaingun
|
|
case 587 => nchev_falcon
|
|
case 588 => nchev_scattercannon
|
|
case 589 => nchev_sparrow
|
|
case 599 => oicw
|
|
case 628 => particle_beam_magrider
|
|
case 629 => pellet_gun
|
|
case 666 => phalanx_avcombo
|
|
case 668 => phalanx_flakcombo
|
|
case 670 => phalanx_sgl_hevgatcan
|
|
case 673 => phoenix
|
|
case 699 => prowler_weapon_systemA
|
|
case 700 => prowler_weapon_systemB
|
|
case 701 => pulsar
|
|
case 706 => punisher
|
|
case 709 => quadassault_weapon_system
|
|
case 714 => r_shotgun
|
|
case 716 => radiator
|
|
case 730 => repeater
|
|
case 737 => rocklet
|
|
case 740 => rotarychaingun_mosquito
|
|
case 747 => scythe
|
|
case 761 => six_shooter
|
|
case 788 => skyguard_weapon_system
|
|
case 817 => spiker
|
|
case 822 => spitfire_aa_weapon
|
|
case 827 => spitfire_weapon
|
|
case 838 => striker
|
|
case 845 => suppressor
|
|
case 864 => thumper
|
|
case 866 => thunderer_weapon_systema
|
|
case 867 => thunderer_weapon_systemb
|
|
case 888 => trhev_burster
|
|
case 889 => trhev_dualcycler
|
|
case 890 => trhev_pounder
|
|
case 927 => vanguard_weapon_system
|
|
case 968 => vshev_comet
|
|
case 969 => vshev_quasar
|
|
case 970 => vshev_starfire
|
|
case 987 => vulture_bomb_bay
|
|
case 990 => vulture_nose_weapon_system
|
|
case 992 => vulture_tail_cannon
|
|
case 1002 => wasp_weapon_system
|
|
case 1003 => winchester
|
|
case 267 => dynomite
|
|
case 330 => frag_grenade
|
|
case 416 => jammer_grenade
|
|
case 680 => plasma_grenade
|
|
//medkits
|
|
case 536 => medkit
|
|
case 842 => super_armorkit
|
|
case 843 => super_medkit
|
|
case 844 => super_staminakit
|
|
//tools
|
|
case 728 => remote_electronics_kit
|
|
case 876 => trek
|
|
case 531 => medicalapplicator
|
|
case 132 => bank
|
|
case 577 => nano_dispenser
|
|
case 213 => command_detonater
|
|
case 297 => flail_targeting_laser
|
|
//deployables
|
|
case 32 => ace
|
|
case 39 => advanced_ace
|
|
case 148 => boomer
|
|
case 149 => boomer_trigger
|
|
case _ => frag_grenade
|
|
}
|
|
}
|
|
/**
|
|
* 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(fromZoneId.equals("Nowhere") || 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(toIndex) //the players have already been swapped in the backend object
|
|
val fromCharId = fromMember.CharId
|
|
val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object
|
|
val toCharId = toMember.CharId
|
|
val id = 11
|
|
if(toCharId > 0) {
|
|
//toMember and fromMember have swapped places
|
|
val fromElem = squadUI(fromCharId)
|
|
val toElem = squadUI(toCharId)
|
|
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(fromCharId)
|
|
squadUI(fromCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position)
|
|
sendResponse(SquadMemberEvent.Remove(id, fromCharId, fromIndex))
|
|
sendResponse(SquadMemberEvent.Add(id, fromCharId, toIndex, elem.name, elem.zone, unk7 = 0))
|
|
sendResponse(
|
|
SquadState(
|
|
PlanetSideGUID(id),
|
|
List(SquadStateInfo(fromCharId, 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 NoSquadUpdates() : Unit = { }
|
|
|
|
def SquadUpdates() : Unit = {
|
|
squadService ! SquadServiceMessage(
|
|
player,
|
|
continent,
|
|
continent.GUID(player.VehicleSeated) match {
|
|
case Some(vehicle : Vehicle) =>
|
|
SquadServiceAction.Update(player.CharId, vehicle.Health, vehicle.MaxHealth, vehicle.Shields, vehicle.MaxShields, vehicle.Position, continent.Number)
|
|
case Some(obj : PlanetSideGameObject with WeaponTurret) =>
|
|
SquadServiceAction.Update(player.CharId, obj.Health, obj.MaxHealth, 0, 0, obj.Position, continent.Number)
|
|
case _ =>
|
|
SquadServiceAction.Update(player.CharId, player.Health, player.MaxHealth, player.Armor, player.MaxArmor, player.Position, continent.Number)
|
|
}
|
|
)
|
|
}
|
|
|
|
def PeriodicUpdatesWhenEnrolledInSquad() : Unit = {
|
|
queuedSquadActions(squadUpdateCounter)()
|
|
squadUpdateCounter = (squadUpdateCounter + 1) % queuedSquadActions.length
|
|
}
|
|
|
|
def CapacitorTick(jump_thrust : Boolean): Unit = {
|
|
if(player.ExoSuit == ExoSuitType.MAX) {
|
|
//Discharge
|
|
if(jump_thrust || player.isOverdrived || player.isShielded) {
|
|
if(player.CapacitorState == CapacitorStateType.Discharging) {
|
|
// Previous tick was already discharging, calculate how much energy to drain from time between the two ticks
|
|
val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000
|
|
val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff
|
|
player.Capacitor -= drainAmount
|
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
|
|
} else {
|
|
// Start discharging
|
|
player.CapacitorState = CapacitorStateType.Discharging
|
|
}
|
|
}
|
|
// Charge
|
|
else if(player.Capacitor < player.ExoSuitDef.MaxCapacitor
|
|
&& (player.CapacitorState == CapacitorStateType.Idle || player.CapacitorState == CapacitorStateType.Charging || (player.CapacitorState == CapacitorStateType.ChargeDelay && System.currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis)))
|
|
{
|
|
if(player.CapacitorState == CapacitorStateType.Charging) {
|
|
val timeDiff = (System.currentTimeMillis() - player.CapacitorLastChargedMillis).toFloat / 1000
|
|
val chargeAmount = player.ExoSuitDef.CapacitorRechargePerSecond * timeDiff
|
|
player.Capacitor += chargeAmount
|
|
sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt))
|
|
} else {
|
|
player.CapacitorState = CapacitorStateType.Charging
|
|
}
|
|
}
|
|
|
|
if(player.Faction == PlanetSideEmpire.VS) {
|
|
// Start charge delay for VS when not boosting
|
|
if(!jump_thrust && player.CapacitorState == CapacitorStateType.Discharging ) {
|
|
player.CapacitorState = CapacitorStateType.ChargeDelay
|
|
}
|
|
}
|
|
else {
|
|
// Start charge delay for other factions if capacitor is empty or special ability is off
|
|
if(player.CapacitorState == CapacitorStateType.Discharging && (player.Capacitor == 0 || (!player.isOverdrived && !player.isShielded))) {
|
|
player.CapacitorState = CapacitorStateType.ChargeDelay
|
|
ToggleMaxSpecialState(enable = false)
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if(player.CapacitorState != CapacitorStateType.Idle) {
|
|
player.CapacitorState = CapacitorStateType.Idle
|
|
}
|
|
}
|
|
}
|
|
|
|
def ToggleMaxSpecialState(enable : Boolean): Unit = {
|
|
if(player.ExoSuit == ExoSuitType.MAX) {
|
|
if(enable) {
|
|
player.Faction match {
|
|
case PlanetSideEmpire.TR => if(player.Capacitor == player.ExoSuitDef.MaxCapacitor) player.UsingSpecial = SpecialExoSuitDefinition.Mode.Overdrive
|
|
case PlanetSideEmpire.NC => if(player.Capacitor > 0) player.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded
|
|
case _ => log.warn(s"Player ${player.Name} tried to use a MAX special ability but their faction doesn't have one")
|
|
}
|
|
if(player.UsingSpecial == SpecialExoSuitDefinition.Mode.Overdrive || player.UsingSpecial == SpecialExoSuitDefinition.Mode.Shielded) {
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 1))
|
|
}
|
|
}
|
|
else {
|
|
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 0))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The main purpose of this method is to determine which targets will receive "locked on" warnings from remote projectiles.
|
|
* For a given series of globally unique identifiers, indicating targets,
|
|
* and that may include mounted elements (players),
|
|
* estimate a series of channel names for communication with the vulnerable targets.
|
|
* @param targets the globally unique identifiers of the immediate detected targets
|
|
* @return channels names that allow direct communication to specific realized targets
|
|
*/
|
|
def FindDetectedProjectileTargets(targets : Iterable[PlanetSideGUID]) : Iterable[String] = {
|
|
targets
|
|
.map { ValidObject }
|
|
.flatMap {
|
|
case Some(obj : Vehicle) if !obj.Cloaked =>
|
|
//TODO hint: vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.ProjectileAutoLockAwareness(mode))
|
|
obj.Seats.values.collect { case seat if seat.isOccupied => seat.Occupant.get.Name }
|
|
case Some(obj : Mountable) =>
|
|
obj.Seats.values.collect { case seat if seat.isOccupied => seat.Occupant.get.Name }
|
|
case Some(obj : Player) if obj.ExoSuit == ExoSuitType.MAX =>
|
|
Seq(obj.Name)
|
|
case _ =>
|
|
Seq.empty[String]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a given registered remote projectile, perform all the actions necessary to properly dispose of it.
|
|
* Those actions involve:
|
|
* informing that the projectile should explode,
|
|
* unregistering the projectile's globally unique identifier,
|
|
* and managing the projectiles's local status information.
|
|
* @see `CleanUpRemoteProjectile(PlanetSideGUID, Projectile, Int)`
|
|
* @param projectile_guid the globally unique identifier of the projectile
|
|
* @param projectile the projectile
|
|
*/
|
|
def CleanUpRemoteProjectile(projectile_guid : PlanetSideGUID, projectile : Projectile) : Unit = {
|
|
projectiles.indexWhere({
|
|
case Some(p) => p eq projectile
|
|
case None => false
|
|
}) match {
|
|
case -1 => ; //required catch
|
|
case index if projectilesToCleanUp(index) =>
|
|
CleanUpRemoteProjectile(projectile_guid, projectile, index)
|
|
case _ => ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a given registered remote projectile, perform all the actions necessary to properly dispose of it.
|
|
* Those actions involve:
|
|
* informing that the projectile should explode,
|
|
* unregistering the projectile's globally unique identifier,
|
|
* and managing the projectiles's local status information.
|
|
* @param projectile_guid the globally unique identifier of the projectile
|
|
* @param projectile the projectile
|
|
* @param local_index an index of the absolute sequence of the projectile, for internal lists
|
|
*/
|
|
def CleanUpRemoteProjectile(projectile_guid : PlanetSideGUID, projectile : Projectile, local_index : Int) : Unit = {
|
|
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ProjectileExplodes(player.GUID, projectile_guid, projectile))
|
|
taskResolver ! UnregisterProjectile(projectile)
|
|
projectiles(local_index) match {
|
|
case Some(obj) if !obj.isResolved => obj.Miss
|
|
case _ => ;
|
|
}
|
|
projectilesToCleanUp(local_index) = false
|
|
}
|
|
|
|
def DeactivateImplants() : Unit = {
|
|
for(slot <- 0 to player.Implants.length - 1) {
|
|
player.Actor ! Player.ImplantActivation(slot, 0)
|
|
}
|
|
}
|
|
|
|
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 CreateCharacter(name : String, head : Int, voice : CharacterVoice.Value, gender : CharacterGender.Value, empire : PlanetSideEmpire.Value)
|
|
private final case class ListAccountCharacters()
|
|
private final case class SetCurrentAvatar(tplayer : Player)
|
|
private final case class VehicleLoaded(vehicle : Vehicle)
|
|
private final case class ZoningReset()
|
|
|
|
/**
|
|
* The message that progresses some form of user-driven activity with a certain eventual outcome
|
|
* and potential feedback per cycle.
|
|
* @param delta how much the progress value changes each tick, which will be treated as a percentage;
|
|
* must be a positive value
|
|
* @param completionAction a finalizing action performed once the progress reaches 100(%)
|
|
* @param tickAction an action that is performed for each increase of progress
|
|
*/
|
|
final case class ProgressEvent(delta : Float, completionAction : ()=>Unit, tickAction : Float=>Boolean)
|
|
|
|
private final val zoningCountdownMessages : Seq[Int] = Seq(5,10,20)
|
|
|
|
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)
|
|
|
|
private final case class LoadedRemoteProjectile(projectile_guid : PlanetSideGUID, projectile : Option[Projectile])
|
|
}
|