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

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

View file

@ -1,17 +1,17 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.avatar.DeployableToolbox
import net.psforever.objects.avatar.{DeployableToolbox, LoadoutManager}
import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition}
import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot}
import net.psforever.objects.loadouts.Loadout
import net.psforever.packet.game.objectcreate.Cosmetics
import net.psforever.types._
import scala.annotation.tailrec
import scala.collection.mutable
class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : CharacterVoice.Value) {
class Avatar(private val char_id : Long, val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : CharacterVoice.Value) {
/** char_id, Character ID; a unique identifier corresponding to a database table row index */
/** Battle Experience Points */
private var bep : Long = 0
/** Command Experience Points */
@ -29,11 +29,15 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
* @see `DetailedCharacterData.implants`
*/
private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot)
/** Loadouts<br>
* 0-9 are Infantry loadouts
/** Equipment Loadouts<br>
* 0-9 are Infantry loadouts<br>
* 10-14 are Vehicle loadouts
*/
private val loadouts : Array[Option[Loadout]] = Array.fill[Option[Loadout]](15)(None)
private val equipmentLoadouts : LoadoutManager = new LoadoutManager(15)
/**
* Squad Loadouts
*/
private val squadLoadouts : LoadoutManager = new LoadoutManager(10)
/** Locker */
private val locker : LockerContainer = new LockerContainer() {
override def toString : String = {
@ -42,6 +46,14 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
}
private val deployables : DeployableToolbox = new DeployableToolbox
/**
* Looking For Squad:<br>
* Indicates both a player state and the text on the marquee under the player nameplate.
* Should only be valid when the player is not in a squad.
*/
private var lfs : Boolean = false
def CharId : Long = char_id
def BEP : Long = bep
@ -164,25 +176,9 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
})
}
def SaveLoadout(owner : Player, label : String, line : Int) : Unit = {
if(line > -1 && line < 10) {
loadouts(line) = Some(Loadout.Create(owner, label))
}
}
def EquipmentLoadouts : LoadoutManager = equipmentLoadouts
def SaveLoadout(owner : Vehicle, label : String, line : Int) : Unit = {
if(line > 9 && line < loadouts.length) {
loadouts(line) = Some(Loadout.Create(owner, label))
}
}
def LoadLoadout(line : Int) : Option[Loadout] = loadouts.lift(line).flatten
def DeleteLoadout(line : Int) : Unit = {
loadouts(line) = None
}
def Loadouts : Seq[(Int, Loadout)] = loadouts.zipWithIndex.collect { case(Some(loadout), index) => (index, loadout) } toSeq
def SquadLoadouts : LoadoutManager = squadLoadouts
def Locker : LockerContainer = locker
@ -194,6 +190,13 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
def Deployables : DeployableToolbox = deployables
def LFS : Boolean = lfs
def LFS_=(looking : Boolean) : Boolean = {
lfs = looking
LFS
}
def Definition : AvatarDefinition = GlobalDefinitions.avatar
/*
@ -228,7 +231,7 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
object Avatar {
def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : CharacterVoice.Value) : Avatar = {
new Avatar(name, faction, sex, head, voice)
new Avatar(0L, name, faction, sex, head, voice)
}
def toString(avatar : Avatar) : String = s"${avatar.faction} ${avatar.name}"

View file

@ -1,10 +1,10 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import net.psforever.objects.avatar.LoadoutManager
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
import net.psforever.objects.loadouts.Loadout
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
@ -58,6 +58,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
Player.SuitSetup(this, exosuit)
def CharId : Long = core.CharId
def Name : String = core.name
def Faction : PlanetSideEmpire.Value = core.faction
@ -68,6 +70,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
def Voice : CharacterVoice.Value = core.voice
def LFS : Boolean = core.LFS
def isAlive : Boolean = alive
def isBackpack : Boolean = backpack
@ -294,7 +298,9 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
def RadiationShielding = exosuit.RadiationShielding
def LoadLoadout(line : Int) : Option[Loadout] = core.LoadLoadout(line)
def EquipmentLoadouts : LoadoutManager = core.EquipmentLoadouts
def SquadLoadouts : LoadoutManager = core.SquadLoadouts
def BEP : Long = core.BEP

View file

@ -0,0 +1,29 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects.avatar
import net.psforever.objects.loadouts.Loadout
import net.psforever.types.LoadoutType
import scala.util.Success
class LoadoutManager(size : Int) {
private val entries : Array[Option[Loadout]] = Array.fill[Option[Loadout]](size)(None)
def SaveLoadout(owner : Any, label : String, line : Int) : Unit = {
Loadout.Create(owner, label) match {
case Success(loadout) if entries.length > line =>
entries(line) = Some(loadout)
case _ => ;
}
}
def LoadLoadout(line : Int) : Option[Loadout] = entries.lift(line).flatten
def DeleteLoadout(line : Int) : Unit = {
if(entries.length > line) {
entries(line) = None
}
}
def Loadouts : Seq[(Int, Loadout)] = entries.zipWithIndex.collect { case(Some(loadout), index) => (index, loadout) } toSeq
}

View file

@ -81,7 +81,7 @@ object AvatarConverter {
),
obj.ExoSuit,
0,
0L,
obj.CharId,
0,
0,
0,
@ -98,7 +98,7 @@ object AvatarConverter {
false,
facingPitch = obj.Orientation.y,
facingYawUpper = obj.FacingYawUpper,
lfs = true,
obj.LFS,
GrenadeState.None,
obj.Cloaked,
false,

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.loadouts
/**
* The base of all specific kinds of blueprint containers.
* This previous state can be restored on any appropriate template from which the loadout was copied
* by reconstructing any items (if warranted and permitted) or restoring any appropriate fields.
* The three fields are the name assigned to the loadout,
* the visible items that are created (which obey different rules depending on the source),
* and the concealed items that are created and added to the source's `Inventory`.<br>
* For example, the `visible_slots` on a `Player`-borne loadout will transform into the form `Array[EquipmentSlot]`;
* `Vehicle`-originating loadouts transform into the form `Map[Int, Equipment]`.
* <br>
* The lists of user-specific loadouts are initialized with `FavoritesMessage` packets.
* Specific entries are loaded or removed using `FavoritesRequest` packets.
* @param label the name by which this inventory will be known when displayed in a Favorites list
* @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target
* @param inventory simplified representation of the `Equipment` in the target's inventory or trunk
*/
abstract class EquipmentLoadout(label : String,
visible_slots : List[Loadout.SimplifiedEntry],
inventory : List[Loadout.SimplifiedEntry]) extends Loadout(label)

View file

@ -28,7 +28,7 @@ final case class InfantryLoadout(label : String,
visible_slots : List[Loadout.SimplifiedEntry],
inventory : List[Loadout.SimplifiedEntry],
exosuit : ExoSuitType.Value,
subtype : Int) extends Loadout(label, visible_slots, inventory)
subtype : Int) extends EquipmentLoadout(label, visible_slots, inventory)
object InfantryLoadout {
import net.psforever.objects.Player

View file

@ -5,30 +5,29 @@ import net.psforever.objects._
import net.psforever.objects.definition._
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.teamwork.Squad
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
/**
* The base of all specific kinds of blueprint containers.
* This previous state can be restored on any appropriate template from which the loadout was copied
* by reconstructing the items (if permitted).
* The three fields are the name assigned to the loadout,
* the visible items that are created (which obey different rules depending on the source),
* and the concealed items that are created and added to the source's `Inventory`.<br>
* For example, the `visible_slots` on a `Player`-borne loadout will transform into the form `Array[EquipmentSlot]`;
* `Vehicle`-originating loadouts transform into the form `Map[Int, Equipment]`.
* <br>
* The lists of user-specific loadouts are initialized with `FavoritesMessage` packets.
* Specific entries are loaded or removed using `FavoritesRequest` packets.
* by reconstructing any items (if warranted and permitted) or restoring any appropriate fields.
* @param label the name by which this inventory will be known when displayed in a Favorites list
* @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target
* @param inventory simplified representation of the `Equipment` in the target's inventory or trunk
*/
abstract class Loadout(label : String,
visible_slots : List[Loadout.SimplifiedEntry],
inventory : List[Loadout.SimplifiedEntry])
abstract class Loadout(label : String)
object Loadout {
def Create(owner : Any, label : String) : Try[Loadout] = {
owner match {
case p : Player => Success(Create(p, label))
case v : Vehicle => Success(Create(v, label))
case s : Squad => Success(Create(s, s.Task))
case _ => Failure(new MatchError(s"can not create a loadout based on the (current status of) $owner"))
}
}
/**
* Produce the blueprint on a player.
* @param player the player
@ -54,12 +53,31 @@ object Loadout {
def Create(vehicle : Vehicle, label : String) : Loadout = {
VehicleLoadout(
label,
packageSimplifications(vehicle.Weapons.map({ case ((index, weapon)) => InventoryItem(weapon.Equipment.get, index) }).toList),
packageSimplifications(vehicle.Weapons.map({ case (index, weapon) => InventoryItem(weapon.Equipment.get, index) }).toList),
packageSimplifications(vehicle.Trunk.Items),
vehicle.Definition
)
}
/**
* na
*/
def Create(squad : Squad, label : String) : Loadout = {
SquadLoadout(
label,
if(squad.CustomZoneId) { Some(squad.ZoneId) } else { None },
squad.Membership
.zipWithIndex
.filter { case (_, index) =>
squad.Availability(index)
}
.map {case (member, index) =>
SquadPositionLoadout(index, member.Role, member.Orders, member.Requirements)
}
.toList
)
}
/**
* A basic `Trait` connecting all of the `Equipment` blueprints.
*/

View file

@ -0,0 +1,10 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects.loadouts
import net.psforever.types.CertificationType
final case class SquadPositionLoadout(index : Int, role : String, orders : String, requirements : Set[CertificationType.Value])
final case class SquadLoadout(task : String,
zone_id : Option[Int],
members : List[SquadPositionLoadout]) extends Loadout(task)

View file

@ -24,4 +24,4 @@ import net.psforever.objects.definition._
final case class VehicleLoadout(label : String,
visible_slots : List[Loadout.SimplifiedEntry],
inventory : List[Loadout.SimplifiedEntry],
vehicle_definition : VehicleDefinition) extends Loadout(label, visible_slots, inventory)
vehicle_definition : VehicleDefinition) extends EquipmentLoadout(label, visible_slots, inventory)

View file

@ -258,7 +258,7 @@ object OrderTerminalDefinition {
//TODO block equipment by blocking ammunition type
final case class InfantryLoadoutPage() extends LoadoutTab {
override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
player.LoadLoadout(msg.unk1) match {
player.EquipmentLoadouts.LoadLoadout(msg.unk1) match {
case Some(loadout : InfantryLoadout) if !Exclude.contains(loadout.exosuit) && !Exclude.contains((loadout.exosuit, loadout.subtype)) =>
val holsters = loadout.visible_slots
.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) })
@ -287,7 +287,7 @@ object OrderTerminalDefinition {
*/
final case class VehicleLoadoutPage() extends LoadoutTab {
override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
player.LoadLoadout(msg.unk1 + 10) match {
player.EquipmentLoadouts.LoadLoadout(msg.unk1 + 10) match {
case Some(loadout : VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
val weapons = loadout.visible_slots
.map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) })

View file

@ -0,0 +1,89 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects.teamwork
import net.psforever.types.{CertificationType, Vector3}
class Member {
//about the position to be filled
private var role : String = ""
private var orders : String = ""
private var requirements : Set[CertificationType.Value] = Set()
//about the individual filling the position
private var name : String = ""
private var charId : Long = 0L
private var health : Int = 0
private var armor : Int = 0
private var zoneId : Int = 0
private var position : Vector3 = Vector3.Zero
def Role : String = role
def Role_=(title : String) : String = {
role = title
Role
}
def Orders : String = orders
def Orders_=(text : String) : String = {
orders = text
Orders
}
def Requirements : Set[CertificationType.Value] = requirements
def Requirements_=(req : Set[CertificationType.Value]) : Set[CertificationType.Value] = {
requirements = req
Requirements
}
def Name : String = name
def Name_=(moniker : String) : String = {
name = moniker
Name
}
def CharId : Long = charId
def CharId_=(id : Long) : Long = {
charId = id
CharId
}
def Health : Int = health
def Health_=(red : Int) : Int = {
health = red
Health
}
def Armor : Int = armor
def Armor_=(blue : Int) : Int = {
armor = blue
Armor
}
def ZoneId : Int = zoneId
def ZoneId_=(id : Int) : Int = {
zoneId = id
ZoneId
}
def Position : Vector3 = position
def Position_=(pos : Vector3) : Vector3 = {
position = pos
Position
}
def isAvailable : Boolean = {
charId == 0
}
def isAvailable(certs : Set[CertificationType.Value]) : Boolean = {
isAvailable && certs.intersect(requirements) == requirements
}
}

View file

@ -0,0 +1,85 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects.teamwork
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types.{CertificationType, PlanetSideEmpire}
class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extends IdentifiableEntity {
super.GUID_=(squadId)
private val faction : PlanetSideEmpire.Value = alignment //does not change
private var zoneId : Option[Int] = None
private var task : String = ""
private val membership : Array[Member] = Array.fill[Member](10)(new Member)
private val availability : Array[Boolean] = Array.fill[Boolean](10)(elem = true)
override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
def Faction : PlanetSideEmpire.Value = faction
def CustomZoneId : Boolean = zoneId.isDefined
def ZoneId : Int = zoneId.getOrElse(membership(0).ZoneId)
def ZoneId_=(id : Int) : Int = {
ZoneId_=(Some(id))
}
def ZoneId_=(id : Option[Int]) : Int = {
zoneId = id
ZoneId
}
def Task : String = task
def Task_=(assignment : String) : String = {
task = assignment
Task
}
def Membership : Array[Member] = membership
def Availability : Array[Boolean] = availability
def Leader : Member = {
membership(0) match {
case member if !member.Name.equals("") =>
member
case _ =>
throw new Exception("can not find squad leader!")
}
}
def Size : Int = membership.count(member => member.CharId != 0)
def Capacity : Int = availability.count(open => open)
def isAvailable(role : Int) : Boolean = {
availability.lift(role) match {
case Some(true) =>
membership(role).isAvailable
case _ =>
false
}
}
def isAvailable(role : Int, certs : Set[CertificationType.Value]) : Boolean = {
availability.lift(role) match {
case Some(true) =>
membership(role).isAvailable(certs)
case _ =>
false
}
}
}
object Squad {
final val Blank = new Squad(PlanetSideGUID(0), PlanetSideEmpire.NEUTRAL) {
override def ZoneId : Int = 0
override def ZoneId_=(id : Int) : Int = 0
override def ZoneId_=(id : Option[Int]) : Int = 0
override def Task_=(assignment : String) : String = ""
override def Membership : Array[Member] = Array.empty[Member]
override def Availability : Array[Boolean] = Array.fill[Boolean](10)(false)
}
}

View file

@ -0,0 +1,142 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects.teamwork
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.types.SquadWaypoints
import services.teamwork.SquadService.WaypointData
import services.teamwork.SquadSwitchboard
class SquadFeatures(val Squad : Squad) {
/**
* `initialAssociation` per squad is similar to "Does this squad want to recruit members?"
* The squad does not have to be flagged.
* Dispatches an `AssociateWithSquad` `SquadDefinitionActionMessage` packet to the squad leader and ???
* and then a `SquadDetailDefinitionUpdateMessage` that includes at least the squad owner name and char id.
* Dispatched only once when a squad is first listed
* or when the squad leader searches for recruits by proximity or for certain roles or by invite
* or when a spontaneous squad forms,
* whatever happens first.
* Additionally, the packets are also sent when the check is made when the continent is changed (or set).
*/
private var initialAssociation : Boolean = true
/**
* na
*/
private var switchboard : ActorRef = ActorRef.noSender
/**
* Waypoint data.
* The first four slots are used for squad waypoints.
* The fifth slot is used for the squad leader experience waypoint.<br>
* <br>
* All of the waypoints constantly exist as long as the squad to which they are attached exists.
* They are merely "activated" and "deactivated."
* When "activated," the waypoint knows on which continent to appear and where on the map and in the game world to be positioned.
* Waypoints manifest in the game world as a far-off beam of light that extends into the sky
* and whose ground contact utilizes a downwards pulsating arrow.
* On the continental map and deployment map, they appear as a diamond, with a differentiating number where applicable.
* The squad leader experience rally, for example, does not have a number like the preceding four waypoints.
* @see `Start`
*/
private var waypoints : Array[WaypointData] = Array[WaypointData]()
/**
* The particular position being recruited right at the moment.
* When `None`. no highlighted searches have been indicated.
* When a positive integer or 0, indicates distributed `LookingForSquadRoleInvite` messages as recorded by `proxyInvites`.
* Only one position may bne actively recruited at a time in this case.
* When -1, indicates distributed `ProximityIvite` messages as recorded by `proxyInvites`.
* Previous efforts may or may not be forgotten if there is a switch between the two modes.
*/
private var searchForRole : Option[Int] = None
/**
* Handle persistent data related to `ProximityInvite` and `LookingForSquadRoleInvite` messages
*/
private var proxyInvites : List[Long] = Nil
/**
* These useres rejected invitation to this squad.
* For the purposes of wide-searches for membership
* such as Looking For Squad checks and proximity invitation,
* the unique character identifier numbers in this list are skipped.
* Direct invitation requests from the non sqad member should remain functional.
*/
private var refusedPlayers : List[Long] = Nil
private var autoApproveInvitationRequests : Boolean = true
private var locationFollowsSquadLead : Boolean = true
private var listed : Boolean = false
private lazy val channel : String = s"${Squad.Faction}-Squad${Squad.GUID.guid}"
def Start(implicit context : ActorContext) : SquadFeatures = {
switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${Squad.GUID.guid}")
waypoints = Array.fill[WaypointData](SquadWaypoints.values.size)(new WaypointData())
this
}
def Stop : SquadFeatures = {
switchboard ! akka.actor.PoisonPill
switchboard = Actor.noSender
waypoints = Array.empty
this
}
def InitialAssociation : Boolean = initialAssociation
def InitialAssociation_=(assoc : Boolean) : Boolean = {
initialAssociation = assoc
InitialAssociation
}
def Switchboard : ActorRef = switchboard
def Waypoints : Array[WaypointData] = waypoints
def SearchForRole : Option[Int] = searchForRole
def SearchForRole_=(role : Int) : Option[Int] = SearchForRole_=(Some(role))
def SearchForRole_=(role : Option[Int]) : Option[Int] = {
searchForRole = role
SearchForRole
}
def ProxyInvites : List[Long] = proxyInvites
def ProxyInvites_=(list : List[Long]) : List[Long] = {
proxyInvites = list
ProxyInvites
}
def Refuse : List[Long] = refusedPlayers
def Refuse_=(charId : Long) : List[Long] = {
Refuse_=(List(charId))
}
def Refuse_=(list : List[Long]) : List[Long] = {
refusedPlayers = list ++ refusedPlayers
Refuse
}
def LocationFollowsSquadLead : Boolean = locationFollowsSquadLead
def LocationFollowsSquadLead_=(follow : Boolean) : Boolean = {
locationFollowsSquadLead = follow
LocationFollowsSquadLead
}
def AutoApproveInvitationRequests : Boolean = autoApproveInvitationRequests
def AutoApproveInvitationRequests_=(autoApprove : Boolean) : Boolean = {
autoApproveInvitationRequests = autoApprove
AutoApproveInvitationRequests
}
def Listed : Boolean = listed
def Listed_=(announce : Boolean) : Boolean = {
listed = announce
Listed
}
def ToChannel : String = channel
}

View file

@ -446,18 +446,18 @@ object GamePacketOpcode extends Enumeration {
case 0x6b => game.TriggerSoundMessage.decode
case 0x6c => game.LootItemMessage.decode
case 0x6d => game.VehicleSubStateMessage.decode
case 0x6e => noDecoder(SquadMembershipRequest)
case 0x6f => noDecoder(SquadMembershipResponse)
case 0x6e => game.SquadMembershipRequest.decode
case 0x6f => game.SquadMembershipResponse.decode
// OPCODES 0x70-7f
case 0x70 => noDecoder(SquadMemberEvent)
case 0x70 => game.SquadMemberEvent.decode
case 0x71 => noDecoder(PlatoonEvent)
case 0x72 => game.FriendsRequest.decode
case 0x73 => game.FriendsResponse.decode
case 0x74 => game.TriggerEnvironmentalDamageMessage.decode
case 0x75 => game.TrainingZoneMessage.decode
case 0x76 => game.DeployableObjectsInfoMessage.decode
case 0x77 => noDecoder(SquadState)
case 0x77 => game.SquadState.decode
// 0x78
case 0x78 => game.OxygenStateMessage.decode
case 0x79 => noDecoder(TradeMessage)
@ -472,7 +472,7 @@ object GamePacketOpcode extends Enumeration {
case 0x80 => noDecoder(GenericObjectAction2Message)
case 0x81 => game.DestroyDisplayMessage.decode
case 0x82 => noDecoder(TriggerBotAction)
case 0x83 => noDecoder(SquadWaypointRequest)
case 0x83 => game.SquadWaypointRequest.decode
case 0x84 => game.SquadWaypointEvent.decode
case 0x85 => noDecoder(OffshoreVehicleMessage)
case 0x86 => game.ObjectDeployedMessage.decode
@ -592,11 +592,11 @@ object GamePacketOpcode extends Enumeration {
case 0xe6 => game.ReplicationStreamMessage.decode
case 0xe7 => game.SquadDefinitionActionMessage.decode
// 0xe8
case 0xe8 => noDecoder(SquadDetailDefinitionUpdateMessage)
case 0xe8 => game.SquadDetailDefinitionUpdateMessage.decode
case 0xe9 => noDecoder(TacticsMessage)
case 0xea => noDecoder(RabbitUpdateMessage)
case 0xeb => noDecoder(SquadInvitationRequestMessage)
case 0xec => noDecoder(CharacterKnowledgeMessage)
case 0xeb => game.SquadInvitationRequestMessage.decode
case 0xec => game.CharacterKnowledgeMessage.decode
case 0xed => noDecoder(GameScoreUpdateMessage)
case 0xee => noDecoder(UnknownMessage238)
case 0xef => noDecoder(OrderTerminalBugMessage)

View file

@ -0,0 +1,57 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.CertificationType
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
final case class CharacterKnowledgeInfo(name : String,
permissions : Set[CertificationType.Value],
unk1 : Int,
unk2 : Int,
unk3 : PlanetSideGUID)
final case class CharacterKnowledgeMessage(char_id : Long,
info : Option[CharacterKnowledgeInfo])
extends PlanetSideGamePacket {
type Packet = CharacterKnowledgeMessage
def opcode = GamePacketOpcode.CharacterKnowledgeMessage
def encode = CharacterKnowledgeMessage.encode(this)
}
object CharacterKnowledgeMessage extends Marshallable[CharacterKnowledgeMessage] {
def apply(char_id : Long) : CharacterKnowledgeMessage =
CharacterKnowledgeMessage(char_id, None)
def apply(char_id : Long, info : CharacterKnowledgeInfo) : CharacterKnowledgeMessage =
CharacterKnowledgeMessage(char_id, Some(info))
private val inverter : Codec[Boolean] = bool.xmap[Boolean] (
state => !state,
state => !state
)
private val info_codec : Codec[CharacterKnowledgeInfo] = (
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) ::
("permissions" | ulongL(bits = 46)) ::
("unk1" | uint(bits = 6)) ::
("unk2" | uint(bits = 3)) ::
("unk3" | PlanetSideGUID.codec)
).xmap[CharacterKnowledgeInfo] (
{
case name :: permissions :: u1 :: u2 :: u3 :: HNil =>
CharacterKnowledgeInfo(name, CertificationType.fromEncodedLong(permissions), u1, u2, u3)
},
{
case CharacterKnowledgeInfo(name, permissions, u1, u2, u3) =>
name :: CertificationType.toEncodedLong(permissions) :: u1 :: u2 :: u3 :: HNil
}
)
implicit val codec : Codec[CharacterKnowledgeMessage] = (
("char_id" | uint32L) ::
("info" | optional(inverter, info_codec))
).as[CharacterKnowledgeMessage]
}

View file

@ -118,8 +118,12 @@ import scodec.codecs._
* `27 - PA_JAMMED - plays jammed buzzing sound`<br>
* `28 - PA_IMPLANT_ACTIVE - Plays implant sounds. Valid values seem to be up to 20.`<br>
* `29 - PA_VAPORIZED - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`<br>
* `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`<br>
* `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`<br>
* `31 - Looking for Squad info (marquee and ui):<br>
* ` - 0 is LFS`<br>
* ` - 1 is LFSM (Looking for Squad Members)`<br>
* ` - n is the supplemental squad identifier number; same as "LFS;" for the leader, sets "LFSM" after the first manual flagging`<br>
* `32 - Maintain the squad role index, when a member of a squad;<br>
* - OLD: "Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`"<br>
* `35 - BR. Value is the BR`<br>
* `36 - CR. Value is the CR`<br>
* `43 - Info on avatar name : 0 = Nothing, 1 = "(LD)" message`<br>

View file

@ -2,26 +2,375 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.CertificationType
import scodec.bits.BitVector
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* The generic superclass of a specific behavior for this type of squad definition action.
* All behaviors have a "code" that indicates how the rest of the data is parsed.
* @param code the action behavior code
*/
abstract class SquadAction(val code : Int)
object SquadAction{
object SearchMode extends Enumeration {
type Type = Value
val
AnyPositions,
AvailablePositions,
SomeCertifications,
AllCertifications
= Value
implicit val codec : Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
}
final case class DisplaySquad() extends SquadAction(0)
/**
* Dispatched from client to server to indicate a squad detail update that has no foundation entry to update?
* Not dissimilar from `DisplaySquad`.
*/
final case class SquadMemberInitializationIssue() extends SquadAction(1)
final case class SaveSquadFavorite() extends SquadAction(3)
final case class LoadSquadFavorite() extends SquadAction(4)
final case class DeleteSquadFavorite() extends SquadAction(5)
final case class ListSquadFavorite(name : String) extends SquadAction(7)
final case class RequestListSquad() extends SquadAction(8)
final case class StopListSquad() extends SquadAction(9)
final case class SelectRoleForYourself(state : Int) extends SquadAction(10)
final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15)
final case class AssociateWithSquad() extends SquadAction(16)
final case class SetListSquad() extends SquadAction(17)
final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19)
final case class ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20)
final case class CloseSquadMemberPosition(position : Int) extends SquadAction(21)
final case class AddSquadMemberPosition(position : Int) extends SquadAction(22)
final case class ChangeSquadMemberRequirementsRole(u1 : Int, role : String) extends SquadAction(23)
final case class ChangeSquadMemberRequirementsDetailedOrders(u1 : Int, orders : String) extends SquadAction(24)
final case class ChangeSquadMemberRequirementsCertifications(u1 : Int, certs : Set[CertificationType.Value]) extends SquadAction(25)
final case class ResetAll() extends SquadAction(26)
final case class AutoApproveInvitationRequests(state : Boolean) extends SquadAction(28)
final case class LocationFollowsSquadLead(state : Boolean) extends SquadAction(31)
final case class SearchForSquadsWithParticularRole(role: String, requirements : Set[CertificationType.Value], zone_id: Int, mode : SearchMode.Value) extends SquadAction(34)
final case class CancelSquadSearch() extends SquadAction(35)
final case class AssignSquadMemberToRole(position : Int, char_id : Long) extends SquadAction(38)
final case class NoSquadSearchResults() extends SquadAction(39)
final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40)
final case class CancelFind() extends SquadAction(41)
final case class Unknown(badCode : Int, data : BitVector) extends SquadAction(badCode)
object Unknown {
import scodec.bits._
val StandardBits : BitVector = hex"00".toBitVector.take(6)
def apply(badCode : Int) : Unknown = Unknown(badCode, StandardBits)
}
/**
* The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream.
*/
object Codecs {
private val everFailCondition = conditional(included = false, bool)
val displaySquadCodec = everFailCondition.xmap[DisplaySquad] (
_ => DisplaySquad(),
{
case DisplaySquad() => None
}
)
val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadMemberInitializationIssue] (
_ => SquadMemberInitializationIssue(),
{
case SquadMemberInitializationIssue() => None
}
)
val saveSquadFavoriteCodec = everFailCondition.xmap[SaveSquadFavorite] (
_ => SaveSquadFavorite(),
{
case SaveSquadFavorite() => None
}
)
val loadSquadFavoriteCodec = everFailCondition.xmap[LoadSquadFavorite] (
_ => LoadSquadFavorite(),
{
case LoadSquadFavorite() => None
}
)
val deleteSquadFavoriteCodec = everFailCondition.xmap[DeleteSquadFavorite] (
_ => DeleteSquadFavorite(),
{
case DeleteSquadFavorite() => None
}
)
val listSquadFavoriteCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadFavorite] (
text => ListSquadFavorite(text),
{
case ListSquadFavorite(text) => text
}
)
val requestListSquadCodec = everFailCondition.xmap[RequestListSquad] (
_ => RequestListSquad(),
{
case RequestListSquad() => None
}
)
val stopListSquadCodec = everFailCondition.xmap[StopListSquad] (
_ => StopListSquad(),
{
case StopListSquad() => None
}
)
val selectRoleForYourselfCodec = uint4.xmap[SelectRoleForYourself] (
value => SelectRoleForYourself(value),
{
case SelectRoleForYourself(value) => value
}
)
val cancelSelectRoleForYourselfCodec = uint32.xmap[CancelSelectRoleForYourself] (
value => CancelSelectRoleForYourself(value),
{
case CancelSelectRoleForYourself(value) => value
}
)
val associateWithSquadCodec = everFailCondition.xmap[AssociateWithSquad] (
_ => AssociateWithSquad(),
{
case AssociateWithSquad() => None
}
)
val setListSquadCodec = everFailCondition.xmap[SetListSquad] (
_ => SetListSquad(),
{
case SetListSquad() => None
}
)
val changeSquadPurposeCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ChangeSquadPurpose] (
purpose => ChangeSquadPurpose(purpose),
{
case ChangeSquadPurpose(purpose) => purpose
}
)
val changeSquadZoneCodec = uint16L.xmap[ChangeSquadZone] (
value => ChangeSquadZone(PlanetSideZoneID(value)),
{
case ChangeSquadZone(value) => value.zoneId.toInt
}
)
val closeSquadMemberPositionCodec = uint4.xmap[CloseSquadMemberPosition] (
position => CloseSquadMemberPosition(position),
{
case CloseSquadMemberPosition(position) => position
}
)
val addSquadMemberPositionCodec = uint4.xmap[AddSquadMemberPosition] (
position => AddSquadMemberPosition(position),
{
case AddSquadMemberPosition(position) => position
}
)
val changeSquadMemberRequirementsRoleCodec = (uint4L :: PacketHelpers.encodedWideStringAligned(2)).xmap[ChangeSquadMemberRequirementsRole] (
{
case u1 :: role :: HNil => ChangeSquadMemberRequirementsRole(u1, role)
},
{
case ChangeSquadMemberRequirementsRole(u1, role) => u1 :: role :: HNil
}
)
val changeSquadMemberRequirementsDetailedOrdersCodec = (uint4L :: PacketHelpers.encodedWideStringAligned(2)).xmap[ChangeSquadMemberRequirementsDetailedOrders] (
{
case u1 :: role :: HNil => ChangeSquadMemberRequirementsDetailedOrders(u1, role)
},
{
case ChangeSquadMemberRequirementsDetailedOrders(u1, role) => u1 :: role :: HNil
}
)
val changeSquadMemberRequirementsCertificationsCodec = (uint4 :: ulongL(46)).xmap[ChangeSquadMemberRequirementsCertifications] (
{
case u1 :: u2 :: HNil =>
ChangeSquadMemberRequirementsCertifications(u1, CertificationType.fromEncodedLong(u2))
},
{
case ChangeSquadMemberRequirementsCertifications(u1, u2) =>
u1 :: CertificationType.toEncodedLong(u2) :: HNil
}
)
val resetAllCodec = everFailCondition.xmap[ResetAll] (
_ => ResetAll(),
{
case ResetAll() => None
}
)
val autoApproveInvitationRequestsCodec = bool.xmap[AutoApproveInvitationRequests] (
state => AutoApproveInvitationRequests(state),
{
case AutoApproveInvitationRequests(state) => state
}
)
val locationFollowsSquadLeadCodec = bool.xmap[LocationFollowsSquadLead] (
state => LocationFollowsSquadLead(state),
{
case LocationFollowsSquadLead(state) => state
}
)
val searchForSquadsWithParticularRoleCodec = (
PacketHelpers.encodedWideStringAligned(6) ::
ulongL(46) ::
uint16L ::
SearchMode.codec).xmap[SearchForSquadsWithParticularRole] (
{
case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, CertificationType.fromEncodedLong(u2), u3, u4)
},
{
case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: CertificationType.toEncodedLong(u2) :: u3 :: u4 :: HNil
}
)
val cancelSquadSearchCodec = everFailCondition.xmap[CancelSquadSearch] (
_ => CancelSquadSearch(),
{
case CancelSquadSearch() => None
}
)
val assignSquadMemberToRoleCodec = (uint4 :: uint32L).xmap[AssignSquadMemberToRole] (
{
case u1 :: u2 :: HNil => AssignSquadMemberToRole(u1, u2)
},
{
case AssignSquadMemberToRole(u1, u2) => u1 :: u2 :: HNil
}
)
val noSquadSearchResultsCodec = everFailCondition.xmap[NoSquadSearchResults] (
_ => NoSquadSearchResults(),
{
case NoSquadSearchResults() => None
}
)
val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] (
state => FindLfsSoldiersForRole(state),
{
case FindLfsSoldiersForRole(state) => state
}
)
val cancelFindCodec = everFailCondition.xmap[CancelFind] (
_ => CancelFind(),
{
case CancelFind() => None
}
)
/**
* A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
* @param action the action behavior code
* @return a transformation between the action code and the unknown bit data
*/
def unknownCodec(action : Int) = bits.xmap[Unknown] (
data => Unknown(action, data),
{
case Unknown(_, data) => data
}
)
/**
* The action code was completely unanticipated!
* @param action the action behavior code
* @return nothing; always fail
*/
def failureCodec(action : Int)= everFailCondition.exmap[SquadAction] (
_ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
_ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
)
}
}
/**
* Manage composition and details of a player's current squad, or the currently-viewed squad.<br>
* <br>
* The `action` code indicates the format of the remainder data in the packet.
* The following formats are translated; their purposes are listed:<br>
* &nbsp;&nbsp;`(None)`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition
* &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad
* &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All
* &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search
* &nbsp;&nbsp;&nbsp;&nbsp;`41` - Cancel Find
* &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - Answer Squad Join Request<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`5 ` - Delete Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - Request List Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - Stop List Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`16` - Associate with Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`17` - Set List Squad (ui)<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`32` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`39` - No Squad Search Results<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`41` - Cancel Find<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`42` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`43` - UNKNOWN<br>
* &nbsp;&nbsp;`Boolean`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`28` - Auto-approve Requests for Invitation<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`29` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`30` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`31` - Location Follows Squad Lead<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`31` - Location Follows Squad Leader<br>
* &nbsp;&nbsp;`Int`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`10` - Select this Role for Yourself<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`11` - UNKNOWN<br>
@ -33,49 +382,30 @@ import shapeless.{::, HNil}
* &nbsp;&nbsp;`Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`13` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`14` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`15` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`15` - Select this Role for Yourself<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`37` - UNKNOWN<br>
* &nbsp;&nbsp;`String`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`19` - (Squad leader) Change Squad Purpose<br>
* &nbsp;&nbsp;`Int :: Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`12` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`25` - (Squad leader) Change Squad Member Requirements - Weapons<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`38` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`38` - Assign Squad Member To Role<br>
* &nbsp;&nbsp;`Int :: String`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`23` - (Squad leader) Change Squad Member Requirements - Role<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`24` - (Squad leader) Change Squad Member Requirements - Detailed Orders<br>
* &nbsp;&nbsp;`Long :: Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`36` - UNKNOWN<br>
* &nbsp;&nbsp;`String :: Long :: Int :: Int`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`34` - Search for Squads with a Particular Role<br>
* <br>
* Exploration:<br>
* Some notes regarding the full list of action codes follows after this packet.
* Asides from codes whose behaviors are unknown, some codes also have unknown data format.
* No information for codes 1, 5, 9, 27, or 35 has been found yet.
* &nbsp;&nbsp;&nbsp;&nbsp;`34` - Search for Squads with a Particular Role
* @param squad_guid the unique identifier of the squad, if non-zero
* @param line the original listing line number, if applicable
* @param action the purpose of this packet;
* also decides the content of the parameter fields
* @param unk1 na
* @param unk2 na
* @param string_opt the optional `String` parameter
* @param int1_opt the first optional `Int` parameter;
* will not necessarily conform to a single bit length
* @param int2_opt the second optional `Int` parameter
* @param long1_opt the first optional `Long` parameter;
* will not necessarily conform to a single bit length
* @param long2_opt the second optional `Long` parameter
* @param bool_opt the optional `Boolean` parameter
*/
final case class SquadDefinitionActionMessage(action : Int,
unk1 : Int,
unk2 : Int,
string_opt : Option[String],
int1_opt : Option[Int],
int2_opt : Option[Int],
long1_opt : Option[Long],
long2_opt : Option[Long],
bool_opt : Option[Boolean])
final case class SquadDefinitionActionMessage(squad_guid : PlanetSideGUID,
line : Int,
action : SquadAction)
extends PlanetSideGamePacket {
type Packet = SquadDefinitionActionMessage
def opcode = GamePacketOpcode.SquadDefinitionActionMessage
@ -84,309 +414,78 @@ final case class SquadDefinitionActionMessage(action : Int,
object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMessage] {
/**
* Common pattern for the parameters, with enough fields to support all possible outputs.
* All fields are `Option`al purposefully.
* Use the action code to transform between
* the specific action object and its field data
* and the stream of bits of the original packet.
* @param code the action behavior code
* @return the `SquadAction` `Codec` to use for the given `code`
*/
private type allPattern = Option[String] :: Option[Int] :: Option[Int] :: Option[Long] :: Option[Long] :: Option[Boolean] :: HNil
/**
* `Codec` for reading nothing from the remainder of the stream data.
* @return a filled-out `allPattern` if successful
*/
def noneCodec : Codec[allPattern] = ignore(0).xmap[allPattern] (
{
case () =>
None :: None :: None :: None :: None :: None :: HNil
},
{
case _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
()
}
)
/**
* `Codec` for reading a single `Boolean` from remaining stream data.
* @return a filled-out `allPattern` if successful
*/
def boolCodec : Codec[allPattern] = bool.hlist.exmap[allPattern] (
{
case n :: HNil =>
Attempt.successful(None :: None :: None :: None :: None :: Some(n) :: HNil)
},
{
case _ :: _ :: _ :: _ :: _ :: None :: HNil =>
Attempt.failure(Err("expected a boolean value but found nothing"))
case _ :: _ :: _ :: _ :: _ :: Some(n) :: HNil =>
Attempt.successful(n :: HNil)
}
)
/**
* `Codec` for reading a single `Int` from remaining stream data.
* Multiple bit lengths can be processed from this reading.
* @param icodec the `Codec[Int]` read by this method
* @return a filled-out `allPattern` if successful
*/
def intCodec(icodec : Codec[Int]) : Codec[allPattern] = icodec.hlist.exmap[allPattern] (
{
case n :: HNil =>
Attempt.successful(None :: Some(n) :: None :: None :: None :: None :: HNil)
},
{
case _ :: None :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("expected an integer value but found nothing"))
case _ :: Some(n) :: _ :: _ :: _ :: _ :: HNil =>
Attempt.successful(n :: HNil)
}
)
/**
* `Codec` for reading a single `Long` from remaining stream data.
* @return a filled-out `allPattern` if successful
*/
def longCodec : Codec[allPattern] = uint32L.hlist.exmap[allPattern] (
{
case n :: HNil =>
Attempt.successful(None :: None :: None :: Some(n) :: None :: None :: HNil)
},
{
case _ :: _ :: _ :: None :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a long value but found nothing"))
case _ :: _ :: _ :: Some(n) :: _ :: _ :: HNil =>
Attempt.successful(n :: HNil)
}
)
/**
* `Codec` for reading a `String` from remaining stream data.
* All `String`s processed by this reading are wide character and are padded by six.
* @return a filled-out `allPattern` if successful
*/
def stringCodec : Codec[allPattern] = PacketHelpers.encodedWideStringAligned(6).hlist.exmap[allPattern] (
{
case a :: HNil =>
Attempt.successful(Some(a) :: None :: None :: None :: None :: None :: HNil)
},
{
case None:: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a string value but found nothing"))
case Some(a) :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.successful(a :: HNil)
}
)
/**
* `Codec` for reading an `Int` followed by a `Long` from remaining stream data.
* Multiple bit lengths can be processed for the `Long1` value from this reading.
* @param lcodec the `Codec[Long]` read by this method
* @return a filled-out `allPattern` if successful
*/
def intLongCodec(lcodec : Codec[Long]) : Codec[allPattern] = (
uint4L ::
lcodec
).exmap[allPattern] (
{
case a :: b :: HNil =>
Attempt.successful(None :: Some(a) :: None :: Some(b) :: None :: None :: HNil)
},
{
case _ :: None :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a integer value but found nothing"))
case _ :: _ :: _ :: None :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a long value but found nothing"))
case _ :: Some(a) :: _ :: Some(b) :: _ :: _ :: HNil =>
Attempt.successful(a :: b :: HNil)
}
)
/**
* `Codec` for reading an `Int` followed by a `String` from remaining stream data.
* All `String`s processed by this reading are wide character and are padded by two.
* @return a filled-out `allPattern` if successful
*/
def intStringCodec : Codec[allPattern] = (
uint4L ::
PacketHelpers.encodedWideStringAligned(2)
).exmap[allPattern] (
{
case a :: b :: HNil =>
Attempt.successful(Some(b) :: Some(a) :: None :: None :: None :: None :: HNil)
},
{
case None:: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a string value but found nothing"))
case _ :: None :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("expected an integer value but found nothing"))
case Some(b) :: Some(a) :: _ :: _ :: _ :: _ :: HNil =>
Attempt.successful(a :: b :: HNil)
}
)
/**
* `Codec` for reading two `Long`s from remaining stream data.
* @return a filled-out `allPattern` if successful
*/
def longLongCodec : Codec[allPattern] = (
ulongL(46) ::
uint32L
).exmap[allPattern] (
{
case a :: b :: HNil =>
Attempt.successful(None :: None :: None :: Some(a) :: Some(b) :: None :: HNil)
},
{
case (_ :: _ :: _ :: None :: _ :: _ :: HNil) | (_ :: _ :: _ :: _ :: None :: _ :: HNil) =>
Attempt.failure(Err("expected two long values but found one"))
case _ :: _ :: _ :: Some(a) :: Some(b) :: _ :: HNil =>
Attempt.successful(a :: b :: HNil)
}
)
/**
* `Codec` for reading a `String`, a `Long`, and two `Int`s from remaining stream data.
* All `String`s processed by this reading are wide character and are padded by six.
* @return a filled-out `allPattern` if successful
*/
def complexCodec : Codec[allPattern] = (
PacketHelpers.encodedWideStringAligned(6) ::
ulongL(46) ::
uint16L ::
uintL(3)
).exmap[allPattern] (
{
case a :: b :: c :: d :: HNil =>
Attempt.successful(Some(a) :: Some(c) :: Some(d) :: Some(b) :: None :: None :: HNil)
},
{
case None:: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a string value but found nothing"))
case _ :: _ :: _ :: None :: _ :: _ :: HNil =>
Attempt.failure(Err("expected a long value but found nothing"))
case (_ :: None :: _ :: _ :: _ :: _ :: HNil) | (_ :: _ :: None :: _ :: _ :: _ :: HNil) =>
Attempt.failure(Err("expected two integer values but found one"))
case Some(a) :: Some(c) :: Some(d) :: Some(b) :: _ :: _ :: HNil =>
Attempt.successful(a :: b :: c :: d :: HNil)
}
)
import scala.annotation.switch
/**
* Select the `Codec` to translate bit data in this packet with an `allPattern` format.
* @param action the purpose of this packet;
* also decides the content of the parameter fields
* @return an `allPattern` `Codec` that parses the appropriate data
*/
def selectCodec(action : Int) : Codec[allPattern] = (action : @switch) match {
case 3 | 8 | 26 | 35 | 41 => //TODO double check these
noneCodec
case 28 | 29 | 30 | 31 =>
boolCodec
case 33 =>
intCodec(uintL(3))
case 10 | 11 | 21 | 22 | 40 =>
intCodec(uint4L)
case 20 =>
intCodec(uint16L)
case 13 | 14 | 15 | 37 =>
longCodec
case 7 | 19 =>
stringCodec
case 12 | 38 =>
intLongCodec(uint32L)
case 25 =>
intLongCodec(ulongL(46))
case 23 | 24 =>
intStringCodec
case 36 =>
longLongCodec
case 34 =>
complexCodec
case _ =>
//TODO for debugging purposes only; normal failure condition below
bits.hlist.exmap[allPattern] (
{
case x :: HNil =>
org.log4s.getLogger.warn(s"can not match a codec pattern for decoding $action")
Attempt.successful(Some(x.toString) :: None :: None :: None :: None :: None :: HNil)
},
{
case Some(x) :: None :: None :: None :: None :: None :: HNil =>
org.log4s.getLogger.warn(s"can not match a codec pattern for encoding $action")
Attempt.successful(scodec.bits.BitVector.fromValidBin(x) :: HNil)
}
)
// ignore(0).exmap[allPattern] (
// {
// case () =>
// Attempt.failure(Err(s"can not match a codec pattern for decoding $action"))
// },
// {
// case _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
// Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
// }
// )
def selectFromActionCode(code : Int) : Codec[SquadAction] = {
import SquadAction.Codecs._
import scala.annotation.switch
((code : @switch) match {
case 0 => displaySquadCodec
case 1 => squadMemberInitializationIssueCodec
case 2 => unknownCodec(action = 2)
case 3 => saveSquadFavoriteCodec
case 4 => loadSquadFavoriteCodec
case 5 => deleteSquadFavoriteCodec
case 6 => unknownCodec(action = 6)
case 7 => listSquadFavoriteCodec
case 8 => requestListSquadCodec
case 9 => stopListSquadCodec
case 10 => selectRoleForYourselfCodec
case 11 => unknownCodec(action = 11)
case 12 => unknownCodec(action = 12)
case 13 => unknownCodec(action = 13)
case 14 => unknownCodec(action = 14)
case 15 => cancelSelectRoleForYourselfCodec
case 16 => associateWithSquadCodec
case 17 => setListSquadCodec
case 18 => unknownCodec(action = 18)
case 19 => changeSquadPurposeCodec
case 20 => changeSquadZoneCodec
case 21 => closeSquadMemberPositionCodec
case 22 => addSquadMemberPositionCodec
case 23 => changeSquadMemberRequirementsRoleCodec
case 24 => changeSquadMemberRequirementsDetailedOrdersCodec
case 25 => changeSquadMemberRequirementsCertificationsCodec
case 26 => resetAllCodec
//case 27 => ?
case 28 => autoApproveInvitationRequestsCodec
case 29 => unknownCodec(action = 29)
case 30 => unknownCodec(action = 30)
case 31 => locationFollowsSquadLeadCodec
case 32 => unknownCodec(action = 32)
case 33 => unknownCodec(action = 33)
case 34 => searchForSquadsWithParticularRoleCodec
case 35 => cancelSquadSearchCodec
case 36 => unknownCodec(action = 36)
case 37 => unknownCodec(action = 37)
case 38 => assignSquadMemberToRoleCodec
case 39 => noSquadSearchResultsCodec
case 40 => findLfsSoldiersForRoleCodec
case 41 => cancelFindCodec
case 42 => unknownCodec(action = 42)
case 43 => unknownCodec(action = 43)
case _ => failureCodec(code)
}).asInstanceOf[Codec[SquadAction]]
}
implicit val codec : Codec[SquadDefinitionActionMessage] = (
("action" | uintL(6)) >>:~ { action =>
("unk1" | uint16L) ::
("unk2" | uint4L) ::
selectCodec(action)
uintL(6) >>:~ { code =>
("squad_guid" | PlanetSideGUID.codec) ::
("line" | uint4L) ::
("action" | selectFromActionCode(code))
}
).as[SquadDefinitionActionMessage]
).xmap[SquadDefinitionActionMessage] (
{
case _ :: guid :: line :: action :: HNil =>
SquadDefinitionActionMessage(guid, line, action)
},
{
case SquadDefinitionActionMessage(guid, line, action) =>
action.code :: guid :: line :: action :: HNil
}
)
}
/*
("change" specifically indicates the perspective is from the SL; "update" indicates squad members other than the oen who made the change
("[#]" indicates the mode is detected but not properly parsed; the length of the combined fields may follow
[0] - clicking on a squad listed in the "Find Squad" tab / cancel squad search (6 bits/pad?)
[2] - ? (6 bits/pad?)
[3] - save squad favorite (6 bits/pad?)
[4] - load a squad definition favorite (6 bits/pad?)
[6] - ? (6 bits/pad?)
7 - ?
[8] - list squad (6 bits/pad?)
10 - select this role for yourself
11 - ?
12 - ?
13 - ?
14 - ?
15 - ?
[16] - ? (6 bits/pad?)
[17] - ? (6 bits/pad?)
[18] - ? (6 bits/pad?)
19 - change purpose
20 - change zone
21 - change/close squad member position
22 - change/add squad member position
23 - change squad member req role
24 - change squad member req detailed orders
25 - change squad member req weapons
[26] - reset all (6 bits/pad?)
28 - auto-approve requests for invitation
29 -
30 -
31 - location follows squad lead
[32] - ? (6 bits/pad?)
33 -
34 - search for squads with a particular role
36 -
37 -
38 -
[39] - ? (?)
40 - find LFS soldiers that meet the requirements for this role
[41] - cancel search for LFS soldiers (6 bits)
[42] - ? (6 bits/pad?)
[43] - ? (6 bits/pad?)
*/

View file

@ -0,0 +1,41 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
/**
* A message for communicating squad invitation.
* When received by a client, the event message "You have invited `name` to join your squad" is produced
* and a `SquadMembershipRequest` packet of type `Invite`
* using `char_id` as the optional unique character identifier field is dispatched to the server.
* The message is equivalent to a dispatched packet of type `SquadMembershipResponse`
* with an `Invite` event with the referral field set to `true`.
* @see `SquadMembershipResponse`
* @param squad_guid the squad's GUID
* @param slot a potentially valid slot index;
* 0-9; higher numbers produce no response
* @param char_id the unique character identifier
* @param name the character's name;
* frequently, though that does not produce a coherent message,
* the avatar's own name is supplied in the event message instead of the name of another player
*/
final case class SquadInvitationRequestMessage(squad_guid : PlanetSideGUID,
slot : Int,
char_id : Long,
name : String)
extends PlanetSideGamePacket {
type Packet = SquadInvitationRequestMessage
def opcode = GamePacketOpcode.SquadInvitationRequestMessage
def encode = SquadInvitationRequestMessage.encode(this)
}
object SquadInvitationRequestMessage extends Marshallable[SquadInvitationRequestMessage] {
implicit val codec : Codec[SquadInvitationRequestMessage] = (
("squad_guid" | PlanetSideGUID.codec) ::
("slot" | uint4) ::
("char_id" | uint32L) ::
("name" | PacketHelpers.encodedWideStringAligned(adjustment = 4))
).as[SquadInvitationRequestMessage]
}

View file

@ -0,0 +1,81 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
object MemberEvent extends Enumeration {
type Type = Value
val
Add,
Remove,
Promote,
UpdateZone,
Unknown4
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
}
final case class SquadMemberEvent(action : MemberEvent.Value,
unk2 : Int,
char_id : Long,
position : Int,
player_name : Option[String],
zone_number : Option[Int],
unk7 : Option[Long])
extends PlanetSideGamePacket {
type Packet = SquadMemberEvent
def opcode = GamePacketOpcode.SquadMemberEvent
def encode = SquadMemberEvent.encode(this)
}
object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
def apply(action : MemberEvent.Value, unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent =
SquadMemberEvent(action, unk2, char_id, position, None, None, None)
def Add(unk2 : Int, char_id : Long, position : Int, player_name : String, zone_number : Int, unk7 : Long) : SquadMemberEvent =
SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(unk7))
def Remove(unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent =
SquadMemberEvent(MemberEvent.Remove, unk2, char_id, position, None, None, None)
def Promote(unk2 : Int, char_id : Long) : SquadMemberEvent =
SquadMemberEvent(MemberEvent.Promote, unk2, char_id, 0, None, None, None)
def UpdateZone(unk2 : Int, char_id : Long, position : Int, zone_number : Int) : SquadMemberEvent =
SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, position, None, Some(zone_number), None)
def Unknown4(unk2 : Int, char_id : Long, position : Int, unk7 : Long) : SquadMemberEvent =
SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, position, None, None, Some(unk7))
implicit val codec : Codec[SquadMemberEvent] = (
("action" | MemberEvent.codec) >>:~ { action =>
("unk2" | uint16L) ::
("char_id" | uint32L) ::
("position" | uint4) ::
conditional(action == MemberEvent.Add, "player_name" | PacketHelpers.encodedWideStringAligned(1)) ::
conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, "zone_number" | uint16L) ::
conditional(action == MemberEvent.Add || action == MemberEvent.Unknown4, "unk7" | uint32L)
}).exmap[SquadMemberEvent] (
{
case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil =>
Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, unk7))
},
{
case SquadMemberEvent(MemberEvent.Add, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) =>
Attempt.Successful(MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(unk7) :: HNil)
case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) =>
Attempt.Successful(MemberEvent.UpdateZone :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil)
case SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, member_position, None, None, Some(unk7)) =>
Attempt.Successful(MemberEvent.Unknown4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil)
case SquadMemberEvent(action, unk2, char_id, member_position, None, None, None) =>
Attempt.Successful(action :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil)
case data =>
Attempt.Failure(Err(s"SquadMemberEvent can not encode with this pattern - $data"))
}
)
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.SquadRequestType
import scodec.Codec
import scodec.codecs._
/**
* Dispatched by the client as manipulation protocol for squad and platoon members.
* Answerable by a `SquadMembershipResponse` packet.
* @param request_type the purpose of the request
* @param char_id a squad member unique identifier;
* usually, the player being addresses by thie packet
* @param unk3 na
* @param player_name name of the player being affected, if applicable
* @param unk5 na
*/
final case class SquadMembershipRequest(request_type : SquadRequestType.Value,
char_id : Long,
unk3 : Option[Long],
player_name : String,
unk5 : Option[Option[String]])
extends PlanetSideGamePacket {
request_type match {
case SquadRequestType.Accept | SquadRequestType.Reject | SquadRequestType.Disband |
SquadRequestType.PlatoonAccept | SquadRequestType.PlatoonReject | SquadRequestType.PlatoonDisband =>
assert(unk3.isEmpty, s"a $request_type request requires the unk3 field be undefined")
case _ =>
assert(unk3.nonEmpty, s"a $request_type request requires the unk3 field be defined")
}
if(request_type == SquadRequestType.Invite) {
assert(unk5.nonEmpty, "an Invite request requires the unk5 field be defined")
}
type Packet = SquadMembershipRequest
def opcode = GamePacketOpcode.SquadMembershipRequest
def encode = SquadMembershipRequest.encode(this)
}
object SquadMembershipRequest extends Marshallable[SquadMembershipRequest] {
implicit val codec : Codec[SquadMembershipRequest] = (
("request_type" | SquadRequestType.codec) >>:~ { request_type =>
("unk2" | uint32L) ::
conditional(request_type != SquadRequestType.Accept &&
request_type != SquadRequestType.Reject &&
request_type != SquadRequestType.Disband &&
request_type != SquadRequestType.PlatoonAccept &&
request_type != SquadRequestType.PlatoonReject &&
request_type != SquadRequestType.PlatoonDisband, "unk3" | uint32L) ::
(("player_name" | PacketHelpers.encodedWideStringAligned(4)) >>:~ { pname =>
conditional(request_type == SquadRequestType.Invite,
"unk5" | optional(bool, PacketHelpers.encodedWideStringAligned({if(pname.length == 0) 3 else 7}))
).hlist
})
}).as[SquadMembershipRequest]
}

View file

@ -0,0 +1,96 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.SquadResponseType
import scodec.Codec
import scodec.codecs._
/**
* Dispatched by the server as message generation protocol for squad and platoon members.
* Prompted by and answers for a `SquadMembershipRequest` packet.
* @param request_type the purpose of the request
* @param unk1 na
* @param unk2 na
* @param char_id a squad member unique identifier;
* usually, the player being addresses by thie packet
* @param other_id another squad member's unique identifier;
* may be the same as `char_id`
* @param player_name name of the player being affected, if applicable
* @param unk5 adjusts the nature of the request-type response based on the message recipient
* @param unk6 na;
* the internal field, the `Option[String]`, never seems to be set
* <br>
* `request_type` (enum value) / `unk5` state (`false`/`true`)<br>
* ----------------------------------------<br>
* - `Invite` (0)<br>
* false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]<br>
* true => "You have invited `player_name` to join your squad."<br>
* - `Unk01` (1)<br>
* false => n/a<br>
* true => n/a<br>
* - `Accept` (2)<br>
* false => "`player_name` has accepted your invitation to join into your squad.<br>
* "You have formed a squad and are now that squad's commander." (if first time)<br>
* true => "You have accepted an invitation to join a squad."<br>
* "You have successfully joined a squad for the first time." (if first time)<br>
* - `Reject` (3)<br>
* false => "`player_name` does not want to join your squad at this time."<br>
* true => "You have declined an invitation to join a squad."<br>
* - `Cancel` (4)<br>
* false => "`player_name` has withdrawn his invitation."<br>
* true => "You have canceled your invitation to `player_name`."<br>
* - `Leave` (5)<br>
* false => "The Squad Leader has kicked you out of the squad."<br>
* true => "You have kicked `player_name` out of the squad."<br>
* - `Disband` (6)<br>
* false => "The squad has been disbanded."<br>
* true => "You have disbanded the squad."<br>
* - `PlatoonInvite` (7)<br>
* false => [PROMPT] "`player_name` has invited you into a platoon." [YES/NO]<br>
* true => "You have invited `player_name`'s squad to join your platoon."<br>
* - `PlatoonAccept` (8)
* false => "`player_name` has accepted your invitation to join into your platoon.<br>
* "You have formed a platoon and are now that platoon commander." (if first time)<br>
* true => "You have accepted an invitation to join a platoon."<br>
* "You have successfully joined a platoon for the first time." (if first time)<br>
* - `PlatoonReject` (9)<br>
* false => "`player_name` does not want to join your platoon at this time."<br>
* true => "You have declined an invitation to join a platoon."<br>
* - `PlatoonCancel` (10)<br>
* false => "`player_name` has withdrawn his invitation."<br>
* true => "You have declined your invitation to `player_name`." (nonsense?)<br>
* - `PlatoonLeave` (11)<br>
* false => "The Platoon Leader has kicked you out of the platoon."<br>
* true => "You have kicked `player_name`'s squad out of the platoon."<br>
* - `PlatoonDisband` (12)<br>
* false => "The platoon has been disbanded."<br>
* true => "You have disbanded the platoon."
*/
final case class SquadMembershipResponse(request_type : SquadResponseType.Value,
unk1 : Int,
unk2 : Int,
char_id : Long,
other_id : Option[Long],
player_name : String,
unk5 : Boolean,
unk6 : Option[Option[String]])
extends PlanetSideGamePacket {
type Packet = SquadMembershipResponse
def opcode = GamePacketOpcode.SquadMembershipResponse
def encode = SquadMembershipResponse.encode(this)
}
object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] {
implicit val codec : Codec[SquadMembershipResponse] = (
"request_type" | SquadResponseType.codec >>:~ { d =>
("unk1" | uint(5)) ::
("unk2" | uint2) ::
("char_id" | uint32L) ::
("other_id" | conditional(d != SquadResponseType.Disband && d != SquadResponseType.PlatoonDisband, uint32L)) ::
("player_name" | PacketHelpers.encodedWideStringAligned(5)) ::
("unk5" | bool) ::
conditional(d != SquadResponseType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
}
).as[SquadMembershipResponse]
}

View file

@ -0,0 +1,101 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.Vector3
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* Information about a specific squad member.
* @param char_id the character's unique identifier
* @param health the character's health value percentage, divided into 64 units
* @param armor the character's armor value percentage, divided into 64 units
* @param pos the world coordinates of the character
* @param unk4 na;
* usually, 2
* @param unk5 na;
* usually, 2
* @param unk6 na;
* usually, false
* @param unk7 na
* @param unk8 na;
* if defined, will be defined with unk9
* @param unk9 na;
* if defined, will be defined with unk8
*/
final case class SquadStateInfo(char_id : Long,
health : Int,
armor : Int,
pos : Vector3,
unk4 : Int,
unk5 : Int,
unk6 : Boolean,
unk7 : Int,
unk8 : Option[Int],
unk9 : Option[Boolean])
/**
* Dispatched by the server to update a squad member's representative icons on the continental maps and the interstellar map.<br>
* <br>
* This packet must be preceded by the correct protocol
* to assign any character who is defined by `char_id` in `info_list`
* as a member of this client's player's assigned squad by means of associating that said `char_id`.
* The said preceding protocol also assigns the player's current zone (continent) and their ordinal position in the squad.
* @see `SquadMemberEvent`
* @param guid the squad's unique identifier;
* must be consistent per packet on a given client;
* does not have to be the global uid of the squad as according to the server
* @param info_list information about the members in this squad who will be updated
*/
final case class SquadState(guid : PlanetSideGUID,
info_list : List[SquadStateInfo])
extends PlanetSideGamePacket {
type Packet = SquadState
def opcode = GamePacketOpcode.SquadState
def encode = SquadState.encode(this)
}
object SquadStateInfo {
def apply(unk1 : Long, unk2 : Int, unk3 : Int, pos : Vector3, unk4 : Int, unk5 : Int, unk6 : Boolean, unk7 : Int) : SquadStateInfo =
SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, None, None)
def apply(unk1 : Long, unk2 : Int, unk3 : Int, pos : Vector3, unk4 : Int, unk5 : Int, unk6 : Boolean, unk7 : Int, unk8 : Int, unk9 : Boolean) : SquadStateInfo =
SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, Some(unk8), Some(unk9))
}
object SquadState extends Marshallable[SquadState] {
private val info_codec : Codec[SquadStateInfo] = (
("char_id" | uint32L) ::
("health" | uint(7)) ::
("armor" | uint(7)) ::
("pos" | Vector3.codec_pos) ::
("unk4" | uint2) ::
("unk5" | uint2) ::
("unk6" | bool) ::
("unk7" | uint16L) ::
(bool >>:~ { out =>
conditional(out, "unk8" | uint16L) ::
conditional(out, "unk9" | bool)
})
).exmap[SquadStateInfo] (
{
case char_id :: health :: armor :: pos :: u4 :: u5 :: u6 :: u7 :: _ :: u8 :: u9 :: HNil =>
Attempt.Successful(SquadStateInfo(char_id, health, armor, pos, u4, u5, u6, u7, u8, u9))
},
{
case SquadStateInfo(char_id, health, armor, pos, u4, u5, u6, u7, Some(u8), Some(u9)) =>
Attempt.Successful(char_id :: health :: armor :: pos :: u4 :: u5 :: u6 :: u7 :: true :: Some(u8) :: Some(u9) :: HNil)
case SquadStateInfo(char_id, health, armor, pos, u4, u5, u6, u7, None, None) =>
Attempt.Successful(char_id :: health :: armor :: pos :: u4 :: u5 :: u6 :: u7 :: false :: None :: None :: HNil)
case data @ (SquadStateInfo(_, _, _, _, _, _, _, _, Some(_), None) | SquadStateInfo(_, _, _, _, _, _, _, _, None, Some(_))) =>
Attempt.Failure(Err(s"SquadStateInfo requires both unk8 and unk9 to be either defined or undefined at the same time - $data"))
}
)
implicit val codec : Codec[SquadState] = (
("guid" | PlanetSideGUID.codec) ::
("info_list" | listOfN(uint4, info_codec))
).as[SquadState]
}

View file

@ -1,22 +1,22 @@
// Copyright (c) 2017 PSForever
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.Vector3
import net.psforever.types.{SquadWaypoints, Vector3}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
final case class WaypointEvent(unk1 : Int,
final case class WaypointEvent(zone_number : Int,
pos : Vector3,
unk2 : Int)
unk : Int)
final case class SquadWaypointEvent(unk1 : Int,
unk2 : Int,
unk3 : Long,
unk4 : Int,
final case class SquadWaypointEvent(event_type : WaypointEventAction.Value,
unk : Int,
char_id : Long,
waypoint_type : SquadWaypoints.Value,
unk5 : Option[Long],
unk6 : Option[WaypointEvent])
waypoint_info : Option[WaypointEvent])
extends PlanetSideGamePacket {
type Packet = SquadWaypointEvent
def opcode = GamePacketOpcode.SquadWaypointEvent
@ -24,55 +24,55 @@ final case class SquadWaypointEvent(unk1 : Int,
}
object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] {
def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Long) : SquadWaypointEvent =
SquadWaypointEvent(unk1, unk2, unk3, unk4, Some(unk_a), None)
def Add(unk : Int, char_id : Long, waypoint_type : SquadWaypoints.Value, waypoint : WaypointEvent) : SquadWaypointEvent =
SquadWaypointEvent(WaypointEventAction.Add, unk, char_id, waypoint_type, None, Some(waypoint))
def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Int, pos : Vector3, unk_b : Int) : SquadWaypointEvent =
SquadWaypointEvent(unk1, unk2, unk3, unk4, None, Some(WaypointEvent(unk_a, pos, unk_b)))
def Unknown1(unk : Int, char_id : Long, waypoint_type : SquadWaypoints.Value, unk_a : Long) : SquadWaypointEvent =
SquadWaypointEvent(WaypointEventAction.Unknown1, unk, char_id, waypoint_type, Some(unk_a), None)
def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int) : SquadWaypointEvent =
SquadWaypointEvent(unk1, unk2, unk3, unk4, None, None)
def Remove(unk : Int, char_id : Long, waypoint_type : SquadWaypoints.Value) : SquadWaypointEvent =
SquadWaypointEvent(WaypointEventAction.Remove, unk, char_id, waypoint_type, None, None)
private val waypoint_codec : Codec[WaypointEvent] = (
("unk1" | uint16L) ::
("zone_number" | uint16L) ::
("pos" | Vector3.codec_pos) ::
("unk2" | uint(3))
("unk" | uint(3))
).as[WaypointEvent]
implicit val codec : Codec[SquadWaypointEvent] = (
("unk1" | uint2) >>:~ { unk1 =>
("unk2" | uint16L) ::
("unk3" | uint32L) ::
("unk4" | uint8L) ::
("unk5" | conditional(unk1 == 1, uint32L)) ::
("unk6" | conditional(unk1 == 0, waypoint_codec))
("event_type" | WaypointEventAction.codec) >>:~ { event_type =>
("unk" | uint16L) ::
("char_id" | uint32L) ::
("waypoint_type" | SquadWaypoints.codec) ::
("unk5" | conditional(event_type == WaypointEventAction.Unknown1, uint32L)) ::
("waypoint_info" | conditional(event_type == WaypointEventAction.Add, waypoint_codec))
}
).exmap[SquadWaypointEvent] (
{
case 0 :: a :: b :: c :: None :: Some(d) :: HNil =>
Attempt.Successful(SquadWaypointEvent(0, a, b, c, None, Some(d)))
case WaypointEventAction.Add :: a :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil =>
Attempt.Successful(SquadWaypointEvent(WaypointEventAction.Add, a, char_id, waypoint_type, None, Some(waypoint)))
case 1 :: a :: b :: c :: Some(d) :: None :: HNil =>
Attempt.Successful(SquadWaypointEvent(1, a, b, c, Some(d), None))
case WaypointEventAction.Unknown1 :: a :: char_id :: waypoint_type :: Some(d) :: None :: HNil =>
Attempt.Successful(SquadWaypointEvent(WaypointEventAction.Unknown1, a, char_id, waypoint_type, Some(d), None))
case a :: b :: c :: d :: None :: None :: HNil =>
Attempt.Successful(SquadWaypointEvent(a, b, c, d, None, None))
case event_type :: b :: char_id :: waypoint_type :: None :: None :: HNil =>
Attempt.Successful(SquadWaypointEvent(event_type, b, char_id, waypoint_type, None, None))
case n :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.Failure(Err(s"unexpected format for unk1 - $n"))
case data =>
Attempt.Failure(Err(s"unexpected format for $data"))
},
{
case SquadWaypointEvent(0, a, b, c, None, Some(d)) =>
Attempt.Successful(0 :: a :: b :: c :: None :: Some(d) :: HNil)
case SquadWaypointEvent(WaypointEventAction.Add, a, char_id, waypoint_type, None, Some(waypoint)) =>
Attempt.Successful(WaypointEventAction.Add :: a :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil)
case SquadWaypointEvent(1, a, b, c, Some(d), None) =>
Attempt.Successful(1 :: a :: b :: c :: Some(d) :: None :: HNil)
case SquadWaypointEvent(WaypointEventAction.Unknown1, a, char_id, waypoint_type, Some(d), None) =>
Attempt.Successful(WaypointEventAction.Unknown1 :: a :: char_id :: waypoint_type :: Some(d) :: None :: HNil)
case SquadWaypointEvent(a, b, c, d, None, None) =>
Attempt.Successful(a :: b :: c :: d :: None :: None :: HNil)
case SquadWaypointEvent(event_type, b, char_id, waypoint_type, None, None) =>
Attempt.Successful(event_type :: b :: char_id :: waypoint_type :: None :: None :: HNil)
case SquadWaypointEvent(n, _, _, _, _, _) =>
Attempt.Failure(Err(s"unexpected format for unk1 - $n"))
case data =>
Attempt.Failure(Err(s"unexpected format for $data"))
}
)
}

View file

@ -0,0 +1,115 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.{SquadWaypoints, Vector3}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* Actions that can be requested of the specific waypoint.
*/
object WaypointEventAction extends Enumeration {
type Type = Value
val
Add,
Unknown1,
Remove,
Unknown3 //unconfirmed
= Value
implicit val codec : Codec[WaypointEventAction.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint2)
}
/**
* na
* @param zone_number the zone
* @param pos the continental map coordinate location of the waypoint;
* the z-coordinate is almost always 0.0
*/
final case class WaypointInfo(zone_number : Int,
pos : Vector3)
/**
* na
* @param request_type the action to be performed
* @param char_id the unique id of player setting the waypoint
* @param waypoint_type the waypoint being updated;
* 0-3 for the standard squad waypoints numbered "1-4";
* 4 for the squad leader experience waypoint;
* cycles through 0-3 continuously
*
* @param unk4 na
* @param waypoint_info essential data about the waypoint
*/
final case class SquadWaypointRequest(request_type : WaypointEventAction.Value,
char_id : Long,
waypoint_type : SquadWaypoints.Value,
unk4 : Option[Long],
waypoint_info : Option[WaypointInfo])
extends PlanetSideGamePacket {
type Packet = SquadWaypointRequest
def opcode = GamePacketOpcode.SquadWaypointRequest
def encode = SquadWaypointRequest.encode(this)
}
object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] {
def Add(char_id : Long, waypoint_type : SquadWaypoints.Value, waypoint : WaypointInfo) : SquadWaypointRequest =
SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint))
def Unknown1(char_id : Long, waypoint_type : SquadWaypoints.Value, unk_a : Long) : SquadWaypointRequest =
SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(unk_a), None)
def Remove(char_id : Long, waypoint_type : SquadWaypoints.Value) : SquadWaypointRequest =
SquadWaypointRequest(WaypointEventAction.Remove, char_id, waypoint_type, None, None)
private val waypoint_codec : Codec[WaypointInfo] = (
("zone_number" | uint16L) ::
("pos" | Vector3.codec_pos)
).xmap[WaypointInfo] (
{
case zone_number :: pos :: HNil => WaypointInfo(zone_number, pos)
},
{
case WaypointInfo(zone_number, pos) => zone_number :: pos.xy :: HNil
}
)
implicit val codec : Codec[SquadWaypointRequest] = (
("request_type" | WaypointEventAction.codec) >>:~ { request_type =>
("char_id" | uint32L) ::
("waypoint_type" | SquadWaypoints.codec) ::
("unk4" | conditional(request_type == WaypointEventAction.Unknown1, uint32L)) ::
("waypoint" | conditional(request_type == WaypointEventAction.Add, waypoint_codec))
}
).exmap[SquadWaypointRequest] (
{
case WaypointEventAction.Add :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil =>
Attempt.Successful(SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint)))
case WaypointEventAction.Unknown1 :: char_id :: waypoint_type :: Some(d) :: None :: HNil =>
Attempt.Successful(SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(d), None))
case request_type :: char_id :: waypoint_type :: None :: None :: HNil =>
Attempt.Successful(SquadWaypointRequest(request_type, char_id, waypoint_type, None, None))
case data =>
Attempt.Failure(Err(s"unexpected format while decoding - $data"))
},
{
case SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint)) =>
Attempt.Successful(WaypointEventAction.Add :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil)
case SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(d), None) =>
Attempt.Successful(WaypointEventAction.Unknown1 :: char_id :: waypoint_type :: Some(d) :: None :: HNil)
case SquadWaypointRequest(request_type, char_id, waypoint_type, None, None) =>
Attempt.Successful(request_type :: char_id :: waypoint_type :: None :: None :: HNil)
case data : SquadWaypointRequest =>
Attempt.Failure(Err(s"unexpected format while encoding - $data"))
}
)
}

View file

@ -23,12 +23,13 @@ import shapeless.{::, HNil}
* -player_guid - does nothing?
* @param exosuit the type of exo-suit the avatar will be depicted in;
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
* @param char_id a unique character reference identification number
*/
final case class CharacterAppearanceA(app : BasicCharacterData,
data : CommonFieldData,
exosuit : ExoSuitType.Value,
unk5 : Int,
unk6 : Long,
char_id : Long,
unk7 : Int,
unk8 : Int,
unk9 : Int,

View file

@ -3,6 +3,8 @@ package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
import scala.annotation.tailrec
/**
* An `Enumeration` of the available certifications.<br>
* <br>
@ -76,4 +78,59 @@ object CertificationType extends Enumeration {
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
/**
* Certifications are often stored, in object form, as a 46-member collection.
* Encode a subset of certification values for packet form.
* @see `ChangeSquadMemberRequirementsCertifications`
* @see `changeSquadMemberRequirementsCertificationsCodec`
* @param certs the certifications, as a sequence of values
* @return the certifications, as a single value
*/
def toEncodedLong(certs : Set[CertificationType.Value]) : Long = {
certs
.map{ cert => math.pow(2, cert.id).toLong }
.foldLeft(0L)(_ + _)
}
/**
* Certifications are often stored, in packet form, as an encoded little-endian `46u` value.
* Decode a representative value into a subset of certification values.
* @see `ChangeSquadMemberRequirementsCertifications`
* @see `changeSquadMemberRequirementsCertificationsCodec`
* @see `fromEncodedLong(Long, Iterable[Long], Set[CertificationType.Value])`
* @param certs the certifications, as a single value
* @return the certifications, as a sequence of values
*/
def fromEncodedLong(certs : Long) : Set[CertificationType.Value] = {
recursiveFromEncodedLong(
certs,
CertificationType.values.map{ cert => math.pow(2, cert.id).toLong }.toSeq.sorted
)
}
/**
* Certifications are often stored, in packet form, as an encoded little-endian `46u` value.
* Decode a representative value into a subset of certification values
* by repeatedly finding the partition point of values less than a specific one,
* providing for both the next lowest value (to subtract) and an index (of a certification).
* @see `ChangeSquadMemberRequirementsCertifications`
* @see `changeSquadMemberRequirementsCertificationsCodec`
* @see `fromEncodedLong(Long)`
* @param certs the certifications, as a single value
* @param splitList the available values to partition
* @param out the accumulating certification values;
* defaults to an empty set
* @return the certifications, as a sequence of values
*/
@tailrec
private def recursiveFromEncodedLong(certs : Long, splitList : Iterable[Long], out : Set[CertificationType.Value] = Set.empty) : Set[CertificationType.Value] = {
if(certs == 0 || splitList.isEmpty) {
out
}
else {
val (less, _) = splitList.partition(_ <= certs)
recursiveFromEncodedLong(certs - less.last, less, out ++ Set(CertificationType(less.size - 1)))
}
}
}

View file

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

View file

@ -0,0 +1,27 @@
// Copyright (c) 2019 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
object SquadRequestType extends Enumeration {
type Type = Value
val
Invite,
ProximityInvite,
Accept,
Reject,
Cancel,
Leave,
Promote,
Disband,
PlatoonInvite,
PlatoonAccept,
PlatoonReject,
PlatoonCancel,
PlatoonLeave,
PlatoonDisband
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2019 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
object SquadResponseType extends Enumeration {
type Type = Value
val
Invite,
Unk01,
Accept,
Reject,
Cancel,
Leave,
Disband,
PlatoonInvite,
PlatoonAccept,
PlatoonReject,
PlatoonCancel,
PlatoonLeave,
PlatoonDisband
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}

View file

@ -0,0 +1,18 @@
// Copyright (c) 2019 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
object SquadWaypoints extends Enumeration {
type Type = Value
val
One,
Two,
Three,
Four,
ExperienceRally
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import net.psforever.objects.Player
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{PlanetSideGUID, SquadAction => PacketSquadAction, WaypointEventAction, WaypointInfo}
import net.psforever.types.{SquadRequestType, SquadWaypoints, Vector3}
final case class SquadServiceMessage(tplayer : Player, zone : Zone, actionMessage : Any)
object SquadServiceMessage {
final case class RecoverSquadMembership()
}
object SquadAction {
sealed trait Action
final case class InitSquadList() extends Action
final case class InitCharId() extends Action
final case class Definition(guid : PlanetSideGUID, line : Int, action : PacketSquadAction) extends Action
final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : SquadWaypoints.Value, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
}

View file

@ -0,0 +1,45 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game._
import net.psforever.types.{SquadResponseType, SquadWaypoints}
import services.GenericEventBusMsg
final case class SquadServiceResponse(toChannel : String, exclude : Iterable[Long], response : SquadResponse.Response) extends GenericEventBusMsg
object SquadServiceResponse {
def apply(toChannel : String, response : SquadResponse.Response) : SquadServiceResponse =
SquadServiceResponse(toChannel, Nil, response)
def apply(toChannel : String, exclude : Long, response : SquadResponse.Response) : SquadServiceResponse =
SquadServiceResponse(toChannel, Seq(exclude), response)
}
object SquadResponse {
sealed trait Response
final case class ListSquadFavorite(line : Int, task : String) extends Response
final case class InitList(info : Vector[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 AssociateWithSquad(squad_guid : PlanetSideGUID) extends Response
final case class SetListSquad(squad_guid : PlanetSideGUID) extends Response
final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
final case class WantsSquadPosition(leader_char_id : Long, bid_name : String) extends Response
final case class Join(squad : Squad, positionsToUpdate : List[Int], channel : String) extends Response
final case class Leave(squad : Squad, positionsToUpdate : List[(Long, Int)]) extends Response
final case class UpdateMembers(squad : Squad, update_info : List[SquadAction.Update]) extends Response
final case class AssignMember(squad : Squad, from_index : Int, to_index : Int) extends Response
final case class PromoteMember(squad : Squad, char_id : Long, from_index : Int, to_index : Int) extends Response
final case class Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
final case class InitWaypoints(char_id : Long, waypoints : Iterable[(SquadWaypoints.Value, WaypointInfo, Int)]) extends Response
final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : SquadWaypoints.Value, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) extends Response
final case class SquadSearchResults() extends Response
}

View file

@ -0,0 +1,142 @@
// Copyright (c) 2019 PSForever
package services.teamwork
import akka.actor.{Actor, ActorRef, Terminated}
import scala.collection.mutable
/**
* The dedicated messaging switchboard for members and observers of a given squad.
* It almost always dispatches messages to `WorldSessionActor` instances, much like any other `Service`.
* The sole purpose of this `ActorBus` container is to manage a subscription model
* that can involuntarily drop subscribers without informing them explicitly
* or can just vanish without having to properly clean itself up.
*/
class SquadSwitchboard extends Actor {
/**
* This collection contains the message-sending contact reference for squad members.
* Users are added to this collection via the `SquadSwitchboard.Join` message, or a
* combination of the `SquadSwitchboard.DelayJoin` message followed by a
* `SquadSwitchboard.Join` message with or without an `ActorRef` hook.
* The message `SquadSwitchboard.Leave` removes the user from this collection.
* key - unique character id; value - `Actor` reference for that character
*/
val UserActorMap : mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]()
/**
* This collection contains the message-sending contact information for would-be squad members.
* Users are added to this collection via the `SquadSwitchboard.DelayJoin` message
* and are promoted to an actual squad member through a `SquadSwitchboard.Join` message.
* The message `SquadSwitchboard.Leave` removes the user from this collection.
* key - unique character id; value - `Actor` reference for that character
*/
val DelayedJoin : mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]()
/**
* This collection contains the message-sending contact information for squad observers.
* Squad observers only get "details" messages as opposed to the sort of messages squad members receive.
* Squad observers are promoted to an actual squad member through a `SquadSwitchboard.Watch` message.
* The message `SquadSwitchboard.Leave` removes the user from this collection.
* The message `SquadSwitchboard.Unwatch` also removes the user from this collection.
* key - unique character id; value - `Actor` reference for that character
*/
val Watchers : mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]()
override def postStop() : Unit = {
UserActorMap.clear()
DelayedJoin.clear()
Watchers.clear()
}
def receive : Receive = {
case SquadSwitchboard.Join(char_id, Some(actor)) =>
UserActorMap(char_id) = DelayedJoin.remove(char_id).orElse( Watchers.remove(char_id)) match {
case Some(_actor) =>
context.watch(_actor)
_actor
case None =>
context.watch(actor)
actor
}
case SquadSwitchboard.Join(char_id, None) =>
DelayedJoin.remove(char_id).orElse( Watchers.remove(char_id)) match {
case Some(actor) =>
UserActorMap(char_id) = actor
case None => ;
}
case SquadSwitchboard.DelayJoin(char_id, actor) =>
context.watch(actor)
DelayedJoin(char_id) = actor
case SquadSwitchboard.Leave(char_id) =>
UserActorMap.find { case(charId, _) => charId == char_id }
.orElse(DelayedJoin.find { case(charId, _) => charId == char_id })
.orElse(Watchers.find { case(charId, _) => charId == char_id }) match {
case Some((member, actor)) =>
context.unwatch(actor)
UserActorMap.remove(member)
DelayedJoin.remove(member)
Watchers.remove(member)
case None => ;
}
case SquadSwitchboard.Watch(char_id, actor) =>
context.watch(actor)
Watchers(char_id) = actor
case SquadSwitchboard.Unwatch(char_id) =>
Watchers.remove(char_id)
case SquadSwitchboard.To(member, msg) =>
UserActorMap.find { case (char_id, _) => char_id == member } match {
case Some((_, actor)) =>
actor ! msg
case None => ;
}
case SquadSwitchboard.ToAll(msg) =>
UserActorMap
.foreach { case (_, actor) =>
actor ! msg
}
case SquadSwitchboard.Except(excluded, msg) =>
UserActorMap
.filterNot { case (char_id, _) => char_id == excluded }
.foreach { case (_, actor) =>
actor ! msg
}
case Terminated(actorRef) =>
UserActorMap.find { case(_, ref) => ref == actorRef }
.orElse(DelayedJoin.find { case(_, ref) => ref == actorRef })
.orElse(Watchers.find { case(_, ref) => ref == actorRef }) match {
case Some((member, actor)) =>
context.unwatch(actor)
UserActorMap.remove(member)
DelayedJoin.remove(member)
Watchers.remove(member)
case None => ;
}
case _ => ;
}
}
object SquadSwitchboard {
final case class Join(char_id : Long, actor : Option[ActorRef])
final case class DelayJoin(char_id : Long, actor : ActorRef)
final case class Leave(char_id : Long)
final case class Watch(char_id : Long, actor : ActorRef)
final case class Unwatch(char_id : Long)
final case class To(member : Long, msg : SquadServiceResponse)
final case class ToAll(msg : SquadServiceResponse)
final case class Except(excluded_member : Long, msg : SquadServiceResponse)
}

View file

@ -0,0 +1,70 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.CertificationType
import scodec.bits._
class CharacterKnowledgeMessageTest extends Specification {
val string = hex"ec cc637a02 45804600720061006e006b0065006e00740061006e006b0003c022dc0008f01800"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case CharacterKnowledgeMessage(char_id, Some(info)) =>
char_id mustEqual 41575372L
info mustEqual CharacterKnowledgeInfo(
"Frankentank",
Set(
CertificationType.StandardAssault,
CertificationType.ArmoredAssault1,
CertificationType.MediumAssault,
CertificationType.ReinforcedExoSuit,
CertificationType.Harasser,
CertificationType.Engineering,
CertificationType.GroundSupport,
CertificationType.AgileExoSuit,
CertificationType.AIMAX,
CertificationType.StandardExoSuit,
CertificationType.AAMAX,
CertificationType.ArmoredAssault2
),
15,
0,
PlanetSideGUID(12)
)
case _ =>
ko
}
}
"encode" in {
val msg = CharacterKnowledgeMessage(
41575372L,
CharacterKnowledgeInfo(
"Frankentank",
Set(
CertificationType.StandardAssault,
CertificationType.ArmoredAssault1,
CertificationType.MediumAssault,
CertificationType.ReinforcedExoSuit,
CertificationType.Harasser,
CertificationType.Engineering,
CertificationType.GroundSupport,
CertificationType.AgileExoSuit,
CertificationType.AIMAX,
CertificationType.StandardExoSuit,
CertificationType.AAMAX,
CertificationType.ArmoredAssault2
),
15,
0,
PlanetSideGUID(12)
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -19,17 +19,7 @@ class ReplicationStreamMessageTest extends Specification {
val stringUpdateLeaderSize = hex"E6 C0 58 10 C3 00 4A0069006D006D0079006E00 43 FF"
val stringUpdateTaskContinent = hex"E6 C0 58 11 40 80 3200 3 04000000 FF0"
val stringUpdateAll = hex"E6 C0 78 30 58 0430 6D00610064006D0075006A00 80 040000000A FF"
//failing conditions
val stringCodecFail = hex"E6 20 A1 19 FE"
val stringListOneFail = hex"E6 B8 01 06 01 00 8B 46007200610067004C0041004E00640049004E004300 84 4600720061006700 0A00 00 01 0A FF"
val stringListTwoFail = hex"E6 B8 01 06 06 00 8E 470065006E006500720061006C0047006F0072006700750074007A00 A1 46004C0059002C0041006C006C002000770065006C0063006F006D0065002C0063006E0020006C0061007300740020006E0069006700680074002100210021002100 0400 00 00 7A 01 83 02 00 45 80 4B004F004B006B006900610073004D00460043004E00 87 5300710075006100640020003200 0400 00 01 6A FF"
val stringUpdateLeaderFail = hex"E6 C0 28 08 44 00 46006100740065004A0048004E004300 FF"
val stringUpdateTaskFail = hex"E6 C0 58 09CE00 52004900500020005000530031002C0020007600690073006900740020005000530046006F00720065007600650072002E006E0065007400 FF"
val stringUpdateContinentFail = hex"E6 C0 38 09 85000001 7F80"
val stringUpdateSizeFail = hex"E6 C0 18 0A B7 F8"
val stringUpdateLeaderSizeFail = hex"E6 C0 58 10 43 00 4A0069006D006D0079006E00 43 FF"
val stringUpdateTaskContinentFail = hex"E6 C0 58 11 C0 80 3200 3 04000000 FF0"
val stringUpdateAllFail = hex"E6 C0 78 30 58 0430 6D00610064006D0075006A00 80 04000001 0A FF"
val stringRemoveUpdate = hex"e6 20201801014aff"
"SquadInfo (w/out squad_guid)" in {
val o = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10)
@ -56,15 +46,27 @@ class ReplicationStreamMessageTest extends Specification {
o.capacity.get mustEqual 7
}
"SquadInfo (Add)" in {
val o1 = SquadInfo(Some("FragLANdINC"), Some("Frag"), Some(PlanetSideZoneID(10)), None, None)
val o2 = SquadInfo(Some(7), 10)
val o3 = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 7, 10)
o1.And(o2) mustEqual o3
}
"SquadInfo (Add, with blocked fields)" in {
val o1 = SquadInfo(Some("FragLANdINC"), None, Some(PlanetSideZoneID(10)), None, Some(10))
val o2 = SquadInfo(Some("Frag"), Some("Frag"), Some(PlanetSideZoneID(15)), Some(7), Some(7))
val o3 = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 7, 10)
o1.And(o2) mustEqual o3
}
"decode (clear)" in {
PacketCoding.DecodePacket(stringListClear).require match {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 5
behavior2.isDefined mustEqual true
behavior2.get mustEqual 6
entries.length mustEqual 1
entries.head.index mustEqual 255
entries.head.listing.isDefined mustEqual false
entries.length mustEqual 0
case _ =>
ko
}
@ -75,27 +77,21 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 5
behavior2.get mustEqual 6
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 0
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 131
entries.head.listing.get.unk2 mustEqual false
entries.head.listing.get.unk3.isDefined mustEqual false
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual true
entries.head.listing.get.info.get.leader.get mustEqual "FragLANdINC"
entries.head.listing.get.info.get.task.isDefined mustEqual true
entries.head.listing.get.info.get.task.get mustEqual "Frag"
entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10)
entries.head.listing.get.info.get.size.isDefined mustEqual true
entries.head.listing.get.info.get.size.get mustEqual 0
entries.head.listing.get.info.get.capacity.isDefined mustEqual true
entries.head.listing.get.info.get.capacity.get mustEqual 10
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual true
entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(1)
entries(1).index mustEqual 255
entries(1).listing.isDefined mustEqual false
entries.head.listing.get.leader.isDefined mustEqual true
entries.head.listing.get.leader.get mustEqual "FragLANdINC"
entries.head.listing.get.task.isDefined mustEqual true
entries.head.listing.get.task.get mustEqual "Frag"
entries.head.listing.get.zone_id.isDefined mustEqual true
entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(10)
entries.head.listing.get.size.isDefined mustEqual true
entries.head.listing.get.size.get mustEqual 0
entries.head.listing.get.capacity.isDefined mustEqual true
entries.head.listing.get.capacity.get mustEqual 10
entries.head.listing.get.squad_guid.isDefined mustEqual true
entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(1)
case _ =>
ko
}
@ -106,28 +102,21 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 5
behavior2.get mustEqual 6
entries.length mustEqual 3
entries.length mustEqual 2
entries.head.index mustEqual 0
entries.head.listing.get.unk1 mustEqual 131
entries.head.listing.get.unk2 mustEqual false
entries.head.listing.get.unk3.isDefined mustEqual false
entries.head.listing.get.info.get.leader.get mustEqual "GeneralGorgutz"
entries.head.listing.get.info.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.info.get.size.get mustEqual 7
entries.head.listing.get.info.get.capacity.get mustEqual 10
entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(6)
entries.head.listing.get.leader.get mustEqual "GeneralGorgutz"
entries.head.listing.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.size.get mustEqual 7
entries.head.listing.get.capacity.get mustEqual 10
entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(6)
entries(1).index mustEqual 1
entries(1).listing.get.unk1 mustEqual 131
entries(1).listing.get.unk2 mustEqual false
entries(1).listing.get.unk3.isDefined mustEqual false
entries(1).listing.get.info.get.leader.get mustEqual "KOKkiasMFCN"
entries(1).listing.get.info.get.task.get mustEqual "Squad 2"
entries(1).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries(1).listing.get.info.get.size.get mustEqual 6
entries(1).listing.get.info.get.capacity.get mustEqual 10
entries(1).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(4)
entries(2).index mustEqual 255
entries(1).listing.get.leader.get mustEqual "KOKkiasMFCN"
entries(1).listing.get.task.get mustEqual "Squad 2"
entries(1).listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries(1).listing.get.size.get mustEqual 6
entries(1).listing.get.capacity.get mustEqual 10
entries(1).listing.get.squad_guid.get mustEqual PlanetSideGUID(4)
case _ =>
ko
}
@ -138,38 +127,28 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 5
behavior2.get mustEqual 6
entries.length mustEqual 4
entries.length mustEqual 3
entries.head.index mustEqual 0
entries.head.listing.get.unk1 mustEqual 131
entries.head.listing.get.unk2 mustEqual false
entries.head.listing.get.unk3.isDefined mustEqual false
entries.head.listing.get.info.get.leader.get mustEqual "GeneralGorgutz"
entries.head.listing.get.info.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.info.get.size.get mustEqual 7
entries.head.listing.get.info.get.capacity.get mustEqual 10
entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(6)
entries.head.listing.get.leader.get mustEqual "GeneralGorgutz"
entries.head.listing.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.size.get mustEqual 7
entries.head.listing.get.capacity.get mustEqual 10
entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(6)
entries(1).index mustEqual 1
entries(1).listing.get.unk1 mustEqual 131
entries(1).listing.get.unk2 mustEqual false
entries(1).listing.get.unk3.isDefined mustEqual false
entries(1).listing.get.info.get.leader.get mustEqual "NIGHT88RAVEN"
entries(1).listing.get.info.get.task.get mustEqual "All Welcome"
entries(1).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10)
entries(1).listing.get.info.get.size.get mustEqual 4
entries(1).listing.get.info.get.capacity.get mustEqual 10
entries(1).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(3)
entries(1).listing.get.leader.get mustEqual "NIGHT88RAVEN"
entries(1).listing.get.task.get mustEqual "All Welcome"
entries(1).listing.get.zone_id.get mustEqual PlanetSideZoneID(10)
entries(1).listing.get.size.get mustEqual 4
entries(1).listing.get.capacity.get mustEqual 10
entries(1).listing.get.squad_guid.get mustEqual PlanetSideGUID(3)
entries(2).index mustEqual 2
entries(2).listing.get.unk1 mustEqual 131
entries(2).listing.get.unk2 mustEqual false
entries(2).listing.get.unk3.isDefined mustEqual false
entries(2).listing.get.info.get.leader.get mustEqual "KOKkiasMFCN"
entries(2).listing.get.info.get.task.get mustEqual "Squad 2"
entries(2).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries(2).listing.get.info.get.size.get mustEqual 6
entries(2).listing.get.info.get.capacity.get mustEqual 10
entries(2).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(4)
entries(3).index mustEqual 255
entries(2).listing.get.leader.get mustEqual "KOKkiasMFCN"
entries(2).listing.get.task.get mustEqual "Squad 2"
entries(2).listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries(2).listing.get.size.get mustEqual 6
entries(2).listing.get.capacity.get mustEqual 10
entries(2).listing.get.squad_guid.get mustEqual PlanetSideGUID(4)
case _ =>
ko
}
@ -180,15 +159,9 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 1
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 5
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 0
entries.head.listing.get.unk2 mustEqual true
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 4
entries.head.listing.get.info.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.isDefined mustEqual false
case _ =>
ko
}
@ -199,22 +172,16 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 2
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 128
entries.head.listing.get.unk2 mustEqual true
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 0
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual true
entries.head.listing.get.info.get.leader.get mustEqual "FateJHNC"
entries.head.listing.get.info.get.task.isDefined mustEqual false
entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
entries.head.listing.get.info.get.size.isDefined mustEqual false
entries.head.listing.get.info.get.capacity.isDefined mustEqual false
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual true
entries.head.listing.get.leader.get mustEqual "FateJHNC"
entries.head.listing.get.task.isDefined mustEqual false
entries.head.listing.get.zone_id.isDefined mustEqual false
entries.head.listing.get.size.isDefined mustEqual false
entries.head.listing.get.capacity.isDefined mustEqual false
entries.head.listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
@ -225,22 +192,16 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 5
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 128
entries.head.listing.get.unk2 mustEqual true
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 1
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual false
entries.head.listing.get.info.get.task.isDefined mustEqual true
entries.head.listing.get.info.get.task.get mustEqual "RIP PS1, visit PSForever.net"
entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
entries.head.listing.get.info.get.size.isDefined mustEqual false
entries.head.listing.get.info.get.capacity.isDefined mustEqual false
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual false
entries.head.listing.get.task.isDefined mustEqual true
entries.head.listing.get.task.get mustEqual "RIP PS1, visit PSForever.net"
entries.head.listing.get.zone_id.isDefined mustEqual false
entries.head.listing.get.size.isDefined mustEqual false
entries.head.listing.get.capacity.isDefined mustEqual false
entries.head.listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
@ -251,22 +212,16 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 3
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 128
entries.head.listing.get.unk2 mustEqual true
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 1
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual false
entries.head.listing.get.info.get.task.isDefined mustEqual false
entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10)
entries.head.listing.get.info.get.size.isDefined mustEqual false
entries.head.listing.get.info.get.capacity.isDefined mustEqual false
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual false
entries.head.listing.get.task.isDefined mustEqual false
entries.head.listing.get.zone_id.isDefined mustEqual true
entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(10)
entries.head.listing.get.size.isDefined mustEqual false
entries.head.listing.get.capacity.isDefined mustEqual false
entries.head.listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
@ -277,22 +232,16 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 1
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 128
entries.head.listing.get.unk2 mustEqual true
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 2
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual false
entries.head.listing.get.info.get.task.isDefined mustEqual false
entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
entries.head.listing.get.info.get.size.isDefined mustEqual true
entries.head.listing.get.info.get.size.get mustEqual 6
entries.head.listing.get.info.get.capacity.isDefined mustEqual false
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual false
entries.head.listing.get.task.isDefined mustEqual false
entries.head.listing.get.zone_id.isDefined mustEqual false
entries.head.listing.get.size.isDefined mustEqual true
entries.head.listing.get.size.get mustEqual 6
entries.head.listing.get.capacity.isDefined mustEqual false
entries.head.listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
@ -303,23 +252,17 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 5
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 129
entries.head.listing.get.unk2 mustEqual false
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 0
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual true
entries.head.listing.get.info.get.leader.get mustEqual "Jimmyn"
entries.head.listing.get.info.get.task.isDefined mustEqual false
entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
entries.head.listing.get.info.get.size.isDefined mustEqual true
entries.head.listing.get.info.get.size.get mustEqual 3
entries.head.listing.get.info.get.capacity.isDefined mustEqual false
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual true
entries.head.listing.get.leader.get mustEqual "Jimmyn"
entries.head.listing.get.task.isDefined mustEqual false
entries.head.listing.get.zone_id.isDefined mustEqual false
entries.head.listing.get.size.isDefined mustEqual true
entries.head.listing.get.size.get mustEqual 3
entries.head.listing.get.capacity.isDefined mustEqual false
entries.head.listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
@ -330,23 +273,17 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 5
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 129
entries.head.listing.get.unk2 mustEqual false
entries.head.listing.get.unk3.isDefined mustEqual true
entries.head.listing.get.unk3.get mustEqual 1
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual false
entries.head.listing.get.info.get.task.isDefined mustEqual true
entries.head.listing.get.info.get.task.get mustEqual "2"
entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.info.get.size.isDefined mustEqual false
entries.head.listing.get.info.get.capacity.isDefined mustEqual false
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual false
entries.head.listing.get.task.isDefined mustEqual true
entries.head.listing.get.task.get mustEqual "2"
entries.head.listing.get.zone_id.isDefined mustEqual true
entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.size.isDefined mustEqual false
entries.head.listing.get.capacity.isDefined mustEqual false
entries.head.listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
@ -357,60 +294,57 @@ class ReplicationStreamMessageTest extends Specification {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 6
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.length mustEqual 1
entries.head.index mustEqual 7
entries.head.listing.isDefined mustEqual true
entries.head.listing.get.unk1 mustEqual 131
entries.head.listing.get.unk2 mustEqual false
entries.head.listing.get.unk3.isDefined mustEqual false
entries.head.listing.get.info.isDefined mustEqual true
entries.head.listing.get.info.get.leader.isDefined mustEqual true
entries.head.listing.get.info.get.leader.get mustEqual "madmuj"
entries.head.listing.get.info.get.task.isDefined mustEqual true
entries.head.listing.get.info.get.task.get mustEqual ""
entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.info.get.size.isDefined mustEqual true
entries.head.listing.get.info.get.size.get mustEqual 0
entries.head.listing.get.info.get.capacity.isDefined mustEqual true
entries.head.listing.get.info.get.capacity.get mustEqual 10
entries.head.listing.get.info.get.squad_guid.isDefined mustEqual true
entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(11)
entries(1).index mustEqual 255
entries.head.listing.get.leader.isDefined mustEqual true
entries.head.listing.get.leader.get mustEqual "madmuj"
entries.head.listing.get.task.isDefined mustEqual true
entries.head.listing.get.task.get mustEqual ""
entries.head.listing.get.zone_id.isDefined mustEqual true
entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
entries.head.listing.get.size.isDefined mustEqual true
entries.head.listing.get.size.get mustEqual 0
entries.head.listing.get.capacity.isDefined mustEqual true
entries.head.listing.get.capacity.get mustEqual 10
entries.head.listing.get.squad_guid.isDefined mustEqual true
entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(11)
case _ =>
ko
}
}
"decode (fails)" in {
PacketCoding.DecodePacket(stringCodecFail).isFailure mustEqual true
//PacketCoding.DecodePacket(stringListOneFail).isFailure mustEqual true -> used to fail
//PacketCoding.DecodePacket(stringListTwoFail).isFailure mustEqual true -> used to fail
PacketCoding.DecodePacket(stringUpdateLeaderFail).isFailure mustEqual true
PacketCoding.DecodePacket(stringUpdateTaskFail).isFailure mustEqual true
//PacketCoding.DecodePacket(stringUpdateContinentFail).isFailure mustEqual true -> used to fail
PacketCoding.DecodePacket(stringUpdateSizeFail).isFailure mustEqual true
PacketCoding.DecodePacket(stringUpdateLeaderSizeFail).isFailure mustEqual true
PacketCoding.DecodePacket(stringUpdateTaskContinentFail).isFailure mustEqual true
//PacketCoding.DecodePacket(stringUpdateAllFail).isFailure mustEqual true -> used to fail
"decode (remove 1 and update 0)" in {
PacketCoding.DecodePacket(stringRemoveUpdate).require match {
case ReplicationStreamMessage(behavior, behavior2, entries) =>
behavior mustEqual 1
behavior2.isDefined mustEqual false
entries.length mustEqual 2
entries.head.index mustEqual 1
entries.head.listing.isDefined mustEqual false
entries(1).listing.get.leader.isDefined mustEqual false
entries(1).listing.get.task.isDefined mustEqual false
entries(1).listing.get.zone_id.isDefined mustEqual false
entries(1).listing.get.size.isDefined mustEqual true
entries(1).listing.get.size.get mustEqual 10
entries(1).listing.get.capacity.isDefined mustEqual false
entries(1).listing.get.squad_guid.isDefined mustEqual false
case _ =>
ko
}
}
"encode (clear)" in {
val msg = ReplicationStreamMessage(5, Some(6),
Vector(
SquadListing(255)
)
)
val msg = ReplicationStreamMessage(5, Some(6), Vector.empty)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringListClear
}
"encode (one)" in {
val msg = ReplicationStreamMessage(5, Some(6),
Vector(
SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10, PlanetSideGUID(1))))),
SquadListing(255)
val msg = ReplicationStreamMessage(
Seq(
SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10, PlanetSideGUID(1))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -419,11 +353,10 @@ class ReplicationStreamMessageTest extends Specification {
}
"encode (two)" in {
val msg = ReplicationStreamMessage(5, Some(6),
Vector(
SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))),
SquadListing(1, Some(SquadHeader(131, false, None, SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))))),
SquadListing(255)
val msg = ReplicationStreamMessage(
Seq(
SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6)),
SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -432,12 +365,11 @@ class ReplicationStreamMessageTest extends Specification {
}
"encode (three)" in {
val msg = ReplicationStreamMessage(5, Some(6),
Vector(
SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))),
SquadListing(1, Some(SquadHeader(131, false, None, SquadInfo("NIGHT88RAVEN", "All Welcome", PlanetSideZoneID(10), 4, 10, PlanetSideGUID(3))))),
SquadListing(2, Some(SquadHeader(131, false, None, SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))))),
SquadListing(255)
val msg = ReplicationStreamMessage(
Seq(
SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6)),
SquadInfo("NIGHT88RAVEN", "All Welcome", PlanetSideZoneID(10), 4, 10, PlanetSideGUID(3)),
SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -448,8 +380,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (remove)" in {
val msg = ReplicationStreamMessage(1, None,
Vector(
SquadListing(5, Some(SquadHeader(0, true, Some(4)))),
SquadListing(255)
SquadListing(5, None)
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -460,8 +391,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update leader)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(2, Some(SquadHeader(128, true, Some(0), SquadInfo("FateJHNC", None)))),
SquadListing(255)
SquadListing(2, SquadInfo("FateJHNC"))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -472,8 +402,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update task)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(5, Some(SquadHeader(128, true, Some(1), SquadInfo(None, "RIP PS1, visit PSForever.net")))),
SquadListing(255)
SquadListing(5, SquadInfo(None, "RIP PS1, visit PSForever.net"))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -484,8 +413,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update continent)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(3, Some(SquadHeader(128, true, Some(1), SquadInfo(PlanetSideZoneID(10))))),
SquadListing(255)
SquadListing(3, SquadInfo(PlanetSideZoneID(10)))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -496,8 +424,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update size)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(1, Some(SquadHeader(128, true, Some(2), SquadInfo(6, None)))),
SquadListing(255)
SquadListing(1, SquadInfo(6))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -508,8 +435,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update leader and size)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(5, Some(SquadHeader(129, false, Some(0), SquadInfo("Jimmyn", 3)))),
SquadListing(255)
SquadListing(5, SquadInfo("Jimmyn").And(SquadInfo(3)))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -520,8 +446,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update task and continent)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(5, Some(SquadHeader(129, false, Some(1), SquadInfo("2", PlanetSideZoneID(4))))),
SquadListing(255)
SquadListing(5, SquadInfo(None, "2").And(SquadInfo(PlanetSideZoneID(4))))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -532,8 +457,7 @@ class ReplicationStreamMessageTest extends Specification {
"encode (update all)" in {
val msg = ReplicationStreamMessage(6, None,
Vector(
SquadListing(7, Some(SquadHeader(131, false, None, SquadInfo("madmuj", "", PlanetSideZoneID(4), 0, 10, PlanetSideGUID(11))))),
SquadListing(255)
SquadListing(7, SquadInfo("madmuj", "", PlanetSideZoneID(4), 0, 10, PlanetSideGUID(11)))
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@ -541,116 +465,15 @@ class ReplicationStreamMessageTest extends Specification {
pkt mustEqual stringUpdateAll
}
"encode (fails)" in {
//encode codec fail
PacketCoding.EncodePacket(
ReplicationStreamMessage(1, None,
Vector(
SquadListing(5, Some(SquadHeader(0, false, Some(4)))),
SquadListing(255)
)
"encode (remove 1 and update 0)" in {
val msg = ReplicationStreamMessage(1, None,
Vector(
SquadListing(1, None),
SquadListing(0, SquadInfo(10))
)
).isFailure mustEqual true
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
//encode one
PacketCoding.EncodePacket(
ReplicationStreamMessage(5, Some(6),
Vector(
SquadListing(0, Some(SquadHeader(131, false, None, Some(SquadInfo(Some("FragLANdINC"), Some("Frag"), None, Some(0),Some(10), Some(PlanetSideGUID(1))))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode two
PacketCoding.EncodePacket(
ReplicationStreamMessage(5, Some(6),
Vector(
SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))),
SquadListing(1, Some(SquadHeader(131, false, None, Some(SquadInfo(Some("KOKkiasMFCN"), Some("Squad 2"), None, Some(6), Some(10), Some(PlanetSideGUID(4))))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode leader
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(2, Some(SquadHeader(128, true, Some(0), Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode task
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(5, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode continent
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(3, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode task or continent
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(3, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, Some(""), Some(PlanetSideZoneID(10)), None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode size
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(1, Some(SquadHeader(128, true, Some(2), Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode leader and size
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(5, Some(SquadHeader(129, false, Some(0), Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode task and continent
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(5, Some(SquadHeader(129, false, Some(1), Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
//encode all
PacketCoding.EncodePacket(
ReplicationStreamMessage(6, None,
Vector(
SquadListing(7, Some(SquadHeader(131, false, None, Some(SquadInfo(None, None, None, None, None, None))))),
SquadListing(255)
)
)
).isFailure mustEqual true
pkt mustEqual stringRemoveUpdate
}
}

View file

@ -3,12 +3,17 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game.SquadAction._
import net.psforever.packet.game._
import net.psforever.types.CertificationType
import scodec.bits._
class SquadDefinitionActionMessageTest extends Specification {
//local test data; note that the second field - unk1 - is always blank for now, but that probably changes
val string_00 = hex"e7 00 0c0000" //guid: 3
val string_03 = hex"E7 0c 0000c0" //index: 3
val string_04 = hex"E7 10 0000c0" //index: 3
val string_07 = hex"e7 1c 0000e68043006f0070007300200061006e00640020004d0069006c006900740061007200790020004f006600660069006300650072007300"
val string_08 = hex"E7 20 000000"
val string_10 = hex"E7 28 000004" //index: 1
val string_19 = hex"E7 4c 0000218041002d005400650061006d00" //"A-Team"
@ -30,18 +35,48 @@ class SquadDefinitionActionMessageTest extends Specification {
val string_40 = hex"E7 a0 000004" //index: 1
val string_41 = hex"E7 a4 000000"
val string_43 = hex"e7 ac 000000"
val string_failure = hex"E7 ff"
"decode (00)" in {
PacketCoding.DecodePacket(string_00).require match {
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(3)
unk2 mustEqual 0
action mustEqual DisplaySquad()
case _ =>
ko
}
}
"decode (03)" in {
PacketCoding.DecodePacket(string_03).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 3
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 3
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SaveSquadFavorite()
case _ =>
ko
}
}
"decode (03)" in {
PacketCoding.DecodePacket(string_04).require match {
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 3
action mustEqual LoadSquadFavorite()
case _ =>
ko
}
}
"decode (07)" in {
PacketCoding.DecodePacket(string_07).require match {
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 3
action mustEqual ListSquadFavorite("Cops and Military Officers")
case _ =>
ko
}
@ -49,16 +84,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (08)" in {
PacketCoding.DecodePacket(string_08).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 8
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual RequestListSquad()
case _ =>
ko
}
@ -66,17 +95,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (10)" in {
PacketCoding.DecodePacket(string_10).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 10
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SelectRoleForYourself(1)
case _ =>
ko
}
@ -84,17 +106,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (19)" in {
PacketCoding.DecodePacket(string_19).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 19
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "A-Team"
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual ChangeSquadPurpose("A-Team")
case _ =>
ko
}
@ -102,17 +117,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (20)" in {
PacketCoding.DecodePacket(string_20).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 20
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual ChangeSquadZone(PlanetSideZoneID(1))
case _ =>
ko
}
@ -120,17 +128,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (21)" in {
PacketCoding.DecodePacket(string_21).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 21
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual true
int1.get mustEqual 2
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual CloseSquadMemberPosition(2)
case _ =>
ko
}
@ -138,17 +139,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (22)" in {
PacketCoding.DecodePacket(string_22).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 22
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual true
int1.get mustEqual 2
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual AddSquadMemberPosition(2)
case _ =>
ko
}
@ -156,18 +150,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (23)" in {
PacketCoding.DecodePacket(string_23).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 23
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "BLUFOR"
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual ChangeSquadMemberRequirementsRole(1, "BLUFOR")
case _ =>
ko
}
@ -175,18 +161,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (24)" in {
PacketCoding.DecodePacket(string_24).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 24
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "kill bad dudes"
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes")
case _ =>
ko
}
@ -194,18 +172,13 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (25)" in {
PacketCoding.DecodePacket(string_25).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 25
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual false
long1.isDefined mustEqual true
long1.get mustEqual 536870928L
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual ChangeSquadMemberRequirementsCertifications(
1,
Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
)
case _ =>
ko
}
@ -213,16 +186,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (26)" in {
PacketCoding.DecodePacket(string_26).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 26
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual ResetAll()
case _ =>
ko
}
@ -230,17 +197,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (28)" in {
PacketCoding.DecodePacket(string_28).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 28
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual true
bool.get mustEqual true
action mustEqual AutoApproveInvitationRequests(true)
case _ =>
ko
}
@ -248,17 +208,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (31)" in {
PacketCoding.DecodePacket(string_31).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 31
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual true
bool.get mustEqual true
action mustEqual LocationFollowsSquadLead(true)
case _ =>
ko
}
@ -266,20 +219,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (34a)" in {
PacketCoding.DecodePacket(string_34a).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 34
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "Badass"
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual true
int2.get mustEqual 0
long1.isDefined mustEqual true
long1.get mustEqual 0
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 1, SearchMode.AnyPositions)
case _ =>
ko
}
@ -287,20 +230,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (34b)" in {
PacketCoding.DecodePacket(string_34b).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 34
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "Badass"
int1.isDefined mustEqual true
int1.get mustEqual 2
int2.isDefined mustEqual true
int2.get mustEqual 0
long1.isDefined mustEqual true
long1.get mustEqual 0
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AnyPositions)
case _ =>
ko
}
@ -308,20 +241,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (34c)" in {
PacketCoding.DecodePacket(string_34c).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 34
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "Badass"
int1.isDefined mustEqual true
int1.get mustEqual 2
int2.isDefined mustEqual true
int2.get mustEqual 1
long1.isDefined mustEqual true
long1.get mustEqual 0
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AvailablePositions)
case _ =>
ko
}
@ -329,20 +252,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (34d)" in {
PacketCoding.DecodePacket(string_34d).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 34
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "Badass"
int1.isDefined mustEqual true
int1.get mustEqual 2
int2.isDefined mustEqual true
int2.get mustEqual 2
long1.isDefined mustEqual true
long1.get mustEqual 536870928L
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications)
case _ =>
ko
}
@ -350,20 +263,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (34e)" in {
PacketCoding.DecodePacket(string_34e).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 34
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual true
str.get mustEqual "Badass"
int1.isDefined mustEqual true
int1.get mustEqual 2
int2.isDefined mustEqual true
int2.get mustEqual 3
long1.isDefined mustEqual true
long1.get mustEqual 536870928L
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications)
case _ =>
ko
}
@ -371,16 +274,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (35)" in {
PacketCoding.DecodePacket(string_35).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 35
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual CancelSquadSearch()
case _ =>
ko
}
@ -388,17 +285,10 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (40)" in {
PacketCoding.DecodePacket(string_40).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 40
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual true
int1.get mustEqual 1
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual FindLfsSoldiersForRole(1)
case _ =>
ko
}
@ -406,165 +296,205 @@ class SquadDefinitionActionMessageTest extends Specification {
"decode (41)" in {
PacketCoding.DecodePacket(string_41).require match {
case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
action mustEqual 41
unk1 mustEqual 0
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
str.isDefined mustEqual false
int1.isDefined mustEqual false
int2.isDefined mustEqual false
long1.isDefined mustEqual false
long2.isDefined mustEqual false
bool.isDefined mustEqual false
action mustEqual CancelFind()
case _ =>
ko
}
}
"decode (43, unknown)" in {
PacketCoding.DecodePacket(string_43).require match {
case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0
action mustEqual Unknown(43, hex"00".toBitVector.take(6))
case _ =>
ko
}
}
"decode (failure)" in {
PacketCoding.DecodePacket(string_failure).isFailure mustEqual true
}
"encode (00)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(3), 0, DisplaySquad())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_00
}
"encode (03)" in {
val msg = SquadDefinitionActionMessage(3, 0, 3, None, None, None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadFavorite())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_03
}
"encode (03)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadFavorite())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_04
}
"encode (07)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, ListSquadFavorite("Cops and Military Officers"))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_07
}
"encode (08)" in {
val msg = SquadDefinitionActionMessage(8, 0, 0, None, None, None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, RequestListSquad())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_08
}
"encode (10)" in {
val msg = SquadDefinitionActionMessage(10, 0, 0, None, Some(1), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SelectRoleForYourself(1))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_10
}
"encode (19)" in {
val msg = SquadDefinitionActionMessage(19, 0, 0, Some("A-Team"), None, None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadPurpose("A-Team"))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_19
}
"encode (20)" in {
val msg = SquadDefinitionActionMessage(20, 0, 0, None, Some(1), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadZone(PlanetSideZoneID(1)))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_20
}
"encode (21)" in {
val msg = SquadDefinitionActionMessage(21, 0, 0, None, Some(2), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, CloseSquadMemberPosition(2))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_21
}
"encode (22)" in {
val msg = SquadDefinitionActionMessage(22, 0, 0, None, Some(2), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, AddSquadMemberPosition(2))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_22
}
"encode (23)" in {
val msg = SquadDefinitionActionMessage(23, 0, 0, Some("BLUFOR"), Some(1), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsRole(1, "BLUFOR"))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_23
}
"encode (24)" in {
val msg = SquadDefinitionActionMessage(24, 0, 0, Some("kill bad dudes"), Some(1), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes"))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_24
}
"encode (25)" in {
val msg = SquadDefinitionActionMessage(25, 0, 0, None, Some(1), None, Some(536870928L), None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsCertifications(
1,
Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_25
}
"encode (26)" in {
val msg = SquadDefinitionActionMessage(26, 0, 0, None, None, None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ResetAll())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_26
}
"encode (28)" in {
val msg = SquadDefinitionActionMessage(28, 0, 0, None, None, None, None, None, Some(true))
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, AutoApproveInvitationRequests(true))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_28
}
"encode (31)" in {
val msg = SquadDefinitionActionMessage(31, 0, 0, None, None, None, None, None, Some(true))
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, LocationFollowsSquadLead(true))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_31
}
"encode (34a)" in {
val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(1), Some(0), Some(0L), None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 1, SearchMode.AnyPositions))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_34a
}
"encode (34b)" in {
val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(0), Some(0L), None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AnyPositions))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_34b
}
"encode (34c)" in {
val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(1), Some(0L), None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AvailablePositions))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_34c
}
"encode (34d)" in {
val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(2), Some(536870928L), None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_34d
}
"encode (34e)" in {
val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(3), Some(536870928L), None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_34e
}
"encode (35)" in {
val msg = SquadDefinitionActionMessage(35, 0, 0, None, None, None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, CancelSquadSearch())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_35
}
"encode (40)" in {
val msg = SquadDefinitionActionMessage(40, 0, 0, None, Some(1), None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, FindLfsSoldiersForRole(1))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_40
}
"encode (41)" in {
val msg = SquadDefinitionActionMessage(41, 0, 0, None, None, None, None, None, None)
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, CancelFind())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_41
}
"encode (43, unknown)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, Unknown(43, BitVector.empty))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_43
}
}

View file

@ -0,0 +1,716 @@
// Copyright (c) 2019 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.CertificationType
import org.specs2.mutable._
import scodec.bits._
class SquadDetailDefinitionUpdateMessageTest extends Specification {
val string_unk1 = hex"e80300818800015c5189004603408c000000012000ff"
val string_leader_char_id = hex"e8050080904d56b808"
val string_unk3LeaderName = hex"e80300821104145011b9be840024284a00610061006b006f008c008118000000024000ff"
val string_task = hex"e8050080ac6041006c006c002000570065006c0063006f006d0065002000"
val string_zone = hex"e8030080b0a8000000"
val string_taskZone = hex"e80200812ce05c002300460046003000300030003000200054006800650020005c002300660066006600660066006600200042006c0061006400650073006040000000"
val string_unk7 = hex"e8030081ac8054006800650020004b0069006e00670027007300200053007100750061006400788c09808c4854006800650020004700750061007200640008808c5054006800650020004b006e00690067006800740007808c4054006800650020004500610072006c0006808c4054006800650020004c006f007200640005808c405400680065002000440075006b00650004808c4854006800650020004200610072006f006e0003808c6054006800650020005000720069006e00630065007300730002808c5054006800650020005000720069006e006300650001808c48540068006500200051007500650065006e0000808c4054006800650020004b0069006e006700ff"
val string_member_closed = hex"e8030080c602c043fe"
val string_member_role = hex"e8070080c60040462443006f006d006d0061006e00640065007200ff"
val string_member_roleRequirements = hex"e8010080c60340862841004400560020004800610063006b00650072005000000002003fc0"
val string_member_charIdName = hex"e8030080c602c08f2658480123004400750063006b006d006100730074006500720034003300ff"
val string_task_memberEtc = hex"e80100812ce05c002300460046003000300030003000200054006800650020005c002300660066006600660066006600200042006c0061006400650073008c09810c005000000000000220230007808c0006808c0005808c0004808c0003808c0002808c0001808c0000808c00ff"
val string_full = hex"e80300848180038021514601288a8400420048006f0066004400bf5c0023006600660064006300300030002a002a002a005c0023003900360034003000660066003d004b004f004b002b005300500043002b0046004c0059003d005c0023006600660064006300300030002a002a002a005c002300460046003400300034003000200041006c006c002000570065006c0063006f006d006500070000009814010650005c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c00230066006600640063003000300020002000200043008000000000800100000c00020c8c5c002300660066006400630030003000200020002000480080eab58a02854f0070006f006c0045000100000c00020c8d5c002300660066006400630030003000200020002000200049008072d47a028b42006f006200610046003300740074003900300037000100000c00020c8c5c0023006600660064006300300030002000200020004e008000000000800100000c00020c8c5c00230066006600640063003000300020002000200041008000000000800100000c00020ca05c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004f008042a28c028448006f00660044000100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c0000"
val string_mixed = hex"e80300812cd85000530046006f007200650076006500720020005000610063006b0065007400200043006f006c006c0065006300740069006f006e00841400000181306400800000000080000000000000220c808000000000800000000000001e0c808000000000800000000000001a0c80800000000080000000000000160c80800000000080000000000000120c808000000000800000000000000e0c808000000000800000000000000a0c80800000000080000000000000060c80800000000080000000000000020c80800000000080000000000003fc"
"SquadDetailDefinitionUpdateMessage" should {
"decode (unk1 + members)" in {
PacketCoding.DecodePacket(string_unk1).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(Some(unk1), None, Some(char_id), None, None, None, None, None, Some(_)) =>
unk1 mustEqual 0
char_id mustEqual 1221560L
//members tests follow ...
case _ =>
ko
}
case _ =>
ko
}
}
"decode (char id)" in {
PacketCoding.DecodePacket(string_leader_char_id).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(5)
detail match {
case SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) =>
char_id mustEqual 30910985
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (unk3 + leader name)" in {
PacketCoding.DecodePacket(string_unk3LeaderName).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(None, None, Some(char_id), Some(unk3), Some(leader), None, None, None, Some(_)) =>
char_id mustEqual 42631712L
unk3 mustEqual 556403L
leader mustEqual "Jaako"
//members tests follow ...
case _ =>
ko
}
case _ =>
ko
}
}
"decode (task)" in {
PacketCoding.DecodePacket(string_task).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(5)
detail match {
case SquadDetail(None, None, None, None, None, Some(task), None, None, None) =>
task mustEqual "All Welcome "
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (zone)" in {
PacketCoding.DecodePacket(string_zone).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(None, None, None, None, None, None, Some(zone), None, None) =>
zone mustEqual PlanetSideZoneID(21)
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (task + zone)" in {
PacketCoding.DecodePacket(string_taskZone).require match {
case SquadDetailDefinitionUpdateMessage(_, detail) =>
detail match {
case SquadDetail(None, None, None, None, None, Some(task), Some(zone), None, None) =>
task mustEqual "\\#FF0000 The \\#ffffff Blades"
zone mustEqual PlanetSideZoneID(4)
case _ =>
ko
}
case _ =>
ko
}
ok
}
"decode (unk7 + members)" in {
PacketCoding.DecodePacket(string_unk7).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(None, None, None, None, None, Some(task), None, Some(unk7), Some(_)) =>
task mustEqual "The King's Squad"
unk7 mustEqual 8
//members tests follow ...
case _ =>
ko
}
case _ =>
ko
}
}
"decode (member closed)" in {
PacketCoding.DecodePacket(string_member_closed).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
members.size mustEqual 1
members.head.index mustEqual 5
members.head.info match {
case Some(SquadPositionDetail(Some(is_closed), None, None, None, None, None)) =>
is_closed mustEqual true
case _ =>
ko
}
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (member role)" in {
PacketCoding.DecodePacket(string_member_role).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(7)
detail match {
case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
members.size mustEqual 1
members.head.index mustEqual 0
members.head.info match {
case Some(SquadPositionDetail(None, Some(role), None, None, None, None)) =>
role mustEqual "Commander"
case _ =>
ko
}
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (member role + requirements)" in {
PacketCoding.DecodePacket(string_member_roleRequirements).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(1)
detail match {
case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
members.size mustEqual 1
members.head.index mustEqual 6
members.head.info match {
case Some(SquadPositionDetail(None, Some(role), None, Some(req), None, None)) =>
role mustEqual "ADV Hacker"
req.size mustEqual 1
req.contains(CertificationType.AdvancedHacking) mustEqual true
case _ =>
ko
}
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (member char id + name)" in {
PacketCoding.DecodePacket(string_member_charIdName).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
members.size mustEqual 1
members.head.index mustEqual 5
members.head.info match {
case Some(SquadPositionDetail(None, None, None, None, Some(char_id), Some(name))) =>
char_id mustEqual 1218249L
name mustEqual "Duckmaster43"
case _ =>
ko
}
case _ =>
ko
}
ok
case _ =>
ko
}
}
"decode (task + member etc)" in {
PacketCoding.DecodePacket(string_task_memberEtc).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(1)
detail match {
case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(members)) =>
task mustEqual "\\#FF0000 The \\#ffffff Blades"
members.size mustEqual 10
//
members.head.index mustEqual 9
members.head.info match {
case Some(SquadPositionDetail(None, Some(role), None, Some(req), None, None)) =>
role mustEqual ""
req mustEqual Set.empty
case _ =>
ko
}
//
(1 to 9).foreach { index =>
members(index).index mustEqual 9 - index
members(index).info match {
case Some(SquadPositionDetail(None, Some(role), None, None, None, None)) =>
role mustEqual ""
case _ =>
ko
}
}
case _ =>
ko
}
case _ =>
ko
}
ok
}
"decode (full squad)" in {
PacketCoding.DecodePacket(string_full).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
u1 mustEqual 3
u2 mustEqual 1792
char_id mustEqual 42771010L
u3 mustEqual 529745L
leader mustEqual "HofD"
task mustEqual "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome"
zone mustEqual PlanetSideZoneID(7)
unk7 mustEqual 4983296
member_list.size mustEqual 10
member_list.head mustEqual SquadPositionEntry(0,Some(
SquadPositionDetail(
Some(false),
Some("\\#ff0000 |||||||||||||||||||||||"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")))
)
member_list(1) mustEqual SquadPositionEntry(1,Some(
SquadPositionDetail(
Some(false),
Some("\\#ffdc00 C"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")))
)
member_list(2) mustEqual SquadPositionEntry(2,Some(
SquadPositionDetail(
Some(false),
Some("\\#ffdc00 H"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(42644970L),
Some("OpolE")
)
))
member_list(3) mustEqual SquadPositionEntry(3,Some(
SquadPositionDetail(
Some(false),
Some("\\#ffdc00 I"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(41604210L),
Some("BobaF3tt907")
)
))
member_list(4) mustEqual SquadPositionEntry(4,Some(
SquadPositionDetail(
Some(false),
Some("\\#ffdc00 N"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")
)
))
member_list(5) mustEqual SquadPositionEntry(5,Some(
SquadPositionDetail(
Some(false),
Some("\\#ffdc00 A"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")
)
))
member_list(6) mustEqual SquadPositionEntry(6,Some(
SquadPositionDetail(
Some(false),
Some("\\#ff0000 |||||||||||||||||||||||"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")
)
))
member_list(7) mustEqual SquadPositionEntry(7,Some(
SquadPositionDetail(
Some(false),
Some("\\#9640ff K"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")
)
))
member_list(8) mustEqual SquadPositionEntry(8,Some(
SquadPositionDetail(
Some(false),
Some("\\#9640ff O"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(42771010L),
Some("HofD")
)
))
member_list(9) mustEqual SquadPositionEntry(9,Some(
SquadPositionDetail(
Some(false),
Some("\\#9640ff K"),
Some(""),
Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
Some(0),
Some("")
)
))
case _ =>
ko
}
case _ =>
ko
}
}
"decode (mixed)" in {
PacketCoding.DecodePacket(string_mixed).require match {
case SquadDetailDefinitionUpdateMessage(guid, detail) =>
guid mustEqual PlanetSideGUID(3)
detail match {
case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(member_list)) =>
task mustEqual "PSForever Packet Collection"
member_list.size mustEqual 10
member_list.head mustEqual SquadPositionEntry(9,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
))
)
member_list(1) mustEqual SquadPositionEntry(8,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
))
)
member_list(2) mustEqual SquadPositionEntry(7,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(3) mustEqual SquadPositionEntry(6,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(4) mustEqual SquadPositionEntry(5,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(5) mustEqual SquadPositionEntry(4,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(6) mustEqual SquadPositionEntry(3,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(7) mustEqual SquadPositionEntry(2,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(8) mustEqual SquadPositionEntry(1,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
member_list(9) mustEqual SquadPositionEntry(0,Some(
SquadPositionDetail(
Some(false),
Some(""),
Some(""),
Some(Set.empty),
Some(0),
Some("")
)
))
case _ =>
ko
}
case _ =>
ko
}
ok
}
"encode (unk1 + members)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail()
.Field1(0)
.LeaderCharId(1221560L)
.Members(List(
SquadPositionEntry(6, SquadPositionDetail().Player(0L, ""))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_unk1
}
"encode (char id)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(5),
SquadDetail().LeaderCharId(30910985L)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_leader_char_id
}
"encode (unk3 + leader name)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail()
.Leader(42631712L, "Jaako")
.Field3(556403L)
.Members(List(
SquadPositionEntry(0, SquadPositionDetail().Player(0L, ""))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_unk3LeaderName
}
"encode (task)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(5),
SquadDetail().Task("All Welcome ")
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_task
}
"encode (zone)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail().ZoneId(PlanetSideZoneID(21))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_zone
}
"encode (task + zone)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(2),
SquadDetail()
.Task("\\#FF0000 The \\#ffffff Blades")
.ZoneId(PlanetSideZoneID(4))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_taskZone
}
"encode (unk7 + members)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail()
.Task("The King's Squad")
.Field7(8)
.Members(List(
SquadPositionEntry(9, SquadPositionDetail().Role("The Guard")),
SquadPositionEntry(8, SquadPositionDetail().Role("The Knight")),
SquadPositionEntry(7, SquadPositionDetail().Role("The Earl")),
SquadPositionEntry(6, SquadPositionDetail().Role("The Lord")),
SquadPositionEntry(5, SquadPositionDetail().Role("The Duke")),
SquadPositionEntry(4, SquadPositionDetail().Role("The Baron")),
SquadPositionEntry(3, SquadPositionDetail().Role("The Princess")),
SquadPositionEntry(2, SquadPositionDetail().Role("The Prince")),
SquadPositionEntry(1, SquadPositionDetail().Role("The Queen")),
SquadPositionEntry(0, SquadPositionDetail().Role("The King"))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_unk7
}
"encode (member closed)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail()
.Members(List(
SquadPositionEntry(5, SquadPositionDetail.Closed)
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_member_closed
}
"encode (member role)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(7),
SquadDetail()
.Members(List(
SquadPositionEntry(0, SquadPositionDetail().Role("Commander"))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_member_role
}
"encode (member role + requirements)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(1),
SquadDetail()
.Members(List(
SquadPositionEntry(6, SquadPositionDetail()
.Role("ADV Hacker")
.Requirements(Set(CertificationType.AdvancedHacking)))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_member_roleRequirements
}
"encode (member char id + name)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail()
.Members(List(
SquadPositionEntry(5, SquadPositionDetail().Player(1218249L, "Duckmaster43"))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_member_charIdName
}
"encode (task + member etc)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(1),
SquadDetail()
.Task("\\#FF0000 The \\#ffffff Blades")
.Members(List(
SquadPositionEntry(9, SquadPositionDetail().Role("").Requirements(Set())),
SquadPositionEntry(8, SquadPositionDetail().Role("")),
SquadPositionEntry(7, SquadPositionDetail().Role("")),
SquadPositionEntry(6, SquadPositionDetail().Role("")),
SquadPositionEntry(5, SquadPositionDetail().Role("")),
SquadPositionEntry(4, SquadPositionDetail().Role("")),
SquadPositionEntry(3, SquadPositionDetail().Role("")),
SquadPositionEntry(2, SquadPositionDetail().Role("")),
SquadPositionEntry(1, SquadPositionDetail().Role("")),
SquadPositionEntry(0, SquadPositionDetail().Role(""))
))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_task_memberEtc
}
"encode (full squad)" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail(
3,
1792,
42771010L,
529745L,
"HofD",
"\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
PlanetSideZoneID(7),
4983296,
List(
SquadPositionEntry(0, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
SquadPositionEntry(1, SquadPositionDetail("\\#ffdc00 C", "", Set(), 0, "")),
SquadPositionEntry(2, SquadPositionDetail("\\#ffdc00 H", "", Set(), 42644970L, "OpolE")),
SquadPositionEntry(3, SquadPositionDetail("\\#ffdc00 I", "", Set(), 41604210L, "BobaF3tt907")),
SquadPositionEntry(4, SquadPositionDetail("\\#ffdc00 N", "", Set(), 0, "")),
SquadPositionEntry(5, SquadPositionDetail("\\#ffdc00 A", "", Set(), 0, "")),
SquadPositionEntry(6, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
SquadPositionEntry(7, SquadPositionDetail("\\#9640ff K", "", Set(), 0, "")),
SquadPositionEntry(8, SquadPositionDetail("\\#9640ff O", "", Set(), 42771010L ,"HofD")),
SquadPositionEntry(9, SquadPositionDetail("\\#9640ff K", "", Set(), 0, ""))
)
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_full
}
"encode (mixed)" in {
val position = Some(SquadPositionDetail("", "", Set(), 0, ""))
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
SquadDetail
.Task("PSForever Packet Collection")
.Members((0 to 9).map { index => SquadPositionEntry(index, position) }.reverse.toList)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_mixed
}
}
}

View file

@ -0,0 +1,32 @@
// Copyright (c) 2019 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game._
import org.specs2.mutable._
import scodec.bits._
class SquadMemberEventTest extends Specification {
val string = hex"7000e008545180410848006f0066004400070051150800"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) =>
u1 mustEqual MemberEvent.Add
u2 mustEqual 7
u3 mustEqual 42771010L
u4 mustEqual 0
u5.contains("HofD") mustEqual true
u6.contains(7) mustEqual true
u7.contains(529745L) mustEqual true
case _ =>
ko
}
}
"encode" in {
val msg = SquadMemberEvent(MemberEvent.Add, 7, 42771010L, 0, Some("HofD"), Some(7), Some(529745L))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,67 @@
// Copyright (c) 2019 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.SquadRequestType
import org.specs2.mutable._
import scodec.bits._
class SquadMembershipRequestTest extends Specification {
//481c897e-b47b-41cc-b7ad-c604606d985e / PSCap-2016-03-18_12-48-12-PM.gcap / Game record 5521 at 662.786844s
val string1 = hex"6e015aa7a0224d87a0280000"
//... / PSCap-2016-06-29_07-49-26-PM (last).gcap / Game record 77 at 9.732430 (found in MultiPacket)
val string2 = hex"6E265DD7A02800"
//TODO find example where player_name field is defined
//TODO find example where unk field is defined and is a string
"decode (1)" in {
PacketCoding.DecodePacket(string1).require match {
case SquadMembershipRequest(req_type, unk2, unk3, p_name, unk5) =>
req_type mustEqual SquadRequestType.Invite
unk2 mustEqual 41593365L
unk3.contains(41605156L) mustEqual true
p_name mustEqual ""
unk5.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (2)" in {
PacketCoding.DecodePacket(string2).require match {
case SquadMembershipRequest(req_type, unk2, unk3, p_name, unk5) =>
req_type mustEqual SquadRequestType.Accept
unk2 mustEqual 41606501L
unk3.isEmpty mustEqual true
p_name mustEqual ""
unk5.isEmpty mustEqual true
case _ =>
ko
}
}
"encode (1)" in {
val msg = SquadMembershipRequest(SquadRequestType.Invite, 41593365, Some(41605156L), "", Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string1
}
"encode (1; failure 1)" in {
SquadMembershipRequest(SquadRequestType.Invite, 41593365, None, "", Some(None)) must throwA[AssertionError]
}
"encode (1; failure 2)" in {
SquadMembershipRequest(SquadRequestType.Invite, 41593365, Some(41605156L), "", None) must throwA[AssertionError]
}
"encode (2)" in {
val msg = SquadMembershipRequest(SquadRequestType.Accept, 41606501, None, "", None)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string2
}
"encode (2; failure)" in {
SquadMembershipRequest(SquadRequestType.Accept, 41606501, Some(41606501), "", None) must throwA[AssertionError]
}
}

View file

@ -0,0 +1,421 @@
// Copyright (c) 2019 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.SquadResponseType
import org.specs2.mutable._
import scodec.bits._
class SquadMembershipResponseTest extends Specification {
val string_01 = hex"6f0 00854518050db2260108048006f006600440000"
val string_02 = hex"6f0 0049e8220112aa1e01100530050004f0049004c0045005200530080"
val string_11 = hex"6f1 995364f2040000000100080"
val string_12 = hex"6f1 90cadcf4040000000100080"
val string_21 = hex"6f2 010db2260085451805140560069007200750073004700690076006500720080"
val string_22 = hex"6f2 010db22601da03aa03140560069007200750073004700690076006500720080"
val string_31 = hex"6f3 07631db202854518050a048004d0046004900430000"
val string_32 = hex"6f3 04c34fb402854518050e0440041004e00310031003100310000"
val string_41 = hex"6f4 04cadcf405bbbef405140530041007200610069007300560061006e00750000"
val string_42 = hex"6f4 05c9c0f405d71aec0516041006900720049006e006a006500630074006f00720000"
val string_51 = hex"6f5 0249e8220049e822010e0430043005200490044004500520080"
val string_71 = hex"6f7 1049e822000000000100080"
val string_72 = hex"6f7 00cadcf4041355ae03100570069007a006b00690064003400350080"
val string_81 = hex"6f8 001355ae02cadcf405100570069007a006b00690064003400350000"
val string_91 = hex"6f9 008310080115aef40500080"
val string_92 = hex"6f9 001355ae02cadcf405100570069007a006b00690064003400350000"
val string_b1 = hex"6fb 021355ae02cadcf405140530041007200610069007300560061006e00750000"
"SquadMembershipResponse" should {
"decode (0-1)" in {
PacketCoding.DecodePacket(string_01).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Invite
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 42771010L
unk5.contains(1300870L) mustEqual true
unk6 mustEqual "HofD"
unk7 mustEqual false
unk8.isEmpty mustEqual true
case _ =>
ko
}
}
"decode (0-2)" in {
PacketCoding.DecodePacket(string_02).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Invite
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 1176612L
unk5.contains(1004937L) mustEqual true
unk6 mustEqual "SPOILERS"
unk7 mustEqual true
unk8.isEmpty mustEqual true
case _ =>
ko
}
}
"decode (1-1)" in {
PacketCoding.DecodePacket(string_11).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Unk01
unk2 mustEqual 19
unk3 mustEqual 0
unk4 mustEqual 41530025L
unk5.contains(0L) mustEqual true
unk6 mustEqual ""
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (1-2)" in {
PacketCoding.DecodePacket(string_12).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Unk01
unk2 mustEqual 18
unk3 mustEqual 0
unk4 mustEqual 41578085L
unk5.contains(0L) mustEqual true
unk6 mustEqual ""
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (2-1)" in {
PacketCoding.DecodePacket(string_21).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Accept
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 1300870L
unk5.contains(42771010L) mustEqual true
unk6 mustEqual "VirusGiver"
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (2-2)" in {
PacketCoding.DecodePacket(string_22).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Accept
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 1300870L
unk5.contains(30736877L) mustEqual true
unk6 mustEqual "VirusGiver"
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (3-1)" in {
PacketCoding.DecodePacket(string_31).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Reject
unk2 mustEqual 0
unk3 mustEqual 3
unk4 mustEqual 31035057L
unk5.contains(42771010L) mustEqual true
unk6 mustEqual "HMFIC"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (3-2)" in {
PacketCoding.DecodePacket(string_32).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Reject
unk2 mustEqual 0
unk3 mustEqual 2
unk4 mustEqual 31106913L
unk5.contains(42771010L) mustEqual true
unk6 mustEqual "DAN1111"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (4-1)" in {
PacketCoding.DecodePacket(string_41).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Cancel
unk2 mustEqual 0
unk3 mustEqual 2
unk4 mustEqual 41578085L
unk5.contains(41607133L) mustEqual true
unk6 mustEqual "SAraisVanu"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (4-2)" in {
PacketCoding.DecodePacket(string_42).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Cancel
unk2 mustEqual 0
unk3 mustEqual 2
unk4 mustEqual 41607396L
unk5.contains(41324011L) mustEqual true
unk6 mustEqual "AirInjector"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (5-1)" in {
PacketCoding.DecodePacket(string_51).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.Leave
unk2 mustEqual 0
unk3 mustEqual 1
unk4 mustEqual 1176612L
unk5.contains(1176612L) mustEqual true
unk6 mustEqual "CCRIDER"
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (7-1)" in {
PacketCoding.DecodePacket(string_71).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.PlatoonInvite
unk2 mustEqual 2
unk3 mustEqual 0
unk4 mustEqual 1176612L
unk5.contains(0L) mustEqual true
unk6 mustEqual ""
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (7-2)" in {
PacketCoding.DecodePacket(string_72).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.PlatoonInvite
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 41578085L
unk5.contains(30910985L) mustEqual true
unk6 mustEqual "Wizkid45"
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (8-1)" in {
PacketCoding.DecodePacket(string_81).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.PlatoonAccept
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 30910985L
unk5.contains(41578085L) mustEqual true
unk6 mustEqual "Wizkid45"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (9-1)" in {
PacketCoding.DecodePacket(string_91).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.PlatoonReject
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 297025L
unk5.contains(41605002L) mustEqual true
unk6 mustEqual ""
unk7 mustEqual true
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (9-2)" in {
PacketCoding.DecodePacket(string_92).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.PlatoonReject
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 30910985L
unk5.contains(41578085L) mustEqual true
unk6 mustEqual "Wizkid45"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"decode (b-1)" in {
PacketCoding.DecodePacket(string_b1).require match {
case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
unk1 mustEqual SquadResponseType.PlatoonLeave
unk2 mustEqual 0
unk3 mustEqual 1
unk4 mustEqual 30910985L
unk5.contains(41578085L) mustEqual true
unk6 mustEqual "SAraisVanu"
unk7 mustEqual false
unk8.contains(None) mustEqual true
case _ =>
ko
}
}
"encode (0-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Invite, 0, 0, 42771010L, Some(1300870L), "HofD", false, None)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_01
}
"encode (0-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.Invite, 0, 0, 1176612L, Some(1004937L), "SPOILERS", true, None)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_02
}
"encode (1-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Unk01, 19, 0, 41530025L, Some(0L), "", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_11
}
"encode (1-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.Unk01, 18, 0, 41578085L, Some(0L), "", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_12
}
"encode (2-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Accept, 0, 0, 1300870L, Some(42771010L), "VirusGiver", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_21
}
"encode (2-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.Accept, 0, 0, 1300870L, Some(30736877L), "VirusGiver", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_22
}
"encode (3-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Reject, 0, 3, 31035057L, Some(42771010L), "HMFIC", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_31
}
"encode (3-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.Reject, 0, 2, 31106913L, Some(42771010L), "DAN1111", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_32
}
"encode (4-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Cancel, 0, 2, 41578085L, Some(41607133L), "SAraisVanu", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_41
}
"encode (4-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.Cancel, 0, 2, 41607396L, Some(41324011L), "AirInjector", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_42
}
"encode (5-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.Leave, 0, 1, 1176612L, Some(1176612L), "CCRIDER", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_51
}
"encode (7-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.PlatoonInvite, 2, 0, 1176612L, Some(0L), "", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_71
}
"encode (7-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.PlatoonInvite, 0, 0, 41578085L, Some(30910985L), "Wizkid45", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_72
}
"encode (8-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.PlatoonAccept, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_81
}
"encode (9-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.PlatoonReject, 0, 0, 297025L, Some(41605002L), "", true, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_91
}
"encode (9-2)" in {
val msg = SquadMembershipResponse(SquadResponseType.PlatoonReject, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_92
}
"encode (b-1)" in {
val msg = SquadMembershipResponse(SquadResponseType.PlatoonLeave, 0, 1, 30910985L, Some(41578085L), "SAraisVanu", false, Some(None))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_b1
}
}
}

View file

@ -0,0 +1,244 @@
// Copyright (c) 2019 PSForever
package game
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.Vector3
import org.specs2.mutable._
import scodec.bits._
class SquadStateTest extends Specification {
val string1 = hex"770700186d9130081001b11b27c1c041680000"
val string2 = hex"770700242a28c020003e9237a90e3382695004eab58a0281017eb95613df4c42950040"
val stringx = hex"7704008dd9ccf010042a9837310e1b82a8c006646c7a028103984f34759c904a800014f01c26f3d014081ddd3896931bc25478037680ea80c081d699a147b01e154000031c0bc81407e08c1a3a890de1542c022070bd0140815958bf29efa6214300108023c01000ae491ac68d1a61342c023623c50140011d6ea0878f3026a00009e014"
"decode (1)" in {
PacketCoding.DecodePacket(string1).require match {
case SquadState(guid, list) =>
guid mustEqual PlanetSideGUID(7)
list.size mustEqual 1
//0
list.head match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 1300870L
u2 mustEqual 64
u3 mustEqual 64
pos mustEqual Vector3(3464.0469f, 4065.5703f, 20.015625f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 0
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
case _ =>
ko
}
}
"decode (2)" in {
PacketCoding.DecodePacket(string2).require match {
case SquadState(guid, list) =>
guid mustEqual PlanetSideGUID(7)
list.size mustEqual 2
//0
list.head match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 42771010L
u2 mustEqual 0
u3 mustEqual 0
pos mustEqual Vector3(6801.953f, 4231.828f, 39.21875f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 680
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(1) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 42644970L
u2 mustEqual 64
u3 mustEqual 64
pos mustEqual Vector3(2908.7422f, 3742.6875f, 67.296875f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 680
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
case _ =>
ko
}
}
"decode (8)" in {
PacketCoding.DecodePacket(stringx).require match {
case SquadState(guid, list) =>
guid mustEqual PlanetSideGUID(4)
list.size mustEqual 8
//0
list.head match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 30383325L
u2 mustEqual 0
u3 mustEqual 16
pos mustEqual Vector3(6849.328f, 4231.5938f, 41.71875f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 864
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(1) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 41577572L
u2 mustEqual 64
u3 mustEqual 64
pos mustEqual Vector3(6183.797f, 4013.6328f, 72.5625f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 0
u8.contains(335) mustEqual true
u9.contains(true) mustEqual true
case _ =>
ko
}
list(2) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 41606788L
u2 mustEqual 64
u3 mustEqual 64
pos mustEqual Vector3(6611.8594f, 4242.586f, 75.46875f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 888
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(3) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 30736877L
u2 mustEqual 64
u3 mustEqual 64
pos mustEqual Vector3(6809.836f, 4218.078f, 40.234375f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 0
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(4) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 41517411L
u2 mustEqual 64
u3 mustEqual 63
pos mustEqual Vector3(6848.0312f, 4232.2266f, 41.734375f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 556
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(5) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 41607488L
u2 mustEqual 64
u3 mustEqual 64
pos mustEqual Vector3(2905.3438f, 3743.9453f, 67.296875f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 304
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(6) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 41419792L
u2 mustEqual 0
u3 mustEqual 5
pos mustEqual Vector3(6800.8906f ,4236.7734f, 39.296875f)
u4 mustEqual 2
u5 mustEqual 2
u6 mustEqual false
u7 mustEqual 556
u8.isEmpty mustEqual true
u9.isEmpty mustEqual true
case _ =>
ko
}
list(7) match {
case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
char_id mustEqual 42616684L
u2 mustEqual 64
u3 mustEqual 0
pos mustEqual Vector3(2927.1094f, 3704.0312f, 78.375f)
u4 mustEqual 1
u5 mustEqual 1
u6 mustEqual false
u7 mustEqual 0
u8.contains(572) mustEqual true
u9.contains(true) mustEqual true
case _ =>
ko
}
case _ =>
ko
}
}
"encode (1)" in {
val msg = SquadState(PlanetSideGUID(7), List(
SquadStateInfo(1300870L, 64, 64, Vector3(3464.0469f, 4065.5703f, 20.015625f), 2, 2, false, 0)
))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string1
}
"encode (2)" in {
val msg = SquadState(PlanetSideGUID(7), List(
SquadStateInfo(42771010L, 0, 0, Vector3(6801.953f, 4231.828f, 39.21875f), 2, 2, false, 680),
SquadStateInfo(42644970L, 64, 64, Vector3(2908.7422f, 3742.6875f, 67.296875f), 2, 2, false, 680)
))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string2
}
"encode (8)" in {
val msg = SquadState(PlanetSideGUID(4), List(
SquadStateInfo(30383325L, 0, 16, Vector3(6849.328f, 4231.5938f, 41.71875f), 2, 2, false, 864),
SquadStateInfo(41577572L, 64, 64, Vector3(6183.797f, 4013.6328f, 72.5625f), 2, 2, false, 0, 335, true),
SquadStateInfo(41606788L, 64, 64, Vector3(6611.8594f, 4242.586f, 75.46875f), 2, 2, false, 888),
SquadStateInfo(30736877L, 64, 64, Vector3(6809.836f, 4218.078f, 40.234375f), 2, 2, false, 0),
SquadStateInfo(41517411L, 64, 63, Vector3(6848.0312f, 4232.2266f, 41.734375f), 2, 2, false, 556),
SquadStateInfo(41607488L, 64, 64, Vector3(2905.3438f, 3743.9453f, 67.296875f), 2, 2, false, 304),
SquadStateInfo(41419792L, 0, 5, Vector3(6800.8906f, 4236.7734f, 39.296875f), 2, 2, false, 556),
SquadStateInfo(42616684L, 64, 0, Vector3(2927.1094f, 3704.0312f, 78.375f), 1, 1, false, 0, 572, true)
))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringx
}
}

View file

@ -3,8 +3,8 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent}
import net.psforever.types.Vector3
import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent, WaypointEventAction}
import net.psforever.types.{SquadWaypoints, Vector3}
import scodec.bits._
class SquadWaypointEventTest extends Specification {
@ -16,12 +16,12 @@ class SquadWaypointEventTest extends Specification {
"decode (1)" in {
PacketCoding.DecodePacket(string_1).require match {
case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
unk1 mustEqual 2
unk1 mustEqual WaypointEventAction.Remove
unk2 mustEqual 11
unk3 mustEqual 31155863L
unk4 mustEqual 0
unk5 mustEqual None
unk6 mustEqual None
unk4 mustEqual SquadWaypoints.One
unk5.isEmpty mustEqual true
unk6.isEmpty mustEqual true
case _ =>
ko
}
@ -30,12 +30,12 @@ class SquadWaypointEventTest extends Specification {
"decode (2)" in {
PacketCoding.DecodePacket(string_2).require match {
case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
unk1 mustEqual 2
unk1 mustEqual WaypointEventAction.Remove
unk2 mustEqual 10
unk3 mustEqual 0L
unk4 mustEqual 4
unk5 mustEqual None
unk6 mustEqual None
unk4 mustEqual SquadWaypoints.ExperienceRally
unk5.isEmpty mustEqual true
unk6.isEmpty mustEqual true
case _ =>
ko
}
@ -44,12 +44,12 @@ class SquadWaypointEventTest extends Specification {
"decode (3)" in {
PacketCoding.DecodePacket(string_3).require match {
case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
unk1 mustEqual 0
unk1 mustEqual WaypointEventAction.Add
unk2 mustEqual 3
unk3 mustEqual 41581052L
unk4 mustEqual 1
unk5 mustEqual None
unk6 mustEqual Some(WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1))
unk4 mustEqual SquadWaypoints.Two
unk5.isEmpty mustEqual true
unk6.contains( WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1) ) mustEqual true
case _ =>
ko
}
@ -58,40 +58,40 @@ class SquadWaypointEventTest extends Specification {
"decode (4)" in {
PacketCoding.DecodePacket(string_4).require match {
case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
unk1 mustEqual 1
unk1 mustEqual WaypointEventAction.Unknown1
unk2 mustEqual 3
unk3 mustEqual 41581052L
unk4 mustEqual 1
unk5 mustEqual Some(4L)
unk6 mustEqual None
unk4 mustEqual SquadWaypoints.Two
unk5.contains( 4L ) mustEqual true
unk6.isEmpty mustEqual true
case _ =>
ko
}
}
"encode (1)" in {
val msg = SquadWaypointEvent(2, 11, 31155863L, 0)
val msg = SquadWaypointEvent.Remove(11, 31155863L, SquadWaypoints.One)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_1
}
"encode (2)" in {
val msg = SquadWaypointEvent(2, 10, 0L, 4)
val msg = SquadWaypointEvent.Remove(10, 0L, SquadWaypoints.ExperienceRally)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_2
}
"encode (3)" in {
val msg = SquadWaypointEvent(0, 3, 41581052L, 1, 10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)
val msg = SquadWaypointEvent.Add(3, 41581052L, SquadWaypoints.Two, WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_3
}
"encode (4)" in {
val msg = SquadWaypointEvent(1, 3, 41581052L, 1, 4L)
val msg = SquadWaypointEvent.Unknown1(3, 41581052L, SquadWaypoints.Two, 4L)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_4

View file

@ -48,7 +48,7 @@ class CharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Reinforced
a.unk5 mustEqual 0
a.unk6 mustEqual 30777081L
a.char_id mustEqual 30777081L
a.unk7 mustEqual 1
a.unk8 mustEqual 4
a.unk9 mustEqual 0
@ -167,7 +167,7 @@ class CharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Reinforced
a.unk5 mustEqual 0
a.unk6 mustEqual 192L
a.char_id mustEqual 192L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
@ -236,7 +236,7 @@ class CharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.MAX
a.unk5 mustEqual 1
a.unk6 mustEqual 0L
a.char_id mustEqual 0L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0

View file

@ -76,7 +76,7 @@ class DetailedCharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Standard
a.unk5 mustEqual 0
a.unk6 mustEqual 41605313L
a.char_id mustEqual 41605313L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
@ -264,7 +264,7 @@ class DetailedCharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Standard
a.unk5 mustEqual 0
a.unk6 mustEqual 192L
a.char_id mustEqual 192L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
@ -449,7 +449,7 @@ class DetailedCharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.MAX
a.unk5 mustEqual 1
a.unk6 mustEqual 41605870L
a.char_id mustEqual 41605870L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
@ -657,7 +657,7 @@ class DetailedCharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Agile
a.unk5 mustEqual 0
a.unk6 mustEqual 733931L
a.char_id mustEqual 733931L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
@ -1165,7 +1165,7 @@ class DetailedCharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Standard
a.unk5 mustEqual 0
a.unk6 mustEqual 1176612L
a.char_id mustEqual 1176612L
a.unk7 mustEqual 15
a.unk8 mustEqual 5
a.unk9 mustEqual 10
@ -1315,7 +1315,7 @@ class DetailedCharacterDataTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Standard
a.unk5 mustEqual 0
a.unk6 mustEqual 1267466L
a.char_id mustEqual 1267466L
a.unk7 mustEqual 3
a.unk8 mustEqual 3
a.unk9 mustEqual 0

View file

@ -62,7 +62,7 @@ class MountedVehiclesTest extends Specification {
a.data.v5.isEmpty mustEqual true
a.exosuit mustEqual ExoSuitType.Agile
a.unk5 mustEqual 0
a.unk6 mustEqual 30777081L
a.char_id mustEqual 30777081L
a.unk7 mustEqual 1
a.unk8 mustEqual 4
a.unk9 mustEqual 0

View file

@ -121,19 +121,19 @@ class AvatarTest extends Specification {
obj.Implants(0).Active mustEqual false
obj.Implants(0).Implant mustEqual ImplantType.None
obj.Implant(0) mustEqual ImplantType.None
obj.Implants(0).Installed mustEqual None
obj.Implants(0).Installed.isEmpty mustEqual true
obj.Implants(1).Unlocked mustEqual false
obj.Implants(1).Initialized mustEqual false
obj.Implants(1).Active mustEqual false
obj.Implants(1).Implant mustEqual ImplantType.None
obj.Implant(1) mustEqual ImplantType.None
obj.Implants(1).Installed mustEqual None
obj.Implants(1).Installed.isEmpty mustEqual true
obj.Implants(2).Unlocked mustEqual false
obj.Implants(2).Initialized mustEqual false
obj.Implants(2).Active mustEqual false
obj.Implants(2).Implant mustEqual ImplantType.None
obj.Implant(2) mustEqual ImplantType.None
obj.Implants(2).Installed mustEqual None
obj.Implants(2).Installed.isEmpty mustEqual true
obj.Implant(3) mustEqual ImplantType.None //invalid slots beyond the third always reports as ImplantType.None
}
@ -142,10 +142,10 @@ class AvatarTest extends Specification {
val testplant : ImplantDefinition = ImplantDefinition(1)
val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.Implants(0).Unlocked = true
obj.InstallImplant(testplant) mustEqual Some(0)
obj.InstallImplant(testplant).contains(0) mustEqual true
obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant
case Some(slot) =>
slot.Installed mustEqual Some(testplant)
slot.Installed.contains(testplant) mustEqual true
case _ =>
ko
}
@ -159,8 +159,8 @@ class AvatarTest extends Specification {
obj.Implants(0).Unlocked = true
obj.Implants(1).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual Some(1)
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).contains(1) mustEqual true
}
"can not install the same type of implant twice" in {
@ -170,8 +170,8 @@ class AvatarTest extends Specification {
obj.Implants(0).Unlocked = true
obj.Implants(1).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual None
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).isEmpty mustEqual true
}
"can not install more implants than slots available (two unlocked)" in {
@ -182,9 +182,9 @@ class AvatarTest extends Specification {
obj.Implants(0).Unlocked = true
obj.Implants(1).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual Some(1)
obj.InstallImplant(testplant3) mustEqual None
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).contains(1) mustEqual true
obj.InstallImplant(testplant3).isEmpty mustEqual true
}
"can not install more implants than slots available (four implants)" in {
@ -197,21 +197,21 @@ class AvatarTest extends Specification {
obj.Implants(1).Unlocked = true
obj.Implants(2).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual Some(1)
obj.InstallImplant(testplant3) mustEqual Some(2)
obj.InstallImplant(testplant4) mustEqual None
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).contains(1) mustEqual true
obj.InstallImplant(testplant3).contains(2) mustEqual true
obj.InstallImplant(testplant4).isEmpty mustEqual true
}
"can uninstall an implant" in {
val testplant : ImplantDefinition = ImplantDefinition(1)
val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.Implants(0).Unlocked = true
obj.InstallImplant(testplant) mustEqual Some(0)
obj.Implants(0).Installed mustEqual Some(testplant)
obj.InstallImplant(testplant).contains(0) mustEqual true
obj.Implants(0).Installed.contains(testplant) mustEqual true
obj.UninstallImplant(testplant.Type) mustEqual Some(0)
obj.Implants(0).Installed mustEqual None
obj.UninstallImplant(testplant.Type).contains(0) mustEqual true
obj.Implants(0).Installed.isEmpty mustEqual true
}
"can uninstall just a specific implant" in {
@ -222,14 +222,14 @@ class AvatarTest extends Specification {
obj.Implants(0).Unlocked = true
obj.Implants(1).Unlocked = true
obj.Implants(2).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual Some(1)
obj.InstallImplant(testplant3) mustEqual Some(2)
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).contains(1) mustEqual true
obj.InstallImplant(testplant3).contains(2) mustEqual true
obj.Implant(0) mustEqual testplant1.Type
obj.Implant(1) mustEqual testplant2.Type
obj.Implant(2) mustEqual testplant3.Type
obj.UninstallImplant(testplant2.Type) mustEqual Some(1)
obj.UninstallImplant(testplant2.Type).contains(1) mustEqual true
obj.Implant(0) mustEqual testplant1.Type
obj.Implant(1) mustEqual ImplantType.None
obj.Implant(2) mustEqual testplant3.Type
@ -243,16 +243,16 @@ class AvatarTest extends Specification {
obj.Implants(0).Unlocked = true
obj.Implants(1).Unlocked = true
obj.Implants(2).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual Some(1)
obj.InstallImplant(testplant3) mustEqual Some(2)
obj.UninstallImplant(testplant2.Type) mustEqual Some(1)
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).contains(1) mustEqual true
obj.InstallImplant(testplant3).contains(2) mustEqual true
obj.UninstallImplant(testplant2.Type).contains(1) mustEqual true
obj.Implant(0) mustEqual testplant1.Type
obj.Implant(1) mustEqual ImplantType.None
obj.Implant(2) mustEqual testplant3.Type
val testplant4 : ImplantDefinition = ImplantDefinition(4)
obj.InstallImplant(testplant4) mustEqual Some(1)
obj.InstallImplant(testplant4).contains(1) mustEqual true
obj.Implant(0) mustEqual testplant1.Type
obj.Implant(1) mustEqual testplant4.Type
obj.Implant(2) mustEqual testplant3.Type
@ -264,8 +264,8 @@ class AvatarTest extends Specification {
val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.Implants(0).Unlocked = true
obj.Implants(1).Unlocked = true
obj.InstallImplant(testplant1) mustEqual Some(0)
obj.InstallImplant(testplant2) mustEqual Some(1)
obj.InstallImplant(testplant1).contains(0) mustEqual true
obj.InstallImplant(testplant2).contains(1) mustEqual true
obj.Implants(0).Initialized = true
obj.Implants(0).Active = true
obj.Implants(1).Initialized = true
@ -281,7 +281,7 @@ class AvatarTest extends Specification {
"does not have any loadout specifications by default" in {
val (_, avatar) = CreatePlayer()
(0 to 9).foreach { avatar.LoadLoadout(_) mustEqual None }
(0 to 9).foreach { avatar.EquipmentLoadouts.LoadLoadout(_).isEmpty mustEqual true }
ok
}
@ -289,9 +289,9 @@ class AvatarTest extends Specification {
val (obj, avatar) = CreatePlayer()
obj.Slot(0).Equipment.get.asInstanceOf[Tool].Magazine = 1 //non-standard but legal
obj.Slot(2).Equipment.get.asInstanceOf[Tool].AmmoSlot.Magazine = 100 //non-standard (and out of range, real=25)
avatar.SaveLoadout(obj, "test", 0)
avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
avatar.LoadLoadout(0) match {
avatar.EquipmentLoadouts.LoadLoadout(0) match {
case Some(items : InfantryLoadout) =>
items.label mustEqual "test"
items.exosuit mustEqual obj.ExoSuit
@ -329,25 +329,25 @@ class AvatarTest extends Specification {
"save player's current inventory as a loadout, only found in the called-out slot number" in {
val (obj, avatar) = CreatePlayer()
avatar.SaveLoadout(obj, "test", 0)
avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
avatar.LoadLoadout(1).isDefined mustEqual false
avatar.LoadLoadout(0).isDefined mustEqual true
avatar.EquipmentLoadouts.LoadLoadout(1).isDefined mustEqual false
avatar.EquipmentLoadouts.LoadLoadout(0).isDefined mustEqual true
}
"try to save player's current inventory as a loadout, but will not save to an invalid slot" in {
val (obj, avatar) = CreatePlayer()
avatar.SaveLoadout(obj, "test", 10)
avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 50)
avatar.LoadLoadout(10) mustEqual None
avatar.EquipmentLoadouts.LoadLoadout(50).isEmpty mustEqual true
}
"save player's current inventory as a loadout, without inventory contents" in {
val (obj, avatar) = CreatePlayer()
obj.Inventory.Clear()
avatar.SaveLoadout(obj, "test", 0)
avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
avatar.LoadLoadout(0) match {
avatar.EquipmentLoadouts.LoadLoadout(0) match {
case Some(items : InfantryLoadout) =>
items.label mustEqual "test"
items.exosuit mustEqual obj.ExoSuit
@ -364,9 +364,9 @@ class AvatarTest extends Specification {
obj.Slot(0).Equipment = None
obj.Slot(2).Equipment = None
obj.Slot(4).Equipment = None
avatar.SaveLoadout(obj, "test", 0)
avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
avatar.LoadLoadout(0) match {
avatar.EquipmentLoadouts.LoadLoadout(0) match {
case Some(items : InfantryLoadout) =>
items.label mustEqual "test"
items.exosuit mustEqual obj.ExoSuit
@ -380,11 +380,11 @@ class AvatarTest extends Specification {
"save, load, delete; rapidly" in {
val (obj, avatar) = CreatePlayer()
avatar.SaveLoadout(obj, "test", 0)
avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
avatar.LoadLoadout(0).isDefined mustEqual true
avatar.DeleteLoadout(0)
avatar.LoadLoadout(0) mustEqual None
avatar.EquipmentLoadouts.LoadLoadout(0).isDefined mustEqual true
avatar.EquipmentLoadouts.DeleteLoadout(0)
avatar.EquipmentLoadouts.LoadLoadout(0).isEmpty mustEqual true
}
"the fifth slot is the locker wrapped in an EquipmentSlot" in {

View file

@ -70,11 +70,11 @@ class DamageCalculationsTests extends Specification {
}
"calculate distance between target and source" in {
DistanceBetweenTargetandSource(resprojectile) mustEqual 10
DistanceBetweenTargetandSource(resprojectile) mustEqual 67.38225f
}
"calculate distance between target and explosion (splash)" in {
DistanceFromExplosionToTarget(resprojectile) mustEqual 64.03124f
DistanceFromExplosionToTarget(resprojectile) mustEqual 63.031242f
}
"calculate no damage from components" in {

View file

@ -245,34 +245,34 @@ class PlayerTest extends Specification {
val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.Slot(-1).Equipment = wep
obj.Slot(-1).Equipment mustEqual None
obj.Slot(-1).Equipment.isEmpty mustEqual true
}
"search for the smallest available slot in which to store equipment" in {
val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.Inventory.Resize(3,3) //fits one item
obj.Fit(Tool(GlobalDefinitions.beamer)) mustEqual Some(0)
obj.Fit(Tool(GlobalDefinitions.beamer)).contains(0) mustEqual true
obj.Fit(Tool(GlobalDefinitions.suppressor)) mustEqual Some(2)
obj.Fit(Tool(GlobalDefinitions.suppressor)).contains(2) mustEqual true
val ammo = AmmoBox(GlobalDefinitions.bullet_9mm)
val ammo2 = AmmoBox(GlobalDefinitions.bullet_9mm)
val ammo3 = AmmoBox(GlobalDefinitions.bullet_9mm)
obj.Fit(ammo) mustEqual Some(6)
obj.Fit(ammo).contains(6) mustEqual true
obj.Slot(6).Equipment = ammo
obj.Fit(ammo2) mustEqual Some(Player.FreeHandSlot)
obj.Fit(ammo2).contains(Player.FreeHandSlot) mustEqual true
obj.Slot(Player.FreeHandSlot).Equipment = ammo2
obj.Fit(ammo3) mustEqual None
obj.Fit(ammo3).isEmpty mustEqual true
}
"can use their free hand to hold things" in {
val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
val ammo = AmmoBox(GlobalDefinitions.bullet_9mm)
obj.FreeHand.Equipment mustEqual None
obj.FreeHand.Equipment.isEmpty mustEqual true
obj.FreeHand.Equipment = ammo
obj.FreeHand.Equipment mustEqual Some(ammo)
obj.FreeHand.Equipment.contains(ammo) mustEqual true
}
"can access the player's locker-space" in {
@ -308,12 +308,12 @@ class PlayerTest extends Specification {
item
}
obj.Find(PlanetSideGUID(1)) mustEqual Some(0) //holsters
obj.Find(PlanetSideGUID(2)) mustEqual Some(4) //holsters, melee
obj.Find(PlanetSideGUID(3)) mustEqual Some(6) //inventory
obj.Find(PlanetSideGUID(4)) mustEqual None //can not find in locker-space
obj.Find(PlanetSideGUID(5)) mustEqual Some(Player.FreeHandSlot) //free hand
obj.Find(PlanetSideGUID(6)) mustEqual None //not here
obj.Find(PlanetSideGUID(1)).contains(0) mustEqual true //holsters
obj.Find(PlanetSideGUID(2)).contains(4) mustEqual true //holsters, melee
obj.Find(PlanetSideGUID(3)).contains(6) mustEqual true //inventory
obj.Find(PlanetSideGUID(4)).isEmpty mustEqual true //can not find in locker-space
obj.Find(PlanetSideGUID(5)).contains(Player.FreeHandSlot) mustEqual true //free hand
obj.Find(PlanetSideGUID(6)).isEmpty mustEqual true //not here
}
"does equipment collision checking (are we already holding something there?)" in {
@ -437,20 +437,20 @@ class PlayerTest extends Specification {
"seat in a vehicle" in {
val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.VehicleSeated mustEqual None
obj.VehicleSeated.isEmpty mustEqual true
obj.VehicleSeated = PlanetSideGUID(65)
obj.VehicleSeated mustEqual Some(PlanetSideGUID(65))
obj.VehicleSeated.contains(PlanetSideGUID(65)) mustEqual true
obj.VehicleSeated = None
obj.VehicleSeated mustEqual None
obj.VehicleSeated.isEmpty mustEqual true
}
"own in a vehicle" in {
val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
obj.VehicleOwned mustEqual None
obj.VehicleOwned.isEmpty mustEqual true
obj.VehicleOwned = PlanetSideGUID(65)
obj.VehicleOwned mustEqual Some(PlanetSideGUID(65))
obj.VehicleOwned.contains(PlanetSideGUID(65)) mustEqual true
obj.VehicleOwned = None
obj.VehicleOwned mustEqual None
obj.VehicleOwned.isEmpty mustEqual true
}
"remember what zone he is in" in {

View file

@ -87,7 +87,7 @@ class OrderTerminalTest extends Specification {
player.ExoSuit = ExoSuitType.Agile
player.Slot(0).Equipment = Tool(GlobalDefinitions.beamer)
player.Slot(6).Equipment = Tool(GlobalDefinitions.beamer)
avatar.SaveLoadout(player, "test", 0)
avatar.EquipmentLoadouts.SaveLoadout(player, "test", 0)
val msg = infantryTerminal.Request(player, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 4, "", 0, PlanetSideGUID(0)))
msg.isInstanceOf[Terminal.InfantryLoadout] mustEqual true
@ -137,7 +137,7 @@ class OrderTerminalTest extends Specification {
"player can retrieve a vehicle loadout" in {
val fury = Vehicle(GlobalDefinitions.fury)
fury.Slot(30).Equipment = AmmoBox(GlobalDefinitions.hellfire_ammo)
avatar.SaveLoadout(fury, "test", 10)
avatar.EquipmentLoadouts.SaveLoadout(fury, "test", 10)
val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Loadout, 4, "test", 0, PlanetSideGUID(0))
terminal.Request(player, msg) match {

View file

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

View file

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

View file

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