commit before changes, and to switch gears momentarily; making charId somewhat native to Avatar and its converter, though not yet essential; protype for squad definition loadouts, even though no example of functionality exists; a lot of unimplemented work in preparation to convert SquadDetailDefinitionUpdateMessage from a static format to a variable-field format

This commit is contained in:
FateJH 2019-06-18 14:43:26 -04:00
parent 14bdcb7a7e
commit fd9a3a0216
22 changed files with 938 additions and 131 deletions

View file

@ -1,10 +1,9 @@
// 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._
@ -12,6 +11,8 @@ 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) {
/** Character ID; a unique identifier corresponding to a database table row index */
private var charId : Long = 0
/** Battle Experience Points */
private var bep : Long = 0
/** Command Experience Points */
@ -29,11 +30,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 = {
@ -43,6 +48,15 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
private val deployables : DeployableToolbox = new DeployableToolbox
def CharId : Long = charId
def CharId_=(id : Long) : Long = {
if(charId == 0) { //does not need to be set but can only set once
charId = id
}
CharId
}
def BEP : Long = bep
def BEP_=(battleExperiencePoints : Long) : Long = {
@ -164,25 +178,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

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
@ -294,7 +296,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,27 @@
// 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) =>
entries(line) = Some(loadout)
case _ => ;
}
}
def LoadLoadout(line : Int) : Option[Loadout] = entries.lift(line).flatten
def DeleteLoadout(line : Int) : Unit = {
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,

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, label))
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,32 @@ 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,
squad.Task,
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,11 @@
// 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(label : String,
task : String,
zone_id : Option[Int],
members : List[SquadPositionLoadout]) extends Loadout(label)

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

@ -10,6 +10,7 @@ class Member {
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
@ -43,6 +44,13 @@ class Member {
Name
}
def CharId : Long = charId
def CharId_=(id : Long) : Long = {
charId = id
CharId
}
def Health : Int = health
def Health_=(red : Int) : Int = {

View file

@ -366,7 +366,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
case 2 | 6 | 9 | 11 |
12 | 13 | 14 | 16 | 17 |
18 | 29 | 30 | 32 | 33 |
36 | 37 | 38 | 42 | 43 => unknownCodec(code)
36 | 37 | 38 | 39 | 42 | 43 => unknownCodec(code)
case _ => failureCodec(code)
}).asInstanceOf[Codec[SquadAction]]
}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2019 PSForever
package net.psforever.packet.game
import net.psforever.packet.game.SquadDetailDefinitionUpdateMessage.defaultRequirements
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.CertificationType
import scodec.bits.BitVector
@ -18,7 +19,9 @@ final case class SquadPositionDetail(is_closed : Boolean,
name : String)
final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
unk : BitVector,
unk1 : Int,
leader_char_id : Long,
unk2 : BitVector,
leader_name : String,
task : String,
zone_id : PlanetSideZoneID,
@ -32,27 +35,17 @@ final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
object SquadPositionDetail {
final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = true, "", "", Set.empty, 0L, "")
private def reliableNameHash(name : String) : Long = {
val hash = name.hashCode.toLong
if(hash < 0) {
-1L * hash
}
else {
hash
}
}
def apply() : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, 0L, "")
def apply(name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, reliableNameHash(name), name)
def apply(char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, char_id, name)
def apply(role : String, detailed_orders : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, 0L, "")
def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, 0L, "")
def apply(role : String, detailed_orders : String, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, reliableNameHash(name), name)
def apply(role : String, detailed_orders : String, char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, char_id, name)
def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, reliableNameHash(name), name)
def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, char_id, name)
}
object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
@ -64,6 +57,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
final val Init = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(0),
0L,
"",
"",
PlanetSideZoneID(0),
@ -81,9 +75,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
)
)
def apply(guid : PlanetSideGUID, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
import scodec.bits._
SquadDetailDefinitionUpdateMessage(guid, hex"080000000000000000000".toBitVector, leader_name, task, zone_id, member_info)
SquadDetailDefinitionUpdateMessage(guid, 1, char_id, hex"000000".toBitVector, leader_name, task, zone_id, member_info)
}
private def memberCodec(pad : Int) : Codec[SquadPositionDetail] = {
@ -191,9 +185,14 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
import shapeless.::
(
("guid" | PlanetSideGUID.codec) ::
uint8 ::
uint4 ::
bits(83) :: //variable fields, but can be 0'd
uint4 :: //constant = 8
uint4 :: //variable = 0-4
bool :: //true, when 4
uint4 :: //variable = 0-12?
("unk1" | uint4) ::
uint24 :: //unknown, but can be 0'd
("leader_char_id" | uint32L) ::
("unk2" | bits(22)) :: //variable fields, but can be 0'd
uint(10) :: //constant = 0
("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
("task" | PacketHelpers.encodedWideString) ::
@ -202,15 +201,721 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
optional(bool, "member_info" | initial_member_codec)
).exmap[SquadDetailDefinitionUpdateMessage] (
{
case guid :: _ :: _ :: _ :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, leader, task, zone, unlinkMemberList(member_list)))
case guid :: _ :: _ :: _ :: _ :: u1 :: _ :: char_id:: u2 :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, u1, char_id, u2, leader, task, zone, unlinkMemberList(member_list)))
case data =>
Attempt.failure(Err(s"can not get squad detail definition from data $data"))
},
{
case SquadDetailDefinitionUpdateMessage(guid, unk, leader, task, zone, member_list) =>
Attempt.Successful(guid :: 132 :: 8 :: unk.take(83) :: 0 :: leader :: task :: zone :: 4983296 :: Some(linkMemberList(member_list.reverse)) :: HNil)
case SquadDetailDefinitionUpdateMessage(guid, unk1, char_id, unk2, leader, task, zone, member_list) =>
Attempt.Successful(guid :: 8 :: 4 :: true :: 0 :: math.max(unk1, 1) :: 0 :: char_id :: unk2.take(22) :: 0 :: leader :: task :: zone :: 4983296 :: Some(linkMemberList(member_list.reverse)) :: HNil)
}
)
}
}
//NEW FORM SquadDetailDefinitionUpdateMessage
private class StreamLengthToken(init : Int = 0) {
private var bitLength : Int = init
def Length : Int = bitLength
def Length_=(toLength : Int) : StreamLengthToken = {
bitLength = toLength
this
}
def Add(more : Int) : StreamLengthToken = {
bitLength += more
this
}
}
final case class SquadPositionDetail2(is_closed : Option[Boolean],
role : Option[String],
detailed_orders : Option[String],
requirements : Option[Set[CertificationType.Value]],
char_id : Option[Long],
name : Option[String]) {
def And(info : SquadPositionDetail2) : SquadPositionDetail2 = {
SquadPositionDetail2(
is_closed match {
case Some(true) | None =>
info.is_closed.orElse(is_closed)
case _ =>
Some(false)
},
role.orElse(info.role),
detailed_orders.orElse(info.detailed_orders),
requirements.orElse(info.requirements),
char_id.orElse(info.char_id),
name.orElse(info.name)
)
}
}
final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail2])
final case class SquadDetail(unk1 : Option[Int],
leader_char_id : Option[Long],
unk2 : Option[BitVector],
leader_name : Option[String],
task : Option[String],
zone_id : Option[PlanetSideZoneID],
member_info : Option[List[SquadPositionEntry]]) {
def And(info : SquadDetail) : SquadDetail = {
SquadDetail(
unk1.orElse(info.unk1),
leader_char_id.orElse(info.leader_char_id),
unk2.orElse(info.unk2),
leader_name.orElse(info.leader_name),
task.orElse(info.task),
zone_id.orElse(info.zone_id),
(member_info, info.member_info) match {
case (Some(info1), Some(info2)) => Some(info1 ++ info2)
case (Some(info1), _) => Some(info1)
case (None, Some(info2)) => Some(info2)
case _ => None
}
)
}
}
final case class SquadDetailDefinitionUpdateMessage2(guid : PlanetSideGUID,
detail : SquadDetail)
object SquadPositionDetail2 {
final val Blank : SquadPositionDetail2 = SquadPositionDetail2()
final val Closed : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(true), None, None, None, None, None)
def apply() : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, None)
def apply(role : String, detailed_orders : Option[String]) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), detailed_orders, None, None, None)
def apply(role : Option[String], detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), role, Some(detailed_orders), None, None, None)
def apply(char_id : Long) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, Some(char_id), None)
def apply(name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, Some(name))
def apply(requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, Some(requirements), None, None)
def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
}
object SquadPositionEntry {
import SquadDetailDefinitionUpdateMessage2.paddedStringMetaCodec
private def roleCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
role => Attempt.successful(SquadPositionDetail2(role)),
{
case SquadPositionDetail2(_, Some(role), _, _, _, _) =>
Attempt.successful(role)
case _ =>
Attempt.failure(Err("failed to encode squad position data for role"))
}
)
private def ordersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
orders => Attempt.successful(SquadPositionDetail2(None, orders)),
{
case SquadPositionDetail2(_, _, Some(orders), _, _, _) =>
Attempt.successful(orders)
case _ =>
Attempt.failure(Err("failed to encode squad position data for detailed orders"))
}
)
private val requirementsCodec : Codec[SquadPositionDetail2] = ulongL(46).exmap[SquadPositionDetail2] (
requirements => Attempt.successful(SquadPositionDetail2(CertificationType.fromEncodedLong(requirements))),
{
case SquadPositionDetail2(_, _, _, Some(requirements), _, _) =>
Attempt.successful(CertificationType.toEncodedLong(requirements))
case _ =>
Attempt.failure(Err("failed to encode squad position data for certification requirements"))
}
)
private val charIdCodec : Codec[SquadPositionDetail2] = uint32L.exmap[SquadPositionDetail2] (
char_id => Attempt.successful(SquadPositionDetail2(char_id)),
{
case SquadPositionDetail2(_, _, _, _, Some(char_id), _) =>
Attempt.successful(char_id)
case _ =>
Attempt.failure(Err("failed to encode squad data for member id"))
}
)
private def nameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
name => Attempt.successful(SquadPositionDetail2(name)),
{
case SquadPositionDetail2(_, _, _, _, _, Some(orders)) =>
Attempt.successful(orders)
case _ =>
Attempt.failure(Err("failed to encode squad position data for member name"))
}
)
/**
* `Codec` for failing to determine a valid `Codec` based on the entry data.
* This `Codec` is an invalid codec that does not read any bit data.
* The `conditional` will always return `None` because
* its determining conditional statement is explicitly `false`
* and all cases involving explicit failure.
*/
private val failureCodec : Codec[SquadPositionDetail2] = conditional(included = false, bool).exmap[SquadPositionDetail2] (
_ => Attempt.failure(Err("decoding with unhandled codec")),
_ => Attempt.failure(Err("encoding with unhandled codec"))
)
private final case class LinkedSquadPositionInfo(code : Int, info : SquadPositionDetail2, next : Option[LinkedSquadPositionInfo])
private def unlinkSquadPositionInfo(info : LinkedSquadPositionInfo) : SquadPositionDetail2 = unlinkSquadPositionInfo(Some(info))
/**
* Concatenate a `SquadInfo` object chain into a single `SquadInfo` object.
* Recursively visits every link in a `SquadInfo` object chain.
* @param info the current link in the chain
* @param squadInfo the persistent `SquadInfo` concatenation object;
* defaults to `SquadInfo.Blank`
* @return the concatenated `SquadInfo` object
*/
@tailrec
private def unlinkSquadPositionInfo(info : Option[LinkedSquadPositionInfo], squadInfo : SquadPositionDetail2 = SquadPositionDetail2.Blank) : SquadPositionDetail2 = {
info match {
case None =>
squadInfo
case Some(sqInfo) =>
unlinkSquadPositionInfo(sqInfo.next, squadInfo And sqInfo.info)
}
}
/**
* Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
* The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
* or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
* @param info a `SquadInfo` object that has all relevant fields populated
* @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
*/
private def linkSquadPositionInfo(info : SquadPositionDetail2) : LinkedSquadPositionInfo = {
//import scala.collection.immutable.::
Seq(
(5, SquadPositionDetail2(None, None, None, info.requirements, None, None)),
(4, SquadPositionDetail2(None, None, None, None, None, info.name)),
(3, SquadPositionDetail2(None, None, None, None, info.char_id, None)),
(2, SquadPositionDetail2(None, None, info.detailed_orders, None, None, None)),
(1, SquadPositionDetail2(None, info.role, None, None, None, None)),
(0, SquadPositionDetail2(info.is_closed, None, None, None, None, None))
) //in reverse order so that the linked list is in the correct order
.filterNot { case (_, sqInfo) => sqInfo == SquadPositionDetail2.Blank}
match {
case Nil =>
throw new Exception("no linked list squad position fields encountered where at least one was expected") //bad end
case x :: Nil =>
val (code, squadInfo) = x
LinkedSquadPositionInfo(code, squadInfo, None)
case x :: xs =>
val (code, squadInfo) = x
linkSquadPositionInfo(xs, LinkedSquadPositionInfo(code, squadInfo, None))
}
}
/**
* Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
* The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
* or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
* @param infoList a series of paired field codes and `SquadInfo` objects with data in the indicated fields
* @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
*/
@tailrec
private def linkSquadPositionInfo(infoList : Seq[(Int, SquadPositionDetail2)], linkedInfo : LinkedSquadPositionInfo) : LinkedSquadPositionInfo = {
if(infoList.isEmpty) {
linkedInfo
}
else {
val (code, data) = infoList.head
linkSquadPositionInfo(infoList.tail, LinkedSquadPositionInfo(code, data, Some(linkedInfo)))
}
}
private def listing_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedSquadPositionInfo] = {
import shapeless.::
(
uint4 >>:~ { code =>
selectCodecAction(code, bitsOverByte.Add(4)) ::
conditional(size - 1 > 0, listing_codec(size - 1, modifyCodecPadValue(code, bitsOverByte)))
}
).xmap[LinkedSquadPositionInfo] (
{
case code :: entry :: next :: HNil =>
LinkedSquadPositionInfo(code, entry, next)
},
{
case LinkedSquadPositionInfo(code, entry, next) =>
code :: entry :: next :: HNil
}
)
}
private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = {
code match {
case 1 => roleCodec(bitsOverByte)
case 2 => ordersCodec(bitsOverByte)
case 3 => charIdCodec
case 4 => nameCodec(bitsOverByte)
case 5 => requirementsCodec
case _ => failureCodec
}
}
private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
code match {
case 1 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 2 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 3 => bitsOverByte //32u = no added padding
case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u
case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
}
}
private def squad_member_details_codec(bitsOverByte : StreamLengthToken) : Codec[LinkedSquadPositionInfo] = {
import shapeless.::
(
uint8 >>:~ { size =>
listing_codec(size, bitsOverByte).hlist
}
).xmap[LinkedSquadPositionInfo] (
{
case _ :: info :: HNil =>
info
},
info => {
var i = 1
var dinfo = info
while(info.next.nonEmpty) {
i += 1
dinfo = info.next.get
}
i :: info :: HNil
}
)
}
def codec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionEntry] = {
import shapeless.::
(
("index" | uint8) >>:~ { index =>
conditional(index < 255, bool) >>:~ { is_open =>
conditional(is_open.contains(true) && index < 255, "info" | squad_member_details_codec(bitsOverByte.Add(1))).hlist
}
}
).xmap[SquadPositionEntry] (
{
case 255 :: _ :: _ :: HNil =>
SquadPositionEntry(255, None)
case ndx :: Some(false) :: None :: HNil =>
SquadPositionEntry(ndx, None)
case ndx :: Some(true) :: Some(info) :: HNil =>
SquadPositionEntry(ndx, Some(unlinkSquadPositionInfo(info)))
},
{
case SquadPositionEntry(255, _) =>
255 :: None :: None :: HNil
case SquadPositionEntry(ndx, None) =>
ndx :: Some(false) :: None :: HNil
case SquadPositionEntry(ndx, Some(info)) =>
ndx :: Some(true) :: Some(linkSquadPositionInfo(info)) :: HNil
}
)
}
}
object SquadDetail {
final val Blank = SquadDetail(None, None, None, None, None, None, None)
def apply(leader_char_id : Long) : SquadDetail = SquadDetail(None, Some(leader_char_id), None, None, None, None, None)
def apply(leader_name : String, task : Option[String]) : SquadDetail = SquadDetail(None, None, None, Some(leader_name), task, None, None)
def apply(leader_name : Option[String], task : String) : SquadDetail = SquadDetail(None, None, None, leader_name, Some(task), None, None)
def apply(zone_id : PlanetSideZoneID) : SquadDetail = SquadDetail(None, None, None, None, None, Some(zone_id), None)
def apply(member_list : List[SquadPositionEntry]) : SquadDetail = SquadDetail(None, None, None, None, None, None, Some(member_list))
def apply(unk1 : Int, leader_char_id : Long, unk2 : BitVector, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionEntry]) : SquadDetail = {
SquadDetail(Some(unk1), Some(leader_char_id), Some(unk2), Some(leader_name), Some(task), Some(zone_id), Some(member_info))
}
}
object SquadDetailDefinitionUpdateMessage2 {
/**
* Produces a `Codec` function for byte-aligned, padded Pascal strings encoded through common manipulations.
* @see `PacketHelpers.encodedWideStringAligned`
* @param bitsOverByte the number of bits past the previous byte-aligned index;
* should be a 0-7 number that gets converted to a 1-7 string padding number
* @return the encoded string `Codec`
*/
def paddedStringMetaCodec(bitsOverByte : Int) : Codec[String] = PacketHelpers.encodedWideStringAligned({
val mod8 = bitsOverByte % 8
if(mod8 == 0) {
0
}
else {
8 - mod8
}
})
private def memberCodec(pad : Int) : Codec[SquadPositionDetail2] = {
import shapeless.::
(
uint8 :: //required value = 6
("is_closed" | bool) :: //if all positions are closed, the squad detail menu display no positions at all
("role" | PacketHelpers.encodedWideStringAligned(pad)) ::
("detailed_orders" | PacketHelpers.encodedWideString) ::
("char_id" | uint32L) ::
("name" | PacketHelpers.encodedWideString) ::
("requirements" | ulongL(46))
).exmap[SquadPositionDetail2] (
{
case 6 :: closed :: role :: orders :: char_id :: name :: requirements :: HNil =>
Attempt.Successful(
SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(defaultRequirements ++ CertificationType.fromEncodedLong(requirements)), Some(char_id), Some(name))
)
case data =>
Attempt.Failure(Err(s"can not decode a SquadDetailDefinitionUpdate member's data - $data"))
},
{
case SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(requirements), Some(char_id), Some(name)) =>
Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(defaultRequirements ++ requirements) :: HNil)
}
)
}
private val first_member_codec : Codec[SquadPositionDetail2] = memberCodec(pad = 7)
private val member_codec : Codec[SquadPositionDetail2] = memberCodec(pad = 0)
private case class LinkedMemberList(member : SquadPositionDetail2, next : Option[LinkedMemberList])
private def subsequent_member_codec : Codec[LinkedMemberList] = {
import shapeless.::
(
//disruptive coupling action (e.g., flatPrepend) is necessary to allow for recursive Codec
("member" | member_codec) >>:~ { _ =>
optional(bool, "next" | subsequent_member_codec).hlist
}
).xmap[LinkedMemberList] (
{
case a :: b :: HNil =>
LinkedMemberList(a, b)
},
{
case LinkedMemberList(a, b) =>
a :: b :: HNil
}
)
}
private def initial_member_codec : Codec[LinkedMemberList] = {
import shapeless.::
(
("member" | first_member_codec) ::
optional(bool, "next" | subsequent_member_codec)
).xmap[LinkedMemberList] (
{
case a :: b :: HNil =>
LinkedMemberList(a, b)
},
{
case LinkedMemberList(a, b) =>
a :: b :: HNil
}
)
}
@tailrec
private def unlinkMemberList(list : LinkedMemberList, out : List[SquadPositionDetail2] = Nil) : List[SquadPositionDetail2] = {
list.next match {
case None =>
out :+ list.member
case Some(next) =>
unlinkMemberList(next, out :+ list.member)
}
}
private def linkMemberList(list : List[SquadPositionDetail2]) : LinkedMemberList = {
list match {
case Nil =>
throw new Exception("")
case x :: Nil =>
LinkedMemberList(x, None)
case x :: xs =>
linkMemberList(xs, LinkedMemberList(x, None))
}
}
@tailrec
private def linkMemberList(list : List[SquadPositionDetail2], out : LinkedMemberList) : LinkedMemberList = {
list match {
case Nil =>
out
case x :: Nil =>
LinkedMemberList(x, Some(out))
case x :: xs =>
linkMemberList(xs, LinkedMemberList(x, Some(out)))
}
}
val full_squad_detail_codec : Codec[SquadDetail] = {
import shapeless.::
(
("unk1" | uint8) ::
uint24 :: //unknown, but can be 0'd
("leader_char_id" | uint32L) ::
("unk2" | bits(22)) :: //variable fields, but can be 0'd
uint(10) :: //constant = 0
("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
("task" | PacketHelpers.encodedWideString) ::
("zone_id" | PlanetSideZoneID.codec) ::
uint(23) :: //constant = 4983296
optional(bool, "member_info" | initial_member_codec)
).exmap[SquadDetail] (
{
case u1 :: _ :: char_id :: u2 :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
Attempt.Successful(
SquadDetail(Some(u1), Some(char_id), Some(u2), Some(leader), Some(task), Some(zone),
Some(unlinkMemberList(member_list).zipWithIndex.map { case (entry, index) => SquadPositionEntry(index, Some(entry)) })
)
)
case data =>
Attempt.failure(Err(s"can not get squad detail definition from data $data"))
},
{
case SquadDetail(Some(u1), Some(char_id), Some(u2), Some(leader), Some(task), Some(zone), Some(member_list)) =>
Attempt.Successful(
math.max(u1, 1) :: 0 :: char_id :: u2.take(22) :: 0 :: leader :: task :: zone :: 4983296 ::
Some(linkMemberList(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
HNil
)
}
)
}
private val leaderCharIdCodec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
char_id => Attempt.successful(SquadDetail(char_id)),
{
case SquadDetail(_, Some(char_id), _, _, _, _, _) =>
Attempt.successful(char_id)
case _ =>
Attempt.failure(Err("failed to encode squad data for leader id"))
}
)
private def leaderNameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
name => Attempt.successful(SquadDetail(name, None)),
{
case SquadDetail(_, _, _, Some(name), _, _, _) =>
Attempt.successful(name)
case _ =>
Attempt.failure(Err("failed to encode squad data for leader name"))
}
)
private def taskCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
task => Attempt.successful(SquadDetail(None, task)),
{
case SquadDetail(_, _, _, _, Some(task), _, _) =>
Attempt.successful(task)
case _ =>
Attempt.failure(Err("failed to encode squad data for task"))
}
)
private val zoneCodec : Codec[SquadDetail] = PlanetSideZoneID.codec.exmap[SquadDetail] (
zone_id => Attempt.successful(SquadDetail(zone_id)),
{
case SquadDetail(_, _, _, _, _, Some(zone_id), _) =>
Attempt.successful(zone_id)
case _ =>
Attempt.failure(Err("failed to encode squad data for zone id"))
}
)
private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
import shapeless.::
bitsOverByte.Add(19)
(
uint(19) :: //constant = 0x60040, or 393280 in 19u BE
vector(SquadPositionEntry.codec(bitsOverByte))
).exmap[SquadDetail] (
{
case _ :: member_list :: HNil =>
Attempt.successful(SquadDetail(member_list.toList))
},
{
case SquadDetail(_, _, _, _, _, _, Some(member_list)) =>
Attempt.successful(393280 :: member_list.toVector :: HNil)
case _ =>
Attempt.failure(Err("failed to encode squad data for members"))
}
)
}
private val failureCodec : Codec[SquadDetail] = conditional(included = false, bool).exmap[SquadDetail] (
_ => Attempt.failure(Err("decoding with unhandled codec")),
_ => Attempt.failure(Err("encoding with unhandled codec"))
)
private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
code match {
case 2 => leaderCharIdCodec
case 4 => leaderNameCodec(bitsOverByte)
case 5 => taskCodec(bitsOverByte)
case 6 => zoneCodec
case 8 => membersCodec(bitsOverByte)
case _ => failureCodec
}
}
private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
code match {
case 2 => bitsOverByte //32u = no added padding
case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 5 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd
case 6 => bitsOverByte //32u = no added padding
case 8 => bitsOverByte.Length = 0 //end of stream
case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
}
}
private final case class LinkedSquadInfo(code : Int, info : SquadDetail, next : Option[LinkedSquadInfo])
private def unlinkSquadInfo(info : LinkedSquadInfo) : SquadDetail = unlinkSquadInfo(Some(info))
@tailrec
private def unlinkSquadInfo(info : Option[LinkedSquadInfo], squadInfo : SquadDetail = SquadDetail.Blank) : SquadDetail = {
info match {
case None =>
squadInfo
case Some(sqInfo) =>
unlinkSquadInfo(sqInfo.next, squadInfo And sqInfo.info)
}
}
private def linkSquadInfo(info : SquadDetail) : LinkedSquadInfo = {
//import scala.collection.immutable.::
Seq(
(8, SquadDetail(None, None, None, None, None, None, info.member_info)),
(6, SquadDetail(None, None, None, None, None, info.zone_id, None)),
(5, SquadDetail(None, None, None, None, info.task, None, None)),
(4, SquadDetail(None, None, None, info.leader_name, None, None, None)),
(2, SquadDetail(None, info.leader_char_id, None, None, None, None, None))
) //in reverse order so that the linked list is in the correct order
.filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank}
match {
case Nil =>
throw new Exception("no linked list squad fields encountered where at least one was expected") //bad end
case x :: Nil =>
val (code, squadInfo) = x
LinkedSquadInfo(code, squadInfo, None)
case x :: xs =>
val (code, squadInfo) = x
linkSquadInfo(xs, LinkedSquadInfo(code, squadInfo, None))
}
}
@tailrec
private def linkSquadInfo(infoList : Seq[(Int, SquadDetail)], linkedInfo : LinkedSquadInfo) : LinkedSquadInfo = {
if(infoList.isEmpty) {
linkedInfo
}
else {
val (code, data) = infoList.head
linkSquadInfo(infoList.tail, LinkedSquadInfo(code, data, Some(linkedInfo)))
}
}
private def linked_squad_detail_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedSquadInfo] = {
import shapeless.::
(
uint4 >>:~ { code =>
selectCodecAction(code, bitsOverByte.Add(4)) ::
conditional(size - 1 > 0, linked_squad_detail_codec(size - 1, modifyCodecPadValue(code, bitsOverByte)))
}
).exmap[LinkedSquadInfo] (
{
case action :: detail :: next :: HNil =>
Attempt.Successful(LinkedSquadInfo(action, detail, next))
},
{
case LinkedSquadInfo(action, detail, next) =>
Attempt.Successful(action :: detail :: next :: HNil)
}
)
}
def squadDetailSelectCodec(size : Int) : Codec[SquadDetail] = {
if(size == 9) {
full_squad_detail_codec
}
else {
linked_squad_detail_codec(size, new StreamLengthToken(1)).xmap[SquadDetail] (
linkedDetail => unlinkSquadInfo(linkedDetail),
unlinkedDetail => linkSquadInfo(unlinkedDetail)
)
}
}
private def codec() : Codec[SquadDetailDefinitionUpdateMessage2] = {
import shapeless.::
(
("guid" | PlanetSideGUID.codec) ::
bool ::
(uint8 >>:~ { size =>
squadDetailSelectCodec(size).hlist
})
).exmap[SquadDetailDefinitionUpdateMessage2] (
{
case guid :: _ :: _ :: info :: HNil =>
Attempt.Successful(SquadDetailDefinitionUpdateMessage2(guid, info))
},
{
case SquadDetailDefinitionUpdateMessage2(guid, info) =>
val occupiedSquadFieldCount = List(info.unk1, info.leader_char_id, info.unk2, info.leader_name, info.task, info.zone_id, info.member_info)
.count(_.nonEmpty)
if(occupiedSquadFieldCount < 9) {
//itemized detail definition protocol
Attempt.Successful(guid :: true :: occupiedSquadFieldCount :: info :: HNil)
}
else {
info.member_info match {
case Some(list) =>
if(list.size == 10 &&
list
.collect { case position if position.info.nonEmpty =>
val info = position.info.get
List(info.is_closed, info.role, info.detailed_orders, info.requirements, info.char_id, info.name)
}
.flatten
.count(_.isEmpty) == 0) {
//full squad detail definition protocol
Attempt.Successful(guid :: true :: 9 :: info :: HNil)
}
else {
//unhandled state
Attempt.Failure(Err("can not split encoding patterns - all squad fields are defined but not all squad member fields are defined"))
}
case None =>
//impossible?
Attempt.Failure(Err("the members field can not be empty; the existence of this field was already proven"))
}
}
}
)
}
implicit val code : Codec[SquadDetailDefinitionUpdateMessage2] = codec()
}

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

@ -121,6 +121,7 @@ class SquadService extends Actor {
//other players were in the squad; publicly disband it
squad.Membership.foreach(position => {
position.Name = ""
position.CharId = 0L
position.ZoneId = 0
position.Position = Vector3.Zero
position.Health = 0
@ -151,6 +152,8 @@ class SquadService extends Actor {
import net.psforever.packet.game.SquadAction._
val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
action match {
case SaveSquadDefinition() =>
case ChangeSquadPurpose(purpose) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
@ -265,6 +268,7 @@ class SquadService extends Actor {
val hadPreviousPosition = squad.Membership.find(_.Name == name) match {
case Some(currentPosition)=>
currentPosition.Name = ""
currentPosition.CharId = 0L
currentPosition.ZoneId = 0
currentPosition.Health = 0
currentPosition.Armor = 0
@ -274,6 +278,7 @@ class SquadService extends Actor {
false
}
desiredPosition.Name = name
desiredPosition.CharId = tplayer.CharId
desiredPosition.ZoneId = zone_ordinal_number
desiredPosition.Health = tplayer.Health
desiredPosition.Armor = tplayer.Armor
@ -380,7 +385,7 @@ class SquadService extends Actor {
PlanetSideZoneID(squad.ZoneId),
squad.Membership.zipWithIndex.map({ case (p, index) =>
if(squad.Availability(index)) {
SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.Name)
SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name)
}
else {
SquadPositionDetail.Closed

View file

@ -12,7 +12,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
"SquadDetailDefinitionUpdateMessage" should {
"decode" in {
PacketCoding.DecodePacket(string).require match {
case SquadDetailDefinitionUpdateMessage(guid, unk, leader, task, zone, member_info) =>
case SquadDetailDefinitionUpdateMessage(guid, unk1, char_id, unk2, leader, task, zone, member_info) =>
ok
case _ =>
ko
@ -22,24 +22,25 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
"encode" in {
val msg = SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
42771010L,
"HofD",
"\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
PlanetSideZoneID(7),
List(
SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", ""),
SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
SquadPositionDetail("\\#ffdc00 C", ""),
SquadPositionDetail("\\#ffdc00 H", "", "OpoIE"),
SquadPositionDetail("\\#ffdc00 I", "", "BobaF3tt907"),
SquadPositionDetail("\\#ffdc00 H", "", Set(), 42644970L, "OpoIE"),
SquadPositionDetail("\\#ffdc00 I", "", Set(), 41604210L, "BobaF3tt907"),
SquadPositionDetail("\\#ffdc00 N", ""),
SquadPositionDetail("\\#ffdc00 A", ""),
SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", ""),
SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
SquadPositionDetail("\\#9640ff K", ""),
SquadPositionDetail("\\#9640ff O", "", "HofD"),
SquadPositionDetail("\\#9640ff O", "", Set(), 42771010L, "HofD"),
SquadPositionDetail("\\#9640ff K", "")
)
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
ok
pkt mustEqual string
}
}
}

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

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

@ -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(_) mustEqual None }
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", 10)
avatar.LoadLoadout(10) mustEqual None
avatar.EquipmentLoadouts.LoadLoadout(10) mustEqual None
}
"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) mustEqual None
}
"the fifth slot is the locker wrapped in an EquipmentSlot" 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

@ -375,6 +375,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(
SquadDetailDefinitionUpdateMessage(
guid,
member_info.find(_.name == leader).get.char_id,
leader,
task,
zone,
@ -532,7 +533,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.Stamina = stamina
player.Armor = armor
}
sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))
avatar.CharId = 41605313L
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))
@ -2838,7 +2840,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)))
@ -2914,10 +2916,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
)
))
//sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
sendResponse(
SquadDetailDefinitionUpdateMessage(
PlanetSideGUID(3),
42771010L,
"HofD",
"\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
PlanetSideZoneID(7),
@ -3347,17 +3349,19 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
if(deadState == DeadState.Alive) {
if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(41605313),"HofD",false,None))
sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,41605313,Some(42771010),"VirusGiver",true,Some(None)))
sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))
sendResponse(SquadMemberEvent(0,7,42644970,1,Some("OpolE"),Some(7),Some(6418)))
sendResponse(SquadMemberEvent(0,7,41604210,8,Some("BobaF3tt907"),Some(12),Some(8097)))
sendResponse(PlanetsideAttributeMessage(player.GUID, 49, 7))
sendResponse(PlanetsideAttributeMessage(player.GUID, 50, 2))
sendResponse(PlanetsideAttributeMessage(player.GUID, 51, 8))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"".toBitVector)))
sendResponse(PlanetsideAttributeMessage(player.GUID, 83, 0))
sendResponse(SquadState(PlanetSideGUID(7),List(SquadStateInfo(41605313L,64,64,Vector3(3464.0469f,4065.5703f,20.015625f),2,2,false,0,None,None))))
//sendRawResponse(hex"e8030080c603c043fe")
sendRawResponse(hex"e8 0300 80 c 600408c00000001200008811f6200400144004f007a007a0069004b0069006e006700ff")
// sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(avatar.CharId),"HofD",false,None))
// sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,avatar.CharId,Some(42771010),"VirusGiver",true,Some(None)))
// sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))
// sendResponse(SquadMemberEvent(0,7,42644970,1,Some("OpolE"),Some(7),Some(6418)))
// sendResponse(SquadMemberEvent(0,7,41604210,8,Some("BobaF3tt907"),Some(12),Some(8097)))
// sendResponse(PlanetsideAttributeMessage(player.GUID, 49, 7))
// sendResponse(PlanetsideAttributeMessage(player.GUID, 50, 2))
// sendResponse(PlanetsideAttributeMessage(player.GUID, 51, 8))
// sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"".toBitVector)))
// sendResponse(PlanetsideAttributeMessage(player.GUID, 83, 0))
// sendResponse(SquadState(PlanetSideGUID(7),List(SquadStateInfo(avatar.CharId,64,64,Vector3(3464.0469f,4065.5703f,20.015625f),2,2,false,0,None,None))))
}
player.Position = pos
player.Velocity = vel
@ -4579,18 +4583,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 =>
@ -7574,9 +7578,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
@ -7585,12 +7587,16 @@ 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 killerCharId : Long = killer.CharId
val victimCharId : Long = {
//TODO this block of code is only necessary for this particular dev build; use "victim.CharId" normallyLoado
val id = victim.CharId
if(killerCharId == id) {
Int.MaxValue - id + 1
}
else {
id
}
}
val killer_seated = killer match {
case obj : PlayerSource => obj.Seated