Team Building Exercise [Incomplete] (#1013)

* Squad -> SquadFeatures from a common lookup

* refactor to SquadService, etc.; clarification of fields related to outfit ids

* thorough pass over squad functionality in regards to joining a squad or rejecting a squad invitation

* decorating squads in the squad list to indicate availability based on size/capacity or role requirements

* squad list decoration; rudimentary squad list searching

* renovations to squad joining, promotion, and certain invitation/cleanup procedures

* further renovations to squad joining, promotion, and certain invitation/cleanup procedures

* overhaul of squad joining and squad promotion

* separated the invitation system from the squad publishing and manipulating system
This commit is contained in:
Fate-JH 2023-01-16 10:42:05 -05:00 committed by GitHub
parent 3bd50dc89c
commit ebfc028f5c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 4782 additions and 3626 deletions

View file

@ -53,6 +53,7 @@ import net.psforever.util.Database._
import net.psforever.persistence
import net.psforever.util.{Config, Database, DefinitionUtil}
import net.psforever.services.Service
//import org.log4s.Logger
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
object AvatarActor {
@ -435,6 +436,15 @@ object AvatarActor {
}
}
def displayLookingForSquad(session: Session, state: Int): Unit = {
val player = session.player
session.zone.AvatarEvents ! AvatarServiceMessage(
player.Faction.toString,
AvatarAction.PlanetsideAttribute(player.GUID, 53, state)
)
}
/**
* 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.
@ -831,6 +841,11 @@ class AvatarActor(
session = Some(newSession)
Behaviors.same
case SetLookingForSquad(lfs) =>
avatarCopy(avatar.copy(lookingForSquad = lfs))
AvatarActor.displayLookingForSquad(session.get, if (lfs) 1 else 0)
Behaviors.same
case CreateAvatar(name, head, voice, gender, empire) =>
import ctx._
ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete {

View file

@ -823,20 +823,26 @@ class ChatActor(
val popTR = players.count(_.faction == PlanetSideEmpire.TR)
val popNC = players.count(_.faction == PlanetSideEmpire.NC)
val popVS = players.count(_.faction == PlanetSideEmpire.VS)
val contName = session.zone.map.name
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None)
)
if (popNC + popTR + popVS == 0) {
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, false, "", "@Nomatches", None)
)
} else {
val contName = session.zone.map.name
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None)
)
}
case (CMT_ZONE, _, contents) if gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+")

View file

@ -41,7 +41,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.serverobject.zipline.ZipLinePath
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject}
import net.psforever.objects.teamwork.Squad
import net.psforever.objects.teamwork.{Member, Squad}
import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.control.BfrFlight
@ -151,12 +151,13 @@ object SessionActor {
private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20)
protected final case class SquadUIElement(
name: String,
index: Int,
zone: Int,
health: Int,
armor: Int,
position: Vector3
name: String = "",
outfit: Long = 0,
index: Int = -1,
zone: Int = 0,
health: Int = 0,
armor: Int = 0,
position: Vector3 = Vector3.Zero
)
private final case class NtuCharging(tplayer: Player, vehicle: Vehicle)
@ -207,6 +208,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
var updateSquadRef: ActorRef = Default.Actor
var updateSquad: () => Unit = NoSquadUpdates
var recentTeleportAttempt: Long = 0
var lastTerminalOrderFulfillment: Boolean = true
@ -740,26 +742,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
zoningType = Zoning.Method.InstantAction
zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION
zoningStatus = Zoning.Status.Request
/* TODO no ask or adapters from classic to typed so this logic is happening in SpawnPointResponse
implicit val timeout = Timeout(1 seconds)
val future =
ask(cluster.toClassic, ICS.GetInstantActionSpawnPoint(player.Faction, context.self))
.mapTo[ICS.SpawnPointResponse]
Await.result(future, 2 second) match {
case ICS.SpawnPointResponse(None) =>
sendResponse(
ChatMsg(ChatMessageType.CMT_INSTANTACTION, false, "", "@InstantActionNoHotspotsAvailable", None)
)
case ICS.SpawnPointResponse(Some(_)) =>
beginZoningCountdown(() => {
cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
})
}
beginZoningCountdown(() => {
cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
})
*/
cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
case Quit() =>
@ -991,11 +973,26 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
)
)
case SquadResponse.SquadDecoration(guid, squad) =>
val decoration = if (
squadUI.nonEmpty ||
squad.Size == squad.Capacity ||
{
val offer = avatar.certifications
!squad.Membership.exists { _.isAvailable(offer) }
}
) {
SquadListDecoration.NotAvailable
} else {
SquadListDecoration.Available
}
sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration)))
case SquadResponse.Detail(guid, detail) =>
sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
case SquadResponse.AssociateWithSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad()))
case SquadResponse.IdentifyAsSquadLeader(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
case SquadResponse.SetListSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
@ -1003,7 +1000,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case SquadResponse.Membership(request_type, unk1, unk2, charId, 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
//the name of the player indicated by unk3 is needed
LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
case Some(player) =>
player.name
@ -1026,10 +1023,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
)
)
case SquadResponse.Join(squad, positionsToUpdate, toChannel) =>
val leader = squad.Leader
val membershipPositions = positionsToUpdate map squad.Membership.zipWithIndex
membershipPositions.find({ case (member, _) => member.CharId == avatar.id }) match {
case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
val avatarId = avatar.id
val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
.filter { case (mem, index) =>
mem.CharId > 0 && positionsToUpdate.contains(index)
}
membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
case Some((ourMember, ourIndex)) =>
//we are joining the squad
//load each member's entry (our own too)
@ -1043,11 +1043,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
index,
member.Name,
member.ZoneId,
unk7 = 0
outfit_id = 0
)
)
squadUI(member.CharId) =
SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
}
//repeat our entry
sendResponse(
@ -1057,39 +1057,38 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ourIndex,
ourMember.Name,
ourMember.ZoneId,
unk7 = 0
outfit_id = 0
)
)
val playerGuid = player.GUID
//turn lfs off
val factionChannel = s"${player.Faction}"
if (avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false)
}
val playerGuid = player.GUID
val factionChannel = s"${player.Faction}"
//squad colors
GiveSquadColorsInZone()
continent.AvatarEvents ! AvatarServiceMessage(
factionChannel,
AvatarAction.PlanetsideAttribute(playerGuid, 31, squad_supplement_id)
)
GiveSquadColorsToMembers()
GiveSquadColorsForOthers(playerGuid, factionChannel, 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)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
updateSquadRef = ref
updateSquad = PeriodicUpdatesWhenEnrolledInSquad
chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID))
case _ =>
//other player is joining our squad
//load each member's entry
GiveSquadColorsInZone(
GiveSquadColorsToMembers(
membershipPositions.map {
case (member, index) =>
val charId = member.CharId
sendResponse(
SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, unk7 = 0)
SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
)
squadUI(charId) =
SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
charId
}
)
@ -1098,23 +1097,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(
SquadState(
PlanetSideGUID(squad_supplement_id),
membershipPositions
.filterNot { case (member, _) => member.CharId == avatar.id }
.map {
case (member, _) =>
SquadStateInfo(
member.CharId,
member.Health,
member.Armor,
member.Position,
2,
2,
false,
429,
None,
None
)
}
membershipPositions.map { case (member, _) =>
SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
}
)
)
@ -1123,6 +1108,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case Some((ourMember, ourIndex)) =>
//we are leaving the squad
//remove each member's entry (our own too)
updateSquadRef = Default.Actor
positionsToUpdate.foreach {
case (member, index) =>
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
@ -1131,16 +1117,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//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?
continent.AvatarEvents ! AvatarServiceMessage(
s"${player.Faction}",
AvatarAction.PlanetsideAttribute(playerGuid, 31, 0)
)
sendResponse(
PlanetsideAttributeMessage(playerGuid, 32, 0)
) //disassociate with member position in squad?
GiveSquadColorsToSelf(value = 0)
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
lfsm = false
avatarActor ! AvatarActor.SetLookingForSquad(false)
//a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
squad_supplement_id = 0
@ -1149,7 +1130,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
case _ =>
//remove each member's entry
GiveSquadColorsInZone(
GiveSquadColorsToMembers(
positionsToUpdate.map {
case (member, index) =>
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
@ -1164,35 +1145,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//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 factionChannel = s"${player.Faction}"
//are we being demoted?
if (squadUI(charId).index == 0) {
//lfsm -> lfs
case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
if (promotedPlayer != player.CharId) {
//demoted from leader; no longer lfsm
if (lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 0))
continent.AvatarEvents ! AvatarServiceMessage(
factionChannel,
AvatarAction.PlanetsideAttribute(guid, 53, 0)
)
lfsm = false
AvatarActor.displayLookingForSquad(session, state = 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
}
continent.AvatarEvents ! AvatarServiceMessage(
factionChannel,
AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id)
)
//we must fix the squad cards backend
SwapSquadUIElements(squad, from_index, to_index)
sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
//the players have already been swapped in the backend object
PromoteSquadUIElements(squad, from_index)
case SquadResponse.UpdateMembers(squad, positions) =>
case SquadResponse.UpdateMembers(_, positions) =>
val pairedEntries = positions.collect {
case entry if squadUI.contains(entry.char_id) =>
(entry, squadUI(entry.char_id))
@ -1206,13 +1171,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
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)
SquadUIElement(element.name, element.outfit, 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)
SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
entry
})
.filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend
@ -1221,15 +1186,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
SquadState(
PlanetSideGUID(squad_supplement_id),
updatedEntries.map { entry =>
SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2, 2, false, 429, None, None)
SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
}
)
)
}
case SquadResponse.SquadSearchResults() =>
//I don't actually know how to return search results
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
case SquadResponse.SquadSearchResults(results) =>
//TODO positive squad search results message?
if(results.nonEmpty) {
results.foreach { guid =>
sendResponse(SquadDefinitionActionMessage(
guid,
0,
SquadAction.SquadListDecorator(SquadListDecoration.SearchResult))
)
}
} else {
sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults()))
}
sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch()))
case SquadResponse.InitWaypoints(char_id, waypoints) =>
waypoints.foreach {
@ -3086,7 +3065,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
TaskWorkflow.execute(BuyNewEquipmentPutInInventory(
continent.GUID(tplayer.VehicleSeated) match { case Some(v: Vehicle) => v; case _ => player },
continent.GUID(tplayer.VehicleSeated) match { case Some(v: Vehicle) => v; case _ => tplayer },
tplayer,
msg.terminal_guid
)(item))
@ -3642,8 +3621,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, unk5=true))
//looking for squad (members)
if (tplayer.avatar.lookingForSquad || lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
AvatarActor.displayLookingForSquad(session, state = 1)
}
sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0)))
//these are facilities and towers and bunkers in the zone, but not necessarily all of them for some reason
@ -3862,7 +3840,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case (None, _) => ;
}
//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.IdentifyAsSquadLeader()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
@ -3880,11 +3858,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (squad_supplement_id > 0) {
squadUI.get(player.CharId) match {
case Some(elem) =>
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
continent.AvatarEvents ! AvatarServiceMessage(
s"${player.Faction}",
AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id)
)
GiveSquadColorsToSelf(squad_supplement_id)
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, elem.index))
case _ =>
log.warn(s"RespawnSquadSetup: asked to redraw squad information, but ${player.Name} has no squad element for squad $squad_supplement_id")
@ -3892,6 +3866,45 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}
}
/**
* Give the squad colors associated with the current squad to the client's player character.
* @param value value to associate the player
*/
def GiveSquadColorsToSelf(value: Long): Unit = {
GiveSquadColorsToSelf(player.GUID, player.Faction, value)
}
/**
* Give the squad colors associated with the current squad to the client's player character.
* @param guid player guid
* @param faction faction for targeted updates to other players
* @param value value to associate the player
*/
def GiveSquadColorsToSelf(guid: PlanetSideGUID, faction: PlanetSideEmpire.Value, value: Long): Unit = {
sendResponse(PlanetsideAttributeMessage(guid, 31, value))
GiveSquadColorsForOthers(guid, faction, value)
}
/**
* Give the squad colors associated with the current squad to the client's player character.
* @param guid player guid
* @param faction faction for targeted updates to other players
* @param value value to associate the player
*/
def GiveSquadColorsForOthers(guid: PlanetSideGUID, faction: PlanetSideEmpire.Value, value: Long): Unit = {
GiveSquadColorsForOthers(guid, faction.toString, value)
}
/**
* Give the squad colors associated with the current squad to the client's player character to other players.
* @param guid player guid
* @param faction faction for targeted updates to other players
* @param value value to associate the player
*/
def GiveSquadColorsForOthers(guid: PlanetSideGUID, factionChannel: String, value: Long): Unit = {
continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(guid, 31, value))
}
/**
* These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
* During a zone change,
@ -3902,23 +3915,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
def ZoneChangeSquadSetup(): Unit = {
RespawnSquadSetup()
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
GiveSquadColorsInZone()
GiveSquadColorsToMembers()
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)
def GiveSquadColorsToMembers(): Unit = {
GiveSquadColorsToMembers(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)
def GiveSquadColorsToMembers(members: Iterable[Long]): Unit = {
GiveSquadColorsToMembers(members, squad_supplement_id)
}
/**
@ -3927,7 +3940,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
* @param members members of the squad to target
* @param value the assignment value
*/
def GiveSquadColorsInZone(members: Iterable[Long], value: Long): Unit = {
def GiveSquadColorsToMembers(members: Iterable[Long], value: Long): Unit = {
SquadMembersInZone(members).foreach { members =>
sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
}
@ -5784,15 +5797,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else if (action == 30) {
log.info(s"${player.Name} is back")
player.AwayFromKeyboard = false
} else if (action == GenericActionEnum.DropSpecialItem.id) {
DropSpecialSlotItem()
renewCharSavedTimer(
Config.app.game.savedMsg.renewal.fixed,
Config.app.game.savedMsg.renewal.variable
)
}
if (action == GenericActionEnum.DropSpecialItem.id) {
DropSpecialSlotItem()
}
if (action == 15) { //max deployment
} else if (action == 15) { //max deployment
log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground")
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
continent.AvatarEvents ! AvatarServiceMessage(
@ -5851,32 +5862,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else {
log.warn(s"GenericActionMessage: ${player.Name} can't handle action code 21")
}
} else if (action == 36) { //Looking For Squad ON
if (squadUI.nonEmpty) {
if (!lfsm && squadUI(player.CharId).index == 0) {
lfsm = true
continent.AvatarEvents ! AvatarServiceMessage(
s"${player.Faction}",
AvatarAction.PlanetsideAttribute(player.GUID, 53, 1)
)
}
} else if (!avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(true)
}
} else if (action == 37) { //Looking For Squad OFF
if (squadUI.nonEmpty) {
if (lfsm && squadUI(player.CharId).index == 0) {
lfsm = false
continent.AvatarEvents ! AvatarServiceMessage(
s"${player.Faction}",
AvatarAction.PlanetsideAttribute(player.GUID, 53, 0)
)
}
} else if (avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false)
} else if (action == 36 || action == 37) { //Looking For Squad (Members) (on/off)
val state = if (action == 36) { true } else { false }
squadUI.get(player.CharId) match {
case Some(elem) if elem.index == 0 =>
lfsm = state
AvatarActor.displayLookingForSquad(session, boolToInt(state))
case _ =>
avatarActor ! AvatarActor.SetLookingForSquad(state)
}
} else {
log.debug(s"$msg")
log.warn(s"GenericActionMessage: ${player.Name} can't handle $msg")
}
}
@ -9280,66 +9276,131 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
}
}
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))
/**
* Swap the squad UI elements along the top of the screen
* (colloquially referred to as "cards")
* to match recently updated positions of the members in a squad.<br>
* <br>
* The squad membership structure should have already updated to the correct positions of the members.
* By referencing one of the indices in this structure,
* obtain the character identification number,
* use the character identification number to locate that member's card,
* and update the card index to match the squad position in which that member is discovered.
* @see `PlanetsideAttributeMessage`
* @see `Squad`
* @see `SquadMemberEvent.Add`
* @see `SquadMemberEvent.Remove`
* @see `SquadState`
* @see `SquadStateInfo`
* @param squad the squad that supports the membership structure
* @param firstIndex the index of a player in the squad's membership;
* the member position must be occupied
* @param secondIndex the optional index of a player in the squad's membership;
* if the member position is occupied,
* the member at the first index swaps with the member at this second index
*/
def SwapSquadUIElements(squad: Squad, firstIndex: Int, secondIndex: Int): Unit = {
//the players should have already been swapped in the backend object
val firstMember = squad.Membership(firstIndex)
val firstCharId = firstMember.CharId
if (squadUI.nonEmpty && firstCharId > 0) {
//the firstIndex should always point to a valid player in the squad
val sguid = squad.GUID
val id = squad_supplement_id
val isFirstMoving = firstCharId == player.CharId
val secondMember = squad.Membership(secondIndex)
val secondCharId = secondMember.CharId
if (secondCharId > 0 && firstCharId != secondCharId) {
//secondMember and firstMember swap places
val newFirstElem = squadUI(firstCharId).copy(index = firstIndex)
val newSecondElem = squadUI(secondCharId).copy(index = secondIndex)
squadUI.put(firstCharId, newFirstElem)
squadUI.put(secondCharId, newSecondElem)
// sendResponse(SquadMemberEvent.Remove(id, secondCharId, firstIndex))
// sendResponse(SquadMemberEvent.Remove(id, firstCharId, secondIndex))
sendResponse(SquadMemberEvent.Add(id, firstCharId, firstIndex, newFirstElem.name, newFirstElem.zone, outfit_id = 0))
sendResponse(SquadMemberEvent.Add(id, secondCharId, secondIndex, newSecondElem.name, newSecondElem.zone, outfit_id = 0))
if (isFirstMoving) {
sendResponse(PlanetsideAttributeMessage(firstMember.GUID, 32, firstIndex))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
} else if (secondCharId == player.CharId) {
sendResponse(PlanetsideAttributeMessage(secondMember.GUID, 32, secondIndex))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
}
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)
)
)
SquadState(PlanetSideGUID(id), List(
SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position),
SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position)
))
)
} 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))
//firstMember has moved to a new position in squad
val elem = squadUI(firstCharId)
squadUI.put(firstCharId, elem.copy(index = firstIndex))
// sendResponse(SquadMemberEvent.Remove(id, firstCharId, elem.index))
sendResponse(SquadMemberEvent.Add(id, firstCharId, firstIndex, elem.name, elem.zone, outfit_id = 0))
if (firstCharId == player.CharId) {
sendResponse(PlanetsideAttributeMessage(firstMember.GUID, 32, firstIndex))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
}
sendResponse(
SquadState(
PlanetSideGUID(id),
List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
)
SquadState(PlanetSideGUID(id), List(SquadStateInfo(firstCharId, elem.health, elem.armor, elem.position)))
)
}
val charId = avatar.id
if (toCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
} else if (fromCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
}
}
}
def NoSquadUpdates(): Unit = {}
def PromoteSquadUIElements(squad: Squad, fromIndex: Int): Unit = {
//the players should have already been swapped in the backend object
val firstMember = squad.Membership(0)
val firstCharId = firstMember.CharId
val secondMember = squad.Membership(fromIndex)
val secondCharId = secondMember.CharId
if (squadUI.nonEmpty && fromIndex != 0 && firstCharId > 0 && secondCharId > 0) {
val newFirstElem = squadUI(firstCharId).copy(index = 0)
val newSecondElem = squadUI(secondCharId).copy(index = fromIndex)
val charId = player.CharId
val pguid = player.GUID
val sguid = squad.GUID
val id = squad_supplement_id
//secondMember and firstMember swap places
squadUI.put(firstCharId, newFirstElem)
squadUI.put(secondCharId, newSecondElem)
sendResponse(SquadMemberEvent(MemberEvent.Promote, id, firstCharId, position = 0))
//player is being either promoted or demoted?
if (firstCharId == charId) {
sendResponse(PlanetsideAttributeMessage(pguid, 32, 0))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.IdentifyAsSquadLeader()))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
} else if (secondCharId == charId) {
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.IdentifyAsSquadLeader()))
sendResponse(PlanetsideAttributeMessage(pguid, 32, fromIndex))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
}
//seed updates (just for the swapped players)
sendResponse(
SquadState(PlanetSideGUID(id), List(
SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position),
SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position)
))
)
}
}
def NoSquadUpdates(): Unit = { }
def SquadUpdates(): Unit = {
squadService ! SquadServiceMessage(
updateSquadRef ! SquadServiceMessage(
player,
continent,
SquadServiceAction.Update(
player.CharId,
player.GUID,
player.Health,
player.MaxHealth,
player.Armor,
player.MaxArmor,
avatar.certifications,
player.Position,
continent.Number
)

View file

@ -2,7 +2,7 @@
package net.psforever.objects.teamwork
import net.psforever.objects.avatar.Certification
import net.psforever.types.Vector3
import net.psforever.types.{PlanetSideGUID, Vector3}
class Member {
//about the position to be filled
@ -12,10 +12,12 @@ class Member {
//about the individual filling the position
private var name: String = ""
private var charId: Long = 0L
private var guid: Int = 0
private var health: Int = 0
private var armor: Int = 0
private var zoneId: Int = 0
private var position: Vector3 = Vector3.Zero
private var certs: Set[Certification] = Set()
def Role: String = role
@ -52,6 +54,17 @@ class Member {
CharId
}
def GUID: PlanetSideGUID = PlanetSideGUID(guid)
def GUID_=(guid: PlanetSideGUID): PlanetSideGUID = {
GUID_=(guid.guid)
}
def GUID_=(thisGuid: Int): PlanetSideGUID = {
guid = thisGuid
GUID
}
def Health: Int = health
def Health_=(red: Int): Int = {
@ -80,6 +93,13 @@ class Member {
Position
}
def Certifications: Set[Certification] = certs
def Certifications_=(req: Set[Certification]): Set[Certification] = {
certs = req
Certifications
}
def isAvailable: Boolean = {
charId == 0
}

View file

@ -2,9 +2,10 @@
package net.psforever.objects.teamwork
import akka.actor.{ActorContext, ActorRef, Props}
import net.psforever.types.SquadWaypoint
import net.psforever.services.teamwork.SquadService.WaypointData
import net.psforever.services.teamwork.SquadSwitchboard
import net.psforever.objects.Default
import net.psforever.packet.game.WaypointInfo
import net.psforever.types.{PlanetSideGUID, SquadWaypoint, Vector3}
import net.psforever.services.teamwork.{SquadSubscriptionEntity, SquadSwitchboard}
class SquadFeatures(val Squad: Squad) {
@ -40,7 +41,11 @@ class SquadFeatures(val Squad: Squad) {
* Waypoints manifest in the game world as a (usually far-off) beam of light that extends into the sky
* and whose ground contact utilizes a downwards pulsating arrow.
* On the continental map and deployment map, they appear as a diamond, with a different number where applicable.
* The squad leader experience rally, for example, does not have a number like the preceding four waypoints.
* The squad leader experience rally, for example, does not have a number like the preceding four waypoints.<br>
* <br>
* Laze waypoints are as numerous as the number of players in a squad and
* exist only for fifteen seconds at a time.
* They are not counted in this list.
* @see `Start`
*/
private var waypoints: Array[WaypointData] = Array[WaypointData]()
@ -65,25 +70,25 @@ class SquadFeatures(val Squad: Squad) {
* For the purposes of wide-searches for membership
* such as Looking For Squad checks and proximity invitation,
* the unique character identifier numbers in this list are skipped.
* Direct invitation requests from the non sqad member should remain functional.
* Direct invitation requests from the non squad member should remain functional.
*/
private var refusedPlayers: List[Long] = Nil
private var autoApproveInvitationRequests: Boolean = false
private var locationFollowsSquadLead: Boolean = true
private var locationFollowsSquadLead: Boolean = true //TODO false
private var listed: Boolean = false
private lazy val channel: String = s"${Squad.Faction}-Squad${Squad.GUID.guid}"
def Start(implicit context: ActorContext): SquadFeatures = {
switchboard = context.actorOf(Props[SquadSwitchboard](), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}")
def Start(implicit context: ActorContext, subs: SquadSubscriptionEntity): SquadFeatures = {
switchboard = context.actorOf(Props(classOf[SquadSwitchboard], this, subs), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}")
waypoints = Array.fill[WaypointData](SquadWaypoint.values.size)(new WaypointData())
this
}
def Stop: SquadFeatures = {
switchboard ! akka.actor.PoisonPill
switchboard = ActorRef.noSender
switchboard = Default.Actor
waypoints = Array.empty
this
}
@ -99,6 +104,58 @@ class SquadFeatures(val Squad: Squad) {
def Waypoints: Array[WaypointData] = waypoints
/**
* Display the indicated waypoint.<br>
* <br>
* Despite the name, no waypoints are actually "added."
* All of the waypoints constantly exist as long as the squad to which they are attached exists.
* They are merely "activated" and "deactivated."
* No waypoint is ever remembered for the laze-indicated target.
* @see `SquadWaypointRequest`
* @see `WaypointInfo`
* @param guid the squad's unique identifier
* @param waypointType the type of the waypoint
* @param info information about the waypoint, as was reported by the client's packet
* @return the waypoint data, if the waypoint type is changed
*/
def AddWaypoint(
guid: PlanetSideGUID,
waypointType: SquadWaypoint,
info: WaypointInfo
): Option[WaypointData] = {
waypoints.lift(waypointType.value) match {
case Some(point) =>
point.zone_number = info.zone_number
point.pos = info.pos
Some(point)
case None =>
None
}
}
/**
* Hide the indicated waypoint.
* Unused waypoints are marked by having a non-zero z-coordinate.<br>
* <br>
* Despite the name, no waypoints are actually "removed."
* All of the waypoints constantly exist as long as the squad to which they are attached exists.
* They are merely "activated" and "deactivated."
* @param guid the squad's unique identifier
* @param waypointType the type of the waypoint
*/
def RemoveWaypoint(guid: PlanetSideGUID, waypointType: SquadWaypoint): Option[WaypointData] = {
waypoints.lift(waypointType.value) match {
case Some(point) =>
val oldWaypoint = WaypointData(point.zone_number, point.pos)
point.pos = Vector3.z(1)
Some(oldWaypoint)
case None =>
None
}
}
def SearchForRole: Option[Int] = searchForRole
def SearchForRole_=(role: Int): Option[Int] = SearchForRole_=(Some(role))
@ -115,15 +172,24 @@ class SquadFeatures(val Squad: Squad) {
ProxyInvites
}
def Refuse: List[Long] = refusedPlayers
def DeniedPlayers(): List[Long] = refusedPlayers
def Refuse_=(charId: Long): List[Long] = {
Refuse_=(List(charId))
def DeniedPlayers(charId: Long): List[Long] = {
DeniedPlayers(List(charId))
}
def Refuse_=(list: List[Long]): List[Long] = {
def DeniedPlayers(list: List[Long]): List[Long] = {
refusedPlayers = list ++ refusedPlayers
Refuse
DeniedPlayers()
}
def AllowedPlayers(charId: Long): List[Long] = {
AllowedPlayers(List(charId))
}
def AllowedPlayers(list: List[Long]): List[Long] = {
refusedPlayers = refusedPlayers.filterNot(list.contains)
DeniedPlayers()
}
def LocationFollowsSquadLead: Boolean = locationFollowsSquadLead

View file

@ -0,0 +1,21 @@
// Copyright (c) 2022 PSForever
package net.psforever.objects.teamwork
import net.psforever.types.Vector3
/**
* Information necessary to display a specific map marker.
*/
class WaypointData() {
var zone_number: Int = 1
var pos: Vector3 = Vector3.z(1) //a waypoint with a non-zero z-coordinate will flag as not getting drawn
}
object WaypointData{
def apply(zone_number: Int, pos: Vector3): WaypointData = {
val data = new WaypointData()
data.zone_number = zone_number
data.pos = pos
data
}
}

View file

@ -10,10 +10,10 @@ import shapeless.{::, HNil}
final case class CharacterKnowledgeInfo(
name: String,
permissions: Set[Certification],
certifications: Set[Certification],
unk1: Int,
unk2: Int,
unk3: PlanetSideGUID
zoneNumber: Int
)
final case class CharacterKnowledgeMessage(char_id: Long, info: Option[CharacterKnowledgeInfo])
@ -30,25 +30,22 @@ object CharacterKnowledgeMessage extends Marshallable[CharacterKnowledgeMessage]
def apply(char_id: Long, info: CharacterKnowledgeInfo): CharacterKnowledgeMessage =
CharacterKnowledgeMessage(char_id, Some(info))
private val inverter: Codec[Boolean] = bool.xmap[Boolean](
state => !state,
state => !state
)
private val inverter: Codec[Boolean] = bool.xmap[Boolean](state => !state, state => !state)
private val info_codec: Codec[CharacterKnowledgeInfo] = (
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) ::
("permissions" | ulongL(bits = 46)) ::
("certifications" | ulongL(bits = 46)) ::
("unk1" | uint(bits = 6)) ::
("unk2" | uint(bits = 3)) ::
("unk3" | PlanetSideGUID.codec)
("zone" | uint16L)
).xmap[CharacterKnowledgeInfo](
{
case name :: permissions :: u1 :: u2 :: u3 :: HNil =>
CharacterKnowledgeInfo(name, Certification.fromEncodedLong(permissions), u1, u2, u3)
case name :: certs :: u1 :: u2 :: zone :: HNil =>
CharacterKnowledgeInfo(name, Certification.fromEncodedLong(certs), u1, u2, zone)
},
{
case CharacterKnowledgeInfo(name, permissions, u1, u2, u3) =>
name :: Certification.toEncodedLong(permissions) :: u1 :: u2 :: u3 :: HNil
case CharacterKnowledgeInfo(name, certs, u1, u2, zone) =>
name :: Certification.toEncodedLong(certs) :: u1 :: u2 :: zone :: HNil
}
)

View file

@ -65,8 +65,8 @@ final case class SquadInfo(
this And SquadInfo(None, None, Some(zone), None, None, None)
def ZoneId(zone: Option[PlanetSideZoneID]): SquadInfo =
zone match {
case Some(zoneId) => this And SquadInfo(None, None, zone, None, None, None)
case None => SquadInfo(leader, task, zone, size, capacity, squad_guid)
case Some(_) => this And SquadInfo(None, None, zone, None, None, None)
case None => SquadInfo(leader, task, zone, size, capacity, squad_guid)
}
def Size(sz: Int): SquadInfo =
this And SquadInfo(None, None, None, Some(sz), None, None)
@ -612,8 +612,8 @@ object SquadHeader {
(bool >>:~ { unk1 =>
uint8 >>:~ { unk2 =>
conditional(!unk1 && unk2 == 1, removeCodec) ::
conditional(unk1 && unk2 == 6, providedCodec) ::
conditional(unk1 && unk2 != 6, listing_codec(unk2))
conditional(unk1 && unk2 == 6, providedCodec) ::
conditional(unk1 && unk2 != 6, listing_codec(unk2))
}
}).exmap[Option[SquadInfo]](
{
@ -691,10 +691,8 @@ object SquadListing {
private def meta_codec(entryFunc: Int => Codec[Option[SquadInfo]]): Codec[SquadListing] =
(("index" | uint8L) >>:~ { index =>
conditional(index < 255, "listing" | entryFunc(index)) ::
conditional(
index == 255,
bits
) //consume n < 8 bits after the tail entry, else vector will try to operate on invalid data
conditional(index == 255, bits)
//consume n < 8 bits after the tail entry, else vector will try to operate on invalid data
}).xmap[SquadListing](
{
case ndx :: Some(lstng) :: _ :: HNil =>
@ -717,7 +715,7 @@ object SquadListing {
* `Codec` for branching types of `SquadListing` initializations.
*/
val info_codec: Codec[SquadListing] = meta_codec({ index: Int =>
newcodecs.binary_choice(index == 0, "listing" | SquadHeader.info_codec, "listing" | SquadHeader.alt_info_codec)
newcodecs.binary_choice(index == 0, SquadHeader.info_codec, SquadHeader.alt_info_codec)
})
}
@ -738,14 +736,15 @@ object ReplicationStreamMessage extends Marshallable[ReplicationStreamMessage] {
)
}
implicit val codec: Codec[ReplicationStreamMessage] = (("behavior" | uintL(3)) >>:~ { behavior =>
conditional(behavior == 5, "behavior2" | uintL(3)) ::
implicit val codec: Codec[ReplicationStreamMessage] = (
("behavior" | uintL(bits = 3)) >>:~ { behavior =>
("behavior2" | conditional(behavior == 5, uintL(bits = 3))) ::
conditional(behavior != 1, bool) ::
newcodecs.binary_choice(
("entries" | newcodecs.binary_choice(
behavior != 5,
"entries" | vector(SquadListing.codec),
"entries" | vector(SquadListing.info_codec)
)
vector(SquadListing.codec),
vector(SquadListing.info_codec)
))
}).xmap[ReplicationStreamMessage](
{
case bhvr :: bhvr2 :: _ :: lst :: HNil =>

View file

@ -3,7 +3,7 @@ package net.psforever.packet.game
import net.psforever.objects.avatar.Certification
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideGUID
import net.psforever.types.{PlanetSideGUID, SquadListDecoration}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
@ -25,71 +25,73 @@ object SquadAction {
implicit val codec: Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
}
final case class DisplaySquad() extends SquadAction(0)
final case class DisplaySquad() extends SquadAction(code = 0)
/**
* Dispatched from client to server to indicate a squad detail update that has no foundation entry to update?
* Not dissimilar from `DisplaySquad`.
*/
final case class SquadMemberInitializationIssue() extends SquadAction(1)
final case class SquadInitializationIssue() extends SquadAction(code = 1)
final case class SaveSquadFavorite() extends SquadAction(3)
final case class SaveSquadFavorite() extends SquadAction(code = 3)
final case class LoadSquadFavorite() extends SquadAction(4)
final case class LoadSquadFavorite() extends SquadAction(code = 4)
final case class DeleteSquadFavorite() extends SquadAction(5)
final case class DeleteSquadFavorite() extends SquadAction(code = 5)
final case class ListSquadFavorite(name: String) extends SquadAction(7)
final case class ListSquadFavorite(name: String) extends SquadAction(code = 7)
final case class RequestListSquad() extends SquadAction(8)
final case class RequestListSquad() extends SquadAction(code = 8)
final case class StopListSquad() extends SquadAction(9)
final case class StopListSquad() extends SquadAction(code = 9)
final case class SelectRoleForYourself(state: Int) extends SquadAction(10)
final case class SelectRoleForYourself(state: Int) extends SquadAction(code = 10)
final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15)
final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(code = 15)
final case class AssociateWithSquad() extends SquadAction(16)
final case class IdentifyAsSquadLeader() extends SquadAction(code = 16)
final case class SetListSquad() extends SquadAction(17)
final case class SetListSquad() extends SquadAction(code = 17)
final case class ChangeSquadPurpose(purpose: String) extends SquadAction(19)
final case class ChangeSquadPurpose(purpose: String) extends SquadAction(code = 19)
final case class ChangeSquadZone(zone: PlanetSideZoneID) extends SquadAction(20)
final case class ChangeSquadZone(zone: PlanetSideZoneID) extends SquadAction(code = 20)
final case class CloseSquadMemberPosition(position: Int) extends SquadAction(21)
final case class CloseSquadMemberPosition(position: Int) extends SquadAction(code = 21)
final case class AddSquadMemberPosition(position: Int) extends SquadAction(22)
final case class AddSquadMemberPosition(position: Int) extends SquadAction(code = 22)
final case class ChangeSquadMemberRequirementsRole(u1: Int, role: String) extends SquadAction(23)
final case class ChangeSquadMemberRequirementsRole(u1: Int, role: String) extends SquadAction(code = 23)
final case class ChangeSquadMemberRequirementsDetailedOrders(u1: Int, orders: String) extends SquadAction(24)
final case class ChangeSquadMemberRequirementsDetailedOrders(u1: Int, orders: String) extends SquadAction(code = 24)
final case class ChangeSquadMemberRequirementsCertifications(u1: Int, certs: Set[Certification])
extends SquadAction(25)
extends SquadAction(code = 25)
final case class ResetAll() extends SquadAction(26)
final case class ResetAll() extends SquadAction(code = 26)
final case class AutoApproveInvitationRequests(state: Boolean) extends SquadAction(28)
final case class AutoApproveInvitationRequests(state: Boolean) extends SquadAction(code = 28)
final case class LocationFollowsSquadLead(state: Boolean) extends SquadAction(31)
final case class LocationFollowsSquadLead(state: Boolean) extends SquadAction(code = 31)
final case class SquadListDecorator(state: SquadListDecoration.Value) extends SquadAction(code = 33)
final case class SearchForSquadsWithParticularRole(
role: String,
requirements: Set[Certification],
zone_id: Int,
mode: SearchMode.Value
) extends SquadAction(34)
) extends SquadAction(code = 34)
final case class CancelSquadSearch() extends SquadAction(35)
final case class CancelSquadSearch() extends SquadAction(code = 35)
final case class AssignSquadMemberToRole(position: Int, char_id: Long) extends SquadAction(38)
final case class AssignSquadMemberToRole(position: Int, char_id: Long) extends SquadAction(code = 38)
final case class NoSquadSearchResults() extends SquadAction(39)
final case class NoSquadSearchResults() extends SquadAction(code = 39)
final case class FindLfsSoldiersForRole(state: Int) extends SquadAction(40)
final case class FindLfsSoldiersForRole(state: Int) extends SquadAction(code = 40)
final case class CancelFind() extends SquadAction(41)
final case class CancelFind() extends SquadAction(code = 41)
final case class Unknown(badCode: Int, data: BitVector) extends SquadAction(badCode)
@ -114,10 +116,10 @@ object SquadAction {
}
)
val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadMemberInitializationIssue](
_ => SquadMemberInitializationIssue(),
val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadInitializationIssue](
_ => SquadInitializationIssue(),
{
case SquadMemberInitializationIssue() => None
case SquadInitializationIssue() => None
}
)
@ -179,10 +181,10 @@ object SquadAction {
}
)
val associateWithSquadCodec = everFailCondition.xmap[AssociateWithSquad](
_ => AssociateWithSquad(),
val identifyAsSquadLeaderCodec = everFailCondition.xmap[IdentifyAsSquadLeader](
_ => IdentifyAsSquadLeader(),
{
case AssociateWithSquad() => None
case IdentifyAsSquadLeader() => None
}
)
@ -276,6 +278,18 @@ object SquadAction {
}
)
val squadListDecoratorCodec = (
SquadListDecoration.codec ::
ignore(size = 3)
).xmap[SquadListDecorator](
{
case value :: _ :: HNil => SquadListDecorator(value)
},
{
case SquadListDecorator(value) => value :: () :: HNil
}
)
val searchForSquadsWithParticularRoleCodec = (PacketHelpers.encodedWideStringAligned(6) ::
ulongL(46) ::
uint16L ::
@ -389,7 +403,7 @@ object SquadAction {
* &nbsp;&nbsp;&nbsp;&nbsp;`20` - (Squad leader) Change Squad Zone<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`21` - (Squad leader) Close Squad Member Position<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`22` - (Squad leader) Add Squad Member Position<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`33` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`33` - Decorate a Squad in the List of Squads with Color<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`40` - Find LFS Soldiers that Meet the Requirements for this Role<br>
* &nbsp;&nbsp;`Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`13` - UNKNOWN<br>
@ -451,7 +465,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
case 13 => unknownCodec(action = 13)
case 14 => unknownCodec(action = 14)
case 15 => cancelSelectRoleForYourselfCodec
case 16 => associateWithSquadCodec
case 16 => identifyAsSquadLeaderCodec
case 17 => setListSquadCodec
case 18 => unknownCodec(action = 18)
case 19 => changeSquadPurposeCodec
@ -468,7 +482,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
case 30 => unknownCodec(action = 30)
case 31 => locationFollowsSquadLeadCodec
case 32 => unknownCodec(action = 32)
case 33 => unknownCodec(action = 33)
case 33 => squadListDecoratorCodec
case 34 => searchForSquadsWithParticularRoleCodec
case 35 => cancelSquadSearchCodec
case 36 => unknownCodec(action = 36)

View file

@ -143,12 +143,12 @@ final case class SquadPositionEntry(index: Int, info: Option[SquadPositionDetail
* All fields are optional for that reason.<br>
* <br>
* The squad leader does not necessarily have to be a person from the `member_info` list.
* @param unk1 na;
* @param guid na;
* must be non-zero when parsed in a FullSquad pattern
* @param unk2 na;
* not associated with any fields during itemized parsing
* @param leader_char_id he unique character identification number for the squad leader
* @param unk3 na
* @param outfit_id na
* @param leader_name the name of the player who is the squad leader
* @param task the suggested responsibilities or mission statement of the squad
* @param zone_id the suggested area of engagement for this squad's activities;
@ -157,10 +157,10 @@ final case class SquadPositionEntry(index: Int, info: Option[SquadPositionDetail
* @param member_info a list of squad position data
*/
final case class SquadDetail(
unk1: Option[Int],
guid: Option[Int],
unk2: Option[Int],
leader_char_id: Option[Long],
unk3: Option[Long],
outfit_id: Option[Long],
leader_name: Option[String],
task: Option[String],
zone_id: Option[PlanetSideZoneID],
@ -176,10 +176,10 @@ final case class SquadDetail(
*/
def And(info: SquadDetail): SquadDetail = {
SquadDetail(
unk1.orElse(info.unk1),
guid.orElse(info.guid),
unk2.orElse(info.unk2),
leader_char_id.orElse(info.leader_char_id),
unk3.orElse(info.unk3),
outfit_id.orElse(info.outfit_id),
leader_name.orElse(info.leader_name),
task.orElse(info.task),
zone_id.orElse(info.zone_id),
@ -202,12 +202,14 @@ final case class SquadDetail(
}
//methods intended to combine the fields of itself and another object
def Field1(value: Int): SquadDetail =
this And SquadDetail(Some(value), None, None, None, None, None, None, None, None)
def Guid(guid: Int): SquadDetail =
this And SquadDetail(Some(guid), None, None, None, None, None, None, None, None)
def Field2(value: Int): SquadDetail =
this And SquadDetail(None, Some(value), None, None, None, None, None, None, None)
def LeaderCharId(char_id: Long): SquadDetail =
this And SquadDetail(None, None, Some(char_id), None, None, None, None, None, None)
def Field3(value: Long): SquadDetail =
this And SquadDetail(None, None, None, Some(value), None, None, None, None, None)
def OutfitId(outfit: Long): SquadDetail =
this And SquadDetail(None, None, None, Some(outfit), None, None, None, None, None)
def LeaderName(name: String): SquadDetail =
this And SquadDetail(None, None, None, None, Some(name), None, None, None, None)
def Leader(char_id: Long, name: String): SquadDetail =
@ -228,14 +230,14 @@ final case class SquadDetail(
*/
def Complete: SquadDetail =
SquadDetail(
unk1.orElse(Some(1)),
guid.orElse(Some(1)),
unk2.orElse(Some(0)),
leader_char_id.orElse(Some(0L)),
unk3.orElse(Some(0L)),
outfit_id.orElse(Some(0L)),
leader_name.orElse(Some("")),
task.orElse(Some("")),
zone_id.orElse(Some(PlanetSideZoneID(0))),
unk7.orElse(Some(4983296)), //FullSquad value
unk7.orElse(Some(4983296)), //FullSquad value?
{
val complete = SquadPositionDetail().Complete
Some(member_info match {
@ -359,7 +361,7 @@ object SquadDetail {
unk1: Int,
unk2: Int,
leader_char_id: Long,
unk3: Long,
outfit_id: Long,
leader_name: String,
task: String,
zone_id: PlanetSideZoneID,
@ -370,7 +372,7 @@ object SquadDetail {
Some(unk1),
Some(unk2),
Some(leader_char_id),
Some(unk3),
Some(outfit_id),
Some(leader_name),
Some(task),
Some(zone_id),
@ -380,12 +382,12 @@ object SquadDetail {
}
//individual field overloaded constructors
def Field1(unk1: Int): SquadDetail =
SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)
def Guid(guid: Int): SquadDetail =
SquadDetail(Some(guid), None, None, None, None, None, None, None, None)
def LeaderCharId(char_id: Long): SquadDetail =
SquadDetail(None, None, Some(char_id), None, None, None, None, None, None)
def Field3(char_id: Option[Long], unk3: Long): SquadDetail =
SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)
def OutfitId(char_id: Option[Long], outfit_id: Long): SquadDetail =
SquadDetail(None, None, None, Some(outfit_id), None, None, None, None, None)
def LeaderName(name: String): SquadDetail =
SquadDetail(None, None, None, None, Some(name), None, None, None, None)
def Leader(char_id: Long, name: String): SquadDetail =
@ -400,9 +402,9 @@ object SquadDetail {
SquadDetail(None, None, None, None, None, None, None, None, Some(list))
object Fields {
final val Field1 = 1
final val Guid = 1
final val CharId = 2
final val Field3 = 3
final val Outfit = 3
final val Leader = 4
final val Task = 5
final val ZoneId = 6
@ -541,10 +543,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
val codec: Codec[SquadDetail] = {
import shapeless.::
(
("unk1" | uint8) ::
("unk2" | uint24) :: //unknown, but can be 0'd
("guid" | uint16L) ::
("unk2" | uint16L) :: //unknown, but can be 0'd
("leader_char_id" | uint32L) ::
("unk3" | uint32L) :: //variable fields, but can be 0'd
("outfit_id" | uint32L) :: //can be 0'd
("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
("task" | PacketHelpers.encodedWideString) ::
("zone_id" | PlanetSideZoneID.codec) ::
@ -552,13 +554,13 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
optional(bool, "member_info" | initial_member_codec)
).exmap[SquadDetail](
{
case u1 :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil =>
case guid :: u2 :: char_id :: outfit_id :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil =>
Attempt.Successful(
SquadDetail(
Some(u1),
Some(guid),
Some(u2),
Some(char_id),
Some(u3),
Some(outfit_id),
Some(leader),
Some(task),
Some(zone),
@ -573,10 +575,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
},
{
case SquadDetail(
Some(u1),
Some(guid),
Some(u2),
Some(char_id),
Some(u3),
Some(outfit_id),
Some(leader),
Some(task),
Some(zone),
@ -584,7 +586,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
Some(member_list)
) =>
Attempt.Successful(
math.max(u1, 1) :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 ::
math.max(guid, 1) :: u2 :: char_id :: outfit_id :: leader :: task :: zone :: unk7 ::
Some(linkFields(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
HNil
)
@ -605,15 +607,15 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
object ItemizedSquad {
/**
* A pattern for data related to "field1."
* A pattern for data related to the `guid` field.
*/
private val field1Codec: Codec[SquadDetail] = uint16L.exmap[SquadDetail](
unk1 => Attempt.successful(SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)),
private val guidCodec: Codec[SquadDetail] = uint16L.exmap[SquadDetail](
guid => Attempt.successful(SquadDetail(Some(guid), None, None, None, None, None, None, None, None)),
{
case SquadDetail(Some(unk1), _, _, _, _, _, _, _, _) =>
Attempt.successful(unk1)
case SquadDetail(Some(guid), _, _, _, _, _, _, _, _) =>
Attempt.successful(guid)
case _ =>
Attempt.failure(Err("failed to encode squad data for unknown field #1"))
Attempt.failure(Err("failed to encode squad data for the guid"))
}
)
@ -631,13 +633,13 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
)
/**
* A pattern for data related to "field3."
* A pattern for data related to the "outfit id" field.
*/
private val field3Codec: Codec[SquadDetail] = uint32L.exmap[SquadDetail](
unk3 => Attempt.successful(SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)),
private val outfitCodec: Codec[SquadDetail] = uint32L.exmap[SquadDetail](
outfit_id => Attempt.successful(SquadDetail(None, None, None, Some(outfit_id), None, None, None, None, None)),
{
case SquadDetail(_, _, _, Some(unk3), _, _, _, _, _) =>
Attempt.successful(unk3)
case SquadDetail(_, _, _, Some(outfit_id), _, _, _, _, _) =>
Attempt.successful(outfit_id)
case _ =>
Attempt.failure(Err("failed to encode squad data for unknown field #3"))
}
@ -807,9 +809,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
*/
private def selectCodedAction(code: Int, bitsOverByte: StreamLengthToken): Codec[SquadDetail] = {
code match {
case 1 => field1Codec
case 1 => guidCodec
case 2 => leaderCharIdCodec
case 3 => field3Codec
case 3 => outfitCodec
case 4 => leaderNameCodec(bitsOverByte)
case 5 => taskCodec(bitsOverByte)
case 6 => zoneCodec
@ -820,7 +822,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
}
/**
* Advance information about the current stream length because on which pattern was previously utilized.
* Advance information about the current stream length based on which pattern was previously utilized.
* @see `selectCodedAction(Int, StreamLengthToken)`
* @param code the action code, connecting to a field pattern
* @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
@ -834,7 +836,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 5 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 6 => bitsOverByte //32u = no added padding
case 7 => bitsOverByte.Add(4) //additional 4u
case 7 => bitsOverByte.Add(more = 4) //additional 4u
case 8 => bitsOverByte.Length = 0 //end of stream
case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
}
@ -886,9 +888,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
(6, SquadDetail(None, None, None, None, None, None, info.zone_id, None, None)),
(5, SquadDetail(None, None, None, None, None, info.task, None, None, None)),
(4, SquadDetail(None, None, None, None, info.leader_name, None, None, None, None)),
(3, SquadDetail(None, None, None, info.unk3, None, None, None, None, None)),
(3, SquadDetail(None, None, None, info.outfit_id, None, None, None, None, None)),
(2, SquadDetail(None, None, info.leader_char_id, None, None, None, None, None, None)),
(1, SquadDetail(info.unk1, None, None, None, None, None, None, None, None))
(1, SquadDetail(info.guid, None, None, None, None, None, None, None, None))
) //in reverse order so that the linked list is in the correct order
.filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank } match {
case Nil =>
@ -1102,12 +1104,12 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
*/
private def modifyCodedPadValue(code: Int, bitsOverByte: StreamLengthToken): StreamLengthToken = {
code match {
case 0 => bitsOverByte.Add(1) //additional 1u
case 0 => bitsOverByte.Add(1) //additional 1u
case 1 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 2 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 3 => bitsOverByte //32u = no added padding
case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u
case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u
case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
}
}
@ -1587,10 +1589,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
{
case SquadDetailDefinitionUpdateMessage(guid, info) =>
val occupiedSquadFieldCount = List(
info.unk1,
info.guid,
info.unk2,
info.leader_char_id,
info.unk3,
info.outfit_id,
info.leader_name,
info.task,
info.zone_id,

View file

@ -9,7 +9,7 @@ import shapeless.{::, HNil}
object MemberEvent extends Enumeration {
type Type = Value
val Add, Remove, Promote, UpdateZone, Unknown4 = Value
val Add, Remove, Promote, UpdateZone, Outfit = Value
implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
}
@ -21,7 +21,7 @@ final case class SquadMemberEvent(
position: Int,
player_name: Option[String],
zone_number: Option[Int],
unk7: Option[Long]
outfit_id: Option[Long]
) extends PlanetSideGamePacket {
type Packet = SquadMemberEvent
def opcode = GamePacketOpcode.SquadMemberEvent
@ -38,9 +38,9 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
position: Int,
player_name: String,
zone_number: Int,
unk7: Long
outfit_id: Long
): SquadMemberEvent =
SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(unk7))
SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(outfit_id))
def Remove(unk2: Int, char_id: Long, position: Int): SquadMemberEvent =
SquadMemberEvent(MemberEvent.Remove, unk2, char_id, position, None, None, None)
@ -51,20 +51,20 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
def UpdateZone(unk2: Int, char_id: Long, position: Int, zone_number: Int): SquadMemberEvent =
SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, position, None, Some(zone_number), None)
def Unknown4(unk2: Int, char_id: Long, position: Int, unk7: Long): SquadMemberEvent =
SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, position, None, None, Some(unk7))
def Outfit(unk2: Int, char_id: Long, position: Int, outfit_id: Long): SquadMemberEvent =
SquadMemberEvent(MemberEvent.Outfit, unk2, char_id, position, None, None, Some(outfit_id))
implicit val codec: Codec[SquadMemberEvent] = (("action" | MemberEvent.codec) >>:~ { action =>
("unk2" | uint16L) ::
("char_id" | uint32L) ::
("position" | uint4) ::
conditional(action == MemberEvent.Add, "player_name" | PacketHelpers.encodedWideStringAligned(1)) ::
conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, "zone_number" | uint16L) ::
conditional(action == MemberEvent.Add || action == MemberEvent.Unknown4, "unk7" | uint32L)
("player_name" | conditional(action == MemberEvent.Add, PacketHelpers.encodedWideStringAligned(adjustment = 1))) ::
("zone_number" | conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, uint16L)) ::
("outfit_id" | conditional(action == MemberEvent.Add || action == MemberEvent.Outfit, uint32L))
}).exmap[SquadMemberEvent](
{
case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil =>
Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, unk7))
case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: outfit_id :: HNil =>
Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, outfit_id))
},
{
case SquadMemberEvent(
@ -74,20 +74,20 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
member_position,
Some(player_name),
Some(zone_number),
Some(unk7)
Some(outfit_id)
) =>
Attempt.Successful(
MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(
unk7
outfit_id
) :: HNil
)
case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) =>
Attempt.Successful(
MemberEvent.UpdateZone :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil
)
case SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, member_position, None, None, Some(unk7)) =>
case SquadMemberEvent(MemberEvent.Outfit, unk2, char_id, member_position, None, None, Some(outfit_id)) =>
Attempt.Successful(
MemberEvent.Unknown4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil
MemberEvent.Outfit :: unk2 :: char_id :: member_position :: None :: None :: Some(outfit_id) :: HNil
)
case SquadMemberEvent(action, unk2, char_id, member_position, None, None, None) =>
Attempt.Successful(action :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil)

View file

@ -26,7 +26,7 @@ import scodec.codecs._
* - `Invite` (0)<br>
* false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]<br>
* true => "You have invited `player_name` to join your squad."<br>
* - `Unk01` (1)<br>
* - `ProximityInvite` (1)<br>
* false => n/a<br>
* true => n/a<br>
* - `Accept` (2)<br>

View file

@ -58,14 +58,16 @@ final case class SquadState(guid: PlanetSideGUID, info_list: List[SquadStateInfo
}
object SquadStateInfo {
def apply(unk1: Long, unk2: Int, unk3: Int, pos: Vector3, unk4: Int, unk5: Int, unk6: Boolean, unk7: Int)
: SquadStateInfo =
SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, None, None)
def apply(charId: Long, health: Int, armor: Int, pos: Vector3): SquadStateInfo =
SquadStateInfo(charId, health, armor, pos, 2, 2, unk6=false, 429, None, None)
def apply(charId: Long, health: Int, armor: Int, pos: Vector3, unk4: Int, unk5: Int, unk6: Boolean, unk7: Int): SquadStateInfo =
SquadStateInfo(charId, health, armor, pos, unk4, unk5, unk6, unk7, None, None)
def apply(
unk1: Long,
unk2: Int,
unk3: Int,
charId: Long,
health: Int,
armor: Int,
pos: Vector3,
unk4: Int,
unk5: Int,
@ -74,22 +76,22 @@ object SquadStateInfo {
unk8: Int,
unk9: Boolean
): SquadStateInfo =
SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, Some(unk8), Some(unk9))
SquadStateInfo(charId, health, armor, pos, unk4, unk5, unk6, unk7, Some(unk8), Some(unk9))
}
object SquadState extends Marshallable[SquadState] {
private val info_codec: Codec[SquadStateInfo] = (
("char_id" | uint32L) ::
("health" | uint(7)) ::
("armor" | uint(7)) ::
("health" | uint(bits = 7)) ::
("armor" | uint(bits = 7)) ::
("pos" | Vector3.codec_pos) ::
("unk4" | uint2) ::
("unk5" | uint2) ::
("unk6" | bool) ::
("unk7" | uint16L) ::
(bool >>:~ { out =>
conditional(out, "unk8" | uint16L) ::
conditional(out, "unk9" | bool)
("unk8" | conditional(out, uint16L)) ::
("unk9" | conditional(out, bool))
})
).exmap[SquadStateInfo](
{

View file

@ -68,7 +68,7 @@ final case class CharacterAppearanceA(
* @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line
*/
final case class CharacterAppearanceB(
unk0: Long,
outfit_id: Long,
outfit_name: String,
outfit_logo: Int,
unk1: Boolean,
@ -394,7 +394,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
*/
def b_codec(alt_model: Boolean, name_padding: Int): Codec[CharacterAppearanceB] =
(
("unk0" | uint32L) :: //for outfit_name (below) to be visible in-game, this value should be non-zero
("outfit_id" | uint32L) :: //for outfit_name (below) to be visible in-game, this value should be non-zero
("outfit_name" | PacketHelpers.encodedWideStringAligned(outfitNamePadding)) ::
("outfit_logo" | uint8L) ::
("unk1" | bool) :: //unknown
@ -414,12 +414,12 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
optional(bool, "on_zipline" | zipline_codec)
).exmap[CharacterAppearanceB](
{
case u0 :: outfit :: logo :: u1 :: bpack :: u2 :: u3 :: u4 :: facingPitch :: facingYawUpper :: lfs :: gstate :: cloaking :: u5 :: u6 :: charging :: u7 :: zipline :: HNil =>
case outfit_id :: outfit :: logo :: u1 :: bpack :: u2 :: u3 :: u4 :: facingPitch :: facingYawUpper :: lfs :: gstate :: cloaking :: u5 :: u6 :: charging :: u7 :: zipline :: HNil =>
val lfsBool = if (lfs == 0) false else true
val bpackBool = bpack match { case Some(_) => alt_model; case None => false }
Attempt.successful(
CharacterAppearanceB(
u0,
outfit_id,
outfit,
logo,
u1,
@ -442,7 +442,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
},
{
case CharacterAppearanceB(
u0,
outfit_id,
outfit,
logo,
u1,
@ -461,10 +461,10 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
u7,
zipline
) =>
val u0Long = if (u0 == 0 && outfit.nonEmpty) {
val u0Long = if (outfit_id == 0 && outfit.nonEmpty) {
outfit.length.toLong
} else {
u0
outfit_id
} //TODO this is a kludge; unk0 must be (some) non-zero if outfit_name is defined
val (bpackOpt, zipOpt) = if (alt_model) {
val bpackOpt = if (bpack) { Some(true) }

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
package net.psforever.services.teamwork
import net.psforever.objects.Player
import net.psforever.objects.avatar.Certification
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{WaypointEventAction, WaypointInfo, SquadAction => PacketSquadAction}
import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoint, Vector3}
@ -16,8 +17,9 @@ object SquadServiceMessage {
object SquadAction {
sealed trait Action
final case class InitSquadList() extends Action
final case class InitCharId() extends Action
final case class InitSquadList() extends Action
final case class InitCharId() extends Action
final case class ReloadDecoration() extends Action
final case class Definition(guid: PlanetSideGUID, line: Int, action: PacketSquadAction) extends Action
final case class Membership(
@ -35,10 +37,12 @@ object SquadAction {
) extends Action
final case class Update(
char_id: Long,
guid: PlanetSideGUID,
health: Int,
max_health: Int,
armor: Int,
max_armor: Int,
certifications: Set[Certification],
pos: Vector3,
zone_number: Int
) extends Action

View file

@ -1,6 +1,8 @@
// Copyright (c) 2019 PSForever
package net.psforever.services.teamwork
import akka.actor.ActorRef
import net.psforever.objects.avatar.Certification
import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo}
import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoint}
@ -26,7 +28,7 @@ object SquadResponse {
final case class UpdateList(infos: Iterable[(Int, SquadInfo)]) extends Response
final case class RemoveFromList(infos: Iterable[Int]) extends Response
final case class AssociateWithSquad(squad_guid: PlanetSideGUID) extends Response
final case class IdentifyAsSquadLeader(squad_guid: PlanetSideGUID) extends Response
final case class SetListSquad(squad_guid: PlanetSideGUID) extends Response
final case class Membership(
@ -40,11 +42,11 @@ object SquadResponse {
unk6: Option[Option[String]]
) extends Response //see SquadMembershipResponse
final case class WantsSquadPosition(leader_char_id: Long, bid_name: String) extends Response
final case class Join(squad: Squad, positionsToUpdate: List[Int], channel: String) extends Response
final case class Join(squad: Squad, positionsToUpdate: List[Int], channel: String, ref: ActorRef) extends Response
final case class Leave(squad: Squad, positionsToUpdate: List[(Long, Int)]) extends Response
final case class UpdateMembers(squad: Squad, update_info: List[SquadAction.Update]) extends Response
final case class AssignMember(squad: Squad, from_index: Int, to_index: Int) extends Response
final case class PromoteMember(squad: Squad, char_id: Long, from_index: Int, to_index: Int) extends Response
final case class PromoteMember(squad: Squad, char_id: Long, from_index: Int) extends Response
final case class Detail(guid: PlanetSideGUID, squad_detail: SquadDetail) extends Response
@ -59,5 +61,16 @@ object SquadResponse {
unk: Int
) extends Response
final case class SquadSearchResults() extends Response
final case class SquadDecoration(guid: PlanetSideGUID, squad: Squad) extends Response
final case class SquadSearchResults(results: List[PlanetSideGUID]) extends Response
final case class CharacterKnowledge(
id: Long,
name: String,
certs: Set[Certification],
unk1: Int,
unk2: Int,
zoneNumber: Int
) extends Response
}

View file

@ -0,0 +1,316 @@
// Copyright (c) 2022 PSForever
package net.psforever.services.teamwork
import akka.actor.ActorRef
import scala.collection.mutable
import net.psforever.objects.teamwork.{Squad, SquadFeatures}
import net.psforever.packet.game.SquadDetail
import net.psforever.services.GenericEventBus
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
class SquadSubscriptionEntity {
private[this] val log = org.log4s.getLogger(name="SquadService")
/**
* This is a formal `ActorEventBus` object that is reserved for faction-wide messages and squad-specific messages.
* When the user joins the `SquadService` with a `Service.Join` message
* that includes a confirmed faction affiliation identifier,
* the origin `ActorRef` is added as a subscription.
* Squad channels are produced when a squad is created,
* and are subscribed to as users join the squad,
* and unsubscribed from as users leave the squad.<br>
* key - a `PlanetSideEmpire` value; value - `ActorRef` reference<br>
* key - a consistent squad channel name; value - `ActorRef` reference
* @see `CloseSquad`
* @see `JoinSquad`
* @see `LeaveSquad`
* @see `Service.Join`
* @see `Service.Leave`
*/
val SquadEvents = new GenericEventBus[SquadServiceResponse]
/**
* This collection contains the message-sending contact reference for individuals.
* When the user joins the `SquadService` with a `Service.Join` message
* that includes their unique character identifier,
* the origin `ActorRef` is added as a subscription.
* It is maintained until they disconnect entirely.
* The subscription is anticipated to belong to an instance of `SessionActor`.<br>
* key - unique character identifier number; value - `ActorRef` reference for that character
* @see `Service.Join`
*/
val UserEvents: mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]()
/**
* Players who are interested in updated details regarding a certain squad
* though they may not be a member of the squad.<br>
* key - unique character identifier number; value - a squad identifier number
*/
val MonitorSquadDetails: mutable.LongMap[SquadSubscriptionEntity.MonitorEntry] = mutable.LongMap[SquadSubscriptionEntity.MonitorEntry]()
def postStop(): Unit = {
MonitorSquadDetails.clear()
UserEvents.foreach {
case (_, actor) =>
SquadEvents.unsubscribe(actor)
}
UserEvents.clear()
}
/**
* Overloaded message-sending operation.
* The `Actor` version wraps around the expected `!` functionality.
* @param to an `ActorRef` which to send the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
*/
def Publish(to: ActorRef, msg: SquadResponse.Response): Unit = {
Publish(to, msg, Nil)
}
/**
* Overloaded message-sending operation.
* The `Actor` version wraps around the expected `!` functionality.
* @param to an `ActorRef` which to send the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
* @param excluded a group of character identifier numbers who should not receive the message
* (resolved at destination)
*/
def Publish(to: ActorRef, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
to ! SquadServiceResponse("", excluded, msg)
}
/**
* Overloaded message-sending operation.
* Always publishes on the `SquadEvents` object.
* @param to a faction affiliation used as the channel for the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
*/
def Publish(to: PlanetSideEmpire.Type, msg: SquadResponse.Response): Unit = {
Publish(to, msg, Nil)
}
/**
* Overloaded message-sending operation.
* Always publishes on the `SquadEvents` object.
* @param to a faction affiliation used as the channel for the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
* @param excluded a group of character identifier numbers who should not receive the message
* (resolved at destination)
*/
def Publish(to: PlanetSideEmpire.Type, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
SquadEvents.publish(SquadServiceResponse(s"/$to/Squad", excluded, msg))
}
/**
* Overloaded message-sending operation.
* Strings come in three accepted patterns.
* The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string.
* The second resolves into a squad's dedicated channel, a name that is formulaic.
* The third resolves as a unique character identifier number.
* @param to a string used as the channel for the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
*/
def Publish(to: String, msg: SquadResponse.Response): Unit = {
Publish(to, msg, Nil)
}
/**
* Overloaded message-sending operation.
* Strings come in three accepted patterns.
* The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string.
* The second resolves into a squad's dedicated channel, a name that is formulaic.
* The third resolves as a unique character identifier number.
* @param to a string used as the channel for the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
* @param excluded a group of character identifier numbers who should not receive the message
* (resolved at destination, usually)
*/
def Publish(to: String, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
to match {
case str if "TRNCVS".indexOf(str) > -1 || str.matches("(TR|NC|VS)-Squad\\d+") =>
SquadEvents.publish(SquadServiceResponse(s"/$str/Squad", excluded, msg))
case str if str.matches("\\d+") =>
Publish(to.toLong, msg, excluded)
case _ =>
log.warn(s"Publish(String): subscriber information is an unhandled format - $to")
}
}
/**
* Overloaded message-sending operation.
* Always publishes on the `ActorRef` objects retained by the `UserEvents` object.
* @param to a unique character identifier used as the channel for the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
*/
def Publish(to: Long, msg: SquadResponse.Response): Unit = {
UserEvents.get(to) match {
case Some(user) =>
user ! SquadServiceResponse("", msg)
case None =>
log.warn(s"Publish(Long): subscriber information can not be found - $to")
}
}
/**
* Overloaded message-sending operation.
* Always publishes on the `ActorRef` objects retained by the `UserEvents` object.
* @param to a unique character identifier used as the channel for the message
* @param msg a message that can be stored in a `SquadServiceResponse` object
* @param excluded a group of character identifier numbers who should not receive the message
*/
def Publish(to: Long, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
if (!excluded.exists(_ == to)) {
Publish(to, msg)
}
}
/**
* Overloaded message-sending operation.
* No message can be sent using this distinction.
* Log a warning.
* @param to something that was expected to be used as the channel for the message
* but is not handled as such
* @param msg a message that can be stored in a `SquadServiceResponse` object
*/
def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response): Unit = {
log.warn(s"Publish(Any): subscriber information is an unhandled format - $to")
}
/**
* Overloaded message-sending operation.
* No message can be sent using this distinction.
* Log a warning.
* @param to something that was expected to be used as the channel for the message
* but is not handled as such
* @param msg a message that can be stored in a `SquadServiceResponse` object
* @param excluded a group of character identifier numbers who should not receive the message
*/
def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
log.warn(s"Publish(Any): subscriber information is an unhandled format - $to")
}
/* The following functions are related to common communications of squad information, mainly detail. */
/**
* Dispatch a message entailing the composition of this squad.
* This is considered the first time this information will be dispatched to any relevant observers
* so the details of the squad will be updated in full and be sent to all relevant observers,
* namely, all the occupants of the squad.
* External observers are ignored.
* @see `InitSquadDetail(PlanetSideGUID, Iterable[Long], Squad)`
* @param features the squad
*/
def InitSquadDetail(features: SquadFeatures): Unit = {
val squad = features.Squad
InitSquadDetail(
squad.GUID,
squad.Membership.collect { case member if member.CharId > 0 => member.CharId },
squad
)
}
/**
* Dispatch an intial message entailing the strategic information and the composition of this squad.
* The details of the squad will be updated in full and be sent to all indicated observers.
* @see `SquadService.PublishFullDetails`
* @param guid the unique squad identifier to be used when composing the details for this message
* @param to the unique character identifier numbers of the players who will receive this message
* @param squad the squad from which the squad details shall be composed
*/
def InitSquadDetail(guid: PlanetSideGUID, to: Iterable[Long], squad: Squad): Unit = {
val output = SquadResponse.Detail(guid, SquadService.PublishFullDetails(squad))
to.foreach { Publish(_, output) }
}
/**
* Send a message entailing the strategic information and the composition of the squad to the existing members of the squad.
* @see `SquadService.PublishFullDetails`
* @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)`
* @param features the squad
*/
def UpdateSquadDetail(features: SquadFeatures): Unit = {
val squad = features.Squad
UpdateSquadDetail(
squad.GUID,
features.ToChannel,
Nil,
SquadService.PublishFullDetails(squad)
)
}
/**
* Send a message entailing some of the strategic information and the composition to the existing members of the squad.
* @see `SquadResponse.Detail`
* @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)`
* @param features information about the squad
* @param details the squad details to be included in the message
*/
def UpdateSquadDetail(features: SquadFeatures, details: SquadDetail): Unit = {
UpdateSquadDetail(
features.Squad.GUID,
features.ToChannel,
Nil,
details
)
}
/**
* Send a message entailing some of the strategic information and the composition to the existing members of the squad.
* Also send the same information to any users who are watching the squad, potentially for want to join it.
* The squad-specific message is contingent on finding the squad's features using the unique identifier number
* and, from that, reporting to the specific squad's messaging channel.
* Anyone watching the squad will always be updated the given details.
* @see `DisplaySquad`
* @see `Publish`
* @see `SquadDetail`
* @see `SquadResponse.Detail`
* @param guid the unique squad identifier number to be used for the squad detail message
* @param toChannel the squad broadcast channel name
* @param excluding the explicit unique character identifier numbers of individuals who should not receive the message
* @param details the squad details to be included in the message
*/
def UpdateSquadDetail(
guid: PlanetSideGUID,
toChannel: String,
excluding: Iterable[Long],
details: SquadDetail
): Unit = {
val output = SquadResponse.Detail(guid, details)
Publish(toChannel, output, excluding)
PublishToMonitorTargets(guid, excluding).foreach { charId => Publish(charId, output, Nil) }
}
/**
* na
* @see `LongMap.subtractOne`
* @see `SquadSubscriptionEntity.MonitorEntry`
* @param guid the unique squad identifier number to be used for the squad detail message
* @param excluding the explicit unique character identifier numbers of individuals who should not receive the message
*/
def PublishToMonitorTargets(
guid: PlanetSideGUID,
excluding: Iterable[Long]
): Iterable[Long] = {
val curr = System.currentTimeMillis()
MonitorSquadDetails
.toSeq
.collect {
case out @ (charId: Long, entry: SquadSubscriptionEntity.MonitorEntry)
if entry.squadGuid == guid && !excluding.exists(_ == charId) =>
if (curr - entry.time < 300000L) {
Some(out._1)
} else {
MonitorSquadDetails.subtractOne(charId)
None
}
}
.flatten
}
}
object SquadSubscriptionEntity {
private[teamwork] case class MonitorEntry(squadGuid: PlanetSideGUID) {
val time: Long = System.currentTimeMillis()
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) 2022 PSForever
package net.psforever.types
import scodec.codecs.uint
object SquadListDecoration extends Enumeration {
type Type = Value
val NotAvailable = Value(0)
val Available = Value(1)
val CertQualified = Value(2)
val SearchResult = Value(3)
implicit val codec = uint(bits = 3).xmap[SquadListDecoration.Value](
{
value =>
if (value < 4) {
SquadListDecoration(value)
} else if (value < 7) {
SquadListDecoration.Available
} else {
SquadListDecoration.NotAvailable
}
},
_.id
)
}

View file

@ -4,9 +4,10 @@ package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
//unk01 is some sort of reply to ProximityInvite
object SquadResponseType extends Enumeration {
type Type = Value
val Invite, Unk01, Accept, Reject, Cancel, Leave, Disband, PlatoonInvite, PlatoonAccept, PlatoonReject, PlatoonCancel,
val Invite, ProximityInvite, Accept, Reject, Cancel, Leave, Disband, PlatoonInvite, PlatoonAccept, PlatoonReject, PlatoonCancel,
PlatoonLeave, PlatoonDisband = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)

View file

@ -37,7 +37,7 @@ sealed abstract class StandardWaypoint(override val value: Int) extends SquadWay
* is indicated in the game world, on the proximity map, and on the continental map,
* and is designated by the number of the squad member that produced it.
* Only one laze waypoint may be made visible from any one squad member at any given time, overwritten when replaced.
* When viewed by a squad member seated in a Flail, the waypoint includes an elevation reticle for aiming purposes.
* When viewed by a squad member seated in a Flail, the waypoint includes an elevation reticule for aiming purposes.
* YMMV.
* @see `SquadWaypointEvent`
* @see `SquadWaypointRequest`

View file

@ -33,7 +33,7 @@ class CharacterKnowledgeMessageTest extends Specification {
),
15,
0,
PlanetSideGUID(12)
12
)
case _ =>
ko
@ -61,7 +61,7 @@ class CharacterKnowledgeMessageTest extends Specification {
),
15,
0,
PlanetSideGUID(12)
12
)
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector

View file

@ -6,7 +6,7 @@ import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game.SquadAction._
import net.psforever.packet.game._
import net.psforever.types.PlanetSideGUID
import net.psforever.types.{PlanetSideGUID, SquadListDecoration}
import scodec.bits._
class SquadDefinitionActionMessageTest extends Specification {
@ -29,6 +29,7 @@ class SquadDefinitionActionMessageTest extends Specification {
val string_26 = hex"E7 68 000000"
val string_28 = hex"E7 70 000020" //On
val string_31 = hex"E7 7c 000020" //On
val string_33 = hex"E7 84 0C0008"
val string_34a =
hex"E7 88 00002180420061006400610073007300000000000000040000" //"Badass", Solsar, Any matching position
val string_34b =
@ -224,6 +225,17 @@ class SquadDefinitionActionMessageTest extends Specification {
}
}
"decode (33)" in {
PacketCoding.decodePacket(string_33).require match {
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(3)
unk2 mustEqual 0
action mustEqual SquadListDecorator(SquadListDecoration.Available)
case _ =>
ko
}
}
"decode (34a)" in {
PacketCoding.decodePacket(string_34a).require match {
case SquadDefinitionActionMessage(unk1, unk2, action) =>
@ -460,6 +472,13 @@ class SquadDefinitionActionMessageTest extends Specification {
pkt mustEqual string_31
}
"encode (33)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.SquadListDecorator(SquadListDecoration.Available))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_33
}
"encode (34a)" in {
val msg = SquadDefinitionActionMessage(
PlanetSideGUID(0),

View file

@ -651,7 +651,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail()
.Field1(0)
.Guid(0)
.LeaderCharId(1221560L)
.Members(
List(
@ -677,7 +677,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
PlanetSideGUID(3),
SquadDetail()
.Leader(42631712L, "Jaako")
.Field3(556403L)
.OutfitId(556403L)
.Members(
List(
SquadPositionEntry(0, SquadPositionDetail().Player(0L, ""))

View file

@ -62,7 +62,7 @@ class SquadMembershipResponseTest extends Specification {
"decode (1-1)" in {
PacketCoding.decodePacket(string_11).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Unk01
unk1 mustEqual SquadResponseType.ProximityInvite
unk2 mustEqual 19
unk3 mustEqual 0
unk4 mustEqual 41530025L
@ -78,7 +78,7 @@ class SquadMembershipResponseTest extends Specification {
"decode (1-2)" in {
PacketCoding.decodePacket(string_12).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Unk01
unk1 mustEqual SquadResponseType.ProximityInvite
unk2 mustEqual 18
unk3 mustEqual 0
unk4 mustEqual 41578085L
@ -315,14 +315,14 @@ class SquadMembershipResponseTest extends Specification {
}
"encode (1-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Unk01, 19, 0, 41530025L, Some(0L), "", true, Some(None))
val msg = SquadMembershipResponse(SquadResponseType.ProximityInvite, 19, 0, 41530025L, Some(0L), "", true, Some(None))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_11
}
"encode (1-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.Unk01, 18, 0, 41578085L, Some(0L), "", true, Some(None))
val msg = SquadMembershipResponse(SquadResponseType.ProximityInvite, 18, 0, 41578085L, Some(0L), "", true, Some(None))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string_12

View file

@ -57,6 +57,7 @@ class CharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_id mustEqual 316554L
b.outfit_name mustEqual "Black Beret Armoured Corps"
b.outfit_logo mustEqual 23
b.backpack mustEqual false
@ -67,7 +68,6 @@ class CharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 316554L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
@ -174,6 +174,7 @@ class CharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_id mustEqual 26L
b.outfit_name mustEqual "Black Beret Armoured Corps"
b.outfit_logo mustEqual 23
b.backpack mustEqual false
@ -184,7 +185,6 @@ class CharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 26L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
@ -243,6 +243,7 @@ class CharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_id mustEqual 529687L
b.outfit_name mustEqual "Original District"
b.outfit_logo mustEqual 23
b.backpack mustEqual true
@ -253,7 +254,6 @@ class CharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 529687L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false

View file

@ -87,6 +87,7 @@ class DetailedCharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 65535
b.outfit_id mustEqual 0L
b.outfit_name mustEqual ""
b.outfit_logo mustEqual 0
b.backpack mustEqual false
@ -97,7 +98,6 @@ class DetailedCharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 0L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
@ -275,6 +275,7 @@ class DetailedCharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_id mustEqual 0L
b.outfit_name mustEqual ""
b.outfit_logo mustEqual 0
b.backpack mustEqual false
@ -285,7 +286,6 @@ class DetailedCharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 0L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
@ -460,6 +460,7 @@ class DetailedCharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 65535
b.outfit_id mustEqual 0L
b.outfit_name mustEqual ""
b.outfit_logo mustEqual 0
b.backpack mustEqual false
@ -470,7 +471,6 @@ class DetailedCharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 0L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
@ -687,6 +687,7 @@ class DetailedCharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_id mustEqual 556539L
b.outfit_name mustEqual ""
b.outfit_logo mustEqual 14
b.backpack mustEqual false
@ -697,7 +698,6 @@ class DetailedCharacterDataTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 556539L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
@ -1208,7 +1208,7 @@ class DetailedCharacterDataTest extends Specification {
a.unk9 mustEqual 10
a.unkA mustEqual 1
b.unk0 mustEqual 25044L
b.outfit_id mustEqual 25044L
b.outfit_name mustEqual "Black Armored Reapers"
b.outfit_logo mustEqual 15
b.unk1 mustEqual false
@ -1355,7 +1355,7 @@ class DetailedCharacterDataTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.unk0 mustEqual 16507L
b.outfit_id mustEqual 16507L
b.outfit_name mustEqual "Hooked On Insanity"
b.outfit_logo mustEqual 5
b.unk1 mustEqual false

View file

@ -75,6 +75,7 @@ class MountedVehiclesTest extends Specification {
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_id mustEqual 316554L
b.outfit_name mustEqual "Black Beret Armoured Corps"
b.outfit_logo mustEqual 23
b.backpack mustEqual false
@ -85,7 +86,6 @@ class MountedVehiclesTest extends Specification {
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline.isEmpty mustEqual true
b.unk0 mustEqual 316554L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false