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.persistence
import net.psforever.util.{Config, Database, DefinitionUtil} import net.psforever.util.{Config, Database, DefinitionUtil}
import net.psforever.services.Service import net.psforever.services.Service
//import org.log4s.Logger
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
object AvatarActor { 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. * 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. * If discovered, run a function based on the avatar's characteristics.
@ -831,6 +841,11 @@ class AvatarActor(
session = Some(newSession) session = Some(newSession)
Behaviors.same 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) => case CreateAvatar(name, head, voice, gender, empire) =>
import ctx._ import ctx._
ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete { ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete {

View file

@ -823,20 +823,26 @@ class ChatActor(
val popTR = players.count(_.faction == PlanetSideEmpire.TR) val popTR = players.count(_.faction == PlanetSideEmpire.TR)
val popNC = players.count(_.faction == PlanetSideEmpire.NC) val popNC = players.count(_.faction == PlanetSideEmpire.NC)
val popVS = players.count(_.faction == PlanetSideEmpire.VS) val popVS = players.count(_.faction == PlanetSideEmpire.VS)
val contName = session.zone.map.name
sessionActor ! SessionActor.SendResponse( if (popNC + popTR + popVS == 0) {
ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None) sessionActor ! SessionActor.SendResponse(
) ChatMsg(ChatMessageType.CMT_WHO, false, "", "@Nomatches", None)
sessionActor ! SessionActor.SendResponse( )
ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None) } else {
) val contName = session.zone.map.name
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None) ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None)
) )
sessionActor ! SessionActor.SendResponse( sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None) ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None)
) )
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None)
)
sessionActor ! SessionActor.SendResponse(
ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None)
)
}
case (CMT_ZONE, _, contents) if gmCommandAllowed => case (CMT_ZONE, _, contents) if gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+") 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.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.serverobject.zipline.ZipLinePath
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} 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.Utility.InternalTelepad
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.vehicles.control.BfrFlight
@ -151,12 +151,13 @@ object SessionActor {
private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20)
protected final case class SquadUIElement( protected final case class SquadUIElement(
name: String, name: String = "",
index: Int, outfit: Long = 0,
zone: Int, index: Int = -1,
health: Int, zone: Int = 0,
armor: Int, health: Int = 0,
position: Vector3 armor: Int = 0,
position: Vector3 = Vector3.Zero
) )
private final case class NtuCharging(tplayer: Player, vehicle: Vehicle) 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]] = val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
var updateSquadRef: ActorRef = Default.Actor
var updateSquad: () => Unit = NoSquadUpdates var updateSquad: () => Unit = NoSquadUpdates
var recentTeleportAttempt: Long = 0 var recentTeleportAttempt: Long = 0
var lastTerminalOrderFulfillment: Boolean = true var lastTerminalOrderFulfillment: Boolean = true
@ -740,26 +742,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
zoningType = Zoning.Method.InstantAction zoningType = Zoning.Method.InstantAction
zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION
zoningStatus = Zoning.Status.Request 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) cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self)
case Quit() => 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) => case SquadResponse.Detail(guid, detail) =>
sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
case SquadResponse.AssociateWithSquad(squad_guid) => case SquadResponse.IdentifyAsSquadLeader(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad())) sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
case SquadResponse.SetListSquad(squad_guid) => case SquadResponse.SetListSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) 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) => case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) =>
val name = request_type match { val name = request_type match {
case SquadResponseType.Invite if unk5 => 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 { LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
case Some(player) => case Some(player) =>
player.name player.name
@ -1026,10 +1023,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
) )
) )
case SquadResponse.Join(squad, positionsToUpdate, toChannel) => case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
val leader = squad.Leader val avatarId = avatar.id
val membershipPositions = positionsToUpdate map squad.Membership.zipWithIndex val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
membershipPositions.find({ case (member, _) => member.CharId == avatar.id }) match { .filter { case (mem, index) =>
mem.CharId > 0 && positionsToUpdate.contains(index)
}
membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
case Some((ourMember, ourIndex)) => case Some((ourMember, ourIndex)) =>
//we are joining the squad //we are joining the squad
//load each member's entry (our own too) //load each member's entry (our own too)
@ -1043,11 +1043,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
index, index,
member.Name, member.Name,
member.ZoneId, member.ZoneId,
unk7 = 0 outfit_id = 0
) )
) )
squadUI(member.CharId) = 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 //repeat our entry
sendResponse( sendResponse(
@ -1057,39 +1057,38 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
ourIndex, ourIndex,
ourMember.Name, ourMember.Name,
ourMember.ZoneId, ourMember.ZoneId,
unk7 = 0 outfit_id = 0
) )
) )
val playerGuid = player.GUID
//turn lfs off //turn lfs off
val factionChannel = s"${player.Faction}"
if (avatar.lookingForSquad) { if (avatar.lookingForSquad) {
avatarActor ! AvatarActor.SetLookingForSquad(false) avatarActor ! AvatarActor.SetLookingForSquad(false)
} }
val playerGuid = player.GUID
val factionChannel = s"${player.Faction}"
//squad colors //squad colors
GiveSquadColorsInZone() GiveSquadColorsToMembers()
continent.AvatarEvents ! AvatarServiceMessage( GiveSquadColorsForOthers(playerGuid, factionChannel, squad_supplement_id)
factionChannel,
AvatarAction.PlanetsideAttribute(playerGuid, 31, squad_supplement_id)
)
//associate with member position in squad //associate with member position in squad
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex)) sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
//a finalization? what does this do? //a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18))) sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
updateSquadRef = ref
updateSquad = PeriodicUpdatesWhenEnrolledInSquad updateSquad = PeriodicUpdatesWhenEnrolledInSquad
chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID)) chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID))
case _ => case _ =>
//other player is joining our squad //other player is joining our squad
//load each member's entry //load each member's entry
GiveSquadColorsInZone( GiveSquadColorsToMembers(
membershipPositions.map { membershipPositions.map {
case (member, index) => case (member, index) =>
val charId = member.CharId val charId = member.CharId
sendResponse( 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) = 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 charId
} }
) )
@ -1098,23 +1097,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse( sendResponse(
SquadState( SquadState(
PlanetSideGUID(squad_supplement_id), PlanetSideGUID(squad_supplement_id),
membershipPositions membershipPositions.map { case (member, _) =>
.filterNot { case (member, _) => member.CharId == avatar.id } SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
.map { }
case (member, _) =>
SquadStateInfo(
member.CharId,
member.Health,
member.Armor,
member.Position,
2,
2,
false,
429,
None,
None
)
}
) )
) )
@ -1123,6 +1108,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case Some((ourMember, ourIndex)) => case Some((ourMember, ourIndex)) =>
//we are leaving the squad //we are leaving the squad
//remove each member's entry (our own too) //remove each member's entry (our own too)
updateSquadRef = Default.Actor
positionsToUpdate.foreach { positionsToUpdate.foreach {
case (member, index) => case (member, index) =>
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
@ -1131,16 +1117,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
//uninitialize //uninitialize
val playerGuid = player.GUID val playerGuid = player.GUID
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
sendResponse(PlanetsideAttributeMessage(playerGuid, 31, 0)) //disassociate with squad? GiveSquadColorsToSelf(value = 0)
continent.AvatarEvents ! AvatarServiceMessage( sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
s"${player.Faction}",
AvatarAction.PlanetsideAttribute(playerGuid, 31, 0)
)
sendResponse(
PlanetsideAttributeMessage(playerGuid, 32, 0)
) //disassociate with member position in squad?
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated? sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
lfsm = false lfsm = false
avatarActor ! AvatarActor.SetLookingForSquad(false)
//a finalization? what does this do? //a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
squad_supplement_id = 0 squad_supplement_id = 0
@ -1149,7 +1130,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID)) chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
case _ => case _ =>
//remove each member's entry //remove each member's entry
GiveSquadColorsInZone( GiveSquadColorsToMembers(
positionsToUpdate.map { positionsToUpdate.map {
case (member, index) => case (member, index) =>
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, 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 //we've already swapped position internally; now we swap the cards
SwapSquadUIElements(squad, from_index, to_index) SwapSquadUIElements(squad, from_index, to_index)
case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) => case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
val charId = player.CharId if (promotedPlayer != player.CharId) {
val guid = player.GUID //demoted from leader; no longer lfsm
lazy val factionChannel = s"${player.Faction}"
//are we being demoted?
if (squadUI(charId).index == 0) {
//lfsm -> lfs
if (lfsm) { if (lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 0)) lfsm = false
continent.AvatarEvents ! AvatarServiceMessage( AvatarActor.displayLookingForSquad(session, state = 0)
factionChannel,
AvatarAction.PlanetsideAttribute(guid, 53, 0)
)
} }
lfsm = false
sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad
} }
//are we being promoted? sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
else if (charId == char_id) { //the players have already been swapped in the backend object
sendResponse(PlanetsideAttributeMessage(guid, 32, 0)) //associate with member position in squad PromoteSquadUIElements(squad, from_index)
}
continent.AvatarEvents ! AvatarServiceMessage(
factionChannel,
AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id)
)
//we must fix the squad cards backend
SwapSquadUIElements(squad, from_index, to_index)
case SquadResponse.UpdateMembers(squad, positions) => case SquadResponse.UpdateMembers(_, positions) =>
val pairedEntries = positions.collect { val pairedEntries = positions.collect {
case entry if squadUI.contains(entry.char_id) => case entry if squadUI.contains(entry.char_id) =>
(entry, squadUI(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) SquadMemberEvent.UpdateZone(squad_supplement_id, entry.char_id, element.index, entry.zone_number)
) )
squadUI(entry.char_id) = 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 entry
case (entry, element) case (entry, element)
if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
//other elements that need to be updated //other elements that need to be updated
squadUI(entry.char_id) = 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 entry
}) })
.filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend .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( SquadState(
PlanetSideGUID(squad_supplement_id), PlanetSideGUID(squad_supplement_id),
updatedEntries.map { entry => 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() => case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
//I don't actually know how to return search results sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
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) => case SquadResponse.InitWaypoints(char_id, waypoints) =>
waypoints.foreach { waypoints.foreach {
@ -3086,7 +3065,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case None => case None =>
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
TaskWorkflow.execute(BuyNewEquipmentPutInInventory( 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, tplayer,
msg.terminal_guid msg.terminal_guid
)(item)) )(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)) sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, unk5=true))
//looking for squad (members) //looking for squad (members)
if (tplayer.avatar.lookingForSquad || lfsm) { if (tplayer.avatar.lookingForSquad || lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) AvatarActor.displayLookingForSquad(session, state = 1)
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
} }
sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) 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 //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, _) => ; case (None, _) => ;
} }
//non-squad GUID-0 counts as the settings when not joined with a squad //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.SetListSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
@ -3880,11 +3858,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
if (squad_supplement_id > 0) { if (squad_supplement_id > 0) {
squadUI.get(player.CharId) match { squadUI.get(player.CharId) match {
case Some(elem) => case Some(elem) =>
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id)) GiveSquadColorsToSelf(squad_supplement_id)
continent.AvatarEvents ! AvatarServiceMessage(
s"${player.Faction}",
AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id)
)
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, elem.index)) sendResponse(PlanetsideAttributeMessage(player.GUID, 32, elem.index))
case _ => case _ =>
log.warn(s"RespawnSquadSetup: asked to redraw squad information, but ${player.Name} has no squad element for squad $squad_supplement_id") 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. * These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
* During a zone change, * During a zone change,
@ -3902,23 +3915,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
def ZoneChangeSquadSetup(): Unit = { def ZoneChangeSquadSetup(): Unit = {
RespawnSquadSetup() RespawnSquadSetup()
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
GiveSquadColorsInZone() GiveSquadColorsToMembers()
squadSetup = RespawnSquadSetup squadSetup = RespawnSquadSetup
} }
/** /**
* Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color. * Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color.
*/ */
def GiveSquadColorsInZone(): Unit = { def GiveSquadColorsToMembers(): Unit = {
GiveSquadColorsInZone(squadUI.keys, squad_supplement_id) GiveSquadColorsToMembers(squadUI.keys, squad_supplement_id)
} }
/** /**
* Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color. * 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 * @param members members of the squad to target
*/ */
def GiveSquadColorsInZone(members: Iterable[Long]): Unit = { def GiveSquadColorsToMembers(members: Iterable[Long]): Unit = {
GiveSquadColorsInZone(members, squad_supplement_id) 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 members members of the squad to target
* @param value the assignment value * @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 => SquadMembersInZone(members).foreach { members =>
sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value)) sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
} }
@ -5784,15 +5797,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else if (action == 30) { } else if (action == 30) {
log.info(s"${player.Name} is back") log.info(s"${player.Name} is back")
player.AwayFromKeyboard = false player.AwayFromKeyboard = false
} else if (action == GenericActionEnum.DropSpecialItem.id) {
DropSpecialSlotItem()
renewCharSavedTimer( renewCharSavedTimer(
Config.app.game.savedMsg.renewal.fixed, Config.app.game.savedMsg.renewal.fixed,
Config.app.game.savedMsg.renewal.variable Config.app.game.savedMsg.renewal.variable
) )
} } else if (action == 15) { //max deployment
if (action == GenericActionEnum.DropSpecialItem.id) {
DropSpecialSlotItem()
}
if (action == 15) { //max deployment
log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground") log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground")
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
continent.AvatarEvents ! AvatarServiceMessage( continent.AvatarEvents ! AvatarServiceMessage(
@ -5851,32 +5862,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
} else { } else {
log.warn(s"GenericActionMessage: ${player.Name} can't handle action code 21") log.warn(s"GenericActionMessage: ${player.Name} can't handle action code 21")
} }
} else if (action == 36) { //Looking For Squad ON } else if (action == 36 || action == 37) { //Looking For Squad (Members) (on/off)
if (squadUI.nonEmpty) { val state = if (action == 36) { true } else { false }
if (!lfsm && squadUI(player.CharId).index == 0) { squadUI.get(player.CharId) match {
lfsm = true case Some(elem) if elem.index == 0 =>
continent.AvatarEvents ! AvatarServiceMessage( lfsm = state
s"${player.Faction}", AvatarActor.displayLookingForSquad(session, boolToInt(state))
AvatarAction.PlanetsideAttribute(player.GUID, 53, 1) case _ =>
) avatarActor ! AvatarActor.SetLookingForSquad(state)
}
} 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 { } 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) { * Swap the squad UI elements along the top of the screen
val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object * (colloquially referred to as "cards")
val fromCharId = fromMember.CharId * to match recently updated positions of the members in a squad.<br>
val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object * <br>
val toCharId = toMember.CharId * The squad membership structure should have already updated to the correct positions of the members.
val id = 11 * By referencing one of the indices in this structure,
if (toCharId > 0) { * obtain the character identification number,
//toMember and fromMember have swapped places * use the character identification number to locate that member's card,
val fromElem = squadUI(fromCharId) * and update the card index to match the squad position in which that member is discovered.
val toElem = squadUI(toCharId) * @see `PlanetsideAttributeMessage`
squadUI(toCharId) = * @see `Squad`
SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position) * @see `SquadMemberEvent.Add`
squadUI(fromCharId) = * @see `SquadMemberEvent.Remove`
SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position) * @see `SquadState`
sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0)) * @see `SquadStateInfo`
sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, unk7 = 0)) * @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( sendResponse(
SquadState( SquadState(PlanetSideGUID(id), List(
PlanetSideGUID(id), SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position),
List( SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position)
SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, false, 429, None, None), ))
SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, false, 429, None, None)
)
)
) )
} else { } else {
//previous fromMember has moved toMember //firstMember has moved to a new position in squad
val elem = squadUI(fromCharId) val elem = squadUI(firstCharId)
squadUI(fromCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position) squadUI.put(firstCharId, elem.copy(index = firstIndex))
sendResponse(SquadMemberEvent.Remove(id, fromCharId, fromIndex)) // sendResponse(SquadMemberEvent.Remove(id, firstCharId, elem.index))
sendResponse(SquadMemberEvent.Add(id, fromCharId, toIndex, elem.name, elem.zone, unk7 = 0)) sendResponse(SquadMemberEvent.Add(id, firstCharId, firstIndex, elem.name, elem.zone, outfit_id = 0))
if (firstCharId == player.CharId) {
sendResponse(PlanetsideAttributeMessage(firstMember.GUID, 32, firstIndex))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
}
sendResponse( sendResponse(
SquadState( SquadState(PlanetSideGUID(id), List(SquadStateInfo(firstCharId, elem.health, elem.armor, elem.position)))
PlanetSideGUID(id),
List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
)
) )
} }
val charId = avatar.id
if (toCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
} else if (fromCharId == charId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
}
} }
} }
def NoSquadUpdates(): Unit = {} def PromoteSquadUIElements(squad: Squad, fromIndex: Int): Unit = {
//the players should have already been swapped in the backend object
val firstMember = squad.Membership(0)
val firstCharId = firstMember.CharId
val secondMember = squad.Membership(fromIndex)
val secondCharId = secondMember.CharId
if (squadUI.nonEmpty && fromIndex != 0 && firstCharId > 0 && secondCharId > 0) {
val newFirstElem = squadUI(firstCharId).copy(index = 0)
val newSecondElem = squadUI(secondCharId).copy(index = fromIndex)
val charId = player.CharId
val pguid = player.GUID
val sguid = squad.GUID
val id = squad_supplement_id
//secondMember and firstMember swap places
squadUI.put(firstCharId, newFirstElem)
squadUI.put(secondCharId, newSecondElem)
sendResponse(SquadMemberEvent(MemberEvent.Promote, id, firstCharId, position = 0))
//player is being either promoted or demoted?
if (firstCharId == charId) {
sendResponse(PlanetsideAttributeMessage(pguid, 32, 0))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.IdentifyAsSquadLeader()))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
} else if (secondCharId == charId) {
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.IdentifyAsSquadLeader()))
sendResponse(PlanetsideAttributeMessage(pguid, 32, fromIndex))
sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18)))
}
//seed updates (just for the swapped players)
sendResponse(
SquadState(PlanetSideGUID(id), List(
SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position),
SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position)
))
)
}
}
def NoSquadUpdates(): Unit = { }
def SquadUpdates(): Unit = { def SquadUpdates(): Unit = {
squadService ! SquadServiceMessage( updateSquadRef ! SquadServiceMessage(
player, player,
continent, continent,
SquadServiceAction.Update( SquadServiceAction.Update(
player.CharId, player.CharId,
player.GUID,
player.Health, player.Health,
player.MaxHealth, player.MaxHealth,
player.Armor, player.Armor,
player.MaxArmor, player.MaxArmor,
avatar.certifications,
player.Position, player.Position,
continent.Number continent.Number
) )

View file

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

View file

@ -2,9 +2,10 @@
package net.psforever.objects.teamwork package net.psforever.objects.teamwork
import akka.actor.{ActorContext, ActorRef, Props} import akka.actor.{ActorContext, ActorRef, Props}
import net.psforever.types.SquadWaypoint import net.psforever.objects.Default
import net.psforever.services.teamwork.SquadService.WaypointData import net.psforever.packet.game.WaypointInfo
import net.psforever.services.teamwork.SquadSwitchboard import net.psforever.types.{PlanetSideGUID, SquadWaypoint, Vector3}
import net.psforever.services.teamwork.{SquadSubscriptionEntity, SquadSwitchboard}
class SquadFeatures(val Squad: Squad) { 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 * 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. * 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. * 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` * @see `Start`
*/ */
private var waypoints: Array[WaypointData] = Array[WaypointData]() private var waypoints: Array[WaypointData] = Array[WaypointData]()
@ -65,25 +70,25 @@ class SquadFeatures(val Squad: Squad) {
* For the purposes of wide-searches for membership * For the purposes of wide-searches for membership
* such as Looking For Squad checks and proximity invitation, * such as Looking For Squad checks and proximity invitation,
* the unique character identifier numbers in this list are skipped. * 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 refusedPlayers: List[Long] = Nil
private var autoApproveInvitationRequests: Boolean = false private var autoApproveInvitationRequests: Boolean = false
private var locationFollowsSquadLead: Boolean = true private var locationFollowsSquadLead: Boolean = true //TODO false
private var listed: Boolean = false private var listed: Boolean = false
private lazy val channel: String = s"${Squad.Faction}-Squad${Squad.GUID.guid}" private lazy val channel: String = s"${Squad.Faction}-Squad${Squad.GUID.guid}"
def Start(implicit context: ActorContext): SquadFeatures = { def Start(implicit context: ActorContext, subs: SquadSubscriptionEntity): SquadFeatures = {
switchboard = context.actorOf(Props[SquadSwitchboard](), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}") switchboard = context.actorOf(Props(classOf[SquadSwitchboard], this, subs), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}")
waypoints = Array.fill[WaypointData](SquadWaypoint.values.size)(new WaypointData()) waypoints = Array.fill[WaypointData](SquadWaypoint.values.size)(new WaypointData())
this this
} }
def Stop: SquadFeatures = { def Stop: SquadFeatures = {
switchboard ! akka.actor.PoisonPill switchboard ! akka.actor.PoisonPill
switchboard = ActorRef.noSender switchboard = Default.Actor
waypoints = Array.empty waypoints = Array.empty
this this
} }
@ -99,6 +104,58 @@ class SquadFeatures(val Squad: Squad) {
def Waypoints: Array[WaypointData] = waypoints 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: Option[Int] = searchForRole
def SearchForRole_=(role: Int): Option[Int] = SearchForRole_=(Some(role)) def SearchForRole_=(role: Int): Option[Int] = SearchForRole_=(Some(role))
@ -115,15 +172,24 @@ class SquadFeatures(val Squad: Squad) {
ProxyInvites ProxyInvites
} }
def Refuse: List[Long] = refusedPlayers def DeniedPlayers(): List[Long] = refusedPlayers
def Refuse_=(charId: Long): List[Long] = { def DeniedPlayers(charId: Long): List[Long] = {
Refuse_=(List(charId)) DeniedPlayers(List(charId))
} }
def Refuse_=(list: List[Long]): List[Long] = { def DeniedPlayers(list: List[Long]): List[Long] = {
refusedPlayers = list ++ refusedPlayers 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 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( final case class CharacterKnowledgeInfo(
name: String, name: String,
permissions: Set[Certification], certifications: Set[Certification],
unk1: Int, unk1: Int,
unk2: Int, unk2: Int,
unk3: PlanetSideGUID zoneNumber: Int
) )
final case class CharacterKnowledgeMessage(char_id: Long, info: Option[CharacterKnowledgeInfo]) 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 = def apply(char_id: Long, info: CharacterKnowledgeInfo): CharacterKnowledgeMessage =
CharacterKnowledgeMessage(char_id, Some(info)) CharacterKnowledgeMessage(char_id, Some(info))
private val inverter: Codec[Boolean] = bool.xmap[Boolean]( private val inverter: Codec[Boolean] = bool.xmap[Boolean](state => !state, state => !state)
state => !state,
state => !state
)
private val info_codec: Codec[CharacterKnowledgeInfo] = ( private val info_codec: Codec[CharacterKnowledgeInfo] = (
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) :: ("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) ::
("permissions" | ulongL(bits = 46)) :: ("certifications" | ulongL(bits = 46)) ::
("unk1" | uint(bits = 6)) :: ("unk1" | uint(bits = 6)) ::
("unk2" | uint(bits = 3)) :: ("unk2" | uint(bits = 3)) ::
("unk3" | PlanetSideGUID.codec) ("zone" | uint16L)
).xmap[CharacterKnowledgeInfo]( ).xmap[CharacterKnowledgeInfo](
{ {
case name :: permissions :: u1 :: u2 :: u3 :: HNil => case name :: certs :: u1 :: u2 :: zone :: HNil =>
CharacterKnowledgeInfo(name, Certification.fromEncodedLong(permissions), u1, u2, u3) CharacterKnowledgeInfo(name, Certification.fromEncodedLong(certs), u1, u2, zone)
}, },
{ {
case CharacterKnowledgeInfo(name, permissions, u1, u2, u3) => case CharacterKnowledgeInfo(name, certs, u1, u2, zone) =>
name :: Certification.toEncodedLong(permissions) :: u1 :: u2 :: u3 :: HNil name :: Certification.toEncodedLong(certs) :: u1 :: u2 :: zone :: HNil
} }
) )

View file

@ -65,8 +65,8 @@ final case class SquadInfo(
this And SquadInfo(None, None, Some(zone), None, None, None) this And SquadInfo(None, None, Some(zone), None, None, None)
def ZoneId(zone: Option[PlanetSideZoneID]): SquadInfo = def ZoneId(zone: Option[PlanetSideZoneID]): SquadInfo =
zone match { 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) case None => SquadInfo(leader, task, zone, size, capacity, squad_guid)
} }
def Size(sz: Int): SquadInfo = def Size(sz: Int): SquadInfo =
this And SquadInfo(None, None, None, Some(sz), None, None) this And SquadInfo(None, None, None, Some(sz), None, None)
@ -612,8 +612,8 @@ object SquadHeader {
(bool >>:~ { unk1 => (bool >>:~ { unk1 =>
uint8 >>:~ { unk2 => uint8 >>:~ { unk2 =>
conditional(!unk1 && unk2 == 1, removeCodec) :: conditional(!unk1 && unk2 == 1, removeCodec) ::
conditional(unk1 && unk2 == 6, providedCodec) :: conditional(unk1 && unk2 == 6, providedCodec) ::
conditional(unk1 && unk2 != 6, listing_codec(unk2)) conditional(unk1 && unk2 != 6, listing_codec(unk2))
} }
}).exmap[Option[SquadInfo]]( }).exmap[Option[SquadInfo]](
{ {
@ -691,10 +691,8 @@ object SquadListing {
private def meta_codec(entryFunc: Int => Codec[Option[SquadInfo]]): Codec[SquadListing] = private def meta_codec(entryFunc: Int => Codec[Option[SquadInfo]]): Codec[SquadListing] =
(("index" | uint8L) >>:~ { index => (("index" | uint8L) >>:~ { index =>
conditional(index < 255, "listing" | entryFunc(index)) :: conditional(index < 255, "listing" | entryFunc(index)) ::
conditional( conditional(index == 255, bits)
index == 255, //consume n < 8 bits after the tail entry, else vector will try to operate on invalid data
bits
) //consume n < 8 bits after the tail entry, else vector will try to operate on invalid data
}).xmap[SquadListing]( }).xmap[SquadListing](
{ {
case ndx :: Some(lstng) :: _ :: HNil => case ndx :: Some(lstng) :: _ :: HNil =>
@ -717,7 +715,7 @@ object SquadListing {
* `Codec` for branching types of `SquadListing` initializations. * `Codec` for branching types of `SquadListing` initializations.
*/ */
val info_codec: Codec[SquadListing] = meta_codec({ index: Int => 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 => implicit val codec: Codec[ReplicationStreamMessage] = (
conditional(behavior == 5, "behavior2" | uintL(3)) :: ("behavior" | uintL(bits = 3)) >>:~ { behavior =>
("behavior2" | conditional(behavior == 5, uintL(bits = 3))) ::
conditional(behavior != 1, bool) :: conditional(behavior != 1, bool) ::
newcodecs.binary_choice( ("entries" | newcodecs.binary_choice(
behavior != 5, behavior != 5,
"entries" | vector(SquadListing.codec), vector(SquadListing.codec),
"entries" | vector(SquadListing.info_codec) vector(SquadListing.info_codec)
) ))
}).xmap[ReplicationStreamMessage]( }).xmap[ReplicationStreamMessage](
{ {
case bhvr :: bhvr2 :: _ :: lst :: HNil => 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.objects.avatar.Certification
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} 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.bits.BitVector
import scodec.{Attempt, Codec, Err} import scodec.{Attempt, Codec, Err}
import scodec.codecs._ import scodec.codecs._
@ -25,71 +25,73 @@ object SquadAction {
implicit val codec: Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3)) 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? * Dispatched from client to server to indicate a squad detail update that has no foundation entry to update?
* Not dissimilar from `DisplaySquad`. * 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]) 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( final case class SearchForSquadsWithParticularRole(
role: String, role: String,
requirements: Set[Certification], requirements: Set[Certification],
zone_id: Int, zone_id: Int,
mode: SearchMode.Value 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) final case class Unknown(badCode: Int, data: BitVector) extends SquadAction(badCode)
@ -114,10 +116,10 @@ object SquadAction {
} }
) )
val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadMemberInitializationIssue]( val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadInitializationIssue](
_ => SquadMemberInitializationIssue(), _ => SquadInitializationIssue(),
{ {
case SquadMemberInitializationIssue() => None case SquadInitializationIssue() => None
} }
) )
@ -179,10 +181,10 @@ object SquadAction {
} }
) )
val associateWithSquadCodec = everFailCondition.xmap[AssociateWithSquad]( val identifyAsSquadLeaderCodec = everFailCondition.xmap[IdentifyAsSquadLeader](
_ => AssociateWithSquad(), _ => 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) :: val searchForSquadsWithParticularRoleCodec = (PacketHelpers.encodedWideStringAligned(6) ::
ulongL(46) :: ulongL(46) ::
uint16L :: uint16L ::
@ -389,7 +403,7 @@ object SquadAction {
* &nbsp;&nbsp;&nbsp;&nbsp;`20` - (Squad leader) Change Squad Zone<br> * &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;`21` - (Squad leader) Close Squad Member Position<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`22` - (Squad leader) Add 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;&nbsp;&nbsp;`40` - Find LFS Soldiers that Meet the Requirements for this Role<br>
* &nbsp;&nbsp;`Long`<br> * &nbsp;&nbsp;`Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`13` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`13` - UNKNOWN<br>
@ -451,7 +465,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
case 13 => unknownCodec(action = 13) case 13 => unknownCodec(action = 13)
case 14 => unknownCodec(action = 14) case 14 => unknownCodec(action = 14)
case 15 => cancelSelectRoleForYourselfCodec case 15 => cancelSelectRoleForYourselfCodec
case 16 => associateWithSquadCodec case 16 => identifyAsSquadLeaderCodec
case 17 => setListSquadCodec case 17 => setListSquadCodec
case 18 => unknownCodec(action = 18) case 18 => unknownCodec(action = 18)
case 19 => changeSquadPurposeCodec case 19 => changeSquadPurposeCodec
@ -468,7 +482,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
case 30 => unknownCodec(action = 30) case 30 => unknownCodec(action = 30)
case 31 => locationFollowsSquadLeadCodec case 31 => locationFollowsSquadLeadCodec
case 32 => unknownCodec(action = 32) case 32 => unknownCodec(action = 32)
case 33 => unknownCodec(action = 33) case 33 => squadListDecoratorCodec
case 34 => searchForSquadsWithParticularRoleCodec case 34 => searchForSquadsWithParticularRoleCodec
case 35 => cancelSquadSearchCodec case 35 => cancelSquadSearchCodec
case 36 => unknownCodec(action = 36) 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> * All fields are optional for that reason.<br>
* <br> * <br>
* The squad leader does not necessarily have to be a person from the `member_info` list. * 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 * must be non-zero when parsed in a FullSquad pattern
* @param unk2 na; * @param unk2 na;
* not associated with any fields during itemized parsing * not associated with any fields during itemized parsing
* @param leader_char_id he unique character identification number for the squad leader * @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 leader_name the name of the player who is the squad leader
* @param task the suggested responsibilities or mission statement of the squad * @param task the suggested responsibilities or mission statement of the squad
* @param zone_id the suggested area of engagement for this squad's activities; * @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 * @param member_info a list of squad position data
*/ */
final case class SquadDetail( final case class SquadDetail(
unk1: Option[Int], guid: Option[Int],
unk2: Option[Int], unk2: Option[Int],
leader_char_id: Option[Long], leader_char_id: Option[Long],
unk3: Option[Long], outfit_id: Option[Long],
leader_name: Option[String], leader_name: Option[String],
task: Option[String], task: Option[String],
zone_id: Option[PlanetSideZoneID], zone_id: Option[PlanetSideZoneID],
@ -176,10 +176,10 @@ final case class SquadDetail(
*/ */
def And(info: SquadDetail): SquadDetail = { def And(info: SquadDetail): SquadDetail = {
SquadDetail( SquadDetail(
unk1.orElse(info.unk1), guid.orElse(info.guid),
unk2.orElse(info.unk2), unk2.orElse(info.unk2),
leader_char_id.orElse(info.leader_char_id), leader_char_id.orElse(info.leader_char_id),
unk3.orElse(info.unk3), outfit_id.orElse(info.outfit_id),
leader_name.orElse(info.leader_name), leader_name.orElse(info.leader_name),
task.orElse(info.task), task.orElse(info.task),
zone_id.orElse(info.zone_id), 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 //methods intended to combine the fields of itself and another object
def Field1(value: Int): SquadDetail = def Guid(guid: Int): SquadDetail =
this And SquadDetail(Some(value), None, None, None, None, None, None, None, None) 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 = def LeaderCharId(char_id: Long): SquadDetail =
this And SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) this And SquadDetail(None, None, Some(char_id), None, None, None, None, None, None)
def Field3(value: Long): SquadDetail = def OutfitId(outfit: Long): SquadDetail =
this And SquadDetail(None, None, None, Some(value), None, None, None, None, None) this And SquadDetail(None, None, None, Some(outfit), None, None, None, None, None)
def LeaderName(name: String): SquadDetail = def LeaderName(name: String): SquadDetail =
this And SquadDetail(None, None, None, None, Some(name), None, None, None, None) this And SquadDetail(None, None, None, None, Some(name), None, None, None, None)
def Leader(char_id: Long, name: String): SquadDetail = def Leader(char_id: Long, name: String): SquadDetail =
@ -228,14 +230,14 @@ final case class SquadDetail(
*/ */
def Complete: SquadDetail = def Complete: SquadDetail =
SquadDetail( SquadDetail(
unk1.orElse(Some(1)), guid.orElse(Some(1)),
unk2.orElse(Some(0)), unk2.orElse(Some(0)),
leader_char_id.orElse(Some(0L)), leader_char_id.orElse(Some(0L)),
unk3.orElse(Some(0L)), outfit_id.orElse(Some(0L)),
leader_name.orElse(Some("")), leader_name.orElse(Some("")),
task.orElse(Some("")), task.orElse(Some("")),
zone_id.orElse(Some(PlanetSideZoneID(0))), zone_id.orElse(Some(PlanetSideZoneID(0))),
unk7.orElse(Some(4983296)), //FullSquad value unk7.orElse(Some(4983296)), //FullSquad value?
{ {
val complete = SquadPositionDetail().Complete val complete = SquadPositionDetail().Complete
Some(member_info match { Some(member_info match {
@ -359,7 +361,7 @@ object SquadDetail {
unk1: Int, unk1: Int,
unk2: Int, unk2: Int,
leader_char_id: Long, leader_char_id: Long,
unk3: Long, outfit_id: Long,
leader_name: String, leader_name: String,
task: String, task: String,
zone_id: PlanetSideZoneID, zone_id: PlanetSideZoneID,
@ -370,7 +372,7 @@ object SquadDetail {
Some(unk1), Some(unk1),
Some(unk2), Some(unk2),
Some(leader_char_id), Some(leader_char_id),
Some(unk3), Some(outfit_id),
Some(leader_name), Some(leader_name),
Some(task), Some(task),
Some(zone_id), Some(zone_id),
@ -380,12 +382,12 @@ object SquadDetail {
} }
//individual field overloaded constructors //individual field overloaded constructors
def Field1(unk1: Int): SquadDetail = def Guid(guid: Int): SquadDetail =
SquadDetail(Some(unk1), None, None, None, None, None, None, None, None) SquadDetail(Some(guid), None, None, None, None, None, None, None, None)
def LeaderCharId(char_id: Long): SquadDetail = def LeaderCharId(char_id: Long): SquadDetail =
SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) SquadDetail(None, None, Some(char_id), None, None, None, None, None, None)
def Field3(char_id: Option[Long], unk3: Long): SquadDetail = def OutfitId(char_id: Option[Long], outfit_id: Long): SquadDetail =
SquadDetail(None, None, None, Some(unk3), None, None, None, None, None) SquadDetail(None, None, None, Some(outfit_id), None, None, None, None, None)
def LeaderName(name: String): SquadDetail = def LeaderName(name: String): SquadDetail =
SquadDetail(None, None, None, None, Some(name), None, None, None, None) SquadDetail(None, None, None, None, Some(name), None, None, None, None)
def Leader(char_id: Long, name: String): SquadDetail = 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)) SquadDetail(None, None, None, None, None, None, None, None, Some(list))
object Fields { object Fields {
final val Field1 = 1 final val Guid = 1
final val CharId = 2 final val CharId = 2
final val Field3 = 3 final val Outfit = 3
final val Leader = 4 final val Leader = 4
final val Task = 5 final val Task = 5
final val ZoneId = 6 final val ZoneId = 6
@ -541,10 +543,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
val codec: Codec[SquadDetail] = { val codec: Codec[SquadDetail] = {
import shapeless.:: import shapeless.::
( (
("unk1" | uint8) :: ("guid" | uint16L) ::
("unk2" | uint24) :: //unknown, but can be 0'd ("unk2" | uint16L) :: //unknown, but can be 0'd
("leader_char_id" | uint32L) :: ("leader_char_id" | uint32L) ::
("unk3" | uint32L) :: //variable fields, but can be 0'd ("outfit_id" | uint32L) :: //can be 0'd
("leader" | PacketHelpers.encodedWideStringAligned(7)) :: ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
("task" | PacketHelpers.encodedWideString) :: ("task" | PacketHelpers.encodedWideString) ::
("zone_id" | PlanetSideZoneID.codec) :: ("zone_id" | PlanetSideZoneID.codec) ::
@ -552,13 +554,13 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
optional(bool, "member_info" | initial_member_codec) optional(bool, "member_info" | initial_member_codec)
).exmap[SquadDetail]( ).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( Attempt.Successful(
SquadDetail( SquadDetail(
Some(u1), Some(guid),
Some(u2), Some(u2),
Some(char_id), Some(char_id),
Some(u3), Some(outfit_id),
Some(leader), Some(leader),
Some(task), Some(task),
Some(zone), Some(zone),
@ -573,10 +575,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
}, },
{ {
case SquadDetail( case SquadDetail(
Some(u1), Some(guid),
Some(u2), Some(u2),
Some(char_id), Some(char_id),
Some(u3), Some(outfit_id),
Some(leader), Some(leader),
Some(task), Some(task),
Some(zone), Some(zone),
@ -584,7 +586,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
Some(member_list) Some(member_list)
) => ) =>
Attempt.Successful( 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)) :: Some(linkFields(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
HNil HNil
) )
@ -605,15 +607,15 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
object ItemizedSquad { 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]( private val guidCodec: Codec[SquadDetail] = uint16L.exmap[SquadDetail](
unk1 => Attempt.successful(SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)), guid => Attempt.successful(SquadDetail(Some(guid), None, None, None, None, None, None, None, None)),
{ {
case SquadDetail(Some(unk1), _, _, _, _, _, _, _, _) => case SquadDetail(Some(guid), _, _, _, _, _, _, _, _) =>
Attempt.successful(unk1) Attempt.successful(guid)
case _ => 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]( private val outfitCodec: Codec[SquadDetail] = uint32L.exmap[SquadDetail](
unk3 => Attempt.successful(SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)), outfit_id => Attempt.successful(SquadDetail(None, None, None, Some(outfit_id), None, None, None, None, None)),
{ {
case SquadDetail(_, _, _, Some(unk3), _, _, _, _, _) => case SquadDetail(_, _, _, Some(outfit_id), _, _, _, _, _) =>
Attempt.successful(unk3) Attempt.successful(outfit_id)
case _ => case _ =>
Attempt.failure(Err("failed to encode squad data for unknown field #3")) 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] = { private def selectCodedAction(code: Int, bitsOverByte: StreamLengthToken): Codec[SquadDetail] = {
code match { code match {
case 1 => field1Codec case 1 => guidCodec
case 2 => leaderCharIdCodec case 2 => leaderCharIdCodec
case 3 => field3Codec case 3 => outfitCodec
case 4 => leaderNameCodec(bitsOverByte) case 4 => leaderNameCodec(bitsOverByte)
case 5 => taskCodec(bitsOverByte) case 5 => taskCodec(bitsOverByte)
case 6 => zoneCodec 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)` * @see `selectCodedAction(Int, StreamLengthToken)`
* @param code the action code, connecting to a field pattern * @param code the action code, connecting to a field pattern
* @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding * @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 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 5 => 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 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 8 => bitsOverByte.Length = 0 //end of stream
case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect 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)), (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)), (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)), (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)), (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 ) //in reverse order so that the linked list is in the correct order
.filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank } match { .filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank } match {
case Nil => case Nil =>
@ -1102,12 +1104,12 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
*/ */
private def modifyCodedPadValue(code: Int, bitsOverByte: StreamLengthToken): StreamLengthToken = { private def modifyCodedPadValue(code: Int, bitsOverByte: StreamLengthToken): StreamLengthToken = {
code match { code match {
case 0 => bitsOverByte.Add(1) //additional 1u case 0 => bitsOverByte.Add(1) //additional 1u
case 1 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 1 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 2 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 2 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 3 => bitsOverByte //32u = no added padding case 3 => bitsOverByte //32u = no added padding
case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u
case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
} }
} }
@ -1587,10 +1589,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
{ {
case SquadDetailDefinitionUpdateMessage(guid, info) => case SquadDetailDefinitionUpdateMessage(guid, info) =>
val occupiedSquadFieldCount = List( val occupiedSquadFieldCount = List(
info.unk1, info.guid,
info.unk2, info.unk2,
info.leader_char_id, info.leader_char_id,
info.unk3, info.outfit_id,
info.leader_name, info.leader_name,
info.task, info.task,
info.zone_id, info.zone_id,

View file

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

View file

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

View file

@ -58,14 +58,16 @@ final case class SquadState(guid: PlanetSideGUID, info_list: List[SquadStateInfo
} }
object SquadStateInfo { object SquadStateInfo {
def apply(unk1: Long, unk2: Int, unk3: Int, pos: Vector3, unk4: Int, unk5: Int, unk6: Boolean, unk7: Int) def apply(charId: Long, health: Int, armor: Int, pos: Vector3): SquadStateInfo =
: SquadStateInfo = SquadStateInfo(charId, health, armor, pos, 2, 2, unk6=false, 429, None, None)
SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, 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( def apply(
unk1: Long, charId: Long,
unk2: Int, health: Int,
unk3: Int, armor: Int,
pos: Vector3, pos: Vector3,
unk4: Int, unk4: Int,
unk5: Int, unk5: Int,
@ -74,22 +76,22 @@ object SquadStateInfo {
unk8: Int, unk8: Int,
unk9: Boolean unk9: Boolean
): SquadStateInfo = ): 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] { object SquadState extends Marshallable[SquadState] {
private val info_codec: Codec[SquadStateInfo] = ( private val info_codec: Codec[SquadStateInfo] = (
("char_id" | uint32L) :: ("char_id" | uint32L) ::
("health" | uint(7)) :: ("health" | uint(bits = 7)) ::
("armor" | uint(7)) :: ("armor" | uint(bits = 7)) ::
("pos" | Vector3.codec_pos) :: ("pos" | Vector3.codec_pos) ::
("unk4" | uint2) :: ("unk4" | uint2) ::
("unk5" | uint2) :: ("unk5" | uint2) ::
("unk6" | bool) :: ("unk6" | bool) ::
("unk7" | uint16L) :: ("unk7" | uint16L) ::
(bool >>:~ { out => (bool >>:~ { out =>
conditional(out, "unk8" | uint16L) :: ("unk8" | conditional(out, uint16L)) ::
conditional(out, "unk9" | bool) ("unk9" | conditional(out, bool))
}) })
).exmap[SquadStateInfo]( ).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 * @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( final case class CharacterAppearanceB(
unk0: Long, outfit_id: Long,
outfit_name: String, outfit_name: String,
outfit_logo: Int, outfit_logo: Int,
unk1: Boolean, unk1: Boolean,
@ -394,7 +394,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
*/ */
def b_codec(alt_model: Boolean, name_padding: Int): Codec[CharacterAppearanceB] = 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_name" | PacketHelpers.encodedWideStringAligned(outfitNamePadding)) ::
("outfit_logo" | uint8L) :: ("outfit_logo" | uint8L) ::
("unk1" | bool) :: //unknown ("unk1" | bool) :: //unknown
@ -414,12 +414,12 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
optional(bool, "on_zipline" | zipline_codec) optional(bool, "on_zipline" | zipline_codec)
).exmap[CharacterAppearanceB]( ).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 lfsBool = if (lfs == 0) false else true
val bpackBool = bpack match { case Some(_) => alt_model; case None => false } val bpackBool = bpack match { case Some(_) => alt_model; case None => false }
Attempt.successful( Attempt.successful(
CharacterAppearanceB( CharacterAppearanceB(
u0, outfit_id,
outfit, outfit,
logo, logo,
u1, u1,
@ -442,7 +442,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
}, },
{ {
case CharacterAppearanceB( case CharacterAppearanceB(
u0, outfit_id,
outfit, outfit,
logo, logo,
u1, u1,
@ -461,10 +461,10 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
u7, u7,
zipline zipline
) => ) =>
val u0Long = if (u0 == 0 && outfit.nonEmpty) { val u0Long = if (outfit_id == 0 && outfit.nonEmpty) {
outfit.length.toLong outfit.length.toLong
} else { } else {
u0 outfit_id
} //TODO this is a kludge; unk0 must be (some) non-zero if outfit_name is defined } //TODO this is a kludge; unk0 must be (some) non-zero if outfit_name is defined
val (bpackOpt, zipOpt) = if (alt_model) { val (bpackOpt, zipOpt) = if (alt_model) {
val bpackOpt = if (bpack) { Some(true) } 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 package net.psforever.services.teamwork
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.avatar.Certification
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{WaypointEventAction, WaypointInfo, SquadAction => PacketSquadAction} import net.psforever.packet.game.{WaypointEventAction, WaypointInfo, SquadAction => PacketSquadAction}
import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoint, Vector3} import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoint, Vector3}
@ -16,8 +17,9 @@ object SquadServiceMessage {
object SquadAction { object SquadAction {
sealed trait Action sealed trait Action
final case class InitSquadList() extends Action final case class InitSquadList() extends Action
final case class InitCharId() 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 Definition(guid: PlanetSideGUID, line: Int, action: PacketSquadAction) extends Action
final case class Membership( final case class Membership(
@ -35,10 +37,12 @@ object SquadAction {
) extends Action ) extends Action
final case class Update( final case class Update(
char_id: Long, char_id: Long,
guid: PlanetSideGUID,
health: Int, health: Int,
max_health: Int, max_health: Int,
armor: Int, armor: Int,
max_armor: Int, max_armor: Int,
certifications: Set[Certification],
pos: Vector3, pos: Vector3,
zone_number: Int zone_number: Int
) extends Action ) extends Action

View file

@ -1,6 +1,8 @@
// Copyright (c) 2019 PSForever // Copyright (c) 2019 PSForever
package net.psforever.services.teamwork package net.psforever.services.teamwork
import akka.actor.ActorRef
import net.psforever.objects.avatar.Certification
import net.psforever.objects.teamwork.Squad import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo} import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo}
import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoint} 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 UpdateList(infos: Iterable[(Int, SquadInfo)]) extends Response
final case class RemoveFromList(infos: Iterable[Int]) 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 SetListSquad(squad_guid: PlanetSideGUID) extends Response
final case class Membership( final case class Membership(
@ -40,11 +42,11 @@ object SquadResponse {
unk6: Option[Option[String]] unk6: Option[Option[String]]
) extends Response //see SquadMembershipResponse ) extends Response //see SquadMembershipResponse
final case class WantsSquadPosition(leader_char_id: Long, bid_name: String) extends Response 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 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 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 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 final case class Detail(guid: PlanetSideGUID, squad_detail: SquadDetail) extends Response
@ -59,5 +61,16 @@ object SquadResponse {
unk: Int unk: Int
) extends Response ) 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 net.psforever.packet.PacketHelpers
import scodec.codecs._ import scodec.codecs._
//unk01 is some sort of reply to ProximityInvite
object SquadResponseType extends Enumeration { object SquadResponseType extends Enumeration {
type Type = Value 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 PlatoonLeave, PlatoonDisband = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) 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, * 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. * 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. * 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. * YMMV.
* @see `SquadWaypointEvent` * @see `SquadWaypointEvent`
* @see `SquadWaypointRequest` * @see `SquadWaypointRequest`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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