introduced service-level squad management operations, specifically to read SquadDefinitionAction packets from WorldSessionActor and send ReplicationStreamMessage packets back to relevant WSA when necessary

This commit is contained in:
FateJH 2019-05-28 15:15:24 -04:00
parent afb10f57c3
commit ceb145d94f
10 changed files with 413 additions and 9 deletions

View file

@ -6,12 +6,13 @@ import net.psforever.types.{CertificationType, Vector3}
class Member {
//about the position to be filled
private var role : String = ""
private var orders : String = ""
private var restrictions : Set[CertificationType.Value] = Set()
//about the individual filling the position
private var name : String = ""
private var health : Int = 0
private var armor : Int = 0
private var zoneId : String = "Nowhere"
private var zoneId : Int = 0
private var position : Vector3 = Vector3.Zero
def Role : String = role
@ -21,6 +22,13 @@ class Member {
Role
}
def Orders : String = orders
def Orders_=(text : String) : String = {
orders = text
Orders
}
def Restrictions : Set[CertificationType.Value] = restrictions
def Restrictions_=(requirements : Set[CertificationType.Value]) = {
@ -49,9 +57,9 @@ class Member {
Armor
}
def ZoneId : String = zoneId
def ZoneId : Int = zoneId
def ZoneId_=(id : String) : String = {
def ZoneId_=(id : Int) : Int = {
zoneId = id
ZoneId
}
@ -62,4 +70,15 @@ class Member {
position = pos
Position
}
def Close() : Unit = {
role = ""
restrictions = Set()
//about the individual filling the position
name = ""
health = 0
armor = 0
zoneId = 0
position = Vector3.Zero
}
}

View file

@ -8,25 +8,35 @@ import net.psforever.types.PlanetSideEmpire
class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extends IdentifiableEntity {
super.GUID_=(squadId)
private val faction : PlanetSideEmpire.Value = alignment //does not change
private val zoneId : Option[String] = None
private var zoneId : Option[Int] = None
private var task : String = ""
private var description : String = ""
private val membership : Array[Member] = Array.fill[Member](10)(new Member)
private val availability : Array[Boolean] = Array.fill[Boolean](10)(true)
private var listed : Boolean = false
override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
def Faction : PlanetSideEmpire.Value = faction
def ZoneId : String = zoneId.getOrElse({
def ZoneId : Int = zoneId.getOrElse({
membership.headOption match {
case Some(leader) =>
leader.ZoneId
case _ =>
"Nowhere"
0
}
})
def ZoneId_=(id : Int) : Int = {
ZoneId_=(Some(id))
}
def ZoneId_=(id : Option[Int]) : Int = {
zoneId = id
ZoneId
}
def Task : String = task
def Task_=(assignment : String) : String = {
@ -41,10 +51,26 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
Description
}
def Listed : Boolean = listed
def Listed_=(announce : Boolean) : Boolean = {
listed = announce
Listed
}
def Membership : Array[Member] = membership
def Availability : Array[Boolean] = availability
def Leader : String = {
membership.headOption match {
case Some(member) =>
member.Name
case None =>
""
}
}
def Size : Int = membership.count(member => !member.Name.equals(""))
def Capacity : Int = availability.count(open => open)

View file

@ -12,7 +12,7 @@ import shapeless.{::, HNil}
* All behaviors have a "code" that indicates how the rest of the data is parsed.
* @param code the action behavior code
*/
protected abstract class SquadAction(val code : Int)
abstract class SquadAction(val code : Int)
object SquadAction{
final case class SaveSquadDefinition() extends SquadAction(3)

View file

@ -12,4 +12,13 @@ object PlanetSideEmpire extends Enumeration {
val TR, NC, VS, NEUTRAL = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
def apply(id : String) : PlanetSideEmpire.Value = {
values.find(_.toString.equals(id)) match {
case Some(faction) =>
faction
case None =>
throw new NoSuchElementException(s"can not find an empire associated with $id")
}
}
}

View file

@ -0,0 +1,12 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import net.psforever.packet.game.SquadInfo
object SquadResponse {
trait Response
final case class Init(info : Vector[SquadInfo]) extends Response
final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response
final case class Remove(infos : Iterable[Int]) extends Response
}

View file

@ -0,0 +1,276 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import akka.actor.Actor
import net.psforever.objects.Player
import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game._
import net.psforever.types.PlanetSideEmpire
import services.{GenericEventBus, Service}
import scala.collection.concurrent.TrieMap
import scala.collection.mutable.ListBuffer
//import scala.concurrent.duration._
class SquadService extends Actor {
private var memberToSquad : TrieMap[String, Squad] = new TrieMap[String, Squad]()
private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
private var i : Int = 1
private var publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
PlanetSideEmpire.TR -> ListBuffer.empty,
PlanetSideEmpire.NC -> ListBuffer.empty,
PlanetSideEmpire.VS -> ListBuffer.empty
)
private [this] val log = org.log4s.getLogger
override def preStart = {
log.info("Starting...")
}
def GetNextSquadId() : PlanetSideGUID = {
val out = i
val j = i + 1
if(j == 65536) {
i = 1
}
else {
i = j
}
PlanetSideGUID(out)
}
def GetSquadFromPlayer(player : Player) : Squad = {
val name = player.Name
val faction = player.Faction
memberToSquad.get(name) match {
case Some(squad) =>
squad
case None =>
val id = GetNextSquadId()
val squad = new Squad(id, faction)
val leadPosition = squad.Membership(0)
leadPosition.Name = name
leadPosition.Health = player.Health
leadPosition.Armor = player.Armor
leadPosition.Position = player.Position
leadPosition.ZoneId = 1 //player.Continent //TODO how to resolve this?
log.info(s"$name-$faction has started a new squad")
memberToSquad += name -> squad
idToSquad += id -> squad
squad
}
}
val SquadEvents = new GenericEventBus[SquadServiceResponse]
def receive : Receive = {
//subscribe to a faction's channel - necessary to receive updates about listed squads
case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 =>
val path = s"$faction/Squad"
val who = sender()
log.info(s"$who has joined $path")
SquadEvents.subscribe(who, path)
//send initial squad catalog
sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(publishedLists(PlanetSideEmpire(faction)).toVector))
//subscribe to the player's personal channel - necessary only to inform about any previous squad association
case Service.Join(name) =>
val path = s"$name/Squad"
val who = sender()
log.info(s"$who has joined $path")
SquadEvents.subscribe(who, path)
//check for renewable squad information
memberToSquad.get(name) match {
case None => ;
case Some(squad) =>
sender ! SquadServiceMessage.RecoverSquadMembership()
}
case Service.Leave(Some(name)) => ;
SquadEvents.unsubscribe(sender())
//TODO leave squad, if joined to one, and perform clean-up
case Service.Leave(None) | Service.LeaveAll() =>
SquadEvents.unsubscribe(sender())
//TODO might be better to invalidate these
case SquadServiceMessage.SquadDefinitionAction(tplayer, zone_ordinal_number, _, _, action) =>
import net.psforever.packet.game.SquadAction._
val squad = GetSquadFromPlayer(tplayer)
val member = squad.Membership.find(_.Name == tplayer.Name).get //should never fail
member.ZoneId = zone_ordinal_number //TODO improve this requirement
var listingChanged : List[Int] = Nil
action match {
case ChangeSquadPurpose(purpose) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
squad.Description = purpose
listingChanged = List(SquadInfo.Field.Task)
case ChangeSquadZone(zone) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's ops zone to $zone")
squad.ZoneId = zone.zoneId.toInt
listingChanged = List(SquadInfo.Field.ZoneId)
case CloseSquadMemberPosition(position) =>
if(position > 0) {
squad.Availability.lift(position) match {
case Some(true) =>
squad.Availability.update(position, false)
log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in his squad")
val memberPosition = squad.Membership(position)
listingChanged = if(memberPosition.Name.nonEmpty) {
List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
}
else {
List(SquadInfo.Field.Capacity)
}
memberPosition.Close()
case Some(false) => ;
case None => ;
}
}
else {
log.warn(s"can not close the lead position in squad-${squad.GUID.guid}")
}
case AddSquadMemberPosition(position) =>
squad.Availability.lift(position) match {
case Some(false) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in his squad")
squad.Availability.update(position, true)
listingChanged = List(SquadInfo.Field.Capacity)
case Some(true) => ;
case None => ;
}
case ChangeSquadMemberRequirementsRole(position, role) =>
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Role = role
case Some(false) => ;
case None => ;
}
case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
squad.Availability.lift(position) match {
case Some(true) =>
squad.Membership(position).Orders = orders
case Some(false) => ;
case None => ;
}
case ListSquad() =>
if(!squad.Listed) {
log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for his squad")
squad.Listed = true
}
case ResetAll() =>
squad.Description = ""
squad.ZoneId = None
squad.Availability.indices.foreach { i =>
squad.Availability.update(i, true)
}
//TODO squad members?
case _ => ;
}
//queue updates
if(squad.Listed) {
val entry = SquadService.Publish(squad)
val faction = squad.Faction
val factionListings = publishedLists(faction)
factionListings.find(info => {
info.squad_guid match {
case Some(guid) => guid == squad.GUID
case _ => false
}
}) match {
case Some(listedSquad) =>
val index = factionListings.indexOf(listedSquad)
if(squad.Listed) {
//squad information update
log.info(s"Squad will be updated")
factionListings(index) = entry
val changes = if(listingChanged.nonEmpty) {
SquadService.Differences(listingChanged, entry)
}
else {
SquadService.Differences(listedSquad, entry)
}
if(changes != SquadInfo.Blank) {
SquadEvents.publish(
SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes))))
)
}
}
else {
//remove squad from listing
log.info(s"Squad will be removed")
factionListings.remove(index)
SquadEvents.publish(
SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
)
}
case None if squad.Listed =>
log.info(s"Squad will be introduced")
//first time being published?
factionListings += SquadService.Publish(squad)
SquadEvents.publish(
SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
)
case _ => ;
}
}
case msg =>
log.info(s"Unhandled message $msg from $sender")
}
}
object SquadService {
def Publish(squad : Squad) : SquadInfo = {
SquadInfo(
squad.Leader,
squad.Description,
PlanetSideZoneID(squad.ZoneId),
squad.Size,
squad.Capacity,
squad.GUID
)
}
def Differences(updates : List[Int], info : SquadInfo) : SquadInfo = {
if(updates.nonEmpty) {
var out = SquadInfo.Blank
({
val list = Seq(
SquadInfo.Blank, //must be index-0
SquadInfo(info.leader, None, None, None, None),
SquadInfo(None, info.task, None, None, None),
SquadInfo(None, None, info.zone_id, None, None),
SquadInfo(None, None, None, info.size, None),
SquadInfo(None, None, None, None, info.capacity)
)
updates.map(i => list(i)).filterNot { _ == SquadInfo.Blank }
}) //ignore what code inspection tells you - the parenthesis is necessary
.foreach(sinfo => out = out And sinfo )
out
}
else {
SquadInfo.Blank
}
}
def Differences(before : SquadInfo, after : SquadInfo) : SquadInfo = {
SquadInfo(
if(!before.leader.equals(after.leader)) after.leader else None,
if(!before.task.equals(after.task)) after.task else None,
if(!before.zone_id.equals(after.zone_id)) after.zone_id else None,
if(!before.size.equals(after.size)) after.size else None,
if(!before.capacity.equals(after.capacity)) after.capacity else None
)
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import net.psforever.objects.Player
import net.psforever.packet.game.SquadAction
final case class SquadServiceMessage(forChannel : String, actionMessage : Any)
object SquadServiceMessage {
final case class SquadDefinitionAction(player : Player, zone_ordinal_number : Int, u1 : Int, u2 : Int, action : SquadAction)
final case class RecoverSquadMembership()
}

View file

@ -0,0 +1,7 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import net.psforever.packet.game.SquadInfo
import services.GenericEventBusMsg
final case class SquadServiceResponse(toChannel : String, response : SquadResponse.Response) extends GenericEventBusMsg

View file

@ -21,6 +21,7 @@ import services.ServiceManager
import services.avatar._
import services.galaxy.GalaxyService
import services.local._
import services.teamwork.SquadService
import services.vehicle.VehicleService
import scala.collection.JavaConverters._
@ -216,6 +217,7 @@ object PsLogin {
serviceManager ! ServiceManager.Register(Props[LocalService], "local")
serviceManager ! ServiceManager.Register(Props[VehicleService], "vehicle")
serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster")
//attach event bus entry point to each zone

View file

@ -52,6 +52,7 @@ import services.galaxy.{GalaxyResponse, GalaxyServiceResponse}
import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
import services.vehicle.support.TurretUpgrader
import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
import services.teamwork._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
@ -76,6 +77,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
var localService : ActorRef = ActorRef.noSender
var vehicleService : ActorRef = ActorRef.noSender
var galaxyService : ActorRef = ActorRef.noSender
var squadService : ActorRef = ActorRef.noSender
var taskResolver : ActorRef = Actor.noSender
var cluster : ActorRef = Actor.noSender
var continent : Zone = Zone.Nowhere
@ -266,6 +268,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
ServiceManager.serviceManager ! Lookup("taskResolver")
ServiceManager.serviceManager ! Lookup("cluster")
ServiceManager.serviceManager ! Lookup("galaxy")
ServiceManager.serviceManager ! Lookup("squad")
case _ =>
log.error("Unknown message")
@ -291,6 +294,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case ServiceManager.LookupResult("cluster", endpoint) =>
cluster = endpoint
log.info("ID: " + sessionId + " Got cluster service " + endpoint)
case ServiceManager.LookupResult("squad", endpoint) =>
squadService = endpoint
log.info("ID: " + sessionId + " Got squad service " + endpoint)
case ControlPacket(_, ctrl) =>
handleControlPkt(ctrl)
@ -336,6 +342,37 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleServiceResponse(toChannel, guid, reply) =>
HandleVehicleServiceResponse(toChannel, guid, reply)
case SquadServiceResponse(toChannel, response) =>
response match {
case SquadResponse.Init(infos) if infos.nonEmpty =>
sendResponse(ReplicationStreamMessage(infos))
case SquadResponse.Update(infos) if infos.nonEmpty =>
val o = ReplicationStreamMessage(6, None,
infos.map { case (index, squadInfo) =>
SquadListing(index, squadInfo)
}.toVector
)
log.info(s"updating squad with msg $o")
sendResponse(
ReplicationStreamMessage(6, None,
infos.map { case (index, squadInfo) =>
SquadListing(index, squadInfo)
}.toVector
)
)
case SquadResponse.Remove(infos) if infos.nonEmpty =>
sendResponse(
ReplicationStreamMessage(1, None,
infos.map { index =>
SquadListing(index, None)
}.toVector
)
)
case _ => ;
}
case Deployment.CanDeploy(obj, state) =>
val vehicle_guid = obj.GUID
//TODO remove this arbitrary allowance angle when no longer helpful
@ -806,6 +843,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
vehicleService ! Service.Join(avatar.name) //channel will be player.Name
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
squadService ! Service.Join(avatar.name) //management of any lingering squad information connected to this player
cluster ! InterstellarCluster.GetWorld("home3")
case InterstellarCluster.GiveWorld(zoneId, zone) =>
@ -4733,8 +4772,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ AvatarGrenadeStateMessage(player_guid, state) =>
log.info("AvatarGrenadeStateMessage: " + msg)
case msg @ SquadDefinitionActionMessage(a, b, c, d, e, f, g, h, i) =>
log.info("SquadDefinitionAction: " + msg)
case msg @ SquadDefinitionActionMessage(u1, u2, action) =>
log.info(s"SquadDefinitionAction: $msg")
squadService ! SquadServiceMessage.SquadDefinitionAction(player, continent.Number, u1, u2, action)
case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
log.info("Ouch! " + msg)