mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-20 02:24:45 +00:00
Login Location Persistence (#1009)
* database tables and persistence entities; attempt to retrieve values from database and apply them to the player avatar character; resolve spawn options in sanctuary in different manner * minor database table field adjustments; saving to database when account persistence ends; properly loading from and initializing with data queried from the database; suicide better supported * converting the zoning method/status tokens; better support of zoning handling of persistent locations; messages that explain the consequences of login-spawning into an invalid location * adding triggers for the 'saved char' message, both those prompted by game activity and a 'reassurance' message; also, actually save the char data sometimes * intervals for timing charsaved message set by configuration file now * corrections to spawn tests and tables * random sanctuary spawn in more places than one
This commit is contained in:
parent
190a897dd5
commit
1369da22f0
|
|
@ -1,11 +1,11 @@
|
|||
CREATE TABLE IF NOT EXISTS "friend" (
|
||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||
"avatar_id" INT NOT NULL REFERENCES avatar (id),
|
||||
"char_id" INT NOT NULL REFERENCES avatar (id)
|
||||
"char_id" INT NOT NULL REFERENCES avatar (id),
|
||||
UNIQUE(avatar_id, char_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ignored" (
|
||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||
"avatar_id" INT NOT NULL REFERENCES avatar (id),
|
||||
"char_id" INT NOT NULL REFERENCES avatar (id)
|
||||
"char_id" INT NOT NULL REFERENCES avatar (id),
|
||||
UNIQUE(avatar_id, char_id)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
CREATE TABLE IF NOT EXISTS "savedplayer" (
|
||||
"avatar_id" INT NOT NULL PRIMARY KEY REFERENCES avatar (id),
|
||||
"px" INT NOT NULL,
|
||||
"py" INT NOT NULL,
|
||||
"pz" INT NOT NULL,
|
||||
"orientation" INT NOT NULL,
|
||||
"zone_num" SMALLINT NOT NULL,
|
||||
"health" SMALLINT NOT NULL,
|
||||
"armor" SMALLINT NOT NULL,
|
||||
"exosuit_num" SMALLINT NOT NULL,
|
||||
"loadout" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "savedavatar" (
|
||||
"avatar_id" INT NOT NULL PRIMARY KEY REFERENCES avatar (id),
|
||||
"forget_cooldown" TIMESTAMP NOT NULL,
|
||||
"purchase_cooldowns" TEXT NOT NULL,
|
||||
"use_cooldowns" TEXT NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE account
|
||||
ADD COLUMN last_faction_id SMALLINT DEFAULT 3
|
||||
|
|
@ -147,6 +147,36 @@ game {
|
|||
# When set, however, the next zone unlock is carried out regardless of the amount of time remaining
|
||||
force-rotation-immediately = false
|
||||
}
|
||||
|
||||
saved-msg = {
|
||||
# A brief delay to the @charsaved message, in seconds.
|
||||
# Use this when the message should display very soon for any reason.
|
||||
short = {
|
||||
# This delay always occurs
|
||||
fixed = 5
|
||||
# This delay is applied partially - fixed + [0,delay)
|
||||
variable = 5
|
||||
}
|
||||
|
||||
# A delay to the @charsaved message whenever a previous message has been displayed, in seconds.
|
||||
# Used as the default interval between messages.
|
||||
# It should provide assurance to the player even if nothing happened.
|
||||
# Actual database interaction not assured.
|
||||
renewal = {
|
||||
fixed = 300
|
||||
variable = 600
|
||||
}
|
||||
|
||||
# A delay to the @charsaved message
|
||||
# whenever an action that would cause actual database interaction occurs, in seconds.
|
||||
# Actual database interaction not assured.
|
||||
# The variability, in this case, serves the purpose of hedging against other activity by the player
|
||||
# that would trigger the message again in a short amount of time.
|
||||
interrupted-by-action = {
|
||||
fixed = 15
|
||||
variable = 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anti-cheat {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||
import akka.actor.Cancellable
|
||||
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
|
||||
|
|
@ -192,6 +193,48 @@ object AvatarActor {
|
|||
|
||||
final case class AvatarLoginResponse(avatar: Avatar)
|
||||
|
||||
/**
|
||||
* A player loadout represents all of the items in the player's hands (equipment slots)
|
||||
* and all of the items in the player's backpack (inventory)
|
||||
* with items separated by meaningful punctuation marks.
|
||||
* The CLOB - character large object - is a string of such item data
|
||||
* that can be translated back into the original items
|
||||
* and placed back in the same places in the inventory from which they were extracted.
|
||||
* Together, these are occasionally referred to as an "inventory".
|
||||
* @param owner the player whose inventory is being transcribed
|
||||
* @return the resulting text data that represents an inventory
|
||||
*/
|
||||
def buildClobFromPlayerLoadout(owner: Player): String = {
|
||||
val clobber: mutable.StringBuilder = new mutable.StringBuilder()
|
||||
//encode holsters
|
||||
owner
|
||||
.Holsters()
|
||||
.zipWithIndex
|
||||
.collect {
|
||||
case (slot, index) if slot.Equipment.nonEmpty =>
|
||||
clobber.append(encodeLoadoutClobFragment(slot.Equipment.get, index))
|
||||
}
|
||||
//encode inventory
|
||||
owner.Inventory.Items.foreach {
|
||||
case InventoryItem(obj, index) =>
|
||||
clobber.append(encodeLoadoutClobFragment(obj, index))
|
||||
}
|
||||
clobber.mkString.drop(1) //drop leading punctuation
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform from encoded inventory data as a CLOB - character large object - into individual items.
|
||||
* Install those items into positions in a target container
|
||||
* in the same positions in which they were previously recorded.<br>
|
||||
* <br>
|
||||
* There is no guarantee that the structure of the retained container data encoded in the CLOB
|
||||
* will fit the current dimensions of the container.
|
||||
* No tests are performed.
|
||||
* A partial decompression of the CLOB may occur.
|
||||
* @param container the container in which to place the pieces of equipment produced from the CLOB
|
||||
* @param clob the inventory data in string form
|
||||
* @param log a reference to a logging context
|
||||
*/
|
||||
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 {
|
||||
|
|
@ -222,7 +265,7 @@ object AvatarActor {
|
|||
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")
|
||||
log.error(s"failing to add unknown equipment to a container - $name")
|
||||
}
|
||||
|
||||
toolAmmo foreach { toolAmmo =>
|
||||
|
|
@ -239,6 +282,57 @@ object AvatarActor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the encoded object to time data
|
||||
* into proper object to proper time references
|
||||
* and filter out mappings that have exceeded the sample duration.
|
||||
* @param clob the entity to time data in string form
|
||||
* @param cooldownDurations a base reference for entity to time comparison
|
||||
* @param log a reference to a logging context
|
||||
* @return the resulting text data that represents object to time mappings
|
||||
*/
|
||||
def buildCooldownsFromClob(
|
||||
clob: String,
|
||||
cooldownDurations: Map[BasicDefinition,FiniteDuration],
|
||||
log: org.log4s.Logger
|
||||
): Map[String, LocalDateTime] = {
|
||||
val now = LocalDateTime.now()
|
||||
val cooldowns: mutable.Map[String, LocalDateTime] = mutable.Map()
|
||||
clob.split("/").filter(_.trim.nonEmpty).foreach { value =>
|
||||
value.split(",") match {
|
||||
case Array(name: String, b: String) =>
|
||||
try {
|
||||
val cooldown = LocalDateTime.parse(b)
|
||||
cooldownDurations.get(DefinitionUtil.fromString(name)) match {
|
||||
case Some(duration) if now.compareTo(cooldown.plusMillis(duration.toMillis.toInt)) == -1 =>
|
||||
cooldowns.put(name, cooldown)
|
||||
case _ => ;
|
||||
}
|
||||
} catch {
|
||||
case _: Exception => ;
|
||||
}
|
||||
case _ =>
|
||||
log.warn(s"ignoring invalid cooldown string: '$value'")
|
||||
}
|
||||
}
|
||||
cooldowns.toMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the proper object to proper time references
|
||||
* into encoded object to time data in a string format
|
||||
* and filter out mappings that have exceeded the current time.
|
||||
* @param cooldowns a base reference for entity to time comparison
|
||||
* @return the resulting map that represents object to time string data
|
||||
*/
|
||||
def buildClobfromCooldowns(cooldowns: Map[String, LocalDateTime]): String = {
|
||||
val now = LocalDateTime.now()
|
||||
cooldowns
|
||||
.filter { case (_, cd) => cd.compareTo(now) == -1 }
|
||||
.map { case (name, cd) => s"$name,$cd" }
|
||||
.mkString("/")
|
||||
}
|
||||
|
||||
def resolvePurchaseTimeName(faction: PlanetSideEmpire.Value, item: BasicDefinition): (BasicDefinition, String) = {
|
||||
val factionName: String = faction.toString.toLowerCase
|
||||
val name = item match {
|
||||
|
|
@ -431,6 +525,219 @@ object AvatarActor {
|
|||
def onlineIfNotIgnored(onlinePlayer: Avatar, observedName: String): Boolean = {
|
||||
!onlinePlayer.people.ignored.exists { f => f.name.equals(observedName) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database on information retained in regards to a certain character
|
||||
* when that character had last logged out of the game.
|
||||
* Dummy the data if no entries are found.
|
||||
* @param avatarId the unique character identifier number
|
||||
* @return when completed, a copy of data on that character from the database
|
||||
*/
|
||||
def loadSavedPlayerData(avatarId: Long): Future[persistence.Savedplayer] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[persistence.Savedplayer] = Promise()
|
||||
val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) })
|
||||
queryResult.onComplete {
|
||||
case Success(data) if data.nonEmpty =>
|
||||
out.completeWith(Future(data.head))
|
||||
case _ =>
|
||||
ctx.run(query[persistence.Savedplayer]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.px -> lift(0),
|
||||
_.py -> lift(0),
|
||||
_.pz -> lift(0),
|
||||
_.orientation -> lift(0),
|
||||
_.zoneNum -> lift(0),
|
||||
_.health -> lift(0),
|
||||
_.armor -> lift(0),
|
||||
_.exosuitNum -> lift(0),
|
||||
_.loadout -> lift("")
|
||||
)
|
||||
)
|
||||
out.completeWith(Future(persistence.Savedplayer(avatarId, 0, 0, 0, 0, 0, 0, 0, 0, "")))
|
||||
}
|
||||
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.
|
||||
* If that character is found in the database, update the data for that character.
|
||||
* @param player the player character
|
||||
* @return when completed, return the number of rows updated
|
||||
*/
|
||||
def savePlayerData(player: Player): Future[Int] = {
|
||||
savePlayerData(player, player.Health)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database on information retained in regards to a certain character
|
||||
* when that character had last logged out of the game.
|
||||
* If that character is found in the database, update the data for that character.
|
||||
* Determine if the player's previous health information is valid
|
||||
* by comparing historical information about the player character's campaign.
|
||||
* (This ignored the official health value attached to the character.)
|
||||
* @param player the player character
|
||||
* @return when completed, return the number of rows updated
|
||||
*/
|
||||
def finalSavePlayerData(player: Player): Future[Int] = {
|
||||
val health = (
|
||||
player.History.find(_.isInstanceOf[DamagingActivity]),
|
||||
player.History.find(_.isInstanceOf[HealingActivity])
|
||||
) match {
|
||||
case (Some(damage), Some(heal)) =>
|
||||
//between damage and potential healing, which came last?
|
||||
if (damage.time < heal.time) {
|
||||
heal.asInstanceOf[HealingActivity].amount % player.MaxHealth
|
||||
} else {
|
||||
damage.asInstanceOf[DamagingActivity].data.targetAfter.asInstanceOf[PlayerSource].health
|
||||
}
|
||||
case (Some(damage), None) =>
|
||||
damage.asInstanceOf[DamagingActivity].data.targetAfter.asInstanceOf[PlayerSource].health
|
||||
case (None, Some(heal)) =>
|
||||
heal.asInstanceOf[HealingActivity].amount % player.MaxHealth
|
||||
case _ =>
|
||||
player.MaxHealth
|
||||
}
|
||||
savePlayerData(player, health)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database on information retained in regards to a certain character
|
||||
* when that character had last logged out of the game.
|
||||
* If that character is found in the database, update the data for that character.
|
||||
* If no entries for that character are found, insert a new default-data entry.
|
||||
* @param player the player character
|
||||
* @param health a custom health value to assign the player character's information in the database
|
||||
* @return when completed, return the number of rows updated
|
||||
*/
|
||||
def savePlayerData(player: Player, health: Int): Future[Int] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[Int] = Promise()
|
||||
val avatarId = player.avatar.id
|
||||
val position = player.Position
|
||||
val queryResult = ctx.run(query[persistence.Savedplayer].filter { _.avatarId == lift(avatarId) })
|
||||
queryResult.onComplete {
|
||||
case Success(results) if results.nonEmpty =>
|
||||
ctx.run(query[persistence.Savedplayer]
|
||||
.filter { _.avatarId == lift(avatarId) }
|
||||
.update(
|
||||
_.px -> lift((position.x * 1000).toInt),
|
||||
_.py -> lift((position.y * 1000).toInt),
|
||||
_.pz -> lift((position.z * 1000).toInt),
|
||||
_.orientation -> lift((player.Orientation.z * 1000).toInt),
|
||||
_.zoneNum -> lift(player.Zone.Number),
|
||||
_.health -> lift(health),
|
||||
_.armor -> lift(player.Armor),
|
||||
_.exosuitNum -> lift(player.ExoSuit.id),
|
||||
_.loadout -> lift(buildClobFromPlayerLoadout(player))
|
||||
)
|
||||
)
|
||||
out.completeWith(Future(1))
|
||||
case _ =>
|
||||
out.completeWith(Future(0))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database on information retained in regards to a certain character
|
||||
* when that character had last logged out of the game.
|
||||
* If that character is found in the database, update only specific fields for that character
|
||||
* related to the character's physical location in the game world.
|
||||
* @param player the player character
|
||||
* @return when completed, return the number of rows updated
|
||||
*/
|
||||
def savePlayerLocation(player: Player): Future[Int] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[Int] = Promise()
|
||||
val avatarId = player.avatar.id
|
||||
val position = player.Position
|
||||
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]
|
||||
.filter { _.avatarId == lift(avatarId) }
|
||||
.update(
|
||||
_.px -> lift((position.x * 1000).toInt),
|
||||
_.py -> lift((position.y * 1000).toInt),
|
||||
_.pz -> lift((position.z * 1000).toInt),
|
||||
_.orientation -> lift((player.Orientation.z * 1000).toInt),
|
||||
_.zoneNum -> lift(player.Zone.Number)
|
||||
)
|
||||
)
|
||||
out.completeWith(Future(1))
|
||||
case _ =>
|
||||
out.completeWith(Future(0))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database on information retained in regards to a certain player avatar
|
||||
* when a character associated with the avatar had last logged out of the game.
|
||||
* If that player avatar is found in the database, recover the retained information.
|
||||
* If no entries for that avatar are found, insert a new default-data entry and dummy an entry for use.
|
||||
* Useful mainly for player avatar login evaluations.
|
||||
* @param avatarId a unique identifier number associated with the player avatar
|
||||
* @return when completed, return the persisted data
|
||||
*/
|
||||
def loadSavedAvatarData(avatarId: Long): Future[persistence.Savedavatar] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[persistence.Savedavatar] = Promise()
|
||||
val queryResult = ctx.run(query[persistence.Savedavatar].filter { _.avatarId == lift(avatarId) })
|
||||
queryResult.onComplete {
|
||||
case Success(data) if data.nonEmpty =>
|
||||
out.completeWith(Future(data.head))
|
||||
case _ =>
|
||||
val now = LocalDateTime.now()
|
||||
ctx.run(query[persistence.Savedavatar]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatarId),
|
||||
_.forgetCooldown -> lift(now),
|
||||
_.purchaseCooldowns -> lift(""),
|
||||
_.useCooldowns -> lift("")
|
||||
)
|
||||
)
|
||||
out.completeWith(Future(persistence.Savedavatar(avatarId, now, "", "")))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the database on information retained in regards to a certain player avatar
|
||||
* when a character associated with the avatar had last logged out of the game.
|
||||
* If that player avatar is found in the database, update important information.
|
||||
* Useful mainly for player avatar login evaluations.
|
||||
* @param avatar a unique player avatar
|
||||
* @return when completed, return the number of rows updated
|
||||
*/
|
||||
def saveAvatarData(avatar: Avatar): Future[Int] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[Int] = Promise()
|
||||
val avatarId = avatar.id
|
||||
val queryResult = ctx.run(query[persistence.Savedavatar].filter { _.avatarId == lift(avatarId) })
|
||||
queryResult.onComplete {
|
||||
case Success(results) if results.nonEmpty =>
|
||||
ctx.run(query[persistence.Savedavatar]
|
||||
.filter { _.avatarId == lift(avatarId) }
|
||||
.update(
|
||||
_.purchaseCooldowns -> lift(buildClobfromCooldowns(avatar.cooldowns.purchase)),
|
||||
_.useCooldowns -> lift(buildClobfromCooldowns(avatar.cooldowns.use))
|
||||
)
|
||||
)
|
||||
out.completeWith(Future(1))
|
||||
case _ =>
|
||||
out.completeWith(Future(0))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarActor(
|
||||
|
|
@ -568,14 +875,30 @@ class AvatarActor(
|
|||
val result = 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)
|
||||
_ <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(id)).delete)
|
||||
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(id)).delete)
|
||||
_ <- ctx.run(query[persistence.Friend].filter(_.avatarId == lift(id)).delete)
|
||||
_ <- ctx.run(query[persistence.Ignored].filter(_.avatarId == lift(id)).delete)
|
||||
_ <- ctx.run(query[persistence.Savedavatar].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)))
|
||||
} yield r
|
||||
|
||||
result.onComplete {
|
||||
case Success(_) =>
|
||||
log.debug(s"AvatarActor: avatar $id deleted")
|
||||
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
|
||||
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())
|
||||
)
|
||||
)
|
||||
log.debug(s"AvatarActor: avatar $id deleted")
|
||||
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
|
||||
case _ => ;
|
||||
}
|
||||
sendAvatars(account)
|
||||
case Failure(e) => log.error(e)("db failure")
|
||||
}
|
||||
|
|
@ -597,32 +920,42 @@ class AvatarActor(
|
|||
|
||||
case LoginAvatar(replyTo) =>
|
||||
import ctx._
|
||||
|
||||
val avatarId = avatar.id
|
||||
val result = for {
|
||||
_ <- ctx.run(
|
||||
query[persistence.Avatar].filter(_.id == lift(avatar.id))
|
||||
.update(_.lastLogin -> lift(LocalDateTime.now()))
|
||||
//log this login
|
||||
_ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId))
|
||||
.update(_.lastLogin -> lift(LocalDateTime.now()))
|
||||
)
|
||||
avatarId = avatar.id
|
||||
//log this choice of faction (no empire switching)
|
||||
_ <- ctx.run(query[persistence.Account].filter(_.id == lift(account.id))
|
||||
.update(_.lastFactionId -> lift(avatar.faction.id))
|
||||
)
|
||||
//retrieve avatar data
|
||||
loadouts <- initializeAllLoadouts()
|
||||
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
|
||||
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
|
||||
locker <- loadLocker(avatarId)
|
||||
friends <- loadFriendList(avatarId)
|
||||
ignored <- loadIgnoredList(avatarId)
|
||||
} yield (loadouts, implants, certs, locker, friends, ignored)
|
||||
|
||||
saved <- AvatarActor.loadSavedAvatarData(avatarId)
|
||||
} yield (loadouts, implants, certs, locker, friends, ignored, saved)
|
||||
result.onComplete {
|
||||
case Success((_loadouts, implants, certs, locker, friendsList, ignoredList)) =>
|
||||
case Success((_loadouts, implants, certs, locker, friendsList, ignoredList, saved)) =>
|
||||
avatarCopy(
|
||||
avatar.copy(
|
||||
loadouts = avatar.loadouts.copy(suit = _loadouts),
|
||||
// make sure we always have the base certifications
|
||||
certifications =
|
||||
certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
|
||||
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
|
||||
locker = locker,
|
||||
people = MemberLists(friendsList, ignoredList)
|
||||
people = MemberLists(
|
||||
friend = friendsList,
|
||||
ignored = ignoredList
|
||||
),
|
||||
cooldowns = Cooldowns(
|
||||
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
|
||||
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
|
||||
)
|
||||
)
|
||||
)
|
||||
// if we need to start stamina regeneration
|
||||
|
|
@ -699,6 +1032,7 @@ class AvatarActor(
|
|||
sessionActor ! SessionActor.SendResponse(
|
||||
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
||||
)
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -754,6 +1088,7 @@ class AvatarActor(
|
|||
sessionActor ! SessionActor.SendResponse(
|
||||
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
||||
)
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
//wearing invalid armor?
|
||||
if (
|
||||
if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced
|
||||
|
|
@ -807,6 +1142,7 @@ class AvatarActor(
|
|||
.onComplete {
|
||||
case Success(_) =>
|
||||
replaceAvatar(avatar.copy(certifications = certifications))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
case Failure(exception) =>
|
||||
log.error(exception)("db failure")
|
||||
}
|
||||
|
|
@ -855,6 +1191,7 @@ class AvatarActor(
|
|||
ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = true)
|
||||
)
|
||||
context.self ! ResetImplants()
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
case Failure(exception) => log.error(exception)("db failure")
|
||||
}
|
||||
|
||||
|
|
@ -890,6 +1227,7 @@ class AvatarActor(
|
|||
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
||||
)
|
||||
context.self ! ResetImplants()
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
case Failure(exception) => log.error(exception)("db failure")
|
||||
}
|
||||
|
||||
|
|
@ -937,6 +1275,7 @@ class AvatarActor(
|
|||
case Success(loadout) =>
|
||||
val ldouts = avatar.loadouts
|
||||
replaceAvatar(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, Some(loadout)))))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
refreshLoadout(lineNo)
|
||||
case Failure(exception) =>
|
||||
log.error(exception)("db failure (?)")
|
||||
|
|
@ -946,7 +1285,7 @@ class AvatarActor(
|
|||
case DeleteLoadout(player, loadoutType, number) =>
|
||||
log.info(s"${player.Name} wishes to delete a favorite $loadoutType loadout - #${number + 1}")
|
||||
import ctx._
|
||||
val (lineNo, result) = loadoutType match {
|
||||
val (lineNo: Int, result) = loadoutType match {
|
||||
case LoadoutType.Infantry if avatar.loadouts.suit(number).nonEmpty =>
|
||||
(
|
||||
number,
|
||||
|
|
@ -984,6 +1323,7 @@ class AvatarActor(
|
|||
case Success(_) =>
|
||||
val ldouts = avatar.loadouts
|
||||
avatarCopy(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, None))))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
|
||||
case Failure(exception) =>
|
||||
log.error(exception)("db failure (?)")
|
||||
|
|
@ -1003,7 +1343,6 @@ class AvatarActor(
|
|||
Behaviors.same
|
||||
|
||||
case UpdatePurchaseTime(definition, time) =>
|
||||
// TODO save to db
|
||||
var newTimes = avatar.cooldowns.purchase
|
||||
AvatarActor.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition)).foreach {
|
||||
case (item, name) =>
|
||||
|
|
@ -1281,6 +1620,7 @@ class AvatarActor(
|
|||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
AvatarActor.saveAvatarData(avatar)
|
||||
staminaRegenTimer.cancel()
|
||||
implantTimers.values.foreach(_.cancel())
|
||||
saveLockerFunc()
|
||||
|
|
@ -1323,8 +1663,11 @@ class AvatarActor(
|
|||
avatarCopy(avatar.copy(decoration = avatar.decoration.copy(cosmetics = Some(cosmetics))))
|
||||
session.get.zone.AvatarEvents ! AvatarServiceMessage(
|
||||
session.get.zone.id,
|
||||
AvatarAction
|
||||
.PlanetsideAttributeToAll(session.get.player.GUID, 106, Cosmetic.valuesToAttributeValue(cosmetics))
|
||||
AvatarAction.PlanetsideAttributeToAll(
|
||||
session.get.player.GUID,
|
||||
106,
|
||||
Cosmetic.valuesToAttributeValue(cosmetics)
|
||||
)
|
||||
)
|
||||
p.success(())
|
||||
case Failure(exception) =>
|
||||
|
|
@ -1631,23 +1974,8 @@ class AvatarActor(
|
|||
|
||||
def storeLoadout(owner: Player, label: String, line: Int): Future[Loadout] = {
|
||||
import ctx._
|
||||
val items: String = {
|
||||
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||
//encode holsters
|
||||
owner
|
||||
.Holsters()
|
||||
.zipWithIndex
|
||||
.collect {
|
||||
case (slot, index) if slot.Equipment.nonEmpty =>
|
||||
clobber.append(AvatarActor.encodeLoadoutClobFragment(slot.Equipment.get, index))
|
||||
}
|
||||
//encode inventory
|
||||
owner.Inventory.Items.foreach {
|
||||
case InventoryItem(obj, index) =>
|
||||
clobber.append(AvatarActor.encodeLoadoutClobFragment(obj, index))
|
||||
}
|
||||
clobber.mkString.drop(1)
|
||||
}
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
val items: String = AvatarActor.buildClobFromPlayerLoadout(owner)
|
||||
for {
|
||||
loadouts <- ctx.run(
|
||||
query[persistence.Loadout].filter(_.avatarId == lift(owner.CharId)).filter(_.loadoutNumber == lift(line))
|
||||
|
|
@ -1691,6 +2019,7 @@ class AvatarActor(
|
|||
clobber.mkString.drop(1)
|
||||
}
|
||||
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
for {
|
||||
loadouts <- ctx.run(
|
||||
query[persistence.Vehicleloadout]
|
||||
|
|
@ -1743,6 +2072,7 @@ class AvatarActor(
|
|||
|
||||
def pushLockerClobToDataBase(items: String): Database.ctx.Result[Database.ctx.RunActionResult] = {
|
||||
import ctx._
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
ctx.run(
|
||||
query[persistence.Locker]
|
||||
.filter(_.avatarId == lift(avatar.id))
|
||||
|
|
@ -2116,6 +2446,7 @@ class AvatarActor(
|
|||
people = people.copy(friend = people.friend :+ AvatarFriend(charId, name, PlanetSideEmpire(faction), isOnline))
|
||||
))
|
||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.AddFriend, GameFriend(name, isOnline)))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2141,6 +2472,7 @@ class AvatarActor(
|
|||
.delete
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.RemoveFriend, GameFriend(name)))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2202,6 +2534,7 @@ class AvatarActor(
|
|||
avatar.copy(people = people.copy(ignored = people.ignored :+ AvatarIgnored(charId, name)))
|
||||
)
|
||||
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.AddIgnoredPlayer, GameFriend(name)))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2229,5 +2562,6 @@ class AvatarActor(
|
|||
.delete
|
||||
)
|
||||
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.RemoveIgnoredPlayer, GameFriend(name)))
|
||||
sessionActor ! SessionActor.CharSaved
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import net.psforever.objects.serverobject.mblocker.Locker
|
|||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||
import net.psforever.objects.serverobject.shuttle.OrbitalShuttlePad
|
||||
import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
|
||||
import net.psforever.objects.serverobject.terminals._
|
||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||
|
|
@ -126,6 +127,10 @@ object SessionActor {
|
|||
|
||||
final case class UpdateIgnoredPlayers(msg: FriendsResponse) extends Command
|
||||
|
||||
final case object CharSaved extends Command
|
||||
|
||||
private case object CharSavedMsg extends Command
|
||||
|
||||
/**
|
||||
* The message that progresses some form of user-driven activity with a certain eventual outcome
|
||||
* and potential feedback per cycle.
|
||||
|
|
@ -257,11 +262,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second
|
||||
*/
|
||||
var upstreamMessageCount: Int = 0
|
||||
var zoningType: Zoning.Method.Value = Zoning.Method.None
|
||||
var zoningType: Zoning.Method = Zoning.Method.None
|
||||
var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT
|
||||
var zoningStatus: Zoning.Status.Value = Zoning.Status.None
|
||||
var zoningStatus: Zoning.Status = Zoning.Status.None
|
||||
var zoningCounter: Int = 0
|
||||
var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
|
||||
var loginChatMessage: String = ""
|
||||
lazy val unsignedIntMaxValue: Long = Int.MaxValue.toLong * 2L + 1L
|
||||
var serverTime: Long = 0
|
||||
var amsSpawnPoints: List[SpawnPoint] = Nil
|
||||
|
|
@ -293,6 +299,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
var reviveTimer: Cancellable = Default.Cancellable
|
||||
var respawnTimer: Cancellable = Default.Cancellable
|
||||
var zoningTimer: Cancellable = Default.Cancellable
|
||||
var charSavedTimer: Cancellable = Default.Cancellable
|
||||
|
||||
override def supervisorStrategy: SupervisorStrategy = {
|
||||
import net.psforever.objects.inventory.InventoryDisarrayException
|
||||
|
|
@ -541,6 +548,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
override def postStop(): Unit = {
|
||||
//normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
|
||||
charSavedTimer.cancel()
|
||||
clientKeepAlive.cancel()
|
||||
progressBarUpdate.cancel()
|
||||
reviveTimer.cancel()
|
||||
|
|
@ -660,6 +668,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
session = session.copy(avatar = avatar)
|
||||
*/
|
||||
|
||||
case CharSaved =>
|
||||
renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||
Config.app.game.savedMsg.interruptedByAction.variable
|
||||
)
|
||||
|
||||
case CharSavedMsg =>
|
||||
displayCharSavedMsgThenRenewTimer(
|
||||
Config.app.game.savedMsg.renewal.fixed,
|
||||
Config.app.game.savedMsg.renewal.variable
|
||||
)
|
||||
|
||||
case SetAvatar(avatar) =>
|
||||
session = session.copy(avatar = avatar)
|
||||
if (session.player != null) {
|
||||
|
|
@ -669,7 +689,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
case AvatarActor.AvatarResponse(avatar) =>
|
||||
session = session.copy(avatar = avatar)
|
||||
accountPersistence ! AccountPersistenceService.Login(avatar.name)
|
||||
accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id)
|
||||
|
||||
case AvatarActor.AvatarLoginResponse(avatar) =>
|
||||
avatarLoginResponse(avatar)
|
||||
|
|
@ -1290,38 +1310,35 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
|
||||
})
|
||||
|
||||
case Zoning.Method.Reset =>
|
||||
player.ZoningRequest = Zoning.Method.Login
|
||||
zoningType = Zoning.Method.Login
|
||||
response match {
|
||||
case Some((zone, spawnPoint)) =>
|
||||
loginChatMessage = "@login_reposition_to_friendly_facility" //Your previous location was held by the enemy. You have been moved to the nearest friendly facility.
|
||||
val (pos, ori) = spawnPoint.SpecificPoint(player)
|
||||
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint))
|
||||
case _ =>
|
||||
loginChatMessage = "@login_reposition_to_sanctuary" //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary.
|
||||
RequestSanctuaryZoneSpawn(player, player.Zone.Number)
|
||||
}
|
||||
|
||||
case Zoning.Method.Login =>
|
||||
resolveZoningSpawnPointLoad(response, Zoning.Method.Login)
|
||||
|
||||
case ztype =>
|
||||
if (ztype != Zoning.Method.None) {
|
||||
log.warn(
|
||||
s"SpawnPointResponse: ${player.Name}'s zoning was not in order at the time a response was received; attempting to guess what ${player.Sex.pronounSubject} wants to do"
|
||||
)
|
||||
}
|
||||
val previousZoningType = zoningType
|
||||
val previousZoningType = ztype
|
||||
CancelZoningProcess()
|
||||
PlayerActionsToCancel()
|
||||
CancelAllProximityUnits()
|
||||
DropSpecialSlotItem()
|
||||
continent.Population ! Zone.Population.Release(avatar)
|
||||
response match {
|
||||
case Some((zone, spawnPoint)) =>
|
||||
val obj = continent.GUID(player.VehicleSeated) match {
|
||||
case Some(obj: Vehicle) if !obj.Destroyed => obj
|
||||
case _ => player
|
||||
}
|
||||
val (pos, ori) = spawnPoint.SpecificPoint(obj)
|
||||
if (previousZoningType == Zoning.Method.InstantAction)
|
||||
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint))
|
||||
else
|
||||
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint))
|
||||
case None =>
|
||||
log.warn(
|
||||
s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService"
|
||||
)
|
||||
if (Config.app.game.warpGates.defaultToSanctuaryDestination) {
|
||||
log.warn(s"SpawnPointResponse: sending ${player.Name} home")
|
||||
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
||||
}
|
||||
}
|
||||
resolveZoningSpawnPointLoad(response, previousZoningType)
|
||||
}
|
||||
|
||||
case ICS.DroppodLaunchDenial(errorCode, _) =>
|
||||
|
|
@ -1428,6 +1445,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
|
||||
player.Zone match {
|
||||
case Zone.Nowhere =>
|
||||
RandomSanctuarySpawnPosition(player)
|
||||
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
||||
case zone =>
|
||||
log.trace(s"ZoneResponse: zone ${zone.id} will now load for ${player.Name}")
|
||||
|
|
@ -1440,8 +1458,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
oldZone.AvatarEvents ! Service.Leave()
|
||||
oldZone.LocalEvents ! Service.Leave()
|
||||
oldZone.VehicleEvents ! Service.Leave()
|
||||
if (player.isAlive) {
|
||||
self ! NewPlayerLoaded(player)
|
||||
|
||||
if (player.isAlive && zoningType != Zoning.Method.Reset) {
|
||||
if (player.HasGUID) {
|
||||
HandleNewPlayerLoaded(player)
|
||||
} else {
|
||||
//alive but doesn't have a GUID; probably logging in?
|
||||
_session = _session.copy(zone = Zone.Nowhere)
|
||||
self ! ICS.ZoneResponse(Some(player.Zone))
|
||||
}
|
||||
} else {
|
||||
zoneReload = true
|
||||
cluster ! ICS.GetNearbySpawnPoint(
|
||||
|
|
@ -1453,71 +1478,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
}
|
||||
|
||||
case ICS.ZoneResponse(zone) =>
|
||||
log.trace(s"ZoneResponse: zone ${zone.get.id} will now load for ${player.Name}")
|
||||
loadConfZone = true
|
||||
val oldZone = session.zone
|
||||
session = session.copy(zone = zone.get)
|
||||
//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)
|
||||
player.avatar = avatar
|
||||
interstellarFerry match {
|
||||
case Some(vehicle) if vehicle.PassengerInSeat(player).contains(0) =>
|
||||
TaskWorkflow.execute(registerDrivenVehicle(vehicle, player))
|
||||
case _ =>
|
||||
TaskWorkflow.execute(registerNewAvatar(player))
|
||||
}
|
||||
case ICS.ZoneResponse(Some(zone)) =>
|
||||
HandleZoneResponse(zone)
|
||||
|
||||
case NewPlayerLoaded(tplayer) =>
|
||||
//new zone
|
||||
log.info(s"${tplayer.Name} has spawned into ${session.zone.id}")
|
||||
oldRefsMap.clear()
|
||||
persist = UpdatePersistenceAndRefs
|
||||
tplayer.avatar = avatar
|
||||
session = session.copy(player = tplayer)
|
||||
avatarActor ! AvatarActor.CreateImplants()
|
||||
avatarActor ! AvatarActor.InitializeImplants()
|
||||
//LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar
|
||||
val weaponsEnabled =
|
||||
session.zone.map.name != "map11" && session.zone.map.name != "map12" && session.zone.map.name != "map13"
|
||||
sendResponse(
|
||||
LoadMapMessage(
|
||||
session.zone.map.name,
|
||||
session.zone.id,
|
||||
40100,
|
||||
25,
|
||||
weaponsEnabled,
|
||||
session.zone.map.checksum
|
||||
)
|
||||
)
|
||||
if (isAcceptableNextSpawnPoint()) {
|
||||
//important! the LoadMapMessage must be processed by the client before the avatar is created
|
||||
setupAvatarFunc()
|
||||
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
|
||||
turnCounterFunc = interimUngunnedVehicle match {
|
||||
case Some(_) =>
|
||||
TurnCounterDuringInterimWhileInPassengerSeat
|
||||
case None =>
|
||||
TurnCounterDuringInterim
|
||||
}
|
||||
keepAliveFunc = NormalKeepAlive
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = false
|
||||
persist()
|
||||
} else {
|
||||
//look for different spawn point in same zone
|
||||
cluster ! ICS.GetNearbySpawnPoint(
|
||||
session.zone.Number,
|
||||
tplayer,
|
||||
Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS),
|
||||
context.self
|
||||
)
|
||||
}
|
||||
HandleNewPlayerLoaded(tplayer)
|
||||
|
||||
case PlayerLoaded(tplayer) =>
|
||||
//same zone
|
||||
|
|
@ -1652,24 +1617,101 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
avatarActor ! AvatarActor.SetAccount(account)
|
||||
|
||||
case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
|
||||
log.info(s"LoginInfo: player $name is considered a new character")
|
||||
//TODO poll the database for saved zone and coordinates?
|
||||
log.info(s"LoginInfo: player $name is considered a fresh character")
|
||||
persistFunc = UpdatePersistence(sender())
|
||||
deadState = DeadState.RespawnTime
|
||||
|
||||
session = session.copy(player = new Player(avatar))
|
||||
//xy-coordinates indicate sanctuary spawn bias:
|
||||
player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match {
|
||||
case 0 => Vector3(8192, 8192, 0) //NE
|
||||
case 1 => Vector3(8192, 0, 0) //SE
|
||||
case 2 => Vector3(0, 0, 0) //SW
|
||||
case 3 => Vector3(0, 8192, 0) //NW
|
||||
}
|
||||
DefinitionUtil.applyDefaultLoadout(player)
|
||||
val tplayer = new Player(avatar)
|
||||
session = session.copy(player = tplayer)
|
||||
//actual zone is undefined; going to our sanctuary
|
||||
RandomSanctuarySpawnPosition(tplayer)
|
||||
DefinitionUtil.applyDefaultLoadout(tplayer)
|
||||
avatarActor ! AvatarActor.LoginAvatar(context.self)
|
||||
|
||||
case PlayerToken.LoginInfo(playerName, inZone, pos) =>
|
||||
log.info(s"LoginInfo: player $playerName is already logged in zone ${inZone.id}; rejoining that character")
|
||||
case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
|
||||
log.info(s"LoginInfo: player $name is considered a fresh character")
|
||||
persistFunc = UpdatePersistence(sender())
|
||||
deadState = DeadState.RespawnTime
|
||||
session = session.copy(player = new Player(avatar))
|
||||
player.Zone = inZone
|
||||
optionalSavedData match {
|
||||
case Some(results) =>
|
||||
val health = results.health
|
||||
val hasHealthUponLogin = health > 0
|
||||
val position = Vector3(results.px * 0.001f, results.py * 0.001f, results.pz * 0.001f)
|
||||
player.Position = position
|
||||
player.Orientation = Vector3(0f, 0f, results.orientation * 0.001f)
|
||||
/*
|
||||
@reset_sanctuary=You have been returned to the sanctuary because you played another character.
|
||||
*/
|
||||
if (hasHealthUponLogin) {
|
||||
player.Spawn()
|
||||
player.Health = health
|
||||
player.Armor = results.armor
|
||||
player.ExoSuit = ExoSuitType(results.exosuitNum)
|
||||
AvatarActor.buildContainedEquipmentFromClob(player, results.loadout, log)
|
||||
} else {
|
||||
player.ExoSuit = ExoSuitType.Standard
|
||||
DefinitionUtil.applyDefaultLoadout(player)
|
||||
}
|
||||
if (player.isAlive) {
|
||||
zoningType = Zoning.Method.Login
|
||||
player.ZoningRequest = Zoning.Method.Login
|
||||
zoningChatMessageType = ChatMessageType.UNK_227
|
||||
if (Zones.sanctuaryZoneNumber(player.Faction) != inZone.Number) {
|
||||
val pfaction = player.Faction
|
||||
val buildings = inZone.Buildings.values
|
||||
val ourBuildings = buildings.filter { _.Faction == pfaction }.toSeq
|
||||
val playersInZone = inZone.Players
|
||||
val friendlyPlayersInZone = playersInZone.count { _.faction == pfaction }
|
||||
val noFriendlyPlayersInZone = friendlyPlayersInZone == 0
|
||||
if (inZone.map.cavern) {
|
||||
loginChatMessage = "@reset_sanctuary_locked"
|
||||
//You have been returned to the sanctuary because the location you logged out is not available.
|
||||
player.Zone = Zone.Nowhere
|
||||
} else if (ourBuildings.isEmpty && (amsSpawnPoints.isEmpty || noFriendlyPlayersInZone)) {
|
||||
loginChatMessage = "@reset_sanctuary_locked"
|
||||
//You have been returned to the sanctuary because the location you logged out is not available.
|
||||
player.Zone = Zone.Nowhere
|
||||
} else if (friendlyPlayersInZone > 137 || playersInZone.size > 413) {
|
||||
loginChatMessage = "@reset_sanctuary_full"
|
||||
//You have been returned to the sanctuary because the zone you logged out on is full.
|
||||
player.Zone = Zone.Nowhere
|
||||
} else {
|
||||
val inBuildingSOI = buildings.filter { b =>
|
||||
val soi2 = b.Definition.SOIRadius * b.Definition.SOIRadius
|
||||
Vector3.DistanceSquared(b.Position, position) < soi2
|
||||
}
|
||||
if (inBuildingSOI.nonEmpty) {
|
||||
if (!inBuildingSOI.exists { ourBuildings.contains }) {
|
||||
zoningType = Zoning.Method.Reset
|
||||
player.ZoningRequest = Zoning.Method.Reset
|
||||
zoningChatMessageType = ChatMessageType.UNK_228
|
||||
}
|
||||
} else {
|
||||
if (noFriendlyPlayersInZone) {
|
||||
loginChatMessage = "@reset_sanctuary_inactive"
|
||||
//You have been returned to the sanctuary because the location you logged out is not available.
|
||||
player.Zone = Zone.Nowhere
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//player is dead; go back to sanctuary
|
||||
loginChatMessage = "@reset_sanctuary_inactive"
|
||||
//You have been returned to the sanctuary because the location you logged out is not available.
|
||||
player.Zone = Zone.Nowhere
|
||||
}
|
||||
|
||||
case None =>
|
||||
player.Spawn()
|
||||
player.ExoSuit = ExoSuitType.Standard
|
||||
DefinitionUtil.applyDefaultLoadout(player)
|
||||
}
|
||||
avatarActor ! AvatarActor.LoginAvatar(context.self)
|
||||
|
||||
case PlayerToken.RestoreInfo(playerName, inZone, pos) =>
|
||||
log.info(s"RestoreInfo: player $playerName is already logged in zone ${inZone.id}; rejoining that character")
|
||||
persistFunc = UpdatePersistence(sender())
|
||||
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
|
||||
inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection())
|
||||
|
|
@ -1684,7 +1726,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
case (Some(a), Some(p)) if p.isAlive =>
|
||||
//rejoin current avatar/player
|
||||
log.info(s"LoginInfo: player $playerName is alive")
|
||||
log.info(s"RestoreInfo: player $playerName is alive")
|
||||
deadState = DeadState.Alive
|
||||
session = session.copy(player = p, avatar = a)
|
||||
persist()
|
||||
|
|
@ -1694,7 +1736,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
case (Some(a), Some(p)) =>
|
||||
//convert player to a corpse (unless in vehicle); automatic recall to closest spawn point
|
||||
log.info(s"LoginInfo: player $playerName is dead")
|
||||
log.info(s"RestoreInfo: player $playerName is dead")
|
||||
deadState = DeadState.Dead
|
||||
session = session.copy(player = p, avatar = a)
|
||||
persist()
|
||||
|
|
@ -1705,7 +1747,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
case (Some(a), None) =>
|
||||
//respawn avatar as a new player; automatic recall to closest spawn point
|
||||
log.info(s"LoginInfo: player $playerName had released recently")
|
||||
log.info(s"RestoreInfo: player $playerName had released recently")
|
||||
deadState = DeadState.RespawnTime
|
||||
session = session.copy(
|
||||
player = inZone.Corpses.findLast(c => c.Name == playerName) match {
|
||||
|
|
@ -1724,8 +1766,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
case _ =>
|
||||
//fall back to sanctuary/prior?
|
||||
log.info(s"LoginInfo: player $playerName could not be found in game world")
|
||||
self.forward(PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos))
|
||||
log.info(s"RestoreInfo: player $playerName could not be found in game world")
|
||||
self.forward(PlayerToken.LoginInfo(playerName, Zone.Nowhere, None))
|
||||
}
|
||||
|
||||
case PlayerToken.CanNotLogin(playerName, reason) =>
|
||||
|
|
@ -1750,6 +1792,39 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
log.warn(s"Invalid packet class received: $default from ${sender()}")
|
||||
}
|
||||
|
||||
def RandomSanctuarySpawnPosition(target: Player): Unit = {
|
||||
//xy-coordinates indicate spawn bias:
|
||||
val sanctuaryNum = Zones.sanctuaryZoneNumber(target.Faction)
|
||||
val harts = Zones.zones.find(zone => zone.Number == sanctuaryNum) match {
|
||||
case Some(zone) => zone.Buildings
|
||||
.values
|
||||
.filter(b => b.Amenities.exists { a: Amenity => a.isInstanceOf[OrbitalShuttlePad] })
|
||||
.toSeq
|
||||
case None =>
|
||||
Nil
|
||||
}
|
||||
//compass directions to modify spawn destination
|
||||
val directionBias = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 8) match {
|
||||
case 0 => Vector3(-1, 1,0) //NW
|
||||
case 1 => Vector3( 0, 1,0) //N
|
||||
case 2 => Vector3( 1, 1,0) //NE
|
||||
case 3 => Vector3( 1, 0,0) //E
|
||||
case 4 => Vector3( 1,-1,0) //SE
|
||||
case 5 => Vector3( 0,-1,0) //S
|
||||
case 6 => Vector3(-1,-1,0) //SW
|
||||
case 7 => Vector3(-1, 0,0) //W
|
||||
}
|
||||
if (harts.nonEmpty) {
|
||||
//get a hart building and select one of the spawn facilities surrounding it
|
||||
val campusLocation = harts(math.floor(math.abs(math.random()) * harts.size).toInt).Position
|
||||
target.Position = campusLocation + directionBias
|
||||
} else {
|
||||
//weird issue here; should we log?
|
||||
//select closest spawn point based on global cardinal or ordinal direction bias
|
||||
target.Position = directionBias * 8192f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this player avatar for persistence.
|
||||
* Set to `persist` initially.
|
||||
|
|
@ -1863,6 +1938,45 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
(location.id, location.descriptor.toLowerCase)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process recovered spawn request information to start the process of spawning an avatar player entity
|
||||
* in a specific zone in a specific place in that zone after a certain amount of time has elapsed.<br>
|
||||
* <br>
|
||||
* To load: a zone, a spawn point, a spawning target entity, and the time it takes to spawn are required.
|
||||
* Everything but the spawn point can be determined from the information already available to the context
|
||||
* (does not need to be passed in as a parameter).
|
||||
* The zone is more reliable when passed in as a parameter since local references may have changed.
|
||||
* The spawn point defines the spawn position as well as the spawn orientation.
|
||||
* Any of information provided can be used to calculate the time to spawn.
|
||||
* The session's knowledge of the zoning event is also used to assist with the spawning event.<br>
|
||||
* <br>
|
||||
* If no spawn information has been provided, abort the whole process (unsafe!).
|
||||
* @param spawnPointTarget an optional paired zone entity and a spawn point within the zone
|
||||
* @param zoningType a token that references the manner of zone transfer
|
||||
*/
|
||||
def resolveZoningSpawnPointLoad(spawnPointTarget: Option[(Zone, SpawnPoint)], zoningType: Zoning.Method): Unit = {
|
||||
spawnPointTarget match {
|
||||
case Some((zone, spawnPoint)) =>
|
||||
val obj = continent.GUID(player.VehicleSeated) match {
|
||||
case Some(obj: Vehicle) if !obj.Destroyed => obj
|
||||
case _ => player
|
||||
}
|
||||
val (pos, ori) = spawnPoint.SpecificPoint(obj)
|
||||
if (zoningType == Zoning.Method.InstantAction)
|
||||
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint))
|
||||
else
|
||||
LoadZonePhysicalSpawnPoint(zone.id, pos, ori, CountSpawnDelay(zone.id, spawnPoint, continent.id), Some(spawnPoint))
|
||||
case None =>
|
||||
log.warn(
|
||||
s"SpawnPointResponse: ${player.Name} received no spawn point response when asking InterstellarClusterService"
|
||||
)
|
||||
if (Config.app.game.warpGates.defaultToSanctuaryDestination) {
|
||||
log.warn(s"SpawnPointResponse: sending ${player.Name} home")
|
||||
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the player to a droppod vehicle and hurtle them through the stratosphere in some far off world.
|
||||
* Perform all normal operation standardization (state cancels) as if any of form of zoning was being performed,
|
||||
|
|
@ -1900,14 +2014,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
/**
|
||||
* The user no longer expects to perform a zoning event for this reason.
|
||||
*
|
||||
* @param msg the message to the user
|
||||
* @param msgType the type of message, influencing how it is presented to the user;
|
||||
* normally, this message uses the same value as `zoningChatMessageType`s
|
||||
* normally, this message uses the same value as `zoningChatMessageType`;
|
||||
* defaults to `None`
|
||||
*/
|
||||
def CancelZoningProcessWithReason(msg: String, msgType: Option[ChatMessageType] = None): Unit = {
|
||||
if (zoningStatus > Zoning.Status.None) {
|
||||
if (zoningStatus != Zoning.Status.None) {
|
||||
sendResponse(ChatMsg(msgType.getOrElse(zoningChatMessageType), false, "", msg, None))
|
||||
}
|
||||
CancelZoningProcess()
|
||||
|
|
@ -1927,6 +2040,76 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
instantActionFallbackDestination = None
|
||||
}
|
||||
|
||||
def HandleNewPlayerLoaded(tplayer: Player): Unit = {
|
||||
//new zone
|
||||
log.info(s"${tplayer.Name} has spawned into ${session.zone.id}")
|
||||
oldRefsMap.clear()
|
||||
persist = UpdatePersistenceAndRefs
|
||||
tplayer.avatar = avatar
|
||||
session = session.copy(player = tplayer)
|
||||
avatarActor ! AvatarActor.CreateImplants()
|
||||
avatarActor ! AvatarActor.InitializeImplants()
|
||||
//LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar
|
||||
val weaponsEnabled =
|
||||
session.zone.map.name != "map11" && session.zone.map.name != "map12" && session.zone.map.name != "map13"
|
||||
sendResponse(
|
||||
LoadMapMessage(
|
||||
session.zone.map.name,
|
||||
session.zone.id,
|
||||
40100,
|
||||
25,
|
||||
weaponsEnabled,
|
||||
session.zone.map.checksum
|
||||
)
|
||||
)
|
||||
if (isAcceptableNextSpawnPoint()) {
|
||||
//important! the LoadMapMessage must be processed by the client before the avatar is created
|
||||
setupAvatarFunc()
|
||||
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
|
||||
turnCounterFunc = interimUngunnedVehicle match {
|
||||
case Some(_) =>
|
||||
TurnCounterDuringInterimWhileInPassengerSeat
|
||||
case None if zoningType == Zoning.Method.Login || zoningType == Zoning.Method.Reset =>
|
||||
TurnCounterLogin
|
||||
case None =>
|
||||
TurnCounterDuringInterim
|
||||
}
|
||||
keepAliveFunc = NormalKeepAlive
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = false
|
||||
persist()
|
||||
} else {
|
||||
//look for different spawn point in same zone
|
||||
cluster ! ICS.GetNearbySpawnPoint(
|
||||
session.zone.Number,
|
||||
tplayer,
|
||||
Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS),
|
||||
context.self
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def HandleZoneResponse(foundZone: Zone): Unit = {
|
||||
log.trace(s"ZoneResponse: zone ${foundZone.id} will now load for ${player.Name}")
|
||||
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)
|
||||
player.avatar = avatar
|
||||
interstellarFerry match {
|
||||
case Some(vehicle) if vehicle.PassengerInSeat(player).contains(0) =>
|
||||
TaskWorkflow.execute(registerDrivenVehicle(vehicle, player))
|
||||
case _ =>
|
||||
TaskWorkflow.execute(registerNewAvatar(player))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param toChannel na
|
||||
|
|
@ -2110,6 +2293,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
} else {
|
||||
HandleReleaseAvatar(player, continent)
|
||||
}
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
renewCharSavedTimer(fixedLen=1800L, varLen=0L)
|
||||
|
||||
case AvatarResponse.LoadPlayer(pkt) =>
|
||||
if (tplayer_guid != guid) {
|
||||
|
|
@ -2274,6 +2459,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case AvatarResponse.TerminalOrderResult(terminal_guid, action, result) =>
|
||||
sendResponse(ItemTransactionResultMessage(terminal_guid, action, result))
|
||||
lastTerminalOrderFulfillment = true
|
||||
if (result &&
|
||||
(action == TransactionType.Buy || action == TransactionType.Loadout)) {
|
||||
AvatarActor.savePlayerData(player)
|
||||
renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||
Config.app.game.savedMsg.interruptedByAction.variable
|
||||
)
|
||||
}
|
||||
|
||||
case AvatarResponse.ChangeExosuit(
|
||||
target,
|
||||
|
|
@ -3534,6 +3727,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
//killed during spawn setup or possibly a relog into a corpse (by accident?)
|
||||
player.Actor ! Player.Die()
|
||||
}
|
||||
AvatarActor.savePlayerData(player)
|
||||
displayCharSavedMsgThenRenewTimer(
|
||||
Config.app.game.savedMsg.short.fixed,
|
||||
Config.app.game.savedMsg.short.variable
|
||||
)
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = true
|
||||
}
|
||||
|
|
@ -5603,10 +5801,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
if (action == 29) {
|
||||
log.info(s"${player.Name} is AFK")
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
|
||||
player.AwayFromKeyboard = true
|
||||
} else if (action == 30) {
|
||||
log.info(s"${player.Name} is back")
|
||||
player.AwayFromKeyboard = false
|
||||
renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.renewal.fixed,
|
||||
Config.app.game.savedMsg.renewal.variable
|
||||
)
|
||||
}
|
||||
if (action == GenericActionEnum.DropSpecialItem.id) {
|
||||
DropSpecialSlotItem()
|
||||
|
|
@ -7311,7 +7515,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
* @param tplayer the player to be killed
|
||||
*/
|
||||
def suicide(tplayer: Player): Unit = {
|
||||
tplayer.History(PlayerSuicide())
|
||||
tplayer.History(PlayerSuicide(PlayerSource(tplayer)))
|
||||
tplayer.Actor ! Player.Die()
|
||||
}
|
||||
|
||||
|
|
@ -8535,7 +8739,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
* If the player is alive and mounted in a vehicle, a different can of worms is produced.
|
||||
* The ramifications of these conditions are not fully satisfied until the player loads into the new zone.
|
||||
* Even then, the conclusion becomes delayed while a slightly lagged mechanism hoists players between zones.
|
||||
*
|
||||
* @param zoneId the zone in which the player will be placed
|
||||
* @param pos the game world coordinates where the player will be positioned
|
||||
* @param ori the direction in which the player will be oriented
|
||||
|
|
@ -8598,7 +8801,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -8630,15 +8832,15 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
session = session.copy(player = targetPlayer)
|
||||
TaskWorkflow.execute(taskThenZoneChange(
|
||||
GUIDTask.unregisterObject(continent.GUID, original.avatar.locker),
|
||||
ICS.FindZone(_.id == zoneId, context.self)
|
||||
ICS.FindZone(_.id.equals(zoneId), context.self)
|
||||
))
|
||||
} else if (player.HasGUID) {
|
||||
TaskWorkflow.execute(taskThenZoneChange(
|
||||
GUIDTask.unregisterAvatar(continent.GUID, original),
|
||||
ICS.FindZone(_.id == zoneId, context.self)
|
||||
ICS.FindZone(_.id.equals(zoneId), context.self)
|
||||
))
|
||||
} else {
|
||||
cluster ! ICS.FindZone(_.id == zoneId, context.self)
|
||||
cluster ! ICS.FindZone(_.id.equals(zoneId), context.self)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9372,6 +9574,22 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
turnCounterFunc = NormalTurnCounter
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The upstream counter accumulates when the server receives specific messages from the client.<br>
|
||||
* <br>
|
||||
* This accumulator is assigned after a login event.
|
||||
* The main purpose is to display any messages to the client regarding
|
||||
* if their previous log-out location and their current log-in location are different.
|
||||
* Hereafter, the normal accumulator will be referenced.
|
||||
* @param guid the player's globally unique identifier number
|
||||
*/
|
||||
def TurnCounterLogin(guid: PlanetSideGUID): Unit = {
|
||||
NormalTurnCounter(guid)
|
||||
sendResponse(ChatMsg(zoningChatMessageType, false, "", loginChatMessage, None))
|
||||
CancelZoningProcess()
|
||||
loginChatMessage = ""
|
||||
turnCounterFunc = NormalTurnCounter
|
||||
}
|
||||
|
||||
/**
|
||||
* The normal response to receiving a `KeepAliveMessage` packet from the client.<br>
|
||||
|
|
@ -9853,6 +10071,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
cluster ! ICS.FilterZones(_ => true, context.self)
|
||||
}
|
||||
|
||||
def displayCharSavedMsgThenRenewTimer(fixedLen: Long, varLen: Long): Unit = {
|
||||
charSaved()
|
||||
renewCharSavedTimer(fixedLen, varLen)
|
||||
}
|
||||
|
||||
def renewCharSavedTimer(fixedLen: Long, varLen: Long): Unit = {
|
||||
charSavedTimer.cancel()
|
||||
val delay = (fixedLen + (varLen * scala.math.random()).toInt).seconds
|
||||
charSavedTimer = context.system.scheduler.scheduleOnce(delay, self, SessionActor.CharSavedMsg)
|
||||
}
|
||||
|
||||
def charSaved(): Unit = {
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None))
|
||||
}
|
||||
|
||||
def failWithError(error: String) = {
|
||||
log.error(error)
|
||||
middlewareActor ! MiddlewareActor.Teardown()
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class Player(var avatar: Avatar)
|
|||
private var jumping: Boolean = false
|
||||
private var cloaked: Boolean = false
|
||||
private var afk: Boolean = false
|
||||
private var zoning: Zoning.Method.Value = Zoning.Method.None
|
||||
private var zoning: Zoning.Method = Zoning.Method.None
|
||||
|
||||
private var vehicleSeated: Option[PlanetSideGUID] = None
|
||||
|
||||
|
|
@ -526,9 +526,9 @@ class Player(var avatar: Avatar)
|
|||
Carrying
|
||||
}
|
||||
|
||||
def ZoningRequest: Zoning.Method.Value = zoning
|
||||
def ZoningRequest: Zoning.Method = zoning
|
||||
|
||||
def ZoningRequest_=(request: Zoning.Method.Value): Zoning.Method.Value = {
|
||||
def ZoningRequest_=(request: Zoning.Method): Zoning.Method = {
|
||||
zoning = request
|
||||
ZoningRequest
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ case class Session(
|
|||
account: Account = null,
|
||||
player: Player = null,
|
||||
avatar: Avatar = null,
|
||||
zoningType: Zoning.Method.Value = Zoning.Method.None,
|
||||
zoningType: Zoning.Method = Zoning.Method.None,
|
||||
deadState: DeadState.Value = DeadState.Alive,
|
||||
speed: Float = 1.0f,
|
||||
flying: Boolean = false
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.vital
|
||||
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.ballistics.{PlayerSource, VehicleSource}
|
||||
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
||||
import net.psforever.objects.serverobject.terminals.TerminalDefinition
|
||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason}
|
||||
import net.psforever.objects.vital.interaction.DamageResult
|
||||
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
|
||||
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.types.{ExoSuitType, ImplantType}
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ trait VitalsActivity {
|
|||
}
|
||||
|
||||
trait HealingActivity extends VitalsActivity {
|
||||
def amount: Int
|
||||
val time: Long = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
|
|
@ -33,13 +35,19 @@ final case class HealFromEquipment(
|
|||
) extends HealingActivity
|
||||
|
||||
final case class HealFromTerm(term_def: TerminalDefinition, health: Int, armor: Int)
|
||||
extends HealingActivity
|
||||
extends HealingActivity {
|
||||
def amount: Int = health + armor
|
||||
}
|
||||
|
||||
final case class HealFromImplant(implant: ImplantType, health: Int)
|
||||
extends HealingActivity
|
||||
extends HealingActivity {
|
||||
def amount: Int = health
|
||||
}
|
||||
|
||||
final case class HealFromExoSuitChange(exosuit: ExoSuitType.Value)
|
||||
extends HealingActivity
|
||||
extends HealingActivity {
|
||||
def amount: Int = 0
|
||||
}
|
||||
|
||||
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
||||
extends HealingActivity()
|
||||
|
|
@ -71,9 +79,17 @@ final case class DamageFromPainbox(data: DamageResult)
|
|||
final case class DamageFromEnvironment(data: DamageResult)
|
||||
extends DamagingActivity
|
||||
|
||||
final case class PlayerSuicide()
|
||||
final case class PlayerSuicide(player: PlayerSource)
|
||||
extends DamagingActivity {
|
||||
def data: DamageResult = null //TODO do something
|
||||
private lazy val result = {
|
||||
val out = DamageResult(
|
||||
player,
|
||||
player.copy(health = 0),
|
||||
DamageInteraction(player, SuicideReason(), player.Position)
|
||||
)
|
||||
out
|
||||
}
|
||||
def data: DamageResult = result
|
||||
}
|
||||
|
||||
final case class DamageFromExplodingEntity(data: DamageResult)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,32 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.objects.zones
|
||||
|
||||
import enumeratum.values.{StringEnum, StringEnumEntry}
|
||||
import net.psforever.objects.SpawnPoint
|
||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||
|
||||
object Zoning {
|
||||
object Method extends Enumeration {
|
||||
type Type = Value
|
||||
sealed abstract class Method(val value: String) extends StringEnumEntry
|
||||
sealed abstract class Status(val value: String) extends StringEnumEntry
|
||||
|
||||
val None, InstantAction, OutfitRecall, Recall, Quit = Value
|
||||
object Method extends StringEnum[Method] {
|
||||
val values: IndexedSeq[Method] = findValues
|
||||
|
||||
case object None extends Method(value = "None")
|
||||
case object InstantAction extends Method(value = "InstantAction")
|
||||
case object OutfitRecall extends Method(value = "OutfitRecall")
|
||||
case object Recall extends Method(value = "Recall")
|
||||
case object Quit extends Method(value = "Quit")
|
||||
case object Login extends Method(value = "Login")
|
||||
case object Reset extends Method(value = "Reset")
|
||||
}
|
||||
|
||||
object Status extends Enumeration {
|
||||
type Type = Value
|
||||
object Status extends StringEnum[Status] {
|
||||
val values: IndexedSeq[Status] = findValues
|
||||
|
||||
val None, Request, Countdown = Value
|
||||
case object None extends Status(value = "None")
|
||||
case object Request extends Status(value = "Request")
|
||||
case object Countdown extends Status(value = "Countdown")
|
||||
}
|
||||
|
||||
object Time {
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@ case class Account(
|
|||
created: LocalDateTime = LocalDateTime.now(),
|
||||
lastModified: LocalDateTime = LocalDateTime.now(),
|
||||
inactive: Boolean = false,
|
||||
gm: Boolean = false
|
||||
gm: Boolean = false,
|
||||
lastFactionId: Int = 3
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2022 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
case class Friend(id: Int, avatarId: Long, charId: Long)
|
||||
case class Friend(avatarId: Long, charId: Long)
|
||||
|
||||
case class Ignored(id: Int, avatarId: Long, charId: Long)
|
||||
case class Ignored(avatarId: Long, charId: Long)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2022 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
case class Savedplayer(
|
||||
avatarId: Long,
|
||||
px: Int, //Position.x * 1000
|
||||
py: Int, //Position.y * 1000
|
||||
pz: Int, //Position.z * 1000
|
||||
orientation: Int, //Orientation.z * 1000
|
||||
zoneNum: Int,
|
||||
health: Int,
|
||||
armor: Int,
|
||||
exosuitNum: Int,
|
||||
loadout: String
|
||||
)
|
||||
|
||||
case class Savedavatar(
|
||||
avatarId: Long,
|
||||
forgetCooldown: LocalDateTime,
|
||||
purchaseCooldowns: String,
|
||||
useCooldowns: String
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.services.account
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -11,10 +12,14 @@ import net.psforever.objects._
|
|||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.persistence
|
||||
import net.psforever.types.Vector3
|
||||
import net.psforever.services.{Service, ServiceManager}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
||||
import net.psforever.zones.Zones
|
||||
|
||||
import scala.util.Success
|
||||
|
||||
/**
|
||||
* A global service that manages user behavior as divided into the following three categories:
|
||||
|
|
@ -79,7 +84,7 @@ class AccountPersistenceService extends Actor {
|
|||
* but, updating should be reserved for individual persistence monitor callback (by the user who is being monitored).
|
||||
*/
|
||||
val Started: Receive = {
|
||||
case msg @ AccountPersistenceService.Login(name) =>
|
||||
case msg @ AccountPersistenceService.Login(name, _) =>
|
||||
(accounts.get(name) match {
|
||||
case Some(ref) => ref
|
||||
case None => CreateNewPlayerToken(name)
|
||||
|
|
@ -189,7 +194,7 @@ object AccountPersistenceService {
|
|||
* If the persistence monitor already exists, use that instead and synchronize the data.
|
||||
* @param name the unique name of the player
|
||||
*/
|
||||
final case class Login(name: String)
|
||||
final case class Login(name: String, charId: Long)
|
||||
|
||||
/**
|
||||
* Update the persistence monitor that was setup for a user with the given text descriptor (player name).
|
||||
|
|
@ -269,13 +274,31 @@ class PersistenceMonitor(
|
|||
}
|
||||
|
||||
def receive: Receive = {
|
||||
case AccountPersistenceService.Login(_) =>
|
||||
sender() ! (if (kicked) {
|
||||
PlayerToken.CanNotLogin(name, PlayerToken.DeniedLoginReason.Kicked)
|
||||
} else {
|
||||
UpdateTimer()
|
||||
PlayerToken.LoginInfo(name, inZone, lastPosition)
|
||||
})
|
||||
case AccountPersistenceService.Login(_, charId) =>
|
||||
UpdateTimer() //longer!
|
||||
if (kicked) {
|
||||
//persistence hasn't ended yet, but we were kicked out of the game
|
||||
sender() ! PlayerToken.CanNotLogin(name, PlayerToken.DeniedLoginReason.Kicked)
|
||||
} else {
|
||||
if (inZone != Zone.Nowhere) {
|
||||
//persistence hasn't ended yet
|
||||
sender() ! PlayerToken.RestoreInfo(name, inZone, lastPosition)
|
||||
} else {
|
||||
val replyTo = sender()
|
||||
//proper login; what was our last position according to the database?
|
||||
AvatarActor.loadSavedPlayerData(charId).onComplete {
|
||||
case Success(results) =>
|
||||
Zones.zones.find(zone => zone.Number == results.zoneNum) match {
|
||||
case Some(zone) =>
|
||||
replyTo ! PlayerToken.LoginInfo(name, zone, Some(results))
|
||||
case _ =>
|
||||
replyTo ! PlayerToken.LoginInfo(name, inZone, None)
|
||||
}
|
||||
case _ =>
|
||||
replyTo ! PlayerToken.LoginInfo(name, inZone, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case AccountPersistenceService.Update(_, z, p) if !kicked =>
|
||||
inZone = z
|
||||
|
|
@ -344,7 +367,8 @@ class PersistenceMonitor(
|
|||
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
|
||||
//alive or dead in a vehicle
|
||||
//if the avatar is dead while in a vehicle, they haven't released yet
|
||||
//TODO perform any last minute saving now ...
|
||||
AvatarActor.saveAvatarData(avatar)
|
||||
AvatarActor.finalSavePlayerData(player)
|
||||
(inZone.GUID(player.VehicleSeated) match {
|
||||
case Some(obj: Mountable) =>
|
||||
(Some(obj), obj.Seat(obj.PassengerInSeat(player).getOrElse(-1)))
|
||||
|
|
@ -358,13 +382,14 @@ class PersistenceMonitor(
|
|||
|
||||
case (Some(avatar), Some(player)) =>
|
||||
//alive or dead, as standard Infantry
|
||||
//TODO perform any last minute saving now ...
|
||||
AvatarActor.saveAvatarData(avatar)
|
||||
AvatarActor.finalSavePlayerData(player)
|
||||
PlayerAvatarLogout(avatar, player)
|
||||
|
||||
case (Some(avatar), None) =>
|
||||
//player has released
|
||||
//our last body was turned into a corpse; just the avatar remains
|
||||
//TODO perform any last minute saving now ...
|
||||
AvatarActor.saveAvatarData(avatar)
|
||||
inZone.GUID(avatar.vehicle) match {
|
||||
case Some(obj: Vehicle) if obj.OwnerName.contains(avatar.name) =>
|
||||
obj.Actor ! Vehicle.Ownership(None)
|
||||
|
|
@ -373,7 +398,7 @@ class PersistenceMonitor(
|
|||
AvatarLogout(avatar)
|
||||
|
||||
case _ =>
|
||||
//user stalled during initial session, or was caught in between zone transfer
|
||||
//user stalled during initial session, or was caught in between zone transfer
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,9 +476,22 @@ object PlayerToken {
|
|||
* ("Exists" does not imply an ongoing process and can also mean "just joined the game" here.)
|
||||
* @param name the name of the player
|
||||
* @param zone the zone in which the player is location
|
||||
* @param optionalSavedData additional information about the last time the player was in the game
|
||||
*/
|
||||
final case class LoginInfo(name: String, zone: Zone, optionalSavedData: Option[persistence.Savedplayer])
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param name the name of the player
|
||||
* @param zone the zone in which the player is location
|
||||
* @param position where in the zone the player is located
|
||||
*/
|
||||
final case class LoginInfo(name: String, zone: Zone, position: Vector3)
|
||||
final case class RestoreInfo(name: String, zone: Zone, position: Vector3)
|
||||
|
||||
/**
|
||||
* ...
|
||||
* @param name the name of the player
|
||||
* @param reason why the player can not log into the game
|
||||
*/
|
||||
final case class CanNotLogin(name: String, reason: DeniedLoginReason.Value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ case class GameConfig(
|
|||
sharedMaxCooldown: Boolean,
|
||||
baseCertifications: Seq[Certification],
|
||||
warpGates: WarpGateConfig,
|
||||
cavernRotation: CavernRotationConfig
|
||||
cavernRotation: CavernRotationConfig,
|
||||
savedMsg: SavedMessageEvents
|
||||
)
|
||||
|
||||
case class NewAvatar(
|
||||
|
|
@ -204,3 +205,14 @@ case class CavernRotationConfig(
|
|||
enhancedRotationOrder: Seq[Int],
|
||||
forceRotationImmediately: Boolean
|
||||
)
|
||||
|
||||
case class SavedMessageEvents(
|
||||
short: SavedMessageTimings,
|
||||
renewal: SavedMessageTimings,
|
||||
interruptedByAction: SavedMessageTimings
|
||||
)
|
||||
|
||||
case class SavedMessageTimings(
|
||||
fixed: Long,
|
||||
variable: Long
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class VitalityTest extends Specification {
|
|||
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
||||
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
||||
player.History(VehicleShieldCharge(vSource, 10))
|
||||
player.History(PlayerSuicide())
|
||||
player.History(PlayerSuicide(PlayerSource(player)))
|
||||
ok
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class VitalityTest extends Specification {
|
|||
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
||||
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
||||
player.History(VehicleShieldCharge(vSource, 10))
|
||||
player.History(PlayerSuicide())
|
||||
player.History(PlayerSuicide(PlayerSource(player)))
|
||||
player.History.size mustEqual 7
|
||||
|
||||
val list = player.ClearHistory()
|
||||
|
|
@ -92,7 +92,7 @@ class VitalityTest extends Specification {
|
|||
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
||||
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
||||
player.History(VehicleShieldCharge(vSource, 10))
|
||||
player.History(PlayerSuicide())
|
||||
player.History(PlayerSuicide(PlayerSource(player)))
|
||||
|
||||
player.LastShot match {
|
||||
case Some(resolved_projectile) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue