* separating stages of client interaction with the session's avatar; connection closing is caught to avoid unnecessary log messages; changed how ActionResultMessage views its parsing format

* fixed issue with relogging while persisting as dead (thanks Scrawny)

* almost forgot to turn this back on after I finished testing
This commit is contained in:
Fate-JH 2023-01-07 23:13:49 -05:00 committed by GitHub
parent d68ccdfd8d
commit 3bd50dc89c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 90 deletions

View file

@ -297,13 +297,21 @@ class MiddlewareActor(
send(ServerStart(nonce, serverNonce), None, None)
cryptoSetup()
/** Unknown30 is used to reuse an existing crypto session when switching from login to world
* When not handling it, it appears that the client will fall back to using ClientStart
* TODO implement this
*/
case (Unknown30(nonce), _) =>
/*
Unknown30 is used to reuse an existing crypto session when switching from login to world
When not handling it, it appears that the client will fall back to using ClientStart
Do we need to implement this?
*/
connectionClose()
case (ConnectionClose(), _) =>
/*
indicates the user has willingly quit the game world
we do not need to implement this
*/
Behaviors.same
// TODO ResetSequence
case _ =>
log.warn(s"Unexpected packet type $packet in start (before crypto)")

View file

@ -7,15 +7,26 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity}
import org.joda.time.{LocalDateTime, Seconds}
//import org.log4s.Logger
import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success}
import scala.concurrent.duration._
//
import net.psforever.objects.avatar.{Friend => AvatarFriend, Ignored => AvatarIgnored, Shortcut => AvatarShortcut, _}
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.avatar.{
Avatar,
BattleRank,
Certification,
Cooldowns,
Cosmetic,
Friend => AvatarFriend,
Ignored => AvatarIgnored,
Implant,
MemberLists,
PlayerControl,
Shortcut => AvatarShortcut
}
import net.psforever.objects.definition._
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.inventory.Container
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.inventory.InventoryItem
@ -26,19 +37,30 @@ import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.vital.HealFromImplant
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
import net.psforever.types.{MemberAction, PlanetSideEmpire, _}
import net.psforever.types.{
CharacterVoice,
CharacterSex,
ExoSuitType,
ImplantType,
LoadoutType,
MemberAction,
MeritCommendation,
PlanetSideEmpire,
PlanetSideGUID,
TransactionType
}
import net.psforever.util.Database._
import net.psforever.persistence
import net.psforever.util.{Config, Database, DefinitionUtil}
import net.psforever.services.{Service, ServiceManager}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
object AvatarActor {
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
Behaviors
.supervise[Command] {
Behaviors.withStash(100) { buffer =>
Behaviors.setup(context => new AvatarActor(context, buffer, sessionActor).start())
Behaviors.withStash(capacity = 100) { buffer =>
Behaviors.setup(context => new AvatarActor(context, buffer, sessionActor).login())
}
}
.onFailure[Exception](SupervisorStrategy.restart)
@ -75,7 +97,7 @@ object AvatarActor {
/** Log in the currently selected avatar. Must have first sent SelectAvatar. */
final case class LoginAvatar(replyTo: ActorRef[AvatarLoginResponse]) extends Command
/** Send implants to client - TODO this can be done better using a event system on SessionActor */
/** Send implants to client */
final case class CreateImplants() extends Command
/** Replace avatar instance with the provided one */
@ -181,8 +203,6 @@ object AvatarActor {
final case class SetRibbon(ribbon: MeritCommendation.Value, bar: RibbonBarSlot.Value) extends Command
private case class ServiceManagerLookupResult(result: ServiceManager.LookupResult) extends Command
final case class SetStamina(stamina: Int) extends Command
private case class SetImplantInitialized(implantType: ImplantType) extends Command
@ -385,7 +405,7 @@ object AvatarActor {
}
def encodeLockerClob(container: Container): String = {
val clobber: mutable.StringBuilder = new StringBuilder()
val clobber: mutable.StringBuilder = new mutable.StringBuilder()
container.Inventory.Items.foreach {
case InventoryItem(obj, index) =>
clobber.append(encodeLoadoutClobFragment(obj, index))
@ -564,7 +584,7 @@ object AvatarActor {
}
out.future
}
//TODO should return number of rows inserted?
/**
* Query the database on information retained in regards to a certain character
* when that character had last logged out of the game.
@ -664,7 +684,7 @@ object AvatarActor {
val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) })
queryResult.onComplete {
case Success(results) if results.nonEmpty =>
val res=ctx.run(query[persistence.Savedplayer]
ctx.run(query[persistence.Savedplayer]
.filter { _.avatarId == lift(avatarId) }
.update(
_.px -> lift((position.x * 1000).toInt),
@ -771,7 +791,7 @@ class AvatarActor(
sessionActor ! SessionActor.SetAvatar(avatar)
}
def start(): Behavior[Command] = {
def login(): Behavior[Command] = {
Behaviors
.receiveMessage[Command] {
case SetAccount(newAccount) =>
@ -785,11 +805,11 @@ class AvatarActor(
}
case Failure(e) => log.error(e)("db failure")
}
postStartBehaviour()
postLoginBehaviour()
case SetSession(newSession) =>
session = Some(newSession)
postStartBehaviour()
postLoginBehaviour()
case other =>
buffer.stash(other)
@ -797,34 +817,22 @@ class AvatarActor(
}
}
def postStartBehaviour(): Behavior[Command] = {
account match {
case Some(_account) =>
buffer.unstashAll(active(_account))
case _ =>
Behaviors.same
def postLoginBehaviour(): Behavior[Command] = {
(account, session) match {
case (Some(_account), Some(_)) => characterSelect(_account)
case _ => Behaviors.same
}
}
def active(account: Account): Behavior[Command] = {
def characterSelect(account: Account): Behavior[Command] = {
Behaviors
.receiveMessagePartial[Command] {
.receiveMessage[Command] {
case SetSession(newSession) =>
session = Some(newSession)
Behaviors.same
case SetLookingForSquad(lfs) =>
avatarCopy(avatar.copy(lookingForSquad = lfs))
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 53, 0))
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.faction.toString,
AvatarAction.PlanetsideAttribute(session.get.player.GUID, 53, if (lfs) 1 else 0)
)
Behaviors.same
case CreateAvatar(name, head, voice, gender, empire) =>
import ctx._
ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete {
case Success(characters) =>
characters.headOption match {
@ -844,7 +852,6 @@ class AvatarActor(
)
)
} yield ()
result.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: created character $name for account ${account.name}")
@ -858,14 +865,14 @@ class AvatarActor(
}
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(4))
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(3))
sendAvatars(account)
}
Behaviors.same
case DeleteAvatar(id) =>
import ctx._
val result = for {
val performDeletion = for {
_ <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(id)).delete)
_ <- ctx.run(query[persistence.Loadout].filter(_.avatarId == lift(id)).delete)
_ <- ctx.run(query[persistence.Locker].filter(_.avatarId == lift(id)).delete)
@ -876,24 +883,36 @@ class AvatarActor(
_ <- ctx.run(query[persistence.Savedplayer].filter(_.avatarId == lift(id)).delete)
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(id)))
} yield r
result.onComplete {
performDeletion.onComplete {
case Success(deleted) =>
deleted.headOption match {
case Some(a) if !a.deleted =>
ctx.run(query[persistence.Avatar]
.filter(_.id == lift(id))
.update(
_.deleted -> lift(true),
_.lastModified -> lift(LocalDateTime.now())
val flagDeletion = for {
_ <- ctx.run(query[persistence.Avatar]
.filter(_.id == lift(id))
.update(
_.deleted -> lift(true),
_.lastModified -> lift(LocalDateTime.now())
)
)
)
log.debug(s"AvatarActor: avatar $id deleted")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
case _ => ;
} yield ()
flagDeletion.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: avatar $id deleted")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
sendAvatars(account)
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 4))
sendAvatars(account)
}
case _ =>
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 4))
sendAvatars(account)
}
sendAvatars(account)
case Failure(e) => log.error(e)("db failure")
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 4))
}
Behaviors.same
@ -905,9 +924,13 @@ class AvatarActor(
case Some(character) =>
avatar = character.toAvatar
replyTo ! AvatarResponse(avatar)
case None => log.error(s"selected character $charId not found")
case None =>
log.error(s"selected character $charId not found")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 5))
}
case Failure(e) => log.error(e)("db failure")
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 5))
}
Behaviors.same
@ -946,14 +969,43 @@ class AvatarActor(
)
} yield true
inits.onComplete {
case Success(_) => performAvatarLogin(avatarId, account.id, replyTo)
case Failure(e) => log.error(e)("db failure")
case Success(_) =>
performAvatarLogin(avatarId, account.id, replyTo)
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
}
} else {
performAvatarLogin(avatarId, account.id, replyTo)
}
case Failure(e) => log.error(e)("db failure")
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
}
postCharacterSelectBehaviour()
case ReplaceAvatar(newAvatar) =>
replaceAvatar(newAvatar)
postCharacterSelectBehaviour()
case other =>
buffer.stash(other)
Behaviors.same
}
}
def postCharacterSelectBehaviour(): Behavior[Command] = {
_avatar match {
case Some(_) => buffer.unstashAll(gameplay)
case _ => Behaviors.same
}
}
def gameplay: Behavior[Command] = {
Behaviors
.receiveMessagePartial[Command] {
case SetSession(newSession) =>
session = Some(newSession)
Behaviors.same
case ReplaceAvatar(newAvatar) =>
@ -961,6 +1013,15 @@ class AvatarActor(
startIfStoppedStaminaRegen(initialDelay = 0.5f seconds)
Behaviors.same
case SetLookingForSquad(lfs) =>
avatarCopy(avatar.copy(lookingForSquad = lfs))
sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 53, 0))
session.get.zone.AvatarEvents ! AvatarServiceMessage(
avatar.faction.toString,
AvatarAction.PlanetsideAttribute(session.get.player.GUID, 53, if (lfs) 1 else 0)
)
Behaviors.same
case AddFirstTimeEvent(event) =>
val decor = avatar.decoration
avatarCopy(avatar.copy(decoration = decor.copy(firstTimeEvents = decor.firstTimeEvents ++ Set(event))))
@ -1405,7 +1466,7 @@ class AvatarActor(
if (
implantType match {
case ImplantType.AdvancedRegen =>
//for every 1hp: 2sp (running), 1.5sp (standing), 1sp (crouched)
// for every 1hp: 2sp (running), 1.5sp (standing), 1sp (crouched)
// to simulate '1.5sp (standing)', find if 0.0...1.0 * 100 is an even number
val cost = implant.definition.StaminaCost -
(if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0)
@ -1521,7 +1582,6 @@ class AvatarActor(
case SetBep(bep) =>
import ctx._
val result = for {
_ <-
if (BattleRank.withExperience(bep).value < BattleRank.BR24.value) setCosmetics(Set())
@ -2145,7 +2205,7 @@ class AvatarActor(
def storeVehicleLoadout(owner: Player, label: String, line: Int, vehicle: Vehicle): Future[Loadout] = {
import ctx._
val items: String = {
val clobber: mutable.StringBuilder = new StringBuilder()
val clobber: mutable.StringBuilder = new mutable.StringBuilder()
//encode holsters
vehicle.Weapons
.collect {

View file

@ -1453,7 +1453,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
val oldZone = continent
session = session.copy(zone = zone)
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
continent.AvatarEvents ! Service.Join(player.Name)
zone.AvatarEvents ! Service.Join(player.Name)
persist()
oldZone.AvatarEvents ! Service.Leave()
oldZone.LocalEvents ! Service.Leave()
@ -1470,7 +1470,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else {
zoneReload = true
cluster ! ICS.GetNearbySpawnPoint(
continent.Number,
zone.Number,
player,
Seq(SpawnGroup.Facility, SpawnGroup.Tower),
context.self
@ -1744,7 +1744,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
deadState = DeadState.Dead
session = session.copy(player = p, avatar = a)
persist()
player.Zone = inZone
HandleReleaseAvatar(p, inZone)
avatarActor ! AvatarActor.ReplaceAvatar(a)
avatarLoginResponse(a)
@ -2098,13 +2097,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
loadConfZone = true
val oldZone = session.zone
session = session.copy(zone = foundZone)
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
continent.AvatarEvents ! Service.Join(player.Name)
persist()
oldZone.AvatarEvents ! Service.Leave()
oldZone.LocalEvents ! Service.Leave()
oldZone.VehicleEvents ! Service.Leave()
continent.Population ! Zone.Population.Join(avatar)
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
foundZone.AvatarEvents ! Service.Join(player.Name)
foundZone.Population ! Zone.Population.Join(avatar)
player.avatar = avatar
interstellarFerry match {
case Some(vehicle) if vehicle.PassengerInSeat(player).contains(0) =>
@ -9513,7 +9512,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
*/
def TurnCounterDuringInterim(guid: PlanetSideGUID): Unit = {
upstreamMessageCount = 0
if (player != null && player.GUID == guid && player.Zone == continent) {
if (player != null && player.HasGUID && player.GUID == guid && player.Zone == continent) {
turnCounterFunc = NormalTurnCounter
}
}

View file

@ -6,34 +6,34 @@ import scodec.Codec
import scodec.codecs._
/**
* Is sent by the server when the client has performed an action from a menu item
* (i.e create character, delete character, etc...)
*/
final case class ActionResultMessage(successful: Boolean, errorCode: Option[Long]) extends PlanetSideGamePacket {
* Is sent by the server when the client has performed an action from a menu item
* (i.e create character, delete character, etc...).
* Error messages usually are accompanied by an angry beep.<br>
* Error 0 is a common code but it doesn't do anything specific on its own.<br>
* Error 1 generates the message box: a character with that name already exists.<br>
* Error 2 generates the message box: something to do with the word filter.<br>
* Other errors during the character login screen generate a generic error message box and list the code.
*/
final case class ActionResultMessage(errorCode: Option[Long]) extends PlanetSideGamePacket {
type Packet = ActionResultMessage
def opcode = GamePacketOpcode.ActionResultMessage
def encode = ActionResultMessage.encode(this)
}
object ActionResultMessage extends Marshallable[ActionResultMessage] {
/**
* A message where the result is always a pass.
* @return an `ActionResultMessage` object
*/
def Pass: ActionResultMessage = ActionResultMessage(true, None)
def Pass: ActionResultMessage = ActionResultMessage(None)
/**
* A message where the result is always a failure.
* @param error the error code
* @return an `ActionResultMessage` object
*/
def Fail(error: Long): ActionResultMessage = ActionResultMessage(false, Some(error))
def Fail(error: Long): ActionResultMessage = ActionResultMessage(Some(error))
implicit val codec: Codec[ActionResultMessage] = (
("successful" | bool) >>:~ { res =>
// if not successful, look for an error code
conditional(!res, "error_code" | uint32L).hlist
}
).as[ActionResultMessage]
implicit val codec: Codec[ActionResultMessage] =
("error_code" | optional(bool.xmap[Boolean](state => !state, state => !state), uint32L)).as[ActionResultMessage]
}

View file

@ -12,9 +12,8 @@ class ActionResultMessageTest extends Specification {
"decode (pass)" in {
PacketCoding.decodePacket(string_pass).require match {
case ActionResultMessage(okay, code) =>
okay mustEqual true
code mustEqual None
case ActionResultMessage(code) =>
code.isEmpty mustEqual true
case _ =>
ko
}
@ -22,16 +21,15 @@ class ActionResultMessageTest extends Specification {
"decode (fail)" in {
PacketCoding.decodePacket(string_fail).require match {
case ActionResultMessage(okay, code) =>
okay mustEqual false
code mustEqual Some(1)
case ActionResultMessage(code) =>
code.contains(1) mustEqual true
case _ =>
ko
}
}
"encode (pass, full)" in {
val msg = ActionResultMessage(true, None)
val msg = ActionResultMessage(None)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_pass
@ -45,7 +43,7 @@ class ActionResultMessageTest extends Specification {
}
"encode (fail, full)" in {
val msg = ActionResultMessage(false, Some(1))
val msg = ActionResultMessage(Some(1))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_fail