Locker Fix (#1005)

* movement of items between locker and player repaired

* fixed saving and loading of lockers contents
This commit is contained in:
Fate-JH 2022-07-24 23:53:17 -04:00 committed by GitHub
parent 9d2be17c1c
commit ed705d2cb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 325 additions and 269 deletions

View file

@ -2,7 +2,6 @@
package net.psforever.actors.session package net.psforever.actors.session
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import akka.actor.Cancellable import akka.actor.Cancellable
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy} import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
@ -22,7 +21,7 @@ import net.psforever.packet.game._
import net.psforever.types._ import net.psforever.types._
import net.psforever.util.Database._ import net.psforever.util.Database._
import net.psforever.persistence import net.psforever.persistence
import net.psforever.util.{Config, DefinitionUtil} import net.psforever.util.{Config, Database, DefinitionUtil}
import org.joda.time.{LocalDateTime, Seconds} import org.joda.time.{LocalDateTime, Seconds}
import net.psforever.services.ServiceManager import net.psforever.services.ServiceManager
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -32,6 +31,7 @@ import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
import scala.concurrent.duration._ import scala.concurrent.duration._
import net.psforever.services.Service import net.psforever.services.Service
import org.log4s.Logger
object AvatarActor { object AvatarActor {
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] = def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
@ -191,6 +191,122 @@ object AvatarActor {
final case class AvatarLoginResponse(avatar: Avatar) final case class AvatarLoginResponse(avatar: Avatar)
def buildContainedEquipmentFromClob(container: Container, clob: String, log: org.log4s.Logger): Unit = {
clob.split("/").filter(_.trim.nonEmpty).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))
case _ =>
log.warn(s"ignoring invalid item string: '$value'")
return
}
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.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 resolvePurchaseTimeName(faction: PlanetSideEmpire.Value, item: BasicDefinition): (BasicDefinition, String) = {
val factionName: String = faction.toString.toLowerCase
val name = item match {
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.nchev_scattercannon |
GlobalDefinitions.vshev_quasar =>
s"${factionName}hev_antipersonnel"
case GlobalDefinitions.trhev_pounder | GlobalDefinitions.nchev_falcon | GlobalDefinitions.vshev_comet =>
s"${factionName}hev_antivehicular"
case GlobalDefinitions.trhev_burster | GlobalDefinitions.nchev_sparrow | GlobalDefinitions.vshev_starfire =>
s"${factionName}hev_antiaircraft"
case _ =>
item.Name
}
(item, name)
}
def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = {
val (definition, name) = pair
if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) {
val faction = name.take(2)
(if (faction.equals("nc")) {
Seq(GlobalDefinitions.nchev_scattercannon, GlobalDefinitions.nchev_falcon, GlobalDefinitions.nchev_sparrow)
} else if (faction.equals("vs")) {
Seq(GlobalDefinitions.vshev_quasar, GlobalDefinitions.vshev_comet, GlobalDefinitions.vshev_starfire)
} else {
Seq(GlobalDefinitions.trhev_dualcycler, GlobalDefinitions.trhev_pounder, GlobalDefinitions.trhev_burster)
}).zip(
Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft")
)
} else {
definition match {
case vdef: VehicleDefinition if GlobalDefinitions.isBattleFrameFlightVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val gunner = bframe + "_gunner"
Seq((DefinitionUtil.fromString(gunner), gunner), (vdef, name))
case vdef: VehicleDefinition if GlobalDefinitions.isBattleFrameGunnerVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val flight = bframe + "_flight"
Seq((vdef, name), (DefinitionUtil.fromString(flight), flight))
case _ =>
Seq(pair)
}
}
}
def encodeLockerClob(container: Container): String = {
val clobber: StringBuilder = new StringBuilder()
container.Inventory.Items.foreach {
case InventoryItem(obj, index) =>
clobber.append(encodeLoadoutClobFragment(obj, index))
}
clobber.mkString.drop(1)
}
def encodeLoadoutClobFragment(equipment: Equipment, index: Int): String = {
val ammoInfo: String = equipment match {
case tool: Tool =>
tool.AmmoSlots.zipWithIndex.collect {
case (ammoSlot, index2) if ammoSlot.AmmoTypeIndex != 0 =>
s"_$index2-${ammoSlot.AmmoTypeIndex}-${ammoSlot.AmmoType.id}"
}.mkString
case _ =>
""
}
s"/${equipment.getClass.getSimpleName},$index,${equipment.Definition.ObjectId},$ammoInfo"
}
def changeRibbons(ribbons: RibbonBars, ribbon: MeritCommendation.Value, bar: RibbonBarSlot.Value): RibbonBars = { def changeRibbons(ribbons: RibbonBars, ribbon: MeritCommendation.Value, bar: RibbonBarSlot.Value): RibbonBars = {
bar match { bar match {
case RibbonBarSlot.Top => ribbons.copy(upper = ribbon) case RibbonBarSlot.Top => ribbons.copy(upper = ribbon)
@ -368,8 +484,7 @@ class AvatarActor(
val result = for { val result = for {
_ <- ctx.run( _ <- ctx.run(
query[persistence.Avatar] query[persistence.Avatar].filter(_.id == lift(avatar.id))
.filter(_.id == lift(avatar.id))
.update(_.lastLogin -> lift(LocalDateTime.now())) .update(_.lastLogin -> lift(LocalDateTime.now()))
) )
loadouts <- initializeAllLoadouts() loadouts <- initializeAllLoadouts()
@ -422,14 +537,13 @@ class AvatarActor(
val replace = certification.replaces.intersect(avatar.certifications) val replace = certification.replaces.intersect(avatar.certifications)
Future Future
.sequence(replace.map(cert => { .sequence(replace.map(cert => {
ctx ctx.run(
.run( query[persistence.Certification]
query[persistence.Certification] .filter(_.avatarId == lift(avatar.id))
.filter(_.avatarId == lift(avatar.id)) .filter(_.id == lift(cert.value))
.filter(_.id == lift(cert.value)) .delete
.delete )
) .map(_ => cert)
.map(_ => cert)
})) }))
.onComplete { .onComplete {
case Failure(exception) => case Failure(exception) =>
@ -443,11 +557,10 @@ class AvatarActor(
PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value) PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
) )
} }
ctx ctx.run(
.run( query[persistence.Certification]
query[persistence.Certification] .insert(_.id -> lift(certification.value), _.avatarId -> lift(avatar.id))
.insert(_.id -> lift(certification.value), _.avatarId -> lift(avatar.id)) )
)
.onComplete { .onComplete {
case Failure(exception) => case Failure(exception) =>
log.error(exception)("db failure") log.error(exception)("db failure")
@ -494,14 +607,13 @@ class AvatarActor(
avatar.certifications avatar.certifications
.intersect(requiredByCert) .intersect(requiredByCert)
.map(cert => { .map(cert => {
ctx ctx.run(
.run( query[persistence.Certification]
query[persistence.Certification] .filter(_.avatarId == lift(avatar.id))
.filter(_.avatarId == lift(avatar.id)) .filter(_.id == lift(cert.value))
.filter(_.id == lift(cert.value)) .delete
.delete )
) .map(_ => cert)
.map(_ => cert)
}) })
) )
.onComplete { .onComplete {
@ -551,13 +663,12 @@ class AvatarActor(
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value) PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value)
) )
ctx ctx.run(
.run( query[persistence.Certification]
query[persistence.Certification] .filter(_.avatarId == lift(avatar.id))
.filter(_.avatarId == lift(avatar.id)) .filter(_.id == lift(cert.value))
.filter(_.id == lift(cert.value)) .delete
.delete )
)
}) ++ }) ++
certifications certifications
.diff(avatar.certifications) .diff(avatar.certifications)
@ -642,25 +753,24 @@ class AvatarActor(
index match { index match {
case Some(_index) => case Some(_index) =>
import ctx._ import ctx._
ctx ctx.run(
.run( query[persistence.Implant]
query[persistence.Implant] .filter(_.name == lift(definition.Name))
.filter(_.name == lift(definition.Name)) .filter(_.avatarId == lift(avatar.id))
.filter(_.avatarId == lift(avatar.id)) .delete
.delete )
) .onComplete {
.onComplete { case Success(_) =>
case Success(_) => replaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None)))
replaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None))) sessionActor ! SessionActor.SendResponse(
sessionActor ! SessionActor.SendResponse( AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, _index, 0)
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, _index, 0) )
) sessionActor ! SessionActor.SendResponse(
sessionActor ! SessionActor.SendResponse( ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true) )
) context.self ! ResetImplants()
context.self ! ResetImplants() case Failure(exception) => log.error(exception)("db failure")
case Failure(exception) => log.error(exception)("db failure") }
}
case None => case None =>
log.warn("attempted to sell implant but could not find slot") log.warn("attempted to sell implant but could not find slot")
@ -772,13 +882,17 @@ class AvatarActor(
case UpdatePurchaseTime(definition, time) => case UpdatePurchaseTime(definition, time) =>
// TODO save to db // TODO save to db
var newTimes = avatar.purchaseTimes var newTimes = avatar.purchaseTimes
resolveSharedPurchaseTimeNames(resolvePurchaseTimeName(avatar.faction, definition)).foreach { AvatarActor.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition)).foreach {
case (item, name) => case (item, name) =>
Avatar.purchaseCooldowns.get(item) match { Avatar.purchaseCooldowns.get(item) match {
case Some(cooldown) => case Some(cooldown) =>
//only send for items with cooldowns //only send for items with cooldowns
newTimes = newTimes.updated(name, time) newTimes = newTimes.updated(name, time)
updatePurchaseTimer(name, cooldown.toSeconds, unk1 = true) updatePurchaseTimer(
name,
cooldown.toSeconds,
DefinitionUtil.fromString(name).isInstanceOf[VehicleDefinition]
)
case _ => ; case _ => ;
} }
} }
@ -976,13 +1090,12 @@ class AvatarActor(
val implants = avatar.implants.zipWithIndex.map { val implants = avatar.implants.zipWithIndex.map {
case (implant, index) => case (implant, index) =>
if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) { if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) {
ctx ctx.run(
.run( query[persistence.Implant]
query[persistence.Implant] .filter(_.name == lift(implant.get.definition.Name))
.filter(_.name == lift(implant.get.definition.Name)) .filter(_.avatarId == lift(avatar.id))
.filter(_.avatarId == lift(avatar.id)) .delete
.delete )
)
.onComplete { .onComplete {
case Success(_) => case Success(_) =>
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
@ -1071,12 +1184,11 @@ class AvatarActor(
val p = Promise[Unit]() val p = Promise[Unit]()
import ctx._ import ctx._
ctx ctx.run(
.run( query[persistence.Avatar]
query[persistence.Avatar] .filter(_.id == lift(avatar.id))
.filter(_.id == lift(avatar.id)) .update(_.cosmetics -> lift(Some(Cosmetic.valuesToObjectCreateValue(cosmetics)): Option[Int]))
.update(_.cosmetics -> lift(Some(Cosmetic.valuesToObjectCreateValue(cosmetics)): Option[Int])) )
)
.onComplete { .onComplete {
case Success(_) => case Success(_) =>
avatarCopy(avatar.copy(cosmetics = Some(cosmetics))) avatarCopy(avatar.copy(cosmetics = Some(cosmetics)))
@ -1089,7 +1201,6 @@ class AvatarActor(
case Failure(exception) => case Failure(exception) =>
p.failure(exception) p.failure(exception)
} }
p.future p.future
} }
@ -1343,9 +1454,7 @@ class AvatarActor(
} }
) )
player.GUID = PlanetSideGUID(gen.getAndIncrement) player.GUID = PlanetSideGUID(gen.getAndIncrement)
player.Spawn() player.Spawn()
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ObjectCreateDetailedMessage( ObjectCreateDetailedMessage(
ObjectClass.avatar, ObjectClass.avatar,
@ -1353,7 +1462,6 @@ class AvatarActor(
converter.DetailedConstructorData(player).get converter.DetailedConstructorData(player).get
) )
) )
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
CharacterInfoMessage( CharacterInfoMessage(
15, 15,
@ -1364,7 +1472,6 @@ class AvatarActor(
secondsSinceLastLogin secondsSinceLastLogin
) )
) )
/** After the user has selected a character to load from the "character select screen," /** 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. * 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. * Characters that were not selected may be destroyed along with their temporary GUIDs.
@ -1385,7 +1492,6 @@ class AvatarActor(
) )
player.Invalidate() player.Invalidate()
} }
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
CharacterInfoMessage(15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, 0) CharacterInfoMessage(15, PlanetSideZoneID(0), 0, PlanetSideGUID(0), finished = true, 0)
) )
@ -1404,16 +1510,15 @@ class AvatarActor(
.zipWithIndex .zipWithIndex
.collect { .collect {
case (slot, index) if slot.Equipment.nonEmpty => case (slot, index) if slot.Equipment.nonEmpty =>
clobber.append(encodeLoadoutClobFragment(slot.Equipment.get, index)) clobber.append(AvatarActor.encodeLoadoutClobFragment(slot.Equipment.get, index))
} }
//encode inventory //encode inventory
owner.Inventory.Items.foreach { owner.Inventory.Items.foreach {
case InventoryItem(obj, index) => case InventoryItem(obj, index) =>
clobber.append(encodeLoadoutClobFragment(obj, index)) clobber.append(AvatarActor.encodeLoadoutClobFragment(obj, index))
} }
clobber.mkString.drop(1) clobber.mkString.drop(1)
} }
for { for {
loadouts <- ctx.run( loadouts <- ctx.run(
query[persistence.Loadout].filter(_.avatarId == lift(owner.CharId)).filter(_.loadoutNumber == lift(line)) query[persistence.Loadout].filter(_.avatarId == lift(owner.CharId)).filter(_.loadoutNumber == lift(line))
@ -1447,12 +1552,12 @@ class AvatarActor(
vehicle.Weapons vehicle.Weapons
.collect { .collect {
case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty => case (index, slot: EquipmentSlot) if slot.Equipment.nonEmpty =>
clobber.append(encodeLoadoutClobFragment(slot.Equipment.get, index)) clobber.append(AvatarActor.encodeLoadoutClobFragment(slot.Equipment.get, index))
} }
//encode inventory //encode inventory
vehicle.Inventory.Items.foreach { vehicle.Inventory.Items.foreach {
case InventoryItem(obj, index) => case InventoryItem(obj, index) =>
clobber.append(encodeLoadoutClobFragment(obj, index)) clobber.append(AvatarActor.encodeLoadoutClobFragment(obj, index))
} }
clobber.mkString.drop(1) clobber.mkString.drop(1)
} }
@ -1486,32 +1591,15 @@ class AvatarActor(
def storeNewLocker(): Unit = { def storeNewLocker(): Unit = {
if (_avatar.nonEmpty) { if (_avatar.nonEmpty) {
val items: String = { pushLockerClobToDataBase(AvatarActor.encodeLockerClob(avatar.locker))
val clobber: StringBuilder = new StringBuilder() .onComplete {
avatar.locker.Inventory.Items.foreach { case Success(_) =>
case InventoryItem(obj, index) => saveLockerFunc = storeLocker
clobber.append(encodeLoadoutClobFragment(obj, index)) log.debug(s"saving locker contents belonging to ${avatar.name}")
case Failure(e) =>
saveLockerFunc = doNotStoreLocker
log.error(e)("db failure")
} }
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")
}
}
} }
} }
@ -1520,16 +1608,12 @@ class AvatarActor(
} }
def storeLocker(): Unit = { 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}") log.debug(s"saving locker contents belonging to ${avatar.name}")
pushLockerClobToDataBase(AvatarActor.encodeLockerClob(avatar.locker))
}
def pushLockerClobToDataBase(items: String): Database.ctx.Result[Database.ctx.RunActionResult] = {
import ctx._
ctx.run( ctx.run(
query[persistence.Locker] query[persistence.Locker]
.filter(_.avatarId == lift(avatar.id)) .filter(_.avatarId == lift(avatar.id))
@ -1537,19 +1621,6 @@ class AvatarActor(
) )
} }
def encodeLoadoutClobFragment(equipment: Equipment, index: Int): String = {
val ammoInfo: String = equipment match {
case tool: Tool =>
tool.AmmoSlots.zipWithIndex.collect {
case (ammoSlot, index2) if ammoSlot.AmmoTypeIndex != 0 =>
s"_$index2-${ammoSlot.AmmoTypeIndex}-${ammoSlot.AmmoType.id}"
}.mkString
case _ =>
""
}
s"/${equipment.getClass.getSimpleName},$index,${equipment.Definition.ObjectId},$ammoInfo"
}
def initializeAllLoadouts(): Future[Seq[Option[Loadout]]] = { def initializeAllLoadouts(): Future[Seq[Option[Loadout]]] = {
for { for {
infantry <- loadLoadouts().andThen { infantry <- loadLoadouts().andThen {
@ -1571,7 +1642,7 @@ class AvatarActor(
loadouts.map { loadout => loadouts.map { loadout =>
val doll = new Player(Avatar(0, "doll", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) val doll = new Player(Avatar(0, "doll", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute))
doll.ExoSuit = ExoSuitType(loadout.exosuitId) doll.ExoSuit = ExoSuitType(loadout.exosuitId)
buildContainedEquipmentFromClob(doll, loadout.items) AvatarActor.buildContainedEquipmentFromClob(doll, loadout.items, log)
val result = (loadout.loadoutNumber, Loadout.Create(doll, loadout.name)) val result = (loadout.loadoutNumber, Loadout.Create(doll, loadout.name))
(0 until 4).foreach(index => { (0 until 4).foreach(index => {
@ -1592,7 +1663,7 @@ class AvatarActor(
loadouts.map { loadout => loadouts.map { loadout =>
val definition = DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition] val definition = DefinitionUtil.idToDefinition(loadout.vehicle).asInstanceOf[VehicleDefinition]
val toy = new Vehicle(definition) val toy = new Vehicle(definition)
buildContainedEquipmentFromClob(toy, loadout.items) AvatarActor.buildContainedEquipmentFromClob(toy, loadout.items, log)
val result = (loadout.loadoutNumber, Loadout.Create(toy, loadout.name)) val result = (loadout.loadoutNumber, Loadout.Create(toy, loadout.name))
toy.Weapons.values.foreach(slot => { toy.Weapons.values.foreach(slot => {
@ -1699,61 +1770,34 @@ class AvatarActor(
def loadLocker(): Future[LockerContainer] = { def loadLocker(): Future[LockerContainer] = {
val locker = Avatar.makeLocker() val locker = Avatar.makeLocker()
var notLoaded: Boolean = false
import ctx._ import ctx._
ctx val out = ctx.run(query[persistence.Locker]
.run(query[persistence.Locker].filter(_.avatarId == lift(avatar.id))) .filter(_.avatarId == lift(avatar.id)))
.map { entry => .map { entry =>
saveLockerFunc = storeLocker notLoaded = false
entry.foreach { contents => buildContainedEquipmentFromClob(locker, contents.items) } entry.foreach { contents => AvatarActor.buildContainedEquipmentFromClob(locker, contents.items, log) }
} }
.map { _ => locker } .map { _ => locker }
} out.onComplete {
case Success(_) =>
def buildContainedEquipmentFromClob(container: Container, clob: String): Unit = { saveLockerFunc = storeLocker
clob.split("/").filter(_.trim.nonEmpty).foreach { value => case Failure(_) =>
val (objectType, objectIndex, objectId, toolAmmo) = value.split(",") match { notLoaded = true
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))
case _ =>
log.warn(s"ignoring invalid item string: '$value'")
return
}
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.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))
}
}
} }
if (notLoaded) {
//default empty locker
ctx.run(query[persistence.Locker]
.insert(_.avatarId -> lift(avatar.id), _.items -> lift("")))
.onComplete {
case Success(_) =>
saveLockerFunc = storeLocker
case Failure(e) =>
saveLockerFunc = doNotStoreLocker
log.error(e)("db failure")
}
}
out
} }
def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = { def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = {
@ -1781,53 +1825,6 @@ class AvatarActor(
}) })
} }
def resolvePurchaseTimeName(faction: PlanetSideEmpire.Value, item: BasicDefinition): (BasicDefinition, String) = {
val factionName: String = faction.toString.toLowerCase
val name = item match {
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.nchev_scattercannon |
GlobalDefinitions.vshev_quasar =>
s"${factionName}hev_antipersonnel"
case GlobalDefinitions.trhev_pounder | GlobalDefinitions.nchev_falcon | GlobalDefinitions.vshev_comet =>
s"${factionName}hev_antivehicular"
case GlobalDefinitions.trhev_burster | GlobalDefinitions.nchev_sparrow | GlobalDefinitions.vshev_starfire =>
s"${factionName}hev_antiaircraft"
case _ =>
item.Name
}
(item, name)
}
def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = {
val (definition, name) = pair
if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) {
val faction = name.take(2)
(if (faction.equals("nc")) {
Seq(GlobalDefinitions.nchev_scattercannon, GlobalDefinitions.nchev_falcon, GlobalDefinitions.nchev_sparrow)
} else if (faction.equals("vs")) {
Seq(GlobalDefinitions.vshev_quasar, GlobalDefinitions.vshev_comet, GlobalDefinitions.vshev_starfire)
} else {
Seq(GlobalDefinitions.trhev_dualcycler, GlobalDefinitions.trhev_pounder, GlobalDefinitions.trhev_burster)
}).zip(
Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft")
)
} else {
definition match {
case vdef: VehicleDefinition if GlobalDefinitions.isBattleFrameFlightVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val gunner = bframe + "_gunner"
Seq((DefinitionUtil.fromString(gunner), gunner), (vdef, name))
case vdef: VehicleDefinition if GlobalDefinitions.isBattleFrameGunnerVehicle(vdef) =>
val bframe = name.substring(0, name.indexOf('_'))
val flight = bframe + "_flight"
Seq((vdef, name), (DefinitionUtil.fromString(flight), flight))
case _ =>
Seq(pair)
}
}
}
def refreshPurchaseTimes(keys: Set[String]): Unit = { def refreshPurchaseTimes(keys: Set[String]): Unit = {
var keysToDrop: Seq[String] = Nil var keysToDrop: Seq[String] = Nil
keys.foreach { key => keys.foreach { key =>
@ -1836,8 +1833,12 @@ class AvatarActor(
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
Avatar.purchaseCooldowns.find(_._1.Name == name) match { Avatar.purchaseCooldowns.find(_._1.Name == name) match {
case Some((obj, cooldown)) if cooldown.toSeconds - secondsSincePurchase > 0 => case Some((obj, cooldown)) if cooldown.toSeconds - secondsSincePurchase > 0 =>
val (_, name) = resolvePurchaseTimeName(avatar.faction, obj) val (_, name) = AvatarActor.resolvePurchaseTimeName(avatar.faction, obj)
updatePurchaseTimer(name, cooldown.toSeconds - secondsSincePurchase, unk1 = true) updatePurchaseTimer(
name,
cooldown.toSeconds - secondsSincePurchase,
DefinitionUtil.fromString(name).isInstanceOf[VehicleDefinition]
)
case _ => case _ =>
keysToDrop = keysToDrop :+ key //key has timed-out keysToDrop = keysToDrop :+ key //key has timed-out
@ -1850,10 +1851,9 @@ class AvatarActor(
} }
} }
def updatePurchaseTimer(name: String, time: Long, unk1: Boolean): Unit = { def updatePurchaseTimer(name: String, time: Long, isActuallyAVehicle: Boolean): Unit = {
//TODO? unk1 is: vehicles = true, everything else = false
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
AvatarVehicleTimerMessage(session.get.player.GUID, name, time, unk1 = true) AvatarVehicleTimerMessage(session.get.player.GUID, name, time, isActuallyAVehicle)
) )
} }

View file

@ -282,6 +282,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
var heightLast: Float = 0f var heightLast: Float = 0f
var heightTrend: Boolean = false //up = true, down = false var heightTrend: Boolean = false //up = true, down = false
var heightHistory: Float = 0f var heightHistory: Float = 0f
var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0)
val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap() val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap()
var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery
@ -586,7 +587,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case Some(_: LocalLockerItem) => case Some(_: LocalLockerItem) =>
player.avatar.locker.Inventory.hasItem(guid) match { player.avatar.locker.Inventory.hasItem(guid) match {
case out @ Some(_) => case out @ Some(_) =>
contextSafeEntity = guid
out out
case None if contextSafeEntity == guid =>
//safeguard
None
case None => case None =>
//delete stale entity reference from client //delete stale entity reference from client
log.warn( log.warn(
@ -614,6 +619,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(ObjectDeleteMessage(guid, 0)) sendResponse(ObjectDeleteMessage(guid, 0))
None None
case None if contextSafeEntity == guid =>
//safeguard
None
case _ => case _ =>
None None
} }
@ -4951,10 +4960,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ValidObject(item_guid, decorator = "MoveItem") ValidObject(item_guid, decorator = "MoveItem")
) match { ) match {
case ( case (
Some(source: PlanetSideServerObject with Container), Some(source: PlanetSideServerObject with Container),
Some(destination: PlanetSideServerObject with Container), Some(destination: PlanetSideServerObject with Container),
Some(item: Equipment) Some(item: Equipment)
) => ) =>
ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest)) ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest))
case (None, _, _) => case (None, _, _) =>
log.error( log.error(

View file

@ -3,7 +3,7 @@ package net.psforever.login
import akka.actor.ActorRef import akka.actor.ActorRef
import akka.pattern.{AskTimeoutException, ask} import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout import akka.util.Timeout
import net.psforever.objects.equipment.{Ammo, Equipment} import net.psforever.objects.equipment.{Ammo, Equipment, EquipmentSize}
import net.psforever.objects.guid._ import net.psforever.objects.guid._
import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.locker.LockerContainer import net.psforever.objects.locker.LockerContainer
@ -653,39 +653,77 @@ object WorldSession {
item: Equipment, item: Equipment,
dest: Int dest: Int
): Unit = { ): Unit = {
TaskWorkflow.execute(TaskBundle( val (performSwap, swapItemGUID): (Boolean, Option[PlanetSideGUID]) = {
new StraightforwardTask() { val destInv = destination.Inventory
val localGUID = item.GUID //original GUID if (destInv.Offset <= dest && destInv.Offset + destInv.TotalCapacity >= dest) {
val localChannel = toChannel val tile = item.Definition.Tile
val localSource = source destInv.CheckCollisionsVar(dest, tile.Width, tile.Height)
} else {
val slot = destination.Slot(dest)
if (slot.Size != EquipmentSize.Blocked) {
slot.Equipment match {
case Some(thing) => Success(List(InventoryItem(thing, dest)))
case None => Success(Nil)
}
} else {
Failure(new Exception(""))
}
}
} match {
case Success(Nil) =>
//no swap item
(true, None)
case Success(List(swapEntry: InventoryItem)) =>
//the swap item is to be registered to the source's zone
(true, Some(swapEntry.obj.GUID))
case _ =>
//too many swap items or other error; this attempt will not execute
(false, None)
}
if (performSwap) {
def moveItemTaskFunc(toSlot: Int): Task = new StraightforwardTask() {
val localGUID = swapItemGUID //the swap item's original GUID, if any swap item
val localChannel = toChannel
val localSource = source
val localDestination = destination val localDestination = destination
val localItem = item val localItem = item
val localSlot = dest val localDestSlot = dest
/* val localSrcSlot = toSlot
source is a locker container that has its own internal unique number system val localMoveOnComplete: Try[Any] => Unit = {
the item is currently registered to this system case Success(Containable.ItemPutInSlot(_, _, _, Some(swapItem))) =>
the item will be moved into the system in which the destination operates //swapItem is not registered right now, we can not drop the item without re-registering it
to facilitate the transfer, the item needs to be partially unregistered from the source's system TaskWorkflow.execute(PutNewEquipmentInInventorySlot(localSource)(swapItem, localSrcSlot))
to facilitate the transfer, the item needs to be preemptively registered to the destination's system case _ => ;
invalidating the current unique number is sufficient for both of these steps
*/
localItem.Invalidate()
localItem match {
case t: Tool => t.AmmoSlots.foreach { _.Box.Invalidate() }
case _ => ;
} }
override def description(): String = s"registering $localItem in ${localDestination.Zone.id} before removing from $localSource" override def description(): String = s"registering $localItem in ${localDestination.Zone.id} before removing from $localSource"
def action(): Future[Any] = { def action(): Future[Any] = {
val zone = localSource.Zone localGUID match {
//see LockerContainerControl.RemoveItemFromSlotCallback case Some(guid) =>
zone.AvatarEvents ! AvatarServiceMessage(localChannel, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, localGUID)) //see LockerContainerControl.RemoveItemFromSlotCallback
ask(localSource.Actor, Containable.MoveItem(localDestination, localItem, localSlot)) localSource.Zone.AvatarEvents ! AvatarServiceMessage(
localChannel,
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, guid)
)
case None => ;
}
val moveResult = ask(localDestination.Actor, Containable.PutItemInSlotOrAway(localItem, Some(localDestSlot)))
moveResult.onComplete(localMoveOnComplete)
moveResult
} }
}, }
GUIDTask.registerEquipment(destination.Zone.GUID, item)) val resultOnComplete: Try[Any] => Unit = {
) case Success(Containable.ItemFromSlot(fromSource, Some(itemToMove), Some(fromSlot))) =>
TaskWorkflow.execute(TaskBundle(
moveItemTaskFunc(fromSlot),
GUIDTask.registerEquipment(fromSource.Zone.GUID, itemToMove)
))
case _ => ;
}
val result = ask(source.Actor, Containable.RemoveItemFromSlot(item))
result.onComplete(resultOnComplete)
}
} }
/** /**

View file

@ -23,7 +23,10 @@ class LocalLockerItem extends PlanetSideServerObject {
object LocalLockerItem { object LocalLockerItem {
import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.ObjectDefinition
def local = new ObjectDefinition(0) { Name = "locker-equipment" } def local = new ObjectDefinition(0) {
Name = "locker-equipment"
registerAs = "locker-contents"
}
/** /**
* Instantiate and configure a `LocalProjectile` object. * Instantiate and configure a `LocalProjectile` object.

View file

@ -22,7 +22,10 @@ class LocalProjectile extends PlanetSideServerObject {
object LocalProjectile { object LocalProjectile {
import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.definition.ObjectDefinition
def local = new ObjectDefinition(0) { Name = "projectile" } def local = new ObjectDefinition(0) {
Name = "projectile"
registerAs = "projectiles"
}
/** /**
* Instantiate and configure a `LocalProjectile` object. * Instantiate and configure a `LocalProjectile` object.

View file

@ -12,7 +12,7 @@ import net.psforever.types.{PlanetSideEmpire, Vector3}
/** /**
* A control agency mainly for manipulating the equipment stowed by a player in a `LockerContainer` * A control agency mainly for manipulating the equipment stowed by a player in a `LockerContainer`
* and reporting back to a specific xchannel in the event system about these changes. * and reporting back to a specific channel in the event system about these changes.
* @param locker the governed player-facing locker component * @param locker the governed player-facing locker component
* @param toChannel the channel to which to publish events, typically the owning player's name * @param toChannel the channel to which to publish events, typically the owning player's name
*/ */

View file

@ -339,13 +339,6 @@ object Zones {
turretWeaponGuid turretWeaponGuid
) )
(Projectile.baseUID until Projectile.rangeUID) foreach {
zoneMap.addLocalObject(_, LocalProjectile.Constructor)
}
40150 until 40450 foreach {
zoneMap.addLocalObject(_, LocalLockerItem.Constructor)
}
lattice.asObject.get(mapid).foreach { obj => lattice.asObject.get(mapid).foreach { obj =>
obj.asArray.get.foreach { entry => obj.asArray.get.foreach { entry =>
val arr = entry.asArray.get val arr = entry.asArray.get
@ -687,6 +680,16 @@ object Zones {
override def SetupNumberPools() : Unit = addPoolsFunc() override def SetupNumberPools() : Unit = addPoolsFunc()
override def init(implicit context: ActorContext): Unit = { override def init(implicit context: ActorContext): Unit = {
guids.find { pool => pool.name.equals("projectiles") } match {
case Some(pool) =>
(pool.start to pool.max).foreach { map.addLocalObject(_, LocalProjectile.Constructor) }
case None => ;
}
guids.find { pool => pool.name.equals("locker-contents") } match {
case Some(pool) =>
(pool.start to pool.max).foreach { map.addLocalObject(_, LocalLockerItem.Constructor) }
case None => ;
}
super.init(context) super.init(context)
if (!info.id.startsWith("tz")) { if (!info.id.startsWith("tz")) {