diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 1307ee58d..01ce2d46b 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -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
- * 0-9 are Infantry loadouts + /** Equipment Loadouts
+ * 0-9 are Infantry loadouts
* 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 diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index e1e015f2c..7577b545c 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala new file mode 100644 index 000000000..c59787231 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala @@ -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 +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index a141529c2..bbd9f1f54 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -81,7 +81,7 @@ object AvatarConverter { ), obj.ExoSuit, 0, - 0L, + obj.CharId, 0, 0, 0, diff --git a/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala new file mode 100644 index 000000000..53925f90c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala @@ -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`.
+ * 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]`. + *
+ * 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) diff --git a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala index 0f9986e29..18c2f57fe 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala @@ -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 diff --git a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala index 12e0cf410..58490f60e 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -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`.
- * 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]`. - *
- * 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. */ diff --git a/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala new file mode 100644 index 000000000..9039c5a07 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala @@ -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) diff --git a/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala index 5f7e0b63a..c4199e0ce 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala @@ -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) diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala index 4784d8de3..9c4e34d7c 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala @@ -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) }) diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala index 4d3b24e88..1ddfc46da 100644 --- a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala +++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala @@ -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 = { diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala index 43009a3b4..7b9b46472 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala @@ -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]] } diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala index 76fae0227..98d6adef9 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala @@ -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() +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala index c2b064d37..c3f179217 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala @@ -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, diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index f7b96aa0f..245c3a29a 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -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 diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala index 5ad56bd07..cf56fb86a 100644 --- a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala +++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala @@ -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 } } } diff --git a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala index eece51214..22a1e7db4 100644 --- a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala +++ b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala @@ -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 diff --git a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala index e64dd8fcf..34175c272 100644 --- a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala +++ b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala @@ -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 diff --git a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index 5b73667f0..1c831f74b 100644 --- a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -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 diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala index c2c482c06..33b53ce79 100644 --- a/common/src/test/scala/objects/AvatarTest.scala +++ b/common/src/test/scala/objects/AvatarTest.scala @@ -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 { diff --git a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala index 34b21670b..c60e1c17f 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala @@ -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 { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index b20a2364e..e59fe4232 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -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