diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 4b689187..59f2a10f 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -53,6 +53,8 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet */ private var lfs : Boolean = false + private var vehicleOwned : Option[PlanetSideGUID] = None + def CharId : Long = char_id def BEP : Long = bep @@ -197,6 +199,15 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet LFS } + def VehicleOwned : Option[PlanetSideGUID] = vehicleOwned + + def VehicleOwned_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = VehicleOwned_=(Some(guid)) + + def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { + vehicleOwned = guid + VehicleOwned + } + def Definition : AvatarDefinition = GlobalDefinitions.avatar /* diff --git a/common/src/main/scala/net/psforever/objects/Deployables.scala b/common/src/main/scala/net/psforever/objects/Deployables.scala index 7ee7d6d6..273fb88d 100644 --- a/common/src/main/scala/net/psforever/objects/Deployables.scala +++ b/common/src/main/scala/net/psforever/objects/Deployables.scala @@ -1,15 +1,17 @@ // Copyright (c) 2017 PSForever package net.psforever.objects +import akka.actor.ActorRef +import scala.concurrent.duration._ + import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.zones.Zone import net.psforever.packet.game.{DeployableInfo, DeploymentAction} import net.psforever.types.PlanetSideGUID import services.RemoverActor import services.local.{LocalAction, LocalServiceMessage} -import scala.concurrent.duration.FiniteDuration - object Deployables { object Make { def apply(item : DeployedItem.Value) : ()=>PlanetSideGameObject with Deployable = cemap(item) @@ -72,4 +74,32 @@ object Deployables { zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(target), zone)) zone.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(target, zone, time)) } + + /** + * Collect all deployables previously owned by the player, + * dissociate the avatar's globally unique identifier to remove turnover ownership, + * and, on top of performing the above manipulations, dispose of any boomers discovered. + * (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation + * if they had not already been handled by the time this function is executed.) + * @return all previously-owned deployables after they have been processed; + * boomers are listed before all other deployable types + */ + def Disown(zone : Zone, avatar : Avatar, replyTo : ActorRef) : List[PlanetSideGameObject with Deployable] = { + val (boomers, deployables) = + avatar.Deployables.Clear() + .map(zone.GUID) + .collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] } + .partition(_.isInstanceOf[BoomerDeployable]) + //do not change the OwnerName field at this time + boomers.collect({ case obj : BoomerDeployable => + zone.LocalEvents.tell(LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone, Some(0 seconds))), replyTo) //near-instant + obj.Owner = None + obj.Trigger = None + }) + deployables.foreach(obj => { + zone.LocalEvents.tell(LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, zone)), replyTo) //normal decay + obj.Owner = None + }) + boomers ++ deployables + } } diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 17671a60..d00227f7 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -51,7 +51,6 @@ class Player(private val core : Avatar) extends PlanetSideServerObject private var cloaked : Boolean = false private var vehicleSeated : Option[PlanetSideGUID] = None - private var vehicleOwned : Option[PlanetSideGUID] = None Continent = "home2" //the zone id @@ -605,14 +604,11 @@ class Player(private val core : Avatar) extends PlanetSideServerObject VehicleSeated } - def VehicleOwned : Option[PlanetSideGUID] = vehicleOwned + def VehicleOwned : Option[PlanetSideGUID] = core.VehicleOwned - def VehicleOwned_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = VehicleOwned_=(Some(guid)) + def VehicleOwned_=(guid : PlanetSideGUID) : Option[PlanetSideGUID] = core.VehicleOwned_=(Some(guid)) - def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = { - vehicleOwned = guid - VehicleOwned - } + def VehicleOwned_=(guid : Option[PlanetSideGUID]) : Option[PlanetSideGUID] = core.VehicleOwned_=(guid) def DamageModel = exosuit.asInstanceOf[DamageResistanceModel] @@ -658,7 +654,6 @@ object Player { def Respawn(player : Player) : Player = { if(player.Release) { val obj = new Player(player.core) - obj.VehicleOwned = player.VehicleOwned obj.Continent = player.Continent obj } diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index 6b2adfb7..bd3a741e 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -102,6 +102,9 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner */ private var mountedIn : Option[PlanetSideGUID] = None + private var vehicleGatingManifest : Option[VehicleManifest] = None + private var previousVehicleGatingManifest : Option[VehicleManifest] = None + //init LoadDefinition() @@ -523,6 +526,23 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends AmenityOwner */ def TrunkLockState : VehicleLockState.Value = groupPermissions(3) + def PrepareGatingManifest() : VehicleManifest = { + val manifest = VehicleManifest(this) + seats.collect { case (index, seat) if index > 0 => seat.Occupant = None } + vehicleGatingManifest = Some(manifest) + previousVehicleGatingManifest = None + manifest + } + + def PublishGatingManifest() : Option[VehicleManifest] = { + val out = vehicleGatingManifest + previousVehicleGatingManifest = vehicleGatingManifest + vehicleGatingManifest = None + out + } + + def PreviousGatingManifest() : Option[VehicleManifest] = previousVehicleGatingManifest + def DamageModel = Definition.asInstanceOf[DamageResistanceModel] /** diff --git a/common/src/main/scala/net/psforever/objects/Vehicles.scala b/common/src/main/scala/net/psforever/objects/Vehicles.scala new file mode 100644 index 00000000..01635b13 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Vehicles.scala @@ -0,0 +1,154 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects + +import net.psforever.objects.vehicles.VehicleLockState +import net.psforever.objects.zones.Zone +import net.psforever.types.PlanetSideGUID +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.vehicle.{VehicleAction, VehicleServiceMessage} + +object Vehicles { + /** + * na + * @param vehicle na + * @param tplayer na + * @return na + */ + def Own(vehicle : Vehicle, tplayer : Player) : Option[Vehicle] = Own(vehicle, Some(tplayer)) + + /** + * na + * @param vehicle na + * @param playerOpt na + * @return na + */ + def Own(vehicle : Vehicle, playerOpt : Option[Player]) : Option[Vehicle] = { + playerOpt match { + case Some(tplayer) => + tplayer.VehicleOwned = vehicle.GUID + vehicle.AssignOwnership(playerOpt) + vehicle.Zone.VehicleEvents ! VehicleServiceMessage(vehicle.Zone.Id, VehicleAction.Ownership(tplayer.GUID, vehicle.GUID)) + Vehicles.ReloadAccessPermissions(vehicle, tplayer.Name) + Some(vehicle) + case None => + None + } + } + + /** + * Disassociate a player from a vehicle that he owns. + * The vehicle must exist in the game world on the specified continent. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is the player side of vehicle ownership removal. + * @param player the player + */ + def Disown(player : Player, zone : Zone) : Option[Vehicle] = Disown(player, Some(zone)) + /** + * Disassociate a player from a vehicle that he owns. + * The vehicle must exist in the game world on the specified continent. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is the player side of vehicle ownership removal. + * @param player the player + */ + def Disown(player : Player, zoneOpt : Option[Zone]) : Option[Vehicle] = { + player.VehicleOwned match { + case Some(vehicle_guid) => + player.VehicleOwned = None + zoneOpt.getOrElse(player.Zone).GUID(vehicle_guid) match { + case Some(vehicle : Vehicle) => + Disown(player, vehicle) + case _ => + None + } + case None => + None + } + } + + /** + * Disassociate a player from a vehicle that he owns without associating a different player as the owner. + * Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire," + * then reload them for all clients. + * This is the vehicle side of vehicle ownership removal. + * @param player the player + */ + def Disown(player : Player, vehicle : Vehicle) : Option[Vehicle] = { + val pguid = player.GUID + if(vehicle.Owner.contains(pguid)) { + vehicle.AssignOwnership(None) + val factionChannel = s"${vehicle.Faction}" + vehicle.Zone.VehicleEvents ! VehicleServiceMessage(factionChannel, VehicleAction.Ownership(pguid, PlanetSideGUID(0))) + val vguid = vehicle.GUID + val empire = VehicleLockState.Empire.id + (0 to 2).foreach(group => { + vehicle.PermissionGroup(group, empire) + vehicle.Zone.VehicleEvents ! VehicleServiceMessage(factionChannel, VehicleAction.SeatPermissions(pguid, vguid, group, empire)) + }) + ReloadAccessPermissions(vehicle, player.Name) + Some(vehicle) + } + else { + None + } + } + + /** + * Iterate over vehicle permissions and turn them into `PlanetsideAttributeMessage` packets.
+ *
+ * For the purposes of ensuring that other players are always aware of the proper permission state of the trunk and seats, + * packets are intentionally dispatched to the current client to update the states. + * Perform this action just after any instance where the client would initially gain awareness of the vehicle. + * The most important examples include either the player or the vehicle itself spawning in for the first time. + * @param vehicle the `Vehicle` + */ + def ReloadAccessPermissions(vehicle : Vehicle, toChannel : String) : Unit = { + val vehicle_guid = vehicle.GUID + (0 to 3).foreach(group => { + vehicle.Zone.AvatarEvents ! AvatarServiceMessage( + toChannel, + AvatarAction.PlanetsideAttributeToAll(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id) + ) + }) + } + + /** + * A recursive test that explores all the seats of a target vehicle + * and all the seats of any discovered cargo vehicles + * and then the same criteria in those cargo vehicles + * to determine if any of their combined passenger roster remains in a given zone.
+ *
+ * The original zone is expected to be defined in the internal vehicle gating manifest file + * and, if this file does not exist, we fail the testing process. + * The target zone is the one wherever the vehicle currently is located (`vehicle.Zone`). + * All participant passengers, also defined in the manifest, are expected to be in the target zone at the same time. + * This test excludes (rejects) same-zone transitions + * though it would automatically pass the test under those conditions.
+ *
+ * While it should be possible to recursively explore up a parent-child relationship - + * testing the ferrying vehicle to which the current tested vehicle is considered a cargo vehicle - + * the relationship expressed is one of globally unique refertences and not one of object references - + * that suggested super-ferrying vehicle may not exist in the zone unless special considerations are imposed. + * For the purpose of these special considerations, + * implemented by enforcing a strictly downwards order of vehicular zone transportation, + * where drivers move vehicles and call passengers and immediate cargo vehicle drivers, + * it becomes unnecessary to test any vehicle that might be ferrying the target vehicle. + * @see `ZoneAware` + * @param vehicle the target vehicle being moved around between zones + * @return `true`, if all passengers of the vehicle, and its cargo vehicles, etc., have reported being in the same zone; + * `false`, if no manifest entry exists, or if the vehicle is moving to the same zone + */ + def AllGatedOccupantsInSameZone(vehicle : Vehicle) : Boolean = { + val vzone = vehicle.Zone + vehicle.PreviousGatingManifest() match { + case Some(manifest) if vzone != manifest.origin => + val manifestPassengers = manifest.passengers.collect { case (name, _) => name } :+ manifest.driverName + val manifestPassengerResults = manifestPassengers.map { name => vzone.Players.exists(_.name.equals(name)) } + manifestPassengerResults.forall(_ == true) && + vehicle.CargoHolds.values + .collect { case hold if hold.isOccupied => AllGatedOccupantsInSameZone(hold.Occupant.get) } + .forall(_ == true) + case _ => + false + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 8dad02b9..cffa60df 100644 --- a/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/common/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -22,6 +22,9 @@ class PlayerControl(player : Player) extends Actor with JammableBehavior { def JammableObject = player + private [this] val log = org.log4s.getLogger(player.Name) + private [this] val damageLog = org.log4s.getLogger("DamageResolution") + def receive : Receive = jammableBehavior.orElse { case Player.Die() => PlayerControl.HandleDestructionAwareness(player, player.GUID, None) @@ -40,8 +43,7 @@ class PlayerControl(player : Player) extends Actor val damageToCapacitor = originalCapacitor - capacitor PlayerControl.HandleDamageResolution(player, cause, damageToHealth, damageToArmor, damageToCapacitor) if(damageToHealth != 0 || damageToArmor != 0 || damageToCapacitor != 0) { - org.log4s.getLogger("DamageResolution") - .info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor") + damageLog.info(s"${player.Name}-infantry: BEFORE=$originalHealth/$originalArmor/$originalCapacitor, AFTER=$health/$armor/$capacitor, CHANGE=$damageToHealth/$damageToArmor/$damageToCapacitor") } } case _ => ; diff --git a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala index 9693d704..7da956a7 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala @@ -183,7 +183,7 @@ class GridInventory extends Container { val starth : Int = starty + h - 1 if(actualSlot < 0 || actualSlot >= grid.length || startw >= width || starth >= height) { val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" } - Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h")) + Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx + $w, $starty + $h")) } else { val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]() @@ -220,22 +220,31 @@ class GridInventory extends Container { val starty : Int = actualSlot / width val startw : Int = startx + w - 1 val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" } - Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx, $starty; $w x $h")) + Failure(new IndexOutOfBoundsException(s"requested region escapes the $bounds edge of the grid inventory - $startx + $w, $starty + $h")) } else { val collisions : mutable.Set[InventoryItem] = mutable.Set[InventoryItem]() var curr = actualSlot val fixedItems = items.toMap val fixedGrid = grid.toList - for(_ <- 0 until h) { - for(col <- 0 until w) { - if(fixedGrid(curr + col) > -1) { - collisions += fixedItems(fixedGrid(curr + col)) + try { + for(_ <- 0 until h) { + for(col <- 0 until w) { + val itemIndex = fixedGrid(curr + col) + if(itemIndex > -1) { + collisions += fixedItems(itemIndex) + } } + curr += width } - curr += width + Success(collisions.toList) + } + catch { + case e : NoSuchElementException => + Failure(InventoryDisarrayException(s"inventory contained old item data", e)) + case e : Exception => + Failure(e) } - Success(collisions.toList) } } @@ -280,6 +289,7 @@ class GridInventory extends Container { /** * Define a region of inventory grid cells and set them to a given value. + * @see `SetCellsNoOffset` * @param start the initial inventory index * @param w the width of the region * @param h the height of the region @@ -287,77 +297,136 @@ class GridInventory extends Container { * defaults to -1 (which is "nothing") */ def SetCells(start : Int, w : Int, h : Int, value : Int = -1) : Unit = { - SetCellsOffset(math.max(start - offset, 0), w, h, value) + grid = SetCellsNoOffset(start - offset, w, h, value) } /** * Define a region of inventory grid cells and set them to a given value. + * Perform basic boundary checking for the current inventory dimensions. + * @see `SetCellsOnlyNoOffset` * @param start the initial inventory index, without the inventory offset (required) * @param w the width of the region * @param h the height of the region * @param value the value to set all the cells in the defined region; * defaults to -1 (which is "nothing") + * @return a copy of the inventory as a grid, with the anticipated modifications * @throws IndexOutOfBoundsException if the region extends outside of the grid boundaries */ - def SetCellsOffset(start : Int, w : Int, h : Int, value : Int = -1) : Unit = { + def SetCellsNoOffset(start : Int, w : Int, h : Int, value : Int = -1) : Array[Int] = { if(start < 0 || start > grid.length || (start % width) + w - 1 > width || (start / width) + h- 1 > height) { val startx : Int = start % width val starty : Int = start / width val startw : Int = startx + w - 1 val bounds : String = if(startx < 0) { "left" } else if(startw >= width) { "right" } else { "bottom" } - throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx, $starty; $w x $h") + throw new IndexOutOfBoundsException(s"requested region escapes the $bounds of the grid inventory - $startx + $w, $starty + $h") } - else { - var curr = start - for(_ <- 0 until h) { - for(col <- 0 until w) { - grid(curr + col) = value - } - curr += width + SetCellsOnlyNoOffset(start, w, h, value) + } + + /** + * Define a region of inventory grid cells and set them to a given value. + * Ignore inventory boundary checking and just set the appropriate cell values. + * Do not use this unless boundary checking was already performed. + * @param start the initial inventory index, without the inventory offset (required) + * @param w the width of the region + * @param h the height of the region + * @param value the value to set all the cells in the defined region; + * defaults to -1 (which is "nothing") + * @return a copy of the inventory as a grid, with the anticipated modifications + */ + private def SetCellsOnlyNoOffset(start : Int, w : Int, h : Int, value : Int = -1) : Array[Int] = { + val out : Array[Int] = grid.clone() + var curr = start + for(_ <- 0 until h) { + for(col <- 0 until w) { + out(curr + col) = value } + curr += width } + out } def Insert(start : Int, obj : Equipment) : Boolean = { val key : Int = entryIndex.getAndIncrement() items.get(key) match { - case None => //no redundant insertions or other collisions + case None => //no redundant insertions Insertion_CheckCollisions(start, obj, key) case _ => false } } + /** + * Perform a collisions check and, if it passes, perform the insertion. + * @param start the starting slot + * @param obj the `Equipment` item to be inserted + * @param key the internal numeric identifier for this item + * @return the success or the failure of the insertion process + */ def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = { CheckCollisions(start, obj) match { case Success(Nil) => - InsertQuickly(start, obj, key) + val tile = obj.Tile + grid = SetCellsOnlyNoOffset(start - offset, tile.Width, tile.Height, key) + val card = InventoryItem(obj, start) + items += key -> card + true case _ => false } } + /** + * Just insert an item into the inventory without checking for item collisions. + * @param start the starting slot + * @param obj the `Equipment` item to be inserted + * @return whether the insertion succeeded + */ def InsertQuickly(start : Int, obj : Equipment) : Boolean = InsertQuickly(start, obj, entryIndex.getAndIncrement()) + /** + * Just insert an item into the inventory without checking for item collisions. + * Inventory boundary checking still occurs but the `Exception` is caught and discarded. + * Discarding the `Exception` is normally bad practice; the result is the only thing we care about here. + * @param start the starting slot + * @param obj the `Equipment` item to be inserted + * @param key the internal numeric identifier for this item + * @return whether the insertion succeeded + */ private def InsertQuickly(start : Int, obj : Equipment, key : Int) : Boolean = { - val card = InventoryItem(obj, start) - items += key -> card - val tile = obj.Tile - SetCells(start, tile.Width, tile.Height, key) - true + try { + val tile = obj.Tile + val updated = SetCellsNoOffset(start - offset, tile.Width, tile.Height, key) + val card = InventoryItem(obj, start) + items += key -> card + grid = updated + true + } + catch { + case _ : Exception => + false + } } def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2) def Remove(index : Int) : Boolean = { - val key = grid(index - Offset) - items.remove(key) match { - case Some(item) => - val tile = item.obj.Tile - SetCells(item.start, tile.Width, tile.Height) - true - case None => - false + val keyVal = index - offset + if(keyVal > -1 && keyVal < grid.length) { + val key = grid(index - offset) + items.get(key) match { + case Some(item) => + val tile = item.obj.Tile + val updated = SetCellsNoOffset(item.start - offset, tile.Width, tile.Height) + items.remove(key) + grid = updated + true + case None => + false + } + } + else { + false } } @@ -365,10 +434,12 @@ class GridInventory extends Container { def Remove(guid : PlanetSideGUID) : Boolean = { recursiveFindIdentifiedObject(items.keys.iterator, guid) match { - case Some(index) => - val item = items.remove(index).get + case Some(key) => + val item = items(key) val tile = item.obj.Tile - SetCells(item.start, tile.Width, tile.Height) + val updated = SetCellsNoOffset(item.start - offset, tile.Width, tile.Height) + items.remove(key) + grid = updated true case None => false @@ -406,6 +477,99 @@ class GridInventory extends Container { } } + /** + * Align the "inventory as a grid" with the "inventory as a list." + * The grid is a faux-two-dimensional map of object identifiers that should point to items in the list. + * (Not the same as the global unique identifier number.) + * The objects in the list are considered actually being in the inventory. + * Only the references to those objects in grid-space can be considered out of alignment + * by not pointing to objects in the list. + * The inventory as a grid can be repaired but only a higher authority can perform inventory synchronization. + * @see `InventoryDisarrayException` + * @return the number of stale object references found and corrected + */ + def ElementsOnGridMatchList() : Int = { + var misses : Int = 0 + grid = grid.map { + case n if items.get(n).nonEmpty => n + case -1 => -1 + case _ => + misses += 1 + -1 + } + misses + } + + /** + * Check whether any items in the "inventory as a list" datastructure would overlap in the "inventory as a grid". + * Most likely, if an overlap is discovered, + * the grid-space is already compromised by having lost a section of some item's `Tile`. + * The inventory system actually lacks mechanics to properly resolve any discovered issues. + * For that reason, it will return a list of overlap issues that need to be resolved by a higher authority. + * @see `InventoryDisarrayException` + * @see `recursiveRelatedListCollisions` + * @return a list of item overlap collision combinations + */ + def ElementsInListCollideInGrid() : List[List[InventoryItem]] = { + val testGrid : mutable.Map[Int, List[Int]] = mutable.Map[Int, List[Int]]() + //on average this will run the same number of times as capacity + items.foreach { + case (itemId, item) => + var start = item.start + val wide = item.obj.Tile.Width + val high = item.obj.Tile.Height + //allocate all of the slots that comprise this item's tile + (0 until high).foreach { _ => + (0 until wide).foreach { w => + val slot = start + w + testGrid(slot) = testGrid.getOrElse(slot, Nil) :+ itemId + } + start += width + } + } + //lists with multiple entries represent a group of items that collide + //perform a specific distinct on the list of lists of numeric id overlaps + //THEN map each list of list's item id to an item + val out = testGrid.collect { + case (_, list) if list.size > 1 => list.sortWith(_ < _) + } + recursiveRelatedListCollisions(out.iterator, out.toList).map { list => list.map { items } } + } + + /** + * Filter out element overlap combinations until only unique overlap combinations remain. + * The filtration operation not only removes exact duplicates, leaving but one of an entry's kind, + * but also removes that very entry if another element contains the entry as a subset of itself. + * For example: + * if A overlaps B and B overlaps C, but A doesn't overlap C, the output will be A-B and B-C; + * if A and C do overlap, however, the output will be A-B-C, eliminating both A-B and B-C in the process. + * @see `ElementsInListCollideInGrid` + * @see `SeqLike.containsSlice` + * @see `tailrec` + * @param original an iterator of the original list of overlapping elements + * @param updated an list of overlapping elements not filtered out of the original list + * @return the final list of unique overlapping element combinations + */ + @tailrec private def recursiveRelatedListCollisions(original : Iterator[List[Int]], updated : List[List[Int]]) : List[List[Int]] = { + if(original.hasNext) { + val target = original.next + val filtered = updated.filterNot(item => item.equals(target)) + val newupdated = if(filtered.size == updated.size) { + updated //the lists are the same size, nothing was filtered + } + else if(updated.exists(test => test.containsSlice(target) && !test.equals(target))) { + filtered //some element that is not the target element contains the target element as a subset + } + else { + filtered :+ target //restore one entry for the target element + } + recursiveRelatedListCollisions(original, newupdated) + } + else { + updated + } + } + /** * Clear the inventory by removing all of its items. * @return a `List` of the previous items in the inventory as their `InventoryItemData` tiles @@ -413,7 +577,8 @@ class GridInventory extends Container { def Clear() : List[InventoryItem] = { val list = items.values.toList items.clear - SetCellsOffset(0, width, height) + //entryIndex.set(0) + grid = SetCellsOnlyNoOffset(0, width, height) list } diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryDisarrayException.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryDisarrayException.scala new file mode 100644 index 00000000..d3280207 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryDisarrayException.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.inventory + +/** + * Some data in the grid portion of a `GridInventory` + * does not match against data that is expected to be found in the "list" portion of `GridInventory`. + * While merely eliminating the old data is possible, + * the discovery of this errant data could be hiding significantly greater issues, + * and these greater issues must be explored at a higher level of governance. + * @param message the explanation of why the exception was thrown + * @param cause any prior `Exception` that was thrown then wrapped in this one + */ +final case class InventoryDisarrayException(private val message: String = "", private val cause: Throwable) + extends Exception(message, cause) + +object InventoryDisarrayException { + /** + * Overloaded constructor that constructs the `Exception` without nesting any prior `Exceptions`. + * Just the custom error message is included. + * @param message the explanation of why the exception was thrown + * @return an `InventoryDisarrayException` object + */ + def apply(message : String) : InventoryDisarrayException = + InventoryDisarrayException(message, None.orNull) +} diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala index f387eb73..e7096a11 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala @@ -35,7 +35,7 @@ class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInven val tile = equip.Definition.Tile inv.CheckCollisionsVar(slot, tile.Width, tile.Height) match { case Success(Nil) => inv.InsertQuickly(slot, equip) - case _ => ; + case _ => ; //TODO we should handle the exception } case None => diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala index e6b6ea6f..7a9ca07d 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/Amenity.scala @@ -31,7 +31,7 @@ abstract class Amenity extends PlanetSideServerObject with ZoneAware { */ def Owner : AmenityOwner = { if(owner == Building.NoBuilding) { - log.warn(s"Amenity $GUID in zone $Zone tried to access owner, but doesn't have one.") + log.warn(s"Amenity $GUID in zone ${Zone.Id} tried to access owner, but doesn't have one.") } owner } diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala new file mode 100644 index 00000000..92d9b989 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleManifest.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2020 PSForever +package net.psforever.objects.vehicles + +import net.psforever.objects.Vehicle +import net.psforever.objects.zones.Zone + +/** + * na + * @param file the id of this manifest entry; + * used as the channel name for summoning passengers to the vehicle + * after it has been loaded to a new location or to a new zone; + * this channel name should be unique to the vehicle for at least the duration of the transition; + * the vehicle-specific channel with which all passengers are coordinated back to the original vehicle + * @param vehicle na + * @param origin na + * @param driverName na + * @param passengers na + * @param cargo na + */ +/** + * The channel name for summoning passengers to the vehicle + * after it has been loaded to a new location or to a new zone. + * This channel name should be unique to the vehicle for at least the duration of the transition. + * The vehicle-specific channel with which all passengers are coordinated back to the original vehicle. + * @param vehicle the vehicle being moved (or having been moved) + * @return the channel name + */ +final case class VehicleManifest(file : String, + vehicle : Vehicle, + origin : Zone, + driverName : String, + passengers : List[(String, Int)], + cargo : List[(String, Int)]) + +object VehicleManifest { + def apply(vehicle : Vehicle) : VehicleManifest = { + val driverName = vehicle.Seats(0).Occupant match { + case Some(driver) => driver.Name + case None => "MISSING_DRIVER" + } + val passengers = vehicle.Seats.collect { case (index, seat) if index > 0 && seat.isOccupied => + (seat.Occupant.get.Name, index) + } + val cargo = vehicle.CargoHolds.collect { case (index, hold) if hold.Occupant.nonEmpty => + hold.Occupant.get.Seats(0).Occupant match { + case Some(driver) => + (driver.Name, index) + case None => + ("MISSING_DRIVER", index) + } + } + VehicleManifest(ManifestChannelName(vehicle), vehicle, vehicle.Zone, driverName, passengers.toList, cargo.toList) + } + + def ManifestChannelName(vehicle : Vehicle) : String = { + s"transport-vehicle-channel-${vehicle.GUID.guid}" + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala index 1b5364c8..1f7bdf0a 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala @@ -30,7 +30,9 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player case Zone.Population.Leave(avatar) => PopulationLeave(avatar, playerMap) match { case None => ; - case player @ Some(_) => + case player @ Some(tplayer) => + tplayer.Zone = Zone.Nowhere + PlayerLeave(tplayer) sender ! Zone.Population.PlayerHasLeft(zone, player) if(playerMap.isEmpty) { zone.StopPlayerManagementSystems() @@ -45,7 +47,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player sender ! Zone.Population.PlayerAlreadySpawned(zone, player) } else if(newToZone) { - player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"${player.Name}_${player.GUID.guid}_${System.currentTimeMillis}") + player.Actor = context.actorOf(Props(classOf[PlayerControl], player), s"${player.CharId}_${player.GUID.guid}_${System.currentTimeMillis}") player.Zone = zone } case None => @@ -55,8 +57,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player case Zone.Population.Release(avatar) => PopulationRelease(avatar, playerMap) match { case Some(tplayer) => - tplayer.Actor ! akka.actor.PoisonPill - tplayer.Actor = ActorRef.noSender + PlayerLeave(tplayer) case None => sender ! Zone.Population.PlayerHasLeft(zone, None) } @@ -175,6 +176,11 @@ object ZonePopulationActor { } } + def PlayerLeave(player : Player) : Unit = { + player.Actor ! akka.actor.PoisonPill + player.Actor = ActorRef.noSender + } + /** * A recursive function that finds and removes a specific player from a list of players. * @param iter an `Iterator` of `Player` objects diff --git a/common/src/main/scala/services/account/AccountPersistenceService.scala b/common/src/main/scala/services/account/AccountPersistenceService.scala new file mode 100644 index 00000000..32c2c3d2 --- /dev/null +++ b/common/src/main/scala/services/account/AccountPersistenceService.scala @@ -0,0 +1,383 @@ +// Copyright (c) 2020 PSForever +package services.account + +import akka.actor.{Actor, ActorRef, Cancellable, Props} + +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import net.psforever.objects.guid.GUIDTask +import net.psforever.objects._ +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.zones.Zone +import net.psforever.types.Vector3 +import services.{RemoverActor, Service, ServiceManager} +import services.avatar.{AvatarAction, AvatarServiceMessage} +import services.vehicle.VehicleServiceMessage + +/** + * A global service that manages user behavior as divided into the following three categories: + * persistence (ongoing participation in the game world), + * relogging (short-term client connectivity issue resolution), and + * logout (end-of-life conditions involving the separation of a user from the game world).
+ *
+ * A user polls this service and the services either creates a new `PersistenceMonitor` entity + * or returns whatever `PersistenceMonitor` entity currently exists. + * Performing informative pdates to the monitor about the user's eventual player avatar instance + * (which can be performed by messaging the service indirectly, + * though sending directly to the monitor is recommended) + * facilitate the management of persistence. + * If connectivity isssues with the client are encountered by the user, + * within a reasonable amount of time to connection restoration, + * the user may regain control of their existing persistence monitor and, thus, the same player avatar. + * End of life is mainly managed by the monitors internally + * and the monitors only communicate up to this service when executing their "end-of-life" operations. + */ +class AccountPersistenceService extends Actor { + /** an association of user text descriptors - player names - and their current monitor indices
+ * key - player name, value - monitor index + */ + var userIndices : mutable.Map[String, Int] = mutable.Map[String, Int]() + /** + * an association of user test descriptors - player names - and their current monitor
+ * key - player name, value - player monitor + */ + val accounts : mutable.Map[String, ActorRef] = mutable.Map[String, ActorRef]() + /** squad service event hook */ + var squad : ActorRef = ActorRef.noSender + /** task resolver service event hook */ + var resolver : ActorRef = ActorRef.noSender + /** log, for trace and warnings only */ + val log = org.log4s.getLogger + + /** + * Retrieve the required system event service hooks. + * @see `ServiceManager.LookupResult` + */ + override def preStart : Unit = { + ServiceManager.serviceManager ! ServiceManager.Lookup("squad") + ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver") + log.trace("Awaiting system service hooks ...") + } + + override def postStop : Unit = { + accounts.foreach { case (_, monitor) => context.stop(monitor) } + accounts.clear + } + + def receive : Receive = Setup + + /** + * Entry point for persistence monitoring setup. + * Primarily intended to deal with the initial condition of verifying/assuring of an enqueued persistence monitor. + * Updates to persistence can be received and will be distributed, if possible; + * but, updating should be reserved for individual persistence monitor callback (by the user who is being monitored). + */ + val Started : Receive = { + case msg @ AccountPersistenceService.Login(name) => + (accounts.get(name) match { + case Some(ref) => ref + case None => CreateNewPlayerToken(name) + }).tell(msg, sender) + + case msg @ AccountPersistenceService.Update(name, _, _) => + accounts.get(name) match { + case Some(ref) => + ref ! msg + case None => + log.warn(s"tried to update a player entry ($name) that did not yet exist; rebuilding entry ...") + CreateNewPlayerToken(name).tell(msg, sender) + } + + case Logout(target) => //TODO use context.watch and Terminated? + accounts.remove(target) + + case _ => ; + } + + /** + * Process the system event service hooks when they arrive, before starting proper persistence monitoring. + * @see `ServiceManager.LookupResult` + */ + val Setup : Receive = { + case ServiceManager.LookupResult(id, endpoint) => + id match { + case "squad" => + squad = endpoint + case "taskResolver" => + resolver = endpoint + } + if(squad != ActorRef.noSender && + resolver != ActorRef.noSender) { + log.trace("Service hooks obtained. Continuing with standard operation.") + context.become(Started) + } + + case msg => + log.warn(s"Not yet started; received a $msg that will go unhandled") + } + + /** + * Enqueue a new persistency monitor object for this player. + * @param name the unique name of the player + * @return the persistence monitor object + */ + def CreateNewPlayerToken(name : String) : ActorRef = { + val ref = context.actorOf(Props(classOf[PersistenceMonitor], name, squad, resolver), s"$name-${NextPlayerIndex(name)}") + accounts += name -> ref + ref + } + + /** + * Get the next account unique login index. + * The index suggests the number of times the player has logged into the game. + * The main purpose is to give each player a meaninfgul ordinal number of logging agencies + * whose names did not interfere with each other (`Actor` name uniqueness). + * @param name the text personal descriptor used by the player + * @return the next index for this player, starting at 0 + */ + def NextPlayerIndex(name : String) : Int = { + userIndices.get(name) match { + case Some(n) => + val p = n + 1 + userIndices += name -> p + p + case None => + userIndices += name -> 0 + 0 + } + } +} + +object AccountPersistenceService { + /** + * Message to begin persistence monitoring of user with this text descriptor (player name). + * If the persistence monitor already exists, use that instead and synchronize the data. + * @param name the unique name of the player + */ + final case class Login(name : String) + + /** + * Update the persistence monitor that was setup for a user with the given text descriptor (player name). + * The player's name should be able to satisfy the condition:
+ * `zone.LivePlayers.exists(p => p.Name.equals(name))`
+ * @param name the unique name of the player + * @param zone the current zone the player is in + * @param position the location of the player in game world coordinates + */ + final case class Update(name : String, zone : Zone, position : Vector3) +} + +/** + * Observe and manage the persistence of a single named player avatar entity in the game world, + * with special care to the conditions of short interruption in connectivity (relogging) + * and end-of-life operations. + * Upon login, the monitor will echo all of the current information about the user's (recent) login back to the `sender`. + * With a zone and a coordinate position in that zone, a user's player avatar can be properly reconnected + * or can be reconstructed. + * Without actual recent activity, + * the default information about the zone is an indication that the user must start this player avatar from scratch. + * The monitor expects a reliable update messaging (heartbeat) to keep track of the important information + * and to determine the conditions for end-of-life activity. + * @param name the unique name of the player + * @param squadService a hook into the `SquadService` event system + * @param taskResolver a hook into the `TaskResolver` event system; + * used for object unregistering + */ +class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver : ActorRef) extends Actor { + /** the last-reported zone of this player */ + var inZone : Zone = Zone.Nowhere + /** the last-reported game coordinate position of this player */ + var lastPosition : Vector3 = Vector3.Zero + /** the ongoing amount of permissible inactivity */ + var timer : Cancellable = DefaultCancellable.obj + /** the sparingly-used log */ + val log = org.log4s.getLogger + + /** + * Perform logout operations before the persistence monitor finally stops. + */ + override def postStop() : Unit = { + timer.cancel + PerformLogout() + } + + def receive : Receive = { + case AccountPersistenceService.Login(_) => + sender ! PlayerToken.LoginInfo(name, inZone, lastPosition) + UpdateTimer() + + case AccountPersistenceService.Update(_, z, p) => + inZone = z + lastPosition = p + UpdateTimer() + + case Logout(_) => + context.parent ! Logout(name) + context.stop(self) + + case _ => ; + } + + /** + * Restart the minimum activity timer. + */ + def UpdateTimer() : Unit = { + timer.cancel + timer = context.system.scheduler.scheduleOnce(60 seconds, self, Logout(name)) + } + + /** + * When the sustenance updates of the persistence monitor come to an end, + * and the persistence monitor itself is about to clean itself up, + * the player and avatar combination that has been associated with it will also undergo independent end of life activity. + * This is the true purpose of the persistence object - to perform a proper logout.
+ *
+ * The updates have been providing the zone + * and the basic information about the user (player name) has been provided since the beginning + * and it's a trivial matter to find where the avatar and player and asess their circumstances. + * The four important vectors are: + * the player avatar is in a vehicle, + * the player avatar is standing, + * only the avatar exists and the player released, + * and neither the avatar nor the player exist. + * It does not matter whether the player, if encountered, is alive or dead, + * only if they have been rendered a corpse and did not respawn. + * The fourth condition is not technically a failure condition, + * and can arise during normal transitional gameplay, + * but should be uncommon. + */ + def PerformLogout() : Unit = { + log.info(s"logout of $name") + (inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match { + case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty => + //alive or dead in a vehicle + //if the avatar is dead while in a vehicle, they haven't released yet + //TODO perform any last minute saving now ... + (inZone.GUID(player.VehicleSeated) match { + case Some(obj : Mountable) => + (Some(obj), obj.Seat(obj.PassengerInSeat(player).getOrElse(-1))) + case _ => (None, None) //bad data? + }) match { + case (Some(_), Some(seat)) => + seat.Occupant = None //unseat + case _ => ; + } + PlayerAvatarLogout(avatar, player) + + case (Some(avatar), Some(player)) => + //alive or dead, as standard Infantry + //TODO perform any last minute saving now ... + PlayerAvatarLogout(avatar, player) + + case (Some(avatar), None) => + //player has released + //our last body was turned into a corpse; just the avatar remains + //TODO perform any last minute saving now ... + AvatarLogout(avatar) + inZone.GUID(avatar.VehicleOwned) match { + case Some(obj : Vehicle) if obj.OwnerName.contains(avatar.name) => + obj.AssignOwnership(None) + case _ => ; + } + taskResolver.tell(GUIDTask.UnregisterLocker(avatar.Locker)(inZone.GUID), context.parent) + + case _ => + //user stalled during initial session, or was caught in between zone transfer + } + } + + /** + * A common set of actions to perform in the course of logging out a player avatar. + * Of the four scenarios described - in transport, on foot, released, missing - two of them utilize these operations. + * One of the other two uses a modified version of some of these activities to facilitate its log out. + * As this persistence monitor is about to become invalid, + * any messages sent in response to what we are sending are received by the monitor's parent. + * @see `Avatar` + * @see `AvatarAction.ObjectDelete` + * @see `AvatarServiceMessage` + * @see `DisownVehicle` + * @see `GUIDTask.UnregisterAvatar` + * @see `Player` + * @see `Zone.AvatarEvents` + * @see `Zone.Population.Release` + * @param avatar the avatar + * @param player the player + */ + def PlayerAvatarLogout(avatar : Avatar, player : Player) : Unit = { + val pguid = player.GUID + val parent = context.parent + player.Position = Vector3.Zero + player.Health = 0 + DisownVehicle(player) + inZone.Population.tell(Zone.Population.Release(avatar), parent) + inZone.AvatarEvents.tell(AvatarServiceMessage(inZone.Id, AvatarAction.ObjectDelete(pguid, pguid)), parent) + AvatarLogout(avatar) + taskResolver.tell(GUIDTask.UnregisterAvatar(player)(inZone.GUID), parent) + } + + /** + * A common set of actions to perform in the course of logging out an avatar. + * Of the four scenarios described - in transport, on foot, released, missing - three of them utilize these operations. + * The avatar will virtually always be in an existential position, one that needs to be handled at logout + * @see `Avatar` + * @see `Deployables.Disown` + * @see `Service.Leave` + * @see `Zone.Population.Leave` + * @param avatar the avatar + */ + def AvatarLogout(avatar : Avatar) : Unit = { + val parent = context.parent + val charId = avatar.CharId + LivePlayerList.Remove(charId) + squadService.tell(Service.Leave(Some(charId.toString)), parent) + Deployables.Disown(inZone, avatar, parent) + inZone.Population.tell(Zone.Population.Leave(avatar), parent) + } + + /** + * Vehicle cleanup that is specific to log out behavior. + * @see `Vehicles.Disown` + * @see `RemoverActor.AddTask` + * @see `RemoverActor.ClearSpecific` + * @see `Vehicle.Flying` + * @see `VehicleDefinition.DeconstructionTime` + * @see `VehicleServiceMessage.Decon` + * @see `Zone.VehicleEvents` + */ + def DisownVehicle(player : Player) : Unit = { + Vehicles.Disown(player, inZone) match { + case Some(vehicle) if vehicle.Health == 0 || (vehicle.Seats.values.forall(seat => !seat.isOccupied) && vehicle.Owner.isEmpty) => + inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), inZone)) + inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, inZone, + if(vehicle.Flying) { + //TODO gravity + Some(0 seconds) //immediate deconstruction + } + else { + vehicle.Definition.DeconstructionTime //normal deconstruction + } + )) + case _ => ; + } + } +} + +/** + * Internal message that flags that the player has surpassed the maximum amount of inactivity allowable + * and should stop existing. + * @param name the unique name of the player + */ +private[this] case class Logout(name : String) + +object PlayerToken { + /** + * Message dispatched to confirm that a player with given locational attributes exists. + * Agencies outside of the `AccountPersistanceService`/`PlayerToken` system make use of this message. + * ("Exists" does not imply an ongoing process and can also mean "just joined the game" here.) + * @param name the name of the player + * @param zone the zone in which the player is location + * @param position where in the zone the player is located + */ + final case class LoginInfo(name : String, zone : Zone, position : Vector3) +} diff --git a/common/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala index 9799d8c7..0a338ad0 100644 --- a/common/src/main/scala/services/avatar/AvatarService.scala +++ b/common/src/main/scala/services/avatar/AvatarService.scala @@ -229,6 +229,11 @@ class AvatarService(zone : Zone) extends Actor { AvatarServiceResponse(s"/$forChannel/Avatar", target_guid, AvatarResponse.Revive(target_guid)) ) + case AvatarAction.TeardownConnection() => + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", Service.defaultPlayerGUID, AvatarResponse.TeardownConnection()) + ) + case _ => ; } diff --git a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala index f6156a25..67188d97 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -62,6 +62,7 @@ object AvatarAction { final case class SendResponse(player_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action final case class SendResponseTargeted(target_guid: PlanetSideGUID, msg: PlanetSideGamePacket) extends Action + final case class TeardownConnection() extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action } diff --git a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala index 84fd846a..21519842 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -54,5 +54,7 @@ object AvatarResponse { final case class SendResponse(msg: PlanetSideGamePacket) extends Response final case class SendResponseTargeted(target_guid : PlanetSideGUID, msg: PlanetSideGamePacket) extends Response + + final case class TeardownConnection() extends Response // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response } diff --git a/common/src/main/scala/services/galaxy/GalaxyService.scala b/common/src/main/scala/services/galaxy/GalaxyService.scala index 8b1501bb..3ba548b0 100644 --- a/common/src/main/scala/services/galaxy/GalaxyService.scala +++ b/common/src/main/scala/services/galaxy/GalaxyService.scala @@ -53,9 +53,9 @@ class GalaxyService extends Actor { GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg)) ) - case GalaxyAction.TransferPassenger(player_guid, temp_channel, vehicle, vehicle_to_delete) => + case GalaxyAction.TransferPassenger(player_guid, temp_channel, vehicle, vehicle_to_delete, manifest) => GalaxyEvents.publish( - GalaxyServiceResponse(s"/$forChannel/Galaxy", GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete)) + GalaxyServiceResponse(s"/$forChannel/Galaxy", GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete, manifest)) ) case _ => ; } diff --git a/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala b/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala index e6b6bd70..60364cd5 100644 --- a/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala +++ b/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala @@ -2,6 +2,7 @@ package services.galaxy import net.psforever.objects.Vehicle +import net.psforever.objects.vehicles.VehicleManifest import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.types.PlanetSideGUID @@ -16,5 +17,5 @@ object GalaxyAction { final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Action - final case class TransferPassenger(player_guid : PlanetSideGUID, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Action + final case class TransferPassenger(player_guid : PlanetSideGUID, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID, manifest : VehicleManifest) extends Action } diff --git a/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala b/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala index 824c3ca8..eb445ede 100644 --- a/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala +++ b/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala @@ -2,6 +2,7 @@ package services.galaxy import net.psforever.objects.Vehicle +import net.psforever.objects.vehicles.VehicleManifest import net.psforever.objects.zones.HotSpotInfo import net.psforever.packet.game.BuildingInfoUpdateMessage import net.psforever.types.PlanetSideGUID @@ -17,5 +18,5 @@ object GalaxyResponse { final case class HotSpotUpdate(zone_id : Int, priority : Int, host_spot_info : List[HotSpotInfo]) extends Response final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Response - final case class TransferPassenger(temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Response + final case class TransferPassenger(temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID, manifest : VehicleManifest) extends Response } diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index ad717e59..e4919d83 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -376,7 +376,23 @@ class SquadService extends Actor { e.printStackTrace() } - case Service.Leave(Some(char_id)) => LeaveService(char_id, sender) + case Service.Leave(Some(faction)) if "TRNCVS".indexOf(faction) > -1 => + val path = s"/$faction/Squad" + val who = sender() + debug(s"$who has left $path") + SquadEvents.unsubscribe(who, path) + + case Service.Leave(Some(char_id)) => + try { + LeaveService(char_id.toLong, sender) + } + catch { + case _ : ClassCastException => + log.warn(s"Service.Leave: tried $char_id as a unique character identifier, but it could not be casted") + case e : Exception => + log.error(s"Service.Leave: unexpected exception using $char_id as data - ${e.getLocalizedMessage}") + e.printStackTrace() + } case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription.path.equals(sender.path)} match { case Some((to, _)) => diff --git a/common/src/main/scala/services/vehicle/VehicleService.scala b/common/src/main/scala/services/vehicle/VehicleService.scala index e4a220fb..bad2bb0b 100644 --- a/common/src/main/scala/services/vehicle/VehicleService.scala +++ b/common/src/main/scala/services/vehicle/VehicleService.scala @@ -133,9 +133,9 @@ class VehicleService(zone : Zone) extends Actor { case VehicleAction.UpdateAmsSpawnPoint(zone : Zone) => sender ! VehicleServiceResponse(s"/$forChannel/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UpdateAmsSpawnPoint(AmsSpawnPoints(zone))) - case VehicleAction.TransferPassengerChannel(player_guid, old_channel, temp_channel, vehicle) => + case VehicleAction.TransferPassengerChannel(player_guid, old_channel, temp_channel, vehicle, vehicle_to_delete) => VehicleEvents.publish( - VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle)) + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle, vehicle_to_delete)) ) case VehicleAction.ForceDismountVehicleCargo(player_guid, vehicle_guid, bailed, requestedByPassenger, kicked) => diff --git a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala index cd8d89dc..931ce8d8 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -44,7 +44,7 @@ object VehicleAction { final case class SendResponse(player_guid: PlanetSideGUID, msg : PlanetSideGamePacket) extends Action final case class UpdateAmsSpawnPoint(zone : Zone) extends Action - final case class TransferPassengerChannel(player_guid : PlanetSideGUID, temp_channel : String, new_channel : String, vehicle : Vehicle) extends Action + final case class TransferPassengerChannel(player_guid : PlanetSideGUID, temp_channel : String, new_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Action final case class ForceDismountVehicleCargo(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Action final case class KickCargo(player_guid : PlanetSideGUID, cargo : Vehicle, speed : Int, delay : Long) extends Action diff --git a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala index 515d0091..8075e8c8 100644 --- a/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala @@ -50,7 +50,7 @@ object VehicleResponse { final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response final case class PeriodicReminder(reason : Reminders.Value, data : Option[Any] = None) extends Response - final case class TransferPassengerChannel(old_channel : String, temp_channel : String, vehicle : Vehicle) extends Response + final case class TransferPassengerChannel(old_channel : String, temp_channel : String, vehicle : Vehicle, vehicle_to_delete : PlanetSideGUID) extends Response final case class ForceDismountVehicleCargo(vehicle_guid : PlanetSideGUID, bailed : Boolean, requestedByPassenger : Boolean, kicked : Boolean) extends Response final case class KickCargo(cargo : Vehicle, speed : Int, delay : Long) extends Response diff --git a/common/src/test/scala/objects/InventoryTest.scala b/common/src/test/scala/objects/InventoryTest.scala index 48b86bd3..828f5a78 100644 --- a/common/src/test/scala/objects/InventoryTest.scala +++ b/common/src/test/scala/objects/InventoryTest.scala @@ -1,10 +1,10 @@ // Copyright (c) 2017 PSForever package objects -import net.psforever.objects.{AmmoBox, SimpleItem} +import net.psforever.objects.{AmmoBox, SimpleItem, Tool} import net.psforever.objects.definition.SimpleItemDefinition -import net.psforever.objects.inventory.{GridInventory, InventoryItem, InventoryTile} -import net.psforever.objects.GlobalDefinitions._ +import net.psforever.objects.inventory.{GridInventory, InventoryDisarrayException, InventoryItem, InventoryTile} +import net.psforever.objects.GlobalDefinitions.{bullet_9mm, suppressor} import net.psforever.types.PlanetSideGUID import org.specs2.mutable._ @@ -19,6 +19,18 @@ class InventoryTest extends Specification { bullet9mmBox2 = AmmoBox(bullet_9mm) bullet9mmBox2.GUID = PlanetSideGUID(2) + "InventoryDisarrayException" should { + "construct" in { + InventoryDisarrayException("slot out of bounds") + ok + } + + "construct (with Throwable)" in { + InventoryDisarrayException("slot out of bounds", new Throwable()) + ok + } + } + "GridInventory" should { "construct" in { val obj : GridInventory = GridInventory() @@ -33,28 +45,28 @@ class InventoryTest extends Specification { obj.Size mustEqual 0 } - "insert item" in { - val obj : GridInventory = GridInventory(9, 6) - obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) - obj += 2 -> bullet9mmBox1 - obj.TotalCapacity mustEqual 54 - obj.Capacity mustEqual 45 - obj.Size mustEqual 1 - obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) - obj.Clear() - obj.Size mustEqual 0 - } - "check for collision with inventory border" in { val obj : GridInventory = GridInventory(3, 3) //safe obj.CheckCollisionsAsList(0, 3, 3) mustEqual Success(Nil) //right - obj.CheckCollisionsAsList(-1, 3, 3).isFailure mustEqual true + obj.CheckCollisionsAsList(-1, 3, 3) match { + case scala.util.Failure(fail) => + fail.isInstanceOf[IndexOutOfBoundsException] mustEqual true + case _ => ko + } //left - obj.CheckCollisionsAsList(1, 3, 3).isFailure mustEqual true + obj.CheckCollisionsAsList(1, 3, 3) match { + case scala.util.Failure(fail) => + fail.isInstanceOf[IndexOutOfBoundsException] mustEqual true + case _ => ko + } //bottom - obj.CheckCollisionsAsList(3, 3, 3).isFailure mustEqual true + obj.CheckCollisionsAsList(3, 3, 3) match { + case scala.util.Failure(fail) => + fail.isInstanceOf[IndexOutOfBoundsException] mustEqual true + case _ => ko + } } "check for item collision (right insert)" in { @@ -64,7 +76,7 @@ class InventoryTest extends Specification { val w = bullet9mmBox2.Tile.Width val h = bullet9mmBox2.Tile.Height val list0 = obj.CheckCollisionsAsList(0, w, h) - list0 match { + obj.CheckCollisionsAsList(0, w, h) match { case scala.util.Success(list) => list.length mustEqual 1 case scala.util.Failure(_) => ko } @@ -263,35 +275,120 @@ class InventoryTest extends Specification { ok } + "insert item" in { + val obj : GridInventory = GridInventory(9, 6) + obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) + obj += 2 -> bullet9mmBox1 + obj.TotalCapacity mustEqual 54 + obj.Capacity mustEqual 45 + obj.Size mustEqual 1 + obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true + obj.Clear() + obj.Size mustEqual 0 + } + + "not insert into an invalid slot (n < 0)" in { + val obj : GridInventory = GridInventory(9, 6) + obj.Capacity mustEqual 54 + obj.Size mustEqual 0 + obj.Insert(-1, bullet9mmBox1) must throwA[IndexOutOfBoundsException] + obj.Capacity mustEqual 54 + obj.Size mustEqual 0 + } + + "not insert into an invalid slot (n > capacity)" in { + val obj : GridInventory = GridInventory(9, 6) + obj.Capacity mustEqual 54 + obj.Size mustEqual 0 + obj.Insert(55, bullet9mmBox1) must throwA[IndexOutOfBoundsException] + obj.Capacity mustEqual 54 + obj.Size mustEqual 0 + } + "block insertion if item collision" in { val obj : GridInventory = GridInventory(9, 6) obj += 0 -> bullet9mmBox1 obj.Capacity mustEqual 45 - obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true obj += 2 -> bullet9mmBox2 - obj.hasItem(PlanetSideGUID(2)) mustEqual None + obj.hasItem(PlanetSideGUID(2)).isEmpty mustEqual true + } + + "insert items quickly (risk overwriting entries)" in { + val obj : GridInventory = GridInventory(6, 6) + (obj += 0 -> bullet9mmBox1) mustEqual true + val collision1 = obj.CheckCollisions(0,1,1) + obj.CheckCollisions(1,1,1) mustEqual collision1 + obj.CheckCollisions(2,1,1) mustEqual collision1 + obj.CheckCollisions(6,1,1) mustEqual collision1 + obj.CheckCollisions(7,1,1) mustEqual collision1 + obj.CheckCollisions(8,1,1) mustEqual collision1 + obj.CheckCollisions(12,1,1) mustEqual collision1 + obj.CheckCollisions(13,1,1) mustEqual collision1 + obj.CheckCollisions(14,1,1) mustEqual collision1 + + (obj += 7 -> bullet9mmBox2) mustEqual false //can not insert overlapping object + obj.CheckCollisions(0,1,1) mustEqual collision1 + obj.CheckCollisions(1,1,1) mustEqual collision1 + obj.CheckCollisions(2,1,1) mustEqual collision1 + obj.CheckCollisions(6,1,1) mustEqual collision1 + obj.CheckCollisions(7,1,1) mustEqual collision1 + obj.CheckCollisions(8,1,1) mustEqual collision1 + obj.CheckCollisions(12,1,1) mustEqual collision1 + obj.CheckCollisions(13,1,1) mustEqual collision1 + obj.CheckCollisions(14,1,1) mustEqual collision1 + + obj.InsertQuickly(7, bullet9mmBox2) mustEqual true //overwrite + val collision2 = obj.CheckCollisions(7,1,1) + obj.CheckCollisions(0,1,1) mustEqual collision1 + obj.CheckCollisions(1,1,1) mustEqual collision1 + obj.CheckCollisions(2,1,1) mustEqual collision1 + obj.CheckCollisions(6,1,1) mustEqual collision1 + obj.CheckCollisions(7,1,1) mustEqual collision2 + obj.CheckCollisions(8,1,1) mustEqual collision2 + obj.CheckCollisions(12,1,1) mustEqual collision1 + obj.CheckCollisions(13,1,1) mustEqual collision2 + obj.CheckCollisions(14,1,1) mustEqual collision2 + } + + "clear all items" in { + val obj : GridInventory = GridInventory(9, 6) + obj += 2 -> bullet9mmBox1 + obj.Size mustEqual 1 + obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true obj.Clear() - ok + obj.Size mustEqual 0 + obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true } "remove item" in { val obj : GridInventory = GridInventory(9, 6) obj += 0 -> bullet9mmBox1 - obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true obj -= PlanetSideGUID(1) - obj.hasItem(PlanetSideGUID(1)) mustEqual None + obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true obj.Clear() ok } + "fail to remove from an invalid slot (n < 0)" in { + val obj : GridInventory = GridInventory(9, 6) + (obj -= -1) mustEqual false + } + + "fail to remove from an invalid slot (n > capacity)" in { + val obj : GridInventory = GridInventory(9, 6) + (obj -= 55) mustEqual false + } + "unblock insertion on item removal" in { val obj : GridInventory = GridInventory(9, 6) obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) obj += 23 -> bullet9mmBox1 - obj.hasItem(PlanetSideGUID(1)) mustEqual Some(bullet9mmBox1) + obj.hasItem(PlanetSideGUID(1)).contains(bullet9mmBox1) mustEqual true obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(1 :: Nil) obj -= PlanetSideGUID(1) - obj.hasItem(PlanetSideGUID(1)) mustEqual None + obj.hasItem(PlanetSideGUID(1)).isEmpty mustEqual true obj.CheckCollisions(23, bullet9mmBox1) mustEqual Success(Nil) obj.Clear() ok @@ -350,41 +447,82 @@ class InventoryTest extends Specification { ok } - "insert items quickly (risk overwriting entries)" in { + "confirm integrity of inventory as a grid" in { val obj : GridInventory = GridInventory(6, 6) (obj += 0 -> bullet9mmBox1) mustEqual true - val collision1 = obj.CheckCollisions(0,1,1) - obj.CheckCollisions(1,1,1) mustEqual collision1 - obj.CheckCollisions(2,1,1) mustEqual collision1 - obj.CheckCollisions(6,1,1) mustEqual collision1 - obj.CheckCollisions(7,1,1) mustEqual collision1 - obj.CheckCollisions(8,1,1) mustEqual collision1 - obj.CheckCollisions(12,1,1) mustEqual collision1 - obj.CheckCollisions(13,1,1) mustEqual collision1 - obj.CheckCollisions(14,1,1) mustEqual collision1 + (obj += 21 -> bullet9mmBox2) mustEqual true + //artificially pollute the inventory grid-space + obj.SetCells(10, 1, 1, 3) + obj.SetCells(19, 2, 2, 4) + obj.ElementsOnGridMatchList() mustEqual 5 //number of misses repaired + } - (obj += 7 -> bullet9mmBox2) mustEqual false //can not insert overlapping object - obj.CheckCollisions(0,1,1) mustEqual collision1 - obj.CheckCollisions(1,1,1) mustEqual collision1 - obj.CheckCollisions(2,1,1) mustEqual collision1 - obj.CheckCollisions(6,1,1) mustEqual collision1 - obj.CheckCollisions(7,1,1) mustEqual collision1 - obj.CheckCollisions(8,1,1) mustEqual collision1 - obj.CheckCollisions(12,1,1) mustEqual collision1 - obj.CheckCollisions(13,1,1) mustEqual collision1 - obj.CheckCollisions(14,1,1) mustEqual collision1 + "confirm integrity of inventory as a list (no overlap)" in { + val obj : GridInventory = GridInventory(9, 9) + val gun = Tool(suppressor) + obj.InsertQuickly(0, gun) + obj.InsertQuickly(33, bullet9mmBox1) + //nothing should overlap + val lists = obj.ElementsInListCollideInGrid() + lists.size mustEqual 0 + } - obj.InsertQuickly(7, bullet9mmBox2) mustEqual true //overwrite - val collision2 = obj.CheckCollisions(7,1,1) - obj.CheckCollisions(0,1,1) mustEqual collision1 - obj.CheckCollisions(1,1,1) mustEqual collision1 - obj.CheckCollisions(2,1,1) mustEqual collision1 - obj.CheckCollisions(6,1,1) mustEqual collision1 - obj.CheckCollisions(7,1,1) mustEqual collision2 - obj.CheckCollisions(8,1,1) mustEqual collision2 - obj.CheckCollisions(12,1,1) mustEqual collision1 - obj.CheckCollisions(13,1,1) mustEqual collision2 - obj.CheckCollisions(14,1,1) mustEqual collision2 + "confirm integrity of inventory as a list (normal overlap)" in { + val obj : GridInventory = GridInventory(9, 9) + val gun = Tool(suppressor) + val bullet9mmBox3 = AmmoBox(bullet_9mm) + obj.InsertQuickly(0, gun) + obj.InsertQuickly(18, bullet9mmBox1) + obj.InsertQuickly(38, bullet9mmBox2) + obj.InsertQuickly(33, bullet9mmBox3) + //gun and box1 should overlap + //box1 and box2 should overlap + //box3 should not overlap with anything + val lists = obj.ElementsInListCollideInGrid() + lists.size mustEqual 2 + lists.foreach { list => + val out = list.map { _.obj } + if(out.size == 2 && out.contains(gun) && out.contains(bullet9mmBox1)) { + ok + } + else if(out.size == 2 && out.contains(bullet9mmBox1) && out.contains(bullet9mmBox2)) { + ok + } + else { + ko + } + } + ok + } + + "confirm integrity of inventory as a list (triple overlap)" in { + val obj : GridInventory = GridInventory(9, 9) + val gun = Tool(suppressor) + val bullet9mmBox3 = AmmoBox(bullet_9mm) + val bullet9mmBox4 = AmmoBox(bullet_9mm) + obj.InsertQuickly(0, gun) + obj.InsertQuickly(18, bullet9mmBox1) + obj.InsertQuickly(36, bullet9mmBox2) + obj.InsertQuickly(38, bullet9mmBox3) + obj.InsertQuickly(33, bullet9mmBox4) + //gun and box1 should overlap + //box1, box2, and box3 should overlap + //box4 should not overlap with anything + val lists = obj.ElementsInListCollideInGrid() + lists.size mustEqual 2 + lists.foreach { list => + val out = list.map { _.obj } + if(out.size == 2 && out.contains(gun) && out.contains(bullet9mmBox1)) { + ok + } + else if(out.size == 3 && out.contains(bullet9mmBox1) && out.contains(bullet9mmBox2) && out.contains(bullet9mmBox3)) { + ok + } + else { + ko + } + } + ok } } } diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 6a1e5922..cfb7a281 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -357,6 +357,7 @@ class ZonePopulationTest extends ActorTest { val zone = new Zone("test", new ZoneMap(""), 0) { override def SetupNumberPools() = { } } val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) + player.GUID = PlanetSideGUID(1) zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), ZoneTest.TestName) zone.Actor ! Zone.Init() expectNoMsg(200 milliseconds) diff --git a/common/src/test/scala/service/VehicleServiceTest.scala b/common/src/test/scala/service/VehicleServiceTest.scala index 94d55f7a..90326ef7 100644 --- a/common/src/test/scala/service/VehicleServiceTest.scala +++ b/common/src/test/scala/service/VehicleServiceTest.scala @@ -261,8 +261,8 @@ class TransferPassengerChannelTest extends ActorTest { val service = system.actorOf(Props(classOf[VehicleService], Zone.Nowhere), "v-service") val fury = Vehicle(GlobalDefinitions.fury) service ! Service.Join("test") - service ! VehicleServiceMessage("test", VehicleAction.TransferPassengerChannel(PlanetSideGUID(10), "old_channel", "new_channel", fury)) - expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.TransferPassengerChannel("old_channel", "new_channel", fury))) + service ! VehicleServiceMessage("test", VehicleAction.TransferPassengerChannel(PlanetSideGUID(10), "old_channel", "new_channel", fury, PlanetSideGUID(11))) + expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.TransferPassengerChannel("old_channel", "new_channel", fury, PlanetSideGUID(11)))) } } } diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index 25133cc3..459e0801 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -11,7 +11,7 @@ import ch.qos.logback.core.joran.spi.JoranException import ch.qos.logback.core.status._ import ch.qos.logback.core.util.StatusPrinter import com.typesafe.config.ConfigFactory -import net.psforever.config.{Valid, Invalid} +import net.psforever.config.{Invalid, Valid} import net.psforever.crypto.CryptoInterface import net.psforever.objects.zones._ import net.psforever.objects.guid.TaskResolver @@ -19,7 +19,7 @@ import org.slf4j import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi.Color._ import services.ServiceManager -import services.account.AccountIntermediaryService +import services.account.{AccountIntermediaryService, AccountPersistenceService} import services.chat.ChatService import services.galaxy.GalaxyService import services.teamwork.SquadService @@ -275,6 +275,7 @@ object PsLogin { serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy") serviceManager ! ServiceManager.Register(Props[SquadService], "squad") serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster") + serviceManager ! ServiceManager.Register(Props[AccountPersistenceService], "accountPersistence") logger.info("Initializing loginRouter & worldRouter") /** Create two actors for handling the login and world server endpoints */ diff --git a/pslogin/src/main/scala/SessionRouter.scala b/pslogin/src/main/scala/SessionRouter.scala index 8777def9..5fd94b81 100644 --- a/pslogin/src/main/scala/SessionRouter.scala +++ b/pslogin/src/main/scala/SessionRouter.scala @@ -6,8 +6,6 @@ import org.log4s.MDC import scodec.bits._ import scala.collection.mutable -import MDCContextAware.Implicits._ -import akka.actor.MDCContextAware.MdcMsg import akka.actor.SupervisorStrategy.Stop import net.psforever.packet.PacketCoding import net.psforever.packet.control.ConnectionClose @@ -35,10 +33,10 @@ case class SessionPipeline(nameTemplate : String, props : Props) * * read() route decrypt * UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor] - * ^ | ^ | ^ | + * /|\ | /|\ | /|\ | * | write() | | encrypt | | response | * +--------------+ +-----------+ +-----------------+ - **/ + */ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger(self.path.name) @@ -57,7 +55,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act override def supervisorStrategy = OneForOneStrategy() { case _ => Stop } override def preStart = { - log.info(s"SessionRouter started...ready for ${role} sessions") + log.info(s"SessionRouter (for ${role}s) initializing ...") } def receive = initializing @@ -65,10 +63,13 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act def initializing : Receive = { case Hello() => inputRef = sender() - context.become(started) ServiceManager.serviceManager ! Lookup("accountIntermediary") + case ServiceManager.LookupResult("accountIntermediary", endpoint) => + accountIntermediary = endpoint + log.info(s"SessionRouter starting; ready for $role sessions") + context.become(started) case default => - log.error(s"Unknown message $default. Stopping...") + log.error(s"Unknown or unexpected message $default before being properly started. Stopping completely...") context.stop(self) } @@ -77,9 +78,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act } def started : Receive = { - case ServiceManager.LookupResult("accountIntermediary", endpoint) => - accountIntermediary = endpoint - case recv @ ReceivedPacket(msg, from) => + case _ @ ReceivedPacket(msg, from) => var session : Session = null if(!idBySocket.contains(from)) { @@ -92,7 +91,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act if(session.state != Closed()) { MDC("sessionId") = session.sessionId.toString - log.trace(s"RECV: ${msg} -> ${session.getPipeline.head.path.name}") + log.trace(s"RECV: $msg -> ${session.getPipeline.head.path.name}") session.receive(RawPacket(msg)) MDC.clear() } @@ -102,7 +101,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act if(session.isDefined) { if(session.get.state != Closed()) { MDC("sessionId") = session.get.sessionId.toString - log.trace(s"SEND: ${msg} -> ${inputRef.path.name}") + log.trace(s"SEND: $msg -> ${inputRef.path.name}") session.get.send(msg) MDC.clear() } @@ -160,7 +159,7 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act sessionByActor{actor} = session } - log.info(s"New session ID=${id} from " + address.toString) + log.info(s"New session ID=$id from " + address.toString) if(role == "Login") { accountIntermediary ! StoreIPAddress(id, new IPAddress(address)) @@ -178,14 +177,14 @@ class SessionRouter(role : String, pipeline : List[SessionPipeline]) extends Act val session : Session = sessionOption.get if(graceful) { - for(i <- 0 to 5) { + for(_ <- 0 to 5) { session.send(closePacket) } } // kill all session specific actors session.dropSession(graceful) - log.info(s"Dropping session ID=${id} (reason: $reason)") + log.info(s"Dropping session ID=$id (reason: $reason)") } def newSessionId = { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index a28ef5d0..e5c9a623 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1,81 +1,77 @@ -// Copyright (c) 2017 PSForever +// Copyright (c) 2017-2020 PSForever +//language imports +import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} +import akka.pattern.ask +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 akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} +import org.log4s.{Logger, MDC} +import scala.annotation.{switch, tailrec} +import scala.collection.concurrent.TrieMap +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.Attempt.{Failure, Successful} +import scodec.bits.ByteVector +//project imports +import csr.{CSRWarp, CSRZone, Traveler} +import MDCContextAware.Implicits._ import net.psforever.packet._ import net.psforever.packet.control._ import net.psforever.packet.game._ -import scodec.Attempt.{Failure, Successful} -import scodec.bits._ -import org.log4s.{Logger, MDC} -import MDCContextAware.Implicits._ -import com.github.mauricio.async.db.general.ArrayRowData -import com.github.mauricio.async.db.{Connection, QueryResult} -import csr.{CSRWarp, CSRZone, Traveler} -import net.psforever.objects.GlobalDefinitions._ -import services.ServiceManager.Lookup +import net.psforever.packet.game.objectcreate.{ConstructorData, DetailedCharacterData, DroppedItemData, ObjectClass, ObjectCreateMessageParent, PlacementData} +import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo} import net.psforever.objects._ import net.psforever.objects.avatar.{Certification, DeployableToolbox} -import net.psforever.objects.ballistics._ -import net.psforever.objects.ce._ +import net.psforever.objects.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.equipment.{CItem, _} -import net.psforever.objects.loadouts._ +import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} +import net.psforever.objects.equipment.{Ammo, CItem, EffectTarget, Equipment, EquipmentSize, EquipmentSlot, FireModeSwitch} +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.serverobject.mount.Mountable +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.deploy.Deployment -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker +import net.psforever.objects.serverobject.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, StructureType, WarpGate} -import net.psforever.objects.serverobject.terminals._ +import net.psforever.objects.serverobject.terminals.{CaptureTerminal, 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} +import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.teamwork.Squad -import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _} -import net.psforever.objects.vital._ +import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, MountedWeapons, Utility, UtilityType, VehicleLockState} +import net.psforever.objects.vehicles.Utility.InternalTelepad +import net.psforever.objects.vital.{DamageFromPainbox, HealFromExoSuitChange, HealFromKit, HealFromTerm, PlayerSuicide, RepairFromKit, Vitality} import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector} -import net.psforever.packet.game.objectcreate._ -import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo} import net.psforever.types._ -import services._ -import services.account.{ReceiveAccountData, RetrieveAccountData} +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.chat._ -import services.vehicle.support.TurretUpgrader -import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} -import services.teamwork.{SquadResponse, SquadService, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} - -import scala.collection.concurrent.TrieMap -import scala.collection.mutable.LongMap -import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.annotation.tailrec -import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ -import scala.util.Success -import akka.pattern.ask -import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} -import net.psforever.objects.serverobject.zipline.ZipLinePath -import net.psforever.objects.vehicles.Utility.InternalTelepad -import net.psforever.types import services.local.support.{HackCaptureActor, RouterTelepadActivation} +import services.ServiceManager.LookupResult import services.support.SupportActor - -import scala.collection.mutable +import services.teamwork.{SquadResponse, SquadService, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} +import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} +import services.vehicle.support.TurretUpgrader class WorldSessionActor extends Actor with MDCContextAware { @@ -87,6 +83,7 @@ class WorldSessionActor extends Actor 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 @@ -131,6 +128,9 @@ class WorldSessionActor extends Actor var recentTeleportAttempt : Long = 0 var lastTerminalOrderFulfillment : Boolean = true var shiftPosition : 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 @@ -186,122 +186,31 @@ class WorldSessionActor extends Actor import scala.language.implicitConversions implicit def boolToInt(b : Boolean) : Int = if(b) 1 else 0 - def JammableObject = player - override def postStop() : Unit = { - //TODO normally, player avatar persists a minute or so after disconnect; we are subject to the SessionReaper + //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() - galaxyService ! Service.Leave() - LivePlayerList.Remove(sessionId) - if(player != null && player.HasGUID) { - PlayerActionsToCancel() - squadService ! Service.Leave(Some(player.CharId.toString)) - val player_guid = player.GUID - //handle orphaned deployables - DisownDeployables() - //clean up boomer triggers and telepads - val equipment = ( - (player.Holsters() - .zipWithIndex - .map({ case ((slot, index)) => (index, slot.Equipment) }) - .collect { case ((index, Some(obj))) => InventoryItem(obj, index) } - ) ++ player.Inventory.Items) - .filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] || obj.isInstanceOf[Telepad] }) - //put any temporary value back into the avatar - //TODO final character save before doing any of this (use equipment) - continent.Population ! Zone.Population.Release(avatar) - if(player.isAlive) { - //actually being alive or manually deconstructing - player.Position = Vector3.Zero - //if seated, dismount - player.VehicleSeated match { - case Some(_) => - //quickly and briefly kill player to avoid disembark animation? - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0)) - DismountVehicleOnLogOut() - case _ => ; - } - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) - taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) - //TODO normally, the actual player avatar persists a minute or so after the user disconnects - } - else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) { - //player disconnected while waiting for a revive, maybe - //similar to handling ReleaseAvatarRequestMessage - player.Release - player.VehicleSeated match { - case None => - FriskCorpse(player) //TODO eliminate dead letters - if(!WellLootedCorpse(player)) { - continent.Population ! Zone.Corpse.Add(player) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) - taskResolver ! GUIDTask.UnregisterLocker(player.Locker)(continent.GUID) //rest of player will be cleaned up with corpses - } - else { - //no items in inventory; leave no corpse - val player_guid = player.GUID - player.Position = Vector3.Zero //save character before doing this - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) - taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) - } - - case Some(_) => - val player_guid = player.GUID - player.Position = Vector3.Zero //save character before doing this - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) - taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) - DismountVehicleOnLogOut() + 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 => ; } } - //disassociate and start the deconstruction timer for any currently owned vehicle - SpecialCaseDisownVehicle() - continent.Population ! Zone.Population.Leave(avatar) - } - } - - /** - * Vehicle cleanup that is specific to log out behavior. - */ - def DismountVehicleOnLogOut() : Unit = { - (continent.GUID(player.VehicleSeated) match { - case Some(obj : Mountable) => - (Some(obj), obj.PassengerInSeat(player)) - case _ => - (None, None) - }) match { - case (Some(mountObj), Some(seatIndex)) => - mountObj.Seats(seatIndex).Occupant = None - - case _ => ; - } - } - - /** - * If a vehicle is owned by a character, disassociate the vehicle, then schedule it for deconstruction. - * @see `DisownVehicle()` - * @return the vehicle previously owned, if any - */ - def SpecialCaseDisownVehicle() : Option[Vehicle] = { - DisownVehicle() match { - case out @ Some(vehicle) => - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent)) - continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, - if(vehicle.Flying) { - Some(0 seconds) //immediate deconstruction - } - else { - vehicle.Definition.DeconstructionTime //normal deconstruction - } - )) - out - case None => - None } } @@ -319,12 +228,15 @@ class WorldSessionActor extends Actor rightRef = sender() } context.become(Started) - ServiceManager.serviceManager ! Lookup("accountIntermediary") - ServiceManager.serviceManager ! Lookup("chat") - ServiceManager.serviceManager ! Lookup("taskResolver") - ServiceManager.serviceManager ! Lookup("cluster") - ServiceManager.serviceManager ! Lookup("galaxy") - ServiceManager.serviceManager ! Lookup("squad") + 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") @@ -348,22 +260,25 @@ class WorldSessionActor extends Actor } def Started : Receive = { - case ServiceManager.LookupResult("accountIntermediary", endpoint) => + case LookupResult("accountIntermediary", endpoint) => accountIntermediary = endpoint log.info("ID: " + sessionId + " Got account intermediary service " + endpoint) - case ServiceManager.LookupResult("chat", 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 ServiceManager.LookupResult("taskResolver", endpoint) => + case LookupResult("taskResolver", endpoint) => taskResolver = endpoint log.info("ID: " + sessionId + " Got task resolver service " + endpoint) - case ServiceManager.LookupResult("galaxy", endpoint) => + case LookupResult("galaxy", endpoint) => galaxyService = endpoint log.info("ID: " + sessionId + " Got galaxy service " + endpoint) - case ServiceManager.LookupResult("cluster", endpoint) => + case LookupResult("cluster", endpoint) => cluster = endpoint log.info("ID: " + sessionId + " Got cluster service " + endpoint) - case ServiceManager.LookupResult("squad", endpoint) => + case LookupResult("squad", endpoint) => squadService = endpoint log.info("ID: " + sessionId + " Got squad service " + endpoint) @@ -371,9 +286,9 @@ class WorldSessionActor extends Actor handleControlPkt(ctrl) case GamePacket(_, _, pkt) => handleGamePkt(pkt) - // temporary hack to keep the client from disconnecting - //it's been a "temporary hack" since 2016 :P + case PokeClient() => + persist() sendResponse(KeepAliveMessage()) case AvatarServiceResponse(toChannel, guid, reply) => @@ -395,19 +310,44 @@ class WorldSessionActor extends Actor case GalaxyResponse.MapUpdate(msg) => sendResponse(msg) - case GalaxyResponse.TransferPassenger(temp_channel, vehicle, vehicle_to_delete) => - vehicle.PassengerInSeat(player) match { - case Some(_) => + 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(vehicle) //on the other continent and registered to that continent's GUID system - interstellarFerryTopLevelGUID = Some(vehicle_to_delete) //vehicle.GUID, or previously a higher level parent - LoadZonePhysicalSpawnPoint(vehicle.Continent, vehicle.Position, vehicle.Orientation, 1) + 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 => - continent.VehicleEvents ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones + galaxyService ! Service.Leave(Some(temp_channel)) //no longer being transferred between zones + interstellarFerryTopLevelGUID = None case Some(_) => ; //wait patiently } @@ -724,83 +664,89 @@ class WorldSessionActor extends Actor case CheckCargoMounting(cargo_guid, carrier_guid, mountPoint, iteration) => HandleCheckCargoMounting(cargo_guid, carrier_guid, mountPoint, iteration) - case CreateCharacter(connection, name, head, voice, gender, empire) => + case CreateCharacter(name, head, voice, gender, empire) => log.info(s"Creating new character $name...") - val accountUserName : String = account.Username - - connection.get.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 Success(insertResult) => - insertResult match { - case result: QueryResult => - if (result.rows.nonEmpty) { - log.info(s"Successfully created new character for $accountUserName") - sendResponse(ActionResultMessage.Pass) - self ! ListAccountCharacters(connection) - } else { - log.error(s"Error creating new character for $accountUserName") - sendResponse(ActionResultMessage.Fail(0)) - self ! ListAccountCharacters(connection) + 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) => + 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)) + if(connection.isConnected) connection.disconnect + self ! ListAccountCharacters() } - case _ => - log.error(s"Error creating new character for $accountUserName") - sendResponse(ActionResultMessage.Fail(3)) - self ! ListAccountCharacters(connection) + case scala.util.Failure(e) => + if(connection.isConnected) connection.disconnect + failWithError(s"CreateCharacter: query failed - ${e.getMessage}") } - case _ => failWithError("Something to do ?") + case scala.util.Failure(e) => + log.error(s"CreateCharacter: no connection - ${e.getMessage}?") } - case ListAccountCharacters(connection) => - val accountUserName : String = account.Username - - StartBundlingPackets() - connection.get.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 Success(queryResult) => - queryResult match { - case result: QueryResult => - if (result.rows.nonEmpty) { + 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(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.info(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) => + 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 lTime = value(7).asInstanceOf[org.joda.time.LocalDateTime].toDateTime().getMillis() / 1000 val secondsSinceLastLogin = nowTimeInSeconds - lTime - if (!lDeleted) { + if(!lDeleted) { avatarArray(i) = new Avatar(value(0).asInstanceOf[Int], lName, lFaction, lGender, lHead, lVoice) - AwardBattleExperiencePoints(avatarArray(i), 20000000L) + 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(StandardPistol(playerArray(i).Faction)) - playerArray(i).Slot(1).Equipment = Tool(MediumPistol(playerArray(i).Faction)) - playerArray(i).Slot(2).Equipment = Tool(HeavyRifle(playerArray(i).Faction)) - playerArray(i).Slot(3).Equipment = Tool(AntiVehicularLauncher(playerArray(i).Faction)) - playerArray(i).Slot(4).Equipment = Tool(katana) + 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 + if(health > 0) { //player can not be dead; stay spawned as alive playerArray(i).Health = health playerArray(i).Stamina = stamina playerArray(i).Armor = armor @@ -811,17 +757,22 @@ class WorldSessionActor extends Actor } sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)) } - Thread.sleep(50) - if(connection.nonEmpty) connection.get.disconnect - } else { - log.info("dunno") } - case _ => - log.error(s"Error listing character(s) for account $accountUserName") + Thread.sleep(50) + if(connection.isConnected) connection.disconnect + + 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 _ => failWithError("Something to do ?") + + case scala.util.Failure(e) => + failWithError(s"ListAccountCharacters: no connection - ${e.getMessage}") } - StopBundlingPackets() case VehicleLoaded(_ /*vehicle*/) => ; //currently being handled by VehicleSpawnPad.LoadVehicle during testing phase @@ -1131,7 +1082,7 @@ class WorldSessionActor extends Actor taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) case InterstellarCluster.ClientInitializationComplete() => - LivePlayerList.Add(sessionId, avatar) + LivePlayerList.Add(avatar.CharId, avatar) traveler = new Traveler(self, continent.Id) StartBundlingPackets() //PropertyOverrideMessage @@ -1151,17 +1102,35 @@ class WorldSessionActor extends Actor 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) - //go home - cluster ! InterstellarCluster.GetWorld("z6") + 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 - continent.AvatarEvents ! Service.Leave() - continent.LocalEvents ! Service.Leave() - continent.VehicleEvents ! Service.Leave() - player.Continent = zoneId + 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) => @@ -1176,13 +1145,15 @@ class WorldSessionActor extends Actor player = tplayer //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100, 25, true, continent.Map.Checksum)) - AvatarCreate() //important! the LoadMapMessage must be processed by the client before the avatar is created + 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 - AvatarCreate() + setupAvatarFunc() + persist() self ! SetCurrentAvatar(tplayer) case PlayerFailedToLoad(tplayer) => @@ -1191,17 +1162,6 @@ class WorldSessionActor extends Actor failWithError(s"${tplayer.Name} failed to load anywhere") } - case UnregisterCorpseOnVehicleDisembark(corpse) => - if(!corpse.isAlive && corpse.HasGUID) { - corpse.VehicleSeated match { - case Some(_) => - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(corpse)) - case None => - taskResolver ! GUIDTask.UnregisterPlayer(corpse)(continent.GUID) - } - } - case SetCurrentAvatar(tplayer) => HandleSetCurrentAvatar(tplayer) @@ -1241,11 +1201,9 @@ class WorldSessionActor extends Actor case ReceiveAccountData(account) => log.info(s"Retrieved account data for accountId = ${account.AccountId}") - this.account = account admin = account.GM - - Database.getConnection.connect.onComplete { // TODO remove that DB access. + Database.getConnection.connect.onComplete { case scala.util.Success(connection) => Database.query(connection.sendPreparedStatement( "SELECT gm FROM accounts where id=?", Array(account.AccountId) @@ -1253,22 +1211,23 @@ class WorldSessionActor extends Actor case scala.util.Success(queryResult) => queryResult match { case row: ArrayRowData => // If we got a row from the database - log.info(s"Ready to load character list for ${account.Username}") - self ! ListAccountCharacters(Some(connection)) + 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"Issue retrieving result set from database for account $account") + log.error(s"ReceiveAccountData: ${account.Username} data not found, or unexpected query result format - ${queryResult.getClass}") Thread.sleep(50) - if (connection.isConnected) connection.disconnect - sendResponse(DropSession(sessionId, "You should not exist !")) + if(connection.isConnected) connection.disconnect + sendResponse(DropSession(sessionId, "You should not exist!")) } case scala.util.Failure(e) => - log.error("Is there a problem ? " + e.getMessage) + log.error(s"ReceiveAccountData: ${e.getMessage}") + if(connection.isConnected) connection.disconnect Thread.sleep(50) - if (connection.isConnected) connection.disconnect } case scala.util.Failure(e) => - log.error("Failed connecting to database for account lookup " + e.getMessage) + log.error(s"RetrieveAccountData: no connection ${e.getMessage}") } + case LoadedRemoteProjectile(projectile_guid, Some(projectile)) => if(projectile.profile.ExistsOnRemoteClients) { //spawn projectile on other clients @@ -1293,10 +1252,200 @@ class WorldSessionActor extends Actor 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 + + 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, -8192, 0) //SE + case 2 => Vector3(-8192, -8192, 0) //SW + case 3 => Vector3(-8192, 8192, 0) //NW + } + player.FirstLoad = true + 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.Health == 0 => + inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(v), inZone)) + inZone.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(v, inZone, if(v.Flying) { + //TODO gravity + Some(0 seconds) //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.
+ *
+ * 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 + } + /** * na * @param toChannel na @@ -1307,6 +1456,10 @@ class WorldSessionActor extends Actor 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) @@ -1908,7 +2061,7 @@ class WorldSessionActor extends Actor } case None => ; } - OwnVehicle(obj, tplayer) + 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 @@ -2282,7 +2435,13 @@ class WorldSessionActor extends Actor 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 }) - taskResolver ! DelayedObjectHeld(tplayer, 0, List(PutEquipmentInSlot(tplayer, maxWeapons.head.obj, 0))) + 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 { @@ -2621,7 +2780,7 @@ class WorldSessionActor extends Actor //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible) if(tplayer_guid != guid) { sendResponse(ObjectCreateMessage(vtype, vguid, vdata)) - ReloadVehicleAccessPermissions(vehicle) + Vehicles.ReloadAccessPermissions(vehicle, player.Name) } case VehicleResponse.MountVehicle(vehicle_guid, seat) => @@ -2660,8 +2819,10 @@ class WorldSessionActor extends Actor } case VehicleResponse.UnloadVehicle(vehicle, vehicle_guid) => - BeforeUnloadVehicle(vehicle) - sendResponse(ObjectDeleteMessage(vehicle_guid, 0)) + //if(tplayer_guid != guid) { + BeforeUnloadVehicle(vehicle) + sendResponse(ObjectDeleteMessage(vehicle_guid, 0)) + //} case VehicleResponse.UnstowEquipment(item_guid) => if(tplayer_guid != guid) { @@ -2683,11 +2844,12 @@ class WorldSessionActor extends Actor amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction) DrawCurrentAmsSpawnPoint() - case VehicleResponse.TransferPassengerChannel(old_channel, temp_channel, vehicle) => + 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 (driver name, plus extra) + galaxyService ! Service.Join(temp_channel) //temporary vehicle-specific channel } case VehicleResponse.ForceDismountVehicleCargo(cargo_guid, bailed, requestedByPassenger, kicked) => @@ -2724,7 +2886,7 @@ class WorldSessionActor extends Actor case VehicleResponse.PlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on - ReloadVehicleAccessPermissions(vehicle) + Vehicles.ReloadAccessPermissions(vehicle, player.Name) ServerVehicleLock(vehicle) case VehicleResponse.ServerVehicleOverrideStart(vehicle, pad) => @@ -3178,8 +3340,9 @@ class WorldSessionActor extends Actor } /** - * na - * @param tplayer na + * 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 @@ -3264,22 +3427,48 @@ class WorldSessionActor extends Actor LoadZoneTransferPassengerMessages( guid, continent.Id, - TransportVehicleChannelName(vehicle), - vehicle, - interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID) + vehicle ) - interstellarFerryTopLevelGUID = None case _ => ; } + interstellarFerryTopLevelGUID = None if (loadConfZone && connectionState == 100) { configZone(continent) loadConfZone = false } - if (noSpawnPointHere) { + 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) } /** @@ -3449,7 +3638,6 @@ class WorldSessionActor extends Actor 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( @@ -3459,29 +3647,31 @@ class WorldSessionActor extends Actor queryResult match { case row: ArrayRowData => // If we got a row from the database if (row(0).asInstanceOf[Int] == account.AccountId) { // create char -// self ! CreateCharacter(Some(connection), name, head, voice, gender, empire) + self ! CreateCharacter(name, head, voice, gender, empire) sendResponse(ActionResultMessage.Fail(1)) Thread.sleep(50) - if (connection.isConnected) connection.disconnect - } else { // send "char already exist" + } + else { // send "char already exist" sendResponse(ActionResultMessage.Fail(1)) Thread.sleep(50) - if (connection.isConnected) connection.disconnect } case _ => // If the char name didn't exist in the database, create char - self ! CreateCharacter(Some(connection), name, head, voice, gender, empire) + self ! CreateCharacter(name, head, voice, gender, empire) } + if(connection.isConnected) connection.disconnect case scala.util.Failure(e) => + if(connection.isConnected) connection.disconnect sendResponse(ActionResultMessage.Fail(4)) - self ! ListAccountCharacters(Some(connection)) + log.error("Returning to character list due to error " + e.getMessage) + self ! ListAccountCharacters() } case scala.util.Failure(e) => - log.error("Failed connecting to database for account lookup " + e.getMessage) + log.error(s"CharacterCreateRequest: no connection - ${e.getMessage}") sendResponse(ActionResultMessage.Fail(5)) } case msg @ CharacterRequestMessage(charId, action) => - log.info("Handling " + msg) + log.info(s"Handling $msg") action match { case CharacterRequestAction.Delete => Database.getConnection.connect.onComplete { @@ -3489,23 +3679,22 @@ class WorldSessionActor extends Actor Database.query(connection.sendPreparedStatement( "UPDATE characters SET deleted = true where id=?", Array(charId) )).onComplete { - case scala.util.Success(e) => - log.info(s"Character id = $charId deleted") + case scala.util.Success(_) => + if(connection.isConnected) connection.disconnect + log.info(s"CharacterRequest/Delete: character id $charId deleted") sendResponse(ActionResultMessage.Pass) - self ! ListAccountCharacters(Some(connection)) + 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) - if (connection.isConnected) connection.disconnect } case scala.util.Failure(e) => - log.error("Failed connecting to database for account lookup " + e.getMessage) + log.error(s"CharacterRequest/Delete: no connection - ${e.getMessage}") } case CharacterRequestAction.Select => - import net.psforever.objects.GlobalDefinitions._ - import net.psforever.types.CertificationType._ - Database.getConnection.connect.onComplete { case scala.util.Success(connection) => Database.query(connection.sendPreparedStatement( @@ -3513,117 +3702,31 @@ class WorldSessionActor extends Actor )).onComplete { case Success(queryResult) => queryResult match { - case row: ArrayRowData => + 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]) - val avatar : Avatar = new Avatar(charId, lName, lFaction, lGender, lHead, lVoice) - 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 + log.info(s"CharacterRequest/Select: character $lName found in records") + avatar = new Avatar(charId, lName, lFaction, lGender, lHead, lVoice) - 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 - // player.Certifications += BattleFrameRobotics - // player.Certifications += BFRAntiInfantry - // player.Certifications += BFRAntiAircraft - this.avatar = avatar - InitializeDeployableQuantities(avatar) //set deployables ui elements - AwardBattleExperiencePoints(avatar, 20000000L) - avatar.CEP = 600000 - - player = new Player(avatar) - - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - - Database.getConnection.connect.onComplete { - case scala.util.Success(connection) => - LoadDataBaseLoadouts(player, Some(connection)) - case scala.util.Failure(e) => - log.info(s"shit : ${e.getMessage}") - } + 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 _ => - log.info("toto tata") + if(connection.isConnected) connection.disconnect + case e => + if(connection.isConnected) connection.disconnect + log.error(s"CharacterRequest/Select: toto tata; unexpected query result format - ${e.getClass}") } - Thread.sleep(200) - Database.query(connection.sendPreparedStatement( - "UPDATE characters SET last_login = ? where id=?", Array(new java.sql.Timestamp(System.currentTimeMillis), charId) - )) - Thread.sleep(50) - var faction : String = "tr" - if (player.Faction == PlanetSideEmpire.NC) faction = "nc" - else if (player.Faction == PlanetSideEmpire.VS) faction = "vs" - whenUsedLastMAXName(2) = faction+"hev_antipersonnel" - whenUsedLastMAXName(3) = faction+"hev_antivehicular" - whenUsedLastMAXName(1) = faction+"hev_antiaircraft" - - (0 until 4).foreach( index => { - player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - player.ExoSuit = ExoSuitType.Standard - player.Slot(0).Equipment = Tool(StandardPistol(player.Faction)) - player.Slot(2).Equipment = Tool(suppressor) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(bullet_9mm) - player.Slot(9).Equipment = AmmoBox(bullet_9mm) - player.Slot(12).Equipment = AmmoBox(bullet_9mm) - player.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) - player.Slot(36).Equipment = AmmoBox(StandardPistolAmmo(player.Faction)) - player.Slot(39).Equipment = SimpleItem(remote_electronics_kit) - player.Inventory.Items.foreach { _.obj.Faction = player.Faction } - player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit) - player.Position = Vector3(4000f ,4000f ,500f) - player.Orientation = Vector3(0f, 354.375f, 157.5f) - player.FirstLoad = true - // LivePlayerList.Add(sessionId, avatar) - - //TODO check if can spawn on last continent/location from player? - //TODO if yes, get continent guid accessors - //TODO if no, get sanctuary guid accessors and reset the player's expectations - cluster ! InterstellarCluster.RequestClientInitialization() - - if(connection.isConnected) connection.disconnect case scala.util.Failure(e) => - log.error("Failed connecting to database for account lookup " + e.getMessage) + log.error(s"CharacterRequest/Select: no connection - ${e.getMessage}") } case default => @@ -3639,7 +3742,6 @@ class WorldSessionActor extends Actor traveler.zone = continentId val faction = player.Faction val factionChannel = s"$faction" - continent.AvatarEvents ! Service.Join(avatar.name) continent.AvatarEvents ! Service.Join(continentId) continent.AvatarEvents ! Service.Join(factionChannel) continent.LocalEvents ! Service.Join(avatar.name) @@ -3777,12 +3879,12 @@ class WorldSessionActor extends Actor ) ) }) - ReloadVehicleAccessPermissions(vehicle) + Vehicles.ReloadAccessPermissions(vehicle, player.Name) }) //our vehicle would have already been loaded; see NewPlayerLoaded/AvatarCreate usedVehicle.headOption match { - case Some(vehicle) if !vehicle.PassengerInSeat(player).contains(0) => - //if passenger, attempt to depict any other passengers already in this zone + 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 }) @@ -3798,6 +3900,10 @@ class WorldSessionActor extends Actor ) ) }) + //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)) + } case _ => ; //driver, or no vehicle } //vehicle wreckages @@ -3902,7 +4008,7 @@ class WorldSessionActor extends Actor } } continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.UpdateAmsSpawnPoint(continent)) - self ! SetCurrentAvatar(player) + 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) => val isMoving = WorldEntity.isMoving(vel) @@ -4006,13 +4112,13 @@ class WorldSessionActor extends Actor 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, spectator, wepInHand)) - updateSquad() + } + 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, 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 @@ -4090,32 +4196,29 @@ class WorldSessionActor extends Actor case msg @ ReleaseAvatarRequestMessage() => log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released") reviveTimer.cancel - continent.Population ! Zone.Population.Release(avatar) GoToDeploymentMap() + continent.Population ! Zone.Population.Release(avatar) player.VehicleSeated match { case None => - FriskCorpse(player) - if(!WellLootedCorpse(player)) { - TurnPlayerIntoCorpse(player) - continent.Population ! Zone.Corpse.Add(player) //TODO move back out of this match case when changing below issue - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) - } - else { - //no items in inventory; leave no corpse - val player_guid = player.GUID - sendResponse(ObjectDeleteMessage(player_guid, 0)) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) - taskResolver ! GUIDTask.UnregisterPlayer(player)(continent.GUID) - } + PrepareToTurnPlayerIntoCorpse(player, continent) case Some(_) => val player_guid = player.GUID sendResponse(ObjectDeleteMessage(player_guid, 0)) - continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) - self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, BailType.Normal, true)) //let vehicle try to clean up its fields - - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player)) + 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) + continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) + continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, Some(0 seconds))) + 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) => @@ -5367,7 +5470,6 @@ class WorldSessionActor extends Actor log.warn(s"DeployObject: $obj is something?") case None => log.warn("DeployObject: nothing?") - } case msg @ GenericObjectStateMsg(object_guid, unk1) => @@ -6232,7 +6334,7 @@ class WorldSessionActor extends Actor def Execute(resolver : ActorRef) : Unit = { localDriver.VehicleSeated = localVehicle.GUID - OwnVehicle(localVehicle, localDriver) + Vehicles.Own(localVehicle, localDriver) localAnnounce ! NewPlayerLoaded(localDriver) //alerts WSA resolver ! scala.util.Success(this) } @@ -6440,13 +6542,16 @@ class WorldSessionActor extends Actor 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 localMsg = InterstellarCluster.GetWorld(zoneId) + private val localServiceMsg = InterstellarCluster.GetWorld(zoneId) override def isComplete : Task.Resolution.Value = priorTask.task.isComplete def Execute(resolver : ActorRef) : Unit = { - localService ! localMsg + localZone.Population ! localAvatarMsg + localService ! localServiceMsg resolver ! scala.util.Success(this) } }, List(priorTask) @@ -6607,7 +6712,7 @@ class WorldSessionActor extends Actor case Some(guid : PlanetSideGUID) => continent.GUID(guid) match { case Some(vehicle: Vehicle) => - DisownVehicle(player, vehicle) + Vehicles.Disown(player, vehicle) case _ => ; } case _ => ; @@ -6618,7 +6723,7 @@ class WorldSessionActor extends Actor // Remove ownership of the vehicle from the previous player continent.GUID(previousOwnerGuid) match { case Some(player: Player) => - DisownVehicle(player, target) + Vehicles.Disown(player, target) case _ => ; // Vehicle already has no owner } case _ => ; @@ -6626,7 +6731,7 @@ class WorldSessionActor extends Actor // Now take ownership of the jacked vehicle target.Faction = player.Faction - OwnVehicle(target, player) + Vehicles.Own(target, player) //todo: Send HackMessage -> HackCleared to vehicle? can be found in packet captures. Not sure if necessary. @@ -6679,119 +6784,6 @@ class WorldSessionActor extends Actor continent.VehicleEvents ! VehicleServiceMessage.TurretUpgrade(TurretUpgrader.AddTask(target, continent, upgrade)) } - /** - * Temporary function that iterates over vehicle permissions and turns them into `PlanetsideAttributeMessage` packets.
- *
- * 2 November 2017:
- * Unexpected behavior causes seat mount points to become blocked when a new driver claims the vehicle. - * For the purposes of ensuring that other players are always aware of the proper permission state of the trunk and seats, - * packets are intentionally dispatched to the current client to update the states. - * Perform this action just after any instance where the client would initially gain awareness of the vehicle. - * The most important examples include either the player or the vehicle itself spawning in for the first time.
- *
- * 20 February 2018:
- * Occasionally, during deployment, local(?) vehicle seat access permissions may change. - * This results in players being locked into their own vehicle. - * Reloading vehicle permissions supposedly ensures the seats will be properly available. - * This is considered a client issue; but, somehow, it also impacts server operation somehow.
- *
- * 22 June 2018:
- * I think vehicle ownership works properly now. - * @param vehicle the `Vehicle` - */ - def ReloadVehicleAccessPermissions(vehicle : Vehicle) : Unit = { - if(vehicle.Faction == player.Faction) { - val vehicle_guid = vehicle.GUID - (0 to 3).foreach(group => { - sendResponse( - PlanetsideAttributeMessage(vehicle_guid, group + 10, vehicle.PermissionGroup(group).get.id) - ) - }) - } - } - - /** - * na - * @param vehicle na - * @param tplayer na - * @return na - */ - def OwnVehicle(vehicle : Vehicle, tplayer : Player) : Option[Vehicle] = OwnVehicle(vehicle, Some(tplayer)) - - /** - * na - * @param vehicle na - * @param playerOpt na - * @return na - */ - def OwnVehicle(vehicle : Vehicle, playerOpt : Option[Player]) : Option[Vehicle] = { - playerOpt match { - case Some(tplayer) => - tplayer.VehicleOwned = vehicle.GUID - vehicle.AssignOwnership(playerOpt) - - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.Ownership(player.GUID, vehicle.GUID)) - ReloadVehicleAccessPermissions(vehicle) - Some(vehicle) - case None => - None - } - } - - /** - * Disassociate this client's player (oneself) from a vehicle that he owns. - */ - def DisownVehicle() : Option[Vehicle] = DisownVehicle(player) - - /** - * Disassociate a player from a vehicle that he owns. - * The vehicle must exist in the game world on the current continent. - * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. - * This is the player side of vehicle ownership removal. - * @param tplayer the player - */ - def DisownVehicle(tplayer : Player) : Option[Vehicle] = { - tplayer.VehicleOwned match { - case Some(vehicle_guid) => - tplayer.VehicleOwned = None - continent.GUID(vehicle_guid) match { - case Some(vehicle : Vehicle) => - DisownVehicle(tplayer, vehicle) - case _ => - None - } - case None => - None - } - } - - /** - * Disassociate a player from a vehicle that he owns without associating a different player as the owner. - * Set the vehicle's driver seat permissions and passenger and gunner seat permissions to "allow empire," - * then reload them for all clients. - * This is the vehicle side of vehicle ownership removal. - * @param tplayer the player - */ - private def DisownVehicle(tplayer : Player, vehicle : Vehicle) : Option[Vehicle] = { - val pguid = tplayer.GUID - if(vehicle.Owner.contains(pguid)) { - vehicle.AssignOwnership(None) - val factionChannel = s"${vehicle.Faction}" - continent.VehicleEvents ! VehicleServiceMessage(factionChannel, VehicleAction.Ownership(pguid, PlanetSideGUID(0))) - val vguid = vehicle.GUID - val empire = VehicleLockState.Empire.id - (0 to 2).foreach(group => { - vehicle.PermissionGroup(group, empire) - continent.VehicleEvents ! VehicleServiceMessage(factionChannel, VehicleAction.SeatPermissions(pguid, vguid, group, empire)) - }) - ReloadVehicleAccessPermissions(vehicle) - Some(vehicle) - } - else { - None - } - } - /** * Gives a target player positive battle experience points only. * If the player has access to more implant slots as a result of changing battle experience points, unlock those slots. @@ -6820,14 +6812,38 @@ class WorldSessionActor extends Actor } } + /** + * 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 fpr shared manipulation updates. + * Construct every object in the vehicle's inventory for shared manipulation updates. * @param vehicle the vehicle */ def AccessContents(vehicle : Vehicle) : Unit = { - continent.VehicleEvents ! Service.Join(s"${vehicle.Actor}") + AccessContentsChannel(vehicle) val parent_guid = vehicle.GUID vehicle.Trunk.Items.foreach(entry => { val obj = entry.obj @@ -6843,6 +6859,10 @@ class WorldSessionActor extends Actor }) } + 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. @@ -6856,6 +6876,11 @@ class WorldSessionActor extends Actor }) } + + 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`.
*
@@ -7603,7 +7628,7 @@ class WorldSessionActor extends Actor def DeploymentActivities(obj : Deployment.DeploymentObject, state : DriveState.Value) : Unit = { obj match { case vehicle : Vehicle => - ReloadVehicleAccessPermissions(vehicle) //TODO we should not have to do this imho + Vehicles.ReloadAccessPermissions(vehicle, player.Name) //TODO we should not have to do this imho //ams if(vehicle.Definition == GlobalDefinitions.ams) { state match { @@ -7899,7 +7924,7 @@ class WorldSessionActor extends Actor case None => ; } - shooting match { + prefire.orElse(shooting) match { case Some(guid) => sendResponse(ChangeFireStateMessage_Stop(guid)) continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ChangeFireState_Stop(player.GUID, guid)) @@ -7922,22 +7947,22 @@ class WorldSessionActor extends Actor * 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.
*
- * If that player is the driver of a vehicle, it will construct the vehicle. - * If that player is the occupant of a vehicle, it will place them inside that vehicle. - * These two previous statements operate through similar though distinct mechanisms and actually imply different conditions. - * The vehicle will not be created unless the player is a living driver; - * but, the second statement will always trigger so long as the player is in a vehicle. - * The first produces a version of the player more suitable to be "another player in the game," and not "the avatar." - * The second would write over the product of the first to produce "the avatar." - * The vehicle should only be constructed once as, if it created a second time, that distinction will become lost. + * 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 `BeginZoningMessage` * @see `CargoMountBehaviorForOthers` * @see `AvatarCreateInVehicle` * @see `GetKnownVehicleAndSeat` * @see `LoadZoneTransferPassengerMessages` * @see `Player.Spawn` - * @see `ReloadVehicleAccessPermissions` - * @see `TransportVehicleChannelName` + * @see `ReloadUsedLastCoolDownTimes` + * @see `Vehicles.Own` + * @see `Vehicles.ReloadAccessPermissions` */ def AvatarCreate() : Unit = { val health = player.Health @@ -7951,13 +7976,6 @@ class WorldSessionActor extends Actor } GetKnownVehicleAndSeat() match { case (Some(vehicle : Vehicle), Some(seat : Int)) => - //vehicle and driver/passenger - interstellarFerry = None - val vdef = vehicle.Definition - val data = vdef.Packet.ConstructorData(vehicle).get - val guid = vehicle.GUID - sendResponse(ObjectCreateMessage(vehicle.Definition.ObjectId, guid, data)) - ReloadVehicleAccessPermissions(vehicle) //if the vehicle is the cargo of another vehicle in this zone val carrierInfo = continent.GUID(vehicle.MountedIn) match { case Some(carrier : Vehicle) => @@ -7965,33 +7983,55 @@ class WorldSessionActor extends Actor case _ => (None, None) } - player.VehicleSeated = guid - if(seat == 0) { - //if driver - OwnVehicle(vehicle, player) + //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 = guid } - continent.Transport ! Zone.Vehicle.Spawn(vehicle) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, guid, data)) + .foreach { _.MountedIn = vguid } + continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.LoadVehicle(player.GUID, vehicle, vdef.ObjectId, vguid, data)) carrierInfo match { case (Some(carrier), Some((index, _))) => CargoMountBehaviorForOthers(carrier, vehicle, index) case _ => vehicle.MountedIn = None } + data } else { - //if passenger; - //meant for same-zone warping; when player changes zones, redundant information will be sent + //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") - //player, passenger AvatarCreateInVehicle(player, vehicle, seat) case _ => @@ -8007,7 +8047,211 @@ class WorldSessionActor extends Actor continent.Population ! Zone.Population.Spawn(avatar, player) //cautious redundancy deadState = DeadState.Alive + ReloadUsedLastCoolDownTimes() + } + /** + * 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.
+ *
+ * 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.
+ *
+ * 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).
+ *
+ * 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.
+ *
+ * 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 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.Seat(0).get + 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) { @@ -8019,87 +8263,6 @@ class WorldSessionActor extends Actor sendResponse(AvatarVehicleTimerMessage(player.GUID, whenUsedLastMAXName(i), 300 - ((lTime - whenUsedLastMAX(i)) / 1000 toInt), true)) } } - - } - - /** - * If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat.
- *
- * For special purposes involved in zone transfers, - * where the vehicle may or may not exist in either of the zones (yet), - * the value of `interstellarFerry` is also polled. - * Making certain this field is blanked after the transfer is completed is important - * to avoid inspecting the wrong vehicle and failing simple vehicle checks where this function may be employed. - * @see `interstellarFerry` - * @return a tuple consisting of a vehicle reference and a seat index - * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; - * `(None, None)`, otherwise (even if the vehicle can be determined) - */ - def GetKnownVehicleAndSeat() : (Option[Vehicle], Option[Int]) = - interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { - case Some(vehicle : Vehicle) => - vehicle.PassengerInSeat(player) match { - case index @ Some(_) => - (Some(vehicle), index) - case None => - (None, None) - } - case _ => - (None, None) - } - - /** - * If the player is seated in a vehicle, find that vehicle and get the seat index number at which the player is sat. - * @return a tuple consisting of a vehicle reference and a seat index - * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; - * `(None, None)`, otherwise (even if the vehicle can be determined) - */ - def GetVehicleAndSeat() : (Option[Vehicle], Option[Int]) = - continent.GUID(player.VehicleSeated) match { - case Some(vehicle : Vehicle) => - vehicle.PassengerInSeat(player) match { - case index @ Some(_) => - (Some(vehicle), index) - case None => - (None, None) - } - case _ => - (None, None) - } - - /** - * Create an avatar character as if that avatar's player is mounted in a vehicle object's seat.
- *
- * This is a very specific configuration of the player character that is not visited very often. - * The value of `player.VehicleSeated` should be set to accommodate `Packet.DetailedConstructorData` and, - * though not explicitly checked, - * should be the same as the globally unique identifier that is assigned to the `vehicle` parameter for the current zone. - * The priority of this function is consider "initial" so it introduces the avatar to the game world in this state - * and is permitted to introduce the avatar to the vehicle's internal settings in a similar way. - * @see `AccessContents` - * @see `UpdateWeaponAtSeatPosition` - * @param tplayer the player avatar seated in the vehicle's seat - * @param vehicle the vehicle the player is driving - * @param seat the seat index - */ - def AvatarCreateInVehicle(tplayer : Player, vehicle : Vehicle, seat : Int) : Unit = { - val pdef = tplayer.Definition - val guid = player.GUID - val parent = ObjectCreateMessageParent(vehicle.GUID, seat) - val data = pdef.Packet.DetailedConstructorData(player).get - sendResponse( - ObjectCreateDetailedMessage( - pdef.ObjectId, - guid, - parent, - data - ) - ) - continent.AvatarEvents ! AvatarServiceMessage(vehicle.Continent, AvatarAction.LoadPlayer(guid, pdef.ObjectId, guid, pdef.Packet.ConstructorData(player).get, Some(parent))) - AccessContents(vehicle) - UpdateWeaponAtSeatPosition(vehicle, seat) - //log.info(s"AvatarCreateInVehicle: $guid -> $data") - log.trace(s"AvatarCreateInVehicle: ${player.Name} in ${vehicle.Definition.Name}") } /** @@ -8112,16 +8275,7 @@ class WorldSessionActor extends Actor def RespawnClone(tplayer : Player) : Player = { val faction = tplayer.Faction val obj = Player.Respawn(tplayer) - obj.Slot(0).Equipment = Tool(StandardPistol(faction)) - obj.Slot(2).Equipment = Tool(suppressor) - obj.Slot(4).Equipment = Tool(StandardMelee(faction)) - obj.Slot(6).Equipment = AmmoBox(bullet_9mm) - obj.Slot(9).Equipment = AmmoBox(bullet_9mm) - obj.Slot(12).Equipment = AmmoBox(bullet_9mm) - obj.Slot(33).Equipment = AmmoBox(bullet_9mm_AP) - obj.Slot(36).Equipment = AmmoBox(StandardPistolAmmo(faction)) - obj.Slot(39).Equipment = SimpleItem(remote_electronics_kit) - obj.Inventory.Items.foreach { _.obj.Faction = faction } + LoadClassicDefault(obj) obj } @@ -8131,7 +8285,7 @@ class WorldSessionActor extends Actor * MAX's have their primary weapon in the designated slot removed. * @param obj the player to be turned into a corpse */ - def FriskCorpse(obj : Player) : Unit = { + def FriskDeadBody(obj : Player) : Unit = { if(obj.isBackpack) { obj.Slot(4).Equipment match { case None => ; @@ -8162,6 +8316,37 @@ class WorldSessionActor extends Actor } } + /** + * 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). @@ -8181,7 +8366,7 @@ class WorldSessionActor extends Actor * @return `true`, if the `obj` is actually a corpse and has no objects in its holsters or backpack; * `false`, otherwise */ - def WellLootedCorpse(obj : Player) : Boolean = { + def WellLootedDeadBody(obj : Player) : Boolean = { obj.isBackpack && obj.Holsters().count(_.Equipment.nonEmpty) == 0 && obj.Inventory.Size == 0 } @@ -8192,7 +8377,7 @@ class WorldSessionActor extends Actor * `false`, otherwise */ def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { - if(WellLootedCorpse(obj)) { + if(WellLootedDeadBody(obj)) { continent.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent)) true } @@ -8215,7 +8400,7 @@ class WorldSessionActor extends Actor val sanctNumber = Zones.SanctuaryZoneNumber(tplayer.Faction) if(currentZone == sanctNumber) { if(!player.isAlive) { - sendResponse(DisconnectMessage("Player failed to load on faction's sanctuary continent. Please relog.")) + 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? } @@ -9194,34 +9379,6 @@ class WorldSessionActor extends Actor }) } - /** - * Collect all deployables previously owned by the player, - * dissociate the avatar's globally unique identifier to remove turnover ownership, - * and, on top of performing the above manipulations, dispose of any boomers discovered. - * (`BoomerTrigger` objects, the companions of the boomers, should be handled by an external implementation - * if they had not already been handled by the time this function is executed.) - * @return all previously-owned deployables after they have been processed; - * boomers are listed before all other deployable types - */ - def DisownDeployables() : List[PlanetSideGameObject with Deployable] = { - val (boomers, deployables) = - avatar.Deployables.Clear() - .map(continent.GUID(_)) - .collect { case Some(obj) => obj.asInstanceOf[PlanetSideGameObject with Deployable] } - .partition(_.isInstanceOf[BoomerDeployable]) - //do not change the OwnerName field at this time - boomers.collect({ case obj : BoomerDeployable => - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent, Some(0 seconds))) //near-instant - obj.Owner = None - obj.Trigger = None - }) - deployables.foreach(obj => { - continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(obj, continent)) //normal decay - obj.Owner = None - }) - boomers ++ deployables - } - /** * The starting point of behavior for a player who: * is dead and is respawning; @@ -9277,13 +9434,18 @@ class WorldSessionActor extends Actor case Some(vehicle : Vehicle) => //driver or passenger in vehicle using a warp gate LoadZoneInVehicle(vehicle, pos, ori, zone_id) - case _ => //player is deconstructing self + 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 @@ -9328,10 +9490,14 @@ class WorldSessionActor extends Actor player = tplayer (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterLocker(original.Locker)(continent.GUID), zone_id)) } - else { + 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)) + } } } @@ -9396,28 +9562,27 @@ class WorldSessionActor extends Actor **/ 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 = TransportVehicleChannelName(vehicle) - //standard passengers - continent.VehicleEvents ! VehicleServiceMessage(s"${vehicle.Actor}", VehicleAction.TransferPassengerChannel(pguid, s"${vehicle.Actor}", toChannel, vehicle)) - //cargo - val occupiedCargoHolds = vehicle.CargoHolds.values.collect { - case hold if hold.isOccupied => - hold.Occupant.get - } - occupiedCargoHolds.foreach{ cargo => - cargo.Seats(0).Occupant match { - case Some(occupant) => - continent.VehicleEvents ! VehicleServiceMessage(s"${occupant.Name}", VehicleAction.TransferPassengerChannel(pguid, s"${cargo.Actor}", toChannel, cargo)) - case _ => - log.error("LoadZoneInVehicleAsDriver: abort; vehicle in cargo hold missing driver") - HandleDismountVehicleCargo(player.GUID, cargo.GUID, cargo, vehicle.GUID, vehicle, false, false, true) - } + 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") + HandleDismountVehicleCargo(pguid, 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) { //transferring a vehicle between spawn points (warp gates) in the same zone - //LoadZoneTransferPassengerMessages(pguid, zone_id, toChannel, vehicle, PlanetSideGUID(0)) (self, PlayerLoaded(player)) } else { @@ -9425,14 +9590,13 @@ class WorldSessionActor extends Actor LoadZoneCommonTransferActivity() player.VehicleSeated = vehicle.GUID player.Continent = zone_id //forward-set the continent id to perform a test - interstellarFerryTopLevelGUID = (if(vehicle.Seats.values.count(_.isOccupied) == 1 && occupiedCargoHolds.size == 0) { + interstellarFerryTopLevelGUID = (if(manifest.passengers.isEmpty && manifest.cargo.count { case (name, _) => !name.equals("MISSING_DRIVER") } == 0) { //do not delete if vehicle has passengers or cargo - val vehicleToDelete = interstellarFerryTopLevelGUID.orElse(player.VehicleSeated).getOrElse(PlanetSideGUID(0)) - continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.UnloadVehicle(pguid, continent, vehicle, vehicleToDelete)) + continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.UnloadVehicle(pguid, continent, vehicle, topLevel)) None } else { - interstellarFerryTopLevelGUID.orElse(Some(vehicle.GUID)) + Some(topLevel) }) //unregister vehicle and driver whole + GiveWorld continent.Transport ! Zone.Vehicle.Despawn(vehicle) @@ -9440,18 +9604,6 @@ class WorldSessionActor extends Actor } } - /** - * The channel name for summoning passengers to the vehicle - * after it has been loaded to a new location or to a new zone. - * This channel name should be unique to the vehicle for at least the duration of the transition. - * The vehicle-specific channel with which all passengers are coordinated back to the original vehicle. - * @param vehicle the vehicle being moved (or having been moved) - * @return the channel name - */ - def TransportVehicleChannelName(vehicle : Vehicle) : String = { - s"transport-vehicle-channel-${interstellarFerryTopLevelGUID.getOrElse(vehicle.GUID).guid}" - } - /** * Deal with a target player as a vehicle passenger in the course of a redeployment action to a target continent * whether that action is the result of a deconstruction (reconstruction) @@ -9468,7 +9620,7 @@ class WorldSessionActor extends Actor * This vehicle can be deleted for everyone if no more work can be detected. * @see `GUIDTask.UnregisterAvatar` * @see `LoadZoneCommonTransferActivity` - * @see `NoVehicleOccupantsInZone` + * @see `Vehicles.AllGatedOccupantsInSameZone` * @see `PlayerLoaded` * @see `TaskBeforeZoneChange` * @see `UnAccessContents` @@ -9487,51 +9639,12 @@ class WorldSessionActor extends Actor player.VehicleSeated = vehicle.GUID player.Continent = zone_id //forward-set the continent id to perform a test val continentId = continent.Id - if(NoVehicleOccupantsInZone(vehicle, continentId)) { - //do not dispatch delete action if any hierarchical occupant has not gotten this far through the summoning process - val vehicleToDelete = interstellarFerryTopLevelGUID.orElse(player.VehicleSeated).getOrElse(PlanetSideGUID(0)) - continent.VehicleEvents ! VehicleServiceMessage(continentId, VehicleAction.UnloadVehicle(player.GUID, continent, vehicle, vehicleToDelete)) - } interstellarFerryTopLevelGUID = None //unregister avatar + GiveWorld (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(player)(continent.GUID), zone_id)) } } - /** - * A recursive test that explores all the seats of a target vehicle - * and all the seats of any discovered cargo vehicles - * and then the same criteria in those cargo vehicles - * to determine if any of their combined passenger roster remains in a given zone.
- *
- * While it should be possible to recursively explore up a parent-child relationship - - * testing the ferrying vehicle to which of the current tested vehicle in consider a cargo vehicle - - * the relationship expressed is one of globally unique refertences and not one of object references - - * that suggested super-ferrying vehicle may not exist in the zone unless special considerations are imposed. - * For the purpose of these special considerations, - * implemented by enforcing a strictly downwards order of vehicular zone transportation, - * where drivers move vehicles and call passengers and immediate cargo vehicle drivers, - * it becomes unnecessary to test any vehicle that might be ferrying the target vehicle. - * @see `ZoneAware` - * @param vehicle the target vehicle being moved around - * @param zone_id the zone in which the vehicle and its passengers should not be located - * @return `true`, if all passengers of the vehicle, and its cargo vehicles, etc., have reported being moved from the zone; - * `false`, otherwise - */ - def NoVehicleOccupantsInZone(vehicle : Vehicle, zoneId : String) : Boolean = { - (vehicle.Seats.values - .collect { case seat if seat.isOccupied && seat.Occupant.get.Continent == zoneId => true } - .isEmpty) && - { - vehicle.CargoHolds.values - .collect { - case hold if hold.isOccupied => - hold.Occupant.get - } - .foldLeft(true)(_ && NoVehicleOccupantsInZone(_, zoneId)) - } - } - /** * Dispatch messages to all target players in immediate passenger and gunner seats * and to the driver of all vehicles in cargo holds @@ -9542,18 +9655,24 @@ class WorldSessionActor extends Actor * @param toZoneId the zone where the target vehicle will be moved * @param toChannel the vehicle-specific channel with which all passengers are coordinated to the vehicle * @param vehicle the vehicle (object) - * @param vehicleToDelete the vehicle as it was identified in the zone that it is being moved from */ - def LoadZoneTransferPassengerMessages(player_guid : PlanetSideGUID, toZoneId : String, toChannel : String, vehicle : Vehicle, vehicleToDelete : PlanetSideGUID) : Unit = { - galaxyService ! GalaxyServiceMessage(toChannel, GalaxyAction.TransferPassenger(player_guid, toChannel, vehicle, vehicleToDelete)) - vehicle.CargoHolds.values - .collect { - case hold if hold.isOccupied => - val cargo = hold.Occupant.get - cargo.Continent = toZoneId - //point to the cargo vehicle to instigate cargo vehicle driver transportation - galaxyService ! GalaxyServiceMessage(toChannel, GalaxyAction.TransferPassenger(player_guid, toChannel, cargo, vehicleToDelete)) - } + 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") + } } /** @@ -9565,10 +9684,9 @@ class WorldSessionActor extends Actor RemoveBoomerTriggersFromInventory().foreach(obj => { taskResolver ! GUIDTask.UnregisterObjectTask(obj)(continent.GUID) }) - DisownDeployables() + Deployables.Disown(continent, avatar, self) drawDeloyableIcon = RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone squadSetup = ZoneChangeSquadSetup - continent.Population ! Zone.Population.Leave(avatar) } /** @@ -9737,7 +9855,7 @@ class WorldSessionActor extends Actor UseRouterTelepadEffect(pguid, sguid, dguid) StopBundlingPackets() // continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(router), continent)) - // continent.VehicleEvents ! VehicleServiceMessage.Decon(RemoverActor.AddTask(router, continent, router.Definition.DeconstructionTime)) + // continent.VehicleEvents p! VehicleServiceMessage.Decon(RemoverActor.AddTask(router, continent, router.Definition.DeconstructionTime)) continent.LocalEvents ! LocalServiceMessage(continent.Id, LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)) } else { @@ -10032,350 +10150,414 @@ class WorldSessionActor extends Actor case row: ArrayRowData => // Update connection.sendPreparedStatement( "UPDATE loadouts SET exosuit_id=?, name=?, items=? where id=?", Array(owner.ExoSuit.id, label, megaList.drop(1), row(0)) - ) // Todo maybe add a .onComplete ? + ).onComplete { + case _ => + if(connection.isConnected) connection.disconnect + } Thread.sleep(50) - if (connection.isConnected) connection.disconnect case _ => // Save connection.sendPreparedStatement( "INSERT INTO loadouts (characters_id, loadout_number, exosuit_id, name, items) VALUES(?,?,?,?,?) RETURNING id", Array(owner.CharId, line, owner.ExoSuit.id, label, megaList.drop(1)) - ) // Todo maybe add a .onComplete ? + ).onComplete { + case _ => + if(connection.isConnected) connection.disconnect + } Thread.sleep(50) - if (connection.isConnected) connection.disconnect } case scala.util.Failure(e) => - log.error("Failed to execute the query " + e.getMessage) + if(connection.isConnected) connection.disconnect + log.error(s"SaveLoadoutToDB: query failed - ${e.getMessage}") } case scala.util.Failure(e) => - log.error("Failed connecting to database " + e.getMessage) + log.error(s"SaveLoadoutToDB: no conenction ${e.getMessage}") } } - def LoadDataBaseLoadouts(owner : Player, connection: Option[Connection]) = { - connection.get.sendPreparedStatement( - "SELECT id, loadout_number, exosuit_id, name, items FROM loadouts where characters_id = ?", Array(owner.CharId) - ).onComplete { - case Success(queryResult) => - queryResult match { - case result: QueryResult => - if (result.rows.nonEmpty) { - 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] + /** + * 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."
+ *
+ * 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) => + 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] - owner.ExoSuit = ExoSuitType(lExosuitId) + 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 + 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" => - owner.Slot(lIndex).Equipment = Tool(GetToolDefFromObjectID(lObjectId).asInstanceOf[ToolDefinition]) - case "AmmoBox" => - owner.Slot(lIndex).Equipment = AmmoBox(GetToolDefFromObjectID(lObjectId).asInstanceOf[AmmoBoxDefinition]) - case "ConstructionItem" => - owner.Slot(lIndex).Equipment = ConstructionItem(GetToolDefFromObjectID(lObjectId).asInstanceOf[ConstructionItemDefinition]) - case "BoomerTrigger" => - log.error("Found a BoomerTrigger in a loadout ?!") - case "SimpleItem" => - owner.Slot(lIndex).Equipment = SimpleItem(GetToolDefFromObjectID(lObjectId).asInstanceOf[SimpleItemDefinition]) - case "Kit" => - owner.Slot(lIndex).Equipment = Kit(GetToolDefFromObjectID(lObjectId).asInstanceOf[KitDefinition]) - case _ => - log.error("What's that item in the loadout ?!") - } - if (args2.length == 4) { - 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 - owner.Slot(lIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(lAmmoSlots).AmmoTypeIndex = lAmmoTypeIndex - owner.Slot(lIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(lAmmoSlots).Box = AmmoBox(AmmoBoxDefinition(lAmmoBoxDefinition)) + 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 "BoomerTrigger" => + log.error("LoadDataBaseLoadouts: found a BoomerTrigger in a loadout?!") + case "SimpleItem" => + doll.Slot(lIndex).Equipment = SimpleItem(GetToolDefFromObjectID(lObjectId).asInstanceOf[SimpleItemDefinition]) + case "Kit" => + doll.Slot(lIndex).Equipment = Kit(GetToolDefFromObjectID(lObjectId).asInstanceOf[KitDefinition]) + case _ => + log.error("LoadDataBaseLoadouts: what's that item in the loadout?!") + } + if (args2.length == 4) { + 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) } - }) - avatar.EquipmentLoadouts.SaveLoadout(owner, lName, lLoadoutNumber) - (0 until 4).foreach( index => { - if (owner.Slot(index).Equipment.isDefined) owner.Slot(index).Equipment = None - }) - owner.Inventory.Clear() + // something to do at end of loading ? + } } - // something to do at end of loading ? - } + case _ => + log.debug(s"LoadDataBaseLoadouts: no saved loadout(s) for character with id ${owner.CharId}") } - Thread.sleep(50) - if (connection.get.isConnected) connection.get.disconnect - case _ => - log.error(s"No saved loadout(s) for character ID : ${owner.CharId}") + if(connection.isConnected) connection.disconnect + 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) => - log.error("Failed connecting to database " + e.getMessage) + val msg = s"LoadDataBaseLoadouts: no connection - ${e.getMessage}" + log.error(msg) + result failure new Throwable(msg) } + result.future } - def LoadDefaultLoadouts() = { + /** + * 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 - player.ExoSuit = ExoSuitType.Agile - player.Slot(0).Equipment = Tool(frag_grenade) - player.Slot(1).Equipment = Tool(bank) - player.Slot(2).Equipment = Tool(HeavyRifle(player.Faction)) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = Tool(medicalapplicator) - player.Slot(9).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(12).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(33).Equipment = Tool(phoenix) - player.Slot(60).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(72).Equipment = Tool(jammer_grenade) - player.Slot(74).Equipment = Kit(medkit) - avatar.EquipmentLoadouts.SaveLoadout(player, "Agile HA/Deci", 0) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Agile - player.Slot(0).Equipment = Tool(frag_grenade) - player.Slot(1).Equipment = Tool(frag_grenade) - player.Slot(2).Equipment = Tool(HeavyRifle(player.Faction)) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(9).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(12).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(33).Equipment = Tool(medicalapplicator) - player.Slot(36).Equipment = Tool(frag_grenade) - player.Slot(38).Equipment = Kit(medkit) - player.Slot(54).Equipment = Tool(frag_grenade) - player.Slot(56).Equipment = Kit(medkit) - player.Slot(60).Equipment = Tool(bank) - player.Slot(72).Equipment = Tool(jammer_grenade) - player.Slot(74).Equipment = Kit(medkit) - avatar.EquipmentLoadouts.SaveLoadout(player, "Agile HA", 1) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Reinforced - player.Slot(0).Equipment = Tool(medicalapplicator) - player.Slot(1).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(2).Equipment = Tool(HeavyRifle(player.Faction)) - player.Slot(3).Equipment = Tool(phoenix) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(9).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(12).Equipment = Kit(medkit) - player.Slot(16).Equipment = Tool(frag_grenade) - player.Slot(36).Equipment = Kit(medkit) - player.Slot(40).Equipment = Tool(frag_grenade) - player.Slot(42).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(45).Equipment = AmmoBox(HeavyRifleAPAmmo(player.Faction)) - player.Slot(60).Equipment = Kit(medkit) - player.Slot(64).Equipment = Tool(jammer_grenade) - player.Slot(78).Equipment = Tool(phoenix) - player.Slot(87).Equipment = Tool(bank) - avatar.EquipmentLoadouts.SaveLoadout(player, "Rexo HA/Deci", 2) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Reinforced - player.Slot(0).Equipment = Tool(medicalapplicator) - player.Slot(1).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(2).Equipment = Tool(MediumRifle(player.Faction)) - player.Slot(3).Equipment = Tool(AntiVehicularLauncher(player.Faction)) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(MediumRifleAmmo(player.Faction)) - player.Slot(9).Equipment = AmmoBox(MediumRifleAmmo(player.Faction)) - player.Slot(12).Equipment = AmmoBox(MediumRifleAmmo(player.Faction)) - player.Slot(15).Equipment = Tool(bank) - player.Slot(42).Equipment = Tool(frag_grenade) - player.Slot(44).Equipment = Tool(jammer_grenade) - player.Slot(46).Equipment = Kit(medkit) - player.Slot(50).Equipment = Kit(medkit) - player.Slot(66).Equipment = AmmoBox(AntiVehicularAmmo(player.Faction)) - player.Slot(70).Equipment = AmmoBox(AntiVehicularAmmo(player.Faction)) - player.Slot(74).Equipment = AmmoBox(AntiVehicularAmmo(player.Faction)) - avatar.EquipmentLoadouts.SaveLoadout(player, "Rexo MA/AV", 3) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Reinforced - player.Slot(0).Equipment = Tool(medicalapplicator) - player.Slot(1).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(2).Equipment = Tool(HeavyRifle(player.Faction)) - player.Slot(3).Equipment = Tool(thumper) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(9).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(12).Equipment = Kit(medkit) - player.Slot(16).Equipment = Tool(frag_grenade) - player.Slot(36).Equipment = Kit(medkit) - player.Slot(40).Equipment = Tool(frag_grenade) - player.Slot(42).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(45).Equipment = AmmoBox(HeavyRifleAPAmmo(player.Faction)) - player.Slot(60).Equipment = Kit(medkit) - player.Slot(64).Equipment = Tool(jammer_grenade) - player.Slot(78).Equipment = Tool(bank) - player.Slot(81).Equipment = AmmoBox(frag_cartridge) - player.Slot(84).Equipment = AmmoBox(frag_cartridge) - player.Slot(87).Equipment = AmmoBox(frag_cartridge) - avatar.EquipmentLoadouts.SaveLoadout(player, "Rexo HA/Thumper", 4) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Reinforced - player.Slot(0).Equipment = Tool(medicalapplicator) - player.Slot(1).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(2).Equipment = Tool(HeavyRifle(player.Faction)) - player.Slot(3).Equipment = Tool(rocklet) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(9).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(12).Equipment = Kit(medkit) - player.Slot(16).Equipment = Tool(frag_grenade) - player.Slot(36).Equipment = Kit(medkit) - player.Slot(40).Equipment = Tool(frag_grenade) - player.Slot(42).Equipment = AmmoBox(HeavyRifleAmmo(player.Faction)) - player.Slot(45).Equipment = AmmoBox(HeavyRifleAPAmmo(player.Faction)) - player.Slot(60).Equipment = Kit(medkit) - player.Slot(64).Equipment = Tool(jammer_grenade) - player.Slot(78).Equipment = Tool(bank) - player.Slot(81).Equipment = AmmoBox(rocket) - player.Slot(84).Equipment = AmmoBox(rocket) - player.Slot(87).Equipment = AmmoBox(frag_cartridge) - avatar.EquipmentLoadouts.SaveLoadout(player, "Rexo HA/Rocklet", 5) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Reinforced - player.Slot(0).Equipment = Tool(medicalapplicator) - player.Slot(1).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(2).Equipment = Tool(MediumRifle(player.Faction)) - player.Slot(3).Equipment = Tool(bolt_driver) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(MediumRifleAmmo(player.Faction)) - player.Slot(9).Equipment = AmmoBox(MediumRifleAmmo(player.Faction)) - player.Slot(12).Equipment = Kit(medkit) - player.Slot(16).Equipment = Tool(frag_grenade) - player.Slot(36).Equipment = Kit(medkit) - player.Slot(40).Equipment = Tool(frag_grenade) - player.Slot(42).Equipment = AmmoBox(MediumRifleAmmo(player.Faction)) - player.Slot(45).Equipment = AmmoBox(MediumRifleAPAmmo(player.Faction)) - player.Slot(60).Equipment = Kit(medkit) - player.Slot(64).Equipment = Tool(jammer_grenade) - player.Slot(78).Equipment = Tool(bank) - player.Slot(81).Equipment = AmmoBox(bolt) - player.Slot(84).Equipment = AmmoBox(bolt) - player.Slot(87).Equipment = AmmoBox(bolt) - avatar.EquipmentLoadouts.SaveLoadout(player, "Rexo MA/Sniper", 6) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.Reinforced - player.Slot(0).Equipment = Tool(medicalapplicator) - player.Slot(1).Equipment = SimpleItem(remote_electronics_kit) - player.Slot(2).Equipment = Tool(flechette) - player.Slot(3).Equipment = Tool(phoenix) - player.Slot(4).Equipment = Tool(StandardMelee(player.Faction)) - player.Slot(6).Equipment = AmmoBox(shotgun_shell) - player.Slot(9).Equipment = AmmoBox(shotgun_shell) - player.Slot(12).Equipment = Kit(medkit) - player.Slot(16).Equipment = Tool(frag_grenade) - player.Slot(36).Equipment = Kit(medkit) - player.Slot(40).Equipment = Tool(frag_grenade) - player.Slot(42).Equipment = AmmoBox(shotgun_shell) - player.Slot(45).Equipment = AmmoBox(shotgun_shell_AP) - player.Slot(60).Equipment = Kit(medkit) - player.Slot(64).Equipment = Tool(jammer_grenade) - player.Slot(78).Equipment = Tool(phoenix) - player.Slot(87).Equipment = Tool(bank) - avatar.EquipmentLoadouts.SaveLoadout(player, "Rexo Sweeper/Deci", 7) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.MAX - player.Slot(0).Equipment = Tool(AI_MAX(player.Faction)) - player.Slot(6).Equipment = AmmoBox(AI_MAXAmmo(player.Faction)) - player.Slot(10).Equipment = AmmoBox(AI_MAXAmmo(player.Faction)) - player.Slot(14).Equipment = AmmoBox(AI_MAXAmmo(player.Faction)) - player.Slot(18).Equipment = AmmoBox(AI_MAXAmmo(player.Faction)) - player.Slot(70).Equipment = AmmoBox(AI_MAXAmmo(player.Faction)) - player.Slot(74).Equipment = AmmoBox(AI_MAXAmmo(player.Faction)) - player.Slot(78).Equipment = Kit(medkit) - player.Slot(98).Equipment = AmmoBox(health_canister) - player.Slot(100).Equipment = AmmoBox(armor_canister) - player.Slot(110).Equipment = Kit(medkit) - player.Slot(134).Equipment = Kit(medkit) - player.Slot(138).Equipment = Kit(medkit) - player.Slot(142).Equipment = Kit(medkit) - player.Slot(146).Equipment = Kit(medkit) - player.Slot(166).Equipment = Kit(medkit) - player.Slot(170).Equipment = Kit(medkit) - player.Slot(174).Equipment = Kit(medkit) - player.Slot(178).Equipment = Kit(medkit) - avatar.EquipmentLoadouts.SaveLoadout(player, "AI MAX", 8) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() - + 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 - player.ExoSuit = ExoSuitType.MAX - player.Slot(0).Equipment = Tool(AV_MAX(player.Faction)) - player.Slot(6).Equipment = AmmoBox(AV_MAXAmmo(player.Faction)) - player.Slot(10).Equipment = AmmoBox(AV_MAXAmmo(player.Faction)) - player.Slot(14).Equipment = AmmoBox(AV_MAXAmmo(player.Faction)) - player.Slot(18).Equipment = AmmoBox(AV_MAXAmmo(player.Faction)) - player.Slot(70).Equipment = AmmoBox(AV_MAXAmmo(player.Faction)) - player.Slot(74).Equipment = AmmoBox(AV_MAXAmmo(player.Faction)) - player.Slot(78).Equipment = Kit(medkit) - player.Slot(98).Equipment = AmmoBox(health_canister) - player.Slot(100).Equipment = AmmoBox(armor_canister) - player.Slot(110).Equipment = Kit(medkit) - player.Slot(134).Equipment = Kit(medkit) - player.Slot(138).Equipment = Kit(medkit) - player.Slot(142).Equipment = Kit(medkit) - player.Slot(146).Equipment = Kit(medkit) - player.Slot(166).Equipment = Kit(medkit) - player.Slot(170).Equipment = Kit(medkit) - player.Slot(174).Equipment = Kit(medkit) - player.Slot(178).Equipment = Kit(medkit) - avatar.EquipmentLoadouts.SaveLoadout(player, "AV MAX", 9) - (0 until 4).foreach( index => { - if (player.Slot(index).Equipment.isDefined) player.Slot(index).Equipment = None - }) - player.Inventory.Clear() + 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 @@ -10629,7 +10811,7 @@ class WorldSessionActor extends Actor */ def CountSpawnDelay(toZoneId : String, toSpawnPoint : SpawnPoint, fromZoneId : String) : Long = { val sanctuaryZoneId = Zones.SanctuaryZoneId(player.Faction) - if(sanctuaryZoneId.equals(toZoneId)) { //to sanctuary + if(fromZoneId.equals("Nowhere") || sanctuaryZoneId.equals(toZoneId)) { //to sanctuary 0L } else if(!player.isAlive) { @@ -10820,6 +11002,8 @@ class WorldSessionActor extends Actor 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] } } @@ -11025,11 +11209,10 @@ object WorldSessionActor { 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(connection: Option[Connection], name : String, head : Int, voice : CharacterVoice.Value, gender : CharacterGender.Value, empire : PlanetSideEmpire.Value) - private final case class ListAccountCharacters(connection: Option[Connection]) + 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 UnregisterCorpseOnVehicleDisembark(corpse : Player) private final case class CheckCargoMounting(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int) private final case class CheckCargoDismount(vehicle_guid : PlanetSideGUID, cargo_vehicle_guid: PlanetSideGUID, cargo_mountpoint: Int, iteration: Int)