Merge pull request #276 from Fate-JH/squad-work

Teamwork
This commit is contained in:
pschord 2019-10-21 21:58:20 -04:00 committed by GitHub
commit ce99ea6ffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 9310 additions and 1566 deletions

View file

@ -22,6 +22,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._
@ -258,6 +259,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

@ -40,6 +40,7 @@ import net.psforever.objects.serverobject.terminals._
import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret}
import net.psforever.objects.teamwork.Squad
import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _}
import net.psforever.objects.vital._
import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector}
@ -52,7 +53,9 @@ 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.{SquadAction => SquadServiceAction, SquadServiceMessage, SquadServiceResponse, SquadResponse, SquadService}
import scala.collection.mutable.LongMap
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.annotation.tailrec
@ -76,6 +79,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
@ -97,6 +101,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
var whenUsedLastKit : Long = 0
val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None)
var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
var updateSquad : () => Unit = NoSquadUpdates
var recentTeleportAttempt : Long = 0
var lastTerminalOrderFulfillment : Boolean = true /**
* used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone)
@ -113,6 +118,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
* no harm should come from leaving the field set to an old unique identifier value after the transfer period
*/
var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
var squad_supplement_id : Int = 0
/**
* When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked.
* This `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component,
* now called "Looking for Squad Member."
* Only the squad leader may toggle the LFSM marquee.
* Upon leaving or disbanding a squad, this value is made false.
* Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
*/
var lfsm : Boolean = false
var squadChannel : Option[String] = None
var squadSetup : () => Unit = FirstTimeSquadSetup
var squadUpdateCounter : Int = 0
val queuedSquadActions : Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
var amsSpawnPoints : List[SpawnPoint] = Nil
var clientKeepAlive : Cancellable = DefaultCancellable.obj
@ -144,6 +164,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
galaxyService ! Service.Leave()
LivePlayerList.Remove(sessionId)
if(player != null && player.HasGUID) {
squadService ! Service.Leave(Some(player.CharId.toString))
val player_guid = player.GUID
//handle orphaned deployables
DisownDeployables()
@ -155,6 +176,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
.collect { case ((index, Some(obj))) => InventoryItem(obj, index) }
) ++ player.Inventory.Items)
.filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] || obj.isInstanceOf[Telepad] })
//put any temporary value back into the avatar
//TODO final character save before doing any of this (use equipment)
continent.Population ! Zone.Population.Release(avatar)
if(player.isAlive) {
@ -266,6 +288,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 +314,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 +362,231 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleServiceResponse(toChannel, guid, reply) =>
HandleVehicleServiceResponse(toChannel, guid, reply)
case SquadServiceResponse(_, excluded, response) =>
if(!excluded.exists(_ == avatar.CharId)) {
response match {
case SquadResponse.ListSquadFavorite(line, task) =>
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
case SquadResponse.InitList(infos) =>
sendResponse(ReplicationStreamMessage(infos))
case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
sendResponse(
ReplicationStreamMessage(6, None,
infos.map { case (index, squadInfo) =>
SquadListing(index, squadInfo)
}.toVector
)
)
case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
sendResponse(
ReplicationStreamMessage(1, None,
infos.map { index =>
SquadListing(index, None)
}.toVector
)
)
case SquadResponse.Detail(guid, detail) =>
sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
case SquadResponse.AssociateWithSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad()))
case SquadResponse.SetListSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
case SquadResponse.Membership(request_type, unk1, unk2, char_id, opt_char_id, player_name, unk5, unk6) =>
val name = request_type match {
case SquadResponseType.Invite if unk5 =>
//player_name is our name; the name of the player indicated by unk3 is needed
LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match {
case Some(player) =>
player.name
case None =>
player_name
}
case _ =>
player_name
}
sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6))
case SquadResponse.WantsSquadPosition(_, name) =>
sendResponse(
ChatMsg(
ChatMessageType.CMT_SQUAD, true, name,
s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
None
)
)
case SquadResponse.Join(squad, positionsToUpdate, toChannel) =>
val leader = squad.Leader
val membershipPositions = positionsToUpdate map squad.Membership.zipWithIndex
StartBundlingPackets()
membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
case Some((ourMember, ourIndex)) =>
//we are joining the squad
//load each member's entry (our own too)
squad_supplement_id = squad.GUID.guid + 1
membershipPositions.foreach { case(member, index) =>
sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
}
//repeat our entry
sendResponse(SquadMemberEvent.Add(squad_supplement_id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
val playerGuid = player.GUID
//turn lfs off
val factionOnContinentChannel = s"${continent.Id}/${player.Faction}"
if(avatar.LFS) {
avatar.LFS = false
sendResponse(PlanetsideAttributeMessage(playerGuid, 53, 0))
avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(playerGuid, 53, 0))
}
//squad colors
GiveSquadColorsInZone()
avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(playerGuid, 31, squad_supplement_id))
//associate with member position in squad
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
updateSquad = PeriodicUpdatesWhenEnrolledInSquad
squadChannel = Some(toChannel)
case _ =>
//other player is joining our squad
//load each member's entry
GiveSquadColorsInZone(
membershipPositions.map { case(member, index) =>
val charId = member.CharId
sendResponse(SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, unk7 = 0))
squadUI(charId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
charId
}
)
}
StopBundlingPackets()
//send an initial dummy update for map icon(s)
sendResponse(SquadState(PlanetSideGUID(squad_supplement_id),
membershipPositions
.filterNot { case (member, _) => member.CharId == avatar.CharId }
.map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) }
.toList
))
case SquadResponse.Leave(squad, positionsToUpdate) =>
StartBundlingPackets()
positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
case Some((ourMember, ourIndex)) =>
//we are leaving the squad
//remove each member's entry (our own too)
positionsToUpdate.foreach { case(member, index) =>
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
squadUI.remove(member)
}
//uninitialize
val playerGuid = player.GUID
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
sendResponse(PlanetsideAttributeMessage(playerGuid, 31, 0)) //disassociate with squad?
avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(playerGuid, 31, 0))
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
lfsm = false
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
squad_supplement_id = 0
squadUpdateCounter = 0
updateSquad = NoSquadUpdates
squadChannel = None
case _ =>
//remove each member's entry
GiveSquadColorsInZone(
positionsToUpdate.map { case(member, index) =>
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
squadUI.remove(member)
member
},
value = 0
)
}
StopBundlingPackets()
case SquadResponse.AssignMember(squad, from_index, to_index) =>
//we've already swapped position internally; now we swap the cards
SwapSquadUIElements(squad, from_index, to_index)
case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) =>
val charId = player.CharId
val guid = player.GUID
lazy val factionOnContinentChannel = s"${continent.Id}/${player.Faction}"
//are we being demoted?
if(squadUI(charId).index == 0) {
//lfsm -> lfs
if(lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 0))
avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 53, 0))
}
lfsm = false
sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad
}
//are we being promoted?
else if(charId == char_id) {
sendResponse(PlanetsideAttributeMessage(guid, 32, 0)) //associate with member position in squad
}
avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id))
//we must fix the squad cards backend
SwapSquadUIElements(squad, from_index, to_index)
case SquadResponse.UpdateMembers(squad, positions) =>
val pairedEntries = positions.collect {
case entry if squadUI.contains(entry.char_id) =>
(entry, squadUI(entry.char_id))
}
//prune entries
val updatedEntries = pairedEntries
.collect({
case (entry, element) if entry.zone_number != element.zone =>
//zone gets updated for these entries
sendResponse(SquadMemberEvent.UpdateZone(squad_supplement_id, entry.char_id, element.index, entry.zone_number))
squadUI(entry.char_id) = SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
entry
case (entry, element) if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
//other elements that need to be updated
squadUI(entry.char_id) = SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
entry
})
.filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend
if(updatedEntries.nonEmpty) {
sendResponse(
SquadState(
PlanetSideGUID(squad_supplement_id),
updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
)
)
}
case SquadResponse.SquadSearchResults() =>
//I don't actually know how to return search results
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
case SquadResponse.InitWaypoints(char_id, waypoints) =>
StartBundlingPackets()
waypoints.foreach { case (waypoint_type, info, unk) =>
sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
}
StopBundlingPackets()
case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type))
case _ => ;
}
}
case Deployment.CanDeploy(obj, state) =>
val vehicle_guid = obj.GUID
//TODO remove this arbitrary allowance angle when no longer helpful
@ -484,7 +735,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.Stamina = stamina
player.Armor = armor
}
sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))
sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), avatar.CharId, player.GUID, false, 6404428))
RemoveCharacterSelectScreenGUID(player)
sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
@ -802,7 +1053,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
traveler = new Traveler(self, continent.Id)
//PropertyOverrideMessage
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //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))
avatarService ! Service.Join(avatar.name) //channel will be player.Name
@ -810,6 +1061,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(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
cluster ! InterstellarCluster.GetWorld("home3")
case InterstellarCluster.GiveWorld(zoneId, zone) =>
@ -2828,7 +3081,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))
sendResponse(ChangeShortcutBankMessage(guid, 0))
//Favorites lists
val (inf, veh) = avatar.Loadouts.partition { case (index, _) => index < 10 }
val (inf, veh) = avatar.EquipmentLoadouts.Loadouts.partition { case (index, _) => index < 10 }
inf.foreach {
case (index, loadout : InfantryLoadout) =>
sendResponse(FavoritesMessage(LoadoutType.Infantry, guid, index, loadout.label, InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)))
@ -2840,9 +3093,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this
deadState = DeadState.Alive
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
//looking for squad (members)
if(tplayer.LFS || lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
}
sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0)))
(1 to 73).foreach(i => {
// not all GUID's are set, and not all of the set ones will always be zero; what does this section do?
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0))
})
(0 to 30).foreach(i => {
@ -2851,7 +3109,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
//AvatarAwardMessage
//DisplayAwardMessage
//SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
//squad stuff (loadouts, assignment)
squadSetup()
//MapObjectStateBlockMessage and ObjectCreateMessage?
//TacticsMessage?
//change the owner on our deployables (re-draw the icons for our deployables too)
@ -2888,6 +3148,97 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
/**
* These messages are dispatched when first starting up the client and connecting to the server for the first time.
* While many of thee messages will be reused for other situations, they appear in this order only during startup.
*/
def FirstTimeSquadSetup() : Unit = {
sendResponse(SquadDetailDefinitionUpdateMessage.Init)
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
//only need to load these once - they persist between zone transfers and respawns
avatar.SquadLoadouts.Loadouts.foreach {
case (index, loadout : SquadLoadout) =>
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task)))
}
//non-squad GUID-0 counts as the settings when not joined with a squad
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.AssociateWithSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId())
squadSetup = RespawnSquadSetup
}
/**
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
* By using `squadUI` to maintain relevant information about squad members,
* especially the unique character identifier number,
* only the zone-specific squad members will receive the important messages about their squad member's spawn.
*/
def RespawnSquadSetup() : Unit = {
if(squadUI.nonEmpty) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id))
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, squadUI(player.CharId).index))
}
}
/**
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
* During a zone change,
* on top of other squad mates in the zone needing to have their knowledge of this player's squad colors changed,
* the player must also set squad colors for each other squad members.
* Default respawn functionality may resume afterwards.
*/
def ZoneChangeSquadSetup() : Unit = {
RespawnSquadSetup()
GiveSquadColorsInZone()
squadSetup = RespawnSquadSetup
}
/**
* Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color.
*/
def GiveSquadColorsInZone() : Unit = {
GiveSquadColorsInZone(squadUI.keys, squad_supplement_id)
}
/**
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
* @param members members of the squad to target
*/
def GiveSquadColorsInZone(members : Iterable[Long]) : Unit = {
GiveSquadColorsInZone(members, squad_supplement_id)
}
/**
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
* @see `PlanetsideAttributeMessage`
* @param members members of the squad to target
* @param value the assignment value
*/
def GiveSquadColorsInZone(members : Iterable[Long], value : Long) : Unit = {
SquadMembersInZone(members).foreach {
members => sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
}
}
/**
* For the listed squad member unique character identifier numbers,
* find and return all squad members in the current zone.
* @param members members of the squad to target
* @return a list of `Player` objects
*/
def SquadMembersInZone(members : Iterable[Long]) : Iterable[Player] = {
val players = continent.LivePlayers
for {
charId <- members
player = players.find { _.CharId == charId }
if player.nonEmpty
} yield player.get
}
def handleControlPkt(pkt : PlanetSideControlPacket) = {
pkt match {
case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) =>
@ -2908,10 +3259,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
log.info(s"New world login to $server with Token:$token. $clientVersion")
//TODO begin temp player character auto-loading; remove later
//this is all just temporary character creation used in the dev branch, making explicit values that allow for testing
//the unique character identifier number for this testing character is based on the original test character,
//whose identifier number was 41605314
//all head features, faction, and sex also match that test character
import net.psforever.objects.GlobalDefinitions._
import net.psforever.types.CertificationType._
val faction = PlanetSideEmpire.VS
val avatar = Avatar(s"TestCharacter$sessionId", faction, CharacterGender.Female, 41, CharacterVoice.Voice1)
val avatar = new Avatar(41605313L+sessionId, s"TestCharacter$sessionId", faction, CharacterGender.Female, 41, CharacterVoice.Voice1)
avatar.Certifications += StandardAssault
avatar.Certifications += MediumAssault
avatar.Certifications += StandardExoSuit
@ -2946,6 +3301,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatar.Certifications += AssaultEngineering
avatar.Certifications += Hacking
avatar.Certifications += AdvancedHacking
avatar.CEP = 6000001
this.avatar = avatar
InitializeDeployableQuantities(avatar) //set deployables ui elements
@ -3043,7 +3399,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(TimeOfDayMessage(1191182336))
//custom
sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary."
sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
//(0 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) })
@ -3339,6 +3695,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case None => false
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand))
updateSquad()
}
case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
@ -3380,6 +3737,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, is_cloaked))
}
updateSquad()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
@ -4466,7 +4824,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
}
}
else if(action == 16) {
else if(action == 16) { //max deployment
log.info(s"GenericObject: $player has released the anchors")
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 19, 0))
@ -4484,6 +4842,30 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
}
}
else if(action == 36) { //Looking For Squad ON
if(squadUI.nonEmpty) {
if(!lfsm && squadUI(player.CharId).index == 0) {
lfsm = true
avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
}
}
else if(!avatar.LFS) {
avatar.LFS = true
avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
}
}
else if(action == 37) { //Looking For Squad OFF
if(squadUI.nonEmpty) {
if(lfsm && squadUI(player.CharId).index == 0) {
lfsm = false
avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
}
}
else if(avatar.LFS) {
avatar.LFS = false
avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
}
}
case msg @ ItemTransactionMessage(terminal_guid, transaction_type, _, _, _, _) =>
log.info("ItemTransaction: " + msg)
@ -4522,18 +4904,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
None
}) match {
case Some(owner : Player) => //InfantryLoadout
avatar.SaveLoadout(owner, name, lineno)
avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
import InfantryLoadout._
sendResponse(FavoritesMessage(list, player_guid, line, name, DetermineSubtypeB(player.ExoSuit, DetermineSubtype(player))))
case Some(owner : Vehicle) => //VehicleLoadout
avatar.SaveLoadout(owner, name, lineno)
avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
sendResponse(FavoritesMessage(list, player_guid, line, name))
case Some(_) | None =>
log.error("FavoritesRequest: unexpected owner for favorites")
}
case FavoritesAction.Delete =>
avatar.DeleteLoadout(lineno)
avatar.EquipmentLoadouts.DeleteLoadout(lineno)
sendResponse(FavoritesMessage(list, player_guid, line, ""))
case FavoritesAction.Unknown =>
@ -4778,8 +5160,17 @@ 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(player, continent, SquadServiceAction.Definition(u1, u2, action))
case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
log.info(s"$msg")
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
case msg @ SquadWaypointRequest(request, _, wtype, unk, info) =>
log.info(s"Waypoint Request: $msg")
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
log.info("Ouch! " + msg)
@ -7520,9 +7911,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
/**
* Properly format a `DestroyDisplayMessage` packet
* given sufficient information about a target (victim) and an actor (killer).
* For the packet, the `*_charId` field is most important to determining distinction between players.
* The "char id" is not a currently supported field for different players so a name hash is used instead.
* The virtually negligent chance of a name hash collision is covered.
* For the packet, the `charId` field is important for determining distinction between players.
* @param killer the killer's entry
* @param victim the victim's entry
* @param method the manner of death
@ -7531,13 +7920,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
* @return a `DestroyDisplayMessage` packet that is properly formatted
*/
def DestroyDisplayMessage(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) : DestroyDisplayMessage = {
//TODO charId should reflect the player more properly
val killerCharId = math.abs(killer.Name.hashCode)
var victimCharId = math.abs(victim.Name.hashCode)
if(killerCharId == victimCharId && !killer.Name.equals(victim.Name)) {
//odds of hash collision in a populated zone should be close to odds of being struck by lightning
victimCharId = Int.MaxValue - victimCharId + 1
}
val killer_seated = killer match {
case obj : PlayerSource => obj.Seated
case _ => false
@ -7547,9 +7929,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case _ => false
}
new DestroyDisplayMessage(
killer.Name, killerCharId, killer.Faction, killer_seated,
killer.Name, killer.CharId, killer.Faction, killer_seated,
unk, method,
victim.Name, victimCharId, victim.Faction, victim_seated
victim.Name, victim.CharId, victim.Faction, victim_seated
)
}
@ -8478,6 +8860,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
DisownDeployables()
drawDeloyableIcon = RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
squadSetup = ZoneChangeSquadSetup
continent.Population ! Zone.Population.Leave(avatar)
}
@ -8947,6 +9330,76 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = {
if(squadUI.nonEmpty) {
val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object
val fromCharId = fromMember.CharId
val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object
val toCharId = toMember.CharId
val id = 11
if(toCharId > 0) {
//toMember and fromMember have swapped places
val fromElem = squadUI(fromCharId)
val toElem = squadUI(toCharId)
squadUI(toCharId) = SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position)
squadUI(fromCharId) = SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position)
sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0))
sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, unk7 = 0))
sendResponse(
SquadState(
PlanetSideGUID(id),
List(
SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, false, 429, None, None),
SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, false, 429, None, None)
)
)
)
}
else {
//previous fromMember has moved toMember
val elem = squadUI(fromCharId)
squadUI(fromCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position)
sendResponse(SquadMemberEvent.Remove(id, fromCharId, fromIndex))
sendResponse(SquadMemberEvent.Add(id, fromCharId, toIndex, elem.name, elem.zone, unk7 = 0))
sendResponse(
SquadState(
PlanetSideGUID(id),
List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
)
)
}
val charId = avatar.CharId
if(toCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
}
else if(fromCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
}
}
}
def NoSquadUpdates() : Unit = { }
def SquadUpdates() : Unit = {
squadService ! SquadServiceMessage(
player,
continent,
continent.GUID(player.VehicleSeated) match {
case Some(vehicle : Vehicle) =>
SquadServiceAction.Update(player.CharId, vehicle.Health, vehicle.MaxHealth, vehicle.Shields, vehicle.MaxShields, vehicle.Position, continent.Number)
case Some(obj : PlanetSideGameObject with WeaponTurret) =>
SquadServiceAction.Update(player.CharId, obj.Health, obj.MaxHealth, 0, 0, obj.Position, continent.Number)
case _ =>
SquadServiceAction.Update(player.CharId, player.Health, player.MaxHealth, player.Armor, player.MaxArmor, player.Position, continent.Number)
}
)
}
def PeriodicUpdatesWhenEnrolledInSquad() : Unit = {
queuedSquadActions(squadUpdateCounter)()
squadUpdateCounter = (squadUpdateCounter + 1) % queuedSquadActions.length
}
def failWithError(error : String) = {
log.error(error)
sendResponse(ConnectionClose())
@ -9103,6 +9556,8 @@ object WorldSessionActor {
completeAction : () => Unit,
tickAction : Option[() => Unit] = None)
protected final case class SquadUIElement(name : String, index : Int, zone : Int, health : Int, armor : Int, position : Vector3)
private final case class NtuCharging(tplayer: Player,
vehicle: Vehicle)
private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID)

View file

@ -485,7 +485,7 @@ class PacketCodingActorITest extends ActorTest {
)
val obj = DetailedPlayerData(pos, app, char, InventoryData(Nil), DrawnSlot.None)
val pkt = MultiPacketBundle(List(ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj)))
val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c00490084524000000000000000000000000000000020000007f35703fffffffffffffffffffffffffffffffc000000000000000000000000000000000000000190019000640000000000c800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000400e0"
val string_hex = hex"00090000186c060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c00490084524000000000000000000000000000000020000007f35703fffffffffffffffffffffffffffffffc000000000000000000000000000000000000000190019000640000000000c800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000100000000400e0"
"PacketCodingActor" should {
"bundle an r-originating packet into an l-facing SlottedMetaPacket byte stream data (SlottedMetaPacket)" in {