diff --git a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala index fdfcd3f1c..043acb135 100644 --- a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala +++ b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala @@ -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)") diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 33883ed9e..0151e6095 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -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 { diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 4b16da307..259763890 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -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 } } diff --git a/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala b/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala index 2f23f161d..e535a4791 100644 --- a/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala +++ b/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala @@ -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.
+ * Error 0 is a common code but it doesn't do anything specific on its own.
+ * Error 1 generates the message box: a character with that name already exists.
+ * Error 2 generates the message box: something to do with the word filter.
+ * 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] } diff --git a/src/test/scala/game/ActionResultMessageTest.scala b/src/test/scala/game/ActionResultMessageTest.scala index 4ce697af8..89e65602e 100644 --- a/src/test/scala/game/ActionResultMessageTest.scala +++ b/src/test/scala/game/ActionResultMessageTest.scala @@ -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