From 8afe7fa2486642035e50101744429f8036a524ea Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 29 Jul 2024 02:46:54 -0400 Subject: [PATCH] QoL: Character Select Screen (#1215) * make character select screen characters look like the player that logged out last * velocity does not matter * adjusting comments; filling out param names --- .../actors/session/AvatarActor.scala | 344 +++++++++++------- .../net/psforever/util/DefinitionUtil.scala | 12 +- 2 files changed, 227 insertions(+), 129 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 9e28f738..df4c7404 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 a8b86e17..27e93f33 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)