diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index 9e28f7387..df4c74044 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -13,7 +13,8 @@ import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDASt
import net.psforever.objects.sourcing.{TurretSource, VehicleSource}
import net.psforever.packet.game.ImplantAction
import net.psforever.services.avatar.AvatarServiceResponse
-import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
+import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement, Vector3}
+import net.psforever.zones.Zones
import org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable
@@ -298,56 +299,136 @@ object AvatarActor {
log: org.log4s.Logger,
restoreAmmo: Boolean = false
): Unit = {
- clob.split("/").filter(_.trim.nonEmpty).foreach { value =>
- val (objectType, objectIndex, objectId, ammoData) = value.split(",") match {
- case Array(a, b: String, c: String) => (a, b.toInt, c.toInt, None)
- case Array(a, b: String, c: String, d) => (a, b.toInt, c.toInt, Some(d))
- case _ =>
- log.warn(s"ignoring invalid item string: '$value'")
- return
+ SplitClobFormat(clob, log)
+ .foreach {
+ case (objectType, objectIndex, objectId, ammoData) =>
+ BuildContainedEquipment(container, log, restoreAmmo, objectType, objectIndex, objectId, ammoData)
}
+ }
- objectType match {
- case "Tool" =>
- val tool = Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition])
- //previous ammunition loaded into each sub-magazine
- ammoData foreach { toolAmmo =>
- toolAmmo.split("_").drop(1).foreach { value =>
- val (ammoSlots, ammoTypeIndex, ammoBoxDefinition, ammoCount) = value.split("-") match {
- case Array(a: String, b: String, c: String) => (a.toInt, b.toInt, c.toInt, None)
- case Array(a: String, b: String, c: String, d: String) => (a.toInt, b.toInt, c.toInt, Some(d.toInt))
- }
- val fireMode = tool.AmmoSlots(ammoSlots)
- fireMode.AmmoTypeIndex = ammoTypeIndex
- fireMode.Box = AmmoBox(DefinitionUtil.idToDefinition(ammoBoxDefinition).asInstanceOf[AmmoBoxDefinition])
- ammoCount.collect {
- case count if restoreAmmo => fireMode.Magazine = count
- }
+ /**
+ * Transform from encoded inventory data as a CLOB - character large object - into individual items.
+ * Install those items into positions in a target container
+ * in the same positions in which they were previously recorded
+ * but limit new equipment only to those that are installed in the holster slots 0 through 4.
+ *
+ * There is no guarantee that the structure of the retained container data encoded in the CLOB
+ * will fit the current dimensions of the container.
+ * No tests are performed.
+ * A partial decompression of the CLOB may occur.
+ * @param container the container in which to place the pieces of equipment produced from the CLOB
+ * @param clob the inventory data in string form
+ * @param log a reference to a logging context
+ * @param restoreAmmo by default, when `false`, use the maximum ammunition for all ammunition boixes and for all tools;
+ * if `true`, load the last saved ammunition count for all ammunition boxes and for all tools
+ */
+ def buildHolsterEquipmentFromClob(
+ container: Container,
+ clob: String,
+ log: org.log4s.Logger,
+ restoreAmmo: Boolean = false
+ ): Unit = {
+ SplitClobFormat(clob, log)
+ .filter {
+ case (_, objectIndex, _, _) =>
+ objectIndex > -1 && objectIndex < 5
+ }
+ .foreach {
+ case (objectType, objectIndex, objectId, ammoData) =>
+ BuildContainedEquipment(container, log, restoreAmmo, objectType, objectIndex, objectId, ammoData)
+ }
+ }
+
+ /**
+ * Transform from encoded inventory data as a CLOB - character large object - into data for individual items.
+ * @param clob the inventory data in string form
+ * @param log a reference to a logging context
+ * @return four-tuple of information regarding an entity to be created:
+ * type of the entity,
+ * where the entity will be installed,
+ * specific identity of entity,
+ * additional data important for creating the entity
+ */
+ private def SplitClobFormat(
+ clob: String,
+ log: org.log4s.Logger
+ ): Array[(String, Int, Int, Option[String])] = {
+ clob
+ .split("/")
+ .filter(_.trim.nonEmpty)
+ .flatMap { value =>
+ value.split(",") match {
+ case Array(a, b: String, c: String) =>
+ Some((a, b.toInt, c.toInt, None))
+ case Array(a, b: String, c: String, d) =>
+ Some((a, b.toInt, c.toInt, Some(d)))
+ case _ =>
+ log.warn(s"ignoring invalid item string: '$value'")
+ None
+ }
+ }
+ }
+
+ /**
+ * Transform from decomposed encoded inventory data into individual items.
+ * Install those items into positions in a target container.
+ * @param container the container in which to place the pieces of equipment produced from the CLOB
+ * @param log a reference to a logging context
+ * @param restoreAmmo use the maximum ammunition for all ammunition boixes and for all tool
+ * @param objectType type of the entity
+ * @param objectIndex where the entity will be installed
+ * @param objectId specific identity of entity
+ * @param ammoData additional data important for creating the entity
+ */
+ private def BuildContainedEquipment(
+ container: Container,
+ log: org.log4s.Logger,
+ restoreAmmo: Boolean,
+ objectType: String,
+ objectIndex: Int,
+ objectId: Int,
+ ammoData: Option[String]
+ ): Unit = {
+ objectType match {
+ case "Tool" =>
+ val tool = Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition])
+ //previous ammunition loaded into each sub-magazine
+ ammoData foreach { toolAmmo =>
+ toolAmmo.split("_").drop(1).foreach { value =>
+ val (ammoSlots, ammoTypeIndex, ammoBoxDefinition, ammoCount) = value.split("-") match {
+ case Array(a: String, b: String, c: String) => (a.toInt, b.toInt, c.toInt, None)
+ case Array(a: String, b: String, c: String, d: String) => (a.toInt, b.toInt, c.toInt, Some(d.toInt))
+ }
+ val fireMode = tool.AmmoSlots(ammoSlots)
+ fireMode.AmmoTypeIndex = ammoTypeIndex
+ fireMode.Box = AmmoBox(DefinitionUtil.idToDefinition(ammoBoxDefinition).asInstanceOf[AmmoBoxDefinition])
+ ammoCount.collect {
+ case count if restoreAmmo => fireMode.Magazine = count
}
}
- container.Slot(objectIndex).Equipment = tool
- case "AmmoBox" =>
- val box = AmmoBox(DefinitionUtil.idToDefinition(objectId).asInstanceOf[AmmoBoxDefinition])
- container.Slot(objectIndex).Equipment = box
- //previous capacity of ammunition box
- ammoData.collect {
- case count if restoreAmmo => box.Capacity = count.toInt
- }
- case "ConstructionItem" =>
- container.Slot(objectIndex).Equipment = ConstructionItem(
- DefinitionUtil.idToDefinition(objectId).asInstanceOf[ConstructionItemDefinition]
- )
- case "SimpleItem" =>
- container.Slot(objectIndex).Equipment =
- SimpleItem(DefinitionUtil.idToDefinition(objectId).asInstanceOf[SimpleItemDefinition])
- case "Kit" =>
- container.Slot(objectIndex).Equipment =
- Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition])
- case "Telepad" | "BoomerTrigger" => ()
- //special types of equipment that are not actually loaded
- case name =>
- log.error(s"failing to add unknown equipment to a container - $name")
- }
+ }
+ container.Slot(objectIndex).Equipment = tool
+ case "AmmoBox" =>
+ val box = AmmoBox(DefinitionUtil.idToDefinition(objectId).asInstanceOf[AmmoBoxDefinition])
+ container.Slot(objectIndex).Equipment = box
+ //previous capacity of ammunition box
+ ammoData.collect {
+ case count if restoreAmmo => box.Capacity = count.toInt
+ }
+ case "ConstructionItem" =>
+ container.Slot(objectIndex).Equipment = ConstructionItem(
+ DefinitionUtil.idToDefinition(objectId).asInstanceOf[ConstructionItemDefinition]
+ )
+ case "SimpleItem" =>
+ container.Slot(objectIndex).Equipment =
+ SimpleItem(DefinitionUtil.idToDefinition(objectId).asInstanceOf[SimpleItemDefinition])
+ case "Kit" =>
+ container.Slot(objectIndex).Equipment =
+ Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition])
+ case "Telepad" | "BoomerTrigger" =>
+ () //special types of equipment that are valid but are not actually loaded
+ case name =>
+ log.error(s"failing to add unknown equipment to a container - $name")
}
}
@@ -2034,88 +2115,99 @@ class AvatarActor(
/** Send list of avatars to client (show character selection screen) */
def sendAvatars(account: Account): Unit = {
import ctx._
- val result = ctx.run(query[persistence.Avatar].filter(_.accountId == lift(account.id)))
- result.onComplete {
- case Success(avatars) =>
- val gen = new AtomicInteger(1)
- val converter = new CharacterSelectConverter
-
- avatars.filter(!_.deleted) foreach { a =>
- val secondsSinceLastLogin = Seconds.secondsBetween(a.lastLogin, LocalDateTime.now()).getSeconds
- val avatar = AvatarActor.toAvatar(a)
- val player = new Player(avatar)
-
- player.ExoSuit = ExoSuitType.Reinforced
- player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(player.Faction))
- player.Slot(1).Equipment = Tool(GlobalDefinitions.MediumPistol(player.Faction))
- player.Slot(2).Equipment = Tool(GlobalDefinitions.HeavyRifle(player.Faction))
- player.Slot(3).Equipment = Tool(GlobalDefinitions.AntiVehicularLauncher(player.Faction))
- player.Slot(4).Equipment = Tool(GlobalDefinitions.katana)
-
- /** After a client has connected to the server, their account is used to generate a list of characters.
- * On the character selection screen, each of these characters is made to exist temporarily when one is selected.
- * This "character select screen" is an isolated portion of the client, so it does not have any external constraints.
- * Temporary global unique identifiers are assigned to the underlying `Player` objects so that they can be turned into packets.
- */
- player
- .Holsters()
- .foreach(holster =>
- holster.Equipment match {
- case Some(tool: Tool) =>
- tool.AmmoSlots.foreach(slot => {
- slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement)
- })
- tool.GUID = PlanetSideGUID(gen.getAndIncrement)
- case Some(item: Equipment) =>
- item.GUID = PlanetSideGUID(gen.getAndIncrement)
- case _ => ()
+ val queryResult = ctx.run(
+ query[persistence.Avatar]
+ .filter { a => a.accountId == lift(account.id) && !a.deleted }
+ .leftJoin(query[persistence.Savedplayer])
+ .on { case (foundAvatar, saved) => foundAvatar.id == saved.avatarId }
+ )
+ queryResult.onComplete {
+ case Success(pairedResults) =>
+ lazy val now = LocalDateTime.now()
+ lazy val gen = new AtomicInteger(1)
+ lazy val converter = new CharacterSelectConverter
+ pairedResults.foreach {
+ case (a, saveOpt) =>
+ //setup character
+ val avatar = AvatarActor.toAvatar(a)
+ val player = new Player(avatar)
+ val zoneNum = saveOpt
+ .collect {
+ case persistence.Savedplayer(_, _, _, _, _, zoneNum, _, _, exosuitNum, loadout) =>
+ player.ExoSuit = ExoSuitType(exosuitNum)
+ AvatarActor.buildHolsterEquipmentFromClob(player, loadout, log)
+ zoneNum
}
- )
- player.GUID = PlanetSideGUID(gen.getAndIncrement)
- player.Spawn()
- sessionActor ! SessionActor.SendResponse(
- ObjectCreateDetailedMessage(
- ObjectClass.avatar,
- player.GUID,
- converter.DetailedConstructorData(player).get
- )
- )
- sessionActor ! SessionActor.SendResponse(
- CharacterInfoMessage(
- 15,
- PlanetSideZoneID(4),
- avatar.id,
- player.GUID,
- finished = false,
- secondsSinceLastLogin
- )
- )
-
- /** After the user has selected a character to load from the "character select screen,"
- * the temporary global unique identifiers used for that screen are stripped from the underlying `Player` object that was selected.
- * Characters that were not selected may be destroyed along with their temporary GUIDs.
- */
- player
- .Holsters()
- .foreach(holster =>
- holster.Equipment match {
- case Some(item: Tool) =>
- item.AmmoSlots.foreach(slot => {
- slot.Box.Invalidate()
- })
- item.Invalidate()
- case Some(item: Equipment) =>
- item.Invalidate()
- case _ => ()
+ .getOrElse {
+ player.ExoSuit = ExoSuitType.Standard
+ DefinitionUtil.applyDefaultHolsters(player)
+ Zones.sanctuaryZoneNumber(avatar.faction)
}
+ /*
+ After a client has connected,
+ their account is used to generate a list of characters on the character selection screen.
+ In terms of globally unique identifiers (GUID's), this is an isolated portion of the client
+ and it does not clash with any other GUID record on the server (zones).
+ Assign incremental temporary GUID's so that the characters from the selection can be turned into packets.
+ */
+ player
+ .Holsters()
+ .foreach(holster =>
+ holster.Equipment match {
+ case Some(tool: Tool) =>
+ tool.AmmoSlots.foreach(slot => {
+ slot.Box.GUID = PlanetSideGUID(gen.getAndIncrement)
+ })
+ tool.GUID = PlanetSideGUID(gen.getAndIncrement)
+ case Some(item: Equipment) =>
+ item.GUID = PlanetSideGUID(gen.getAndIncrement)
+ case _ => ()
+ }
+ )
+ val pguid = player.GUID = PlanetSideGUID(gen.getAndIncrement)
+ player.Spawn()
+ //display character
+ sessionActor ! SessionActor.SendResponse(
+ ObjectCreateDetailedMessage(
+ ObjectClass.avatar,
+ pguid,
+ converter.DetailedConstructorData(player).get
+ )
)
- player.Invalidate()
- }
- sessionActor ! SessionActor.SendResponse(
- CharacterInfoMessage(15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, 0)
- )
-
- case Failure(e) => log.error(e)("db failure")
+ //display zone
+ sessionActor ! SessionActor.SendResponse(
+ CharacterInfoMessage(
+ unk = 15,
+ PlanetSideZoneID(zoneNum),
+ avatar.id,
+ pguid,
+ finished = false,
+ Seconds.secondsBetween(a.lastLogin, now).getSeconds
+ )
+ )
+ //do not keep track of GUID's beyond the packet
+ player
+ .Holsters()
+ .foreach(holster =>
+ holster.Equipment match {
+ case Some(item: Tool) =>
+ item.AmmoSlots.foreach(slot => {
+ slot.Box.Invalidate()
+ })
+ item.Invalidate()
+ case Some(item: Equipment) =>
+ item.Invalidate()
+ case _ => ()
+ }
+ )
+ player.Invalidate()
+ }
+ //finalize list
+ sessionActor ! SessionActor.SendResponse(
+ CharacterInfoMessage(unk = 15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, secondsSinceLastLogin = 0L)
+ )
+ case Failure(e) =>
+ log.error(e)("db failure")
}
}
diff --git a/src/main/scala/net/psforever/util/DefinitionUtil.scala b/src/main/scala/net/psforever/util/DefinitionUtil.scala
index a8b86e174..27e93f336 100644
--- a/src/main/scala/net/psforever/util/DefinitionUtil.scala
+++ b/src/main/scala/net/psforever/util/DefinitionUtil.scala
@@ -316,13 +316,19 @@ object DefinitionUtil {
}
}
+ /** Apply default loadout holsters to given player */
+ def applyDefaultHolsters(player: Player): Unit = {
+ val faction = player.Faction
+ player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(faction))
+ player.Slot(2).Equipment = Tool(GlobalDefinitions.suppressor)
+ player.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(faction))
+ }
+
/** Apply default loadout to given player */
def applyDefaultLoadout(player: Player): Unit = {
val faction = player.Faction
player.ExoSuit = ExoSuitType.Standard
- player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(faction))
- player.Slot(2).Equipment = Tool(GlobalDefinitions.suppressor)
- player.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(faction))
+ applyDefaultHolsters(player)
player.Slot(6).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
player.Slot(9).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
player.Slot(12).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)