Persistent Vehicle Loadouts (#863)

* persisting vehicle loadouts in between sessions, saving to db and loading from db

* reusing refactored code for clob; initial loadout without empty entries; reverting method of stopping session child actors
This commit is contained in:
Fate-JH 2021-06-15 18:32:12 -04:00 committed by GitHub
parent e2a81d68c1
commit 7c64b23e1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 307 additions and 69 deletions

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS "vehicleloadout" (
"id" SERIAL PRIMARY KEY NOT NULL,
"avatar_id" INT NOT NULL REFERENCES avatar (id),
"loadout_number" INT NOT NULL,
"name" VARCHAR(36) NOT NULL,
"vehicle" SMALLINT NOT NULL,
"items" TEXT NOT NULL
);

View file

@ -8,9 +8,10 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import net.psforever.objects.avatar._
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.definition._
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout}
import net.psforever.objects.inventory.Container
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
import net.psforever.objects._
import net.psforever.objects.locker.LockerContainer
import net.psforever.packet.game.objectcreate.ObjectClass
@ -96,7 +97,10 @@ object AvatarActor {
/** Delete a loadout */
final case class DeleteLoadout(player: Player, loadoutType: LoadoutType.Value, number: Int) extends Command
/** Refresh the client's loadouts */
/** Refresh the client's loadouts, excluding empty entries */
final case class InitialRefreshLoadouts() extends Command
/** Refresh all of the client's loadouts */
final case class RefreshLoadouts() extends Command
/** Take all the entries in the player's locker and write it to the database */
@ -349,7 +353,7 @@ class AvatarActor(
.filter(_.id == lift(avatar.id))
.update(_.lastLogin -> lift(LocalDateTime.now()))
)
loadouts <- loadLoadouts()
loadouts <- initializeAllLoadouts()
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatar.id)))
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatar.id)))
locker <- loadLocker()
@ -374,12 +378,7 @@ class AvatarActor(
Behaviors.same
case ReplaceAvatar(newAvatar) =>
avatar = newAvatar
avatar.deployables.UpdateMaxCounts(avatar.certifications)
updateDeployableUIElements(
avatar.deployables.UpdateUI()
)
replaceAvatar(newAvatar)
Behaviors.same
case AddFirstTimeEvent(event) =>
@ -434,7 +433,7 @@ class AvatarActor(
sessionActor ! SessionActor.SendResponse(
PlanetsideAttributeMessage(session.get.player.GUID, 24, certification.value)
)
context.self ! ReplaceAvatar(
replaceAvatar(
avatar.copy(certifications = avatar.certifications.diff(replace) + certification)
)
sessionActor ! SessionActor.SendResponse(
@ -487,7 +486,7 @@ class AvatarActor(
)
case Success(certs) =>
val player = session.get.player
context.self ! ReplaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs)))
replaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs)))
certs.foreach { cert =>
sessionActor ! SessionActor.SendResponse(
PlanetsideAttributeMessage(player.GUID, 25, cert.value)
@ -548,7 +547,7 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
context.self ! ReplaceAvatar(avatar.copy(certifications = certifications))
replaceAvatar(avatar.copy(certifications = certifications))
case Failure(exception) =>
log.error(exception)("db failure")
}
@ -584,9 +583,7 @@ class AvatarActor(
.run(query[persistence.Implant].insert(_.name -> lift(definition.Name), _.avatarId -> lift(avatar.id)))
.onComplete {
case Success(_) =>
context.self ! ReplaceAvatar(
avatar.copy(implants = avatar.implants.updated(_index, Some(Implant(definition))))
)
replaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, Some(Implant(definition)))))
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(
session.get.player.GUID,
@ -627,7 +624,7 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
context.self ! ReplaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None)))
replaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None)))
sessionActor ! SessionActor.SendResponse(
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, _index, 0)
)
@ -647,59 +644,69 @@ class AvatarActor(
Behaviors.same
case SaveLoadout(player, loadoutType, label, number) =>
log.info(s"${player.Name} wishes to save a favorite $loadoutType loadout as #${number+1}")
val name = label.getOrElse(s"missing_loadout_${number + 1}")
loadoutType match {
val (lineNo, result): (Int, Future[Loadout]) = loadoutType match {
case LoadoutType.Infantry =>
storeLoadout(player, name, number).onComplete {
case Success(_) =>
loadLoadouts().onComplete {
case Success(loadouts) =>
context.self ! ReplaceAvatar(avatar.copy(loadouts = loadouts))
context.self ! RefreshLoadouts()
case Failure(exception) => log.error(exception)("db failure")
}
case Failure(exception) => log.error(exception)("db failure")
}
(
number,
storeLoadout(player, name, number)
)
case LoadoutType.Vehicle =>
// TODO
// storeLoadout(player, name, 10 + number)
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, name))
(
number + 10,
player.Zone.GUID(avatar.vehicle) match {
case Some(vehicle: Vehicle) =>
storeVehicleLoadout(player, name, number, vehicle)
case _ =>
throwLoadoutFailure(s"no owned vehicle found for ${player.Name}")
}
)
}
result.onComplete {
case Success(loadout) =>
replaceAvatar(avatar.copy(loadouts = avatar.loadouts.updated(lineNo, Some(loadout))))
refreshLoadout(lineNo)
case Failure(exception) =>
log.error(exception)("db failure (?)")
}
Behaviors.same
case DeleteLoadout(player, loadoutType, number) =>
log.info(s"${player.Name} wishes to delete a favorite $loadoutType loadout - #${number+1}")
import ctx._
ctx
.run(
query[persistence.Loadout]
.filter(_.avatarId == lift(avatar.id))
.filter(_.loadoutNumber == lift(number))
.delete
)
.onComplete {
case Success(_) =>
context.self ! ReplaceAvatar(avatar.copy(loadouts = avatar.loadouts.updated(number, None)))
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
case Failure(exception) =>
log.error(exception)("db failure")
}
Behaviors.same
case RefreshLoadouts() =>
avatar.loadouts.zipWithIndex.foreach {
case (Some(loadout: InfantryLoadout), index) =>
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
LoadoutType.Infantry,
session.get.player.GUID,
index,
loadout.label,
InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)
val (lineNo, result) = loadoutType match {
case LoadoutType.Infantry if avatar.loadouts(number).nonEmpty =>
(
number,
ctx.run(
query[persistence.Loadout]
.filter(_.avatarId == lift(avatar.id))
.filter(_.loadoutNumber == lift(number))
.delete
)
)
case _ => ;
case LoadoutType.Vehicle if avatar.loadouts(number + 10).nonEmpty =>
val lineNo = number + 10
(
lineNo,
ctx.run(
query[persistence.Vehicleloadout]
.filter(_.avatarId == lift(avatar.id))
.filter(_.loadoutNumber == lift(number))
.delete
)
)
case _ =>
(number, throwLoadoutFailure("unhandled loadout type or no loadout"))
}
result.onComplete {
case Success(_) =>
replaceAvatar(avatar.copy(loadouts = avatar.loadouts.updated(lineNo, None)))
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
case Failure(exception) =>
log.error(exception)("db failure (?)")
}
Behaviors.same
@ -707,6 +714,14 @@ class AvatarActor(
saveLockerFunc()
Behaviors.same
case InitialRefreshLoadouts() =>
refreshLoadouts(avatar.loadouts.zipWithIndex)
Behaviors.same
case RefreshLoadouts() =>
refreshLoadouts(avatar.loadouts.zipWithIndex.collect { case out @ (Some(_), _) => out })
Behaviors.same
case UpdatePurchaseTime(definition, time) =>
// TODO save to db
var newTimes = avatar.purchaseTimes
@ -955,6 +970,22 @@ class AvatarActor(
}
}
def throwLoadoutFailure(msg: String): Future[Loadout] = {
throwLoadoutFailure(new Exception(msg))
}
def throwLoadoutFailure(ex: Throwable): Future[Loadout] = {
Future.failed(ex).asInstanceOf[Future[Loadout]]
}
def replaceAvatar(newAvatar: Avatar): Unit = {
avatar = newAvatar
avatar.deployables.UpdateMaxCounts(avatar.certifications)
updateDeployableUIElements(
avatar.deployables.UpdateUI()
)
}
def setCosmetics(cosmetics: Set[Cosmetic]): Future[Unit] = {
val p = Promise[Unit]()
@ -1183,9 +1214,8 @@ class AvatarActor(
}
}
def storeLoadout(owner: Player, label: String, line: Int): Future[Unit] = {
def storeLoadout(owner: Player, label: String, line: Int): Future[Loadout] = {
import ctx._
val items: String = {
val clobber: StringBuilder = new StringBuilder()
//encode holsters
@ -1226,7 +1256,53 @@ class AvatarActor(
)
)
}
} yield ()
} yield Loadout.Create(owner, label)
}
def storeVehicleLoadout(owner: Player, label: String, line: Int, vehicle: Vehicle): Future[Loadout] = {
import ctx._
val items: String = {
val clobber: StringBuilder = new StringBuilder()
//encode holsters
vehicle
.Weapons
.collect {
case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty =>
clobber.append(encodeLoadoutClobFragment(slot.Equipment.get, index))
}
//encode inventory
vehicle.Inventory.Items.foreach {
case InventoryItem(obj, index) =>
clobber.append(encodeLoadoutClobFragment(obj, index))
}
clobber.mkString.drop(1)
}
for {
loadouts <- ctx.run(
query[persistence.Vehicleloadout]
.filter(_.avatarId == lift(owner.CharId))
.filter(_.loadoutNumber == lift(line))
)
_ <- loadouts.headOption match {
case Some(loadout) =>
ctx.run(
query[persistence.Vehicleloadout]
.filter(_.id == lift(loadout.id))
.update(_.name -> lift(label), _.vehicle -> lift(vehicle.Definition.ObjectId), _.items -> lift(items))
)
case None =>
ctx.run(
query[persistence.Vehicleloadout].insert(
_.avatarId -> lift(owner.avatar.id),
_.loadoutNumber -> lift(line),
_.name -> lift(label),
_.vehicle -> lift(vehicle.Definition.ObjectId),
_.items -> lift(items)
)
)
}
} yield Loadout.Create(vehicle, label)
}
def storeNewLocker(): Unit = {
@ -1293,6 +1369,19 @@ class AvatarActor(
s"/${equipment.getClass.getSimpleName},$index,${equipment.Definition.ObjectId},$ammoInfo"
}
def initializeAllLoadouts(): Future[Seq[Option[Loadout]]] = {
for {
infantry <- loadLoadouts().andThen {
case out @ Success(_) => out
case Failure(_) => Future(Array.fill[Option[Loadout]](10)(None).toSeq)
}
vehicles <- loadVehicleLoadouts().andThen {
case out @ Success(_) => out
case Failure(_) => Future(Array.fill[Option[Loadout]](5)(None).toSeq)
}
} yield infantry ++ vehicles
}
def loadLoadouts(): Future[Seq[Option[Loadout]]] = {
import ctx._
ctx
@ -1311,7 +1400,102 @@ class AvatarActor(
result
}
}
.map { loadouts => (0 until 15).map { index => loadouts.find(_._1 == index).map(_._2) } }
.map { loadouts => (0 until 10).map { index => loadouts.find(_._1 == index).map(_._2) } }
}
def loadVehicleLoadouts(): Future[Seq[Option[Loadout]]] = {
import ctx._
ctx
.run(query[persistence.Vehicleloadout].filter(_.avatarId == lift(avatar.id)))
.map { loadouts =>
loadouts.map { loadout =>
val toy = new Vehicle(DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition])
buildContainedEquipmentFromClob(toy, loadout.items)
val result = (loadout.loadoutNumber, Loadout.Create(toy, loadout.name))
toy.Weapons.values.foreach(slot => {
slot.Equipment = None
})
toy.Inventory.Clear()
result
}
}
.map { loadouts => (0 until 5).map { index => loadouts.find(_._1 == index).map(_._2) } }
}
def refreshLoadouts(loadouts: Iterable[(Option[Loadout], Int)]): Unit = {
loadouts.map {
case (Some(loadout: InfantryLoadout), index) =>
FavoritesMessage(
LoadoutType.Infantry,
session.get.player.GUID,
index,
loadout.label,
InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)
)
case (Some(loadout: VehicleLoadout), index) =>
FavoritesMessage(
LoadoutType.Vehicle,
session.get.player.GUID,
index - 10,
loadout.label,
0
)
case (_, index) =>
val (mtype, lineNo) = if (index < 10) {
(LoadoutType.Infantry, index)
} else {
(LoadoutType.Vehicle, index - 10)
}
FavoritesMessage(
mtype,
session.get.player.GUID,
lineNo,
"",
0
)
}.foreach { sessionActor ! SessionActor.SendResponse(_) }
}
def refreshLoadout(line: Int): Unit = {
avatar.loadouts.lift(line) match {
case Some(Some(loadout: InfantryLoadout)) =>
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
LoadoutType.Infantry,
session.get.player.GUID,
line,
loadout.label,
InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)
)
)
case Some(Some(loadout: VehicleLoadout)) =>
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
LoadoutType.Vehicle,
session.get.player.GUID,
line - 10,
loadout.label,
0
)
)
case Some(None) =>
val (mtype, lineNo) = if (line < 10) {
(LoadoutType.Infantry, line)
} else {
(LoadoutType.Vehicle, line - 10)
}
sessionActor ! SessionActor.SendResponse(
FavoritesMessage(
mtype,
session.get.player.GUID,
lineNo,
"",
0
)
)
case _ => ;
}
}
def loadLocker(): Future[LockerContainer] = {

View file

@ -332,8 +332,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}
}
// when going from classic -> typed this seems necessary
akka.actor.TypedActor(context.system).poisonPill(avatarActor)
akka.actor.TypedActor(context.system).poisonPill(chatActor)
context.stop(avatarActor)
context.stop(chatActor)
}
def ValidObject(id: Int): Option[PlanetSideGameObject] = ValidObject(Some(PlanetSideGUID(id)))
@ -3078,7 +3078,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.Medkit))
sendResponse(ChangeShortcutBankMessage(guid, 0))
//Favorites lists
avatarActor ! AvatarActor.RefreshLoadouts()
avatarActor ! AvatarActor.InitialRefreshLoadouts()
sendResponse(
SetChatFilterMessage(ChatChannel.Platoon, false, ChatChannel.values.toList)
@ -5105,7 +5105,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case msg @ FavoritesRequest(player_guid, loadoutType, action, line, label) =>
CancelZoningProcessWithDescriptiveReason("cancel_use")
log.info(s"${player.Name} wishes to load a saved favorite loadout")
action match {
case FavoritesAction.Save => avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line)
case FavoritesAction.Delete => avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line)

View file

@ -0,0 +1,4 @@
// Copyright (c) 2021 PSForever
package net.psforever.persistence
case class Vehicleloadout(id: Int, avatarId: Int, loadoutNumber: Int, name: String, vehicle: Int, items: String)

View file

@ -218,7 +218,50 @@ object DefinitionUtil {
case 39 => advanced_ace
case 148 => boomer
case 149 => boomer_trigger
case _ => frag_grenade
//vehicles
case 67 => apc_tr
case 66 => apc_nc
case 68 => apc_vs
case 46 => ams
case 60 => ant
//case 83 => aphelion_flight
//case 84 => aphelion_gunner
case 118 => aurora
case 135 => battlewagon
//case 199 => colossus_flight
//case 200 => colossus_gunner
case 258 => droppod
case 259 => dropship
case 294 => flail
case 335 => fury
case 338 => galaxy_gunship
case 432 => liberator
case 441 => lightgunship
case 446 => lightning
case 459 => lodestar
case 470 => magrider
case 572 => mosquito
case 532 => mediumtransport
case 608 => orbital_shuttle
//case 642 => peregrine_flight
//case 643 => peregrine_gunner
case 671 => phantasm
case 697 => prowler
case 707 => quadassault
case 710 => quadstealth
case 741 => router
case 784 => skyguard
case 847 => switchblade
case 862 => threemanheavybuggy
case 865 => thunderer
case 896 => two_man_assault_buggy
case 898 => twomanheavybuggy
case 900 => twomanhoverbuggy
case 923 => vanguard
case 986 => vulture
case 997 => wasp
//default
case _ => throw new IllegalArgumentException(s"you can not build $id")
}
}