mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-13 15:34:42 +00:00
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:
parent
8738a42ca0
commit
8afe7fa248
2 changed files with 227 additions and 129 deletions
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue