mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
Enemies (No Friends) (#1008)
* database, transfer objects and storage objects for lists of good friends, and of friends that you want to ignore * friends and ignored players get added to lists, to the database, load in appropriate states, and update at basic appropriate times * ignoring players and being ignored by players cuases loss of communication avenues, especially tells, and visibility * modified the [friend list, ignored player list] x avatar query for better performance as the sizes of the results increases using joins and using targeted column selection * obligatory fixes to tests that come with every update
This commit is contained in:
parent
8747320307
commit
190a897dd5
|
|
@ -0,0 +1,11 @@
|
|||
CREATE TABLE IF NOT EXISTS "friend" (
|
||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||
"avatar_id" INT NOT NULL REFERENCES avatar (id),
|
||||
"char_id" INT NOT NULL REFERENCES avatar (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "ignored" (
|
||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||
"avatar_id" INT NOT NULL REFERENCES avatar (id),
|
||||
"char_id" INT NOT NULL REFERENCES avatar (id)
|
||||
);
|
||||
|
|
@ -5,7 +5,14 @@ import java.util.concurrent.atomic.AtomicInteger
|
|||
import akka.actor.Cancellable
|
||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
||||
import net.psforever.objects.avatar._
|
||||
import org.joda.time.{LocalDateTime, Seconds}
|
||||
//import org.log4s.Logger
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
||||
import scala.util.{Failure, Success}
|
||||
import scala.concurrent.duration._
|
||||
//
|
||||
import net.psforever.objects.avatar.{Friend => AvatarFriend, Ignored => AvatarIgnored, _}
|
||||
import net.psforever.objects.definition.converter.CharacterSelectConverter
|
||||
import net.psforever.objects.definition._
|
||||
import net.psforever.objects.inventory.Container
|
||||
|
|
@ -17,22 +24,14 @@ import net.psforever.objects.ballistics.PlayerSource
|
|||
import net.psforever.objects.locker.LockerContainer
|
||||
import net.psforever.objects.vital.HealFromImplant
|
||||
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types._
|
||||
import net.psforever.packet.game.{Friend => GameFriend, _}
|
||||
import net.psforever.types.{MemberAction, PlanetSideEmpire, _}
|
||||
import net.psforever.util.Database._
|
||||
import net.psforever.persistence
|
||||
import net.psforever.util.{Config, Database, DefinitionUtil}
|
||||
import org.joda.time.{LocalDateTime, Seconds}
|
||||
import net.psforever.services.ServiceManager
|
||||
import net.psforever.services.{Service, ServiceManager}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
|
||||
import scala.util.{Failure, Success}
|
||||
import scala.concurrent.duration._
|
||||
import net.psforever.services.Service
|
||||
import org.log4s.Logger
|
||||
|
||||
object AvatarActor {
|
||||
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
|
||||
Behaviors
|
||||
|
|
@ -187,6 +186,8 @@ object AvatarActor {
|
|||
|
||||
private case class SetImplantInitialized(implantType: ImplantType) extends Command
|
||||
|
||||
final case class MemberListRequest(action: MemberAction.Value, name: String) extends Command
|
||||
|
||||
final case class AvatarResponse(avatar: Avatar)
|
||||
|
||||
final case class AvatarLoginResponse(avatar: Avatar)
|
||||
|
|
@ -286,7 +287,7 @@ object AvatarActor {
|
|||
}
|
||||
|
||||
def encodeLockerClob(container: Container): String = {
|
||||
val clobber: StringBuilder = new StringBuilder()
|
||||
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||
container.Inventory.Items.foreach {
|
||||
case InventoryItem(obj, index) =>
|
||||
clobber.append(encodeLoadoutClobFragment(obj, index))
|
||||
|
|
@ -315,6 +316,121 @@ object AvatarActor {
|
|||
case RibbonBarSlot.TermOfService => ribbons.copy(tos = ribbon)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an avatar being online at the moment by matching against their name.
|
||||
* If discovered, run a function based on the avatar's characteristics.
|
||||
* @param name name of a character being sought
|
||||
* @param func functionality that is called upon discovery of the character
|
||||
* @return if found, the discovered avatar, the avatar's account id, and the avatar's faction affiliation
|
||||
*/
|
||||
def getLiveAvatarForFunc(name: String, func: (Long,String,Int)=>Unit): Option[(Avatar, Long, PlanetSideEmpire.Value)] = {
|
||||
if (name.nonEmpty) {
|
||||
LivePlayerList.WorldPopulation({ case (_, a) => a.name.equals(name) }).headOption match {
|
||||
case Some(otherAvatar) =>
|
||||
func(otherAvatar.id, name, otherAvatar.faction.id)
|
||||
Some((otherAvatar, otherAvatar.id.toLong, otherAvatar.faction))
|
||||
case None =>
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an avatar existing the database of avatars by matching against their name.
|
||||
* If discovered, run a function based on the avatar's characteristics.
|
||||
* @param name name of a character being sought
|
||||
* @param func functionality that is called upon discovery of the character
|
||||
* @return if found online, the discovered avatar, the avatar's account id, and the avatar's faction affiliation;
|
||||
* otherwise, always returns `None` as if no avatar was discovered
|
||||
* (the query is probably still in progress)
|
||||
*/
|
||||
def getAvatarForFunc(name: String, func: (Long,String,Int)=>Unit): Option[(Avatar, Long, PlanetSideEmpire.Value)] = {
|
||||
getLiveAvatarForFunc(name, func).orElse {
|
||||
if (name.nonEmpty) {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
ctx.run(query[persistence.Avatar].filter { _.name.equals(lift(name)) }).onComplete {
|
||||
case Success(otherAvatar) =>
|
||||
otherAvatar.headOption match {
|
||||
case Some(a) =>
|
||||
func(a.id, a.name, a.factionId)
|
||||
case _ => ;
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
None //satisfy the orElse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a `(Long, String, PlanetSideEmpire.Value)` function call
|
||||
* into a `(Long, String)` function call.
|
||||
* @param func replacement `(Long, String)` function call
|
||||
* @param charId unique account identifier
|
||||
* @param name unique character name
|
||||
* @param faction the faction affiliation
|
||||
*/
|
||||
def formatForOtherFunc(func: (Long,String)=>Unit)(charId: Long, name: String, faction: Int): Unit = {
|
||||
func(charId, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if one player considered online to the other person.
|
||||
* @param onlinePlayerName name of a player to be determined if they are online
|
||||
* @param observerName name of a player who might see the former and be seen by the former
|
||||
* @return `true`, if one player is visible to the other
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def onlineIfNotIgnored(onlinePlayerName: String, observerName: String): Boolean = {
|
||||
LivePlayerList.WorldPopulation({ case (_, a) => a.name.equals(onlinePlayerName) }).headOption match {
|
||||
case Some(onlinePlayer) => onlineIfNotIgnored(onlinePlayer, observerName)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if one player considered online to the other person.
|
||||
* Neither player can be ignoring the other.
|
||||
* @param onlinePlayerName name of a player to be determined if they are online
|
||||
* @param observer player who might see the former and be seen by the former
|
||||
* @return `true`, if one player is visible to the other
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def onlineIfNotIgnoredEitherWay(observer: Avatar, onlinePlayerName: String): Boolean = {
|
||||
LivePlayerList.WorldPopulation({ case (_, a) => a.name.equals(onlinePlayerName) }) match {
|
||||
case Nil => false //weird case, but ...
|
||||
case onlinePlayer :: Nil => onlineIfNotIgnoredEitherWay(onlinePlayer, observer)
|
||||
case _ => throw new Exception("only trying to find two players, but too many matching search results!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if one player considered online to the other person.
|
||||
* Neither player can be ignoring the other.
|
||||
* @param onlinePlayer player who is online
|
||||
* @param observer player who might see the former
|
||||
* @return `true`, if the other person is not ignoring us;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def onlineIfNotIgnoredEitherWay(onlinePlayer: Avatar, observer: Avatar): Boolean = {
|
||||
onlineIfNotIgnored(onlinePlayer, observer.name) && onlineIfNotIgnored(observer, onlinePlayer.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if one player is considered online to the other person.
|
||||
* The question is whether first player is ignoring the other player.
|
||||
* @param onlinePlayer player who is online
|
||||
* @param observedName name of the player who may be seen
|
||||
* @return `true`, if the other person is visible;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
def onlineIfNotIgnored(onlinePlayer: Avatar, observedName: String): Boolean = {
|
||||
!onlinePlayer.people.ignored.exists { f => f.name.equals(observedName) }
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarActor(
|
||||
|
|
@ -487,22 +603,26 @@ class AvatarActor(
|
|||
query[persistence.Avatar].filter(_.id == lift(avatar.id))
|
||||
.update(_.lastLogin -> lift(LocalDateTime.now()))
|
||||
)
|
||||
avatarId = avatar.id
|
||||
loadouts <- initializeAllLoadouts()
|
||||
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatar.id)))
|
||||
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatar.id)))
|
||||
locker <- loadLocker()
|
||||
} yield (loadouts, implants, certs, locker)
|
||||
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
|
||||
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
|
||||
locker <- loadLocker(avatarId)
|
||||
friends <- loadFriendList(avatarId)
|
||||
ignored <- loadIgnoredList(avatarId)
|
||||
} yield (loadouts, implants, certs, locker, friends, ignored)
|
||||
|
||||
result.onComplete {
|
||||
case Success((loadouts, implants, certs, locker)) =>
|
||||
case Success((_loadouts, implants, certs, locker, friendsList, ignoredList)) =>
|
||||
avatarCopy(
|
||||
avatar.copy(
|
||||
loadouts = loadouts,
|
||||
loadouts = avatar.loadouts.copy(suit = _loadouts),
|
||||
// make sure we always have the base certifications
|
||||
certifications =
|
||||
certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
|
||||
implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
|
||||
locker = locker
|
||||
locker = locker,
|
||||
people = MemberLists(friendsList, ignoredList)
|
||||
)
|
||||
)
|
||||
// if we need to start stamina regeneration
|
||||
|
|
@ -523,7 +643,8 @@ class AvatarActor(
|
|||
Behaviors.same
|
||||
|
||||
case AddFirstTimeEvent(event) =>
|
||||
avatarCopy(avatar.copy(firstTimeEvents = avatar.firstTimeEvents ++ Set(event)))
|
||||
val decor = avatar.decoration
|
||||
avatarCopy(avatar.copy(decoration = decor.copy(firstTimeEvents = decor.firstTimeEvents ++ Set(event))))
|
||||
Behaviors.same
|
||||
|
||||
case LearnCertification(terminalGuid, certification) =>
|
||||
|
|
@ -814,7 +935,8 @@ class AvatarActor(
|
|||
}
|
||||
result.onComplete {
|
||||
case Success(loadout) =>
|
||||
replaceAvatar(avatar.copy(loadouts = avatar.loadouts.updated(lineNo, Some(loadout))))
|
||||
val ldouts = avatar.loadouts
|
||||
replaceAvatar(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, Some(loadout)))))
|
||||
refreshLoadout(lineNo)
|
||||
case Failure(exception) =>
|
||||
log.error(exception)("db failure (?)")
|
||||
|
|
@ -825,7 +947,7 @@ class AvatarActor(
|
|||
log.info(s"${player.Name} wishes to delete a favorite $loadoutType loadout - #${number + 1}")
|
||||
import ctx._
|
||||
val (lineNo, result) = loadoutType match {
|
||||
case LoadoutType.Infantry if avatar.loadouts(number).nonEmpty =>
|
||||
case LoadoutType.Infantry if avatar.loadouts.suit(number).nonEmpty =>
|
||||
(
|
||||
number,
|
||||
ctx.run(
|
||||
|
|
@ -835,7 +957,7 @@ class AvatarActor(
|
|||
.delete
|
||||
)
|
||||
)
|
||||
case LoadoutType.Vehicle if avatar.loadouts(number + 10).nonEmpty =>
|
||||
case LoadoutType.Vehicle if avatar.loadouts.suit(number + 10).nonEmpty =>
|
||||
(
|
||||
number + 10,
|
||||
ctx.run(
|
||||
|
|
@ -845,7 +967,7 @@ class AvatarActor(
|
|||
.delete
|
||||
)
|
||||
)
|
||||
case LoadoutType.Battleframe if avatar.loadouts(number + 15).nonEmpty =>
|
||||
case LoadoutType.Battleframe if avatar.loadouts.suit(number + 15).nonEmpty =>
|
||||
(
|
||||
number + 15,
|
||||
ctx.run(
|
||||
|
|
@ -860,7 +982,8 @@ class AvatarActor(
|
|||
}
|
||||
result.onComplete {
|
||||
case Success(_) =>
|
||||
avatarCopy(avatar.copy(loadouts = avatar.loadouts.updated(lineNo, None)))
|
||||
val ldouts = avatar.loadouts
|
||||
avatarCopy(avatar.copy(loadouts = ldouts.copy(suit = ldouts.suit.updated(lineNo, None))))
|
||||
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
|
||||
case Failure(exception) =>
|
||||
log.error(exception)("db failure (?)")
|
||||
|
|
@ -872,16 +995,16 @@ class AvatarActor(
|
|||
Behaviors.same
|
||||
|
||||
case InitialRefreshLoadouts() =>
|
||||
refreshLoadouts(avatar.loadouts.zipWithIndex)
|
||||
refreshLoadouts(avatar.loadouts.suit.zipWithIndex)
|
||||
Behaviors.same
|
||||
|
||||
case RefreshLoadouts() =>
|
||||
refreshLoadouts(avatar.loadouts.zipWithIndex.collect { case out @ (Some(_), _) => out })
|
||||
refreshLoadouts(avatar.loadouts.suit.zipWithIndex.collect { case out @ (Some(_), _) => out })
|
||||
Behaviors.same
|
||||
|
||||
case UpdatePurchaseTime(definition, time) =>
|
||||
// TODO save to db
|
||||
var newTimes = avatar.purchaseTimes
|
||||
var newTimes = avatar.cooldowns.purchase
|
||||
AvatarActor.resolveSharedPurchaseTimeNames(AvatarActor.resolvePurchaseTimeName(avatar.faction, definition)).foreach {
|
||||
case (item, name) =>
|
||||
Avatar.purchaseCooldowns.get(item) match {
|
||||
|
|
@ -896,19 +1019,20 @@ class AvatarActor(
|
|||
case _ => ;
|
||||
}
|
||||
}
|
||||
avatarCopy(avatar.copy(purchaseTimes = newTimes))
|
||||
avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = newTimes)))
|
||||
Behaviors.same
|
||||
|
||||
case UpdateUseTime(definition, time) =>
|
||||
if (!Avatar.useCooldowns.contains(definition)) {
|
||||
log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown")
|
||||
}
|
||||
avatarCopy(avatar.copy(useTimes = avatar.useTimes.updated(definition.Name, time)))
|
||||
val cdowns = avatar.cooldowns
|
||||
avatarCopy(avatar.copy(cooldowns = cdowns.copy(use = cdowns.use.updated(definition.Name, time))))
|
||||
sessionActor ! SessionActor.UseCooldownRenewed(definition, time)
|
||||
Behaviors.same
|
||||
|
||||
case RefreshPurchaseTimes() =>
|
||||
refreshPurchaseTimes(avatar.purchaseTimes.keys.toSet)
|
||||
refreshPurchaseTimes(avatar.cooldowns.purchase.keys.toSet)
|
||||
Behaviors.same
|
||||
|
||||
case SetVehicle(vehicle) =>
|
||||
|
|
@ -1135,13 +1259,14 @@ class AvatarActor(
|
|||
Behaviors.same
|
||||
|
||||
case SetRibbon(ribbon, bar) =>
|
||||
val previousRibbonBars = avatar.ribbonBars
|
||||
val decor = avatar.decoration
|
||||
val previousRibbonBars = decor.ribbonBars
|
||||
val useRibbonBars = Seq(previousRibbonBars.upper, previousRibbonBars.middle, previousRibbonBars.lower)
|
||||
.indexWhere { _ == ribbon } match {
|
||||
case -1 => previousRibbonBars
|
||||
case n => AvatarActor.changeRibbons(previousRibbonBars, MeritCommendation.None, RibbonBarSlot(n))
|
||||
}
|
||||
replaceAvatar(avatar.copy(ribbonBars = AvatarActor.changeRibbons(useRibbonBars, ribbon, bar)))
|
||||
replaceAvatar(avatar.copy(decoration = decor.copy(ribbonBars = AvatarActor.changeRibbons(useRibbonBars, ribbon, bar))))
|
||||
val player = session.get.player
|
||||
val zone = player.Zone
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
|
|
@ -1149,6 +1274,10 @@ class AvatarActor(
|
|||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DisplayedAwardMessage(player.GUID, ribbon, bar))
|
||||
)
|
||||
Behaviors.same
|
||||
|
||||
case MemberListRequest(action, name) =>
|
||||
memberListAction(action, name)
|
||||
Behaviors.same
|
||||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
|
|
@ -1191,7 +1320,7 @@ class AvatarActor(
|
|||
)
|
||||
.onComplete {
|
||||
case Success(_) =>
|
||||
avatarCopy(avatar.copy(cosmetics = Some(cosmetics)))
|
||||
avatarCopy(avatar.copy(decoration = avatar.decoration.copy(cosmetics = Some(cosmetics))))
|
||||
session.get.zone.AvatarEvents ! AvatarServiceMessage(
|
||||
session.get.zone.id,
|
||||
AvatarAction
|
||||
|
|
@ -1503,7 +1632,7 @@ class AvatarActor(
|
|||
def storeLoadout(owner: Player, label: String, line: Int): Future[Loadout] = {
|
||||
import ctx._
|
||||
val items: String = {
|
||||
val clobber: StringBuilder = new StringBuilder()
|
||||
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||
//encode holsters
|
||||
owner
|
||||
.Holsters()
|
||||
|
|
@ -1547,7 +1676,7 @@ class AvatarActor(
|
|||
def storeVehicleLoadout(owner: Player, label: String, line: Int, vehicle: Vehicle): Future[Loadout] = {
|
||||
import ctx._
|
||||
val items: String = {
|
||||
val clobber: StringBuilder = new StringBuilder()
|
||||
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||
//encode holsters
|
||||
vehicle.Weapons
|
||||
.collect {
|
||||
|
|
@ -1720,7 +1849,7 @@ class AvatarActor(
|
|||
}
|
||||
|
||||
def refreshLoadout(line: Int): Unit = {
|
||||
avatar.loadouts.lift(line) match {
|
||||
avatar.loadouts.suit.lift(line) match {
|
||||
case Some(Some(loadout: InfantryLoadout)) =>
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
FavoritesMessage.Infantry(
|
||||
|
|
@ -1768,12 +1897,12 @@ class AvatarActor(
|
|||
}
|
||||
}
|
||||
|
||||
def loadLocker(): Future[LockerContainer] = {
|
||||
def loadLocker(charId: Long): Future[LockerContainer] = {
|
||||
val locker = Avatar.makeLocker()
|
||||
var notLoaded: Boolean = false
|
||||
import ctx._
|
||||
val out = ctx.run(query[persistence.Locker]
|
||||
.filter(_.avatarId == lift(avatar.id)))
|
||||
.filter(_.avatarId == lift(charId)))
|
||||
.map { entry =>
|
||||
notLoaded = false
|
||||
entry.foreach { contents => AvatarActor.buildContainedEquipmentFromClob(locker, contents.items, log) }
|
||||
|
|
@ -1800,6 +1929,50 @@ class AvatarActor(
|
|||
out
|
||||
}
|
||||
|
||||
|
||||
|
||||
def loadFriendList(avatarId: Long): Future[List[AvatarFriend]] = {
|
||||
import ctx._
|
||||
val out: Promise[List[AvatarFriend]] = Promise()
|
||||
|
||||
val queryResult = ctx.run(
|
||||
query[persistence.Friend].filter { _.avatarId == lift(avatarId) }
|
||||
.join(query[persistence.Avatar])
|
||||
.on { case (friend, avatar) => friend.charId == avatar.id }
|
||||
.map { case (_, avatar) => (avatar.id, avatar.name, avatar.factionId) }
|
||||
)
|
||||
queryResult.onComplete {
|
||||
case Success(list) =>
|
||||
out.completeWith(Future(
|
||||
list.map { case (id, name, faction) => AvatarFriend(id, name, PlanetSideEmpire(faction)) }.toList
|
||||
))
|
||||
case _ =>
|
||||
out.completeWith(Future(List.empty[AvatarFriend]))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def loadIgnoredList(avatarId: Long): Future[List[AvatarIgnored]] = {
|
||||
import ctx._
|
||||
val out: Promise[List[AvatarIgnored]] = Promise()
|
||||
|
||||
val queryResult = ctx.run(
|
||||
query[persistence.Ignored].filter { _.avatarId == lift(avatarId) }
|
||||
.join(query[persistence.Avatar])
|
||||
.on { case (friend, avatar) => friend.charId == avatar.id }
|
||||
.map { case (_, avatar) => (avatar.id, avatar.name) }
|
||||
)
|
||||
queryResult.onComplete {
|
||||
case Success(list) =>
|
||||
out.completeWith(Future(
|
||||
list.map { case (id, name) => AvatarIgnored(id, name) }.toList
|
||||
))
|
||||
case _ =>
|
||||
out.completeWith(Future(List.empty[AvatarIgnored]))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = {
|
||||
if (staminaRegenTimer.isCancelled) {
|
||||
defaultStaminaRegen(initialDelay)
|
||||
|
|
@ -1828,7 +2001,7 @@ class AvatarActor(
|
|||
def refreshPurchaseTimes(keys: Set[String]): Unit = {
|
||||
var keysToDrop: Seq[String] = Nil
|
||||
keys.foreach { key =>
|
||||
avatar.purchaseTimes.find { case (name, _) => name.equals(key) } match {
|
||||
avatar.cooldowns.purchase.find { case (name, _) => name.equals(key) } match {
|
||||
case Some((name, purchaseTime)) =>
|
||||
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
|
||||
Avatar.purchaseCooldowns.find(_._1.Name == name) match {
|
||||
|
|
@ -1847,7 +2020,8 @@ class AvatarActor(
|
|||
}
|
||||
}
|
||||
if (keysToDrop.nonEmpty) {
|
||||
avatarCopy(avatar.copy(purchaseTimes = avatar.purchaseTimes.removedAll(keysToDrop)))
|
||||
val cdown = avatar.cooldowns
|
||||
avatarCopy(avatar.copy(cooldowns = cdown.copy(purchase = cdown.purchase.removedAll(keysToDrop))))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1870,4 +2044,190 @@ class AvatarActor(
|
|||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Branch based on behavior of the request for the friends list or the ignored people list.
|
||||
* @param action nature of the request
|
||||
* @param name other player's name (can not be our name)
|
||||
*/
|
||||
def memberListAction(action: MemberAction.Value, name: String): Unit = {
|
||||
if (!name.equals(avatar.name)) {
|
||||
action match {
|
||||
case MemberAction.InitializeFriendList => memberActionListManagement(action, transformFriendsList)
|
||||
case MemberAction.InitializeIgnoreList => memberActionListManagement(action, transformIgnoredList)
|
||||
case MemberAction.UpdateFriend => memberActionUpdateFriend(name)
|
||||
case MemberAction.AddFriend => getAvatarForFunc(name, memberActionAddFriend)
|
||||
case MemberAction.RemoveFriend => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveFriend))
|
||||
case MemberAction.AddIgnoredPlayer => getAvatarForFunc(name, memberActionAddIgnored)
|
||||
case MemberAction.RemoveIgnoredPlayer => getAvatarForFunc(name, formatForOtherFunc(memberActionRemoveIgnored))
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the friends list in a list of packet entities.
|
||||
* @return a list of `Friends` suitable for putting into a packet
|
||||
*/
|
||||
def transformFriendsList(): List[GameFriend] = {
|
||||
avatar.people.friend.map { f => GameFriend(f.name, f.online)}
|
||||
}
|
||||
/**
|
||||
* Transform the ignored players list in a list of packet entities.
|
||||
* @return a list of `Friends` suitable for putting into a packet
|
||||
*/
|
||||
def transformIgnoredList(): List[GameFriend] = {
|
||||
avatar.people.ignored.map { f => GameFriend(f.name, f.online)}
|
||||
}
|
||||
/**
|
||||
* Reload the list of friend players or ignored players for the client.
|
||||
* This does not update any player's online status, but merely reloads current states.
|
||||
* @param action nature of the request
|
||||
* (either `InitializeFriendList` or `InitializeIgnoreList`, hopefully)
|
||||
* @param listFunc transformation function that produces data suitable for a game paket
|
||||
*/
|
||||
def memberActionListManagement(action: MemberAction.Value, listFunc: ()=>List[GameFriend]): Unit = {
|
||||
FriendsResponse.packetSequence(action, listFunc()).foreach { msg =>
|
||||
sessionActor ! SessionActor.SendResponse(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another player's data to the list of friend players and report back whether or not that player is online.
|
||||
* Update the database appropriately.
|
||||
* @param charId unique account identifier
|
||||
* @param name unique character name
|
||||
* @param faction a faction affiliation
|
||||
*/
|
||||
def memberActionAddFriend(charId: Long, name: String, faction: Int): Unit = {
|
||||
val people = avatar.people
|
||||
people.friend.find { _.name.equals(name) } match {
|
||||
case Some(_) => ;
|
||||
case None =>
|
||||
import ctx._
|
||||
ctx.run(query[persistence.Friend]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatar.id.toLong),
|
||||
_.charId -> lift(charId)
|
||||
)
|
||||
)
|
||||
val isOnline = onlineIfNotIgnoredEitherWay(avatar, name)
|
||||
replaceAvatar(avatar.copy(
|
||||
people = people.copy(friend = people.friend :+ AvatarFriend(charId, name, PlanetSideEmpire(faction), isOnline))
|
||||
))
|
||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.AddFriend, GameFriend(name, isOnline)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove another player's data from the list of friend players.
|
||||
* Update the database appropriately.
|
||||
* @param charId unique account identifier
|
||||
* @param name unique character name
|
||||
*/
|
||||
def memberActionRemoveFriend(charId: Long, name: String): Unit = {
|
||||
import ctx._
|
||||
val people = avatar.people
|
||||
people.friend.find { _.name.equals(name) } match {
|
||||
case Some(_) =>
|
||||
replaceAvatar(
|
||||
avatar.copy(people = people.copy(friend = people.friend.filterNot { _.charId == charId }))
|
||||
)
|
||||
case None => ;
|
||||
}
|
||||
ctx.run(query[persistence.Friend]
|
||||
.filter(_.avatarId == lift(avatar.id))
|
||||
.filter(_.charId == lift(charId))
|
||||
.delete
|
||||
)
|
||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.RemoveFriend, GameFriend(name)))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name unique character name
|
||||
* @return if the avatar is found, that avatar's unique identifier and the avatar's faction affiliation
|
||||
*/
|
||||
def memberActionUpdateFriend(name: String): Option[(Long, PlanetSideEmpire.Value)] = {
|
||||
if (name.nonEmpty) {
|
||||
val people = avatar.people
|
||||
people.friend.find { _.name.equals(name) } match {
|
||||
case Some(otherFriend) =>
|
||||
val (out, online) = LivePlayerList.WorldPopulation({ case (_, a) => a.name.equals(name) }).headOption match {
|
||||
case Some(otherAvatar) =>
|
||||
(
|
||||
Some((otherAvatar.id.toLong, otherAvatar.faction)),
|
||||
onlineIfNotIgnoredEitherWay(otherAvatar, avatar)
|
||||
)
|
||||
case None =>
|
||||
(None, false)
|
||||
}
|
||||
replaceAvatar(avatar.copy(
|
||||
people = people.copy(
|
||||
friend = people.friend.filterNot { _.name.equals(name) } :+ otherFriend.copy(online = online)
|
||||
)
|
||||
))
|
||||
sessionActor ! SessionActor.SendResponse(FriendsResponse(MemberAction.UpdateFriend, GameFriend(name, online)))
|
||||
out
|
||||
case None =>
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another player's data to the list of ignored players.
|
||||
* Update the database appropriately.
|
||||
* The change affects not only this player but also the player being ignored
|
||||
* by denying online visibility of the former to the latter.
|
||||
* @param charId unique account identifier
|
||||
* @param name unique character name
|
||||
* @param faction a faction affiliation
|
||||
*/
|
||||
def memberActionAddIgnored(charId: Long, name: String, faction: Int): Unit = {
|
||||
val people = avatar.people
|
||||
people.ignored.find { _.name.equals(name) } match {
|
||||
case Some(_) => ;
|
||||
case None =>
|
||||
import ctx._
|
||||
ctx.run(query[persistence.Ignored]
|
||||
.insert(
|
||||
_.avatarId -> lift(avatar.id.toLong),
|
||||
_.charId -> lift(charId)
|
||||
)
|
||||
)
|
||||
replaceAvatar(
|
||||
avatar.copy(people = people.copy(ignored = people.ignored :+ AvatarIgnored(charId, name)))
|
||||
)
|
||||
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.AddIgnoredPlayer, GameFriend(name)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove another player's data from the list of ignored players.
|
||||
* Update the database appropriately.
|
||||
* The change affects not only this player but also the player formerly being ignored
|
||||
* by restoring online visibility of the former to the latter.
|
||||
* @param charId unique account identifier
|
||||
* @param name unique character name
|
||||
*/
|
||||
def memberActionRemoveIgnored(charId: Long, name: String): Unit = {
|
||||
import ctx._
|
||||
val people = avatar.people
|
||||
people.ignored.find { _.name.equals(name) } match {
|
||||
case Some(_) =>
|
||||
replaceAvatar(
|
||||
avatar.copy(people = people.copy(ignored = people.ignored.filterNot { _.charId == charId }))
|
||||
)
|
||||
case None => ;
|
||||
}
|
||||
ctx.run(query[persistence.Ignored]
|
||||
.filter(_.avatarId == lift(avatar.id.toLong))
|
||||
.filter(_.charId == lift(charId))
|
||||
.delete
|
||||
)
|
||||
sessionActor ! SessionActor.UpdateIgnoredPlayers(FriendsResponse(MemberAction.RemoveIgnoredPlayer, GameFriend(name)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import akka.actor.typed.scaladsl.adapter._
|
|||
import net.psforever.services.{CavernRotationService, InterstellarClusterService}
|
||||
import net.psforever.types.ChatMessageType.UNK_229
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
object ChatActor {
|
||||
def apply(
|
||||
sessionActor: ActorRef[SessionActor.Command],
|
||||
|
|
@ -123,6 +125,13 @@ class ChatActor(
|
|||
var chatService: Option[ActorRef[ChatService.Command]] = None
|
||||
var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
|
||||
var silenceTimer: Cancellable = Default.Cancellable
|
||||
/**
|
||||
* when another player is listed as one of our ignored players,
|
||||
* and that other player sends an emote,
|
||||
* that player is assigned a cooldown and only one emote per period will be seen<br>
|
||||
* key - character unique avatar identifier, value - when the current cooldown period will end
|
||||
*/
|
||||
var ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]()
|
||||
|
||||
val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
|
||||
case ChatService.MessageResponse(_session, message, channel) => IncomingMessage(_session, message, channel)
|
||||
|
|
@ -699,11 +708,21 @@ class ChatActor(
|
|||
}
|
||||
|
||||
case (CMT_TELL, _, _) if !session.player.silenced =>
|
||||
chatService ! ChatService.Message(
|
||||
session,
|
||||
message,
|
||||
ChatChannel.Default()
|
||||
)
|
||||
if (AvatarActor.onlineIfNotIgnored(message.recipient, session.avatar.name)) {
|
||||
chatService ! ChatService.Message(
|
||||
session,
|
||||
message,
|
||||
ChatChannel.Default()
|
||||
)
|
||||
} else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_target", None)
|
||||
)
|
||||
} else {
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
ChatMsg(ChatMessageType.UNK_45, false, "none", "@notell_ignore", None)
|
||||
)
|
||||
}
|
||||
|
||||
case (CMT_BROADCAST, _, _) if !session.player.silenced =>
|
||||
chatService ! ChatService.Message(
|
||||
|
|
@ -913,7 +932,7 @@ class ChatActor(
|
|||
}
|
||||
|
||||
case (CMT_TOGGLE_HAT, _, contents) =>
|
||||
val cosmetics = session.avatar.cosmetics.getOrElse(Set())
|
||||
val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
|
||||
val nextCosmetics = contents match {
|
||||
case "off" =>
|
||||
cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
|
||||
|
|
@ -937,7 +956,7 @@ class ChatActor(
|
|||
)
|
||||
|
||||
case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
|
||||
val cosmetics = session.avatar.cosmetics.getOrElse(Set())
|
||||
val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
|
||||
|
||||
val cosmetic = message.messageType match {
|
||||
case CMT_HIDE_HELMET => Cosmetic.NoHelmet
|
||||
|
|
@ -1055,25 +1074,47 @@ class ChatActor(
|
|||
|
||||
case IncomingMessage(fromSession, message, channel) =>
|
||||
message.messageType match {
|
||||
case CMT_TELL | U_CMT_TELLFROM | CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | UNK_45 | UNK_71 |
|
||||
CMT_NOTE | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
|
||||
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_227 | UNK_229 =>
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
|
||||
if (AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)) {
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
}
|
||||
case CMT_OPEN =>
|
||||
if (
|
||||
session.zone == fromSession.zone &&
|
||||
Vector3.Distance(session.player.Position, fromSession.player.Position) < 25 &&
|
||||
session.player.Faction == fromSession.player.Faction
|
||||
Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 625 &&
|
||||
session.player.Faction == fromSession.player.Faction &&
|
||||
AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)
|
||||
) {
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
}
|
||||
case CMT_TELL | U_CMT_TELLFROM |
|
||||
CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
|
||||
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
case CMT_VOICE =>
|
||||
if (
|
||||
session.zone == fromSession.zone &&
|
||||
Vector3.Distance(session.player.Position, fromSession.player.Position) < 25 ||
|
||||
Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 625 ||
|
||||
message.contents.startsWith("SH") // tactical squad voice macro
|
||||
) {
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
val name = fromSession.avatar.name
|
||||
if (!session.avatar.people.ignored.exists { f => f.name.equals(name) } ||
|
||||
{
|
||||
val id = fromSession.avatar.id.toLong
|
||||
val curr = System.currentTimeMillis()
|
||||
ignoredEmoteCooldown.get(id) match {
|
||||
case None =>
|
||||
ignoredEmoteCooldown.put(id, curr + 15000L)
|
||||
true
|
||||
case Some(time) if time < curr =>
|
||||
ignoredEmoteCooldown.put(id, curr + 15000L)
|
||||
true
|
||||
case _ =>
|
||||
false
|
||||
}}
|
||||
) {
|
||||
sessionActor ! SessionActor.SendResponse(message)
|
||||
}
|
||||
}
|
||||
case CMT_SILENCE =>
|
||||
val args = message.contents.split(" ")
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ object SessionActor {
|
|||
|
||||
final case class UseCooldownRenewed(definition: BasicDefinition, time: LocalDateTime) extends Command
|
||||
|
||||
final case class UpdateIgnoredPlayers(msg: FriendsResponse) extends Command
|
||||
|
||||
/**
|
||||
* The message that progresses some form of user-driven activity with a certain eventual outcome
|
||||
* and potential feedback per cycle.
|
||||
|
|
@ -681,6 +683,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case AvatarServiceResponse(toChannel, guid, reply) =>
|
||||
HandleAvatarServiceResponse(toChannel, guid, reply)
|
||||
|
||||
case UpdateIgnoredPlayers(msg) =>
|
||||
sendResponse(msg)
|
||||
msg.friends.foreach { f =>
|
||||
galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name))
|
||||
}
|
||||
|
||||
case SendResponse(packet) =>
|
||||
sendResponse(packet)
|
||||
|
||||
|
|
@ -829,6 +837,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
|
||||
)
|
||||
)
|
||||
|
||||
case GalaxyResponse.MapUpdate(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
|
|
@ -904,6 +913,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
||||
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
||||
|
||||
case GalaxyResponse.LogStatusChange(name) =>
|
||||
if (avatar.people.friend.exists { _.name.equals(name) }) {
|
||||
avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
|
||||
}
|
||||
|
||||
case GalaxyResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
}
|
||||
|
|
@ -1380,6 +1394,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
context.self
|
||||
)
|
||||
LivePlayerList.Add(avatar.id, avatar)
|
||||
galaxyService.tell(GalaxyServiceMessage(GalaxyAction.LogStatusChange(avatar.name)), context.parent)
|
||||
//PropertyOverrideMessage
|
||||
|
||||
implicit val timeout = Timeout(1 seconds)
|
||||
|
|
@ -1391,8 +1406,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
|
||||
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
||||
sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
|
||||
sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
|
||||
(
|
||||
//friend list (some might be online)
|
||||
FriendsResponse.packetSequence(
|
||||
MemberAction.InitializeFriendList,
|
||||
avatar.people.friend
|
||||
.map { f =>
|
||||
game.Friend(f.name, AvatarActor.onlineIfNotIgnoredEitherWay(avatar, f.name))
|
||||
}
|
||||
) ++
|
||||
//ignored list (no one ever online)
|
||||
FriendsResponse.packetSequence(
|
||||
MemberAction.InitializeIgnoreList,
|
||||
avatar.people.ignored.map { f => game.Friend(f.name) }
|
||||
)
|
||||
).foreach { sendResponse }
|
||||
//the following subscriptions last until character switch/logout
|
||||
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
||||
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
|
||||
|
|
@ -3605,7 +3633,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
||||
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
|
||||
//only need to load these once - they persist between zone transfers and respawns
|
||||
avatar.squadLoadouts.zipWithIndex.foreach {
|
||||
avatar.loadouts.squad.zipWithIndex.foreach {
|
||||
case (Some(loadout), index) =>
|
||||
sendResponse(
|
||||
SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task))
|
||||
|
|
@ -6260,7 +6288,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
|
||||
case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) => ;
|
||||
|
||||
case msg @ FriendsRequest(action, friend) => ;
|
||||
case FriendsRequest(action, name) =>
|
||||
avatarActor ! AvatarActor.MemberListRequest(action, name)
|
||||
|
||||
case msg @ HitHint(source_guid, player_guid) => ; //HitHint is manually distributed for proper operation
|
||||
|
||||
|
|
@ -6858,7 +6887,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj)
|
||||
val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj)
|
||||
|
||||
|
||||
xs.foreach(item => {
|
||||
obj.Inventory -= item.start
|
||||
sendResponse(ObjectDeleteMessage(item.obj.GUID, 0))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2022 PSForever
|
||||
package net.psforever.objects.avatar
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
case class Friend(
|
||||
charId: Long = 0,
|
||||
name: String = "",
|
||||
faction: PlanetSideEmpire.Value,
|
||||
online: Boolean = false
|
||||
)
|
||||
|
||||
case class Ignored(
|
||||
charId: Long = 0,
|
||||
name: String = "",
|
||||
online: Boolean = false
|
||||
) {
|
||||
val faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import net.psforever.packet.game.objectcreate.RibbonBars
|
|||
import net.psforever.types._
|
||||
import org.joda.time.{Duration, LocalDateTime, Seconds}
|
||||
|
||||
import scala.collection.immutable.Seq
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object Avatar {
|
||||
|
|
@ -83,6 +82,33 @@ object Avatar {
|
|||
}
|
||||
}
|
||||
|
||||
case class Cooldowns(
|
||||
/** Timestamps of when a vehicle or equipment was last purchased */
|
||||
purchase: Map[String, LocalDateTime] = Map(),
|
||||
/** Timestamps of when a vehicle or equipment was last purchased */
|
||||
use: Map[String, LocalDateTime] = Map()
|
||||
)
|
||||
|
||||
case class Loadouts(
|
||||
suit: Seq[Option[Loadout]] = Seq.fill(20)(None),
|
||||
squad: Seq[Option[SquadLoadout]] = Seq.fill(10)(None)
|
||||
)
|
||||
|
||||
case class ProgressDecoration(
|
||||
cosmetics: Option[Set[Cosmetic]] = None,
|
||||
ribbonBars: RibbonBars = RibbonBars(),
|
||||
firstTimeEvents: Set[String] =
|
||||
FirstTimeEvents.Maps ++ FirstTimeEvents.Monoliths ++
|
||||
FirstTimeEvents.Standard.All ++ FirstTimeEvents.Cavern.All ++
|
||||
FirstTimeEvents.TR.All ++ FirstTimeEvents.NC.All ++ FirstTimeEvents.VS.All ++
|
||||
FirstTimeEvents.Generic
|
||||
)
|
||||
|
||||
case class MemberLists(
|
||||
friend: List[Friend] = List[Friend](),
|
||||
ignored: List[Ignored] = List[Ignored]()
|
||||
)
|
||||
|
||||
case class Avatar(
|
||||
/** unique identifier corresponding to a database table row index */
|
||||
id: Int,
|
||||
|
|
@ -95,25 +121,16 @@ case class Avatar(
|
|||
cep: Long = 0,
|
||||
stamina: Int = 100,
|
||||
fatigued: Boolean = false,
|
||||
cosmetics: Option[Set[Cosmetic]] = None,
|
||||
ribbonBars: RibbonBars = RibbonBars(),
|
||||
certifications: Set[Certification] = Set(),
|
||||
loadouts: Seq[Option[Loadout]] = Seq.fill(20)(None),
|
||||
squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None),
|
||||
implants: Seq[Option[Implant]] = Seq(None, None, None),
|
||||
locker: LockerContainer = Avatar.makeLocker(),
|
||||
deployables: DeployableToolbox = new DeployableToolbox(), // TODO var bad
|
||||
deployables: DeployableToolbox = new DeployableToolbox(),
|
||||
lookingForSquad: Boolean = false,
|
||||
var vehicle: Option[PlanetSideGUID] = None, // TODO var bad
|
||||
firstTimeEvents: Set[String] =
|
||||
FirstTimeEvents.Maps ++ FirstTimeEvents.Monoliths ++
|
||||
FirstTimeEvents.Standard.All ++ FirstTimeEvents.Cavern.All ++
|
||||
FirstTimeEvents.TR.All ++ FirstTimeEvents.NC.All ++ FirstTimeEvents.VS.All ++
|
||||
FirstTimeEvents.Generic,
|
||||
/** Timestamps of when a vehicle or equipment was last purchased */
|
||||
purchaseTimes: Map[String, LocalDateTime] = Map(),
|
||||
/** Timestamps of when a vehicle or equipment was last purchased */
|
||||
useTimes: Map[String, LocalDateTime] = Map()
|
||||
decoration: ProgressDecoration = ProgressDecoration(),
|
||||
loadouts: Loadouts = Loadouts(),
|
||||
cooldowns: Cooldowns = Cooldowns(),
|
||||
people: MemberLists = MemberLists()
|
||||
) {
|
||||
assert(bep >= 0)
|
||||
assert(cep >= 0)
|
||||
|
|
@ -141,12 +158,12 @@ case class Avatar(
|
|||
|
||||
/** Returns the remaining purchase cooldown or None if an object is not on cooldown */
|
||||
def purchaseCooldown(definition: BasicDefinition): Option[Duration] = {
|
||||
cooldown(purchaseTimes, Avatar.purchaseCooldowns, definition)
|
||||
cooldown(cooldowns.purchase, Avatar.purchaseCooldowns, definition)
|
||||
}
|
||||
|
||||
/** Returns the remaining use cooldown or None if an object is not on cooldown */
|
||||
def useCooldown(definition: BasicDefinition): Option[Duration] = {
|
||||
cooldown(useTimes, Avatar.useCooldowns, definition)
|
||||
cooldown(cooldowns.use, Avatar.useCooldowns, definition)
|
||||
}
|
||||
|
||||
def fifthSlot(): EquipmentSlot = {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ object AvatarConverter {
|
|||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars)
|
||||
}
|
||||
|
||||
def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = {
|
||||
|
|
@ -121,7 +121,7 @@ object AvatarConverter {
|
|||
0,
|
||||
obj.avatar.cr.value,
|
||||
obj.avatar.implants.flatten.filter(_.active).flatMap(_.definition.implantType.effect).toList,
|
||||
obj.avatar.cosmetics
|
||||
obj.avatar.decoration.cosmetics
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ object AvatarConverter {
|
|||
obj.avatar.implants.flatten.map(_.toEntry).toList,
|
||||
Nil,
|
||||
Nil,
|
||||
obj.avatar.firstTimeEvents.toList,
|
||||
obj.avatar.decoration.firstTimeEvents.toList,
|
||||
tutorials = List.empty[String], //TODO tutorial list
|
||||
0L,
|
||||
0L,
|
||||
|
|
@ -164,7 +164,7 @@ object AvatarConverter {
|
|||
Nil,
|
||||
Nil,
|
||||
unkC = false,
|
||||
obj.avatar.cosmetics
|
||||
obj.avatar.decoration.cosmetics
|
||||
)
|
||||
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class CharacterSelectConverter extends AvatarConverter {
|
|||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars)
|
||||
}
|
||||
|
||||
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
||||
|
|
@ -123,7 +123,7 @@ class CharacterSelectConverter extends AvatarConverter {
|
|||
Nil,
|
||||
Nil,
|
||||
unkC = false,
|
||||
obj.avatar.cosmetics
|
||||
obj.avatar.decoration.cosmetics
|
||||
)
|
||||
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class CorpseConverter extends AvatarConverter {
|
|||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars)
|
||||
}
|
||||
|
||||
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import net.psforever.packet.game.ItemTransactionMessage
|
|||
*/
|
||||
final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab {
|
||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
||||
player.avatar.loadouts(msg.unk1 + 15) match {
|
||||
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
|
||||
player.avatar.loadouts.suit(msg.unk1 + 15) match {
|
||||
case Some(loadout: VehicleLoadout)
|
||||
if !Exclude.exists(_.checkRule(player, msg, loadout.vehicle_definition)) =>
|
||||
vehicles.get(loadout.vehicle_definition.Name) match {
|
||||
case Some(vehicle) =>
|
||||
val weapons = loadout.visible_slots.map(entry => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import net.psforever.packet.game.ItemTransactionMessage
|
|||
*/
|
||||
final case class InfantryLoadoutPage() extends LoadoutTab {
|
||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
||||
player.avatar.loadouts(msg.unk1) match {
|
||||
player.avatar.loadouts.suit(msg.unk1) match {
|
||||
case Some(loadout: InfantryLoadout)
|
||||
if !Exclude.exists(_.checkRule(player, msg, loadout.exosuit)) &&
|
||||
!Exclude.exists(_.checkRule(player, msg, (loadout.exosuit, loadout.subtype))) =>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import net.psforever.packet.game.ItemTransactionMessage
|
|||
*/
|
||||
final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab {
|
||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
||||
player.avatar.loadouts(msg.unk1 + lineOffset) match {
|
||||
player.avatar.loadouts.suit(msg.unk1 + lineOffset) match {
|
||||
case Some(loadout: VehicleLoadout) =>
|
||||
val weapons = loadout.visible_slots
|
||||
.map(entry => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import net.psforever.types.MemberAction
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
|
|
@ -15,23 +16,12 @@ import scodec.codecs._
|
|||
* No name will be appended or removed from any list until the response to this packet is received.<br>
|
||||
* <br>
|
||||
* Actions that involve the "remove" functionality will locate the entered name in the local list before dispatching this packet.
|
||||
* A complaint will be logged to the event window if the name is not found.<br>
|
||||
* <br>
|
||||
* Actions:<br>
|
||||
* 0 - display friends list<br>
|
||||
* 1 - add player to friends list<br>
|
||||
* 2 - remove player from friends list<br>
|
||||
* 4 - display ignored player list<br>
|
||||
* 5 - add player to ignored player list<br>
|
||||
* 6 - remove player from ignored player list<br>
|
||||
* <br>
|
||||
* Exploration:<br>
|
||||
* Are action = 3 and action = 7 supposed to do anything?
|
||||
* A complaint will be logged to the event window if the name is not found.
|
||||
* @param action the purpose of this packet
|
||||
* @param friend the player name that was entered;
|
||||
* blank in certain situations
|
||||
*/
|
||||
final case class FriendsRequest(action: Int, friend: String) extends PlanetSideGamePacket {
|
||||
final case class FriendsRequest(action: MemberAction.Value, friend: String) extends PlanetSideGamePacket {
|
||||
type Packet = FriendsRequest
|
||||
def opcode = GamePacketOpcode.FriendsRequest
|
||||
def encode = FriendsRequest.encode(this)
|
||||
|
|
@ -39,7 +29,7 @@ final case class FriendsRequest(action: Int, friend: String) extends PlanetSideG
|
|||
|
||||
object FriendsRequest extends Marshallable[FriendsRequest] {
|
||||
implicit val codec: Codec[FriendsRequest] = (
|
||||
("action" | uintL(3)) ::
|
||||
("action" | MemberAction.codec) ::
|
||||
("friend" | PacketHelpers.encodedWideStringAligned(5))
|
||||
).as[FriendsRequest]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,12 @@ package net.psforever.packet.game
|
|||
|
||||
import net.psforever.packet.GamePacketOpcode.Type
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import net.psforever.types.MemberAction
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
import shapeless.{::, HNil}
|
||||
|
||||
object FriendAction extends Enumeration {
|
||||
type Type = Value
|
||||
|
||||
val InitializeFriendList, AddFriend, RemoveFriend, UpdateFriend, InitializeIgnoreList, AddIgnoredPlayer,
|
||||
RemoveIgnoredPlayer = Value
|
||||
|
||||
implicit val codec: Codec[FriendAction.Value] = PacketHelpers.createEnumerationCodec(this, uint(bits = 3))
|
||||
}
|
||||
|
||||
/**
|
||||
* An entry in the list of players known to and tracked by this player.
|
||||
* They're called "friends" even though they can be used for a list of ignored players as well.
|
||||
|
|
@ -30,17 +22,7 @@ final case class Friend(name: String, online: Boolean = false)
|
|||
* <br>
|
||||
* Friends can be remembered and their current playing status can be reported.
|
||||
* Ignored players will have their comments stifled in the given player's chat window.
|
||||
* This does not handle outfit member lists.<br>
|
||||
* <br>
|
||||
* Actions:<br>
|
||||
* 0 - initialize friends list (no logging)<br>
|
||||
* 1 - add entry to friends list<br>
|
||||
* 2 - remove entry from friends list<br>
|
||||
* 3 - update status of player in friends list;
|
||||
* if player is not listed, he is not added<br>
|
||||
* 4 - initialize ignored players list (no logging)<br>
|
||||
* 5 - add entry to ignored players list<br>
|
||||
* 6 - remove entry from ignored players list<br>
|
||||
* This does not handle outfit member lists.
|
||||
* @param action the purpose of the entry(s) in this packet
|
||||
* @param unk1 na;
|
||||
* always 0?
|
||||
|
|
@ -49,11 +31,11 @@ final case class Friend(name: String, online: Boolean = false)
|
|||
* @param friends a list of `Friend`s
|
||||
*/
|
||||
final case class FriendsResponse(
|
||||
action: FriendAction.Value,
|
||||
unk1: Int,
|
||||
first_entry: Boolean,
|
||||
last_entry: Boolean,
|
||||
friends: List[Friend] = Nil
|
||||
action: MemberAction.Value,
|
||||
unk1: Int,
|
||||
first_entry: Boolean,
|
||||
last_entry: Boolean,
|
||||
friends: List[Friend]
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = FriendsResponse
|
||||
|
||||
|
|
@ -64,7 +46,7 @@ final case class FriendsResponse(
|
|||
|
||||
object Friend extends Marshallable[Friend] {
|
||||
implicit val codec: Codec[Friend] = (
|
||||
("name" | PacketHelpers.encodedWideStringAligned(3)) ::
|
||||
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 3)) ::
|
||||
("online" | bool)
|
||||
).as[Friend]
|
||||
|
||||
|
|
@ -73,19 +55,47 @@ object Friend extends Marshallable[Friend] {
|
|||
* Initial byte-alignment creates padding differences which requires a second `Codec`.
|
||||
*/
|
||||
implicit val codec_list: Codec[Friend] = (
|
||||
("name" | PacketHelpers.encodedWideStringAligned(7)) ::
|
||||
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) ::
|
||||
("online" | bool)
|
||||
).as[Friend]
|
||||
}
|
||||
|
||||
object FriendsResponse extends Marshallable[FriendsResponse] {
|
||||
def apply(action: MemberAction.Value, friend: Friend): FriendsResponse = {
|
||||
FriendsResponse(action, unk1=0, first_entry=true, last_entry=true, List(friend))
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a list of members and construct appropriate packets by which they can be dispatched to the client.
|
||||
* Attention needs to be paid to the number of entries in a single packet,
|
||||
* and where the produced packets begin and end.
|
||||
* @param action the purpose of the entry(s) in this packet
|
||||
* @param friends a list of `Friend`s
|
||||
* @return a list of `FriendResponse` packets
|
||||
*/
|
||||
def packetSequence(action: MemberAction.Value, friends: List[Friend]): List[FriendsResponse] = {
|
||||
val lists = friends.grouped(15)
|
||||
val size = lists.size
|
||||
if (size <= 1) {
|
||||
List(FriendsResponse(action, unk1=0, first_entry=true, last_entry=true, friends))
|
||||
} else {
|
||||
val size1 = size - 1
|
||||
val first = lists.take(1)
|
||||
val rest = lists.slice(1, size1)
|
||||
val last = lists.drop(size1)
|
||||
List(FriendsResponse(action, unk1=0, first_entry=true, last_entry=false, first.next())) ++
|
||||
rest.map { FriendsResponse(action, unk1=0, first_entry=false, last_entry=false, _)} ++
|
||||
List(FriendsResponse(action, unk1=0, first_entry=false, last_entry=true, last.next()))
|
||||
}
|
||||
}
|
||||
|
||||
implicit val codec: Codec[FriendsResponse] = (
|
||||
("action" | FriendAction.codec) ::
|
||||
("action" | MemberAction.codec) ::
|
||||
("unk1" | uint4L) ::
|
||||
("first_entry" | bool) ::
|
||||
("last_entry" | bool) ::
|
||||
(("number_of_friends" | uint4L) >>:~ { len =>
|
||||
conditional(len > 0, "friend" | Friend.codec) ::
|
||||
conditional(len > 0, codec = "friend" | Friend.codec) ::
|
||||
("friends" | PacketHelpers.listOfNSized(len - 1, Friend.codec_list))
|
||||
})
|
||||
).xmap[FriendsResponse](
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2022 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
case class Friend(id: Int, avatarId: Long, charId: Long)
|
||||
|
||||
case class Ignored(id: Int, avatarId: Long, charId: Long)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package net.psforever.persistence
|
||||
|
||||
import net.psforever.objects.avatar
|
||||
import net.psforever.objects.avatar.Cosmetic
|
||||
import net.psforever.objects.avatar.{Cosmetic, ProgressDecoration}
|
||||
import org.joda.time.LocalDateTime
|
||||
import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire}
|
||||
|
||||
|
|
@ -32,6 +32,6 @@ case class Avatar(
|
|||
CharacterVoice(voiceId),
|
||||
bep,
|
||||
cep,
|
||||
cosmetics = cosmetics.map(c => Cosmetic.valuesFromObjectCreateValue(c))
|
||||
decoration = ProgressDecoration(cosmetics = cosmetics.map(c => Cosmetic.valuesFromObjectCreateValue(c)))
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import net.psforever.objects.zones.Zone
|
|||
import net.psforever.types.Vector3
|
||||
import net.psforever.services.{Service, ServiceManager}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
|
||||
|
||||
/**
|
||||
* A global service that manages user behavior as divided into the following three categories:
|
||||
|
|
@ -48,9 +49,11 @@ class AccountPersistenceService extends Actor {
|
|||
|
||||
/** squad service event hook */
|
||||
var squad: ActorRef = ActorRef.noSender
|
||||
/** galaxy service event hook */
|
||||
var galaxy: ActorRef = ActorRef.noSender
|
||||
|
||||
/** log, for trace and warnings only */
|
||||
val log = org.log4s.getLogger
|
||||
private val log = org.log4s.getLogger
|
||||
|
||||
/**
|
||||
* Retrieve the required system event service hooks.
|
||||
|
|
@ -59,6 +62,7 @@ class AccountPersistenceService extends Actor {
|
|||
*/
|
||||
override def preStart(): Unit = {
|
||||
ServiceManager.serviceManager ! ServiceManager.Lookup("squad")
|
||||
ServiceManager.serviceManager ! ServiceManager.Lookup("galaxy")
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
|
|
@ -127,15 +131,24 @@ class AccountPersistenceService extends Actor {
|
|||
val Setup: Receive = {
|
||||
case ServiceManager.LookupResult("squad", endpoint) =>
|
||||
squad = endpoint
|
||||
if (squad != ActorRef.noSender) {
|
||||
log.trace("Service hooks obtained. Continuing with standard operation.")
|
||||
context.become(Started)
|
||||
}
|
||||
log.trace("Service hooks obtained. Attempting standard operation.")
|
||||
switchToStarted()
|
||||
|
||||
case ServiceManager.LookupResult("galaxy", endpoint) =>
|
||||
galaxy = endpoint
|
||||
log.trace("Service hooks obtained. Attempting standard operation.")
|
||||
switchToStarted()
|
||||
|
||||
case msg =>
|
||||
log.warn(s"not yet started; received a $msg that will go unhandled")
|
||||
}
|
||||
|
||||
def switchToStarted(): Unit = {
|
||||
if (squad != ActorRef.noSender && galaxy != ActorRef.noSender) {
|
||||
context.become(Started)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a new persistency monitor object for this player.
|
||||
* @param name the unique name of the player
|
||||
|
|
@ -143,7 +156,7 @@ class AccountPersistenceService extends Actor {
|
|||
*/
|
||||
def CreateNewPlayerToken(name: String): ActorRef = {
|
||||
val ref =
|
||||
context.actorOf(Props(classOf[PersistenceMonitor], name, squad), s"${NextPlayerIndex(name)}_${name.hashCode()}")
|
||||
context.actorOf(Props(classOf[PersistenceMonitor], name, squad, galaxy), s"${NextPlayerIndex(name)}_${name.hashCode()}")
|
||||
accounts += name -> ref
|
||||
ref
|
||||
}
|
||||
|
|
@ -219,8 +232,11 @@ object AccountPersistenceService {
|
|||
* @param name the unique name of the player
|
||||
* @param squadService a hook into the `SquadService` event system
|
||||
*/
|
||||
class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor {
|
||||
|
||||
class PersistenceMonitor(
|
||||
name: String,
|
||||
squadService: ActorRef,
|
||||
galaxyService: ActorRef
|
||||
) extends Actor {
|
||||
/** the last-reported zone of this player */
|
||||
var inZone: Zone = Zone.Nowhere
|
||||
|
||||
|
|
@ -242,7 +258,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor {
|
|||
var timer: Cancellable = Default.Cancellable
|
||||
|
||||
/** the sparingly-used log */
|
||||
val log = org.log4s.getLogger
|
||||
private val log = org.log4s.getLogger
|
||||
|
||||
/**
|
||||
* Perform logout operations before the persistence monitor finally stops.
|
||||
|
|
@ -407,6 +423,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor {
|
|||
def AvatarLogout(avatar: Avatar): Unit = {
|
||||
LivePlayerList.Remove(avatar.id)
|
||||
squadService.tell(Service.Leave(Some(avatar.id.toString)), context.parent)
|
||||
galaxyService.tell(GalaxyServiceMessage(GalaxyAction.LogStatusChange(avatar.name)), context.parent)
|
||||
Deployables.Disown(inZone, avatar, context.parent)
|
||||
inZone.Population.tell(Zone.Population.Leave(avatar), context.parent)
|
||||
TaskWorkflow.execute(GUIDTask.unregisterObject(inZone.GUID, avatar.locker))
|
||||
|
|
|
|||
|
|
@ -78,9 +78,20 @@ class GalaxyService extends Actor {
|
|||
)
|
||||
)
|
||||
|
||||
case GalaxyAction.LogStatusChange(name) =>
|
||||
GalaxyEvents.publish(
|
||||
GalaxyServiceResponse(
|
||||
s"/Galaxy",
|
||||
GalaxyResponse.LogStatusChange(name)
|
||||
)
|
||||
)
|
||||
|
||||
case GalaxyAction.SendResponse(msg) =>
|
||||
GalaxyEvents.publish(
|
||||
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.SendResponse(msg))
|
||||
GalaxyServiceResponse(
|
||||
s"/Galaxy",
|
||||
GalaxyResponse.SendResponse(msg)
|
||||
)
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,5 +39,7 @@ object GalaxyAction {
|
|||
|
||||
final case class UnlockedZoneUpdate(zone: Zone) extends Action
|
||||
|
||||
final case class LogStatusChange(name: String) extends Action
|
||||
|
||||
final case class SendResponse(msg: PlanetSideGamePacket) extends Action
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,7 @@ object GalaxyResponse {
|
|||
|
||||
final case class UnlockedZoneUpdate(zone: Zone) extends Response
|
||||
|
||||
final case class LogStatusChange(name: String) extends Response
|
||||
|
||||
final case class SendResponse(msg: PlanetSideGamePacket) extends Response
|
||||
}
|
||||
|
|
|
|||
15
src/main/scala/net/psforever/types/MemberAction.scala
Normal file
15
src/main/scala/net/psforever/types/MemberAction.scala
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) 2022 PSForever
|
||||
package net.psforever.types
|
||||
|
||||
import net.psforever.packet.PacketHelpers
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
|
||||
object MemberAction extends Enumeration {
|
||||
type Type = Value
|
||||
|
||||
val InitializeFriendList, AddFriend, RemoveFriend, UpdateFriend, InitializeIgnoreList, AddIgnoredPlayer,
|
||||
RemoveIgnoredPlayer = Value
|
||||
|
||||
implicit val codec: Codec[MemberAction.Value] = PacketHelpers.createEnumerationCodec(this, uint(bits = 3))
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ package game
|
|||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.MemberAction
|
||||
import scodec.bits._
|
||||
|
||||
class FriendsRequestTest extends Specification {
|
||||
|
|
@ -12,7 +13,7 @@ class FriendsRequestTest extends Specification {
|
|||
"decode" in {
|
||||
PacketCoding.decodePacket(string).require match {
|
||||
case FriendsRequest(action, friend) =>
|
||||
action mustEqual 1
|
||||
action mustEqual MemberAction.AddFriend
|
||||
friend.length mustEqual 5
|
||||
friend mustEqual "FJHNC"
|
||||
case _ =>
|
||||
|
|
@ -21,7 +22,7 @@ class FriendsRequestTest extends Specification {
|
|||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = FriendsRequest(1, "FJHNC")
|
||||
val msg = FriendsRequest(MemberAction.AddFriend, "FJHNC")
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package game
|
|||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.MemberAction
|
||||
import scodec.bits._
|
||||
|
||||
class FriendsResponseTest extends Specification {
|
||||
|
|
@ -15,7 +16,7 @@ class FriendsResponseTest extends Specification {
|
|||
"decode (one friend)" in {
|
||||
PacketCoding.decodePacket(stringOneFriend).require match {
|
||||
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
||||
action mustEqual FriendAction.UpdateFriend
|
||||
action mustEqual MemberAction.UpdateFriend
|
||||
unk2 mustEqual 0
|
||||
unk3 mustEqual true
|
||||
unk4 mustEqual true
|
||||
|
|
@ -30,7 +31,7 @@ class FriendsResponseTest extends Specification {
|
|||
"decode (multiple friends)" in {
|
||||
PacketCoding.decodePacket(stringManyFriends).require match {
|
||||
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
||||
action mustEqual FriendAction.InitializeFriendList
|
||||
action mustEqual MemberAction.InitializeFriendList
|
||||
unk2 mustEqual 0
|
||||
unk3 mustEqual true
|
||||
unk4 mustEqual true
|
||||
|
|
@ -53,7 +54,7 @@ class FriendsResponseTest extends Specification {
|
|||
"decode (short)" in {
|
||||
PacketCoding.decodePacket(stringShort).require match {
|
||||
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
||||
action mustEqual FriendAction.InitializeIgnoreList
|
||||
action mustEqual MemberAction.InitializeIgnoreList
|
||||
unk2 mustEqual 0
|
||||
unk3 mustEqual true
|
||||
unk4 mustEqual true
|
||||
|
|
@ -65,7 +66,7 @@ class FriendsResponseTest extends Specification {
|
|||
|
||||
"encode (one friend)" in {
|
||||
val msg = FriendsResponse(
|
||||
FriendAction.UpdateFriend,
|
||||
MemberAction.UpdateFriend,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
|
|
@ -79,7 +80,7 @@ class FriendsResponseTest extends Specification {
|
|||
|
||||
"encode (multiple friends)" in {
|
||||
val msg = FriendsResponse(
|
||||
FriendAction.InitializeFriendList,
|
||||
MemberAction.InitializeFriendList,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
|
|
@ -96,7 +97,7 @@ class FriendsResponseTest extends Specification {
|
|||
}
|
||||
|
||||
"encode (short)" in {
|
||||
val msg = FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true)
|
||||
val msg = FriendsResponse(MemberAction.InitializeIgnoreList, 0, true, true, Nil)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual stringShort
|
||||
|
|
|
|||
Loading…
Reference in a new issue