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