diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 122657c58..7ce7ef5a8 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -9,9 +9,10 @@ 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.InventoryItem +import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.loadouts.{InfantryLoadout, Loadout} import net.psforever.objects._ +import net.psforever.objects.locker.LockerContainer import net.psforever.packet.game.objectcreate.ObjectClass import net.psforever.packet.game._ import net.psforever.types._ @@ -98,6 +99,9 @@ object AvatarActor { /** Refresh 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 */ + final case class SaveLocker() extends Command + /** Set purchase time for the use of calculating cooldowns */ final case class UpdatePurchaseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command @@ -190,6 +194,7 @@ class AvatarActor( val implantTimers: mutable.Map[Int, Cancellable] = mutable.Map() var staminaRegenTimer: Cancellable = Cancellable.alreadyCancelled var _avatar: Option[Avatar] = None + var saveLockerFunc: () => Unit = storeNewLocker //val topic: ActorRef[Topic.Command[Avatar]] = context.spawnAnonymous(Topic[Avatar]("avatar")) def avatar: Avatar = _avatar.get @@ -347,16 +352,18 @@ class AvatarActor( loadouts <- loadLoadouts() implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatar.id))) certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatar.id))) - } yield (loadouts, implants, certs) + locker <- loadLocker() + } yield (loadouts, implants, certs, locker) result.onComplete { - case Success((loadouts, implants, certs)) => + case Success((loadouts, implants, certs, locker)) => avatar = avatar.copy( loadouts = loadouts, // make sure we always have the base certifications certifications = certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications, - implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None) + implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None), + locker = locker ) staminaRegenTimer.cancel() @@ -372,6 +379,7 @@ class AvatarActor( updateDeployableUIElements( avatar.deployables.UpdateUI() ) + Behaviors.same case AddFirstTimeEvent(event) => @@ -695,6 +703,10 @@ class AvatarActor( } Behaviors.same + case SaveLocker() => + saveLockerFunc() + Behaviors.same + case UpdatePurchaseTime(definition, time) => // TODO save to db var newTimes = avatar.purchaseTimes @@ -938,6 +950,7 @@ class AvatarActor( case (_, PostStop) => staminaRegenTimer.cancel() implantTimers.values.foreach(_.cancel()) + saveLockerFunc() Behaviors.same } } @@ -1216,6 +1229,57 @@ class AvatarActor( } yield () } + def storeNewLocker(): Unit = { + if (_avatar.nonEmpty) { + val items : String = { + val clobber : StringBuilder = new StringBuilder() + avatar.locker.Inventory.Items.foreach { + case InventoryItem(obj, index) => + clobber.append(encodeLoadoutClobFragment(obj, index)) + } + clobber.mkString.drop(1) + } + if (items.nonEmpty) { + saveLockerFunc = storeLocker + import ctx._ + ctx.run( + query[persistence.Locker].insert( + _.avatarId -> lift(avatar.id), + _.items -> lift(items) + ) + ).onComplete { + case Success(_) => + log.debug(s"saving locker contents belonging to ${avatar.name}") + case Failure(e) => + saveLockerFunc = doNotStoreLocker + log.error(e)("db failure") + } + } + } + } + + def doNotStoreLocker(): Unit = { + /* most likely the database encountered an error; don't do anything with it until the restart */ + } + + def storeLocker(): Unit = { + import ctx._ + val items : String = { + val clobber : StringBuilder = new StringBuilder() + avatar.locker.Inventory.Items.foreach { + case InventoryItem(obj, index) => + clobber.append(encodeLoadoutClobFragment(obj, index)) + } + clobber.mkString.drop(1) + } + log.debug(s"saving locker contents belonging to ${avatar.name}") + ctx.run( + query[persistence.Locker] + .filter(_.avatarId == lift(avatar.id)) + .update(_.items -> lift(items)) + ) + } + def encodeLoadoutClobFragment(equipment: Equipment, index: Int): String = { val ammoInfo: String = equipment match { case tool: Tool => @@ -1237,63 +1301,76 @@ class AvatarActor( loadouts.map { loadout => val doll = new Player(Avatar(0, "doll", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) doll.ExoSuit = ExoSuitType(loadout.exosuitId) - - loadout.items.split("/").foreach { - value => - val (objectType, objectIndex, objectId, toolAmmo) = 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)) - } - - objectType match { - case "Tool" => - doll.Slot(objectIndex).Equipment = - Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition]) - case "AmmoBox" => - doll.Slot(objectIndex).Equipment = - AmmoBox(DefinitionUtil.idToDefinition(objectId).asInstanceOf[AmmoBoxDefinition]) - case "ConstructionItem" => - doll.Slot(objectIndex).Equipment = ConstructionItem( - DefinitionUtil.idToDefinition(objectId).asInstanceOf[ConstructionItemDefinition] - ) - case "SimpleItem" => - doll.Slot(objectIndex).Equipment = - SimpleItem(DefinitionUtil.idToDefinition(objectId).asInstanceOf[SimpleItemDefinition]) - case "Kit" => - doll.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 loadout - $name") - } - - toolAmmo foreach { toolAmmo => - toolAmmo.toString.split("_").drop(1).foreach { value => - val (ammoSlots, ammoTypeIndex, ammoBoxDefinition) = value.split("-") match { - case Array(a: String, b: String, c: String) => (a.toInt, b.toInt, c.toInt) - } - doll.Slot(objectIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(ammoSlots).AmmoTypeIndex = - ammoTypeIndex - doll.Slot(objectIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(ammoSlots).Box = - AmmoBox(AmmoBoxDefinition(ammoBoxDefinition)) - } - } - } + buildContainedEquipmentFromClob(doll, loadout.items) val result = (loadout.loadoutNumber, Loadout.Create(doll, loadout.name)) - (0 until 4).foreach(index => { doll.Slot(index).Equipment = None }) doll.Inventory.Clear() - result } } .map { loadouts => (0 until 15).map { index => loadouts.find(_._1 == index).map(_._2) } } } + def loadLocker(): Future[LockerContainer] = { + val locker = Avatar.makeLocker() + import ctx._ + ctx + .run(query[persistence.Locker].filter(_.avatarId == lift(avatar.id))) + .map { entry => + saveLockerFunc = storeLocker + entry.foreach { contents => buildContainedEquipmentFromClob(locker, contents.items) } + } + .map { _ => locker } + } + + def buildContainedEquipmentFromClob(container: Container, clob: String): Unit = { + clob.split("/").foreach { + value => + val (objectType, objectIndex, objectId, toolAmmo) = 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)) + } + + objectType match { + case "Tool" => + container.Slot(objectIndex).Equipment = + Tool(DefinitionUtil.idToDefinition(objectId).asInstanceOf[ToolDefinition]) + case "AmmoBox" => + container.Slot(objectIndex).Equipment = + AmmoBox(DefinitionUtil.idToDefinition(objectId).asInstanceOf[AmmoBoxDefinition]) + 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 locker - $name") + } + + toolAmmo foreach { toolAmmo => + toolAmmo.toString.split("_").drop(1).foreach { value => + val (ammoSlots, ammoTypeIndex, ammoBoxDefinition) = value.split("-") match { + case Array(a: String, b: String, c: String) => (a.toInt, b.toInt, c.toInt) + } + container.Slot(objectIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(ammoSlots).AmmoTypeIndex = + ammoTypeIndex + container.Slot(objectIndex).Equipment.get.asInstanceOf[Tool].AmmoSlots(ammoSlots).Box = + AmmoBox(AmmoBoxDefinition(ammoBoxDefinition)) + } + } + } + } + def defaultStaminaRegen(): Cancellable = { context.system.scheduler.scheduleWithFixedDelay(0.5 seconds, 0.5 seconds)(() => { (session, _avatar) match { diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index a94834e09..183292d48 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -315,10 +315,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con continent.LocalEvents ! Service.Leave() continent.VehicleEvents ! Service.Leave() - // when going from classic -> typed this seems necessary - context.stop(avatarActor) - context.stop(chatActor) - if (avatar != null) { //TODO put any temporary values back into the avatar squadService ! Service.Leave(Some(s"${avatar.faction}")) @@ -333,6 +329,9 @@ 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) } def ValidObject(id: Int): Option[PlanetSideGameObject] = ValidObject(Some(PlanetSideGUID(id))) @@ -6106,6 +6105,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con UnaccessVehicleContainer(v) case o: LockerContainer => UnaccessGenericContainer(o) + avatarActor ! AvatarActor.SaveLocker() case p: Player if p.isBackpack => UnaccessCorpseContainer(p) case _: PlanetSideServerObject with Container => diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index ba3beb1d5..d418a3831 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -65,6 +65,14 @@ object Avatar { GlobalDefinitions.super_medkit -> 20.minutes, GlobalDefinitions.super_staminakit -> 5.minutes // Temporary - Default value is 20 minutes ) + + def makeLocker(): LockerContainer = { + new LockerContainer({ + val inv = new LocallyRegisteredInventory(numbers = 40150 until 40450) // TODO var bad + inv.Resize(30,20) + inv + }) + } } case class Avatar( @@ -84,11 +92,7 @@ case class Avatar( loadouts: Seq[Option[Loadout]] = Seq.fill(15)(None), squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None), implants: Seq[Option[Implant]] = Seq(None, None, None), - locker: LockerContainer = new LockerContainer({ - val inv = new LocallyRegisteredInventory(numbers = 40150 until 40450) // TODO var bad - inv.Resize(30,20) - inv - }), + locker: LockerContainer = Avatar.makeLocker(), deployables: DeployableToolbox = new DeployableToolbox(), // TODO var bad lookingForSquad: Boolean = false, var vehicle: Option[PlanetSideGUID] = None, // TODO var bad diff --git a/src/main/scala/net/psforever/persistence/Locker.scala b/src/main/scala/net/psforever/persistence/Locker.scala index e89d45223..f8fc3b6a1 100644 --- a/src/main/scala/net/psforever/persistence/Locker.scala +++ b/src/main/scala/net/psforever/persistence/Locker.scala @@ -1,3 +1,4 @@ +// Copyright (c) 2020 PSForever package net.psforever.persistence case class Locker(id: Int, avatarId: Int, items: String)