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.objects.sourcing.{TurretSource, VehicleSource}
import net.psforever.packet.game.ImplantAction import net.psforever.packet.game.ImplantAction
import net.psforever.services.avatar.AvatarServiceResponse 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 org.joda.time.{LocalDateTime, Seconds}
import scala.collection.mutable import scala.collection.mutable
@ -298,56 +299,136 @@ object AvatarActor {
log: org.log4s.Logger, log: org.log4s.Logger,
restoreAmmo: Boolean = false restoreAmmo: Boolean = false
): Unit = { ): Unit = {
clob.split("/").filter(_.trim.nonEmpty).foreach { value => SplitClobFormat(clob, log)
val (objectType, objectIndex, objectId, ammoData) = value.split(",") match { .foreach {
case Array(a, b: String, c: String) => (a, b.toInt, c.toInt, None) case (objectType, objectIndex, objectId, ammoData) =>
case Array(a, b: String, c: String, d) => (a, b.toInt, c.toInt, Some(d)) BuildContainedEquipment(container, log, restoreAmmo, objectType, objectIndex, objectId, ammoData)
case _ =>
log.warn(s"ignoring invalid item string: '$value'")
return
} }
}
objectType match { /**
case "Tool" => * Transform from encoded inventory data as a CLOB - character large object - into individual items.
val tool = Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition]) * Install those items into positions in a target container
//previous ammunition loaded into each sub-magazine * in the same positions in which they were previously recorded
ammoData foreach { toolAmmo => * but limit new equipment only to those that are installed in the holster slots 0 through 4.<br>
toolAmmo.split("_").drop(1).foreach { value => * <br>
val (ammoSlots, ammoTypeIndex, ammoBoxDefinition, ammoCount) = value.split("-") match { * There is no guarantee that the structure of the retained container data encoded in the CLOB
case Array(a: String, b: String, c: String) => (a.toInt, b.toInt, c.toInt, None) * will fit the current dimensions of the container.
case Array(a: String, b: String, c: String, d: String) => (a.toInt, b.toInt, c.toInt, Some(d.toInt)) * No tests are performed.
} * A partial decompression of the CLOB may occur.
val fireMode = tool.AmmoSlots(ammoSlots) * @param container the container in which to place the pieces of equipment produced from the CLOB
fireMode.AmmoTypeIndex = ammoTypeIndex * @param clob the inventory data in string form
fireMode.Box = AmmoBox(DefinitionUtil.idToDefinition(ammoBoxDefinition).asInstanceOf[AmmoBoxDefinition]) * @param log a reference to a logging context
ammoCount.collect { * @param restoreAmmo by default, when `false`, use the maximum ammunition for all ammunition boixes and for all tools;
case count if restoreAmmo => fireMode.Magazine = count * 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" => container.Slot(objectIndex).Equipment = tool
val box = AmmoBox(DefinitionUtil.idToDefinition(objectId).asInstanceOf[AmmoBoxDefinition]) case "AmmoBox" =>
container.Slot(objectIndex).Equipment = box val box = AmmoBox(DefinitionUtil.idToDefinition(objectId).asInstanceOf[AmmoBoxDefinition])
//previous capacity of ammunition box container.Slot(objectIndex).Equipment = box
ammoData.collect { //previous capacity of ammunition box
case count if restoreAmmo => box.Capacity = count.toInt ammoData.collect {
} case count if restoreAmmo => box.Capacity = count.toInt
case "ConstructionItem" => }
container.Slot(objectIndex).Equipment = ConstructionItem( case "ConstructionItem" =>
DefinitionUtil.idToDefinition(objectId).asInstanceOf[ConstructionItemDefinition] container.Slot(objectIndex).Equipment = ConstructionItem(
) DefinitionUtil.idToDefinition(objectId).asInstanceOf[ConstructionItemDefinition]
case "SimpleItem" => )
container.Slot(objectIndex).Equipment = case "SimpleItem" =>
SimpleItem(DefinitionUtil.idToDefinition(objectId).asInstanceOf[SimpleItemDefinition]) container.Slot(objectIndex).Equipment =
case "Kit" => SimpleItem(DefinitionUtil.idToDefinition(objectId).asInstanceOf[SimpleItemDefinition])
container.Slot(objectIndex).Equipment = case "Kit" =>
Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition]) container.Slot(objectIndex).Equipment =
case "Telepad" | "BoomerTrigger" => () Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition])
//special types of equipment that are not actually loaded case "Telepad" | "BoomerTrigger" =>
case name => () //special types of equipment that are valid but are not actually loaded
log.error(s"failing to add unknown equipment to a container - $name") 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) */ /** Send list of avatars to client (show character selection screen) */
def sendAvatars(account: Account): Unit = { def sendAvatars(account: Account): Unit = {
import ctx._ import ctx._
val result = ctx.run(query[persistence.Avatar].filter(_.accountId == lift(account.id))) val queryResult = ctx.run(
result.onComplete { query[persistence.Avatar]
case Success(avatars) => .filter { a => a.accountId == lift(account.id) && !a.deleted }
val gen = new AtomicInteger(1) .leftJoin(query[persistence.Savedplayer])
val converter = new CharacterSelectConverter .on { case (foundAvatar, saved) => foundAvatar.id == saved.avatarId }
)
avatars.filter(!_.deleted) foreach { a => queryResult.onComplete {
val secondsSinceLastLogin = Seconds.secondsBetween(a.lastLogin, LocalDateTime.now()).getSeconds case Success(pairedResults) =>
val avatar = AvatarActor.toAvatar(a) lazy val now = LocalDateTime.now()
val player = new Player(avatar) lazy val gen = new AtomicInteger(1)
lazy val converter = new CharacterSelectConverter
player.ExoSuit = ExoSuitType.Reinforced pairedResults.foreach {
player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(player.Faction)) case (a, saveOpt) =>
player.Slot(1).Equipment = Tool(GlobalDefinitions.MediumPistol(player.Faction)) //setup character
player.Slot(2).Equipment = Tool(GlobalDefinitions.HeavyRifle(player.Faction)) val avatar = AvatarActor.toAvatar(a)
player.Slot(3).Equipment = Tool(GlobalDefinitions.AntiVehicularLauncher(player.Faction)) val player = new Player(avatar)
player.Slot(4).Equipment = Tool(GlobalDefinitions.katana) val zoneNum = saveOpt
.collect {
/** After a client has connected to the server, their account is used to generate a list of characters. case persistence.Savedplayer(_, _, _, _, _, zoneNum, _, _, exosuitNum, loadout) =>
* On the character selection screen, each of these characters is made to exist temporarily when one is selected. player.ExoSuit = ExoSuitType(exosuitNum)
* This "character select screen" is an isolated portion of the client, so it does not have any external constraints. AvatarActor.buildHolsterEquipmentFromClob(player, loadout, log)
* Temporary global unique identifiers are assigned to the underlying `Player` objects so that they can be turned into packets. zoneNum
*/
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 _ => ()
} }
) .getOrElse {
player.GUID = PlanetSideGUID(gen.getAndIncrement) player.ExoSuit = ExoSuitType.Standard
player.Spawn() DefinitionUtil.applyDefaultHolsters(player)
sessionActor ! SessionActor.SendResponse( Zones.sanctuaryZoneNumber(avatar.faction)
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 _ => ()
} }
/*
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() //display zone
} sessionActor ! SessionActor.SendResponse(
sessionActor ! SessionActor.SendResponse( CharacterInfoMessage(
CharacterInfoMessage(15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, 0) unk = 15,
) PlanetSideZoneID(zoneNum),
avatar.id,
case Failure(e) => log.error(e)("db failure") 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 */ /** Apply default loadout to given player */
def applyDefaultLoadout(player: Player): Unit = { def applyDefaultLoadout(player: Player): Unit = {
val faction = player.Faction val faction = player.Faction
player.ExoSuit = ExoSuitType.Standard player.ExoSuit = ExoSuitType.Standard
player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(faction)) applyDefaultHolsters(player)
player.Slot(2).Equipment = Tool(GlobalDefinitions.suppressor)
player.Slot(4).Equipment = Tool(GlobalDefinitions.StandardMelee(faction))
player.Slot(6).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) player.Slot(6).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
player.Slot(9).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) player.Slot(9).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)
player.Slot(12).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) player.Slot(12).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm)