* 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) send(ServerStart(nonce, serverNonce), None, None)
cryptoSetup() 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), _) => 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() connectionClose()
case (ConnectionClose(), _) =>
/*
indicates the user has willingly quit the game world
we do not need to implement this
*/
Behaviors.same
// TODO ResetSequence // TODO ResetSequence
case _ => case _ =>
log.warn(s"Unexpected packet type $packet in start (before crypto)") 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 akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
import net.psforever.objects.vital.{DamagingActivity, HealingActivity} import net.psforever.objects.vital.{DamagingActivity, HealingActivity}
import org.joda.time.{LocalDateTime, Seconds} import org.joda.time.{LocalDateTime, Seconds}
//import org.log4s.Logger
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.{ExecutionContextExecutor, Future, Promise} 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.objects.avatar.{Friend => AvatarFriend, Ignored => AvatarIgnored, Shortcut => AvatarShortcut, _} import net.psforever.objects.avatar.{
import net.psforever.objects.definition.converter.CharacterSelectConverter Avatar,
BattleRank,
Certification,
Cooldowns,
Cosmetic,
Friend => AvatarFriend,
Ignored => AvatarIgnored,
Implant,
MemberLists,
PlayerControl,
Shortcut => AvatarShortcut
}
import net.psforever.objects.definition._ import net.psforever.objects.definition._
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.inventory.Container import net.psforever.objects.inventory.Container
import net.psforever.objects.equipment.{Equipment, EquipmentSlot} import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.inventory.InventoryItem 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.objects.vital.HealFromImplant
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars} import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _} 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.util.Database._
import net.psforever.persistence import net.psforever.persistence
import net.psforever.util.{Config, Database, DefinitionUtil} 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} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
object AvatarActor { object AvatarActor {
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] = def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
Behaviors Behaviors
.supervise[Command] { .supervise[Command] {
Behaviors.withStash(100) { buffer => Behaviors.withStash(capacity = 100) { buffer =>
Behaviors.setup(context => new AvatarActor(context, buffer, sessionActor).start()) Behaviors.setup(context => new AvatarActor(context, buffer, sessionActor).login())
} }
} }
.onFailure[Exception](SupervisorStrategy.restart) .onFailure[Exception](SupervisorStrategy.restart)
@ -75,7 +97,7 @@ object AvatarActor {
/** Log in the currently selected avatar. Must have first sent SelectAvatar. */ /** Log in the currently selected avatar. Must have first sent SelectAvatar. */
final case class LoginAvatar(replyTo: ActorRef[AvatarLoginResponse]) extends Command 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 final case class CreateImplants() extends Command
/** Replace avatar instance with the provided one */ /** 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 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 final case class SetStamina(stamina: Int) extends Command
private case class SetImplantInitialized(implantType: ImplantType) extends Command private case class SetImplantInitialized(implantType: ImplantType) extends Command
@ -385,7 +405,7 @@ object AvatarActor {
} }
def encodeLockerClob(container: Container): String = { def encodeLockerClob(container: Container): String = {
val clobber: mutable.StringBuilder = new StringBuilder() val clobber: mutable.StringBuilder = new mutable.StringBuilder()
container.Inventory.Items.foreach { container.Inventory.Items.foreach {
case InventoryItem(obj, index) => case InventoryItem(obj, index) =>
clobber.append(encodeLoadoutClobFragment(obj, index)) clobber.append(encodeLoadoutClobFragment(obj, index))
@ -564,7 +584,7 @@ object AvatarActor {
} }
out.future out.future
} }
//TODO should return number of rows inserted?
/** /**
* Query the database on information retained in regards to a certain character * Query the database on information retained in regards to a certain character
* when that character had last logged out of the game. * 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) }) val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) })
queryResult.onComplete { queryResult.onComplete {
case Success(results) if results.nonEmpty => case Success(results) if results.nonEmpty =>
val res=ctx.run(query[persistence.Savedplayer] ctx.run(query[persistence.Savedplayer]
.filter { _.avatarId == lift(avatarId) } .filter { _.avatarId == lift(avatarId) }
.update( .update(
_.px -> lift((position.x * 1000).toInt), _.px -> lift((position.x * 1000).toInt),
@ -771,7 +791,7 @@ class AvatarActor(
sessionActor ! SessionActor.SetAvatar(avatar) sessionActor ! SessionActor.SetAvatar(avatar)
} }
def start(): Behavior[Command] = { def login(): Behavior[Command] = {
Behaviors Behaviors
.receiveMessage[Command] { .receiveMessage[Command] {
case SetAccount(newAccount) => case SetAccount(newAccount) =>
@ -785,11 +805,11 @@ class AvatarActor(
} }
case Failure(e) => log.error(e)("db failure") case Failure(e) => log.error(e)("db failure")
} }
postStartBehaviour() postLoginBehaviour()
case SetSession(newSession) => case SetSession(newSession) =>
session = Some(newSession) session = Some(newSession)
postStartBehaviour() postLoginBehaviour()
case other => case other =>
buffer.stash(other) buffer.stash(other)
@ -797,34 +817,22 @@ class AvatarActor(
} }
} }
def postStartBehaviour(): Behavior[Command] = { def postLoginBehaviour(): Behavior[Command] = {
account match { (account, session) match {
case Some(_account) => case (Some(_account), Some(_)) => characterSelect(_account)
buffer.unstashAll(active(_account)) case _ => Behaviors.same
case _ =>
Behaviors.same
} }
} }
def active(account: Account): Behavior[Command] = { def characterSelect(account: Account): Behavior[Command] = {
Behaviors Behaviors
.receiveMessagePartial[Command] { .receiveMessage[Command] {
case SetSession(newSession) => case SetSession(newSession) =>
session = Some(newSession) session = Some(newSession)
Behaviors.same 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) => case CreateAvatar(name, head, voice, gender, empire) =>
import ctx._ import ctx._
ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete { ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete {
case Success(characters) => case Success(characters) =>
characters.headOption match { characters.headOption match {
@ -844,7 +852,6 @@ class AvatarActor(
) )
) )
} yield () } yield ()
result.onComplete { result.onComplete {
case Success(_) => case Success(_) =>
log.debug(s"AvatarActor: created character $name for account ${account.name}") log.debug(s"AvatarActor: created character $name for account ${account.name}")
@ -858,14 +865,14 @@ class AvatarActor(
} }
case Failure(e) => case Failure(e) =>
log.error(e)("db failure") log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(4)) sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(3))
sendAvatars(account) sendAvatars(account)
} }
Behaviors.same Behaviors.same
case DeleteAvatar(id) => case DeleteAvatar(id) =>
import ctx._ import ctx._
val result = for { val performDeletion = for {
_ <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(id)).delete) _ <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(id)).delete)
_ <- ctx.run(query[persistence.Loadout].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) _ <- 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) _ <- ctx.run(query[persistence.Savedplayer].filter(_.avatarId == lift(id)).delete)
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(id))) r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(id)))
} yield r } yield r
performDeletion.onComplete {
result.onComplete {
case Success(deleted) => case Success(deleted) =>
deleted.headOption match { deleted.headOption match {
case Some(a) if !a.deleted => case Some(a) if !a.deleted =>
ctx.run(query[persistence.Avatar] val flagDeletion = for {
_ <- ctx.run(query[persistence.Avatar]
.filter(_.id == lift(id)) .filter(_.id == lift(id))
.update( .update(
_.deleted -> lift(true), _.deleted -> lift(true),
_.lastModified -> lift(LocalDateTime.now()) _.lastModified -> lift(LocalDateTime.now())
) )
) )
} yield ()
flagDeletion.onComplete {
case Success(_) =>
log.debug(s"AvatarActor: avatar $id deleted") log.debug(s"AvatarActor: avatar $id deleted")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass) sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
case _ => ;
}
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))
sendAvatars(account)
}
case _ =>
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 4))
sendAvatars(account)
}
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 4))
} }
Behaviors.same Behaviors.same
@ -905,9 +924,13 @@ class AvatarActor(
case Some(character) => case Some(character) =>
avatar = character.toAvatar avatar = character.toAvatar
replyTo ! AvatarResponse(avatar) 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 Behaviors.same
@ -946,14 +969,43 @@ class AvatarActor(
) )
} yield true } yield true
inits.onComplete { inits.onComplete {
case Success(_) => performAvatarLogin(avatarId, account.id, replyTo) case Success(_) =>
case Failure(e) => log.error(e)("db failure") performAvatarLogin(avatarId, account.id, replyTo)
case Failure(e) =>
log.error(e)("db failure")
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Fail(error = 6))
} }
} else { } else {
performAvatarLogin(avatarId, account.id, replyTo) 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 Behaviors.same
case ReplaceAvatar(newAvatar) => case ReplaceAvatar(newAvatar) =>
@ -961,6 +1013,15 @@ class AvatarActor(
startIfStoppedStaminaRegen(initialDelay = 0.5f seconds) startIfStoppedStaminaRegen(initialDelay = 0.5f seconds)
Behaviors.same 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) => case AddFirstTimeEvent(event) =>
val decor = avatar.decoration val decor = avatar.decoration
avatarCopy(avatar.copy(decoration = decor.copy(firstTimeEvents = decor.firstTimeEvents ++ Set(event)))) avatarCopy(avatar.copy(decoration = decor.copy(firstTimeEvents = decor.firstTimeEvents ++ Set(event))))
@ -1405,7 +1466,7 @@ class AvatarActor(
if ( if (
implantType match { implantType match {
case ImplantType.AdvancedRegen => 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 // to simulate '1.5sp (standing)', find if 0.0...1.0 * 100 is an even number
val cost = implant.definition.StaminaCost - val cost = implant.definition.StaminaCost -
(if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0) (if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0)
@ -1521,7 +1582,6 @@ class AvatarActor(
case SetBep(bep) => case SetBep(bep) =>
import ctx._ import ctx._
val result = for { val result = for {
_ <- _ <-
if (BattleRank.withExperience(bep).value < BattleRank.BR24.value) setCosmetics(Set()) 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] = { def storeVehicleLoadout(owner: Player, label: String, line: Int, vehicle: Vehicle): Future[Loadout] = {
import ctx._ import ctx._
val items: String = { val items: String = {
val clobber: mutable.StringBuilder = new StringBuilder() val clobber: mutable.StringBuilder = new mutable.StringBuilder()
//encode holsters //encode holsters
vehicle.Weapons vehicle.Weapons
.collect { .collect {

View file

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

View file

@ -7,33 +7,33 @@ import scodec.codecs._
/** /**
* Is sent by the server when the client has performed an action from a menu item * Is sent by the server when the client has performed an action from a menu item
* (i.e create character, delete character, etc...) * (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(successful: Boolean, errorCode: Option[Long]) extends PlanetSideGamePacket { final case class ActionResultMessage(errorCode: Option[Long]) extends PlanetSideGamePacket {
type Packet = ActionResultMessage type Packet = ActionResultMessage
def opcode = GamePacketOpcode.ActionResultMessage def opcode = GamePacketOpcode.ActionResultMessage
def encode = ActionResultMessage.encode(this) def encode = ActionResultMessage.encode(this)
} }
object ActionResultMessage extends Marshallable[ActionResultMessage] { object ActionResultMessage extends Marshallable[ActionResultMessage] {
/** /**
* A message where the result is always a pass. * A message where the result is always a pass.
* @return an `ActionResultMessage` object * @return an `ActionResultMessage` object
*/ */
def Pass: ActionResultMessage = ActionResultMessage(true, None) def Pass: ActionResultMessage = ActionResultMessage(None)
/** /**
* A message where the result is always a failure. * A message where the result is always a failure.
* @param error the error code * @param error the error code
* @return an `ActionResultMessage` object * @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] = ( implicit val codec: Codec[ActionResultMessage] =
("successful" | bool) >>:~ { res => ("error_code" | optional(bool.xmap[Boolean](state => !state, state => !state), uint32L)).as[ActionResultMessage]
// if not successful, look for an error code
conditional(!res, "error_code" | uint32L).hlist
}
).as[ActionResultMessage]
} }

View file

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