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,8 +823,13 @@ 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
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)
)
@ -837,6 +842,7 @@ class ChatActor(
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,22 +1097,8 @@ 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
sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad
AvatarActor.displayLookingForSquad(session, state = 0)
}
//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))
sendResponse(
SquadState(
PlanetSideGUID(id),
List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
)
)
//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)))
}
val charId = avatar.id
if (toCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
} else if (fromCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
sendResponse(
SquadState(PlanetSideGUID(id), List(SquadStateInfo(firstCharId, elem.health, elem.armor, elem.position)))
)
}
}
}
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,7 +65,7 @@ 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 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 =
@ -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 =>
@ -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}
@ -18,6 +19,7 @@ object SquadAction {
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