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
This commit is contained in:
Fate-JH 2024-07-29 02:46:54 -04:00 committed by GitHub
parent 8738a42ca0
commit 8afe7fa248
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 227 additions and 129 deletions

View file

@ -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.<br>
* <br>
* 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")
}
}

View file

@ -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)