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,15 +299,96 @@ 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
} }
/**
* 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 { objectType match {
case "Tool" => case "Tool" =>
val tool = Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition]) val tool = Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition])
@ -343,13 +425,12 @@ object AvatarActor {
case "Kit" => case "Kit" =>
container.Slot(objectIndex).Equipment = container.Slot(objectIndex).Equipment =
Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition]) Kit(DefinitionUtil.idToDefinition(objectId).asInstanceOf[KitDefinition])
case "Telepad" | "BoomerTrigger" => () case "Telepad" | "BoomerTrigger" =>
//special types of equipment that are not actually loaded () //special types of equipment that are valid but are not actually loaded
case name => case name =>
log.error(s"failing to add unknown equipment to a container - $name") log.error(s"failing to add unknown equipment to a container - $name")
} }
} }
}
/** /**
* Transform the encoded object to time data * Transform the encoded object to time data
@ -2034,28 +2115,40 @@ 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) =>
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 avatar = AvatarActor.toAvatar(a)
val player = new Player(avatar) val player = new Player(avatar)
val zoneNum = saveOpt
player.ExoSuit = ExoSuitType.Reinforced .collect {
player.Slot(0).Equipment = Tool(GlobalDefinitions.StandardPistol(player.Faction)) case persistence.Savedplayer(_, _, _, _, _, zoneNum, _, _, exosuitNum, loadout) =>
player.Slot(1).Equipment = Tool(GlobalDefinitions.MediumPistol(player.Faction)) player.ExoSuit = ExoSuitType(exosuitNum)
player.Slot(2).Equipment = Tool(GlobalDefinitions.HeavyRifle(player.Faction)) AvatarActor.buildHolsterEquipmentFromClob(player, loadout, log)
player.Slot(3).Equipment = Tool(GlobalDefinitions.AntiVehicularLauncher(player.Faction)) zoneNum
player.Slot(4).Equipment = Tool(GlobalDefinitions.katana) }
.getOrElse {
/** After a client has connected to the server, their account is used to generate a list of characters. player.ExoSuit = ExoSuitType.Standard
* On the character selection screen, each of these characters is made to exist temporarily when one is selected. DefinitionUtil.applyDefaultHolsters(player)
* This "character select screen" is an isolated portion of the client, so it does not have any external constraints. Zones.sanctuaryZoneNumber(avatar.faction)
* Temporary global unique identifiers are assigned to the underlying `Player` objects so that they can be turned into packets. }
/*
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 player
.Holsters() .Holsters()
@ -2071,30 +2164,28 @@ class AvatarActor(
case _ => () case _ => ()
} }
) )
player.GUID = PlanetSideGUID(gen.getAndIncrement) val pguid = player.GUID = PlanetSideGUID(gen.getAndIncrement)
player.Spawn() player.Spawn()
//display character
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ObjectCreateDetailedMessage( ObjectCreateDetailedMessage(
ObjectClass.avatar, ObjectClass.avatar,
player.GUID, pguid,
converter.DetailedConstructorData(player).get converter.DetailedConstructorData(player).get
) )
) )
//display zone
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
CharacterInfoMessage( CharacterInfoMessage(
15, unk = 15,
PlanetSideZoneID(4), PlanetSideZoneID(zoneNum),
avatar.id, avatar.id,
player.GUID, pguid,
finished = false, finished = false,
secondsSinceLastLogin Seconds.secondsBetween(a.lastLogin, now).getSeconds
) )
) )
//do not keep track of GUID's beyond the packet
/** 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 player
.Holsters() .Holsters()
.foreach(holster => .foreach(holster =>
@ -2111,11 +2202,12 @@ class AvatarActor(
) )
player.Invalidate() player.Invalidate()
} }
//finalize list
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
CharacterInfoMessage(15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, 0) CharacterInfoMessage(unk = 15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, secondsSinceLastLogin = 0L)
) )
case Failure(e) =>
case Failure(e) => log.error(e)("db failure") 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)