mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44: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" (
|
CREATE TABLE IF NOT EXISTS "friend" (
|
||||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
|
||||||
"avatar_id" INT NOT NULL REFERENCES avatar (id),
|
"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" (
|
CREATE TABLE IF NOT EXISTS "ignored" (
|
||||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
|
||||||
"avatar_id" INT NOT NULL REFERENCES avatar (id),
|
"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
|
# When set, however, the next zone unlock is carried out regardless of the amount of time remaining
|
||||||
force-rotation-immediately = false
|
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 {
|
anti-cheat {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||||
import akka.actor.Cancellable
|
import akka.actor.Cancellable
|
||||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||||
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
||||||
|
import net.psforever.objects.vital.{DamagingActivity, HealingActivity}
|
||||||
import org.joda.time.{LocalDateTime, Seconds}
|
import org.joda.time.{LocalDateTime, Seconds}
|
||||||
//import org.log4s.Logger
|
//import org.log4s.Logger
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
@ -192,6 +193,48 @@ object AvatarActor {
|
||||||
|
|
||||||
final case class AvatarLoginResponse(avatar: Avatar)
|
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 = {
|
def buildContainedEquipmentFromClob(container: Container, clob: String, log: org.log4s.Logger): Unit = {
|
||||||
clob.split("/").filter(_.trim.nonEmpty).foreach { value =>
|
clob.split("/").filter(_.trim.nonEmpty).foreach { value =>
|
||||||
val (objectType, objectIndex, objectId, toolAmmo) = value.split(",") match {
|
val (objectType, objectIndex, objectId, toolAmmo) = value.split(",") match {
|
||||||
|
|
@ -222,7 +265,7 @@ object AvatarActor {
|
||||||
case "Telepad" | "BoomerTrigger" => ;
|
case "Telepad" | "BoomerTrigger" => ;
|
||||||
//special types of equipment that are not actually loaded
|
//special types of equipment that are not actually loaded
|
||||||
case name =>
|
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 =>
|
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) = {
|
def resolvePurchaseTimeName(faction: PlanetSideEmpire.Value, item: BasicDefinition): (BasicDefinition, String) = {
|
||||||
val factionName: String = faction.toString.toLowerCase
|
val factionName: String = faction.toString.toLowerCase
|
||||||
val name = item match {
|
val name = item match {
|
||||||
|
|
@ -431,6 +525,219 @@ object AvatarActor {
|
||||||
def onlineIfNotIgnored(onlinePlayer: Avatar, observedName: String): Boolean = {
|
def onlineIfNotIgnored(onlinePlayer: Avatar, observedName: String): Boolean = {
|
||||||
!onlinePlayer.people.ignored.exists { f => f.name.equals(observedName) }
|
!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(
|
class AvatarActor(
|
||||||
|
|
@ -568,14 +875,30 @@ class AvatarActor(
|
||||||
val result = for {
|
val result = for {
|
||||||
_ <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(id)).delete)
|
_ <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(id)).delete)
|
||||||
_ <- ctx.run(query[persistence.Loadout].filter(_.avatarId == lift(id)).delete)
|
_ <- ctx.run(query[persistence.Loadout].filter(_.avatarId == lift(id)).delete)
|
||||||
|
_ <- ctx.run(query[persistence.Locker].filter(_.avatarId == lift(id)).delete)
|
||||||
_ <- ctx.run(query[persistence.Certification].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
|
} yield r
|
||||||
|
|
||||||
result.onComplete {
|
result.onComplete {
|
||||||
case Success(_) =>
|
case Success(deleted) =>
|
||||||
log.debug(s"AvatarActor: avatar $id deleted")
|
deleted.headOption match {
|
||||||
sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass)
|
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)
|
sendAvatars(account)
|
||||||
case Failure(e) => log.error(e)("db failure")
|
case Failure(e) => log.error(e)("db failure")
|
||||||
}
|
}
|
||||||
|
|
@ -597,32 +920,42 @@ class AvatarActor(
|
||||||
|
|
||||||
case LoginAvatar(replyTo) =>
|
case LoginAvatar(replyTo) =>
|
||||||
import ctx._
|
import ctx._
|
||||||
|
val avatarId = avatar.id
|
||||||
val result = for {
|
val result = for {
|
||||||
_ <- ctx.run(
|
//log this login
|
||||||
query[persistence.Avatar].filter(_.id == lift(avatar.id))
|
_ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId))
|
||||||
.update(_.lastLogin -> lift(LocalDateTime.now()))
|
.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()
|
loadouts <- initializeAllLoadouts()
|
||||||
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
|
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
|
||||||
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
|
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
|
||||||
locker <- loadLocker(avatarId)
|
locker <- loadLocker(avatarId)
|
||||||
friends <- loadFriendList(avatarId)
|
friends <- loadFriendList(avatarId)
|
||||||
ignored <- loadIgnoredList(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 {
|
result.onComplete {
|
||||||
case Success((_loadouts, implants, certs, locker, friendsList, ignoredList)) =>
|
case Success((_loadouts, implants, certs, locker, friendsList, ignoredList, saved)) =>
|
||||||
avatarCopy(
|
avatarCopy(
|
||||||
avatar.copy(
|
avatar.copy(
|
||||||
loadouts = avatar.loadouts.copy(suit = _loadouts),
|
loadouts = avatar.loadouts.copy(suit = _loadouts),
|
||||||
// make sure we always have the base certifications
|
|
||||||
certifications =
|
certifications =
|
||||||
certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
|
certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
|
||||||
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
|
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
|
||||||
locker = locker,
|
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
|
// if we need to start stamina regeneration
|
||||||
|
|
@ -699,6 +1032,7 @@ class AvatarActor(
|
||||||
sessionActor ! SessionActor.SendResponse(
|
sessionActor ! SessionActor.SendResponse(
|
||||||
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
||||||
)
|
)
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -754,6 +1088,7 @@ class AvatarActor(
|
||||||
sessionActor ! SessionActor.SendResponse(
|
sessionActor ! SessionActor.SendResponse(
|
||||||
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
||||||
)
|
)
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
//wearing invalid armor?
|
//wearing invalid armor?
|
||||||
if (
|
if (
|
||||||
if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced
|
if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced
|
||||||
|
|
@ -807,6 +1142,7 @@ class AvatarActor(
|
||||||
.onComplete {
|
.onComplete {
|
||||||
case Success(_) =>
|
case Success(_) =>
|
||||||
replaceAvatar(avatar.copy(certifications = certifications))
|
replaceAvatar(avatar.copy(certifications = certifications))
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
log.error(exception)("db failure")
|
log.error(exception)("db failure")
|
||||||
}
|
}
|
||||||
|
|
@ -855,6 +1191,7 @@ class AvatarActor(
|
||||||
ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = true)
|
ItemTransactionResultMessage(terminalGuid, TransactionType.Learn, success = true)
|
||||||
)
|
)
|
||||||
context.self ! ResetImplants()
|
context.self ! ResetImplants()
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
case Failure(exception) => log.error(exception)("db failure")
|
case Failure(exception) => log.error(exception)("db failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -890,6 +1227,7 @@ class AvatarActor(
|
||||||
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true)
|
||||||
)
|
)
|
||||||
context.self ! ResetImplants()
|
context.self ! ResetImplants()
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
case Failure(exception) => log.error(exception)("db failure")
|
case Failure(exception) => log.error(exception)("db failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -937,6 +1275,7 @@ class AvatarActor(
|
||||||
case Success(loadout) =>
|
case Success(loadout) =>
|
||||||
val ldouts = avatar.loadouts
|
val ldouts = avatar.loadouts
|
||||||
replaceAvatar(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, Some(loadout)))))
|
replaceAvatar(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, Some(loadout)))))
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
refreshLoadout(lineNo)
|
refreshLoadout(lineNo)
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
log.error(exception)("db failure (?)")
|
log.error(exception)("db failure (?)")
|
||||||
|
|
@ -946,7 +1285,7 @@ class AvatarActor(
|
||||||
case DeleteLoadout(player, loadoutType, number) =>
|
case DeleteLoadout(player, loadoutType, number) =>
|
||||||
log.info(s"${player.Name} wishes to delete a favorite $loadoutType loadout - #${number + 1}")
|
log.info(s"${player.Name} wishes to delete a favorite $loadoutType loadout - #${number + 1}")
|
||||||
import ctx._
|
import ctx._
|
||||||
val (lineNo, result) = loadoutType match {
|
val (lineNo: Int, result) = loadoutType match {
|
||||||
case LoadoutType.Infantry if avatar.loadouts.suit(number).nonEmpty =>
|
case LoadoutType.Infantry if avatar.loadouts.suit(number).nonEmpty =>
|
||||||
(
|
(
|
||||||
number,
|
number,
|
||||||
|
|
@ -984,6 +1323,7 @@ class AvatarActor(
|
||||||
case Success(_) =>
|
case Success(_) =>
|
||||||
val ldouts = avatar.loadouts
|
val ldouts = avatar.loadouts
|
||||||
avatarCopy(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, None))))
|
avatarCopy(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, None))))
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
|
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
log.error(exception)("db failure (?)")
|
log.error(exception)("db failure (?)")
|
||||||
|
|
@ -1003,7 +1343,6 @@ class AvatarActor(
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case UpdatePurchaseTime(definition, time) =>
|
case UpdatePurchaseTime(definition, time) =>
|
||||||
// TODO save to db
|
|
||||||
var newTimes = avatar.cooldowns.purchase
|
var newTimes = avatar.cooldowns.purchase
|
||||||
AvatarActor.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition)).foreach {
|
AvatarActor.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition)).foreach {
|
||||||
case (item, name) =>
|
case (item, name) =>
|
||||||
|
|
@ -1281,6 +1620,7 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
.receiveSignal {
|
.receiveSignal {
|
||||||
case (_, PostStop) =>
|
case (_, PostStop) =>
|
||||||
|
AvatarActor.saveAvatarData(avatar)
|
||||||
staminaRegenTimer.cancel()
|
staminaRegenTimer.cancel()
|
||||||
implantTimers.values.foreach(_.cancel())
|
implantTimers.values.foreach(_.cancel())
|
||||||
saveLockerFunc()
|
saveLockerFunc()
|
||||||
|
|
@ -1323,8 +1663,11 @@ class AvatarActor(
|
||||||
avatarCopy(avatar.copy(decoration = avatar.decoration.copy(cosmetics = Some(cosmetics))))
|
avatarCopy(avatar.copy(decoration = avatar.decoration.copy(cosmetics = Some(cosmetics))))
|
||||||
session.get.zone.AvatarEvents ! AvatarServiceMessage(
|
session.get.zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
session.get.zone.id,
|
session.get.zone.id,
|
||||||
AvatarAction
|
AvatarAction.PlanetsideAttributeToAll(
|
||||||
.PlanetsideAttributeToAll(session.get.player.GUID, 106, Cosmetic.valuesToAttributeValue(cosmetics))
|
session.get.player.GUID,
|
||||||
|
106,
|
||||||
|
Cosmetic.valuesToAttributeValue(cosmetics)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
p.success(())
|
p.success(())
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
|
|
@ -1631,23 +1974,8 @@ class AvatarActor(
|
||||||
|
|
||||||
def storeLoadout(owner: Player, label: String, line: Int): Future[Loadout] = {
|
def storeLoadout(owner: Player, label: String, line: Int): Future[Loadout] = {
|
||||||
import ctx._
|
import ctx._
|
||||||
val items: String = {
|
sessionActor ! SessionActor.CharSaved
|
||||||
val clobber: mutable.StringBuilder = new StringBuilder()
|
val items: String = AvatarActor.buildClobFromPlayerLoadout(owner)
|
||||||
//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)
|
|
||||||
}
|
|
||||||
for {
|
for {
|
||||||
loadouts <- ctx.run(
|
loadouts <- ctx.run(
|
||||||
query[persistence.Loadout].filter(_.avatarId == lift(owner.CharId)).filter(_.loadoutNumber == lift(line))
|
query[persistence.Loadout].filter(_.avatarId == lift(owner.CharId)).filter(_.loadoutNumber == lift(line))
|
||||||
|
|
@ -1691,6 +2019,7 @@ class AvatarActor(
|
||||||
clobber.mkString.drop(1)
|
clobber.mkString.drop(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
for {
|
for {
|
||||||
loadouts <- ctx.run(
|
loadouts <- ctx.run(
|
||||||
query[persistence.Vehicleloadout]
|
query[persistence.Vehicleloadout]
|
||||||
|
|
@ -1743,6 +2072,7 @@ class AvatarActor(
|
||||||
|
|
||||||
def pushLockerClobToDataBase(items: String): Database.ctx.Result[Database.ctx.RunActionResult] = {
|
def pushLockerClobToDataBase(items: String): Database.ctx.Result[Database.ctx.RunActionResult] = {
|
||||||
import ctx._
|
import ctx._
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
ctx.run(
|
ctx.run(
|
||||||
query[persistence.Locker]
|
query[persistence.Locker]
|
||||||
.filter(_.avatarId == lift(avatar.id))
|
.filter(_.avatarId == lift(avatar.id))
|
||||||
|
|
@ -2116,6 +2446,7 @@ class AvatarActor(
|
||||||
people = people.copy(friend = people.friend :+ AvatarFriend(charId, name, PlanetSideEmpire(faction), isOnline))
|
people = people.copy(friend = people.friend :+ AvatarFriend(charId, name, PlanetSideEmpire(faction), isOnline))
|
||||||
))
|
))
|
||||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.AddFriend, GameFriend(name, isOnline)))
|
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.AddFriend, GameFriend(name, isOnline)))
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2141,6 +2472,7 @@ class AvatarActor(
|
||||||
.delete
|
.delete
|
||||||
)
|
)
|
||||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.RemoveFriend, GameFriend(name)))
|
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)))
|
avatar.copy(people = people.copy(ignored = people.ignored :+ AvatarIgnored(charId, name)))
|
||||||
)
|
)
|
||||||
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.AddIgnoredPlayer, GameFriend(name)))
|
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.AddIgnoredPlayer, GameFriend(name)))
|
||||||
|
sessionActor ! SessionActor.CharSaved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2229,5 +2562,6 @@ class AvatarActor(
|
||||||
.delete
|
.delete
|
||||||
)
|
)
|
||||||
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.RemoveIgnoredPlayer, GameFriend(name)))
|
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.mount.Mountable
|
||||||
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
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.structures.{Amenity, Building, StructureType, WarpGate}
|
||||||
import net.psforever.objects.serverobject.terminals._
|
import net.psforever.objects.serverobject.terminals._
|
||||||
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
|
||||||
|
|
@ -126,6 +127,10 @@ object SessionActor {
|
||||||
|
|
||||||
final case class UpdateIgnoredPlayers(msg: FriendsResponse) extends Command
|
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
|
* The message that progresses some form of user-driven activity with a certain eventual outcome
|
||||||
* and potential feedback per cycle.
|
* 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
|
* 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 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 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 zoningCounter: Int = 0
|
||||||
var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
|
var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
|
||||||
|
var loginChatMessage: String = ""
|
||||||
lazy val unsignedIntMaxValue: Long = Int.MaxValue.toLong * 2L + 1L
|
lazy val unsignedIntMaxValue: Long = Int.MaxValue.toLong * 2L + 1L
|
||||||
var serverTime: Long = 0
|
var serverTime: Long = 0
|
||||||
var amsSpawnPoints: List[SpawnPoint] = Nil
|
var amsSpawnPoints: List[SpawnPoint] = Nil
|
||||||
|
|
@ -293,6 +299,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
var reviveTimer: Cancellable = Default.Cancellable
|
var reviveTimer: Cancellable = Default.Cancellable
|
||||||
var respawnTimer: Cancellable = Default.Cancellable
|
var respawnTimer: Cancellable = Default.Cancellable
|
||||||
var zoningTimer: Cancellable = Default.Cancellable
|
var zoningTimer: Cancellable = Default.Cancellable
|
||||||
|
var charSavedTimer: Cancellable = Default.Cancellable
|
||||||
|
|
||||||
override def supervisorStrategy: SupervisorStrategy = {
|
override def supervisorStrategy: SupervisorStrategy = {
|
||||||
import net.psforever.objects.inventory.InventoryDisarrayException
|
import net.psforever.objects.inventory.InventoryDisarrayException
|
||||||
|
|
@ -541,6 +548,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
|
|
||||||
override def postStop(): Unit = {
|
override def postStop(): Unit = {
|
||||||
//normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
|
//normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
|
||||||
|
charSavedTimer.cancel()
|
||||||
clientKeepAlive.cancel()
|
clientKeepAlive.cancel()
|
||||||
progressBarUpdate.cancel()
|
progressBarUpdate.cancel()
|
||||||
reviveTimer.cancel()
|
reviveTimer.cancel()
|
||||||
|
|
@ -660,6 +668,18 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
session = session.copy(avatar = avatar)
|
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) =>
|
case SetAvatar(avatar) =>
|
||||||
session = session.copy(avatar = avatar)
|
session = session.copy(avatar = avatar)
|
||||||
if (session.player != null) {
|
if (session.player != null) {
|
||||||
|
|
@ -669,7 +689,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
|
|
||||||
case AvatarActor.AvatarResponse(avatar) =>
|
case AvatarActor.AvatarResponse(avatar) =>
|
||||||
session = session.copy(avatar = avatar)
|
session = session.copy(avatar = avatar)
|
||||||
accountPersistence ! AccountPersistenceService.Login(avatar.name)
|
accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id)
|
||||||
|
|
||||||
case AvatarActor.AvatarLoginResponse(avatar) =>
|
case AvatarActor.AvatarLoginResponse(avatar) =>
|
||||||
avatarLoginResponse(avatar)
|
avatarLoginResponse(avatar)
|
||||||
|
|
@ -1290,38 +1310,35 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
|
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 =>
|
case ztype =>
|
||||||
if (ztype != Zoning.Method.None) {
|
if (ztype != Zoning.Method.None) {
|
||||||
log.warn(
|
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"
|
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()
|
CancelZoningProcess()
|
||||||
PlayerActionsToCancel()
|
PlayerActionsToCancel()
|
||||||
CancelAllProximityUnits()
|
CancelAllProximityUnits()
|
||||||
DropSpecialSlotItem()
|
DropSpecialSlotItem()
|
||||||
continent.Population ! Zone.Population.Release(avatar)
|
continent.Population ! Zone.Population.Release(avatar)
|
||||||
response match {
|
resolveZoningSpawnPointLoad(response, previousZoningType)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case ICS.DroppodLaunchDenial(errorCode, _) =>
|
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)
|
squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
|
||||||
player.Zone match {
|
player.Zone match {
|
||||||
case Zone.Nowhere =>
|
case Zone.Nowhere =>
|
||||||
|
RandomSanctuarySpawnPosition(player)
|
||||||
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
RequestSanctuaryZoneSpawn(player, currentZone = 0)
|
||||||
case zone =>
|
case zone =>
|
||||||
log.trace(s"ZoneResponse: zone ${zone.id} will now load for ${player.Name}")
|
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.AvatarEvents ! Service.Leave()
|
||||||
oldZone.LocalEvents ! Service.Leave()
|
oldZone.LocalEvents ! Service.Leave()
|
||||||
oldZone.VehicleEvents ! 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 {
|
} else {
|
||||||
zoneReload = true
|
zoneReload = true
|
||||||
cluster ! ICS.GetNearbySpawnPoint(
|
cluster ! ICS.GetNearbySpawnPoint(
|
||||||
|
|
@ -1453,71 +1478,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case ICS.ZoneResponse(zone) =>
|
case ICS.ZoneResponse(Some(zone)) =>
|
||||||
log.trace(s"ZoneResponse: zone ${zone.get.id} will now load for ${player.Name}")
|
HandleZoneResponse(zone)
|
||||||
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 NewPlayerLoaded(tplayer) =>
|
case NewPlayerLoaded(tplayer) =>
|
||||||
//new zone
|
HandleNewPlayerLoaded(tplayer)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case PlayerLoaded(tplayer) =>
|
case PlayerLoaded(tplayer) =>
|
||||||
//same zone
|
//same zone
|
||||||
|
|
@ -1652,24 +1617,101 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
avatarActor ! AvatarActor.SetAccount(account)
|
avatarActor ! AvatarActor.SetAccount(account)
|
||||||
|
|
||||||
case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
|
case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
|
||||||
log.info(s"LoginInfo: player $name is considered a new character")
|
log.info(s"LoginInfo: player $name is considered a fresh character")
|
||||||
//TODO poll the database for saved zone and coordinates?
|
|
||||||
persistFunc = UpdatePersistence(sender())
|
persistFunc = UpdatePersistence(sender())
|
||||||
deadState = DeadState.RespawnTime
|
deadState = DeadState.RespawnTime
|
||||||
|
val tplayer = new Player(avatar)
|
||||||
session = session.copy(player = new Player(avatar))
|
session = session.copy(player = tplayer)
|
||||||
//xy-coordinates indicate sanctuary spawn bias:
|
//actual zone is undefined; going to our sanctuary
|
||||||
player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match {
|
RandomSanctuarySpawnPosition(tplayer)
|
||||||
case 0 => Vector3(8192, 8192, 0) //NE
|
DefinitionUtil.applyDefaultLoadout(tplayer)
|
||||||
case 1 => Vector3(8192, 0, 0) //SE
|
|
||||||
case 2 => Vector3(0, 0, 0) //SW
|
|
||||||
case 3 => Vector3(0, 8192, 0) //NW
|
|
||||||
}
|
|
||||||
DefinitionUtil.applyDefaultLoadout(player)
|
|
||||||
avatarActor ! AvatarActor.LoginAvatar(context.self)
|
avatarActor ! AvatarActor.LoginAvatar(context.self)
|
||||||
|
|
||||||
case PlayerToken.LoginInfo(playerName, inZone, pos) =>
|
case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
|
||||||
log.info(s"LoginInfo: player $playerName is already logged in zone ${inZone.id}; rejoining that character")
|
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())
|
persistFunc = UpdatePersistence(sender())
|
||||||
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
|
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
|
||||||
inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection())
|
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 =>
|
case (Some(a), Some(p)) if p.isAlive =>
|
||||||
//rejoin current avatar/player
|
//rejoin current avatar/player
|
||||||
log.info(s"LoginInfo: player $playerName is alive")
|
log.info(s"RestoreInfo: player $playerName is alive")
|
||||||
deadState = DeadState.Alive
|
deadState = DeadState.Alive
|
||||||
session = session.copy(player = p, avatar = a)
|
session = session.copy(player = p, avatar = a)
|
||||||
persist()
|
persist()
|
||||||
|
|
@ -1694,7 +1736,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
|
|
||||||
case (Some(a), Some(p)) =>
|
case (Some(a), Some(p)) =>
|
||||||
//convert player to a corpse (unless in vehicle); automatic recall to closest spawn point
|
//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
|
deadState = DeadState.Dead
|
||||||
session = session.copy(player = p, avatar = a)
|
session = session.copy(player = p, avatar = a)
|
||||||
persist()
|
persist()
|
||||||
|
|
@ -1705,7 +1747,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
|
|
||||||
case (Some(a), None) =>
|
case (Some(a), None) =>
|
||||||
//respawn avatar as a new player; automatic recall to closest spawn point
|
//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
|
deadState = DeadState.RespawnTime
|
||||||
session = session.copy(
|
session = session.copy(
|
||||||
player = inZone.Corpses.findLast(c => c.Name == playerName) match {
|
player = inZone.Corpses.findLast(c => c.Name == playerName) match {
|
||||||
|
|
@ -1724,8 +1766,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
//fall back to sanctuary/prior?
|
//fall back to sanctuary/prior?
|
||||||
log.info(s"LoginInfo: player $playerName could not be found in game world")
|
log.info(s"RestoreInfo: player $playerName could not be found in game world")
|
||||||
self.forward(PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos))
|
self.forward(PlayerToken.LoginInfo(playerName, Zone.Nowhere, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
case PlayerToken.CanNotLogin(playerName, reason) =>
|
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()}")
|
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.
|
* Update this player avatar for persistence.
|
||||||
* Set to `persist` initially.
|
* Set to `persist` initially.
|
||||||
|
|
@ -1863,6 +1938,45 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
(location.id, location.descriptor.toLowerCase)
|
(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.
|
* 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,
|
* 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.
|
* The user no longer expects to perform a zoning event for this reason.
|
||||||
*
|
|
||||||
* @param msg the message to the user
|
* @param msg the message to the user
|
||||||
* @param msgType the type of message, influencing how it is presented 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`
|
* defaults to `None`
|
||||||
*/
|
*/
|
||||||
def CancelZoningProcessWithReason(msg: String, msgType: Option[ChatMessageType] = None): Unit = {
|
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))
|
sendResponse(ChatMsg(msgType.getOrElse(zoningChatMessageType), false, "", msg, None))
|
||||||
}
|
}
|
||||||
CancelZoningProcess()
|
CancelZoningProcess()
|
||||||
|
|
@ -1927,6 +2040,76 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
instantActionFallbackDestination = None
|
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
|
* na
|
||||||
* @param toChannel na
|
* @param toChannel na
|
||||||
|
|
@ -2110,6 +2293,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
} else {
|
} else {
|
||||||
HandleReleaseAvatar(player, continent)
|
HandleReleaseAvatar(player, continent)
|
||||||
}
|
}
|
||||||
|
AvatarActor.savePlayerLocation(player)
|
||||||
|
renewCharSavedTimer(fixedLen=1800L, varLen=0L)
|
||||||
|
|
||||||
case AvatarResponse.LoadPlayer(pkt) =>
|
case AvatarResponse.LoadPlayer(pkt) =>
|
||||||
if (tplayer_guid != guid) {
|
if (tplayer_guid != guid) {
|
||||||
|
|
@ -2274,6 +2459,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
case AvatarResponse.TerminalOrderResult(terminal_guid, action, result) =>
|
case AvatarResponse.TerminalOrderResult(terminal_guid, action, result) =>
|
||||||
sendResponse(ItemTransactionResultMessage(terminal_guid, action, result))
|
sendResponse(ItemTransactionResultMessage(terminal_guid, action, result))
|
||||||
lastTerminalOrderFulfillment = true
|
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(
|
case AvatarResponse.ChangeExosuit(
|
||||||
target,
|
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?)
|
//killed during spawn setup or possibly a relog into a corpse (by accident?)
|
||||||
player.Actor ! Player.Die()
|
player.Actor ! Player.Die()
|
||||||
}
|
}
|
||||||
|
AvatarActor.savePlayerData(player)
|
||||||
|
displayCharSavedMsgThenRenewTimer(
|
||||||
|
Config.app.game.savedMsg.short.fixed,
|
||||||
|
Config.app.game.savedMsg.short.variable
|
||||||
|
)
|
||||||
upstreamMessageCount = 0
|
upstreamMessageCount = 0
|
||||||
setAvatar = true
|
setAvatar = true
|
||||||
}
|
}
|
||||||
|
|
@ -5603,10 +5801,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
}
|
}
|
||||||
if (action == 29) {
|
if (action == 29) {
|
||||||
log.info(s"${player.Name} is AFK")
|
log.info(s"${player.Name} is AFK")
|
||||||
|
AvatarActor.savePlayerLocation(player)
|
||||||
|
displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
|
||||||
player.AwayFromKeyboard = true
|
player.AwayFromKeyboard = true
|
||||||
} else if (action == 30) {
|
} else if (action == 30) {
|
||||||
log.info(s"${player.Name} is back")
|
log.info(s"${player.Name} is back")
|
||||||
player.AwayFromKeyboard = false
|
player.AwayFromKeyboard = false
|
||||||
|
renewCharSavedTimer(
|
||||||
|
Config.app.game.savedMsg.renewal.fixed,
|
||||||
|
Config.app.game.savedMsg.renewal.variable
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (action == GenericActionEnum.DropSpecialItem.id) {
|
if (action == GenericActionEnum.DropSpecialItem.id) {
|
||||||
DropSpecialSlotItem()
|
DropSpecialSlotItem()
|
||||||
|
|
@ -7311,7 +7515,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
* @param tplayer the player to be killed
|
* @param tplayer the player to be killed
|
||||||
*/
|
*/
|
||||||
def suicide(tplayer: Player): Unit = {
|
def suicide(tplayer: Player): Unit = {
|
||||||
tplayer.History(PlayerSuicide())
|
tplayer.History(PlayerSuicide(PlayerSource(tplayer)))
|
||||||
tplayer.Actor ! Player.Die()
|
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.
|
* 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.
|
* 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.
|
* 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 zoneId the zone in which the player will be placed
|
||||||
* @param pos the game world coordinates where the player will be positioned
|
* @param pos the game world coordinates where the player will be positioned
|
||||||
* @param ori the direction in which the player will be oriented
|
* @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)
|
session = session.copy(player = targetPlayer)
|
||||||
TaskWorkflow.execute(taskThenZoneChange(
|
TaskWorkflow.execute(taskThenZoneChange(
|
||||||
GUIDTask.unregisterObject(continent.GUID, original.avatar.locker),
|
GUIDTask.unregisterObject(continent.GUID, original.avatar.locker),
|
||||||
ICS.FindZone(_.id == zoneId, context.self)
|
ICS.FindZone(_.id.equals(zoneId), context.self)
|
||||||
))
|
))
|
||||||
} else if (player.HasGUID) {
|
} else if (player.HasGUID) {
|
||||||
TaskWorkflow.execute(taskThenZoneChange(
|
TaskWorkflow.execute(taskThenZoneChange(
|
||||||
GUIDTask.unregisterAvatar(continent.GUID, original),
|
GUIDTask.unregisterAvatar(continent.GUID, original),
|
||||||
ICS.FindZone(_.id == zoneId, context.self)
|
ICS.FindZone(_.id.equals(zoneId), context.self)
|
||||||
))
|
))
|
||||||
} else {
|
} 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
|
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>
|
* 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)
|
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) = {
|
def failWithError(error: String) = {
|
||||||
log.error(error)
|
log.error(error)
|
||||||
middlewareActor ! MiddlewareActor.Teardown()
|
middlewareActor ! MiddlewareActor.Teardown()
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class Player(var avatar: Avatar)
|
||||||
private var jumping: Boolean = false
|
private var jumping: Boolean = false
|
||||||
private var cloaked: Boolean = false
|
private var cloaked: Boolean = false
|
||||||
private var afk: 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
|
private var vehicleSeated: Option[PlanetSideGUID] = None
|
||||||
|
|
||||||
|
|
@ -526,9 +526,9 @@ class Player(var avatar: Avatar)
|
||||||
Carrying
|
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
|
zoning = request
|
||||||
ZoningRequest
|
ZoningRequest
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ case class Session(
|
||||||
account: Account = null,
|
account: Account = null,
|
||||||
player: Player = null,
|
player: Player = null,
|
||||||
avatar: Avatar = null,
|
avatar: Avatar = null,
|
||||||
zoningType: Zoning.Method.Value = Zoning.Method.None,
|
zoningType: Zoning.Method = Zoning.Method.None,
|
||||||
deadState: DeadState.Value = DeadState.Alive,
|
deadState: DeadState.Value = DeadState.Alive,
|
||||||
speed: Float = 1.0f,
|
speed: Float = 1.0f,
|
||||||
flying: Boolean = false
|
flying: Boolean = false
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
// Copyright (c) 2020 PSForever
|
// Copyright (c) 2020 PSForever
|
||||||
package net.psforever.objects.vital
|
package net.psforever.objects.vital
|
||||||
|
|
||||||
|
import net.psforever.objects.Player
|
||||||
import net.psforever.objects.ballistics.{PlayerSource, VehicleSource}
|
import net.psforever.objects.ballistics.{PlayerSource, VehicleSource}
|
||||||
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
|
||||||
import net.psforever.objects.serverobject.terminals.TerminalDefinition
|
import net.psforever.objects.serverobject.terminals.TerminalDefinition
|
||||||
import net.psforever.objects.vital.environment.EnvironmentReason
|
import net.psforever.objects.vital.environment.EnvironmentReason
|
||||||
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason}
|
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
|
||||||
import net.psforever.objects.vital.interaction.DamageResult
|
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
|
||||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||||
import net.psforever.types.{ExoSuitType, ImplantType}
|
import net.psforever.types.{ExoSuitType, ImplantType}
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ trait VitalsActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
trait HealingActivity extends VitalsActivity {
|
trait HealingActivity extends VitalsActivity {
|
||||||
|
def amount: Int
|
||||||
val time: Long = System.currentTimeMillis()
|
val time: Long = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,13 +35,19 @@ final case class HealFromEquipment(
|
||||||
) extends HealingActivity
|
) extends HealingActivity
|
||||||
|
|
||||||
final case class HealFromTerm(term_def: TerminalDefinition, health: Int, armor: Int)
|
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)
|
final case class HealFromImplant(implant: ImplantType, health: Int)
|
||||||
extends HealingActivity
|
extends HealingActivity {
|
||||||
|
def amount: Int = health
|
||||||
|
}
|
||||||
|
|
||||||
final case class HealFromExoSuitChange(exosuit: ExoSuitType.Value)
|
final case class HealFromExoSuitChange(exosuit: ExoSuitType.Value)
|
||||||
extends HealingActivity
|
extends HealingActivity {
|
||||||
|
def amount: Int = 0
|
||||||
|
}
|
||||||
|
|
||||||
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
|
||||||
extends HealingActivity()
|
extends HealingActivity()
|
||||||
|
|
@ -71,9 +79,17 @@ final case class DamageFromPainbox(data: DamageResult)
|
||||||
final case class DamageFromEnvironment(data: DamageResult)
|
final case class DamageFromEnvironment(data: DamageResult)
|
||||||
extends DamagingActivity
|
extends DamagingActivity
|
||||||
|
|
||||||
final case class PlayerSuicide()
|
final case class PlayerSuicide(player: PlayerSource)
|
||||||
extends DamagingActivity {
|
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)
|
final case class DamageFromExplodingEntity(data: DamageResult)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,32 @@
|
||||||
|
// Copyright (c) 2020 PSForever
|
||||||
package net.psforever.objects.zones
|
package net.psforever.objects.zones
|
||||||
|
|
||||||
|
import enumeratum.values.{StringEnum, StringEnumEntry}
|
||||||
import net.psforever.objects.SpawnPoint
|
import net.psforever.objects.SpawnPoint
|
||||||
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
import net.psforever.types.{PlanetSideEmpire, Vector3}
|
||||||
|
|
||||||
object Zoning {
|
object Zoning {
|
||||||
object Method extends Enumeration {
|
sealed abstract class Method(val value: String) extends StringEnumEntry
|
||||||
type Type = Value
|
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 {
|
object Status extends StringEnum[Status] {
|
||||||
type Type = Value
|
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 {
|
object Time {
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@ case class Account(
|
||||||
created: LocalDateTime = LocalDateTime.now(),
|
created: LocalDateTime = LocalDateTime.now(),
|
||||||
lastModified: LocalDateTime = LocalDateTime.now(),
|
lastModified: LocalDateTime = LocalDateTime.now(),
|
||||||
inactive: Boolean = false,
|
inactive: Boolean = false,
|
||||||
gm: Boolean = false
|
gm: Boolean = false,
|
||||||
|
lastFactionId: Int = 3
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (c) 2022 PSForever
|
// Copyright (c) 2022 PSForever
|
||||||
package net.psforever.persistence
|
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
|
package net.psforever.services.account
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
||||||
|
import net.psforever.actors.session.AvatarActor
|
||||||
|
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
@ -11,10 +12,14 @@ import net.psforever.objects._
|
||||||
import net.psforever.objects.avatar.Avatar
|
import net.psforever.objects.avatar.Avatar
|
||||||
import net.psforever.objects.serverobject.mount.Mountable
|
import net.psforever.objects.serverobject.mount.Mountable
|
||||||
import net.psforever.objects.zones.Zone
|
import net.psforever.objects.zones.Zone
|
||||||
|
import net.psforever.persistence
|
||||||
import net.psforever.types.Vector3
|
import net.psforever.types.Vector3
|
||||||
import net.psforever.services.{Service, ServiceManager}
|
import net.psforever.services.{Service, ServiceManager}
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
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:
|
* 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).
|
* but, updating should be reserved for individual persistence monitor callback (by the user who is being monitored).
|
||||||
*/
|
*/
|
||||||
val Started: Receive = {
|
val Started: Receive = {
|
||||||
case msg @ AccountPersistenceService.Login(name) =>
|
case msg @ AccountPersistenceService.Login(name, _) =>
|
||||||
(accounts.get(name) match {
|
(accounts.get(name) match {
|
||||||
case Some(ref) => ref
|
case Some(ref) => ref
|
||||||
case None => CreateNewPlayerToken(name)
|
case None => CreateNewPlayerToken(name)
|
||||||
|
|
@ -189,7 +194,7 @@ object AccountPersistenceService {
|
||||||
* If the persistence monitor already exists, use that instead and synchronize the data.
|
* If the persistence monitor already exists, use that instead and synchronize the data.
|
||||||
* @param name the unique name of the player
|
* @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).
|
* 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 = {
|
def receive: Receive = {
|
||||||
case AccountPersistenceService.Login(_) =>
|
case AccountPersistenceService.Login(_, charId) =>
|
||||||
sender() ! (if (kicked) {
|
UpdateTimer() //longer!
|
||||||
PlayerToken.CanNotLogin(name, PlayerToken.DeniedLoginReason.Kicked)
|
if (kicked) {
|
||||||
} else {
|
//persistence hasn't ended yet, but we were kicked out of the game
|
||||||
UpdateTimer()
|
sender() ! PlayerToken.CanNotLogin(name, PlayerToken.DeniedLoginReason.Kicked)
|
||||||
PlayerToken.LoginInfo(name, inZone, lastPosition)
|
} 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 =>
|
case AccountPersistenceService.Update(_, z, p) if !kicked =>
|
||||||
inZone = z
|
inZone = z
|
||||||
|
|
@ -344,7 +367,8 @@ class PersistenceMonitor(
|
||||||
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
|
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
|
||||||
//alive or dead in a vehicle
|
//alive or dead in a vehicle
|
||||||
//if the avatar is dead while in a vehicle, they haven't released yet
|
//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 {
|
(inZone.GUID(player.VehicleSeated) match {
|
||||||
case Some(obj: Mountable) =>
|
case Some(obj: Mountable) =>
|
||||||
(Some(obj), obj.Seat(obj.PassengerInSeat(player).getOrElse(-1)))
|
(Some(obj), obj.Seat(obj.PassengerInSeat(player).getOrElse(-1)))
|
||||||
|
|
@ -358,13 +382,14 @@ class PersistenceMonitor(
|
||||||
|
|
||||||
case (Some(avatar), Some(player)) =>
|
case (Some(avatar), Some(player)) =>
|
||||||
//alive or dead, as standard Infantry
|
//alive or dead, as standard Infantry
|
||||||
//TODO perform any last minute saving now ...
|
AvatarActor.saveAvatarData(avatar)
|
||||||
|
AvatarActor.finalSavePlayerData(player)
|
||||||
PlayerAvatarLogout(avatar, player)
|
PlayerAvatarLogout(avatar, player)
|
||||||
|
|
||||||
case (Some(avatar), None) =>
|
case (Some(avatar), None) =>
|
||||||
//player has released
|
//player has released
|
||||||
//our last body was turned into a corpse; just the avatar remains
|
//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 {
|
inZone.GUID(avatar.vehicle) match {
|
||||||
case Some(obj: Vehicle) if obj.OwnerName.contains(avatar.name) =>
|
case Some(obj: Vehicle) if obj.OwnerName.contains(avatar.name) =>
|
||||||
obj.Actor ! Vehicle.Ownership(None)
|
obj.Actor ! Vehicle.Ownership(None)
|
||||||
|
|
@ -373,7 +398,7 @@ class PersistenceMonitor(
|
||||||
AvatarLogout(avatar)
|
AvatarLogout(avatar)
|
||||||
|
|
||||||
case _ =>
|
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.)
|
* ("Exists" does not imply an ongoing process and can also mean "just joined the game" here.)
|
||||||
* @param name the name of the player
|
* @param name the name of the player
|
||||||
* @param zone the zone in which the player is location
|
* @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
|
* @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)
|
final case class CanNotLogin(name: String, reason: DeniedLoginReason.Value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,8 @@ case class GameConfig(
|
||||||
sharedMaxCooldown: Boolean,
|
sharedMaxCooldown: Boolean,
|
||||||
baseCertifications: Seq[Certification],
|
baseCertifications: Seq[Certification],
|
||||||
warpGates: WarpGateConfig,
|
warpGates: WarpGateConfig,
|
||||||
cavernRotation: CavernRotationConfig
|
cavernRotation: CavernRotationConfig,
|
||||||
|
savedMsg: SavedMessageEvents
|
||||||
)
|
)
|
||||||
|
|
||||||
case class NewAvatar(
|
case class NewAvatar(
|
||||||
|
|
@ -204,3 +205,14 @@ case class CavernRotationConfig(
|
||||||
enhancedRotationOrder: Seq[Int],
|
enhancedRotationOrder: Seq[Int],
|
||||||
forceRotationImmediately: Boolean
|
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(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
||||||
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
||||||
player.History(VehicleShieldCharge(vSource, 10))
|
player.History(VehicleShieldCharge(vSource, 10))
|
||||||
player.History(PlayerSuicide())
|
player.History(PlayerSuicide(PlayerSource(player)))
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ class VitalityTest extends Specification {
|
||||||
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
||||||
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
||||||
player.History(VehicleShieldCharge(vSource, 10))
|
player.History(VehicleShieldCharge(vSource, 10))
|
||||||
player.History(PlayerSuicide())
|
player.History(PlayerSuicide(PlayerSource(player)))
|
||||||
player.History.size mustEqual 7
|
player.History.size mustEqual 7
|
||||||
|
|
||||||
val list = player.ClearHistory()
|
val list = player.ClearHistory()
|
||||||
|
|
@ -92,7 +92,7 @@ class VitalityTest extends Specification {
|
||||||
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
player.History(HealFromExoSuitChange(pSource, ExoSuitType.Standard))
|
||||||
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
player.History(RepairFromTerm(vSource, 10, GlobalDefinitions.order_terminal))
|
||||||
player.History(VehicleShieldCharge(vSource, 10))
|
player.History(VehicleShieldCharge(vSource, 10))
|
||||||
player.History(PlayerSuicide())
|
player.History(PlayerSuicide(PlayerSource(player)))
|
||||||
|
|
||||||
player.LastShot match {
|
player.LastShot match {
|
||||||
case Some(resolved_projectile) =>
|
case Some(resolved_projectile) =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue