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:
Fate-JH 2022-10-11 11:16:12 -04:00 committed by GitHub
parent 190a897dd5
commit 1369da22f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 918 additions and 195 deletions

View file

@ -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)
); );

View file

@ -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

View file

@ -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 {

View file

@ -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
} }
} }

View file

@ -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()

View file

@ -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
} }

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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
) )

View file

@ -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)

View file

@ -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
)

View file

@ -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)
} }

View file

@ -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
)

View file

@ -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) =>