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.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.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.converter.CharacterSelectConverter
|
||||||
import net.psforever.objects.definition._
|
import net.psforever.objects.definition._
|
||||||
import net.psforever.objects.inventory.Container
|
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.locker.LockerContainer
|
||||||
import net.psforever.objects.vital.HealFromImplant
|
import net.psforever.objects.vital.HealFromImplant
|
||||||
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
|
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game.{Friend => GameFriend, _}
|
||||||
import net.psforever.types._
|
import net.psforever.types.{MemberAction, PlanetSideEmpire, _}
|
||||||
import net.psforever.util.Database._
|
import net.psforever.util.Database._
|
||||||
import net.psforever.persistence
|
import net.psforever.persistence
|
||||||
import net.psforever.util.{Config, Database, DefinitionUtil}
|
import net.psforever.util.{Config, Database, DefinitionUtil}
|
||||||
import org.joda.time.{LocalDateTime, Seconds}
|
import net.psforever.services.{Service, ServiceManager}
|
||||||
import net.psforever.services.ServiceManager
|
|
||||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
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 {
|
object AvatarActor {
|
||||||
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
|
def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] =
|
||||||
Behaviors
|
Behaviors
|
||||||
|
|
@ -187,6 +186,8 @@ object AvatarActor {
|
||||||
|
|
||||||
private case class SetImplantInitialized(implantType: ImplantType) extends Command
|
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 AvatarResponse(avatar: Avatar)
|
||||||
|
|
||||||
final case class AvatarLoginResponse(avatar: Avatar)
|
final case class AvatarLoginResponse(avatar: Avatar)
|
||||||
|
|
@ -286,7 +287,7 @@ object AvatarActor {
|
||||||
}
|
}
|
||||||
|
|
||||||
def encodeLockerClob(container: Container): String = {
|
def encodeLockerClob(container: Container): String = {
|
||||||
val clobber: StringBuilder = new StringBuilder()
|
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||||
container.Inventory.Items.foreach {
|
container.Inventory.Items.foreach {
|
||||||
case InventoryItem(obj, index) =>
|
case InventoryItem(obj, index) =>
|
||||||
clobber.append(encodeLoadoutClobFragment(obj, index))
|
clobber.append(encodeLoadoutClobFragment(obj, index))
|
||||||
|
|
@ -315,6 +316,121 @@ object AvatarActor {
|
||||||
case RibbonBarSlot.TermOfService => ribbons.copy(tos = ribbon)
|
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(
|
class AvatarActor(
|
||||||
|
|
@ -487,22 +603,26 @@ class AvatarActor(
|
||||||
query[persistence.Avatar].filter(_.id == lift(avatar.id))
|
query[persistence.Avatar].filter(_.id == lift(avatar.id))
|
||||||
.update(_.lastLogin -> lift(LocalDateTime.now()))
|
.update(_.lastLogin -> lift(LocalDateTime.now()))
|
||||||
)
|
)
|
||||||
|
avatarId = avatar.id
|
||||||
loadouts <- initializeAllLoadouts()
|
loadouts <- initializeAllLoadouts()
|
||||||
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatar.id)))
|
implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
|
||||||
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatar.id)))
|
certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
|
||||||
locker <- loadLocker()
|
locker <- loadLocker(avatarId)
|
||||||
} yield (loadouts, implants, certs, locker)
|
friends <- loadFriendList(avatarId)
|
||||||
|
ignored <- loadIgnoredList(avatarId)
|
||||||
|
} yield (loadouts, implants, certs, locker, friends, ignored)
|
||||||
|
|
||||||
result.onComplete {
|
result.onComplete {
|
||||||
case Success((loadouts, implants, certs, locker)) =>
|
case Success((_loadouts, implants, certs, locker, friendsList, ignoredList)) =>
|
||||||
avatarCopy(
|
avatarCopy(
|
||||||
avatar.copy(
|
avatar.copy(
|
||||||
loadouts = loadouts,
|
loadouts = avatar.loadouts.copy(suit = _loadouts),
|
||||||
// make sure we always have the base certifications
|
// 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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// if we need to start stamina regeneration
|
// if we need to start stamina regeneration
|
||||||
|
|
@ -523,7 +643,8 @@ class AvatarActor(
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case AddFirstTimeEvent(event) =>
|
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
|
Behaviors.same
|
||||||
|
|
||||||
case LearnCertification(terminalGuid, certification) =>
|
case LearnCertification(terminalGuid, certification) =>
|
||||||
|
|
@ -814,7 +935,8 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
result.onComplete {
|
result.onComplete {
|
||||||
case Success(loadout) =>
|
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)
|
refreshLoadout(lineNo)
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
log.error(exception)("db failure (?)")
|
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}")
|
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, result) = loadoutType match {
|
||||||
case LoadoutType.Infantry if avatar.loadouts(number).nonEmpty =>
|
case LoadoutType.Infantry if avatar.loadouts.suit(number).nonEmpty =>
|
||||||
(
|
(
|
||||||
number,
|
number,
|
||||||
ctx.run(
|
ctx.run(
|
||||||
|
|
@ -835,7 +957,7 @@ class AvatarActor(
|
||||||
.delete
|
.delete
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case LoadoutType.Vehicle if avatar.loadouts(number + 10).nonEmpty =>
|
case LoadoutType.Vehicle if avatar.loadouts.suit(number + 10).nonEmpty =>
|
||||||
(
|
(
|
||||||
number + 10,
|
number + 10,
|
||||||
ctx.run(
|
ctx.run(
|
||||||
|
|
@ -845,7 +967,7 @@ class AvatarActor(
|
||||||
.delete
|
.delete
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case LoadoutType.Battleframe if avatar.loadouts(number + 15).nonEmpty =>
|
case LoadoutType.Battleframe if avatar.loadouts.suit(number + 15).nonEmpty =>
|
||||||
(
|
(
|
||||||
number + 15,
|
number + 15,
|
||||||
ctx.run(
|
ctx.run(
|
||||||
|
|
@ -860,7 +982,8 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
result.onComplete {
|
result.onComplete {
|
||||||
case Success(_) =>
|
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, ""))
|
sessionActor ! SessionActor.SendResponse(FavoritesMessage(loadoutType, player.GUID, number, ""))
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
log.error(exception)("db failure (?)")
|
log.error(exception)("db failure (?)")
|
||||||
|
|
@ -872,16 +995,16 @@ class AvatarActor(
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case InitialRefreshLoadouts() =>
|
case InitialRefreshLoadouts() =>
|
||||||
refreshLoadouts(avatar.loadouts.zipWithIndex)
|
refreshLoadouts(avatar.loadouts.suit.zipWithIndex)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case RefreshLoadouts() =>
|
case RefreshLoadouts() =>
|
||||||
refreshLoadouts(avatar.loadouts.zipWithIndex.collect { case out @ (Some(_), _) => out })
|
refreshLoadouts(avatar.loadouts.suit.zipWithIndex.collect { case out @ (Some(_), _) => out })
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case UpdatePurchaseTime(definition, time) =>
|
case UpdatePurchaseTime(definition, time) =>
|
||||||
// TODO save to db
|
// TODO save to db
|
||||||
var newTimes = avatar.purchaseTimes
|
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) =>
|
||||||
Avatar.purchaseCooldowns.get(item) match {
|
Avatar.purchaseCooldowns.get(item) match {
|
||||||
|
|
@ -896,19 +1019,20 @@ class AvatarActor(
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
avatarCopy(avatar.copy(purchaseTimes = newTimes))
|
avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = newTimes)))
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case UpdateUseTime(definition, time) =>
|
case UpdateUseTime(definition, time) =>
|
||||||
if (!Avatar.useCooldowns.contains(definition)) {
|
if (!Avatar.useCooldowns.contains(definition)) {
|
||||||
log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown")
|
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)
|
sessionActor ! SessionActor.UseCooldownRenewed(definition, time)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case RefreshPurchaseTimes() =>
|
case RefreshPurchaseTimes() =>
|
||||||
refreshPurchaseTimes(avatar.purchaseTimes.keys.toSet)
|
refreshPurchaseTimes(avatar.cooldowns.purchase.keys.toSet)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case SetVehicle(vehicle) =>
|
case SetVehicle(vehicle) =>
|
||||||
|
|
@ -1135,13 +1259,14 @@ class AvatarActor(
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
case SetRibbon(ribbon, bar) =>
|
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)
|
val useRibbonBars = Seq(previousRibbonBars.upper, previousRibbonBars.middle, previousRibbonBars.lower)
|
||||||
.indexWhere { _ == ribbon } match {
|
.indexWhere { _ == ribbon } match {
|
||||||
case -1 => previousRibbonBars
|
case -1 => previousRibbonBars
|
||||||
case n => AvatarActor.changeRibbons(previousRibbonBars, MeritCommendation.None, RibbonBarSlot(n))
|
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 player = session.get.player
|
||||||
val zone = player.Zone
|
val zone = player.Zone
|
||||||
zone.AvatarEvents ! AvatarServiceMessage(
|
zone.AvatarEvents ! AvatarServiceMessage(
|
||||||
|
|
@ -1149,6 +1274,10 @@ class AvatarActor(
|
||||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DisplayedAwardMessage(player.GUID, ribbon, bar))
|
AvatarAction.SendResponse(Service.defaultPlayerGUID, DisplayedAwardMessage(player.GUID, ribbon, bar))
|
||||||
)
|
)
|
||||||
Behaviors.same
|
Behaviors.same
|
||||||
|
|
||||||
|
case MemberListRequest(action, name) =>
|
||||||
|
memberListAction(action, name)
|
||||||
|
Behaviors.same
|
||||||
}
|
}
|
||||||
.receiveSignal {
|
.receiveSignal {
|
||||||
case (_, PostStop) =>
|
case (_, PostStop) =>
|
||||||
|
|
@ -1191,7 +1320,7 @@ class AvatarActor(
|
||||||
)
|
)
|
||||||
.onComplete {
|
.onComplete {
|
||||||
case Success(_) =>
|
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.AvatarEvents ! AvatarServiceMessage(
|
||||||
session.get.zone.id,
|
session.get.zone.id,
|
||||||
AvatarAction
|
AvatarAction
|
||||||
|
|
@ -1503,7 +1632,7 @@ 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 = {
|
val items: String = {
|
||||||
val clobber: StringBuilder = new StringBuilder()
|
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||||
//encode holsters
|
//encode holsters
|
||||||
owner
|
owner
|
||||||
.Holsters()
|
.Holsters()
|
||||||
|
|
@ -1547,7 +1676,7 @@ class AvatarActor(
|
||||||
def storeVehicleLoadout(owner: Player, label: String, line: Int, vehicle: Vehicle): Future[Loadout] = {
|
def storeVehicleLoadout(owner: Player, label: String, line: Int, vehicle: Vehicle): Future[Loadout] = {
|
||||||
import ctx._
|
import ctx._
|
||||||
val items: String = {
|
val items: String = {
|
||||||
val clobber: StringBuilder = new StringBuilder()
|
val clobber: mutable.StringBuilder = new StringBuilder()
|
||||||
//encode holsters
|
//encode holsters
|
||||||
vehicle.Weapons
|
vehicle.Weapons
|
||||||
.collect {
|
.collect {
|
||||||
|
|
@ -1720,7 +1849,7 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
|
|
||||||
def refreshLoadout(line: Int): Unit = {
|
def refreshLoadout(line: Int): Unit = {
|
||||||
avatar.loadouts.lift(line) match {
|
avatar.loadouts.suit.lift(line) match {
|
||||||
case Some(Some(loadout: InfantryLoadout)) =>
|
case Some(Some(loadout: InfantryLoadout)) =>
|
||||||
sessionActor ! SessionActor.SendResponse(
|
sessionActor ! SessionActor.SendResponse(
|
||||||
FavoritesMessage.Infantry(
|
FavoritesMessage.Infantry(
|
||||||
|
|
@ -1768,12 +1897,12 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def loadLocker(): Future[LockerContainer] = {
|
def loadLocker(charId: Long): Future[LockerContainer] = {
|
||||||
val locker = Avatar.makeLocker()
|
val locker = Avatar.makeLocker()
|
||||||
var notLoaded: Boolean = false
|
var notLoaded: Boolean = false
|
||||||
import ctx._
|
import ctx._
|
||||||
val out = ctx.run(query[persistence.Locker]
|
val out = ctx.run(query[persistence.Locker]
|
||||||
.filter(_.avatarId == lift(avatar.id)))
|
.filter(_.avatarId == lift(charId)))
|
||||||
.map { entry =>
|
.map { entry =>
|
||||||
notLoaded = false
|
notLoaded = false
|
||||||
entry.foreach { contents => AvatarActor.buildContainedEquipmentFromClob(locker, contents.items, log) }
|
entry.foreach { contents => AvatarActor.buildContainedEquipmentFromClob(locker, contents.items, log) }
|
||||||
|
|
@ -1800,6 +1929,50 @@ class AvatarActor(
|
||||||
out
|
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 = {
|
def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = {
|
||||||
if (staminaRegenTimer.isCancelled) {
|
if (staminaRegenTimer.isCancelled) {
|
||||||
defaultStaminaRegen(initialDelay)
|
defaultStaminaRegen(initialDelay)
|
||||||
|
|
@ -1828,7 +2001,7 @@ class AvatarActor(
|
||||||
def refreshPurchaseTimes(keys: Set[String]): Unit = {
|
def refreshPurchaseTimes(keys: Set[String]): Unit = {
|
||||||
var keysToDrop: Seq[String] = Nil
|
var keysToDrop: Seq[String] = Nil
|
||||||
keys.foreach { key =>
|
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)) =>
|
case Some((name, purchaseTime)) =>
|
||||||
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
|
val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds
|
||||||
Avatar.purchaseCooldowns.find(_._1.Name == name) match {
|
Avatar.purchaseCooldowns.find(_._1.Name == name) match {
|
||||||
|
|
@ -1847,7 +2020,8 @@ class AvatarActor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keysToDrop.nonEmpty) {
|
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 _ => ;
|
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.services.{CavernRotationService, InterstellarClusterService}
|
||||||
import net.psforever.types.ChatMessageType.UNK_229
|
import net.psforever.types.ChatMessageType.UNK_229
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
|
||||||
object ChatActor {
|
object ChatActor {
|
||||||
def apply(
|
def apply(
|
||||||
sessionActor: ActorRef[SessionActor.Command],
|
sessionActor: ActorRef[SessionActor.Command],
|
||||||
|
|
@ -123,6 +125,13 @@ class ChatActor(
|
||||||
var chatService: Option[ActorRef[ChatService.Command]] = None
|
var chatService: Option[ActorRef[ChatService.Command]] = None
|
||||||
var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
|
var cluster: Option[ActorRef[InterstellarClusterService.Command]] = None
|
||||||
var silenceTimer: Cancellable = Default.Cancellable
|
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] {
|
val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] {
|
||||||
case ChatService.MessageResponse(_session, message, channel) => IncomingMessage(_session, message, channel)
|
case ChatService.MessageResponse(_session, message, channel) => IncomingMessage(_session, message, channel)
|
||||||
|
|
@ -699,11 +708,21 @@ class ChatActor(
|
||||||
}
|
}
|
||||||
|
|
||||||
case (CMT_TELL, _, _) if !session.player.silenced =>
|
case (CMT_TELL, _, _) if !session.player.silenced =>
|
||||||
|
if (AvatarActor.onlineIfNotIgnored(message.recipient, session.avatar.name)) {
|
||||||
chatService ! ChatService.Message(
|
chatService ! ChatService.Message(
|
||||||
session,
|
session,
|
||||||
message,
|
message,
|
||||||
ChatChannel.Default()
|
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 =>
|
case (CMT_BROADCAST, _, _) if !session.player.silenced =>
|
||||||
chatService ! ChatService.Message(
|
chatService ! ChatService.Message(
|
||||||
|
|
@ -913,7 +932,7 @@ class ChatActor(
|
||||||
}
|
}
|
||||||
|
|
||||||
case (CMT_TOGGLE_HAT, _, contents) =>
|
case (CMT_TOGGLE_HAT, _, contents) =>
|
||||||
val cosmetics = session.avatar.cosmetics.getOrElse(Set())
|
val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
|
||||||
val nextCosmetics = contents match {
|
val nextCosmetics = contents match {
|
||||||
case "off" =>
|
case "off" =>
|
||||||
cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
|
cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
|
||||||
|
|
@ -937,7 +956,7 @@ class ChatActor(
|
||||||
)
|
)
|
||||||
|
|
||||||
case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
|
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 {
|
val cosmetic = message.messageType match {
|
||||||
case CMT_HIDE_HELMET => Cosmetic.NoHelmet
|
case CMT_HIDE_HELMET => Cosmetic.NoHelmet
|
||||||
|
|
@ -1055,26 +1074,48 @@ class ChatActor(
|
||||||
|
|
||||||
case IncomingMessage(fromSession, message, channel) =>
|
case IncomingMessage(fromSession, message, channel) =>
|
||||||
message.messageType match {
|
message.messageType match {
|
||||||
case CMT_TELL | U_CMT_TELLFROM | CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | UNK_45 | UNK_71 |
|
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
|
||||||
CMT_NOTE | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
|
if (AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)) {
|
||||||
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_227 | UNK_229 =>
|
|
||||||
sessionActor ! SessionActor.SendResponse(message)
|
sessionActor ! SessionActor.SendResponse(message)
|
||||||
|
}
|
||||||
case CMT_OPEN =>
|
case CMT_OPEN =>
|
||||||
if (
|
if (
|
||||||
session.zone == fromSession.zone &&
|
session.zone == fromSession.zone &&
|
||||||
Vector3.Distance(session.player.Position, fromSession.player.Position) < 25 &&
|
Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 625 &&
|
||||||
session.player.Faction == fromSession.player.Faction
|
session.player.Faction == fromSession.player.Faction &&
|
||||||
|
AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)
|
||||||
) {
|
) {
|
||||||
sessionActor ! SessionActor.SendResponse(message)
|
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 =>
|
case CMT_VOICE =>
|
||||||
if (
|
if (
|
||||||
session.zone == fromSession.zone &&
|
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
|
message.contents.startsWith("SH") // tactical squad voice macro
|
||||||
|
) {
|
||||||
|
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)
|
sessionActor ! SessionActor.SendResponse(message)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case CMT_SILENCE =>
|
case CMT_SILENCE =>
|
||||||
val args = message.contents.split(" ")
|
val args = message.contents.split(" ")
|
||||||
val (name, time) = (args.lift(0), args.lift(1)) match {
|
val (name, time) = (args.lift(0), args.lift(1)) match {
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,8 @@ object SessionActor {
|
||||||
|
|
||||||
final case class UseCooldownRenewed(definition: BasicDefinition, time: LocalDateTime) extends Command
|
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
|
* 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.
|
||||||
|
|
@ -681,6 +683,12 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
case AvatarServiceResponse(toChannel, guid, reply) =>
|
case AvatarServiceResponse(toChannel, guid, reply) =>
|
||||||
HandleAvatarServiceResponse(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) =>
|
case SendResponse(packet) =>
|
||||||
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) }
|
hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
case GalaxyResponse.MapUpdate(msg) =>
|
case GalaxyResponse.MapUpdate(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
|
|
||||||
|
|
@ -904,6 +913,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
||||||
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
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) =>
|
case GalaxyResponse.SendResponse(msg) =>
|
||||||
sendResponse(msg)
|
sendResponse(msg)
|
||||||
}
|
}
|
||||||
|
|
@ -1380,6 +1394,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
||||||
context.self
|
context.self
|
||||||
)
|
)
|
||||||
LivePlayerList.Add(avatar.id, avatar)
|
LivePlayerList.Add(avatar.id, avatar)
|
||||||
|
galaxyService.tell(GalaxyServiceMessage(GalaxyAction.LogStatusChange(avatar.name)), context.parent)
|
||||||
//PropertyOverrideMessage
|
//PropertyOverrideMessage
|
||||||
|
|
||||||
implicit val timeout = Timeout(1 seconds)
|
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(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
|
||||||
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
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
|
//the following subscriptions last until character switch/logout
|
||||||
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
||||||
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
|
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(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
||||||
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
|
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
|
||||||
//only need to load these once - they persist between zone transfers and respawns
|
//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) =>
|
case (Some(loadout), index) =>
|
||||||
sendResponse(
|
sendResponse(
|
||||||
SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task))
|
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 @ 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
|
case msg @ HitHint(source_guid, player_guid) => ; //HitHint is manually distributed for proper operation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 net.psforever.types._
|
||||||
import org.joda.time.{Duration, LocalDateTime, Seconds}
|
import org.joda.time.{Duration, LocalDateTime, Seconds}
|
||||||
|
|
||||||
import scala.collection.immutable.Seq
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
object Avatar {
|
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(
|
case class Avatar(
|
||||||
/** unique identifier corresponding to a database table row index */
|
/** unique identifier corresponding to a database table row index */
|
||||||
id: Int,
|
id: Int,
|
||||||
|
|
@ -95,25 +121,16 @@ case class Avatar(
|
||||||
cep: Long = 0,
|
cep: Long = 0,
|
||||||
stamina: Int = 100,
|
stamina: Int = 100,
|
||||||
fatigued: Boolean = false,
|
fatigued: Boolean = false,
|
||||||
cosmetics: Option[Set[Cosmetic]] = None,
|
|
||||||
ribbonBars: RibbonBars = RibbonBars(),
|
|
||||||
certifications: Set[Certification] = Set(),
|
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),
|
implants: Seq[Option[Implant]] = Seq(None, None, None),
|
||||||
locker: LockerContainer = Avatar.makeLocker(),
|
locker: LockerContainer = Avatar.makeLocker(),
|
||||||
deployables: DeployableToolbox = new DeployableToolbox(), // TODO var bad
|
deployables: DeployableToolbox = new DeployableToolbox(),
|
||||||
lookingForSquad: Boolean = false,
|
lookingForSquad: Boolean = false,
|
||||||
var vehicle: Option[PlanetSideGUID] = None, // TODO var bad
|
var vehicle: Option[PlanetSideGUID] = None, // TODO var bad
|
||||||
firstTimeEvents: Set[String] =
|
decoration: ProgressDecoration = ProgressDecoration(),
|
||||||
FirstTimeEvents.Maps ++ FirstTimeEvents.Monoliths ++
|
loadouts: Loadouts = Loadouts(),
|
||||||
FirstTimeEvents.Standard.All ++ FirstTimeEvents.Cavern.All ++
|
cooldowns: Cooldowns = Cooldowns(),
|
||||||
FirstTimeEvents.TR.All ++ FirstTimeEvents.NC.All ++ FirstTimeEvents.VS.All ++
|
people: MemberLists = MemberLists()
|
||||||
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()
|
|
||||||
) {
|
) {
|
||||||
assert(bep >= 0)
|
assert(bep >= 0)
|
||||||
assert(cep >= 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 */
|
/** Returns the remaining purchase cooldown or None if an object is not on cooldown */
|
||||||
def purchaseCooldown(definition: BasicDefinition): Option[Duration] = {
|
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 */
|
/** Returns the remaining use cooldown or None if an object is not on cooldown */
|
||||||
def useCooldown(definition: BasicDefinition): Option[Duration] = {
|
def useCooldown(definition: BasicDefinition): Option[Duration] = {
|
||||||
cooldown(useTimes, Avatar.useCooldowns, definition)
|
cooldown(cooldowns.use, Avatar.useCooldowns, definition)
|
||||||
}
|
}
|
||||||
|
|
||||||
def fifthSlot(): EquipmentSlot = {
|
def fifthSlot(): EquipmentSlot = {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ object AvatarConverter {
|
||||||
unk7 = false,
|
unk7 = false,
|
||||||
on_zipline = None
|
on_zipline = None
|
||||||
)
|
)
|
||||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars)
|
||||||
}
|
}
|
||||||
|
|
||||||
def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = {
|
def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = {
|
||||||
|
|
@ -121,7 +121,7 @@ object AvatarConverter {
|
||||||
0,
|
0,
|
||||||
obj.avatar.cr.value,
|
obj.avatar.cr.value,
|
||||||
obj.avatar.implants.flatten.filter(_.active).flatMap(_.definition.implantType.effect).toList,
|
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,
|
obj.avatar.implants.flatten.map(_.toEntry).toList,
|
||||||
Nil,
|
Nil,
|
||||||
Nil,
|
Nil,
|
||||||
obj.avatar.firstTimeEvents.toList,
|
obj.avatar.decoration.firstTimeEvents.toList,
|
||||||
tutorials = List.empty[String], //TODO tutorial list
|
tutorials = List.empty[String], //TODO tutorial list
|
||||||
0L,
|
0L,
|
||||||
0L,
|
0L,
|
||||||
|
|
@ -164,7 +164,7 @@ object AvatarConverter {
|
||||||
Nil,
|
Nil,
|
||||||
Nil,
|
Nil,
|
||||||
unkC = false,
|
unkC = false,
|
||||||
obj.avatar.cosmetics
|
obj.avatar.decoration.cosmetics
|
||||||
)
|
)
|
||||||
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
|
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ class CharacterSelectConverter extends AvatarConverter {
|
||||||
unk7 = false,
|
unk7 = false,
|
||||||
on_zipline = None
|
on_zipline = None
|
||||||
)
|
)
|
||||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
||||||
|
|
@ -123,7 +123,7 @@ class CharacterSelectConverter extends AvatarConverter {
|
||||||
Nil,
|
Nil,
|
||||||
Nil,
|
Nil,
|
||||||
unkC = false,
|
unkC = false,
|
||||||
obj.avatar.cosmetics
|
obj.avatar.decoration.cosmetics
|
||||||
)
|
)
|
||||||
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
|
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class CorpseConverter extends AvatarConverter {
|
||||||
unk7 = false,
|
unk7 = false,
|
||||||
on_zipline = None
|
on_zipline = None
|
||||||
)
|
)
|
||||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
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 {
|
final case class BattleframeSpawnLoadoutPage(vehicles: Map[String, () => Vehicle]) extends LoadoutTab {
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
||||||
player.avatar.loadouts(msg.unk1 + 15) match {
|
player.avatar.loadouts.suit(msg.unk1 + 15) match {
|
||||||
case Some(loadout: VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
|
case Some(loadout: VehicleLoadout)
|
||||||
|
if !Exclude.exists(_.checkRule(player, msg, loadout.vehicle_definition)) =>
|
||||||
vehicles.get(loadout.vehicle_definition.Name) match {
|
vehicles.get(loadout.vehicle_definition.Name) match {
|
||||||
case Some(vehicle) =>
|
case Some(vehicle) =>
|
||||||
val weapons = loadout.visible_slots.map(entry => {
|
val weapons = loadout.visible_slots.map(entry => {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import net.psforever.packet.game.ItemTransactionMessage
|
||||||
*/
|
*/
|
||||||
final case class InfantryLoadoutPage() extends LoadoutTab {
|
final case class InfantryLoadoutPage() extends LoadoutTab {
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
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)
|
case Some(loadout: InfantryLoadout)
|
||||||
if !Exclude.exists(_.checkRule(player, msg, loadout.exosuit)) &&
|
if !Exclude.exists(_.checkRule(player, msg, loadout.exosuit)) &&
|
||||||
!Exclude.exists(_.checkRule(player, msg, (loadout.exosuit, loadout.subtype))) =>
|
!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 {
|
final case class VehicleLoadoutPage(lineOffset: Int) extends LoadoutTab {
|
||||||
override def Buy(player: Player, msg: ItemTransactionMessage): Terminal.Exchange = {
|
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) =>
|
case Some(loadout: VehicleLoadout) =>
|
||||||
val weapons = loadout.visible_slots
|
val weapons = loadout.visible_slots
|
||||||
.map(entry => {
|
.map(entry => {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package net.psforever.packet.game
|
package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
|
import net.psforever.types.MemberAction
|
||||||
import scodec.Codec
|
import scodec.Codec
|
||||||
import scodec.codecs._
|
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>
|
* No name will be appended or removed from any list until the response to this packet is received.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* Actions that involve the "remove" functionality will locate the entered name in the local list before dispatching this packet.
|
* 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>
|
* A complaint will be logged to the event window if the name is not found.
|
||||||
* <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?
|
|
||||||
* @param action the purpose of this packet
|
* @param action the purpose of this packet
|
||||||
* @param friend the player name that was entered;
|
* @param friend the player name that was entered;
|
||||||
* blank in certain situations
|
* 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
|
type Packet = FriendsRequest
|
||||||
def opcode = GamePacketOpcode.FriendsRequest
|
def opcode = GamePacketOpcode.FriendsRequest
|
||||||
def encode = FriendsRequest.encode(this)
|
def encode = FriendsRequest.encode(this)
|
||||||
|
|
@ -39,7 +29,7 @@ final case class FriendsRequest(action: Int, friend: String) extends PlanetSideG
|
||||||
|
|
||||||
object FriendsRequest extends Marshallable[FriendsRequest] {
|
object FriendsRequest extends Marshallable[FriendsRequest] {
|
||||||
implicit val codec: Codec[FriendsRequest] = (
|
implicit val codec: Codec[FriendsRequest] = (
|
||||||
("action" | uintL(3)) ::
|
("action" | MemberAction.codec) ::
|
||||||
("friend" | PacketHelpers.encodedWideStringAligned(5))
|
("friend" | PacketHelpers.encodedWideStringAligned(5))
|
||||||
).as[FriendsRequest]
|
).as[FriendsRequest]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,12 @@ package net.psforever.packet.game
|
||||||
|
|
||||||
import net.psforever.packet.GamePacketOpcode.Type
|
import net.psforever.packet.GamePacketOpcode.Type
|
||||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||||
|
import net.psforever.types.MemberAction
|
||||||
import scodec.bits.BitVector
|
import scodec.bits.BitVector
|
||||||
import scodec.{Attempt, Codec}
|
import scodec.{Attempt, Codec}
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
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.
|
* 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.
|
* 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>
|
* <br>
|
||||||
* Friends can be remembered and their current playing status can be reported.
|
* 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.
|
* Ignored players will have their comments stifled in the given player's chat window.
|
||||||
* This does not handle outfit member lists.<br>
|
* This does not handle outfit member lists.
|
||||||
* <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>
|
|
||||||
* @param action the purpose of the entry(s) in this packet
|
* @param action the purpose of the entry(s) in this packet
|
||||||
* @param unk1 na;
|
* @param unk1 na;
|
||||||
* always 0?
|
* always 0?
|
||||||
|
|
@ -49,11 +31,11 @@ final case class Friend(name: String, online: Boolean = false)
|
||||||
* @param friends a list of `Friend`s
|
* @param friends a list of `Friend`s
|
||||||
*/
|
*/
|
||||||
final case class FriendsResponse(
|
final case class FriendsResponse(
|
||||||
action: FriendAction.Value,
|
action: MemberAction.Value,
|
||||||
unk1: Int,
|
unk1: Int,
|
||||||
first_entry: Boolean,
|
first_entry: Boolean,
|
||||||
last_entry: Boolean,
|
last_entry: Boolean,
|
||||||
friends: List[Friend] = Nil
|
friends: List[Friend]
|
||||||
) extends PlanetSideGamePacket {
|
) extends PlanetSideGamePacket {
|
||||||
type Packet = FriendsResponse
|
type Packet = FriendsResponse
|
||||||
|
|
||||||
|
|
@ -64,7 +46,7 @@ final case class FriendsResponse(
|
||||||
|
|
||||||
object Friend extends Marshallable[Friend] {
|
object Friend extends Marshallable[Friend] {
|
||||||
implicit val codec: Codec[Friend] = (
|
implicit val codec: Codec[Friend] = (
|
||||||
("name" | PacketHelpers.encodedWideStringAligned(3)) ::
|
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 3)) ::
|
||||||
("online" | bool)
|
("online" | bool)
|
||||||
).as[Friend]
|
).as[Friend]
|
||||||
|
|
||||||
|
|
@ -73,19 +55,47 @@ object Friend extends Marshallable[Friend] {
|
||||||
* Initial byte-alignment creates padding differences which requires a second `Codec`.
|
* Initial byte-alignment creates padding differences which requires a second `Codec`.
|
||||||
*/
|
*/
|
||||||
implicit val codec_list: Codec[Friend] = (
|
implicit val codec_list: Codec[Friend] = (
|
||||||
("name" | PacketHelpers.encodedWideStringAligned(7)) ::
|
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) ::
|
||||||
("online" | bool)
|
("online" | bool)
|
||||||
).as[Friend]
|
).as[Friend]
|
||||||
}
|
}
|
||||||
|
|
||||||
object FriendsResponse extends Marshallable[FriendsResponse] {
|
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] = (
|
implicit val codec: Codec[FriendsResponse] = (
|
||||||
("action" | FriendAction.codec) ::
|
("action" | MemberAction.codec) ::
|
||||||
("unk1" | uint4L) ::
|
("unk1" | uint4L) ::
|
||||||
("first_entry" | bool) ::
|
("first_entry" | bool) ::
|
||||||
("last_entry" | bool) ::
|
("last_entry" | bool) ::
|
||||||
(("number_of_friends" | uint4L) >>:~ { len =>
|
(("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))
|
("friends" | PacketHelpers.listOfNSized(len - 1, Friend.codec_list))
|
||||||
})
|
})
|
||||||
).xmap[FriendsResponse](
|
).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
|
package net.psforever.persistence
|
||||||
|
|
||||||
import net.psforever.objects.avatar
|
import net.psforever.objects.avatar
|
||||||
import net.psforever.objects.avatar.Cosmetic
|
import net.psforever.objects.avatar.{Cosmetic, ProgressDecoration}
|
||||||
import org.joda.time.LocalDateTime
|
import org.joda.time.LocalDateTime
|
||||||
import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire}
|
import net.psforever.types.{CharacterSex, CharacterVoice, PlanetSideEmpire}
|
||||||
|
|
||||||
|
|
@ -32,6 +32,6 @@ case class Avatar(
|
||||||
CharacterVoice(voiceId),
|
CharacterVoice(voiceId),
|
||||||
bep,
|
bep,
|
||||||
cep,
|
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.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}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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:
|
||||||
|
|
@ -48,9 +49,11 @@ class AccountPersistenceService extends Actor {
|
||||||
|
|
||||||
/** squad service event hook */
|
/** squad service event hook */
|
||||||
var squad: ActorRef = ActorRef.noSender
|
var squad: ActorRef = ActorRef.noSender
|
||||||
|
/** galaxy service event hook */
|
||||||
|
var galaxy: ActorRef = ActorRef.noSender
|
||||||
|
|
||||||
/** log, for trace and warnings only */
|
/** log, for trace and warnings only */
|
||||||
val log = org.log4s.getLogger
|
private val log = org.log4s.getLogger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the required system event service hooks.
|
* Retrieve the required system event service hooks.
|
||||||
|
|
@ -59,6 +62,7 @@ class AccountPersistenceService extends Actor {
|
||||||
*/
|
*/
|
||||||
override def preStart(): Unit = {
|
override def preStart(): Unit = {
|
||||||
ServiceManager.serviceManager ! ServiceManager.Lookup("squad")
|
ServiceManager.serviceManager ! ServiceManager.Lookup("squad")
|
||||||
|
ServiceManager.serviceManager ! ServiceManager.Lookup("galaxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
override def postStop(): Unit = {
|
override def postStop(): Unit = {
|
||||||
|
|
@ -127,15 +131,24 @@ class AccountPersistenceService extends Actor {
|
||||||
val Setup: Receive = {
|
val Setup: Receive = {
|
||||||
case ServiceManager.LookupResult("squad", endpoint) =>
|
case ServiceManager.LookupResult("squad", endpoint) =>
|
||||||
squad = endpoint
|
squad = endpoint
|
||||||
if (squad != ActorRef.noSender) {
|
log.trace("Service hooks obtained. Attempting standard operation.")
|
||||||
log.trace("Service hooks obtained. Continuing with standard operation.")
|
switchToStarted()
|
||||||
context.become(Started)
|
|
||||||
}
|
case ServiceManager.LookupResult("galaxy", endpoint) =>
|
||||||
|
galaxy = endpoint
|
||||||
|
log.trace("Service hooks obtained. Attempting standard operation.")
|
||||||
|
switchToStarted()
|
||||||
|
|
||||||
case msg =>
|
case msg =>
|
||||||
log.warn(s"not yet started; received a $msg that will go unhandled")
|
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.
|
* Enqueue a new persistency monitor object for this player.
|
||||||
* @param name the unique name of the player
|
* @param name the unique name of the player
|
||||||
|
|
@ -143,7 +156,7 @@ class AccountPersistenceService extends Actor {
|
||||||
*/
|
*/
|
||||||
def CreateNewPlayerToken(name: String): ActorRef = {
|
def CreateNewPlayerToken(name: String): ActorRef = {
|
||||||
val ref =
|
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
|
accounts += name -> ref
|
||||||
ref
|
ref
|
||||||
}
|
}
|
||||||
|
|
@ -219,8 +232,11 @@ object AccountPersistenceService {
|
||||||
* @param name the unique name of the player
|
* @param name the unique name of the player
|
||||||
* @param squadService a hook into the `SquadService` event system
|
* @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 */
|
/** the last-reported zone of this player */
|
||||||
var inZone: Zone = Zone.Nowhere
|
var inZone: Zone = Zone.Nowhere
|
||||||
|
|
||||||
|
|
@ -242,7 +258,7 @@ class PersistenceMonitor(name: String, squadService: ActorRef) extends Actor {
|
||||||
var timer: Cancellable = Default.Cancellable
|
var timer: Cancellable = Default.Cancellable
|
||||||
|
|
||||||
/** the sparingly-used log */
|
/** the sparingly-used log */
|
||||||
val log = org.log4s.getLogger
|
private val log = org.log4s.getLogger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform logout operations before the persistence monitor finally stops.
|
* 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 = {
|
def AvatarLogout(avatar: Avatar): Unit = {
|
||||||
LivePlayerList.Remove(avatar.id)
|
LivePlayerList.Remove(avatar.id)
|
||||||
squadService.tell(Service.Leave(Some(avatar.id.toString)), context.parent)
|
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)
|
Deployables.Disown(inZone, avatar, context.parent)
|
||||||
inZone.Population.tell(Zone.Population.Leave(avatar), context.parent)
|
inZone.Population.tell(Zone.Population.Leave(avatar), context.parent)
|
||||||
TaskWorkflow.execute(GUIDTask.unregisterObject(inZone.GUID, avatar.locker))
|
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) =>
|
case GalaxyAction.SendResponse(msg) =>
|
||||||
GalaxyEvents.publish(
|
GalaxyEvents.publish(
|
||||||
GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.SendResponse(msg))
|
GalaxyServiceResponse(
|
||||||
|
s"/Galaxy",
|
||||||
|
GalaxyResponse.SendResponse(msg)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
case _ => ;
|
case _ => ;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,5 +39,7 @@ object GalaxyAction {
|
||||||
|
|
||||||
final case class UnlockedZoneUpdate(zone: Zone) extends Action
|
final case class UnlockedZoneUpdate(zone: Zone) extends Action
|
||||||
|
|
||||||
|
final case class LogStatusChange(name: String) extends Action
|
||||||
|
|
||||||
final case class SendResponse(msg: PlanetSideGamePacket) 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 UnlockedZoneUpdate(zone: Zone) extends Response
|
||||||
|
|
||||||
|
final case class LogStatusChange(name: String) extends Response
|
||||||
|
|
||||||
final case class SendResponse(msg: PlanetSideGamePacket) 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 org.specs2.mutable._
|
||||||
import net.psforever.packet._
|
import net.psforever.packet._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.types.MemberAction
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
class FriendsRequestTest extends Specification {
|
class FriendsRequestTest extends Specification {
|
||||||
|
|
@ -12,7 +13,7 @@ class FriendsRequestTest extends Specification {
|
||||||
"decode" in {
|
"decode" in {
|
||||||
PacketCoding.decodePacket(string).require match {
|
PacketCoding.decodePacket(string).require match {
|
||||||
case FriendsRequest(action, friend) =>
|
case FriendsRequest(action, friend) =>
|
||||||
action mustEqual 1
|
action mustEqual MemberAction.AddFriend
|
||||||
friend.length mustEqual 5
|
friend.length mustEqual 5
|
||||||
friend mustEqual "FJHNC"
|
friend mustEqual "FJHNC"
|
||||||
case _ =>
|
case _ =>
|
||||||
|
|
@ -21,7 +22,7 @@ class FriendsRequestTest extends Specification {
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode" in {
|
"encode" in {
|
||||||
val msg = FriendsRequest(1, "FJHNC")
|
val msg = FriendsRequest(MemberAction.AddFriend, "FJHNC")
|
||||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual string
|
pkt mustEqual string
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package game
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
import net.psforever.packet._
|
import net.psforever.packet._
|
||||||
import net.psforever.packet.game._
|
import net.psforever.packet.game._
|
||||||
|
import net.psforever.types.MemberAction
|
||||||
import scodec.bits._
|
import scodec.bits._
|
||||||
|
|
||||||
class FriendsResponseTest extends Specification {
|
class FriendsResponseTest extends Specification {
|
||||||
|
|
@ -15,7 +16,7 @@ class FriendsResponseTest extends Specification {
|
||||||
"decode (one friend)" in {
|
"decode (one friend)" in {
|
||||||
PacketCoding.decodePacket(stringOneFriend).require match {
|
PacketCoding.decodePacket(stringOneFriend).require match {
|
||||||
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
||||||
action mustEqual FriendAction.UpdateFriend
|
action mustEqual MemberAction.UpdateFriend
|
||||||
unk2 mustEqual 0
|
unk2 mustEqual 0
|
||||||
unk3 mustEqual true
|
unk3 mustEqual true
|
||||||
unk4 mustEqual true
|
unk4 mustEqual true
|
||||||
|
|
@ -30,7 +31,7 @@ class FriendsResponseTest extends Specification {
|
||||||
"decode (multiple friends)" in {
|
"decode (multiple friends)" in {
|
||||||
PacketCoding.decodePacket(stringManyFriends).require match {
|
PacketCoding.decodePacket(stringManyFriends).require match {
|
||||||
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
||||||
action mustEqual FriendAction.InitializeFriendList
|
action mustEqual MemberAction.InitializeFriendList
|
||||||
unk2 mustEqual 0
|
unk2 mustEqual 0
|
||||||
unk3 mustEqual true
|
unk3 mustEqual true
|
||||||
unk4 mustEqual true
|
unk4 mustEqual true
|
||||||
|
|
@ -53,7 +54,7 @@ class FriendsResponseTest extends Specification {
|
||||||
"decode (short)" in {
|
"decode (short)" in {
|
||||||
PacketCoding.decodePacket(stringShort).require match {
|
PacketCoding.decodePacket(stringShort).require match {
|
||||||
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
case FriendsResponse(action, unk2, unk3, unk4, list) =>
|
||||||
action mustEqual FriendAction.InitializeIgnoreList
|
action mustEqual MemberAction.InitializeIgnoreList
|
||||||
unk2 mustEqual 0
|
unk2 mustEqual 0
|
||||||
unk3 mustEqual true
|
unk3 mustEqual true
|
||||||
unk4 mustEqual true
|
unk4 mustEqual true
|
||||||
|
|
@ -65,7 +66,7 @@ class FriendsResponseTest extends Specification {
|
||||||
|
|
||||||
"encode (one friend)" in {
|
"encode (one friend)" in {
|
||||||
val msg = FriendsResponse(
|
val msg = FriendsResponse(
|
||||||
FriendAction.UpdateFriend,
|
MemberAction.UpdateFriend,
|
||||||
0,
|
0,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
|
@ -79,7 +80,7 @@ class FriendsResponseTest extends Specification {
|
||||||
|
|
||||||
"encode (multiple friends)" in {
|
"encode (multiple friends)" in {
|
||||||
val msg = FriendsResponse(
|
val msg = FriendsResponse(
|
||||||
FriendAction.InitializeFriendList,
|
MemberAction.InitializeFriendList,
|
||||||
0,
|
0,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
|
|
@ -96,7 +97,7 @@ class FriendsResponseTest extends Specification {
|
||||||
}
|
}
|
||||||
|
|
||||||
"encode (short)" in {
|
"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
|
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||||
|
|
||||||
pkt mustEqual stringShort
|
pkt mustEqual stringShort
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue