diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 05caf2bee..3166d5b64 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -4,12 +4,12 @@ package net.psforever.objects import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition} import net.psforever.objects.equipment.EquipmentSize import net.psforever.objects.loadouts.Loadout -import net.psforever.types.{CertificationType, CharacterGender, ImplantType, PlanetSideEmpire} +import net.psforever.types._ import scala.annotation.tailrec import scala.collection.mutable -class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : Int) { +class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : CharacterVoice.Value) { /** Battle Experience Points */ private var bep : Long = 0 /** Command Experience Points */ @@ -212,7 +212,7 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : object Avatar { final private val definition : AvatarDefinition = new AvatarDefinition(121) - def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Avatar = { + def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : CharacterVoice.Value) : Avatar = { new Avatar(name, faction, sex, head, voice) } diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 50a8c8c4e..ecb9a4c6d 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -13,9 +13,12 @@ import net.psforever.objects.serverobject.mblocker.LockerDefinition import net.psforever.objects.serverobject.pad.VehicleSpawnPadDefinition import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.SpawnTubeDefinition +import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition import net.psforever.objects.vehicles.{SeatArmorRestriction, UtilityType} import net.psforever.types.PlanetSideEmpire +import scala.concurrent.duration._ + object GlobalDefinitions { /* Implants @@ -563,6 +566,8 @@ object GlobalDefinitions { val door = new DoorDefinition + val resource_silo = new ResourceSiloDefinition + /** * Given a faction, provide the standard assault melee weapon. * @param faction the faction @@ -2701,7 +2706,11 @@ object GlobalDefinitions { ant.Seats(0).ArmorRestriction = SeatArmorRestriction.NoReinforcedOrMax ant.MountPoints += 1 -> 0 ant.MountPoints += 2 -> 0 + ant.Deployment = true + ant.DeployTime = 1500 + ant.UndeployTime = 1500 ant.AutoPilotSpeeds = (18, 6) + ant.MaximumCapacitor = 1500 ant.Packet = utilityConverter ams.Name = "ams" @@ -2716,6 +2725,7 @@ object GlobalDefinitions { ams.Deployment = true ams.DeployTime = 2000 ams.UndeployTime = 2000 + ams.DeconstructionTime = Some(20 minutes) ams.AutoPilotSpeeds = (18, 6) ams.Packet = utilityConverter @@ -2728,6 +2738,7 @@ object GlobalDefinitions { router.Deployment = true router.DeployTime = 2000 router.UndeployTime = 2000 + router.DeconstructionTime = Duration(20, "minutes") router.AutoPilotSpeeds = (16, 6) router.Packet = variantConverter diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 19cf484be..71398c5ed 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -58,7 +58,7 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio def Head : Int = core.head - def Voice : Int = core.voice + def Voice : CharacterVoice.Value = core.voice def isAlive : Boolean = alive @@ -129,7 +129,12 @@ class Player(private val core : Avatar) extends PlanetSideGameObject with Factio def MaxArmor : Int = exosuit.MaxArmor - def VisibleSlots : Set[Int] = if(exosuit.SuitType == ExoSuitType.MAX) { Set(0) } else { Set(0,1,2,3,4) } + def VisibleSlots : Set[Int] = if(exosuit.SuitType == ExoSuitType.MAX) { + Set(0) + } + else { + (0 to 4).filterNot(index => holsters(index).Size == EquipmentSize.Blocked).toSet + } override def Slot(slot : Int) : EquipmentSlot = { if(inventory.Offset <= slot && slot <= inventory.LastIndex) { diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index e01ea16ba..e3aa7849f 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -21,16 +21,44 @@ import scala.annotation.tailrec * Generally, all seating is declared first - the driver and passengers and and gunners. * Following that are the mounted weapons and other utilities. * Trunk space starts being indexed afterwards. - * To keep it simple, infantry seating, mounted weapons, and utilities are stored separately.
- *
- * Vehicles maintain a `Map` of `Utility` objects in given index positions. + * To keep it simple, infantry seating, mounted weapons, and utilities are stored separately herein. + * The `Map` of `Utility` objects is given using the same inventory index positions. * Positive indices and zero are considered "represented" and must be assigned a globally unique identifier * and must be present in the containing vehicle's `ObjectCreateMessage` packet. * The index is the seat position, reflecting the position in the zero-index inventory. * Negative indices are expected to be excluded from this conversion. - * The value of the negative index does not have a specific meaning. + * The value of the negative index does not have a specific meaning.
+ *
+ * The importance of a vehicle's owner can not be overlooked. + * The owner is someone who can control who can sit in the vehicle's seats + * either through broad categorization or discriminating selection ("kicking") + * and who has access to and can allow access to the vehicle's trunk capacity. + * The driver is the only player that can access a vehicle's saved loadouts through a repair/rearm silo + * and can procure equipment from the said silo. + * The owner of a vehicle and the driver of a vehicle as mostly interchangeable terms for this reason + * and it can be summarized that the player who has access to the driver seat meets the qualifications for the "owner" + * so long as that player is the last person to have sat in that seat. + * All previous ownership information is replaced just as soon as someone else sits in the driver's seat. + * Ownership is also transferred as players die and respawn (from and to the same client) + * and when they leave a continent without taking the vehicle they currently own with them. + * (They also lose ownership when they leave the game, of course.)
+ *
+ * All seats have vehicle-level properties on top of their own internal properties. + * A seat has a glyph projected onto the ground when the vehicle is not moving + * that is used to mark where the seat can be accessed, as well as broadcasting the current access condition of the seat. + * As indicated previously, seats are composed into categories and the categories used to control access. + * The "driver" group has already been mentioned and is usually composed of a single seat, the "first" one. + * The driver seat is typically locked to the person who can sit in it - the owner - unless manually unlocked. + * Any seat besides the "driver" that has a weapon controlled from the seat is called a "gunner" seats. + * Any other seat besides the "driver" seat and "gunner" seats is called a "passenger" seat. + * All of these seats are typically unlocked normally. + * The "trunk" also counts as an access group even though it is not directly attached to a seat and starts as "locked." + * The categories all have their own glyphs, + * sharing a red cross glyph as a "can not access" state, + * and may also use their lack of visibility to express state. + * In terms of individual access, each seat can have its current occupant ejected, save for the driver's seat. * @see `Vehicle.EquipmentUtilities` - * @param vehicleDef the vehicle's definition entry'; + * @param vehicleDef the vehicle's definition entry; * stores and unloads pertinent information about the `Vehicle`'s configuration; * used in the initialization process (`loadVehicleDefinition`) */ @@ -47,6 +75,7 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ private var trunkAccess : Option[PlanetSideGUID] = None private var jammered : Boolean = false private var cloaked : Boolean = false + private var capacitor : Int = 0 /** * Permissions control who gets to access different parts of the vehicle; @@ -170,6 +199,19 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ Cloaked } + def Capacitor : Int = capacitor + + def Capacitor_=(value: Int) : Int = { + if(value > Definition.MaximumCapacitor) { + capacitor = Definition.MaximumCapacitor + } else if (value < 0) { + capacitor = 0 + } else { + capacitor = value + } + Capacitor + } + /** * Given the index of an entry mounting point, return the infantry-accessible `Seat` associated with it. * @param mountPoint an index representing the seat position / mounting point diff --git a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala index 5742cbd3e..ab3c5da9b 100644 --- a/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/VehicleDefinition.scala @@ -6,6 +6,7 @@ import net.psforever.objects.inventory.InventoryTile import net.psforever.objects.vehicles.UtilityType import scala.collection.mutable +import scala.concurrent.duration._ /** * An object definition system used to construct and retain the parameters of various vehicles. @@ -30,6 +31,8 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { private var canCloak : Boolean = false private var canBeOwned : Boolean = true private var serverVehicleOverrideSpeeds : (Int, Int) = (0, 0) + private var deconTime : Option[FiniteDuration] = None + private var maximumCapacitor : Int = 0 Name = "vehicle" Packet = VehicleDefinition.converter @@ -85,6 +88,18 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { DeployTime } + def DeconstructionTime : Option[FiniteDuration] = deconTime + + def DeconstructionTime_=(time : FiniteDuration) : Option[FiniteDuration] = { + deconTime_=(Some(time)) + DeconstructionTime + } + + def DeconstructionTime_=(time : Option[FiniteDuration]) : Option[FiniteDuration] = { + deconTime = time + DeconstructionTime + } + def UndeployTime : Int = deploymentTime_Undeploy def UndeployTime_=(dtime : Int) : Int = { @@ -116,6 +131,13 @@ class VehicleDefinition(objectId : Int) extends ObjectDefinition(objectId) { def AutoPilotSpeed1 : Int = serverVehicleOverrideSpeeds._1 def AutoPilotSpeed2 : Int = serverVehicleOverrideSpeeds._2 + + def MaximumCapacitor : Int = maximumCapacitor + + def MaximumCapacitor_=(maxCapacitor: Int) : Int = { + maximumCapacitor = maxCapacitor + MaximumCapacitor + } } object VehicleDefinition { 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 094980fc7..c1193cc8a 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 @@ -3,80 +3,129 @@ package net.psforever.objects.definition.converter import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, Cosmetics, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} +import net.psforever.packet.game.objectcreate._ import net.psforever.types.{GrenadeState, ImplantType} import scala.annotation.tailrec import scala.util.{Success, Try} class AvatarConverter extends ObjectCreateConverter[Player]() { - override def ConstructorData(obj : Player) : Try[CharacterData] = { - val MaxArmor = obj.MaxArmor + override def ConstructorData(obj : Player) : Try[PlayerData] = { + import AvatarConverter._ Success( - CharacterData( - MakeAppearanceData(obj), - 255 * obj.Health / obj.MaxHealth, //TODO not precise - if(MaxArmor == 0) { 0 } else { 255 * obj.Armor / MaxArmor }, //TODO not precise - DressBattleRank(obj), - DressCommandRank(obj), - recursiveMakeImplantEffects(obj.Implants.iterator), - MakeCosmetics(obj.BEP), - InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), //TODO is sorting necessary? - GetDrawnSlot(obj) - ) - ) - //TODO tidy this mess up - } - - override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { - Success( - DetailedCharacterData( - MakeAppearanceData(obj), - obj.BEP, - obj.CEP, - obj.MaxHealth, - obj.Health, - obj.Armor, - obj.MaxStamina, - obj.Stamina, - obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary? - MakeImplantEntries(obj), - List.empty[String], //TODO fte list - List.empty[String], //TODO tutorial list - MakeCosmetics(obj.BEP), - InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), - GetDrawnSlot(obj) - ) + if(obj.VehicleSeated.isEmpty) { + PlayerData( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + MakeAppearanceData(obj), + MakeCharacterData(obj), + MakeInventoryData(obj), + GetDrawnSlot(obj) + ) + } + else { + PlayerData( + MakeAppearanceData(obj), + MakeCharacterData(obj), + MakeInventoryData(obj), + DrawnSlot.None + ) + } ) } + override def DetailedConstructorData(obj : Player) : Try[DetailedPlayerData] = { + import AvatarConverter._ + Success( + if(obj.VehicleSeated.isEmpty) { + DetailedPlayerData.apply( + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + MakeAppearanceData(obj), + MakeDetailedCharacterData(obj), + MakeDetailedInventoryData(obj), + GetDrawnSlot(obj) + ) + } + else { + DetailedPlayerData.apply( + MakeAppearanceData(obj), + MakeDetailedCharacterData(obj), + MakeDetailedInventoryData(obj), + DrawnSlot.None + ) + } + ) + } +} + +object AvatarConverter { /** * Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`. * @param obj the `Player` game object * @return the resulting `CharacterAppearanceData` */ - private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { + def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = { CharacterAppearanceData( - PlacementData(obj.Position, obj.Orientation, obj.Velocity), BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice), - 0, - false, - false, + voice2 = 0, + black_ops = false, + jammered = false, obj.ExoSuit, - "", - 0, + outfit_name = "", + outfit_logo = 0, obj.isBackpack, - obj.Orientation.y, - obj.FacingYawUpper, - true, + facingPitch = obj.Orientation.y, + facingYawUpper = obj.FacingYawUpper, + lfs = true, GrenadeState.None, - false, - false, - false, + is_cloaking = false, + charging_pose = false, + on_zipline = false, RibbonBars() ) } + def MakeCharacterData(obj : Player) : (Boolean,Boolean)=>CharacterData = { + val MaxArmor = obj.MaxArmor + CharacterData( + 255 * obj.Health / obj.MaxHealth, //TODO not precise + if(MaxArmor == 0) { + 0 + } + else { + 255 * obj.Armor / MaxArmor + }, //TODO not precise + DressBattleRank(obj), + DressCommandRank(obj), + recursiveMakeImplantEffects(obj.Implants.iterator), + MakeCosmetics(obj.BEP) + ) + } + + def MakeDetailedCharacterData(obj : Player) : (Option[Int])=>DetailedCharacterData = { + DetailedCharacterData( + obj.BEP, + obj.CEP, + obj.MaxHealth, + obj.Health, + obj.Armor, + obj.MaxStamina, + obj.Stamina, + obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary? + MakeImplantEntries(obj), + firstTimeEvents = List.empty[String], //TODO fte list + tutorials = List.empty[String], //TODO tutorial list + MakeCosmetics(obj.BEP) + ) + } + + def MakeInventoryData(obj : Player) : InventoryData = { + InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)) + } + + def MakeDetailedInventoryData(obj : Player) : InventoryData = { + InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)) + } + /** * Select the appropriate `UniformStyle` design for a player's accumulated battle experience points. * At certain battle ranks, all exo-suits undergo some form of coloration change. @@ -183,7 +232,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @see `Cosmetics` * @return the `Cosmetics` options */ - protected def MakeCosmetics(bep : Long) : Option[Cosmetics] = + def MakeCosmetics(bep : Long) : Option[Cosmetics] = if(DetailedCharacterData.isBR24(bep)) { Some(Cosmetics(false, false, false, false, false)) } @@ -200,12 +249,12 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { */ private def MakeInventory(obj : Player) : List[InternalSlot] = { obj.Inventory.Items - .map({ - case(_, item) => + .map(item => { val equip : Equipment = item.obj InternalSlot(equip.Definition.ObjectId, equip.GUID, item.start, equip.Definition.Packet.DetailedConstructorData(equip).get) - }).toList + }) } + /** * Given a player with equipment holsters, convert the contents of those holsters into converted-decoded packet data. * The decoded packet form is determined by the function in the parameters as both `0x17` and `0x18` conversions are available, @@ -251,7 +300,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @param equip the game object * @return the game object in decoded packet form */ - protected def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { + def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = { InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) } @@ -289,7 +338,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { * @param obj the `Player` game object * @return the holster's Enumeration value */ - protected def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { + def GetDrawnSlot(obj : Player) : DrawnSlot.Value = { try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None } } } diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala index 293c71ced..4a432fea8 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala @@ -3,8 +3,8 @@ package net.psforever.objects.definition.converter import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars} -import net.psforever.types.{GrenadeState, ImplantType} +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{CharacterVoice, GrenadeState, ImplantType} import scala.annotation.tailrec import scala.util.{Failure, Success, Try} @@ -15,21 +15,29 @@ import scala.util.{Failure, Success, Try} * Details that would not be apparent on that screen such as implants or certifications are ignored. */ class CharacterSelectConverter extends AvatarConverter { - override def ConstructorData(obj : Player) : Try[CharacterData] = Failure(new Exception("CharacterSelectConverter should not be used to generate CharacterData")) + override def ConstructorData(obj : Player) : Try[PlayerData] = Failure(new Exception("CharacterSelectConverter should not be used to generate CharacterData")) - override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { + override def DetailedConstructorData(obj : Player) : Try[DetailedPlayerData] = { Success( - DetailedCharacterData( + DetailedPlayerData.apply( + PlacementData(0, 0, 0), MakeAppearanceData(obj), - obj.BEP, - obj.CEP, - 1, 1, 0, 1, 1, - Nil, - MakeImplantEntries(obj), //necessary for correct stream length - Nil, Nil, - MakeCosmetics(obj.BEP), + DetailedCharacterData( + obj.BEP, + obj.CEP, + healthMax = 1, + health = 1, + armor = 0, + staminaMax = 1, + stamina = 1, + certs = Nil, + MakeImplantEntries(obj), //necessary for correct stream length + firstTimeEvents = Nil, + tutorials = Nil, + AvatarConverter.MakeCosmetics(obj.BEP) + ), InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)), - GetDrawnSlot(obj) + AvatarConverter.GetDrawnSlot(obj) ) ) } @@ -40,24 +48,23 @@ class CharacterSelectConverter extends AvatarConverter { * @see `AvatarConverter.MakeAppearanceData` * @return the resulting `CharacterAppearanceData` */ - private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { + private def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = { CharacterAppearanceData( - PlacementData(0f, 0f, 0f), - BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, 1), - 0, - false, - false, + BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, CharacterVoice.Mute), + voice2 = 0, + black_ops = false, + jammered = false, obj.ExoSuit, - "", - 0, - false, - 0f, - 0f, - true, + outfit_name = "", + outfit_logo = 0, + backpack = false, + facingPitch = 0, + facingYawUpper = 0, + lfs = true, GrenadeState.None, - false, - false, - false, + is_cloaking = false, + charging_pose = false, + on_zipline = false, RibbonBars() ) } @@ -90,7 +97,7 @@ class CharacterSelectConverter extends AvatarConverter { val equip : Equipment = slot.Equipment.get recursiveMakeHolsters( iter, - list :+ BuildDetailedEquipment(index, equip), + list :+ AvatarConverter.BuildDetailedEquipment(index, equip), index + 1 ) } diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala index 68b7df90d..762b48da4 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/CorpseConverter.scala @@ -3,23 +3,35 @@ package net.psforever.objects.definition.converter import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars} -import net.psforever.types.{CharacterGender, GrenadeState, Vector3} +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{CharacterGender, CharacterVoice, GrenadeState, Vector3} import scala.annotation.tailrec import scala.util.{Failure, Success, Try} class CorpseConverter extends AvatarConverter { - override def ConstructorData(obj : Player) : Try[CharacterData] = + override def ConstructorData(obj : Player) : Try[PlayerData] = Failure(new Exception("CorpseConverter should not be used to generate CharacterData")) - override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = { + override def DetailedConstructorData(obj : Player) : Try[DetailedPlayerData] = { Success( - DetailedCharacterData( + DetailedPlayerData.apply( + PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)), MakeAppearanceData(obj), - 0, 0, 0, 0, 0, 0, 0, - Nil, Nil, Nil, Nil, - None, + DetailedCharacterData( + bep = 0, + cep = 0, + healthMax = 0, + health = 0, + armor = 0, + staminaMax = 0, + stamina = 0, + certs = Nil, + implants = Nil, + firstTimeEvents = Nil, + tutorials = Nil, + cosmetics = None + ), InventoryData((MakeHolsters(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), DrawnSlot.None ) @@ -31,24 +43,23 @@ class CorpseConverter extends AvatarConverter { * @param obj the `Player` game object * @return the resulting `CharacterAppearanceData` */ - private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = { + private def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = { CharacterAppearanceData( - PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)), - BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, 0), - 0, - false, - false, + BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, CharacterVoice.Mute), + voice2 = 0, + black_ops = false, + jammered = false, obj.ExoSuit, - "", - 0, - true, - obj.Orientation.y, //TODO is this important? - 0, - true, + outfit_name = "", + outfit_logo = 0, + backpack = true, + facingPitch = obj.Orientation.y, //TODO is this important? + facingYawUpper = 0, + lfs = true, GrenadeState.None, - false, - false, - false, + is_cloaking = false, + charging_pose = false, + on_zipline = false, RibbonBars() ) } @@ -62,11 +73,10 @@ class CorpseConverter extends AvatarConverter { */ private def MakeInventory(obj : Player) : List[InternalSlot] = { obj.Inventory.Items - .map({ - case(_, item) => + .map(item => { val equip : Equipment = item.obj BuildEquipment(item.start, equip) - }).toList + }) } /** diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala index bd3213136..124a4c971 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/LockerContainerConverter.scala @@ -30,11 +30,10 @@ class LockerContainerConverter extends ObjectCreateConverter[LockerContainer]() */ private def MakeInventory(inv : GridInventory) : List[InternalSlot] = { inv.Items - .map({ - case(_, item) => + .map(item => { val equip : Equipment = item.obj InternalSlot(equip.Definition.ObjectId, equip.GUID, item.start, equip.Definition.Packet.ConstructorData(equip).get) - }).toList + }) } /** @@ -45,10 +44,9 @@ class LockerContainerConverter extends ObjectCreateConverter[LockerContainer]() */ private def MakeDetailedInventory(inv : GridInventory) : List[InternalSlot] = { inv.Items - .map({ - case(_, item) => + .map(item => { val equip : Equipment = item.obj InternalSlot(equip.Definition.ObjectId, equip.GUID, item.start, equip.Definition.Packet.DetailedConstructorData(equip).get) - }).toList + }) } } diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 81f3f2b65..f88c98b38 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -10,29 +10,74 @@ import scala.util.{Failure, Success, Try} class VehicleConverter extends ObjectCreateConverter[Vehicle]() { override def DetailedConstructorData(obj : Vehicle) : Try[VehicleData] = - Failure(new Exception("VehicleConverter should not be used to generate detailed VehicleData")) + Failure(new Exception("VehicleConverter should not be used to generate detailed VehicleData (nothing should)")) override def ConstructorData(obj : Vehicle) : Try[VehicleData] = { + val health = 255 * obj.Health / obj.MaxHealth //TODO not precise Success( VehicleData( - CommonFieldData( - PlacementData(obj.Position, obj.Orientation, obj.Velocity), - obj.Faction, - 0, - PlanetSideGUID(0) //if(obj.Owner.isDefined) { obj.Owner.get } else { PlanetSideGUID(0) } //TODO is this really Owner? - ), - 0, - 255 * obj.Health / obj.MaxHealth, //TODO not precise - false, false, + PlacementData(obj.Position, obj.Orientation, obj.Velocity), + obj.Faction, + bops = false, + destroyed = health < 3, + unk1 = 0, + obj.Jammered, + unk2 = false, + obj.Owner match { + case Some(owner) => owner + case None => PlanetSideGUID(0) + }, + unk3 = false, + health, + unk4 = false, + no_mount_points = false, obj.DeploymentState, - false, - false, + unk5 = false, + unk6 = false, obj.Cloaked, SpecificFormatData(obj), - Some(InventoryData((MakeUtilities(obj) ++ MakeMountings(obj)).sortBy(_.parentSlot))) + Some(InventoryData(MakeDriverSeat(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj))) )(SpecificFormatModifier) ) } + + private def MakeDriverSeat(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { + val offset : Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) + obj.Seats(0).Occupant match { + case Some(player) => + val mountedPlayer = VehicleData.PlayerData( + AvatarConverter.MakeAppearanceData(player), + AvatarConverter.MakeCharacterData(player), + AvatarConverter.MakeInventoryData(player), + AvatarConverter.GetDrawnSlot(player), + offset + ) + List(InventoryItemData(ObjectClass.avatar, player.GUID, 0, mountedPlayer)) + case None => + Nil + } + } + + //TODO do not use for now; causes vehicle access permission issues; may not mesh with workflows; player GUID requirements +// private def MakeSeats(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { +// var offset : Long = VehicleData.InitialStreamLengthToSeatEntries(obj.Velocity.nonEmpty, SpecificFormatModifier) +// obj.Seats +// .filter({ case (_, seat) => seat.isOccupied }) +// .map({case (index, seat) => +// val player = seat.Occupant.get +// val mountedPlayer = VehicleData.PlayerData( +// AvatarConverter.MakeAppearanceData(player), +// AvatarConverter.MakeCharacterData(player), +// AvatarConverter.MakeInventoryData(player), +// AvatarConverter.GetDrawnSlot(player), +// offset +// ) +// val entry = InventoryItemData(ObjectClass.avatar, player.GUID, index, mountedPlayer) +// //println(s"seat 0 offset: $offset, size: ${entry.bitsize}, pad: ${mountedPlayer.basic_appearance.NamePadding}") +// offset += entry.bitsize +// entry +// }).toList +// } private def MakeMountings(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { obj.Weapons.map({ diff --git a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala index 2ea7fb4a5..af5c2db43 100644 --- a/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala +++ b/common/src/main/scala/net/psforever/objects/guid/GUIDTask.scala @@ -100,7 +100,7 @@ object GUIDTask { * @return a list of `TaskResolver.GiveTask` messages */ def RegisterInventory(container : Container)(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { - container.Inventory.Items.values.map(entry => { RegisterEquipment(entry.obj)}).toList + container.Inventory.Items.map(entry => { RegisterEquipment(entry.obj)}) } /** @@ -257,7 +257,7 @@ object GUIDTask { * @return a list of `TaskResolver.GiveTask` messages */ def UnregisterInventory(container : Container)(implicit guid : ActorRef) : List[TaskResolver.GiveTask] = { - container.Inventory.Items.values.map(entry => { UnregisterEquipment(entry.obj)}).toList + container.Inventory.Items.map(entry => { UnregisterEquipment(entry.obj)}) } /** diff --git a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala index c05c04607..72967165a 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/GridInventory.scala @@ -8,7 +8,6 @@ import net.psforever.objects.EquipmentSlot import net.psforever.packet.game.PlanetSideGUID import scala.annotation.tailrec -import scala.collection.immutable.Map import scala.collection.mutable import scala.util.{Failure, Success, Try} @@ -38,7 +37,7 @@ class GridInventory extends Container { private val entryIndex : AtomicInteger = new AtomicInteger(0) private var grid : Array[Int] = Array.fill[Int](1)(-1) - def Items : Map[Int, InventoryItem] = items.toMap[Int, InventoryItem] + def Items : List[InventoryItem] = items.values.toList def Width : Int = width @@ -331,26 +330,23 @@ class GridInventory extends Container { def Insertion_CheckCollisions(start : Int, obj : Equipment, key : Int) : Boolean = { CheckCollisions(start, obj) match { case Success(Nil) => - val card = InventoryItem(obj, start) - items += key -> card - val tile = obj.Tile - SetCells(start, tile.Width, tile.Height, key) - true + InsertQuickly(start, obj, key) case _ => false } } - def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2) + def InsertQuickly(start : Int, obj : Equipment) : Boolean = InsertQuickly(start, obj, entryIndex.getAndIncrement()) -// def InsertQuickly(start : Int, obj : Equipment) : Boolean = { -// val guid : Int = obj.GUID.guid -// val card = InventoryItemData(obj, start) -// items += guid -> card -// val tile = obj.Tile -// SetCellsOffset(start, tile.width, tile.height, guid) -// true -// } + private def InsertQuickly(start : Int, obj : Equipment, key : Int) : Boolean = { + val card = InventoryItem(obj, start) + items += key -> card + val tile = obj.Tile + SetCells(start, tile.Width, tile.Height, key) + true + } + + def +=(kv : (Int, Equipment)) : Boolean = Insert(kv._1, kv._2) def Remove(index : Int) : Boolean = { val key = grid(index - Offset) diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala index 575a43a10..f387eb73e 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryEquipmentSlot.scala @@ -34,7 +34,7 @@ class InventoryEquipmentSlot(private val slot : Int, private val inv : GridInven case Some(equip) => val tile = equip.Definition.Tile inv.CheckCollisionsVar(slot, tile.Width, tile.Height) match { - case Success(Nil) => inv += slot -> equip + case Success(Nil) => inv.InsertQuickly(slot, equip) case _ => ; } diff --git a/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala b/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala index ea0b58c3e..a1468cd5c 100644 --- a/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala +++ b/common/src/main/scala/net/psforever/objects/inventory/InventoryTile.scala @@ -26,14 +26,14 @@ object InventoryTile { final val Tile33 = InventoryTile(3,3) //ammo box, pistols, ace final val Tile44 = InventoryTile(4,4) //large ammo box final val Tile55 = InventoryTile(5,5) //bfr ammo box - final val Tile66 = InventoryTile(6,6) //standard assault inventory + final val Tile66 = InventoryTile(6,6) //infiltration suit inventory final val Tile63 = InventoryTile(6,3) //rifles final val Tile93 = InventoryTile(9,3) //long-body weapons - final val Tile96 = InventoryTile(9,6) //standard exo-suit - final val Tile99 = InventoryTile(9,9) //agile exo-suit + final val Tile96 = InventoryTile(9,6) //standard exo-suit inventory + final val Tile99 = InventoryTile(9,9) //agile exo-suit inventory final val Tile1107 = InventoryTile(11, 7) //uncommon small trunk capacity - phantasm final val Tile1111 = InventoryTile(11,11) //common small trunk capacity - final val Tile1209 = InventoryTile(12, 9) //reinforced exo-suit + final val Tile1209 = InventoryTile(12, 9) //reinforced exo-suit inventory final val Tile1511 = InventoryTile(15,11) //common medium trunk capacity final val Tile1515 = InventoryTile(15,15) //common large trunk capacity final val Tile1611 = InventoryTile(16,11) //uncommon medium trunk capacity - vulture 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 5ff4f92de..82cab3d47 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -39,7 +39,7 @@ object Loadout { InfantryLoadout( label, packageSimplifications(player.Holsters()), - packageSimplifications(player.Inventory.Items.values.toList), + packageSimplifications(player.Inventory.Items), player.ExoSuit, DetermineSubtype(player) ) @@ -55,7 +55,7 @@ object Loadout { VehicleLoadout( label, packageSimplifications(vehicle.Weapons.map({ case ((index, weapon)) => InventoryItem(weapon.Equipment.get, index) }).toList), - packageSimplifications(vehicle.Trunk.Items.values.toList), + packageSimplifications(vehicle.Trunk.Items), vehicle.Definition ) } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala b/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala new file mode 100644 index 000000000..ef5bbd3f3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/hackable/Hackable.scala @@ -0,0 +1,49 @@ +package net.psforever.objects.serverobject.hackable + +import net.psforever.objects.Player +import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound} +import net.psforever.types.Vector3 + +trait Hackable { + /** An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received. */ + private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None + + private var hackSound : TriggeredSound.Value = TriggeredSound.HackDoor + + def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy + + def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) + + /** + * Set the hack state of this object by recording important information about the player that caused it. + * Set the hack state if there is no current hack state. + * Override the hack state with a new hack state if the new user has different faction affiliation. + * @param agent a `Player`, or no player + * @return the player hack entry + */ + def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = { + hackedBy match { + case None => + //set the hack state if there is no current hack state + if(agent.isDefined) { + hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) + } + case Some(_) => + //clear the hack state + if(agent.isEmpty) { + hackedBy = None + } + //override the hack state with a new hack state if the new user has different faction affiliation + else if(agent.get.Faction != hackedBy.get._1.Faction) { + hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) + } + } + HackedBy + } + + def HackSound : TriggeredSound.Value = hackSound + def HackSound_=(sound : TriggeredSound.Value) : TriggeredSound.Value = { + hackSound = sound + hackSound + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala index 8142fd9af..6c21bdc41 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala @@ -1,10 +1,9 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.locks -import net.psforever.objects.Player +import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.Amenity -import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.Vector3 +import net.psforever.packet.game.TriggeredSound /** * A structure-owned server object that is a "door lock."
@@ -15,44 +14,9 @@ import net.psforever.types.Vector3 * The `IFFLock` is ideally associated with a server map object - a `Door` - to which it acts as a gatekeeper. * @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class IFFLock(private val idef : IFFLockDefinition) extends Amenity { - /** - * An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received. - */ - private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None - - def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy - - def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) - - /** - * Set the hack state of this object by recording important information about the player that caused it. - * Set the hack state if there is no current hack state. - * Override the hack state with a new hack state if the new user has different faction affiliation. - * @param agent a `Player`, or no player - * @return the player hack entry - */ - def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = { - hackedBy match { - case None => - //set the hack state if there is no current hack state - if(agent.isDefined) { - hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) - } - case Some(_) => - //clear the hack state - if(agent.isEmpty) { - hackedBy = None - } - //override the hack state with a new hack state if the new user has different faction affiliation - else if(agent.get.Faction != hackedBy.get._1.Faction) { - hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) - } - } - HackedBy - } - +class IFFLock(private val idef : IFFLockDefinition) extends Amenity with Hackable { def Definition : IFFLockDefinition = idef + HackSound = TriggeredSound.HackDoor } object IFFLock { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala index 0af058115..960a1c3c5 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala @@ -16,7 +16,7 @@ class IFFLockControl(lock : IFFLock) extends Actor with FactionAffinityBehavior. def receive : Receive = checkBehavior.orElse { case CommonMessages.Hack(player) => lock.HackedBy = player - + sender ! true case CommonMessages.ClearHack() => lock.HackedBy = None diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala index d614b53a8..c91c787d3 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/Locker.scala @@ -3,10 +3,13 @@ package net.psforever.objects.serverobject.mblocker import akka.actor.{ActorContext, Props} import net.psforever.objects.GlobalDefinitions +import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.packet.game.TriggeredSound -class Locker extends Amenity { +class Locker extends Amenity with Hackable { def Definition : LockerDefinition = GlobalDefinitions.mb_locker + HackSound = TriggeredSound.HackTerminal } object Locker { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala index 2e037e644..7bced8d81 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/mblocker/LockerControl.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.mblocker import akka.actor.Actor +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} /** @@ -12,6 +13,11 @@ class LockerControl(locker : Locker) extends Actor with FactionAffinityBehavior. def FactionObject : FactionAffinity = locker def receive : Receive = checkBehavior.orElse { + case CommonMessages.Hack(player) => + locker.HackedBy = player + sender ! true + case CommonMessages.ClearHack() => + locker.HackedBy = None case _ => ; } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala index 2b67fa471..2fb0f248f 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlSeatDriver.scala @@ -4,6 +4,7 @@ package net.psforever.objects.serverobject.pad.process import akka.actor.{ActorRef, Props} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.types.Vector3 import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -46,7 +47,7 @@ class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnC trace("driver to be made seated in vehicle") entry.sendTo ! VehicleSpawnPad.StartPlayerSeatedInVehicle(entry.vehicle, pad) entry.vehicle.Actor.tell(Mountable.TryMount(driver, 0), entry.sendTo) //entry.sendTo should handle replies to TryMount - context.system.scheduler.scheduleOnce(1000 milliseconds, self, VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry)) + context.system.scheduler.scheduleOnce(1500 milliseconds, self, VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry)) } else { trace("driver lost; vehicle stranded on pad") @@ -55,11 +56,18 @@ class VehicleSpawnControlSeatDriver(pad : VehicleSpawnPad) extends VehicleSpawnC case VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry) => val driver = entry.driver - if(entry.sendTo == ActorRef.noSender || driver.Continent != Continent.Id) { + if(entry.sendTo == ActorRef.noSender || !driver.isAlive || driver.Continent != Continent.Id) { trace("driver lost, but operations can continue") vehicleOverride ! VehicleSpawnControl.Process.ServerVehicleOverride(entry) } + else if(entry.vehicle.Health == 0 || entry.vehicle.Position == Vector3.Zero) { + //skip ahead for cleanup + vehicleOverride ! VehicleSpawnControl.Process.ServerVehicleOverride(entry) + } else if(driver.isAlive && driver.VehicleSeated.isEmpty) { + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.DetachFromRails(entry.vehicle, pad, Continent.Id) + } context.system.scheduler.scheduleOnce(100 milliseconds, self, VehicleSpawnControlSeatDriver.AwaitDriverInSeat(entry)) } else { diff --git a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala index 41194d465..d73316180 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlServerVehicleOverride.scala @@ -3,6 +3,7 @@ package net.psforever.objects.serverobject.pad.process import akka.actor.{ActorRef, Props} import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.types.Vector3 import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -26,27 +27,33 @@ class VehicleSpawnControlServerVehicleOverride(pad : VehicleSpawnPad) extends Ve def receive : Receive = { case VehicleSpawnControl.Process.ServerVehicleOverride(entry) => val vehicle = entry.vehicle - val pad_railed = pad.Railed - if(pad_railed) { - Continent.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad, Continent.Id) - } - if(vehicle.Health == 0) { - trace(s"vehicle was already destroyed; but, everything is fine") - if(pad_railed) { - Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + val vehicleFailState = vehicle.Health == 0 || vehicle.Position == Vector3.Zero + val driverFailState = !entry.driver.isAlive || entry.driver.Continent != Continent.Id || (if(vehicle.HasGUID) { !entry.driver.VehicleSeated.contains(vehicle.GUID) } else { true }) + if(vehicleFailState || driverFailState) { + if(vehicleFailState) { + trace(s"vehicle was already destroyed") + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + } } + else { + trace(s"driver is not ready") + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad, Continent.Id) + } + } + Continent.VehicleEvents ! VehicleSpawnPad.RevealPlayer(entry.driver.GUID, Continent.Id) vehicleGuide ! VehicleSpawnControl.Process.FinalClearance(entry) } - else if(entry.sendTo != ActorRef.noSender && entry.driver.isAlive && entry.driver.Continent == Continent.Id && entry.driver.VehicleSeated.contains(vehicle.GUID)) { - trace(s"telling ${entry.driver.Name} that the server is assuming control of the ${vehicle.Definition.Name}") - entry.sendTo ! VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) - context.system.scheduler.scheduleOnce(3000 milliseconds, vehicleGuide, VehicleSpawnControl.Process.StartGuided(entry)) - } else { - if(pad_railed) { - Continent.VehicleEvents ! VehicleSpawnPad.ResetSpawnPad(pad, Continent.Id) + if(pad.Railed) { + Continent.VehicleEvents ! VehicleSpawnPad.DetachFromRails(vehicle, pad, Continent.Id) + } + if(entry.sendTo != ActorRef.noSender) { + trace(s"telling ${entry.driver.Name} that the server is assuming control of the ${vehicle.Definition.Name}") + entry.sendTo ! VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) + context.system.scheduler.scheduleOnce(3000 milliseconds, vehicleGuide, VehicleSpawnControl.Process.StartGuided(entry)) } - vehicleGuide ! VehicleSpawnControl.Process.FinalClearance(entry) } case msg @ (VehicleSpawnControl.ProcessControl.Reminder | VehicleSpawnControl.ProcessControl.GetNewOrder) => diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala new file mode 100644 index 000000000..d62dccb0b --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSilo.scala @@ -0,0 +1,85 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.resourcesilo + +import akka.actor.{ActorContext, Props} +import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.packet.game.UseItemMessage + +class ResourceSilo extends Amenity { + private var chargeLevel : Int = 0 + private val maximumCharge : Int = 1000 + // For the flashing red light on top of the NTU silo on + private var lowNtuWarningOn : Int = 0 + + // For the NTU display bar + private var capacitorDisplay : Long = 0 + + def ChargeLevel : Int = chargeLevel + + // Do not call directly. Use ResourceSilo.UpdateChargeLevel message to handle logic such as low ntu warnings + def ChargeLevel_=(charge: Int) : Int = { + if(charge < 0 ) { + chargeLevel = 0 + } else if (charge > maximumCharge) { + chargeLevel = maximumCharge + } else { + chargeLevel = charge + } + ChargeLevel + } + + def MaximumCharge : Int = maximumCharge + + def LowNtuWarningOn : Int = lowNtuWarningOn + def LowNtuWarningOn_=(enabled: Int) : Int = { + lowNtuWarningOn = enabled + LowNtuWarningOn + } + + def CapacitorDisplay : Long = capacitorDisplay + def CapacitorDisplay_=(value: Long) : Long = { + capacitorDisplay = value + CapacitorDisplay + } + + def Definition : ResourceSiloDefinition = GlobalDefinitions.resource_silo + + def Use(player: Player, msg : UseItemMessage) : ResourceSilo.Exchange = { + ResourceSilo.ChargeEvent() + } +} + + +object ResourceSilo { + + final case class Use(player: Player, msg : UseItemMessage) + final case class UpdateChargeLevel(amount: Int) + final case class LowNtuWarning(enabled: Int) + sealed trait Exchange + final case class ChargeEvent() extends Exchange + final case class ResourceSiloMessage(player: Player, msg : UseItemMessage, response : Exchange) + + + /** + * Overloaded constructor. + * @return the `Resource Silo` object + */ + def apply() : ResourceSilo = { + new ResourceSilo() + } + + /** + * Instantiate an configure a `Resource Silo` object + * @param id the unique id that will be assigned to this entity + * @param context a context to allow the object to properly set up `ActorSystem` functionality; + * not necessary for this object, but required by signature + * @return the `Locker` object + */ + def Constructor(id : Int, context : ActorContext) : ResourceSilo = { + val obj = ResourceSilo() + obj.Actor = context.actorOf(Props(classOf[ResourceSiloControl], obj), s"${obj.Definition.Name}_$id") + obj.Actor ! "startup" + obj + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala new file mode 100644 index 000000000..ae1a5db93 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala @@ -0,0 +1,90 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.resourcesilo + +import akka.actor.{Actor, ActorRef} +import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.structures.Building +import net.psforever.packet.game.PlanetSideGUID +import services.ServiceManager.Lookup +import services._ +import services.avatar.{AvatarAction, AvatarServiceMessage} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + + +/** + * An `Actor` that handles messages being dispatched to a specific `Resource Silo`. + * @param resourceSilo the `Resource Silo` object being governed + */ +class ResourceSiloControl(resourceSilo : ResourceSilo) extends Actor with FactionAffinityBehavior.Check { + def FactionObject : FactionAffinity = resourceSilo + var avatarService : ActorRef = Actor.noSender + private[this] val log = org.log4s.getLogger + + def receive : Receive = { + case "startup" => + ServiceManager.serviceManager ! Lookup("avatar") + + case ServiceManager.LookupResult("avatar", endpoint) => + avatarService = endpoint + log.info("ResourceSiloControl: Silo " + resourceSilo.GUID + " Got avatar service " + endpoint) + + // todo: This is just a temporary solution to drain NTU over time. When base object destruction is properly implemented NTU should be deducted when base objects repair themselves + context.system.scheduler.schedule(5 second, 5 second, self, ResourceSilo.UpdateChargeLevel(-1)) + context.become(Processing) + + case _ => ; + } + + def Processing : Receive = checkBehavior.orElse { + case ResourceSilo.Use(player, msg) => + sender ! ResourceSilo.ResourceSiloMessage(player, msg, resourceSilo.Use(player, msg)) + case ResourceSilo.LowNtuWarning(enabled: Int) => + resourceSilo.LowNtuWarningOn = enabled + log.trace(s"LowNtuWarning: Silo ${resourceSilo.GUID} low ntu warning set to ${enabled}") + avatarService ! AvatarServiceMessage(resourceSilo.Owner.asInstanceOf[Building].Zone.Id, AvatarAction.PlanetsideAttribute(PlanetSideGUID(resourceSilo.Owner.asInstanceOf[Building].ModelId), 47, resourceSilo.LowNtuWarningOn)) + + case ResourceSilo.UpdateChargeLevel(amount: Int) => + val siloChargeBeforeChange = resourceSilo.ChargeLevel + + // Increase if positive passed in or decrease charge level if negative number is passed in + resourceSilo.ChargeLevel += amount + if(resourceSilo.ChargeLevel > 0) { + log.trace(s"UpdateChargeLevel: Silo ${resourceSilo.GUID} set to ${resourceSilo.ChargeLevel}") + } + + + val ntuBarLevel = scala.math.round((resourceSilo.ChargeLevel.toFloat / resourceSilo.MaximumCharge.toFloat) * 10).toInt + // Only send updated capacitor display value to all clients if it has actually changed + if(resourceSilo.CapacitorDisplay != ntuBarLevel) { + log.trace(s"Silo ${resourceSilo.GUID} NTU bar level has changed from ${resourceSilo.CapacitorDisplay} to ${ntuBarLevel}") + resourceSilo.CapacitorDisplay = ntuBarLevel + resourceSilo.Owner.Actor ! Building.SendMapUpdateToAllClients() + avatarService ! AvatarServiceMessage(resourceSilo.Owner.asInstanceOf[Building].Zone.Id, AvatarAction.PlanetsideAttribute(resourceSilo.GUID, 45, resourceSilo.CapacitorDisplay)) + } + + val ntuIsLow = resourceSilo.ChargeLevel.toFloat / resourceSilo.MaximumCharge.toFloat < 0.2f + if(resourceSilo.LowNtuWarningOn == 1 && !ntuIsLow){ + self ! ResourceSilo.LowNtuWarning(0) + } else if (resourceSilo.LowNtuWarningOn == 0 && ntuIsLow) { + self ! ResourceSilo.LowNtuWarning(1) + } + + + if(resourceSilo.ChargeLevel == 0 && siloChargeBeforeChange > 0) { + // Oops, someone let the base run out of power. Shut it all down. + //todo: Make base neutral if silo hits zero NTU + + // temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it. +// avatarService ! AvatarServiceMessage(resourceSilo.Owner.asInstanceOf[Building].Zone.Id, AvatarAction.PlanetsideAttribute(PlanetSideGUID(resourceSilo.Owner.asInstanceOf[Building].ModelId), 48, 1)) + } else if (siloChargeBeforeChange == 0 && resourceSilo.ChargeLevel > 0) { + // Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal. + //todo: Check generator is online before starting up + avatarService ! AvatarServiceMessage(resourceSilo.Owner.asInstanceOf[Building].Zone.Id, AvatarAction.PlanetsideAttribute(PlanetSideGUID(resourceSilo.Owner.asInstanceOf[Building].ModelId), 48, 0)) + } + case _ => ; + } + + +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala new file mode 100644 index 000000000..1f5570622 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloDefinition.scala @@ -0,0 +1,12 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.resourcesilo + +import net.psforever.objects.definition.ObjectDefinition + +/** + * The definition for any `Resource Silo`. + * Object Id 731. + */ +class ResourceSiloDefinition extends ObjectDefinition(731) { + Name = "resource_silo" +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index 844da483b..416b86626 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -11,8 +11,8 @@ import net.psforever.types.{PlanetSideEmpire, Vector3} class Building(private val mapId : Int, private val zone : Zone, private val buildingType : StructureType.Value) extends PlanetSideServerObject { /** * The mapId is the identifier number used in BuildingInfoUpdateMessage. - * The modelId is the identifier number used in SetEmpireMessage. - */ + * The modelId is the identifier number used in SetEmpireMessage / Facility hacking / PlanetSideAttributeMessage. + */ private var modelId : Option[Int] = None private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var amenities : List[Amenity] = List.empty @@ -68,6 +68,7 @@ object Building { val obj = new Building(id, zone, buildingType) obj.Position = location obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-$buildingType-building") + obj.Actor ! "startup" obj } @@ -75,6 +76,9 @@ object Building { import akka.actor.Props val obj = new Building(id, zone, buildingType) obj.Actor = context.actorOf(Props(classOf[BuildingControl], obj), s"$id-$buildingType-building") + obj.Actor ! "startup" obj } + + final case class SendMapUpdateToAllClients() } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala index 2f7b7b7ce..f2daedbaa 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/structures/BuildingControl.scala @@ -1,20 +1,75 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.structures -import akka.actor.Actor +import akka.actor.{Actor, ActorRef} +import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.packet.game.{BuildingInfoUpdateMessage, PlanetSideGeneratorState} +import net.psforever.types.PlanetSideEmpire +import services.ServiceManager +import services.ServiceManager.Lookup +import services.galaxy.{GalaxyAction, GalaxyServiceMessage} class BuildingControl(building : Building) extends Actor with FactionAffinityBehavior.Check { def FactionObject : FactionAffinity = building + var galaxyService : ActorRef = Actor.noSender + private[this] val log = org.log4s.getLogger - def receive : Receive = checkBehavior.orElse { + def receive : Receive = { + case "startup" => + ServiceManager.serviceManager ! Lookup("galaxy") //ask for a resolver to deal with the GUID system + + case ServiceManager.LookupResult("galaxy", endpoint) => + galaxyService = endpoint + log.trace("BuildingControl: Building " + building.ModelId + " Got galaxy service " + endpoint) + + // todo: This is just a temporary solution to drain NTU over time. When base object destruction is properly implemented NTU should be deducted when base objects repair themselves + context.become(Processing) + + case _ => log.warn("Message received before startup called"); + } + + def Processing : Receive = checkBehavior.orElse { case FactionAffinity.ConvertFactionAffinity(faction) => val originalAffinity = building.Faction if(originalAffinity != (building.Faction = faction)) { building.Amenities.foreach(_.Actor forward FactionAffinity.ConfirmFactionAffinity()) } sender ! FactionAffinity.AssertFactionAffinity(building, faction) - - case _ => ; + case Building.SendMapUpdateToAllClients() => + log.info(s"Sending BuildingInfoUpdateMessage update to all clients. Zone: ${building.Zone.Number} - Building: ${building.ModelId}") + var ntuLevel = 0 + building.Amenities.filter(x => (x.Definition == GlobalDefinitions.resource_silo)).headOption.asInstanceOf[Option[ResourceSilo]] match { + case Some(obj: ResourceSilo) => + ntuLevel = obj.CapacitorDisplay.toInt + case _ => ; + } + galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate( + BuildingInfoUpdateMessage( + continent_id = building.Zone.Number, //Zone + building_id = building.Id, //Facility + ntu_level = ntuLevel, + is_hacked = false, //Hacked + PlanetSideEmpire.NEUTRAL, //Base hacked by + hack_time_remaining = 0, //Time remaining for hack (ms) + empire_own = building.Faction, //Base owned by + unk1 = 0, //!! Field != 0 will cause malformed packet. See class def. + unk1x = None, + generator_state = PlanetSideGeneratorState.Normal, //Generator state + spawn_tubes_normal = true, //Respawn tubes operating state + force_dome_active = false, //Force dome state + lattice_benefit = 0, //Lattice benefits + cavern_benefit = 0, //!! Field > 0 will cause malformed packet. See class def. + unk4 = Nil, + unk5 = 0, + unk6 = false, + unk7 = 8, //!! Field != 8 will cause malformed packet. See class def. + unk7x = None, + boost_spawn_pain = false, //Boosted spawn room pain field + boost_generator_pain = false //Boosted generator room pain field + ))) + case default => + log.warn(s"BuildingControl: Unknown message ${default} received from ${sender().path}") } } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala index 753cdb65c..fcc8c073a 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.terminals import akka.actor.Actor +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} /** @@ -18,6 +19,11 @@ class ProximityTerminalControl(term : Terminal with ProximityUnit) extends Actor def receive : Receive = checkBehavior .orElse(proximityBehavior) .orElse { + case CommonMessages.Hack(player) => + term.HackedBy = player + sender ! true + case CommonMessages.ClearHack() => + term.HackedBy = None case _ => ; } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index 7a8676bbc..61f587e79 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -3,48 +3,17 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects.Player import net.psforever.objects.definition.VehicleDefinition +import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.structures.Amenity -import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{TransactionType, Vector3} +import net.psforever.packet.game.{ItemTransactionMessage, TriggeredSound} +import net.psforever.types.TransactionType /** * A structure-owned server object that is a "terminal" that can be accessed for amenities and services. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields */ -class Terminal(tdef : TerminalDefinition) extends Amenity { - /** An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received. */ - private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None - - def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy - - def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent)) - - /** - * Set the hack state of this object by recording important information about the player that caused it. - * Set the hack state if there is no current hack state. - * Override the hack state with a new hack state if the new user has different faction affiliation. - * @param agent a `Player`, or no player - * @return the player hack entry - */ - def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = { - hackedBy match { - case None => - //set the hack state if there is no current hack state - if(agent.isDefined) { - hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) - } - case Some(_) => - //clear the hack state - if(agent.isEmpty) { - hackedBy = None - } - //override the hack state with a new hack state if the new user has different faction affiliation - else if(agent.get.Faction != hackedBy.get._1.Faction) { - hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position) - } - } - HackedBy - } +class Terminal(tdef : TerminalDefinition) extends Amenity with Hackable { + HackSound = TriggeredSound.HackTerminal //the following fields and related methods are neither finalized nor integrated; GOTO Request private var health : Int = 100 //TODO not real health value diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala index 4efa324e1..7474706c9 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.terminals import akka.actor.Actor +import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} /** @@ -15,6 +16,12 @@ class TerminalControl(term : Terminal) extends Actor with FactionAffinityBehavio case Terminal.Request(player, msg) => sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg)) + case CommonMessages.Hack(player) => + term.HackedBy = player + sender ! true + case CommonMessages.ClearHack() => + term.HackedBy = None + case _ => ; } diff --git a/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala b/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala index f69f927df..bf9077409 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/Seat.scala @@ -15,7 +15,7 @@ class Seat(private val seatDef : SeatDefinition) { /** * Is this seat occupied? - * @return the GUID of the player sitting in this seat, or `None` if it is left vacant + * @return the Player object of the player sitting in this seat, or `None` if it is left vacant */ def Occupant : Option[Player] = { this.occupant @@ -25,7 +25,7 @@ class Seat(private val seatDef : SeatDefinition) { * The player is trying to sit down. * Seats are exclusive positions that can only hold one occupant at a time. * @param player the player who wants to sit, or `None` if the occupant is getting up - * @return the GUID of the player sitting in this seat, or `None` if it is left vacant + * @return the Player object of the player sitting in this seat, or `None` if it is left vacant */ def Occupant_=(player : Player) : Option[Player] = Occupant_=(Some(player)) diff --git a/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala b/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala index 8558b3866..af01fb1e7 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/SeatArmorRestriction.scala @@ -4,7 +4,7 @@ package net.psforever.objects.vehicles /** * An `Enumeration` of exo-suit-based seat access restrictions.
*
- * The default value is `NoMax` as that is the most common seat. + * The default value is `NoMax` as that is the most common seat type. * `NoReinforcedOrMax` is next most common. * `MaxOnly` is a rare seat restriction found in pairs on Galaxies and on the large "Ground Transport" vehicles. */ diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index adb2bd3a5..dc114e68c 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -3,9 +3,10 @@ package net.psforever.objects.vehicles import akka.actor.Actor import net.psforever.objects.Vehicle -import net.psforever.objects.serverobject.mount.MountableBehavior +import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.deploy.DeploymentBehavior +import net.psforever.types.ExoSuitType /** * An `Actor` that handles messages being dispatched to a specific `Vehicle`.
@@ -32,9 +33,32 @@ class VehicleControl(vehicle : Vehicle) extends Actor def Enabled : Receive = checkBehavior .orElse(deployBehavior) - .orElse(mountBehavior) .orElse(dismountBehavior) .orElse { + case Mountable.TryMount(user, seat_num) => + val exosuit = user.ExoSuit + val restriction = vehicle.Seats(seat_num).ArmorRestriction + val seatGroup = vehicle.SeatPermissionGroup(seat_num).getOrElse(AccessPermissionGroup.Passenger) + val permission = vehicle.PermissionGroup(seatGroup.id).getOrElse(VehicleLockState.Empire) + if( + (if(seatGroup == AccessPermissionGroup.Driver) { + vehicle.Owner.contains(user.GUID) || vehicle.Owner.isEmpty || permission != VehicleLockState.Locked + } + else { + permission != VehicleLockState.Locked + }) && + (exosuit match { + case ExoSuitType.MAX => restriction == SeatArmorRestriction.MaxOnly + case ExoSuitType.Reinforced => restriction == SeatArmorRestriction.NoMax + case _ => restriction != SeatArmorRestriction.MaxOnly + }) + ) { + mountBehavior.apply(Mountable.TryMount(user, seat_num)) + } + else { + sender ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, seat_num)) + } + case FactionAffinity.ConvertFactionAffinity(faction) => val originalAffinity = vehicle.Faction if(originalAffinity != (vehicle.Faction = faction)) { diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 1c25abaa4..930ac574c 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -89,7 +89,7 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) { if(accessor == ActorRef.noSender) { implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly accessor = context.actorOf(RandomPool(25).props(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid))), s"$Id-uns") - ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground") + ground = context.actorOf(Props(classOf[ZoneGroundActor], this, equipmentOnGround), s"$Id-ground") transport = context.actorOf(Props(classOf[ZoneVehicleActor], this, vehicles), s"$Id-vehicles") population = context.actorOf(Props(classOf[ZonePopulationActor], this, players, corpses), s"$Id-players") @@ -386,27 +386,17 @@ object Zone { final case class NoValidSpawnPoint(zone_number : Int, spawn_group : Option[Int]) } - /** - * Message to relinguish an item and place in on the ground. - * @param item the piece of `Equipment` - * @param pos where it is dropped - * @param orient in which direction it is facing when dropped - */ - final case class DropItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) + object Ground { + final case class DropItem(item : Equipment, pos : Vector3, orient : Vector3) + final case class ItemOnGround(item : Equipment, pos : Vector3, orient : Vector3) + final case class CanNotDropItem(zone : Zone, item : Equipment, reason : String) - /** - * Message to attempt to acquire an item from the ground (before somoene else?). - * @param player who wants the piece of `Equipment` - * @param item_guid the unique identifier of the piece of `Equipment` - */ - final case class GetItemOnGround(player : Player, item_guid : PlanetSideGUID) + final case class PickupItem(item_guid : PlanetSideGUID) + final case class ItemInHand(item : Equipment) + final case class CanNotPickupItem(zone : Zone, item_guid : PlanetSideGUID, reason : String) - /** - * Message to give an item from the ground to a specific user. - * @param player who wants the piece of `Equipment` - * @param item the piece of `Equipment` - */ - final case class ItemFromGround(player : Player, item : Equipment) + final case class RemoveItem(item_guid : PlanetSideGUID) + } object Vehicle { final case class Spawn(vehicle : Vehicle) diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index 4d6f0c4f4..80f6a5471 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -50,10 +50,10 @@ class ZoneActor(zone : Zone) extends Actor { zone.Population forward msg //frwd to Ground Actor - case msg @ Zone.DropItemOnGround => + case msg @ Zone.Ground.DropItem => zone.Ground forward msg - case msg @ Zone.GetItemOnGround => + case msg @ Zone.Ground.PickupItem => zone.Ground forward msg //frwd to Vehicle Actor diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala index 5a001e47d..5b4f3e676 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneGroundActor.scala @@ -12,22 +12,35 @@ import scala.collection.mutable.ListBuffer * na * @param equipmentOnGround a `List` of items (`Equipment`) dropped by players on the ground and can be collected again */ -class ZoneGroundActor(equipmentOnGround : ListBuffer[Equipment]) extends Actor { +class ZoneGroundActor(zone : Zone, equipmentOnGround : ListBuffer[Equipment]) extends Actor { //private[this] val log = org.log4s.getLogger def receive : Receive = { - case Zone.DropItemOnGround(item, pos, orient) => - item.Position = pos - item.Orientation = orient - equipmentOnGround += item - - case Zone.GetItemOnGround(player, item_guid) => - FindItemOnGround(item_guid) match { - case Some(item) => - sender ! Zone.ItemFromGround(player, item) - case None => - org.log4s.getLogger.warn(s"item on ground $item_guid was requested by $player for pickup but was not found") + case Zone.Ground.DropItem(item, pos, orient) => + sender ! (if(!item.HasGUID) { + Zone.Ground.CanNotDropItem(zone, item, "not registered yet") } + else if(zone.GUID(item.GUID).isEmpty) { + Zone.Ground.CanNotDropItem(zone, item, "registered to some other zone") + } + else if(equipmentOnGround.contains(item)) { + Zone.Ground.CanNotDropItem(zone, item, "already dropped") + } + else { + equipmentOnGround += item + Zone.Ground.ItemOnGround(item, pos, orient) + }) + + case Zone.Ground.PickupItem(item_guid) => + sender ! (FindItemOnGround(item_guid) match { + case Some(item) => + Zone.Ground.ItemInHand(item) + case None => + Zone.Ground.CanNotPickupItem(zone, item_guid, "can not find") + }) + + case Zone.Ground.RemoveItem(item_guid) => + FindItemOnGround(item_guid) //intentionally no callback case _ => ; } diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 15bdaa72a..bb5162080 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -231,6 +231,21 @@ object PacketHelpers { * @return a codec that works on a List of A but excludes the size from the encoding */ def listOfNSized[A](size : Long, codec : Codec[A]) : Codec[List[A]] = PacketHelpers.listOfNAligned(provide(if(size < 0) 0 else size), 0, codec) + + /** + * A `peek` that decodes like the normal but encodes nothing. + * Decoding `Codec[A]` from the input vector emits a value but reverts to the prior read position. + * Encoding `Codec[A]` to the input vector appends no new data to the input vector. + * In effect, `peek` is a harmless meta-`Codec` that introduces no changes to the input vector. + * @see `scodec.codecs.peek` or `codecs/package.scala:peek` + * @param target codec that decodes the value + * @return codec that behaves the same as `target` but resets remainder to the input vector + */ + def peek[A](target: Codec[A]): Codec[A] = new Codec[A] { + def sizeBound = target.sizeBound + def encode(a: A) = Attempt.Successful(BitVector.empty) + def decode(b: BitVector) = target.decode(b).map { _.mapRemainder(_ => b) } + } } /** diff --git a/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala index fe7f0b44a..b8b7d9c5d 100644 --- a/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ActionResultMessage.scala @@ -19,13 +19,18 @@ final case class ActionResultMessage(successful : Boolean, } object ActionResultMessage extends Marshallable[ActionResultMessage] { - def apply() : ActionResultMessage = { - ActionResultMessage(true, None) - } + /** + * A message where the result is always a pass. + * @return an `ActionResultMessage` object + */ + def Pass : ActionResultMessage = ActionResultMessage(true, None) - def apply(error : Long) : ActionResultMessage = { - ActionResultMessage(false, Some(error)) - } + /** + * A message where the result is always a failure. + * @param error the error code + * @return an `ActionResultMessage` object + */ + def Fail(error : Long) : ActionResultMessage = ActionResultMessage(false, Some(error)) implicit val codec : Codec[ActionResultMessage] = ( ("successful" | bool) >>:~ { res => diff --git a/common/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala b/common/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala index 29724f0c9..635fa0740 100644 --- a/common/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala @@ -2,7 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} @@ -12,7 +12,7 @@ import shapeless.{::, HNil} */ final case class CharacterCreateRequestMessage(name : String, headId : Int, - voiceId : Int, + voiceId : CharacterVoice.Value, gender : CharacterGender.Value, empire : PlanetSideEmpire.Value) extends PlanetSideGamePacket { @@ -22,10 +22,12 @@ final case class CharacterCreateRequestMessage(name : String, } object CharacterCreateRequestMessage extends Marshallable[CharacterCreateRequestMessage] { + private val character_voice_codec = PacketHelpers.createEnumerationCodec(CharacterVoice, uint8) + implicit val codec : Codec[CharacterCreateRequestMessage] = ( ("name" | PacketHelpers.encodedWideString) :: ("headId" | uint8L) :: - ("voiceId" | uint8L) :: + ("voiceId" | character_voice_codec) :: ("gender" | CharacterGender.codec) :: ("empire" | PlanetSideEmpire.codec) ).exmap[CharacterCreateRequestMessage] ( diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala index 4040714fe..7bbd21e88 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala @@ -71,36 +71,20 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess ObjectCreateDetailedMessage(ObjectCreateBase.streamLen(None, data), objectClass, guid, None, Some(data)) } - /** - * Take the important information of a game piece and transform it into bit data. - * This function is fail-safe because it catches errors involving bad parsing of the object data. - * Generally, the `Exception` messages themselves are not useful here. - * @param objClass the code for the type of object being deconstructed - * @param obj the object data - * @return the bitstream data - * @see ObjectClass.selectDataCodec - */ - def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = { - var out = BitVector.empty - try { - val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption - if(outOpt.isDefined) - out = outOpt.get - } - catch { - case _ : Exception => - //catch and release, any sort of parse error - } - out - } - implicit val codec : Codec[ObjectCreateDetailedMessage] = ObjectCreateBase.baseCodec.exmap[ObjectCreateDetailedMessage] ( { case _ :: _ :: _ :: _ :: BitVector.empty :: HNil => Attempt.failure(Err("no data to decode")) case len :: cls :: guid :: par :: data :: HNil => - val obj = ObjectCreateBase.decodeData(cls, data, ObjectClass.selectDataDetailedCodec) + val obj = ObjectCreateBase.decodeData(cls, data, + if(par.isDefined) { + ObjectClass.selectDataDetailedCodec + } + else { + ObjectClass.selectDataDroppedDetailedCodec + } + ) Attempt.successful(ObjectCreateDetailedMessage(len, cls, guid, par, obj)) }, { @@ -109,7 +93,14 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess case ObjectCreateDetailedMessage(_, cls, guid, par, Some(obj)) => val len = ObjectCreateBase.streamLen(par, obj) //even if a stream length has been assigned, it can not be trusted during encoding - val bitvec = ObjectCreateBase.encodeData(cls, obj, ObjectClass.selectDataDetailedCodec) + val bitvec = ObjectCreateBase.encodeData(cls, obj, + if(par.isDefined) { + ObjectClass.selectDataDetailedCodec + } + else { + ObjectClass.selectDataDroppedDetailedCodec + } + ) Attempt.successful(len :: cls :: guid :: par :: bitvec :: HNil) } ) diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala index a68fa1fbc..5ab2d7b2c 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectDetachMessage.scala @@ -40,6 +40,14 @@ final case class ObjectDetachMessage(parent_guid : PlanetSideGUID, } object ObjectDetachMessage extends Marshallable[ObjectDetachMessage] { + def apply(parent_guid : PlanetSideGUID, child_guid : PlanetSideGUID, pos : Vector3, orient : Vector3) : ObjectDetachMessage = { + ObjectDetachMessage(parent_guid, child_guid, pos, orient.x, orient.y, orient.z) + } + + def apply(parent_guid : PlanetSideGUID, child_guid : PlanetSideGUID, pos : Vector3, orient_z : Float) : ObjectDetachMessage = { + ObjectDetachMessage(parent_guid, child_guid, pos, 0, 0, orient_z) + } + implicit val codec : Codec[ObjectDetachMessage] = ( ("parent_guid" | PlanetSideGUID.codec) :: ("child_guid" | PlanetSideGUID.codec) :: diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index 636a49b4f..1dc999bf2 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -39,7 +39,7 @@ import scodec.codecs._ *
* Players/General:
* Server to client :
- * `0 - health`
+ * `0 - health (setting to zero on vehicles/terminals will destroy them)`
* `1 - healthMax`
* `2 - stamina`
* `3 - staminaMax`
@@ -104,6 +104,11 @@ import scodec.codecs._ * `35 - BR. Value is the BR`
* `36 - CR. Value is the CR`
* `43 - Info on avatar name : 0 = Nothing, 1 = "(LD)" message`
+ * `45 - NTU charge bar 0-10, 5 = 50% full. Seems to apply to both ANT and NTU Silo (possibly siphons?)`
+ * 47 - Sets base NTU level to CRITICAL. MUST use base modelId not base GUID + * 48 - Set to 1 to send base power loss message & turns on red warning lights throughout base. MUST use base modelId not base GUID + * 49 - Vehicle texture effects state? (>0 turns on ANT panel glow or ntu silo panel glow + orbs) (bit?) + * `52 - Vehicle particle effects? (>0 turns on orbs going towards ANT. Doesn't affect silo) (bit?) * `53 - LFS. Value is 1 to flag LFS`
* `54 - Player "Aura". Values can be expressed in the first byte's lower nibble:`
* - 0 is nothing
@@ -114,10 +119,12 @@ import scodec.codecs._ * -- e.g., 13 = 8 + 4 + 1 = fire and LLU and plasma
* `55 - "Someone is attempting to Heal you". Value is 1`
* `56 - "Someone is attempting to Repair you". Value is 1`
+ * `67 - Enables base shields (from cavern module/lock). MUST use base modelId not GUID`
* `73 - "You are locked into the Core Beam. Charging your Module now.". Value is 1 to active`
* `77 - Cavern Facility Captures. Value is the number of captures`
* `78 - Cavern Kills. Value is the number of kills`
* `106 - Custom Head`
+ * `116 - Apply colour to REK beam and REK icon above players (0 = yellow, 1 = red, 2 = purple, 3 = blue)`
* Client to Server :
* `106 - Custom Head`
*
@@ -126,13 +133,15 @@ import scodec.codecs._ * `11 - Gunner seat(s) permissions (same)`
* `12 - Passenger seat(s) permissions (same)`
* `13 - Trunk permissions (same)`
- * `21 - Asserts first time event eligibility / makes owner if no owner is assigned`
+ * `21 - Declare a player the vehicle's owner, by globally unique identifier`
* `22 - Toggles gunner and passenger mount points (1 = hides, 0 = reveals; this also locks their permissions)`
- * `68 - ???`
+ * `54 - Vehicle EMP? Plays sound as if vehicle had been hit by EMP`
+ * `68 - Vehicle shield health`
* `79 - ???`
* `80 - Damage vehicle (unknown value)`
* `81 - ???`
- * `113 - ???` + * `113 - `Vehicle capacitor - e.g. Leviathan EMP charge` + * * @param player_guid the player * @param attribute_type na * @param attribute_value na diff --git a/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala b/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala index 24cc5fa92..68bda057f 100644 --- a/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala @@ -16,8 +16,8 @@ object TriggeredSound extends Enumeration { val SpawnInTube, - Unknown1, - Hack, + HackTerminal, + HackVehicle, HackDoor, Unknown4, LockedOut, diff --git a/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala b/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala index 0df5c38f5..b66180b9f 100644 --- a/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/UseItemMessage.scala @@ -9,7 +9,7 @@ import scodec.codecs._ /** * (Where the child object was before it was moved is not specified or important.)
* @param avatar_guid the player. - * @param unk1 dont know how call that. It's the "item" ID when use a rek to hack or a medkit to heal. + * @param item_used_guid The "item" GUID used e.g. a rek to hack or a medkit to heal. * @param object_guid can be : Door, Terminal, Avatar (medkit). * @param unk2 ??? * @param unk3 ??? true when use a rek (false when door, medkit or open equip term) @@ -21,7 +21,7 @@ import scodec.codecs._ * @param itemType object ID from game_objects.adb (ex 612 is an equipment terminal, for medkit we have 121 (avatar)) */ final case class UseItemMessage(avatar_guid : PlanetSideGUID, - unk1 : Int, + item_used_guid : Int, object_guid : PlanetSideGUID, unk2 : Long, unk3 : Boolean, @@ -40,7 +40,7 @@ final case class UseItemMessage(avatar_guid : PlanetSideGUID, object UseItemMessage extends Marshallable[UseItemMessage] { implicit val codec : Codec[UseItemMessage] = ( ("avatar_guid" | PlanetSideGUID.codec) :: - ("unk1" | uint16L) :: + ("item_used_guid" | uint16L) :: ("object_guid" | PlanetSideGUID.codec) :: ("unk2" | uint32L) :: ("unk3" | bool) :: diff --git a/common/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala b/common/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala index d3a87bdf4..eb3dc21f2 100644 --- a/common/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/WeaponFireMessage.scala @@ -6,10 +6,25 @@ import net.psforever.types.Vector3 import scodec.Codec import scodec.codecs._ -/** WeaponFireMessage seems to be sent each time a weapon actually shoots. +/** + * WeaponFireMessage seems to be sent each time a weapon actually shoots. * - * See [[PlayerStateMessageUpstream]] for explanation of seq_time. - */ + * + * @param seq_time See [[PlayerStateMessageUpstream]] for explanation of seq_time. + * @param weapon_guid + * @param projectile_guid + * @param shot_origin + * @param unk1 Always zero from testing so far + * @param unk2 Seems semi-random + * @param unk3 Seems semi-random + * @param unk4 Maximum travel distance in meters - seems to be zero for decimator rockets + * @param unk5 Possibly always 255 from testing + * @param unk6 0 for bullet + * 1 for possibly delayed explosion (thumper alt fire) or thresher/leviathan flux cannon + * 2 for vs starfire (lockon type?) + * 3 for thrown (e.g. grenades) + * @param unk7 Seems to be thrown weapon velocity/direction +*/ final case class WeaponFireMessage(seq_time : Int, weapon_guid : PlanetSideGUID, projectile_guid : PlanetSideGUID, diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala new file mode 100644 index 000000000..39cacc32b --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/BasicCharacterData.scala @@ -0,0 +1,25 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.types._ + +/** + * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
+ *
+ * This partition of the data stream contains information used to represent how the player's avatar is presented. + * This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet. + * @see `PlanetSideEmpire`
+ * `CharacterGender` + * @param name the unique name of the avatar; + * minimum of two characters + * @param faction the empire to which the avatar belongs + * @param sex whether the avatar is `Male` or `Female` + * @param head the avatar's face and hair; + * by row and column on the character creation screen, the high nibble is the row and the low nibble is the column + * @param voice the avatar's voice selection + */ +final case class BasicCharacterData(name : String, + faction : PlanetSideEmpire.Value, + sex : CharacterGender.Value, + head : Int, + voice : CharacterVoice.Value) 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 92dd6e6b1..fb6eee6ed 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 @@ -7,38 +7,6 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} -/** - * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
- *
- * This partition of the data stream contains information used to represent how the player's avatar is presented. - * This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet.
- *
- * Voice:
- * `    MALE      FEMALE`
- * `0 - no voice  no voice`
- * `1 - male_1    female_1`
- * `2 - male_2    female_2`
- * `3 - male_3    female_3`
- * `4 - male_4    female_4`
- * `5 - male_5    female_5`
- * `6 - female_1  no voice`
- * `7 - female_2  no voice` - * @param name the unique name of the avatar; - * minimum of two characters - * @param faction the empire to which the avatar belongs - * @param sex whether the avatar is `Male` or `Female` - * @param head the avatar's face and hair; - * by row and column on the character creation screen, the high nibble is the row and the low nibble is the column - * @param voice the avatar's voice selection - * @see `PlanetSideEmpire` - * @see `CharacaterGender` - */ -final case class BasicCharacterData(name : String, - faction : PlanetSideEmpire.Value, - sex : CharacterGender.Value, - head : Int, - voice : Int) - /** * A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.
*
@@ -60,8 +28,13 @@ final case class BasicCharacterData(name : String, *
* Exploration:
* How do I crouch? - * @param pos the position of the character in the world environment (in three coordinates) - * @param basic_appearance the player's cardinal appearance settings + * @see `CharacterData`
+ * `DetailedCharacterData`
+ * `ExoSuitType`
+ * `GrenadeState`
+ * `RibbonBars` + * @see `http://www.planetside-universe.com/p-outfit-decals-31.htm` + * @param app the player's cardinal appearance settings * @param voice2 na; * affects the frequency by which the character's voice is heard (somehow); * commonly 3 for best results @@ -86,16 +59,8 @@ final case class BasicCharacterData(name : String, * @param charging_pose animation pose for both charging modules and BFR imprinting * @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line * @param ribbons the four merit commendation ribbon medals - * @see `CharacterData` - * @see `DetailedCharacterData` - * @see `PlacementData` - * @see `ExoSuitType` - * @see `GrenadeState` - * @see `RibbonBars` - * @see `http://wiki.planetsidesyndicate.com/index.php?title=Outfit_Logo` for a list of outfit decals */ -final case class CharacterAppearanceData(pos : PlacementData, - basic_appearance : BasicCharacterData, +final case class CharacterAppearanceData(app : BasicCharacterData, voice2 : Int, black_ops : Boolean, jammered : Boolean, @@ -110,22 +75,36 @@ final case class CharacterAppearanceData(pos : PlacementData, is_cloaking : Boolean, charging_pose : Boolean, on_zipline : Boolean, - ribbons : RibbonBars) extends StreamBitSize { + ribbons : RibbonBars) + (name_padding : Int) extends StreamBitSize { override def bitsize : Long = { //factor guard bool values into the base size, not its corresponding optional field - val placementSize : Long = pos.bitsize - val nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.vel) - val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding + val nameStringSize : Long = StreamBitSize.stringBitSize(app.name, 16) + name_padding + val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + + CharacterAppearanceData.outfitNamePadding //even if the outfit_name is blank, string is always padded val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0) - 335L + placementSize + nameStringSize + outfitStringSize + altModelSize + 335L + nameStringSize + outfitStringSize + altModelSize //base value includes the ribbons } + + /** + * External access to the value padding on the name field. + * The padding will always be a number 0-7. + * @return the pad length in bits + */ + def NamePadding : Int = name_padding + + /** + * When a player is released-dead or attached to a zipline, their basic infantry model is replaced with a different one. + * @return the length of the variable field that exists when using alternate models + */ + def altModelBit : Option[Int] = CharacterAppearanceData.altModelBit(this) } object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { /** * When a player is released-dead or attached to a zipline, their basic infantry model is replaced with a different one. - * In the former casde, a backpack. + * In the former case, a backpack. * In the latter case, a ball of colored energy. * In this state, the length of the stream of data is modified. * @param app the appearance @@ -138,20 +117,6 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { None } - /** - * Get the padding of the player's name. - * The padding will always be a number 0-7. - * @return the pad length in bits - */ - def namePadding(move : Option[_]) : Int = { - if(move.isDefined) { - 2 - } - else { - 4 - } - } - /** * Get the padding of the outfit's name. * The padding will always be a number 0-7. @@ -161,64 +126,63 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { 6 } - implicit val codec : Codec[CharacterAppearanceData] = ( - ("pos" | PlacementData.codec) >>:~ { pos => - ("faction" | PlanetSideEmpire.codec) :: - ("black_ops" | bool) :: - (("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models) + def codec(name_padding : Int) : Codec[CharacterAppearanceData] = ( + ("faction" | PlanetSideEmpire.codec) :: + ("black_ops" | bool) :: + (("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models) + ignore(1) :: //unknown + ("jammered" | bool) :: + bool :: //crashes client + uint(16) :: //unknown, but usually 0 + ("name" | PacketHelpers.encodedWideStringAligned(name_padding)) :: + ("exosuit" | ExoSuitType.codec) :: + ignore(2) :: //unknown + ("sex" | CharacterGender.codec) :: + ("head" | uint8L) :: + ("voice" | CharacterVoice.codec) :: + ("voice2" | uint2L) :: + ignore(78) :: //unknown + uint16L :: //usually either 0 or 65535 + uint32L :: //for outfit_name (below) to be visible in-game, this value should be non-zero + ("outfit_name" | PacketHelpers.encodedWideStringAligned( outfitNamePadding )) :: + ("outfit_logo" | uint8L) :: ignore(1) :: //unknown - ("jammered" | bool) :: - bool :: //crashes client - uint(16) :: //unknown, but usually 0 - ("name" | PacketHelpers.encodedWideStringAligned( namePadding(pos.vel) )) :: - ("exosuit" | ExoSuitType.codec) :: - ignore(2) :: //unknown - ("sex" | CharacterGender.codec) :: - ("head" | uint8L) :: - ("voice" | uint(3)) :: - ("voice2" | uint2L) :: - ignore(78) :: //unknown - uint16L :: //usually either 0 or 65535 - uint32L :: //for outfit_name (below) to be visible in-game, this value should be non-zero - ("outfit_name" | PacketHelpers.encodedWideStringAligned( outfitNamePadding )) :: - ("outfit_logo" | uint8L) :: - ignore(1) :: //unknown - ("backpack" | bool) :: //requires alt_model flag (does NOT require health == 0) - bool :: //stream misalignment when set - ("facingPitch" | Angular.codec_pitch) :: - ("facingYawUpper" | Angular.codec_yaw(0f)) :: - ignore(1) :: //unknown - conditional(alt_model, bool) :: //alt_model flag adds a bit before lfs - ignore(1) :: //an alternate lfs? - ("lfs" | bool) :: - ("grenade_state" | GrenadeState.codec_2u) :: //note: bin10 and bin11 are neutral (bin00 is not defined) - ("is_cloaking" | bool) :: - ignore(1) :: //unknown - bool :: //stream misalignment when set - ("charging_pose" | bool) :: - ignore(1) :: //alternate charging pose? - ("on_zipline" | bool) :: //requires alt_model flag - ("ribbons" | RibbonBars.codec) - }) - }).exmap[CharacterAppearanceData] ( + ("backpack" | bool) :: //requires alt_model flag (does NOT require health == 0) + bool :: //stream misalignment when set + ("facingPitch" | Angular.codec_pitch) :: + ("facingYawUpper" | Angular.codec_yaw(0f)) :: + ignore(1) :: //unknown + conditional(alt_model, bool) :: //alt_model flag adds a bit before lfs + ignore(1) :: //an alternate lfs? + ("lfs" | bool) :: + ("grenade_state" | GrenadeState.codec_2u) :: //note: bin10 and bin11 are neutral (bin00 is not defined) + ("is_cloaking" | bool) :: + ignore(1) :: //unknown + bool :: //stream misalignment when set + ("charging_pose" | bool) :: + ignore(1) :: //alternate charging pose? + ("on_zipline" | bool) :: //requires alt_model flag + ("ribbons" | RibbonBars.codec) + }) + ).exmap[CharacterAppearanceData] ( { - case _ :: _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil | - _ :: _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: HNil => + case _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil | + _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: HNil => Attempt.Failure(Err("invalid character appearance data; can not encode alternate model without required bit set")) - case pos :: faction :: bops :: _ :: _ :: jamd :: false :: 0 :: name :: suit :: _ :: sex :: head :: v1 :: v2 :: _ :: _ :: _/*has_outfit_name*/ :: outfit :: logo :: _ :: bpack :: false :: facingPitch :: facingYawUpper :: _ :: _ :: _ :: lfs :: gstate :: cloaking :: _ :: false :: charging :: _ :: zipline :: ribbons :: HNil => + case faction :: bops :: _ :: _ :: jamd :: false :: 0 :: name :: suit :: _ :: sex :: head :: v1 :: v2 :: _ :: _ :: _/*has_outfit_name*/ :: outfit :: logo :: _ :: bpack :: false :: facingPitch :: facingYawUpper :: _ :: _ :: _ :: lfs :: gstate :: cloaking :: _ :: false :: charging :: _ :: zipline :: ribbons :: HNil => Attempt.successful( - CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) + CharacterAppearanceData(BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons)(name_padding) ) case _ => Attempt.Failure(Err("invalid character appearance data; can not encode")) }, { - case CharacterAppearanceData(_, BasicCharacterData(name, PlanetSideEmpire.NEUTRAL, _, _, _), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => + case CharacterAppearanceData(BasicCharacterData(name, PlanetSideEmpire.NEUTRAL, _, _, _), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => Attempt.failure(Err(s"character $name's faction can not declare as neutral")) - case CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) => + case CharacterAppearanceData(BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) => val has_outfit_name : Long = outfit.length.toLong //TODO this is a kludge var alt_model : Boolean = false var alt_model_extrabit : Option[Boolean] = None @@ -227,11 +191,13 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { alt_model_extrabit = Some(false) } Attempt.successful( - pos :: faction :: bops :: alt_model :: () :: jamd :: false :: 0 :: name :: suit :: () :: sex :: head :: v1 :: v2 :: () :: 0 :: has_outfit_name :: outfit :: logo :: () :: bpack :: false :: facingPitch :: facingYawUpper :: () :: alt_model_extrabit :: () :: lfs :: gstate :: cloaking :: () :: false :: charging :: () :: zipline :: ribbons :: HNil + faction :: bops :: alt_model :: () :: jamd :: false :: 0 :: name :: suit :: () :: sex :: head :: v1 :: v2 :: () :: 0 :: has_outfit_name :: outfit :: logo :: () :: bpack :: false :: facingPitch :: facingYawUpper :: () :: alt_model_extrabit :: () :: lfs :: gstate :: cloaking :: () :: false :: charging :: () :: zipline :: ribbons :: HNil ) case _ => Attempt.Failure(Err("invalid character appearance data; can not decode")) } ) + + implicit val codec : Codec[CharacterAppearanceData] = codec(0) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index 51d08d63c..d89e6d379 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -42,32 +42,25 @@ object UniformStyle extends Enumeration { } /** - * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data. - * This densely-packed information outlines most of the specifics of depicting some other character.
+ * A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.
*
- * The character created by this data is treated like an NPC from the perspective of the server. + * This densely-packed information outlines most of the specifics required to depict some other player's character. * Someone else decides how that character is behaving and the server tells each client how to depict that behavior. * For that reason, the character is mostly for presentation purposes, rather than really being fleshed-out. - * (As far as the client is concerned, nothing stops this character from being declared an "avatar." - * A player would find such a client-controlled character lacking many important details and have poor equipment. - * They would also be competing with some other player for input control, if they could control the character at all.)
+ * Of the inventory for this character, only the initial five weapon slots are defined.
*
- * Divisions exist to make the data more manageable. - * The first division of data only manages the general appearance of the player's in-game model. - * The second division (currently, the fields actually in this class) manages the status of the character. - * In general, it passes more simplified data about the character, the minimum that is necessary to explain status to some other player. - * For example, health and armor are percentages, and are depicted as bars over the player's head near the nameplate. - * The third is the inventory (composed of normal-type objects). - * Rather than equipment other players would never interact with, it only comprises the contents of the five holster slots.
- *
- * If this player is spawned as dead - with their `health` at 0% - he will start standing and then immediately fall into a lying pose. - * The death pose selected is randomized, can not be influenced, and is not be shared across clients. - * @param appearance the player's cardinal appearance settings + * In the "backend of the client," the character produced by this data is no different + * from the kind of character that could be declared a given player's avatar. + * In terms of equipment and complicated features common to an avatar character, however, + * any user would find this character ill-equipped. * @param health the amount of health the player has, as a percentage of a filled bar; * the bar has 85 states, with 3 points for each state; - * when 0% (less than 3 of 255), the player will collapse into a death pose on the ground + * when 0% (less than 3 of 255), the player will collapse into a death pose on the ground; + * while `is_corpse == true`, `health` will always report as 0; + * while `is_seated == true`, `health` will (try to) report as 100 * @param armor the amount of armor the player has, as a percentage of a filled bar; - * the bar has 85 states, with 3 points for each state + * the bar has 85 states, with 3 points for each state; + * while `is_seated == true`, `armor` will always report as 0 * @param uniform_upgrade the level of upgrade to apply to the player's base uniform * @param command_rank the player's command rank as a number from 0 to 5; * cosmetic armor associated with the command rank will be applied automatically @@ -76,89 +69,100 @@ object UniformStyle extends Enumeration { * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands; * they become available at battle rank 24, but here they require the third uniform upgrade (rank 25); * these flags do not exist if they are not applicable - * @param inventory the avatar's inventory; - * typically, only the tools and weapons in the equipment holster slots - * @param drawn_slot the holster that is initially drawn; - * defaults to `DrawnSlot.None` - * @see `CharacterAppearanceData` - * @see `DetailedCharacterData` - * @see `InventoryData` - * @see `DrawnSlot` + * @param is_backpack this player character should be depicted as a corpse; + * corpses are either coffins (defunct), backpacks (normal), or a pastry (festive); + * the alternate model bit should be flipped + * @param is_seated this player character is seated in a vehicle or mounted to some other object; + * alternate format for data parsing applies + * @see `DetailedCharacterData`
+ * `CharacterAppearanceData` */ -final case class CharacterData(appearance : CharacterAppearanceData, - health : Int, +final case class CharacterData(health : Int, armor : Int, uniform_upgrade : UniformStyle.Value, + unk : Int, command_rank : Int, implant_effects : Option[ImplantEffects.Value], - cosmetics : Option[Cosmetics], - inventory : Option[InventoryData], - drawn_slot : DrawnSlot.Value = DrawnSlot.None - ) extends ConstructorData { + cosmetics : Option[Cosmetics]) + (is_backpack : Boolean, + is_seated : Boolean) extends ConstructorData { override def bitsize : Long = { //factor guard bool values into the base size, not its corresponding optional field - val appearanceSize : Long = appearance.bitsize + val seatedSize = if(is_seated) { 0 } else { 16 } val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L } val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L } - val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L } - 32L + appearanceSize + effectsSize + cosmeticsSize + inventorySize + 11L + seatedSize + effectsSize + cosmeticsSize } } object CharacterData extends Marshallable[CharacterData] { /** * An overloaded constructor for `CharacterData` that allows for a not-optional inventory. - * @param appearance the player's cardinal appearance settings * @param health the amount of health the player has, as a percentage of a filled bar * @param armor the amount of armor the player has, as a percentage of a filled bar * @param uniform the level of upgrade to apply to the player's base uniform * @param cr the player's command rank as a number from 0 to 5 * @param implant_effects the effects of implants that can be seen on a player's character * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands - * @param inv the avatar's inventory - * @param drawn_slot the holster that is initially drawn * @return a `CharacterData` object */ - def apply(appearance : CharacterAppearanceData, health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics], inv : InventoryData, drawn_slot : DrawnSlot.Value) : CharacterData = - new CharacterData(appearance, health, armor, uniform, cr, implant_effects, cosmetics, Some(inv), drawn_slot) + def apply(health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics]) : (Boolean,Boolean)=>CharacterData = + CharacterData(health, armor, uniform, 0, cr, implant_effects, cosmetics) - implicit val codec : Codec[CharacterData] = ( - ("app" | CharacterAppearanceData.codec) :: - ("health" | uint8L) :: //dead state when health == 0 + def codec(is_backpack : Boolean) : Codec[CharacterData] = ( + ("health" | uint8L) :: //dead state when health == 0 ("armor" | uint8L) :: (("uniform_upgrade" | UniformStyle.codec) >>:~ { style => ignore(3) :: //unknown ("command_rank" | uintL(3)) :: - bool :: //stream misalignment when != 1 + bool :: //misalignment when == 1 optional(bool, "implant_effects" | ImplantEffects.codec) :: - conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) :: - optional(bool, "inventory" | InventoryData.codec) :: - ("drawn_slot" | DrawnSlot.codec) :: - bool //usually false + conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) }) - ).exmap[CharacterData] ( + ).exmap[CharacterData] ( { - case app :: health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: inv :: drawn_slot :: false :: HNil => - var newHealth = health - if(app.backpack) { - newHealth = 0 - } - Attempt.Successful(CharacterData(app, newHealth, armor, uniform, cr, implant_effects, cosmetics, inv, drawn_slot)) + case health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil => + val newHealth = if(is_backpack) { 0 } else { health } + Attempt.Successful(CharacterData(newHealth, armor, uniform, 0, cr, implant_effects, cosmetics)(is_backpack, false)) case _ => Attempt.Failure(Err("invalid character data; can not encode")) }, { - case CharacterData(app, health, armor, uniform, cr, implant_effects, cosmetics, inv, drawn_slot) => - var newHealth = health - if(app.backpack) { - newHealth = 0 - } - Attempt.Successful(app :: newHealth :: armor :: uniform :: () :: cr :: false :: implant_effects :: cosmetics :: inv :: drawn_slot :: false :: HNil) + case CharacterData(health, armor, uniform, _, cr, implant_effects, cosmetics) => + val newHealth = if(is_backpack) { 0 } else { health } + Attempt.Successful(newHealth :: armor :: uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil) case _ => Attempt.Failure(Err("invalid character data; can not decode")) } ) + + def codec_seated(is_backpack : Boolean) : Codec[CharacterData] = ( + ("uniform_upgrade" | UniformStyle.codec) >>:~ { style => + ignore(3) :: //unknown + ("command_rank" | uintL(3)) :: + bool :: //stream misalignment when != 1 + optional(bool, "implant_effects" | ImplantEffects.codec) :: + conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) + } + ).exmap[CharacterData] ( + { + case uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil => + Attempt.Successful(new CharacterData(100, 0, uniform, 0, cr, implant_effects, cosmetics)(is_backpack, true)) + + case _ => + Attempt.Failure(Err("invalid character data; can not encode")) + }, + { + case obj @ CharacterData(_, _, uniform, _, cr, implant_effects, cosmetics) => + Attempt.Successful(uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil) + + case _ => + Attempt.Failure(Err("invalid character data; can not decode")) + } + ) + + implicit val codec : Codec[CharacterData] = codec(false) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala index c2fa2d747..fa7f7f933 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala @@ -27,22 +27,14 @@ final case class ImplantEntry(implant : ImplantType.Value, } /** - * A representation of the avatar portion of `ObjectCreateDetailedMessage` packet data. - * This densely-packed information outlines most of the specifics required to depict a character as an avatar.
+ * A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.
*
- * As an avatar, the character created by this data is expected to be controllable by the client that gets sent this data. - * It goes into depth about information related to the given character in-game career that is not revealed to other players.
- *
- * Divisions exist to make the data more manageable. - * The first division of data only manages the general appearance of the player's in-game model. - * It is shared between `DetailedCharacterData` avatars and `CharacterData` player characters. - * The second division (of fields) manages the status of the character as an avatar. - * In general, it passes more thorough data about the character that the client can display to the owner of the client. + * This densely-packed information outlines most of the specifics required to depict a character as an avatar. + * It goes into depth about information related to the given character in-game career that is not revealed to other players. + * To be specific, it passes more thorough data about the character that the client can display to the owner of the client. * For example, health is a full number, rather than a percentage. * Just as prominent is the list of first time events and the list of completed tutorials. - * The third subdivision is also exclusive to avatar-prepared characters and contains (omitted). - * The fourth is the inventory (composed of `Direct`-type objects). - * @param appearance data about the avatar's basic aesthetics + * Additionally, a full inventory, as opposed to the initial five weapon slots. * @param bep the avatar's battle experience points, which determines his Battle Rank * @param cep the avatar's command experience points, which determines his Command Rank * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value; @@ -73,16 +65,10 @@ final case class ImplantEntry(implant : ImplantType.Value, * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands; * they become available at battle rank 24; * these flags do not exist if they are not applicable - * @param inventory the avatar's inventory - * @param drawn_slot the holster that is initially drawn - * @see `CharacterAppearanceData`
- * `CharacterData`
- * `CertificationType`
- * `InventoryData`
- * `DrawnSlot` + * @see `CharacterData`
+ * `CertificationType` */ -final case class DetailedCharacterData(appearance : CharacterAppearanceData, - bep : Long, +final case class DetailedCharacterData(bep : Long, cep : Long, healthMax : Int, health : Int, @@ -96,20 +82,17 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData, implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], - cosmetics : Option[Cosmetics], - inventory : Option[InventoryData], - drawn_slot : DrawnSlot.Value = DrawnSlot.None - ) extends ConstructorData { + cosmetics : Option[Cosmetics]) + (pad_length : Option[Int]) extends ConstructorData { override def bitsize : Long = { //factor guard bool values into the base size, not corresponding optional fields, unless contained or enumerated - val appearanceSize = appearance.bitsize val certSize = (certs.length + 1) * 8 //cert list var implantSize : Long = 0L //implant list for(entry <- implants) { implantSize += entry.bitsize } - val implantPadding = DetailedCharacterData.implantFieldPadding(implants, CharacterAppearanceData.altModelBit(appearance)) + val implantPadding = DetailedCharacterData.implantFieldPadding(implants, pad_length) val fteLen = firstTimeEvents.size //fte list var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding) for(str <- firstTimeEvents) { @@ -123,20 +106,13 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData, val br24 = DetailedCharacterData.isBR24(bep) //character is at least BR24 val extraBitSize : Long = if(br24) { 33L } else { 46L } val cosmeticsSize : Long = if(br24) { cosmetics.get.bitsize } else { 0L } - val inventorySize : Long = if(inventory.isDefined) { //inventory - inventory.get.bitsize - } - else { - 0L - } - 603L + appearanceSize + certSize + implantSize + eventListSize + extraBitSize + cosmeticsSize + tutorialListSize + inventorySize + 598L + certSize + implantSize + eventListSize + extraBitSize + cosmeticsSize + tutorialListSize } } object DetailedCharacterData extends Marshallable[DetailedCharacterData] { /** * Overloaded constructor for `DetailedCharacterData` that requires an inventory and drops unknown values. - * @param appearance data about the avatar's basic aesthetics * @param bep the avatar's battle experience points, which determines his Battle Rank * @param cep the avatar's command experience points, which determines his Command Rank * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value @@ -148,12 +124,10 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { * @param implants the `List` of implant slots currently possessed by this avatar * @param firstTimeEvents the list of first time events performed by this avatar * @param tutorials the list of tutorials completed by this avatar - * @param inventory the avatar's inventory - * @param drawn_slot the holster that is initially drawn * @return a `DetailedCharacterData` object */ - def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], cosmetics : Option[Cosmetics], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData = - new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics, Some(inventory), drawn_slot) + def apply(bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], cosmetics : Option[Cosmetics]) : (Option[Int])=>DetailedCharacterData = + DetailedCharacterData(bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics) /** * `Codec` for entries in the `List` of implants. @@ -278,57 +252,52 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { def isBR24(bep : Long) : Boolean = bep > 2286230 - implicit val codec : Codec[DetailedCharacterData] = ( - ("appearance" | CharacterAppearanceData.codec) >>:~ { app => - ("bep" | uint32L) >>:~ { bep => - ("cep" | uint32L) :: - ignore(96) :: - ("healthMax" | uint16L) :: - ("health" | uint16L) :: - ignore(1) :: - ("armor" | uint16L) :: - ignore(9) :: - ("unk1" | uint8L) :: - ignore(8) :: - ("unk2" | uint4L) :: - ("unk3" | uintL(3)) :: - ("staminaMax" | uint16L) :: - ("stamina" | uint16L) :: - ignore(147) :: - ("certs" | listOfN(uint8L, CertificationType.codec)) :: - optional(bool, uint32L) :: //ask about sample CCRIDER - ignore(4) :: - (("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants => - ignore(12) :: - (("firstTimeEvent_length" | uint32L) >>:~ { len => - conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) :: - ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: - (("tutorial_length" | uint32L) >>:~ { len2 => - conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) :: - ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: - ignore(160) :: - (bool >>:~ { br24 => //BR24+ - newcodecs.binary_choice(br24, ignore(33), ignore(46)) :: - conditional(br24, Cosmetics.codec) :: - optional(bool, "inventory" | InventoryData.codec_detailed) :: - ("drawn_slot" | DrawnSlot.codec) :: - bool //usually false - }) - }) - }) - }) - } + def codec(pad_length : Option[Int]) : Codec[DetailedCharacterData] = ( + ("bep" | uint32L) >>:~ { bep => + ("cep" | uint32L) :: + ignore(96) :: + ("healthMax" | uint16L) :: + ("health" | uint16L) :: + ignore(1) :: + ("armor" | uint16L) :: + ignore(9) :: + ("unk1" | uint8L) :: + ignore(8) :: + ("unk2" | uint4L) :: + ("unk3" | uintL(3)) :: + ("staminaMax" | uint16L) :: + ("stamina" | uint16L) :: + ignore(147) :: + ("certs" | listOfN(uint8L, CertificationType.codec)) :: + optional(bool, uint32L) :: //ask about sample CCRIDER + ignore(4) :: + (("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants => + ignore(12) :: + (("firstTimeEvent_length" | uint32L) >>:~ { len => + conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, pad_length)))) :: + ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: + (("tutorial_length" | uint32L) >>:~ { len2 => + conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, pad_length)))) :: + ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: + ignore(160) :: + (bool >>:~ { br24 => //BR24+ + newcodecs.binary_choice(br24, ignore(33), ignore(46)) :: + conditional(br24, Cosmetics.codec) + }) + }) + }) + }) } ).exmap[DetailedCharacterData] ( { - case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: inv :: drawn :: false :: HNil => + case bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: HNil => //prepend the displaced first elements to their lists val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else { fte1 } val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else { tut1 } - Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cosmetics, inv, drawn)) + Attempt.successful(DetailedCharacterData(bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cosmetics)(pad_length)) }, { - case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cos, inv, drawn) => + case DetailedCharacterData(bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cos) => val implantCapacity : Int = numberOfImplantSlots(bep) val implantList = if(implants.length > implantCapacity) { implants.slice(0, implantCapacity) @@ -349,7 +318,9 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] { } val br24 : Boolean = isBR24(bep) val cosmetics : Option[Cosmetics] = if(br24) { cos } else { None } - Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: br24 :: () :: cosmetics :: inv :: drawn :: false :: HNil) + Attempt.successful(bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: br24 :: () :: cosmetics :: HNil) } ) + + implicit val codec : Codec[DetailedCharacterData] = codec(None) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedPlayerData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedPlayerData.scala new file mode 100644 index 000000000..20b6f3e49 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedPlayerData.scala @@ -0,0 +1,133 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.codecs._ +import scodec.Codec +import shapeless.{::, HNil} + +/** + * A representation of an `avatar` player for the `ObjectCreateDetailedMessage` packet. + * As an avatar, the character created by this data is expected to be controllable by the client that gets sent this data.
+ *
+ * Divisions exist to make the data more manageable. + * The first division defines the player's location within the game coordinate system. + * The second division defines features of the `avatar` + * that are shared by both the `ObjectCreateDetailedMessage` version of a controlled player character (this) + * and the `ObjectCreateMessage` version of a player character. + * The third field expands on the nature of the character and this avatar's campaign. + * Expansive information about previous interactions, the contents of their inventory, and equipment permissions are included.
+ *
+ * The presence or absence of position data as the first division creates a cascading effect + * causing all of fields in the other two divisions to gain offsets. + * These offsets exist in the form of `String` and `List` padding. + * @see `DetailedCharacterData`
+ * `InventoryData`
+ * `DrawnSlot` + * @param pos the optional position of the character in the world environment + * @param basic_appearance common fields regarding the the character's appearance + * @param character_data the class-specific data that explains about the character + * @param position_defined used by the `Codec` to seed the state of the optional `pos` field + * @param inventory the player's full inventory + * @param drawn_slot the holster that is initially drawn + */ +final case class DetailedPlayerData(pos : Option[PlacementData], + basic_appearance : CharacterAppearanceData, + character_data : DetailedCharacterData, + inventory : Option[InventoryData], + drawn_slot : DrawnSlot.Value) + (position_defined : Boolean) extends ConstructorData { + override def bitsize : Long = { + //factor guard bool values into the base size, not its corresponding optional field + val posSize : Long = if(pos.isDefined) { pos.get.bitsize } else { 0L } + val appSize : Long = basic_appearance.bitsize + val charSize = character_data.bitsize + val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L } + 5L + posSize + appSize + charSize + inventorySize + } +} + +object DetailedPlayerData extends Marshallable[DetailedPlayerData] { + /** + * Overloaded constructor that ignores the coordinate information but includes the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param inventory the player's inventory + * @param drawn_slot the holster that is initially drawn; + * technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity + * @return a `DetailedPlayerData` object + */ + def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = { + val appearance = basic_appearance(5) + DetailedPlayerData(None, appearance, character_data(appearance.altModelBit), Some(inventory), drawn_slot)(false) + } + + /** + * Overloaded constructor that ignores the coordinate information and the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param drawn_slot the holster that is initially drawn; + * technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity + * @return a `DetailedPlayerData` object + */ + def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = { + val appearance = basic_appearance(5) + DetailedPlayerData(None, appearance, character_data(appearance.altModelBit), None, drawn_slot)(false) + } + + /** + * Overloaded constructor that includes the coordinate information and the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are standing apart from other containers. + * @param pos the optional position of the character in the world environment + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param inventory the player's inventory + * @param drawn_slot the holster that is initially drawn + * @return a `DetailedPlayerData` object + */ + def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = { + val appearance = basic_appearance(PlayerData.PaddingOffset(Some(pos))) + DetailedPlayerData(Some(pos), appearance, character_data(appearance.altModelBit), Some(inventory), drawn_slot)(true) + } + + /** + * Overloaded constructor that includes the coordinate information but ignores the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are standing apart from other containers. + * @param pos the optional position of the character in the world environment + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param drawn_slot the holster that is initially drawn + * @return a `DetailedPlayerData` object + */ + def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = { + val appearance = basic_appearance(PlayerData.PaddingOffset(Some(pos))) + DetailedPlayerData(Some(pos), appearance, character_data(appearance.altModelBit), None, drawn_slot)(true) + } + + def codec(position_defined : Boolean) : Codec[DetailedPlayerData] = ( + conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos => + ("basic_appearance" | CharacterAppearanceData.codec(PlayerData.PaddingOffset(pos))) >>:~ { app => + ("character_data" | DetailedCharacterData.codec(app.altModelBit)) :: + optional(bool, "inventory" | InventoryData.codec_detailed) :: + ("drawn_slot" | DrawnSlot.codec) :: + bool //usually false + } + }).xmap[DetailedPlayerData] ( + { + case pos :: app :: data :: inv :: hand :: _ :: HNil => + DetailedPlayerData(pos, app, data, inv, hand)(pos.isDefined) + }, + { + case DetailedPlayerData(pos, app, data, inv, hand) => + pos :: app :: data :: inv :: hand :: false :: HNil + } + ) + + implicit val codec : Codec[DetailedPlayerData] = codec(false) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 40a95b499..281360a94 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -666,13 +666,21 @@ object ObjectClass { case ObjectClass.advanced_ace => ConstructorData.genericCodec(DetailedACEData.codec, "advanced ace") case ObjectClass.boomer_trigger => ConstructorData.genericCodec(DetailedBoomerTriggerData.codec, "boomer trigger") //other - case ObjectClass.avatar => ConstructorData.genericCodec(DetailedCharacterData.codec, "avatar") + case ObjectClass.avatar => ConstructorData.genericCodec(DetailedPlayerData.codec(false), "avatar") case ObjectClass.locker_container => ConstructorData.genericCodec(DetailedLockerContainerData.codec, "locker container") //failure case case _ => defaultFailureCodec(objClass) } + def selectDataDroppedDetailedCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = + (objClass : @switch) match { + //special cases + case ObjectClass.avatar => ConstructorData.genericCodec(DetailedPlayerData.codec(true), "avatar") + //defer to other codec selection + case _ => selectDataDetailedCodec(objClass) + } + /** * Given an object class, retrieve the `Codec` used to parse and translate the constructor data for that type. * This function services `0x17` `ObjectCreateMessage` packet data.
@@ -953,6 +961,7 @@ object ObjectClass { //other case ObjectClass.ams_order_terminal => ConstructorData.genericCodec(CommonTerminalData.codec, "terminal") case ObjectClass.ams_respawn_tube => ConstructorData.genericCodec(CommonTerminalData.codec, "terminal") + case ObjectClass.avatar => ConstructorData.genericCodec(PlayerData.codec(false), "avatar") case ObjectClass.bfr_rearm_terminal => ConstructorData.genericCodec(CommonTerminalData.codec, "terminal") case ObjectClass.implant_terminal_interface => ConstructorData.genericCodec(CommonTerminalData.codec, "implant terminal") case ObjectClass.lodestar_repair_terminal => ConstructorData.genericCodec(CommonTerminalData.codec, "terminal") @@ -1268,7 +1277,7 @@ object ObjectClass { case ObjectClass.wasp => ConstructorData.genericCodec(VehicleData.codec(VehicleFormat.Variant), "vehicle") //other case ObjectClass.ams_respawn_tube => DroppedItemData.genericCodec(CommonTerminalData.codec, "terminal") - case ObjectClass.avatar => ConstructorData.genericCodec(CharacterData.codec, "avatar") + case ObjectClass.avatar => ConstructorData.genericCodec(PlayerData.codec(true), "avatar") case ObjectClass.capture_flag => ConstructorData.genericCodec(CaptureFlagData.codec, "capture flag") case ObjectClass.implant_terminal_interface => ConstructorData.genericCodec(CommonTerminalData.codec, "implant terminal") case ObjectClass.locker_container => ConstructorData.genericCodec(LockerContainerData.codec, "locker container") diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala new file mode 100644 index 000000000..7452d1be1 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala @@ -0,0 +1,213 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game.objectcreate + +import net.psforever.newcodecs._ +import net.psforever.packet.Marshallable +import scodec.codecs._ +import scodec.Codec +import shapeless.{::, HNil} + +/** + * A representation of another player's character for the `ObjectCreateMessage` packet. + * In general, this packet is used to describe other players.
+ *
+ * Divisions exist to make the data more manageable. + * The first division defines the player's location within the game coordinate system. + * The second division defines features of the character + * that are shared by both the `ObjectCreateDetailedMessage` version of a controlled player character + * and the `ObjectCreateMessage` version of a player character (this). + * The third field provides further information on the appearance of the player character, albeit condensed. + * The fourth field involves the player's `Equipment` holsters and their inventory. + * The hand that the player has exposed is last. + * One of the most compact forms of a player character description is transcribed using this information.
+ *
+ * The presence or absence of position data as the first division creates a cascading effect + * causing all of fields in the other two divisions to gain offset values. + * These offsets exist in the form of `String` and `List` padding. + * @see `CharacterData`
+ * `InventoryData`
+ * `DrawnSlot` + * @param pos the optional position of the character in the world environment + * @param basic_appearance common fields regarding the the character's appearance + * @param character_data the class-specific data that explains about the character + * @param inventory the player's inventory; + * typically, only the tools and weapons in the equipment holster slots + * @param drawn_slot the holster that is initially drawn + * @param position_defined used by the `Codec` to seed the state of the optional `pos` field + */ +final case class PlayerData(pos : Option[PlacementData], + basic_appearance : CharacterAppearanceData, + character_data : CharacterData, + inventory : Option[InventoryData], + drawn_slot : DrawnSlot.Value) + (position_defined : Boolean) extends ConstructorData { + override def bitsize : Long = { + //factor guard bool values into the base size, not its corresponding optional field + val posSize : Long = if(pos.isDefined) { pos.get.bitsize } else { 0L } + val appSize : Long = basic_appearance.bitsize + val charSize = character_data.bitsize + val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L } + 5L + posSize + appSize + charSize + inventorySize + } +} + +object PlayerData extends Marshallable[PlayerData] { + /** + * Overloaded constructor that ignores the coordinate information but includes the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param inventory the player's inventory + * @param drawn_slot the holster that is initially drawn; + * technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity + * @return a `PlayerData` object + */ + def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = { + val appearance = basic_appearance(5) + PlayerData(None, appearance, character_data(appearance.backpack, true), Some(inventory), drawn_slot)(false) + } + /** + * Overloaded constructor that ignores the coordinate information and the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param drawn_slot the holster that is initially drawn; + * technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity + * @return a `PlayerData` object + */ + def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type) : PlayerData = { + val appearance = basic_appearance(5) + PlayerData(None, appearance, character_data(appearance.backpack, true), None, drawn_slot)(false) + } + + /** + * Overloaded constructor that includes the coordinate information and the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are standing apart from other containers. + * @param pos the optional position of the character in the world environment + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param inventory the player's inventory + * @param drawn_slot the holster that is initially drawn + * @return a `PlayerData` object + */ + def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = { + val appearance = basic_appearance( PaddingOffset(Some(pos)) ) + PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), Some(inventory), drawn_slot)(true) + } + /** + * Overloaded constructor that includes the coordinate information but ignores the inventory. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are standing apart from other containers. + * @param pos the optional position of the character in the world environment + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param drawn_slot the holster that is initially drawn + * @return a `PlayerData` object + */ + def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type) : PlayerData = { + val appearance = basic_appearance( PaddingOffset(Some(pos)) ) + PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), None, drawn_slot)(true) + } + + /** + * Determine the padding offset for a subsequent field given the existence of `PlacementData`. + * With the `PlacementData` objects, a question of the optional velocity field also exists.
+ *
+ * With just `PlacementData`, the bit distance to the name field is 164 (padding: 4 bits). + * With `PlacementData` with velocity, the bit distance to the name field is 206 (padding: 2 bits). + * Without `PlacementData`, the distance to the name field is either 107 or 115 (padding: 5 bits). + * The padding will always be a number 0-7. + * @see `PlacementData` + * @param pos the optional `PlacementData` object that creates the shift in bits + * @return the pad length in bits + */ + def PaddingOffset(pos : Option[PlacementData]) : Int = { + /* + The `ObjectCreateMessage` length is either 32 + 12 + 16 + 81 - 141 - with `PlacementData`, + with an additional +42 - 183 - with the optional velocity field, + or 32 + 12 + 16 + 16 + 8/16 - 84/92 - without any `PlacementData`. + 23 is the distance of all the fields before the player's `name` field in `CharacterAppearanceData`. + */ + pos match { + case Some(place) => + if(place.vel.isDefined) { 2 } else { 4 } + case None => + 5 //with ObjectCreateMessageParent data + } + } + + /** + * Find the number of trailing bits that need to be added to make the current value perfectly divisible by eight. + * @param length the current length of a stream + * @return the number of bits needed to pad it + */ + def ByteAlignmentPadding(length : Long) : Int = { + val pad = (length - math.floor(length / 8) * 8).toInt + if(pad > 0) { + 8 - pad + } + else { + 0 + } + } + + /** + * This `Codec` is generic. + * However, it should not be used to translate a `Player` object + * in the middle of translating that `Player`'s mounting object. + * The offset value is calculated internally. + * @param position_defined this entry has `PlacementData` that defines position, orientation, and, optionally, motion + * @return a `Codec` that translates a `PlayerData` object + */ + def codec(position_defined : Boolean) : Codec[PlayerData] = ( + conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos => + ("basic_appearance" | CharacterAppearanceData.codec(PaddingOffset(pos))) >>:~ { app => + ("character_data" | newcodecs.binary_choice(position_defined, + CharacterData.codec(app.backpack), + CharacterData.codec_seated(app.backpack))) :: + optional(bool, "inventory" | InventoryData.codec) :: + ("drawn_slot" | DrawnSlot.codec) :: + bool //usually false + } + }).xmap[PlayerData] ( + { + case pos :: app :: data :: inv :: hand :: _ :: HNil => + PlayerData(pos, app, data, inv, hand)(pos.isDefined) + }, + { + case PlayerData(pos, app, data, inv, hand) => + pos :: app :: data :: inv :: hand :: false :: HNil + } + ) + + /** + * This `Codec` is exclusively for translating a `Player` object + * while that `Player` object is encountered in the process of translating its mounting object. + * In other words, the player is "seated" or "mounted." + * @see `CharacterAppearanceData.codec` + * @param offset the padding for the player's name field + * @return a `Codec` that translates a `PlayerData` object + */ + def codec(offset : Int) : Codec[PlayerData] = ( + ("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app => + ("character_data" | CharacterData.codec_seated(app.backpack)) :: + optional(bool, "inventory" | InventoryData.codec) :: + ("drawn_slot" | DrawnSlot.codec) :: + bool //usually false + } + ).xmap[PlayerData] ( + { + case app :: data :: inv :: hand :: _ :: HNil => + PlayerData(None, app, data, inv, hand)(false) + }, + { + case PlayerData(None, app, data, inv, hand) => + app :: data :: inv :: hand :: false :: HNil + } + ) + + implicit val codec : Codec[PlayerData] = codec(false) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/Prefab.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/Prefab.scala index acfaa4079..a767591d6 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/Prefab.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/Prefab.scala @@ -13,22 +13,23 @@ import net.psforever.types.{DriveState, PlanetSideEmpire} object Prefab { object Vehicle { def ams(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, driveState : DriveState.Value, matrix_guid : PlanetSideGUID, respawn_guid : PlanetSideGUID, term_a_guid : PlanetSideGUID, term_b_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, driveState, false, false, false, Some(UtilityVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 0), health, driveState, false, UtilityVehicleData(0), Some(InventoryData(List( InternalSlot(ObjectClass.matrix_terminalc, matrix_guid, 1, CommonTerminalData(faction)), InternalSlot(ObjectClass.ams_respawn_tube, respawn_guid, 2, CommonTerminalData(faction)), InternalSlot(ObjectClass.order_terminala, term_a_guid, 3, CommonTerminalData(faction)), InternalSlot(ObjectClass.order_terminalb, term_b_guid, 4, CommonTerminalData(faction)) ))) - )(VehicleFormat.Utility) + ) } def ant(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, driveState : DriveState.Value) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, driveState, false, false, false, Some(UtilityVehicleData(0)), None)(VehicleFormat.Utility) + VehicleData(CommonFieldData(loc, faction, 0), health, driveState, false, UtilityVehicleData(0), None) } def apc_nc(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID, weapon4_guid : PlanetSideGUID, ammo4_guid : PlanetSideGUID, weapon5_guid : PlanetSideGUID, ammo5_guid : PlanetSideGUID, weapon6_guid : PlanetSideGUID, ammo6_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.apc_weapon_systemc_nc, weapon1_guid, 11, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_20mm, ammo1_guid, 0, AmmoBoxData(8)) @@ -49,11 +50,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_12mm, ammo6_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def apc_tr(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID, weapon4_guid : PlanetSideGUID, ammo4_guid : PlanetSideGUID, weapon5_guid : PlanetSideGUID, ammo5_guid : PlanetSideGUID, weapon6_guid : PlanetSideGUID, ammo6_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.apc_weapon_systemc_tr, weapon1_guid, 11, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_15mm, ammo1_guid, 0, AmmoBoxData(8)) @@ -74,11 +76,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_12mm, ammo6_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def apc_vs(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID, weapon4_guid : PlanetSideGUID, ammo4_guid : PlanetSideGUID, weapon5_guid : PlanetSideGUID, ammo5_guid : PlanetSideGUID, weapon6_guid : PlanetSideGUID, ammo6_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.apc_weapon_systemc_vs, weapon1_guid, 11, WeaponData(0x6, 0x8, 0, ObjectClass.flux_cannon_thresher_battery, ammo1_guid, 0, AmmoBoxData(8)) @@ -99,12 +102,13 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_12mm, ammo6_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def aurora(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo11_guid : PlanetSideGUID, ammo12_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo21_guid : PlanetSideGUID, ammo22_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, - Some(InventoryData( + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, + Some(InventoryData( InventoryItemData(ObjectClass.aurora_weapon_systema, weapon1_guid, 5, WeaponData(0x6, 0x8, 0, ObjectClass.fluxpod_ammo, ammo11_guid, 0, AmmoBoxData(0x8)) ) :: @@ -112,11 +116,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.fluxpod_ammo, ammo21_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def battlewagon(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID, weapon4_guid : PlanetSideGUID, ammo4_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.battlewagon_weapon_systema, weapon1_guid, 5, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_15mm, ammo1_guid, 0, AmmoBoxData(0x8)) @@ -131,11 +136,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_15mm, ammo4_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def dropship(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.cannon_dropship_20mm, weapon1_guid, 12, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_20mm, ammo1_guid, 0, AmmoBoxData(8)) @@ -147,32 +153,35 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_20mm, ammo3_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def flail(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID, terminal_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.Mobile, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.flail_weapon, weapon_guid, 1, WeaponData(0x6, 0x8, 0, ObjectClass.ancient_ammo_vehicle, ammo_guid, 0, AmmoBoxData(8)) ) :: InventoryItemData(ObjectClass.targeting_laser_dispenser, terminal_guid, 2, CommonTerminalData(faction, 2)) :: Nil )) - )(VehicleFormat.Variant) + ) } def fury(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.fury_weapon_systema, weapon_guid, 1, WeaponData(0x4, 0x8, ObjectClass.hellfire_ammo, ammo_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def galaxy_gunship(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID, weapon4_guid : PlanetSideGUID, ammo4_guid : PlanetSideGUID, weapon5_guid : PlanetSideGUID, ammo5_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.galaxy_gunship_cannon, weapon1_guid, 6, WeaponData(0x6, 0x8, 0, ObjectClass.heavy_grenade_mortar, ammo1_guid, 0, AmmoBoxData(8)) @@ -190,11 +199,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_35mm, ammo5_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def liberator(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo4_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.liberator_weapon_system, weapon1_guid, 3, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_35mm, ammo1_guid, 0, AmmoBoxData(8)) @@ -206,31 +216,34 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_25mm, ammo4_guid, 0 ,AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def lightgunship(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.Mobile, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.lightgunship_weapon_system, weapon_guid, 1, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_20mm, ammo1_guid, 0, AmmoBoxData(8), ObjectClass.reaver_rocket, ammo2_guid,1, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def lightning(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.lightning_weapon_system, weapon_guid, 1, WeaponData(0x4, 0x8, 0, ObjectClass.bullet_75mm, ammo1_guid, 0, AmmoBoxData(0x0), ObjectClass.bullet_12mm, ammo2_guid, 1, AmmoBoxData(0x0)) ) :: Nil) ) - )(VehicleFormat.Normal) + ) } def lodestar(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, repair1_guid : PlanetSideGUID, repair2_guid : PlanetSideGUID, veh_rearm1_guid : PlanetSideGUID, veh_rearm2_guid : PlanetSideGUID, bfr_rearm1_guid : PlanetSideGUID, bfr_rearm2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData(List( InternalSlot(ObjectClass.lodestar_repair_terminal, repair1_guid, 2, CommonTerminalData(faction, 2)), InternalSlot(ObjectClass.lodestar_repair_terminal, repair2_guid, 3, CommonTerminalData(faction, 2)), @@ -239,11 +252,12 @@ object Prefab { InternalSlot(ObjectClass.bfr_rearm_terminal, bfr_rearm1_guid, 6, CommonTerminalData(faction, 2)), InternalSlot(ObjectClass.bfr_rearm_terminal, bfr_rearm2_guid, 7, CommonTerminalData(faction, 2)) ))) - )(VehicleFormat.Variant) + ) } def magrider(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.particle_beam_magrider, weapon1_guid, 2, WeaponData(0x6, 0x8, 0, ObjectClass.pulse_battery, ammo1_guid, 0, AmmoBoxData(8)) @@ -252,11 +266,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.heavy_rail_beam_battery, ammo2_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def mediumtransport(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID): VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.mediumtransport_weapon_systemA, weapon1_guid, 5, WeaponData(0x6, 0x8, ObjectClass.bullet_20mm, ammo1_guid, 0, AmmoBoxData(0x8)) @@ -265,25 +280,28 @@ object Prefab { WeaponData(0x6, 0x8, ObjectClass.bullet_20mm, ammo2_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def mosquito(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.rotarychaingun_mosquito, weapon_guid, 1, WeaponData(0x6, 0x8, ObjectClass.bullet_12mm, ammo_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def phantasm(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), None)(VehicleFormat.Variant) + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), None)(VehicleFormat.Variant) + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, VariantVehicleData(0), None) } def prowler(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.prowler_weapon_systemA, weapon1_guid, 3, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_105mm, ammo1_guid, 0, AmmoBoxData(8)) @@ -292,53 +310,59 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_15mm, ammo2_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def quadassault(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.quadassault_weapon_system, weapon_guid, 1, WeaponData(0x6, 0x8, ObjectClass.bullet_12mm, ammo_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def quadstealth(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, false, false, false, None, None)(VehicleFormat.Normal) + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, false, false, false, None, None)(VehicleFormat.Normal) + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, None) } def router(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, terminal_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.Mobile, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.teleportpad_terminal, terminal_guid, 1, CommonTerminalData(faction, 2)) :: Nil )) - )(VehicleFormat.Variant) + ) } def skyguard(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.skyguard_weapon_system, weapon_guid, 2, WeaponData(0x6, 0x8, 0, ObjectClass.skyguard_flak_cannon_ammo, ammo1_guid, 0, AmmoBoxData(8), ObjectClass.bullet_12mm, ammo2_guid, 1, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def switchblade(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, driveState : DriveState.Value, weapon_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.scythe, weapon_guid, 1, WeaponData(0x6, 0x8, 0, ObjectClass.ancient_ammo_vehicle, ammo1_guid, 0, AmmoBoxData(0x8), ObjectClass.ancient_ammo_vehicle, ammo2_guid, 1, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def threemanheavybuggy(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.chaingun_p, weapon1_guid, 3, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_12mm, ammo1_guid, 0, AmmoBoxData(0x8)) @@ -347,11 +371,12 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.heavy_grenade_mortar, ammo2_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def thunderer(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.thunderer_weapon_systema, weapon1_guid, 5, WeaponData(0x6, 0x8, 0, ObjectClass.gauss_cannon_ammo, ammo1_guid, 0, AmmoBoxData(0x8)) @@ -360,51 +385,56 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.gauss_cannon_ammo, ammo2_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def two_man_assault_buggy(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.chaingun_p, weapon_guid, 2, WeaponData(0x6, 0x8, ObjectClass.bullet_12mm, ammo_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def twomanheavybuggy(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.advanced_missile_launcher_t, weapon_guid, 2, WeaponData(0x6, 0x8, 0, ObjectClass.firebird_missile, ammo_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def twomanhoverbuggy(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.flux_cannon_thresher, weapon_guid, 2, WeaponData(0x6, 0x8, 0, ObjectClass.flux_cannon_thresher_battery, ammo_guid, 0, AmmoBoxData(0x8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def vanguard(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, None, + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, Some(InventoryData( InventoryItemData(ObjectClass.vanguard_weapon_system, weapon_guid, 2, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_150mm, ammo1_guid, 0, AmmoBoxData(8), ObjectClass.bullet_20mm, ammo2_guid, 1, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Normal) + ) } def vulture(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon1_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, weapon2_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID, weapon3_guid : PlanetSideGUID, ammo3_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 2), 0, health, false, false, DriveState.State7, true, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 2), health, DriveState.State7, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.vulture_nose_weapon_system, weapon1_guid, 3, WeaponData(0x6, 0x8, 0, ObjectClass.bullet_35mm, ammo1_guid, 0, AmmoBoxData(8)) @@ -416,17 +446,18 @@ object Prefab { WeaponData(0x6, 0x8, 0, ObjectClass.bullet_25mm, ammo3_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } def wasp(loc : PlacementData, faction : PlanetSideEmpire.Value, health : Int, weapon_guid : PlanetSideGUID, ammo1_guid : PlanetSideGUID, ammo2_guid : PlanetSideGUID) : VehicleData = { - VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + //VehicleData(CommonFieldData(loc, faction, 0), 0, health, false, false, DriveState.Mobile, false, false, false, Some(VariantVehicleData(0)), + VehicleData(CommonFieldData(loc, faction, 0), health, DriveState.Mobile, false, VariantVehicleData(0), Some(InventoryData( InventoryItemData(ObjectClass.wasp_weapon_system, weapon_guid, 1, WeaponData(0x6, 0x8, 0, ObjectClass.wasp_gun_ammo, ammo1_guid, 0, AmmoBoxData(8), ObjectClass.wasp_rocket_ammo, ammo2_guid, 0, AmmoBoxData(8)) ) :: Nil )) - )(VehicleFormat.Variant) + ) } } } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala index 34eac81db..773e9bb14 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala @@ -1,12 +1,15 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game.objectcreate + +import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.{Marshallable, PacketHelpers} import scodec.Attempt.{Failure, Successful} import scodec.{Attempt, Codec, Err} +import shapeless.HNil //note: do not import shapeless.:: here; it messes up List's :: functionality import scodec.codecs._ -import shapeless.{::, HNil} +import net.psforever.types.{DriveState, PlanetSideEmpire} -import net.psforever.types.DriveState +import scala.collection.mutable.ListBuffer /** * An `Enumeration` of the various formats that known structures that the stream of bits for `VehicleData` can assume. @@ -47,100 +50,138 @@ final case class VariantVehicleData(unk : Int) extends SpecificVehicleData { } /** - * A representation of a generic vehicle.
- *
- * Vehicles utilize their own packet to communicate position to the server, known as `VehicleStateMessage`. - * This takes the place of `PlayerStateMessageUpstream` when the player avatar is in control; - * and, it takes the place of `PlayerStateMessage` for other players when they are in control. - * If the vehicle is sufficiently complicated, a `ChildObjectStateMessage` will be used. - * This packet will control any turret(s) on the vehicle. - * For very complicated vehicles, the packets `FrameVehicleStateMessage` and `VehicleSubStateMessage` will also be employed. - * The tasks that these packets perform are different based on the vehicle that responds or generates them. - * @param basic data common to objects - * @param unk1 na - * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) + * A representation of a generic vehicle. + * @param pos where the vehicle is and how it is oriented in the game world + * @param faction the faction that is aligned with this vehicle + * @param bops this vehicle belongs to the Black Ops, regardless of the faction field; + * activates the green camo and adjusts permissions + * @param destroyed this vehicle has ben destroyed; + * it's health should be less than 3/255, or 0% + * @param unk1 na. Valid values seem to be 0-3. Anything higher spawns a completely broken NC vehicle with no guns that can't move + * @param jammered this vehicle is under the influence of a jammer grenade * @param unk2 na + * @param owner_guid the vehicle's (official) owner; + * verified as a living player in the game world on the same continent as the vehicle; + * sitting in the driver's seat or a `PlanetSideAttributeMessage` of type 21 can influence + * @param unk3 na + * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) + * @param unk4 na * @param no_mount_points do not display entry points for the seats * @param driveState a representation for the current mobility state; - * various vehicles also use this field to indicate "deployment," e.g., AMS - * @param unk3 na + * various vehicles also use this field to indicate "deployment," e.g., the advanced mobile spawn * @param unk5 na - * @param cloak if a cloakable vehicle is cloaked - * @param unk4 na + * @param unk6 na + * @param cloak if a vehicle (that can cloak) is cloaked + * @param vehicle_format_data extra information necessary to implement special-type vehicles; + * see `vehicle_type` * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included; - * will also include trunk contents + * will also include trunk contents; + * the driver is the only valid seat entry (more will cause the access permissions to act up) * @param vehicle_type a modifier for parsing the vehicle data format differently; + * see `vehicle_format_data`; * defaults to `Normal` */ -final case class VehicleData(basic : CommonFieldData, +final case class VehicleData(pos : PlacementData, + faction : PlanetSideEmpire.Value, + bops : Boolean, + destroyed : Boolean, unk1 : Int, - health : Int, + jammered : Boolean, unk2 : Boolean, + owner_guid : PlanetSideGUID, + unk3 : Boolean, + health : Int, + unk4 : Boolean, no_mount_points : Boolean, driveState : DriveState.Value, - unk3 : Boolean, unk5 : Boolean, + unk6 : Boolean, cloak : Boolean, - unk4 : Option[SpecificVehicleData], - inventory : Option[InventoryData] = None - )(val vehicle_type : VehicleFormat.Value = VehicleFormat.Normal) extends ConstructorData { + vehicle_format_data : Option[SpecificVehicleData], + inventory : Option[InventoryData] = None) + (val vehicle_type : VehicleFormat.Value = VehicleFormat.Normal) extends ConstructorData { override def bitsize : Long = { - val basicSize = basic.bitsize - val extraBitsSize : Long = if(unk4.isDefined) { unk4.get.bitsize } else { 0L } + //factor guard bool values into the base size, not its corresponding optional field + val posSize : Long = pos.bitsize + val extraBitsSize : Long = if(vehicle_format_data.isDefined) { vehicle_format_data.get.bitsize } else { 0L } val inventorySize = if(inventory.isDefined) { inventory.get.bitsize } else { 0L } - 24L + basicSize + extraBitsSize + inventorySize + 47L + posSize + extraBitsSize + inventorySize } } object VehicleData extends Marshallable[VehicleData] { /** * Overloaded constructor for specifically handling `Normal` vehicle format. - * @param basic data common to objects - * @param unk1 na + * @param basic a field that encompasses some data used by the vehicle, including `faction` and `owner` * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) - * @param unk2 na - * @param driveState a representation for the current mobility state; - * @param unk3 na - * @param unk4 na + * @param driveState a representation for the current mobility state + * @param cloak if a vehicle (that can cloak) is cloaked * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included - * @return a `VehicleData` object */ - def apply(basic : CommonFieldData, unk1 : Int, health : Int, unk2 : Int, driveState : DriveState.Value, unk3 : Boolean, unk4 : Int, inventory : Option[InventoryData]) : VehicleData = { - new VehicleData(basic, unk1, health, unk2>0, false, driveState, unk3, unk4>0, false, None, inventory)(VehicleFormat.Normal) + def apply(basic : CommonFieldData, health : Int, driveState : DriveState.Value, cloak : Boolean, inventory : Option[InventoryData]) : VehicleData = { + VehicleData(basic.pos, basic.faction, basic.bops, basic.destroyed, 0, basic.jammered, false, basic.player_guid, + false, health, false, false, driveState, false, false, cloak, None, inventory)(VehicleFormat.Normal) } /** * Overloaded constructor for specifically handling `Utility` vehicle format. - * @param basic data common to objects - * @param unk1 na + * @param basic a field that encompasses some data used by the vehicle, including `faction` and `owner` * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) - * @param unk2 na - * @param driveState a representation for the current mobility state; - * @param unk3 na - * @param unk4 utility-specific field - * @param unk5 na + * @param driveState a representation for the current mobility state + * @param cloak if a vehicle (that can cloak) is cloaked * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included - * @return a `VehicleData` object */ - def apply(basic : CommonFieldData, unk1 : Int, health : Int, unk2 : Int, driveState : DriveState.Value, unk3 : Boolean, unk4 : UtilityVehicleData, unk5 : Int, inventory : Option[InventoryData]) : VehicleData = { - new VehicleData(basic, unk1, health, unk2>0, false, driveState, unk3, unk5>0, false, Some(unk4), inventory)(VehicleFormat.Utility) + def apply(basic : CommonFieldData, health : Int, driveState : DriveState.Value, cloak : Boolean, format : UtilityVehicleData, inventory : Option[InventoryData]) : VehicleData = { + VehicleData(basic.pos, basic.faction, basic.bops, basic.destroyed, 0, basic.jammered, false, basic.player_guid, + false, health, false, false, driveState, false, false, cloak, Some(format), inventory)(VehicleFormat.Utility) } /** * Overloaded constructor for specifically handling `Variant` vehicle format. - * @param basic data common to objects - * @param unk1 na + * @param basic a field that encompasses some data used by the vehicle, including `faction` and `owner` * @param health the amount of health the vehicle has, as a percentage of a filled bar (255) - * @param unk2 na - * @param driveState a representation for the current mobility state; - * @param unk3 na - * @param unk4 variant-specific field - * @param unk5 na + * @param driveState a representation for the current mobility state + * @param cloak if a vehicle (that can cloak) is cloaked * @param inventory the seats, mounted weapons, and utilities (such as terminals) that are currently included - * @return a `VehicleData` object */ - def apply(basic : CommonFieldData, unk1 : Int, health : Int, unk2 : Int, driveState : DriveState.Value, unk3 : Boolean, unk4 : VariantVehicleData, unk5 : Int, inventory : Option[InventoryData]) : VehicleData = { - new VehicleData(basic, unk1, health, unk2>0, false, driveState, unk3, unk5>0, false, Some(unk4), inventory)(VehicleFormat.Variant) + def apply(basic : CommonFieldData, health : Int, driveState : DriveState.Value, cloak : Boolean, format : VariantVehicleData, inventory : Option[InventoryData]) : VehicleData = { + VehicleData(basic.pos, basic.faction, basic.bops, basic.destroyed, 0, basic.jammered, false, basic.player_guid, + false, health, false, false, driveState, false, false, cloak, Some(format), inventory)(VehicleFormat.Variant) + } + + import net.psforever.packet.game.objectcreate.{PlayerData => Player_Data} + /** + * Constructor that ignores the coordinate information + * and performs a vehicle-unique calculation of the padding value. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param inventory the player's inventory + * @param drawn_slot the holster that is initially drawn + * @param accumulative the input position for the stream up to which this entry; + * used to calculate the padding value for the player's name in `CharacterAppearanceData` + * @return a `PlayerData` object + */ + def PlayerData(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type, accumulative : Long) : Player_Data = { + val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) + Player_Data(None, appearance, character_data(appearance.backpack, true), Some(inventory), drawn_slot)(false) + } + /** + * Constructor for `PlayerData` that ignores the coordinate information and the inventory + * and performs a vehicle-unique calculation of the padding value. + * It passes information between the three major divisions for the purposes of offset calculations. + * This constructor should be used for players that are mounted. + * @param basic_appearance a curried function for the common fields regarding the the character's appearance + * @param character_data a curried function for the class-specific data that explains about the character + * @param drawn_slot the holster that is initially drawn + * @param accumulative the input position for the stream up to which this entry; + * used to calculate the padding value for the player's name in `CharacterAppearanceData` + * @return a `PlayerData` object + */ + def PlayerData(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type, accumulative : Long) : Player_Data = { + val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) + Player_Data.apply(None, appearance, character_data(appearance.backpack, true), None, drawn_slot)(false) } private val driveState8u = PacketHelpers.createEnumerationCodec(DriveState, uint8L) @@ -148,33 +189,39 @@ object VehicleData extends Marshallable[VehicleData] { /** * `Codec` for the "utility" format. */ - private val utility_data_codec : Codec[SpecificVehicleData] = uintL(6).hlist.exmap[SpecificVehicleData] ( - { - case n :: HNil => - Successful(UtilityVehicleData(n).asInstanceOf[SpecificVehicleData]) - }, - { - case UtilityVehicleData(n) => - Successful(n :: HNil) - case _ => - Failure(Err("wrong kind of vehicle data object (wants 'Utility')")) - } - ) + private val utility_data_codec : Codec[SpecificVehicleData] = { + import shapeless.:: + uintL(6).hlist.exmap[SpecificVehicleData] ( + { + case n :: HNil => + Successful(UtilityVehicleData(n).asInstanceOf[SpecificVehicleData]) + }, + { + case UtilityVehicleData(n) => + Successful(n :: HNil) + case _ => + Failure(Err("wrong kind of vehicle data object (wants 'Utility')")) + } + ) + } /** * `Codec` for the "variant" format. */ - private val variant_data_codec : Codec[SpecificVehicleData] = uint8L.hlist.exmap[SpecificVehicleData] ( - { - case n :: HNil => - Successful(VariantVehicleData(n).asInstanceOf[SpecificVehicleData]) - }, - { - case VariantVehicleData(n) => - Successful(n :: HNil) - case _ => - Failure(Err("wrong kind of vehicle data object (wants 'Variant')")) - } - ) + private val variant_data_codec : Codec[SpecificVehicleData] = { + import shapeless.:: + uint8L.hlist.exmap[SpecificVehicleData] ( + { + case n :: HNil => + Successful(VariantVehicleData(n).asInstanceOf[SpecificVehicleData]) + }, + { + case VariantVehicleData(n) => + Successful(n :: HNil) + case _ => + Failure(Err("wrong kind of vehicle data object (wants 'Variant')")) + } + ) + } /** * Select an appropriate `Codec` in response to the requested stream format @@ -190,47 +237,303 @@ object VehicleData extends Marshallable[VehicleData] { Failure(Err(s"$vehicleFormat is not a valid vehicle format for parsing data")).asInstanceOf[Codec[SpecificVehicleData]] } - def codec(vehicle_type : VehicleFormat.Value) : Codec[VehicleData] = ( - ("basic" | CommonFieldData.codec) :: - ("unk1" | uint2L) :: - ("health" | uint8L) :: - ("unk2" | bool) :: //usually 0 - ("no_mount_points" | bool) :: - ("driveState" | driveState8u) :: //used for deploy state - ("unk3" | bool) :: //unknown but generally false; can cause stream misalignment if set when unexpectedly - ("unk4" | bool) :: - ("cloak" | bool) :: //cloak as wraith, phantasm - conditional(vehicle_type != VehicleFormat.Normal, "unk5" | selectFormatReader(vehicle_type)) :: //padding? - optional(bool, "inventory" | InventoryData.codec) - ).exmap[VehicleData] ( - { - case basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: u5 :: cloak :: inv :: HNil => - Attempt.successful(new VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, u5, cloak, inv)(vehicle_type)) + def codec(vehicle_type : VehicleFormat.Value) : Codec[VehicleData] = { + import shapeless.:: + ( + ("pos" | PlacementData.codec) >>:~ { pos => + ("faction" | PlanetSideEmpire.codec) :: + ("bops" | bool) :: + ("destroyed" | bool) :: + ("unk1" | uint2L) :: //3 - na, 2 - common, 1 - na, 0 - common? + ("jammered" | bool) :: + ("unk2" | bool) :: + ("owner_guid" | PlanetSideGUID.codec) :: + ("unk3" | bool) :: + ("health" | uint8L) :: + ("unk4" | bool) :: //usually 0 + ("no_mount_points" | bool) :: + ("driveState" | driveState8u) :: //used for deploy state + ("unk5" | bool) :: //unknown but generally false; can cause stream misalignment if set when unexpectedly + ("unk6" | bool) :: + ("cloak" | bool) :: //cloak as wraith, phantasm + conditional(vehicle_type != VehicleFormat.Normal, "vehicle_format_data" | selectFormatReader(vehicle_type)) :: //padding? + optional(bool, "inventory" | custom_inventory_codec(InitialStreamLengthToSeatEntries(pos.vel.isDefined, vehicle_type))) + } + ).exmap[VehicleData] ( + { + case pos :: faction :: bops :: destroyed :: u1 :: jamd :: u2 :: owner :: u3 :: health :: u4 :: no_mount :: driveState :: u5 :: u6 :: cloak :: format :: inv :: HNil => + Attempt.successful(new VehicleData(pos, faction, bops, destroyed, u1, jamd, u2, owner, u3, health, u4, no_mount, driveState, u5, u6, cloak, format, inv)(vehicle_type)) - case _ => - Attempt.failure(Err("invalid vehicle data format")) - }, - { - case obj @ VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, cloak, Some(u5), inv) => - if(obj.vehicle_type == VehicleFormat.Normal) { - Attempt.failure(Err("invalid vehicle data format; variable bits not expected; will ignore ...")) - } - else { - Attempt.successful(basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: cloak :: Some(u5) :: inv :: HNil) - } + case _ => + Attempt.failure(Err("invalid vehicle data format")) + }, + { + case obj @ VehicleData(pos, faction, bops, destroyed, u1, jamd, u2, owner, u3, health, u4, no_mount, driveState, u5, u6, cloak, format, inv) => + if(obj.vehicle_type == VehicleFormat.Normal && format.nonEmpty) { + Attempt.failure(Err("invalid vehicle data format; variable bits not expected")) + } + else if(obj.vehicle_type != VehicleFormat.Normal && format.isEmpty) { + Attempt.failure(Err(s"invalid vehicle data format; variable bits for ${obj.vehicle_type} expected")) + } + else { + Attempt.successful(pos :: faction :: bops :: destroyed :: u1 :: jamd :: u2 :: owner :: u3 :: health :: u4 :: no_mount :: driveState :: u5 :: u6 :: cloak :: format :: inv :: HNil) + } - case obj @ VehicleData(basic, u1, health, u2, no_mount, driveState, u3, u4, cloak, None, inv) => - if(obj.vehicle_type != VehicleFormat.Normal) { - Attempt.failure(Err("invalid vehicle data format; variable bits expected")) - } - else { - Attempt.successful(basic :: u1 :: health :: u2 :: no_mount :: driveState :: u3 :: u4 :: cloak :: None :: inv :: HNil) - } + case _ => + Attempt.failure(Err("invalid vehicle data format")) + } + ) + } - case _ => - Attempt.failure(Err("invalid vehicle data format")) + /** + * Distance from the length field of a vehicle creation packet up until the start of the vehicle's inventory data. + * The only field excluded belongs to the original opcode for the packet. + * The parameters outline reasons why the length of the stream would be different + * and are used to determine the exact difference value.
+ * Note:
+ * 198 includes the `ObjectCreateMessage` packet fields, without parent data, + * the `VehicleData` fields, + * and the first three fields of the `InternalSlot`. + * @see `ObjectCreateMessage` + * @param hasVelocity the presence of a velocity field - `vel` - in the `PlacementData` object for this vehicle + * @param format the `Codec` subtype for this vehicle + * @return the length of the bitstream + */ + def InitialStreamLengthToSeatEntries(hasVelocity : Boolean, format : VehicleFormat.Type) : Long = { + 198 + + (if(hasVelocity) { 42 } else { 0 }) + + (format match { + case VehicleFormat.Utility => 6 + case VehicleFormat.Variant => 8 + case _ => 0 + }) + } + + /** + * Increment the distance to the next mounted player's `name` field with the length of the previous entry, + * then calculate the new padding value for that next entry's `name` field. + * @param base the original distance to the last entry + * @param next the length of the last entry, if one was parsed + * @return the padding value, 0-7 bits + */ + def CumulativeSeatedPlayerNamePadding(base : Long, next : Option[StreamBitSize]) : Int = { + CumulativeSeatedPlayerNamePadding(base + (next match { + case Some(o) => o.bitsize + case None => 0 + })) + } + + /** + * Calculate the padding value for the next mounted player character's name `String`. + * Due to the depth of seated player characters, the `name` field can have a variable amount of padding + * between the string size field and the first character. + * Specifically, the padding value is the number of bits after the size field + * that would cause the first character of the name to be aligned to the first bit of the next byte. + * The 35 counts the object class, unique identifier, and slot fields of the enclosing `InternalSlot`. + * The 23 counts all of the fields before the player's `name` field in `CharacterAppearanceData`. + * @see `InternalSlot`
+ * `CharacterAppearanceData.name`
+ * `VehicleData.InitialStreamLengthToSeatEntries` + * @param accumulative current entry stream offset (start of this player's entry) + * @return the padding value, 0-7 bits + */ + private def CumulativeSeatedPlayerNamePadding(accumulative : Long) : Int = { + Player_Data.ByteAlignmentPadding(accumulative + 23 + 35) + } + + /** + * A special method of handling mounted players within the same inventory space as normal `Equipment` can be encountered + * before restoring normal inventory operations.
+ *
+ * Due to variable-length fields within `PlayerData` extracted from the input, + * the distance of the bit(stream) vector to the initial inventory entry is calculated + * to produce the initial value for padding the `PlayerData` object's name field. + * After player-related entries have been extracted and processed in isolation, + * the remainder of the inventory must be handled as standard inventory + * and finally both groups must be repackaged into a single standard `InventoryData` object. + * Due to the unique value for the mounted players that must be updated for each entry processed, + * the entries are temporarily formatted into a linked list before being put back into a normal `List`.
+ *
+ * 6 June 2018:
+ * Due to curious behavior in the vehicle seat access controls, + * please only encode and decode the driver seat even though all seats are currently reachable. + * @param length the distance in bits to the first inventory entry + * @return a `Codec` that translates `InventoryData` + */ + private def custom_inventory_codec(length : Long) : Codec[InventoryData] = { + import shapeless.:: + ( + uint8 >>:~ { size => + uint2 :: + (inventory_seat_codec( + length, //length of stream until current seat + CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat + ) >>:~ { seats => + PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist + }) + } + ).xmap[InventoryData] ( + { + case _ :: _ :: None :: inv :: HNil => + InventoryData(inv) + + case _ :: _ :: seats :: inv :: HNil => + InventoryData(unlinkSeats(seats) ++ inv) + }, + { + case InventoryData(inv) => + val (seats, slots) = inv.partition(entry => entry.objectClass == ObjectClass.avatar) + inv.size :: 0 :: chainSeats(seats) :: slots :: HNil + } + ) + } + + /** + * The format for the linked list of extracted mounted `PlayerData`. + * @param seat data for this entry extracted via `PlayerData` + * @param next the next entry + */ + private case class InventorySeat(seat : Option[InternalSlot], next : Option[InventorySeat]) + + /** + * Look ahead at the next value to determine if it is an example of a player character + * and would be processed as a `PlayerData` object. + * Update the stream read position with each extraction. + * Continue to process values so long as they represent player character data. + * @param length the distance in bits to the current inventory entry + * @param offset the padding value for this entry's player character's `name` field + * @return a recursive `Codec` that translates subsequent `PlayerData` entries until exhausted + */ + private def inventory_seat_codec(length : Long, offset : Int) : Codec[Option[InventorySeat]] = { + import shapeless.:: + ( + PacketHelpers.peek(uintL(11)) >>:~ { objClass => + conditional(objClass == ObjectClass.avatar, seat_codec(offset)) >>:~ { seat => + conditional(objClass == ObjectClass.avatar, inventory_seat_codec( + { //length of stream until next seat + length + (seat match { + case Some(o) => o.bitsize + case None => 0 + }) + }, + CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat + )).hlist + } + } + ).exmap[Option[InventorySeat]] ( + { + case _ :: None :: None :: HNil => + Successful(None) + + case _ :: slot :: Some(next) :: HNil => + Successful(Some(InventorySeat(slot, next))) + }, + { + case None => + Successful(0 :: None :: None :: HNil) + + case Some(InventorySeat(slot, None)) => + Successful(ObjectClass.avatar :: slot :: None :: HNil) + + case Some(InventorySeat(slot, next)) => + Successful(ObjectClass.avatar :: slot :: Some(next) :: HNil) + } + ) + } + + /** + * Translate data the is verified to involve a player who is seated (mounted) to the parent object at a given slot. + * The operation performed by this `Codec` is very similar to `InternalSlot.codec`. + * @param pad the padding offset for the player's name; + * 0-7 bits; + * this padding value must recalculate for each represented seat + * @see `CharacterAppearanceData`
+ * `VehicleData.InitialStreamLengthToSeatEntries`
+ * `CumulativeSeatedPlayerNamePadding` + * @return a `Codec` that translates `PlayerData` + */ + private def seat_codec(pad : Int) : Codec[InternalSlot] = { + import shapeless.:: + ( + ("objectClass" | uintL(11)) :: + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + ("obj" | Player_Data.codec(pad)) + ).xmap[InternalSlot] ( + { + case objectClass :: guid :: parentSlot :: obj :: HNil => + InternalSlot(objectClass, guid, parentSlot, obj) + }, + { + case InternalSlot(objectClass, guid, parentSlot, obj) => + objectClass :: guid :: parentSlot :: obj.asInstanceOf[PlayerData] :: HNil + } + ) + } + + /** + * Count the number of entries in a linked list. + * @param chain the head of the linked list + * @return the number of entries + */ + private def countSeats(chain : Option[InventorySeat]) : Int = { + chain match { + case Some(_) => + var curr = chain + var count = 0 + do { + val link = curr.get + count += (if(link.seat.nonEmpty) { 1 } else { 0 }) + curr = link.next + } + while(curr.nonEmpty) + count + + case None => + 0 } - ) + } + + /** + * Transform a linked list of `InventorySlot` slot objects into a formal list of `InternalSlot` objects. + * @param chain the head of the linked list + * @return a proper list of the contents of the input linked list + */ + private def unlinkSeats(chain : Option[InventorySeat]) : List[InternalSlot] = { + var curr = chain + val out = new ListBuffer[InternalSlot] + while(curr.isDefined) { + val link = curr.get + link.seat match { + case None => + curr = None + case Some(seat) => + out += seat + curr = link.next + } + } + out.toList + } + + /** + * Transform a formal list of `InternalSlot` objects into a linked list of `InventorySlot` slot objects. + * @param list a proper list of objects + * @return a linked list composed of the contents of the input list + */ + private def chainSeats(list : List[InternalSlot]) : Option[InventorySeat] = { + list match { + case Nil => + None + case x :: Nil => + Some(InventorySeat(Some(x), None)) + case _ :: _ => + var link = InventorySeat(Some(list.last), None) //build the chain in reverse order, starting with the last entry + list.reverse.drop(1).foreach(seat => { + link = InventorySeat(Some(seat), Some(link)) + }) + Some(link) + } + } implicit val codec : Codec[VehicleData] = codec(VehicleFormat.Normal) } diff --git a/common/src/main/scala/net/psforever/types/CharacterVoice.scala b/common/src/main/scala/net/psforever/types/CharacterVoice.scala new file mode 100644 index 000000000..556b712fd --- /dev/null +++ b/common/src/main/scala/net/psforever/types/CharacterVoice.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package net.psforever.types + +import net.psforever.packet.PacketHelpers +import scodec.codecs.uint + +/** + * The voice used by the player character, from a selection of ten divided between five male voices and five female voices. + * The first entry (0) is no voice. + * While it is technically not valid to have a wrong-gendered voice, + * unlisted sixth and seventh entries would give a male character a female voice; + * a female character with either entry would become mute, however. + * @see `CharacterGender` + */ +object CharacterVoice extends Enumeration { + type Type = Value + + val + Mute, + Voice1, //grizzled, tough + Voice2, //greenhorn, clueless + Voice3, //roughneck, gruff + Voice4, //stalwart, smooth + Voice5 //daredevil, calculating + = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3)) +} diff --git a/common/src/main/scala/services/RemoverActor.scala b/common/src/main/scala/services/RemoverActor.scala new file mode 100644 index 000000000..85e211e02 --- /dev/null +++ b/common/src/main/scala/services/RemoverActor.scala @@ -0,0 +1,502 @@ +// Copyright (c) 2017 PSForever +package services + +import akka.actor.{Actor, ActorRef, Cancellable} +import net.psforever.objects.guid.TaskResolver +import net.psforever.objects.zones.Zone +import net.psforever.objects.{DefaultCancellable, PlanetSideGameObject} +import net.psforever.types.Vector3 + +import scala.annotation.tailrec +import scala.concurrent.duration._ + +/** + * The base class for a type of "destruction `Actor`" intended to be used for delaying object cleanup activity. + * Objects submitted to this process should be registered to a global unique identified system for a given region + * as is specified in their submission.
+ *
+ * Two waiting lists are used to pool the objects being removed. + * The first list is a basic pooling list that precludes any proper removal actions + * and is almost expressly for delaying the process. + * Previously-submitted tasks can be removed from this list so long as a matching object can be found. + * Tasks in this list can also be expedited into the second list without having to consider delays. + * After being migrated to the secondary list, the object is considered beyond the point of no return. + * Followup activity will lead to its inevitable unregistering and removal.
+ *
+ * Functions have been provided for `override` in order to interject the appropriate cleanup operations. + * The activity itself is typically removing the object in question from a certain list, + * dismissing it with a mass distribution of `ObjectDeleteMessage` packets, + * and finally unregistering it. + * Some types of object have (de-)implementation variations which should be made explicit through the overrides. + */ +abstract class RemoverActor extends Actor { + /** + * The timer that checks whether entries in the first pool are still eligible for that pool. + */ + var firstTask : Cancellable = DefaultCancellable.obj + /** + * The first pool of objects waiting to be processed for removal. + */ + var firstHeap : List[RemoverActor.Entry] = List() + + /** + * The timer that checks whether entries in the second pool are still eligible for that pool. + */ + var secondTask : Cancellable = DefaultCancellable.obj + /** + * The second pool of objects waiting to be processed for removal. + */ + var secondHeap : List[RemoverActor.Entry] = List() + + private var taskResolver : ActorRef = Actor.noSender + + private[this] val log = org.log4s.getLogger + def trace(msg : String) : Unit = log.trace(msg) + def debug(msg : String) : Unit = log.debug(msg) + + /** + * Send the initial message that requests a task resolver for assisting in the removal process. + */ + override def preStart() : Unit = { + super.preStart() + self ! RemoverActor.Startup() + } + + /** + * Sufficiently clean up the current contents of these waiting removal jobs. + * Cancel all timers, rush all entries in the lists through their individual steps, then empty the lists. + * This is an improved `HurryAll`, but still faster since it also railroads entries through the second queue as well. + */ + override def postStop() = { + super.postStop() + firstTask.cancel + secondTask.cancel + firstHeap.foreach(entry => { + FirstJob(entry) + SecondJob(entry) + }) + secondHeap.foreach { SecondJob } + firstHeap = Nil + secondHeap = Nil + taskResolver = ActorRef.noSender + } + + def receive : Receive = { + case RemoverActor.Startup() => + ServiceManager.serviceManager ! ServiceManager.Lookup("taskResolver") //ask for a resolver to deal with the GUID system + + case ServiceManager.LookupResult("taskResolver", endpoint) => + taskResolver = endpoint + context.become(Processing) + + case msg => + log.error(s"received message $msg before being properly initialized") + } + + def Processing : Receive = { + case RemoverActor.AddTask(obj, zone, duration) => + val entry = RemoverActor.Entry(obj, zone, duration.getOrElse(FirstStandardDuration).toNanos) + if(InclusionTest(entry) && !secondHeap.exists(test => RemoverActor.Similarity(test, entry) )) { + InitialJob(entry) + if(firstHeap.isEmpty) { + //we were the only entry so the event must be started from scratch + firstHeap = List(entry) + trace(s"a remover task has been added: $entry") + RetimeFirstTask() + } + else { + //unknown number of entries; append, sort, then re-time tasking + val oldHead = firstHeap.head + if(!firstHeap.exists(test => RemoverActor.Similarity(test, entry))) { + firstHeap = (firstHeap :+ entry).sortBy(_.duration) + trace(s"a remover task has been added: $entry") + if(oldHead != firstHeap.head) { + RetimeFirstTask() + } + } + else { + trace(s"$obj is already queued for removal") + } + } + } + else { + trace(s"$obj either does not qualify for this Remover or is already queued") + } + + case RemoverActor.HurrySpecific(targets, zone) => + HurrySpecific(targets, zone) + + case RemoverActor.HurryAll() => + HurryAll() + + case RemoverActor.ClearSpecific(targets, zone) => + ClearSpecific(targets, zone) + + case RemoverActor.ClearAll() => + ClearAll() + + //private messages from RemoverActor to RemoverActor + case RemoverActor.StartDelete() => + firstTask.cancel + secondTask.cancel + val now : Long = System.nanoTime + val (in, out) = firstHeap.partition(entry => { now - entry.time >= entry.duration }) + firstHeap = out + secondHeap = secondHeap ++ in.map { RepackageEntry } + in.foreach { FirstJob } + RetimeFirstTask() + if(secondHeap.nonEmpty) { + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + trace(s"item removal task has found ${in.size} items to remove") + + case RemoverActor.TryDelete() => + secondTask.cancel + val (in, out) = secondHeap.partition { ClearanceTest } + secondHeap = out + in.foreach { SecondJob } + if(out.nonEmpty) { + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + trace(s"item removal task has removed ${in.size} items") + + case RemoverActor.FailureToWork(entry, ex) => + log.error(s"${entry.obj} from ${entry.zone} not properly deleted - $ex") + + case _ => ; + } + + /** + * Expedite some entries from the first pool into the second. + * @param targets a list of objects to pick + * @param zone the zone in which these objects must be discovered; + * all targets must be in this zone, with the assumption that this is the zone where they were registered + */ + def HurrySpecific(targets : List[PlanetSideGameObject], zone : Zone) : Unit = { + CullTargetsFromFirstHeap(targets, zone) match { + case Nil => + debug(s"no tasks matching the targets $targets have been hurried") + case list => + debug(s"the following tasks have been hurried: $list") + secondTask.cancel + list.foreach { FirstJob } + secondHeap = secondHeap ++ list.map { RepackageEntry } + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + } + + /** + * Expedite all entries from the first pool into the second. + */ + def HurryAll() : Unit = { + trace("all tasks have been hurried") + firstTask.cancel + firstHeap.foreach { FirstJob } + secondHeap = secondHeap ++ firstHeap.map { RepackageEntry } + firstHeap = Nil + secondTask.cancel + import scala.concurrent.ExecutionContext.Implicits.global + secondTask = context.system.scheduler.scheduleOnce(SecondStandardDuration, self, RemoverActor.TryDelete()) + } + + /** + * Remove specific entries from the first pool. + */ + def ClearSpecific(targets : List[PlanetSideGameObject], zone : Zone) : Unit = { + CullTargetsFromFirstHeap(targets, zone) match { + case Nil => + debug(s"no tasks matching the targets $targets have been cleared") + case list => + debug(s"the following tasks have been cleared: $list") + } + } + + /** + * No entries in the first pool. + */ + def ClearAll() : Unit = { + firstTask.cancel + firstHeap = Nil + } + + /** + * Retime an individual entry by recreating it. + * @param entry an existing entry + * @return a new entry, containing the same object and zone information; + * this new entry is always set to last for the duration of the second pool + */ + private def RepackageEntry(entry : RemoverActor.Entry) : RemoverActor.Entry = { + RemoverActor.Entry(entry.obj, entry.zone, SecondStandardDuration.toNanos) + } + + /** + * Search the first pool of entries awaiting removal processing. + * If any entry has the same object as one of the targets and belongs to the same zone, remove it from the first pool. + * If no targets are selected (an empty list), all discovered targets within the appropriate zone are removed. + * @param targets a list of objects to pick + * @param zone the zone in which these objects must be discovered; + * all targets must be in this zone, with the assumption that this is the zone where they were registered + * @return all of the discovered entries + */ + private def CullTargetsFromFirstHeap(targets : List[PlanetSideGameObject], zone : Zone) : List[RemoverActor.Entry] = { + val culledEntries = if(targets.nonEmpty) { + if(targets.size == 1) { + debug(s"a target submitted: ${targets.head}") + //simple selection + RemoverActor.recursiveFind(firstHeap.iterator, RemoverActor.Entry(targets.head, zone, 0)) match { + case None => ; + Nil + case Some(index) => + val entry = firstHeap(index) + firstHeap = (firstHeap.take(index) ++ firstHeap.drop(index + 1)).sortBy(_.duration) + List(entry) + } + } + else { + debug(s"multiple targets submitted: $targets") + //cumbersome partition + //a - find targets from entries + val locatedTargets = for { + a <- targets.map(RemoverActor.Entry(_, zone, 0)) + b <- firstHeap//.filter(entry => entry.zone == zone) + if b.obj.HasGUID && a.obj.HasGUID && RemoverActor.Similarity(b, a) + } yield b + if(locatedTargets.nonEmpty) { + //b - entries, after the found targets are removed (cull any non-GUID entries while at it) + firstHeap = (for { + a <- locatedTargets + b <- firstHeap + if b.obj.HasGUID && a.obj.HasGUID && !RemoverActor.Similarity(b, a) + } yield b).sortBy(_.duration) + locatedTargets + } + else { + Nil + } + } + } + else { + debug(s"all targets within the specified zone $zone will be submitted") + //no specific targets; split on all targets in the given zone instead + val (in, out) = firstHeap.partition(entry => entry.zone == zone) + firstHeap = out.sortBy(_.duration) + in + } + if(culledEntries.nonEmpty) { + RetimeFirstTask() + culledEntries + } + else { + Nil + } + } + + /** + * Common function to reset the first task's delayed execution. + * Cancels the scheduled timer and will only restart the timer if there is at least one entry in the first pool. + * @param now the time (in nanoseconds); + * defaults to the current time (in nanoseconds) + */ + def RetimeFirstTask(now : Long = System.nanoTime) : Unit = { + firstTask.cancel + if(firstHeap.nonEmpty) { + val short_timeout : FiniteDuration = math.max(1, firstHeap.head.duration - (now - firstHeap.head.time)) nanoseconds + import scala.concurrent.ExecutionContext.Implicits.global + firstTask = context.system.scheduler.scheduleOnce(short_timeout, self, RemoverActor.StartDelete()) + } + } + + def SecondJob(entry : RemoverActor.Entry) : Unit = { + entry.obj.Position = Vector3.Zero //somewhere it will not disturb anything + taskResolver ! FinalTask(entry) + } + + def FinalTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { + import net.psforever.objects.guid.Task + TaskResolver.GiveTask ( + new Task() { + private val localEntry = entry + private val localAnnounce = self + + override def isComplete : Task.Resolution.Value = if(!localEntry.obj.HasGUID) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + + def Execute(resolver : ActorRef) : Unit = { + resolver ! scala.util.Success(this) + } + + override def onFailure(ex : Throwable): Unit = { + localAnnounce ! RemoverActor.FailureToWork(localEntry, ex) + } + }, List(DeletionTask(entry)) + ) + } + + /** + * Default time for entries waiting in the first list. + * Override. + * @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds) + */ + def FirstStandardDuration : FiniteDuration + + /** + * Default time for entries waiting in the second list. + * Override. + * @return the time as a `FiniteDuration` object (to be later transformed into nanoseconds) + */ + def SecondStandardDuration : FiniteDuration + + /** + * Determine whether or not the resulting entry is valid for this removal process. + * The primary purpose of this function should be to determine if the appropriate type of object is being submitted. + * Override. + * @param entry the entry + * @return `true`, if it can be processed; `false`, otherwise + */ + def InclusionTest(entry : RemoverActor.Entry) : Boolean + + /** + * Performed when the entry is initially added to the first list. + * Override. + * @param entry the entry + */ + def InitialJob(entry : RemoverActor.Entry) : Unit + + /** + * Performed when the entry is shifted from the first list to the second list. + * Override. + * @param entry the entry + */ + def FirstJob(entry : RemoverActor.Entry) : Unit + + /** + * Performed to determine when an entry can be shifted off from the second list. + * Override. + * @param entry the entry + */ + def ClearanceTest(entry : RemoverActor.Entry) : Boolean + + /** + * The specific action that is necessary to complete the removal process. + * Override. + * @see `GUIDTask` + * @param entry the entry + */ + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask +} + +object RemoverActor { + /** + * All information necessary to apply to the removal process to produce an effect. + * Internally, all entries have a "time created" field. + * @param obj the target + * @param zone the zone in which this target is registered + * @param duration how much longer the target will exist in its current state (in nanoseconds) + */ + case class Entry(obj : PlanetSideGameObject, zone : Zone, duration : Long) { + /** The time when this entry was created (in nanoseconds) */ + val time : Long = System.nanoTime + } + + /** + * A message that prompts the retrieval of a `TaskResolver` for us in the removal process. + */ + case class Startup() + + /** + * Message to submit an object to the removal process. + * @see `FirstStandardDuration` + * @param obj the target + * @param zone the zone in which this target is registered + * @param duration how much longer the target will exist in its current state (in nanoseconds); + * a default time duration is provided by implementation + */ + case class AddTask(obj : PlanetSideGameObject, zone : Zone, duration : Option[FiniteDuration] = None) + + /** + * "Hurrying" shifts entries with the discovered objects (in the same `zone`) + * through their first task and into the second pool. + * If the list of targets is empty, all discovered objects in the given zone will be considered targets. + * @param targets a list of objects to match + * @param zone the zone in which these objects exist; + * the assumption is that all these target objects are registered to this zone + */ + case class HurrySpecific(targets : List[PlanetSideGameObject], zone : Zone) + /** + * "Hurrying" shifts all entries through their first task and into the second pool. + */ + case class HurryAll() + + /** + * "Clearing" cancels entries with the discovered objects (in the same `zone`) + * if they are discovered in the first pool of objects. + * Those entries will no longer be affected by any actions performed by the removal process until re-submitted. + * If the list of targets is empty, all discovered objects in the given zone will be considered targets. + * @param targets a list of objects to match + * @param zone the zone in which these objects exist; + * the assumption is that all these target objects are registered to this zone + */ + case class ClearSpecific(targets : List[PlanetSideGameObject], zone : Zone) + /** + * "Clearing" cancels all entries if they are discovered in the first pool of objects. + * Those entries will no longer be affected by any actions performed by the removal process until re-submitted. + */ + case class ClearAll() + + /** + * Message that indicates that the final stage of the remover process has failed. + * Since the last step is generally unregistering the object, it could be a critical error. + * @param entry the entry that was not properly removed + * @param ex the reason the last entry was not properly removed + */ + protected final case class FailureToWork(entry : RemoverActor.Entry, ex : Throwable) + + /** + * Internal message to flag operations by data in the first list if it has been in that list long enough. + */ + private final case class StartDelete() + + /** + * Internal message to flag operations by data in the second list if it has been in that list long enough. + */ + private final case class TryDelete() + + /** + * Match two entries by object and by zone information. + * @param entry1 the first entry + * @param entry2 the second entry + * @return if they match + */ + private def Similarity(entry1 : RemoverActor.Entry, entry2 : RemoverActor.Entry) : Boolean = { + entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID + } + + /** + * Get the index of an entry in the list of entries. + * @param iter an `Iterator` of entries + * @param target the specific entry to be found + * @param index the incrementing index value + * @return the index of the entry in the list, if a match to the target is found + */ + @tailrec private def recursiveFind(iter : Iterator[RemoverActor.Entry], target : RemoverActor.Entry, index : Int = 0) : Option[Int] = { + if(!iter.hasNext) { + None + } + else { + val entry = iter.next + if(entry.obj.HasGUID && target.obj.HasGUID && Similarity(entry, target)) { + Some(index) + } + else { + recursiveFind(iter, target, index + 1) + } + } + } +} diff --git a/pslogin/src/main/scala/services/Service.scala b/common/src/main/scala/services/Service.scala similarity index 100% rename from pslogin/src/main/scala/services/Service.scala rename to common/src/main/scala/services/Service.scala diff --git a/pslogin/src/main/scala/services/ServiceManager.scala b/common/src/main/scala/services/ServiceManager.scala similarity index 100% rename from pslogin/src/main/scala/services/ServiceManager.scala rename to common/src/main/scala/services/ServiceManager.scala diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/common/src/main/scala/services/avatar/AvatarAction.scala similarity index 76% rename from pslogin/src/main/scala/services/avatar/AvatarAction.scala rename to common/src/main/scala/services/avatar/AvatarAction.scala index 1cfaeb8f1..8d3a0ea28 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarAction.scala +++ b/common/src/main/scala/services/avatar/AvatarAction.scala @@ -1,13 +1,16 @@ // Copyright (c) 2017 PSForever package services.avatar -import net.psforever.objects.Player +import net.psforever.objects.{PlanetSideGameObject, Player} import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.Container import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{CargoMountPointStatusMessage, PlanetSideGUID, PlayerStateMessageUpstream} -import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.types.{ExoSuitType, Vector3} +import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent} +import net.psforever.types.ExoSuitType + +import scala.concurrent.duration.FiniteDuration object AvatarAction { trait Action @@ -18,16 +21,15 @@ object AvatarAction { final case class ChangeFireState_Start(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ChangeFireState_Stop(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Action + final case class DropItem(player_guid : PlanetSideGUID, item : Equipment, zone : Zone) extends Action final case class EquipmentInHand(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action - final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Action - final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action -// final case class LoadMap(msg : PlanetSideGUID) extends Action -// final case class unLoadMap(msg : PlanetSideGUID) extends Action + final case class LoadPlayer(player_guid : PlanetSideGUID, object_id : Int, target_guid : PlanetSideGUID, cdata : ConstructorData, pdata : Option[ObjectCreateMessageParent]) extends Action final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action - final case class Release(player : Player, zone : Zone, time : Option[Long] = None) extends Action + final case class PickupItem(player_guid : PlanetSideGUID, zone : Zone, target : PlanetSideGameObject with Container, slot : Int, item : Equipment, unk : Int = 0) extends Action + final case class Release(player : Player, zone : Zone, time : Option[FiniteDuration] = None) extends Action final case class Reload(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action final case class StowEquipment(player_guid : PlanetSideGUID, target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class WeaponDryFire(player_guid : PlanetSideGUID, weapon_guid : PlanetSideGUID) extends Action @@ -37,5 +39,4 @@ object AvatarAction { // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action // final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action -// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action } diff --git a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala b/common/src/main/scala/services/avatar/AvatarResponse.scala similarity index 75% rename from pslogin/src/main/scala/services/avatar/AvatarResponse.scala rename to common/src/main/scala/services/avatar/AvatarResponse.scala index 0eeab5fb4..b56d92cf6 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarResponse.scala @@ -4,9 +4,9 @@ package services.avatar import net.psforever.objects.Player import net.psforever.objects.equipment.Equipment import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream} import net.psforever.packet.game.objectcreate.ConstructorData -import net.psforever.types.{ExoSuitType, Vector3} +import net.psforever.types.ExoSuitType object AvatarResponse { trait Response @@ -17,11 +17,9 @@ object AvatarResponse { final case class ChangeFireState_Start(weapon_guid : PlanetSideGUID) extends Response final case class ChangeFireState_Stop(weapon_guid : PlanetSideGUID) extends Response final case class ConcealPlayer() extends Response - final case class EquipmentInHand(target_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Response - final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item_id : Int, item_guid : PlanetSideGUID, item_data : ConstructorData) extends Response - final case class LoadPlayer(pdata : ConstructorData) extends Response - // final case class unLoadMap() extends Response - // final case class LoadMap() extends Response + final case class EquipmentInHand(pkt : ObjectCreateMessage) extends Response + final case class DropItem(pkt : ObjectCreateMessage) extends Response + final case class LoadPlayer(pkt : ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response final case class ObjectHeld(slot : Int) extends Response final case class PlanetsideAttribute(attribute_type : Int, attribute_value : Long) extends Response @@ -35,5 +33,4 @@ object AvatarResponse { // final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response // final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response // final case class HitHintReturn(itemID : PlanetSideGUID) extends Response - // final case class ChangeWeapon(facingYaw : Int) extends Response } diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/common/src/main/scala/services/avatar/AvatarService.scala similarity index 64% rename from pslogin/src/main/scala/services/avatar/AvatarService.scala rename to common/src/main/scala/services/avatar/AvatarService.scala index aa0f0aaa0..f74fec1d1 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarService.scala +++ b/common/src/main/scala/services/avatar/AvatarService.scala @@ -2,12 +2,15 @@ package services.avatar import akka.actor.{Actor, ActorRef, Props} -import services.avatar.support.CorpseRemovalActor -import services.{GenericEventBus, Service} +import net.psforever.packet.game.ObjectCreateMessage +import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectCreateMessageParent, PlacementData} +import services.avatar.support.{CorpseRemovalActor, DroppedItemRemover} +import services.{GenericEventBus, RemoverActor, Service} class AvatarService extends Actor { private val undertaker : ActorRef = context.actorOf(Props[CorpseRemovalActor], "corpse-removal-agent") - undertaker ! "startup" + private val janitor = context.actorOf(Props[DroppedItemRemover], "item-remover-agent") + //undertaker ! "startup" private [this] val log = org.log4s.getLogger @@ -62,17 +65,36 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ConcealPlayer()) ) - case AvatarAction.EquipmentInHand(player_guid, target_guid, slot, obj) => - AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentInHand(target_guid, slot, obj)) + case AvatarAction.DropItem(player_guid, item, zone) => + val definition = item.Definition + val objectData = DroppedItemData( + PlacementData(item.Position, item.Orientation), + definition.Packet.ConstructorData(item).get ) - case AvatarAction.EquipmentOnGround(player_guid, pos, orient, item_id, item_guid, item_data) => + janitor forward RemoverActor.AddTask(item, zone) AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, + AvatarResponse.DropItem(ObjectCreateMessage(definition.ObjectId, item.GUID, objectData)) + ) ) - case AvatarAction.LoadPlayer(player_guid, pdata) => + case AvatarAction.EquipmentInHand(player_guid, target_guid, slot, item) => + val definition = item.Definition + val containerData = ObjectCreateMessageParent(target_guid, slot) + val objectData = definition.Packet.ConstructorData(item).get AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.LoadPlayer(pdata)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, + AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, item.GUID, containerData, objectData)) + ) + ) + case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) => + val pkt = pdata match { + case Some(data) => + ObjectCreateMessage(object_id, target_guid, data, cdata) + case None => + ObjectCreateMessage(object_id, target_guid, cdata) + } + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.LoadPlayer(pkt)) ) case AvatarAction.ObjectDelete(player_guid, item_guid, unk) => AvatarEvents.publish( @@ -90,11 +112,24 @@ class AvatarService extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarResponse.PlayerState(msg, spectator, weapon)) ) + case AvatarAction.PickupItem(player_guid, zone, target, slot, item, unk) => + janitor forward RemoverActor.ClearSpecific(List(item), zone) + AvatarEvents.publish( + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, { + val itemGUID = item.GUID + if(target.VisibleSlots.contains(slot)) { + val definition = item.Definition + val containerData = ObjectCreateMessageParent(target.GUID, slot) + val objectData = definition.Packet.ConstructorData(item).get + AvatarResponse.EquipmentInHand(ObjectCreateMessage(definition.ObjectId, itemGUID, containerData, objectData)) + } + else { + AvatarResponse.ObjectDelete(itemGUID, unk) + } + }) + ) case AvatarAction.Release(player, zone, time) => - undertaker ! (time match { - case Some(t) => CorpseRemovalActor.AddCorpse(player, zone, t) - case None => CorpseRemovalActor.AddCorpse(player, zone) - }) + undertaker forward RemoverActor.AddTask(player, zone, time) AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player.GUID, AvatarResponse.Release(player)) ) @@ -119,52 +154,13 @@ class AvatarService extends Actor { } //message to Undertaker - case AvatarServiceMessage.RemoveSpecificCorpse(corpses) => - undertaker ! AvatarServiceMessage.RemoveSpecificCorpse( corpses.filter(corpse => {corpse.HasGUID && corpse.isBackpack}) ) + case AvatarServiceMessage.Corpse(msg) => + undertaker forward msg + + case AvatarServiceMessage.Ground(msg) => + janitor forward msg /* - case AvatarService.PlayerStateMessage(msg) => - // log.info(s"NEW: ${m}") - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid, - AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked) - )) - - } - case AvatarService.LoadMap(msg) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), - AvatarServiceReply.LoadMap() - )) - } - case AvatarService.unLoadMap(msg) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), - AvatarServiceReply.unLoadMap() - )) - } - case AvatarService.ObjectHeld(msg) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid), - AvatarServiceReply.ObjectHeld() - )) - } - case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid, - AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value) - )) - } case AvatarService.PlayerStateShift(killer, guid) => val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid) if (playerOpt.isDefined) { @@ -189,16 +185,8 @@ class AvatarService extends Actor { AvatarServiceReply.DestroyDisplay(source_guid) )) } - case AvatarService.ChangeWeapon(unk1, sessionId) => - val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId) - if (playerOpt.isDefined) { - val player: PlayerAvatar = playerOpt.get - AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid), - AvatarServiceReply.ChangeWeapon(unk1) - )) - } */ case msg => - log.info(s"Unhandled message $msg from $sender") + log.warn(s"Unhandled message $msg from $sender") } } diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala similarity index 65% rename from pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala rename to common/src/main/scala/services/avatar/AvatarServiceMessage.scala index 04b96a901..a600c0c37 100644 --- a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceMessage.scala @@ -1,10 +1,9 @@ // Copyright (c) 2017 PSForever package services.avatar -import net.psforever.objects.Player - final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action) object AvatarServiceMessage { - final case class RemoveSpecificCorpse(corpse : List[Player]) + final case class Corpse(msg : Any) + final case class Ground(msg : Any) } diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala similarity index 100% rename from pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala rename to common/src/main/scala/services/avatar/AvatarServiceResponse.scala diff --git a/common/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/common/src/main/scala/services/avatar/support/CorpseRemovalActor.scala new file mode 100644 index 000000000..f75da9462 --- /dev/null +++ b/common/src/main/scala/services/avatar/support/CorpseRemovalActor.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package services.avatar.support + +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.objects.Player +import services.{RemoverActor, Service} +import services.avatar.{AvatarAction, AvatarServiceMessage} + +import scala.concurrent.duration._ + +class CorpseRemovalActor extends RemoverActor { + final val FirstStandardDuration : FiniteDuration = 3 minutes + + final val SecondStandardDuration : FiniteDuration = 500 milliseconds + + def InclusionTest(entry : RemoverActor.Entry) : Boolean = { + entry.obj.isInstanceOf[Player] && entry.obj.asInstanceOf[Player].isBackpack + } + + def InitialJob(entry : RemoverActor.Entry) : Unit = { } + + def FirstJob(entry : RemoverActor.Entry) : Unit = { + import net.psforever.objects.zones.Zone + entry.zone.Population ! Zone.Corpse.Remove(entry.obj.asInstanceOf[Player]) + context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) + } + + def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.Corpses.contains(entry.obj) + + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { + GUIDTask.UnregisterPlayer(entry.obj.asInstanceOf[Player])(entry.zone.GUID) + } +} diff --git a/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala b/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala new file mode 100644 index 000000000..ce91fdf81 --- /dev/null +++ b/common/src/main/scala/services/avatar/support/DroppedItemRemover.scala @@ -0,0 +1,33 @@ +// Copyright (c) 2017 PSForever +package services.avatar.support + +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import services.{RemoverActor, Service} +import services.avatar.{AvatarAction, AvatarServiceMessage} + +import scala.concurrent.duration._ + +class DroppedItemRemover extends RemoverActor { + final val FirstStandardDuration : FiniteDuration = 3 minutes + + final val SecondStandardDuration : FiniteDuration = 500 milliseconds + + def InclusionTest(entry : RemoverActor.Entry) : Boolean = { + entry.obj.isInstanceOf[Equipment] + } + + def InitialJob(entry : RemoverActor.Entry) : Unit = { } + + def FirstJob(entry : RemoverActor.Entry) : Unit = { + import net.psforever.objects.zones.Zone + entry.zone.Ground ! Zone.Ground.PickupItem(entry.obj.GUID) + context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID)) + } + + def ClearanceTest(entry : RemoverActor.Entry) : Boolean = !entry.zone.EquipmentOnGround.contains(entry.obj) + + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { + GUIDTask.UnregisterEquipment(entry.obj.asInstanceOf[Equipment])(entry.zone.GUID) + } +} diff --git a/common/src/main/scala/services/galaxy/GalaxyAction.scala b/common/src/main/scala/services/galaxy/GalaxyAction.scala new file mode 100644 index 000000000..7bb9c7944 --- /dev/null +++ b/common/src/main/scala/services/galaxy/GalaxyAction.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2017 PSForever +package services.galaxy + +import net.psforever.packet.game.{BuildingInfoUpdateMessage} + +object GalaxyAction { + trait Action + + final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Action +} diff --git a/common/src/main/scala/services/galaxy/GalaxyResponse.scala b/common/src/main/scala/services/galaxy/GalaxyResponse.scala new file mode 100644 index 000000000..8026085fc --- /dev/null +++ b/common/src/main/scala/services/galaxy/GalaxyResponse.scala @@ -0,0 +1,10 @@ +// Copyright (c) 2017 PSForever +package services.galaxy + +import net.psforever.packet.game.{BuildingInfoUpdateMessage} + +object GalaxyResponse { + trait Response + + final case class MapUpdate(msg: BuildingInfoUpdateMessage) extends Response +} diff --git a/common/src/main/scala/services/galaxy/GalaxyService.scala b/common/src/main/scala/services/galaxy/GalaxyService.scala new file mode 100644 index 000000000..262d64b47 --- /dev/null +++ b/common/src/main/scala/services/galaxy/GalaxyService.scala @@ -0,0 +1,49 @@ +// Copyright (c) 2017 PSForever +package services.galaxy + +import akka.actor.{Actor, Props} +import net.psforever.packet.game.BuildingInfoUpdateMessage +import services.local.support.{DoorCloseActor, HackClearActor} +import services.{GenericEventBus, Service} + +class GalaxyService extends Actor { + private [this] val log = org.log4s.getLogger + + override def preStart = { + log.info("Starting...") + } + + val GalaxyEvents = new GenericEventBus[GalaxyServiceResponse] + + def receive = { + // Service.Join requires a channel to be passed in normally but GalaxyService is an exception in that messages go to ALL connected players + case Service.Join(_) => + val path = s"/Galaxy" + val who = sender() + log.info(s"$who has joined $path") + GalaxyEvents.subscribe(who, path) + + case Service.Leave(None) => + GalaxyEvents.unsubscribe(sender()) + + case Service.Leave(_) => + val path = s"/Galaxy" + val who = sender() + log.info(s"$who has left $path") + GalaxyEvents.unsubscribe(who, path) + + case Service.LeaveAll() => + GalaxyEvents.unsubscribe(sender()) + + case GalaxyServiceMessage(action) => + action match { + case GalaxyAction.MapUpdate(msg: BuildingInfoUpdateMessage) => + GalaxyEvents.publish( + GalaxyServiceResponse(s"/Galaxy", GalaxyResponse.MapUpdate(msg)) + ) + case _ => ; + } + case msg => + log.info(s"Unhandled message $msg from $sender") + } +} diff --git a/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala b/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala new file mode 100644 index 000000000..a013af5e4 --- /dev/null +++ b/common/src/main/scala/services/galaxy/GalaxyServiceMessage.scala @@ -0,0 +1,4 @@ +// Copyright (c) 2017 PSForever +package services.galaxy + +final case class GalaxyServiceMessage(actionMessage : GalaxyAction.Action) diff --git a/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala b/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala new file mode 100644 index 000000000..28125a899 --- /dev/null +++ b/common/src/main/scala/services/galaxy/GalaxyServiceResponse.scala @@ -0,0 +1,9 @@ +// Copyright (c) 2017 PSForever +package services.galaxy + +import net.psforever.packet.game.PlanetSideGUID +import services.GenericEventBusMsg + +final case class GalaxyServiceResponse(toChannel : String, + replyMessage : GalaxyResponse.Response + ) extends GenericEventBusMsg diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/common/src/main/scala/services/local/LocalAction.scala similarity index 100% rename from pslogin/src/main/scala/services/local/LocalAction.scala rename to common/src/main/scala/services/local/LocalAction.scala diff --git a/pslogin/src/main/scala/services/local/LocalResponse.scala b/common/src/main/scala/services/local/LocalResponse.scala similarity index 100% rename from pslogin/src/main/scala/services/local/LocalResponse.scala rename to common/src/main/scala/services/local/LocalResponse.scala diff --git a/pslogin/src/main/scala/services/local/LocalService.scala b/common/src/main/scala/services/local/LocalService.scala similarity index 100% rename from pslogin/src/main/scala/services/local/LocalService.scala rename to common/src/main/scala/services/local/LocalService.scala index 5c01ee1a4..2d2570ce9 100644 --- a/pslogin/src/main/scala/services/local/LocalService.scala +++ b/common/src/main/scala/services/local/LocalService.scala @@ -2,8 +2,8 @@ package services.local import akka.actor.{Actor, Props} -import services.local.support.{DoorCloseActor, HackClearActor} import services.{GenericEventBus, Service} +import services.local.support.{DoorCloseActor, HackClearActor} class LocalService extends Actor { private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer") diff --git a/pslogin/src/main/scala/services/local/LocalServiceMessage.scala b/common/src/main/scala/services/local/LocalServiceMessage.scala similarity index 100% rename from pslogin/src/main/scala/services/local/LocalServiceMessage.scala rename to common/src/main/scala/services/local/LocalServiceMessage.scala diff --git a/pslogin/src/main/scala/services/local/LocalServiceResponse.scala b/common/src/main/scala/services/local/LocalServiceResponse.scala similarity index 100% rename from pslogin/src/main/scala/services/local/LocalServiceResponse.scala rename to common/src/main/scala/services/local/LocalServiceResponse.scala diff --git a/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala b/common/src/main/scala/services/local/support/DoorCloseActor.scala similarity index 100% rename from pslogin/src/main/scala/services/local/support/DoorCloseActor.scala rename to common/src/main/scala/services/local/support/DoorCloseActor.scala diff --git a/pslogin/src/main/scala/services/local/support/HackClearActor.scala b/common/src/main/scala/services/local/support/HackClearActor.scala similarity index 100% rename from pslogin/src/main/scala/services/local/support/HackClearActor.scala rename to common/src/main/scala/services/local/support/HackClearActor.scala diff --git a/pslogin/src/main/scala/services/vehicle/VehicleAction.scala b/common/src/main/scala/services/vehicle/VehicleAction.scala similarity index 97% rename from pslogin/src/main/scala/services/vehicle/VehicleAction.scala rename to common/src/main/scala/services/vehicle/VehicleAction.scala index 44b1002f4..e5d623539 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleAction.scala +++ b/common/src/main/scala/services/vehicle/VehicleAction.scala @@ -12,7 +12,6 @@ import net.psforever.types.{BailType, DriveState, Vector3} object VehicleAction { trait Action - final case class Awareness(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID) extends Action final case class ChildObjectState(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, pitch : Float, yaw : Float) extends Action final case class DeployRequest(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, state : DriveState.Value, unk1 : Int, unk2 : Boolean, pos : Vector3) extends Action final case class DismountVehicle(player_guid : PlanetSideGUID, bailType : BailType.Value, unk2 : Boolean) extends Action @@ -21,6 +20,7 @@ object VehicleAction { final case class KickPassenger(player_guid : PlanetSideGUID, unk1 : Int, unk2 : Boolean, vehicle_guid : PlanetSideGUID) extends Action final case class LoadVehicle(player_guid : PlanetSideGUID, vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Action final case class MountVehicle(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, seat : Int) extends Action + final case class Ownership(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID) extends Action final case class SeatPermissions(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Action final case class StowEquipment(player_guid : PlanetSideGUID, vehicle_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action final case class UnloadVehicle(player_guid : PlanetSideGUID, continent : Zone, vehicle : Vehicle) extends Action diff --git a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala b/common/src/main/scala/services/vehicle/VehicleResponse.scala similarity index 97% rename from pslogin/src/main/scala/services/vehicle/VehicleResponse.scala rename to common/src/main/scala/services/vehicle/VehicleResponse.scala index b0ac6cbcf..d34a0e2e3 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleResponse.scala +++ b/common/src/main/scala/services/vehicle/VehicleResponse.scala @@ -12,7 +12,6 @@ object VehicleResponse { trait Response final case class AttachToRails(vehicle_guid : PlanetSideGUID, rails_guid : PlanetSideGUID) extends Response - final case class Awareness(vehicle_guid : PlanetSideGUID) extends Response final case class ChildObjectState(object_guid : PlanetSideGUID, pitch : Float, yaw : Float) extends Response final case class ConcealPlayer(player_guid : PlanetSideGUID) extends Response final case class DeployRequest(object_guid : PlanetSideGUID, state : DriveState.Value, unk1 : Int, unk2 : Boolean, pos : Vector3) extends Response @@ -23,6 +22,7 @@ object VehicleResponse { final case class KickPassenger(seat_num : Int, kickedByDriver : Boolean, vehicle_guid : PlanetSideGUID) extends Response final case class LoadVehicle(vehicle : Vehicle, vtype : Int, vguid : PlanetSideGUID, vdata : ConstructorData) extends Response final case class MountVehicle(object_guid : PlanetSideGUID, seat : Int) extends Response + final case class Ownership(vehicle_guid : PlanetSideGUID) extends Response final case class ResetSpawnPad(pad_guid : PlanetSideGUID) extends Response final case class RevealPlayer(player_guid : PlanetSideGUID) extends Response final case class SeatPermissions(vehicle_guid : PlanetSideGUID, seat_group : Int, permission : Long) extends Response diff --git a/pslogin/src/main/scala/services/vehicle/VehicleService.scala b/common/src/main/scala/services/vehicle/VehicleService.scala similarity index 81% rename from pslogin/src/main/scala/services/vehicle/VehicleService.scala rename to common/src/main/scala/services/vehicle/VehicleService.scala index 5f4faf352..aaa8de472 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleService.scala +++ b/common/src/main/scala/services/vehicle/VehicleService.scala @@ -4,15 +4,14 @@ package services.vehicle import akka.actor.{Actor, ActorRef, Props} import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.zones.Zone -import services.vehicle.support.{DeconstructionActor, DelayedDeconstructionActor} +import services.vehicle.support.VehicleRemover import net.psforever.types.DriveState +import services.{GenericEventBus, RemoverActor, Service} -import services.{GenericEventBus, Service} +import scala.concurrent.duration._ class VehicleService extends Actor { - private val vehicleDecon : ActorRef = context.actorOf(Props[DeconstructionActor], "vehicle-decon-agent") - private val vehicleDelayedDecon : ActorRef = context.actorOf(Props[DelayedDeconstructionActor], "vehicle-delayed-decon-agent") - vehicleDecon ! DeconstructionActor.RequestTaskResolver + private val vehicleDecon : ActorRef = context.actorOf(Props[VehicleRemover], "vehicle-decon-agent") private [this] val log = org.log4s.getLogger override def preStart = { @@ -42,10 +41,6 @@ class VehicleService extends Actor { case VehicleServiceMessage(forChannel, action) => action match { - case VehicleAction.Awareness(player_guid, vehicle_guid) => - VehicleEvents.publish( - VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.Awareness(vehicle_guid)) - ) case VehicleAction.ChildObjectState(player_guid, object_guid, pitch, yaw) => VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.ChildObjectState(object_guid, pitch, yaw)) @@ -78,6 +73,10 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.MountVehicle(vehicle_guid, seat)) ) + case VehicleAction.Ownership(player_guid, vehicle_guid) => + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.Ownership(vehicle_guid)) + ) case VehicleAction.SeatPermissions(player_guid, vehicle_guid, seat_group, permission) => VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.SeatPermissions(vehicle_guid, seat_group, permission)) @@ -87,6 +86,11 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.StowEquipment(vehicle_guid, slot, definition.ObjectId, item.GUID, definition.Packet.DetailedConstructorData(item).get)) ) + case VehicleAction.UnloadVehicle(player_guid, continent, vehicle) => + vehicleDecon ! RemoverActor.ClearSpecific(List(vehicle), continent) //precaution + VehicleEvents.publish( + VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.UnloadVehicle(vehicle.GUID)) + ) case VehicleAction.UnstowEquipment(player_guid, item_guid) => VehicleEvents.publish( VehicleServiceResponse(s"/$forChannel/Vehicle", player_guid, VehicleResponse.UnstowEquipment(item_guid)) @@ -106,23 +110,9 @@ class VehicleService extends Actor { case _ => ; } - //message to DeconstructionActor - case VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent) => - vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, continent) - - //message to DelayedDeconstructionActor - case VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, zone, timeAlive) => - vehicleDelayedDecon ! DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, timeAlive) - - //message to DelayedDeconstructionActor - case VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) => - vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle_guid) - - //response from DeconstructionActor - case DeconstructionActor.DeleteVehicle(vehicle_guid, zone_id) => - VehicleEvents.publish( - VehicleServiceResponse(s"/$zone_id/Vehicle", Service.defaultPlayerGUID, VehicleResponse.UnloadVehicle(vehicle_guid)) - ) + //message to VehicleRemover + case VehicleServiceMessage.Decon(msg) => + vehicleDecon forward msg //from VehicleSpawnControl case VehicleSpawnPad.ConcealPlayer(player_guid, zone_id) => @@ -163,13 +153,12 @@ class VehicleService extends Actor { VehicleEvents.publish( VehicleServiceResponse(s"/${zone.Id}/Vehicle", Service.defaultPlayerGUID, VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata)) ) - vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vguid) - vehicleDelayedDecon ! DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, 600L) //10min + //avoid unattended vehicle spawning blocking the pad; user should mount (and does so normally) to reset decon timer + vehicleDecon forward RemoverActor.AddTask(vehicle, zone, Some(30 seconds)) //from VehicleSpawnControl case VehicleSpawnPad.DisposeVehicle(vehicle, zone) => - vehicleDelayedDecon ! DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle.GUID) - vehicleDecon ! DeconstructionActor.RequestDeleteVehicle(vehicle, zone) + vehicleDecon forward RemoverActor.HurrySpecific(List(vehicle), zone) //correspondence from WorldSessionActor case VehicleServiceMessage.AMSDeploymentChange(zone) => diff --git a/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala similarity index 59% rename from pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala rename to common/src/main/scala/services/vehicle/VehicleServiceMessage.scala index 61ec68542..8ed071e08 100644 --- a/pslogin/src/main/scala/services/vehicle/VehicleServiceMessage.scala +++ b/common/src/main/scala/services/vehicle/VehicleServiceMessage.scala @@ -3,16 +3,14 @@ package services.vehicle import net.psforever.objects.Vehicle import net.psforever.objects.zones.Zone -import net.psforever.packet.game.PlanetSideGUID final case class VehicleServiceMessage(forChannel : String, actionMessage : VehicleAction.Action) object VehicleServiceMessage { - final case class DelayedVehicleDeconstruction(vehicle : Vehicle, continent : Zone, timeAlive : Long) final case class GiveActorControl(vehicle : Vehicle, actorName : String) final case class RevokeActorControl(vehicle : Vehicle) - final case class RequestDeleteVehicle(vehicle : Vehicle, continent : Zone) - final case class UnscheduleDeconstruction(vehicle_guid : PlanetSideGUID) + + final case class Decon(msg : Any) final case class AMSDeploymentChange(zone : Zone) } diff --git a/pslogin/src/main/scala/services/vehicle/VehicleServiceResponse.scala b/common/src/main/scala/services/vehicle/VehicleServiceResponse.scala similarity index 100% rename from pslogin/src/main/scala/services/vehicle/VehicleServiceResponse.scala rename to common/src/main/scala/services/vehicle/VehicleServiceResponse.scala diff --git a/common/src/main/scala/services/vehicle/support/VehicleRemover.scala b/common/src/main/scala/services/vehicle/support/VehicleRemover.scala new file mode 100644 index 000000000..b60c9356b --- /dev/null +++ b/common/src/main/scala/services/vehicle/support/VehicleRemover.scala @@ -0,0 +1,56 @@ +// Copyright (c) 2017 PSForever +package services.vehicle.support + +import net.psforever.objects.Vehicle +import net.psforever.objects.guid.{GUIDTask, TaskResolver} +import net.psforever.objects.zones.Zone +import services.{RemoverActor, Service} +import services.vehicle.{VehicleAction, VehicleServiceMessage} + +import scala.concurrent.duration._ + +class VehicleRemover extends RemoverActor { + final val FirstStandardDuration : FiniteDuration = 5 minutes + + final val SecondStandardDuration : FiniteDuration = 5 seconds + + def InclusionTest(entry : RemoverActor.Entry) : Boolean = { + entry.obj.isInstanceOf[Vehicle] + } + + def InitialJob(entry : RemoverActor.Entry) : Unit = { } + + def FirstJob(entry : RemoverActor.Entry) : Unit = { + val vehicle = entry.obj.asInstanceOf[Vehicle] + val vehicleGUID = vehicle.GUID + val zoneId = entry.zone.Id + vehicle.Actor ! Vehicle.PrepareForDeletion + //kick out all passengers + vehicle.Definition.MountPoints.values.foreach(mount => { + val seat = vehicle.Seat(mount).get + seat.Occupant match { + case Some(tplayer) => + seat.Occupant = None + tplayer.VehicleSeated = None + if(tplayer.HasGUID) { + context.parent ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(tplayer.GUID, 4, false, vehicleGUID)) + } + case None => ; + } + }) + } + + override def SecondJob(entry : RemoverActor.Entry) : Unit = { + val vehicle = entry.obj.asInstanceOf[Vehicle] + val zone = entry.zone + zone.Transport ! Zone.Vehicle.Despawn(vehicle) + context.parent ! VehicleServiceMessage(zone.Id, VehicleAction.UnloadVehicle(Service.defaultPlayerGUID, zone, vehicle)) + super.SecondJob(entry) + } + + def ClearanceTest(entry : RemoverActor.Entry) : Boolean = entry.obj.asInstanceOf[Vehicle].Seats.values.count(_.isOccupied) == 0 + + def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { + GUIDTask.UnregisterVehicle(entry.obj.asInstanceOf[Vehicle])(entry.zone.GUID) + } +} diff --git a/common/src/test/scala/game/ActionResultMessageTest.scala b/common/src/test/scala/game/ActionResultMessageTest.scala index a6bce43c7..29c9d950f 100644 --- a/common/src/test/scala/game/ActionResultMessageTest.scala +++ b/common/src/test/scala/game/ActionResultMessageTest.scala @@ -38,7 +38,7 @@ class ActionResultMessageTest extends Specification { } "encode (pass, minimal)" in { - val msg = ActionResultMessage() + val msg = ActionResultMessage.Pass val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_pass @@ -52,7 +52,7 @@ class ActionResultMessageTest extends Specification { } "encode (fail, minimal)" in { - val msg = ActionResultMessage(1) + val msg = ActionResultMessage.Fail(1) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_fail diff --git a/common/src/test/scala/game/CharacterCreateRequestMessageTest.scala b/common/src/test/scala/game/CharacterCreateRequestMessageTest.scala index 416ac237f..f18406695 100644 --- a/common/src/test/scala/game/CharacterCreateRequestMessageTest.scala +++ b/common/src/test/scala/game/CharacterCreateRequestMessageTest.scala @@ -4,7 +4,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import scodec.bits._ class CharacterCreateRequestMessageTest extends Specification { @@ -15,7 +15,7 @@ class CharacterCreateRequestMessageTest extends Specification { case CharacterCreateRequestMessage(name, head, voice, gender, faction) => name mustEqual "TestChar" head mustEqual 50 - voice mustEqual 5 + voice mustEqual CharacterVoice.Voice5 gender mustEqual CharacterGender.Female faction mustEqual PlanetSideEmpire.NC case _ => @@ -24,7 +24,7 @@ class CharacterCreateRequestMessageTest extends Specification { } "encode" in { - val msg = CharacterCreateRequestMessage("TestChar", 50, 5, CharacterGender.Female, PlanetSideEmpire.NC) + val msg = CharacterCreateRequestMessage("TestChar", 50, CharacterVoice.Voice5, CharacterGender.Female, PlanetSideEmpire.NC) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/common/src/test/scala/game/ObjectDetachMessageTest.scala b/common/src/test/scala/game/ObjectDetachMessageTest.scala index f7ee8b525..a5266513a 100644 --- a/common/src/test/scala/game/ObjectDetachMessageTest.scala +++ b/common/src/test/scala/game/ObjectDetachMessageTest.scala @@ -26,10 +26,24 @@ class ObjectDetachMessageTest extends Specification { } } - "encode" in { + "encode (1)" in { val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), 0f, 0f, 270f) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string } + + "encode (2)" in { + val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), Vector3(0f, 0f, 270f)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } + + "encode (3)" in { + val msg = ObjectDetachMessage(PlanetSideGUID(2916), PlanetSideGUID(2502), Vector3(3567.1406f, 2988.0078f, 71.84375f), 270f) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } } diff --git a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala index 21d1cee08..6115f2311 100644 --- a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala +++ b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala @@ -9,218 +9,264 @@ import org.specs2.mutable._ import scodec.bits._ class CharacterDataTest extends Specification { - val string_character = hex"17 73070000 BC8 3E0F 6C2D7 65535 CA16 00 00 09 9741E4F804000000 234530063007200610077006E00790052006F006E006E0069006500 220B7 E67B540404001000000000022B50100 268042006C00610063006B002000420065007200650074002000410072006D006F007500720065006400200043006F00720070007300 1700E0030050040003BC00000234040001A004000 3FFF67A8F A0A5424E0E800000000080952A9C3A03000001081103E040000000A023782F1080C0000016244108200000000808382403A030000014284C3A0C0000000202512F00B80C00000578F80F840000000280838B3C320300000080" - val string_character_backpack = hex"17 9C030000 BC8 340D F20A9 3956C AF0D 00 00 73 480000 87041006E00670065006C006C006F00 4A148 0000000000000000000000005C54200 24404F0072006900670069006E0061006C00200044006900730074007200690063007400 1740180181E8000000C202000042000000D202000000010A3C00" + val string = hex"17 73070000 BC8 3E0F 6C2D7 65535 CA16 00 00 09 9741E4F804000000 234530063007200610077006E00790052006F006E006E0069006500 220B7 E67B540404001000000000022B50100 268042006C00610063006B002000420065007200650074002000410072006D006F007500720065006400200043006F00720070007300 1700E0030050040003BC00000234040001A004000 3FFF67A8F A0A5424E0E800000000080952A9C3A03000001081103E040000000A023782F1080C0000016244108200000000808382403A030000014284C3A0C0000000202512F00B80C00000578F80F840000000280838B3C320300000080" + //string seated was intentionally-produced test data + val string_seated = + hex"170307000069023c83e0f800000011a0530063007200610077006e00790052006f006e006e0069006500220b700000000000000000000000" ++ + hex"06800000268042006c00610063006b002000420065007200650074002000410072006d006f007500720065006400200043006f0072007000" ++ + hex"73001700e0030050040003bc00000234040001a00400020a8fa0a5424e0e800000000080952a9c3a03000001081103e040000000a023782f" ++ + hex"1080c0000016244108200000000808382403a030000014284c3a0c0000000202512f00b80c00000578f80f840000000280838b3c320300000080" + val string_backpack = hex"17 9C030000 BC8 340D F20A9 3956C AF0D 00 00 73 480000 87041006E00670065006C006C006F00 4A148 0000000000000000000000005C54200 24404F0072006900670069006E0061006C00200044006900730074007200690063007400 1740180181E8000000C202000042000000D202000000010A3C00" "CharacterData" should { "decode" in { - PacketCoding.DecodePacket(string_character).require match { + PacketCoding.DecodePacket(string).require match { case ObjectCreateMessage(len, cls, guid, parent, data) => len mustEqual 1907 cls mustEqual ObjectClass.avatar guid mustEqual PlanetSideGUID(3902) parent.isDefined mustEqual false - data.isDefined mustEqual true - data.get.isInstanceOf[CharacterData] mustEqual true - val pc = data.get.asInstanceOf[CharacterData] - pc.appearance.pos.coord.x mustEqual 3674.8438f - pc.appearance.pos.coord.y mustEqual 2726.789f - pc.appearance.pos.coord.z mustEqual 91.15625f - pc.appearance.pos.orient.x mustEqual 0f - pc.appearance.pos.orient.y mustEqual 0f - pc.appearance.pos.orient.z mustEqual 64.6875f - pc.appearance.pos.vel.isDefined mustEqual true - pc.appearance.pos.vel.get.x mustEqual 1.4375f - pc.appearance.pos.vel.get.y mustEqual -0.4375f - pc.appearance.pos.vel.get.z mustEqual 0f - pc.appearance.basic_appearance.name mustEqual "ScrawnyRonnie" - pc.appearance.basic_appearance.faction mustEqual PlanetSideEmpire.TR - pc.appearance.basic_appearance.sex mustEqual CharacterGender.Male - pc.appearance.basic_appearance.head mustEqual 5 - pc.appearance.basic_appearance.voice mustEqual 5 - pc.appearance.voice2 mustEqual 3 - pc.appearance.black_ops mustEqual false - pc.appearance.jammered mustEqual false - pc.appearance.exosuit mustEqual ExoSuitType.Reinforced - pc.appearance.outfit_name mustEqual "Black Beret Armoured Corps" - pc.appearance.outfit_logo mustEqual 23 - pc.appearance.facingPitch mustEqual 340.3125f - pc.appearance.facingYawUpper mustEqual 0 - pc.appearance.lfs mustEqual false - pc.appearance.grenade_state mustEqual GrenadeState.None - pc.appearance.is_cloaking mustEqual false - pc.appearance.charging_pose mustEqual false - pc.appearance.on_zipline mustEqual false - pc.appearance.ribbons.upper mustEqual MeritCommendation.MarkovVeteran - pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4 - pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster7 - pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR - pc.health mustEqual 255 - pc.armor mustEqual 253 - pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade - pc.command_rank mustEqual 5 - pc.implant_effects.isDefined mustEqual true - pc.implant_effects.get mustEqual ImplantEffects.NoEffects - pc.cosmetics.isDefined mustEqual true - pc.cosmetics.get.no_helmet mustEqual true - pc.cosmetics.get.beret mustEqual true - pc.cosmetics.get.sunglasses mustEqual true - pc.cosmetics.get.earpiece mustEqual true - pc.cosmetics.get.brimmed_cap mustEqual false - //short test of inventory items - pc.inventory.isDefined mustEqual true - val contents = pc.inventory.get.contents - contents.size mustEqual 5 - //0 - contents.head.objectClass mustEqual ObjectClass.plasma_grenade - contents.head.guid mustEqual PlanetSideGUID(3662) - contents.head.parentSlot mustEqual 0 - contents.head.obj.asInstanceOf[WeaponData].fire_mode mustEqual 0 - contents.head.obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.plasma_grenade_ammo - contents.head.obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3751) - //1 - contents(1).objectClass mustEqual ObjectClass.bank - contents(1).guid mustEqual PlanetSideGUID(3908) - contents(1).parentSlot mustEqual 1 - contents(1).obj.asInstanceOf[WeaponData].fire_mode mustEqual 1 - contents(1).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.armor_canister - contents(1).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(4143) - //2 - contents(2).objectClass mustEqual ObjectClass.mini_chaingun - contents(2).guid mustEqual PlanetSideGUID(4164) - contents(2).parentSlot mustEqual 2 - contents(2).obj.asInstanceOf[WeaponData].fire_mode mustEqual 0 - contents(2).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.bullet_9mm - contents(2).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3728) - //3 - contents(3).objectClass mustEqual ObjectClass.phoenix //actually, a decimator - contents(3).guid mustEqual PlanetSideGUID(3603) - contents(3).parentSlot mustEqual 3 - contents(3).obj.asInstanceOf[WeaponData].fire_mode mustEqual 0 - contents(3).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.phoenix_missile - contents(3).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3056) - //4 - contents(4).objectClass mustEqual ObjectClass.chainblade - contents(4).guid mustEqual PlanetSideGUID(4088) - contents(4).parentSlot mustEqual 4 - contents(4).obj.asInstanceOf[WeaponData].fire_mode mustEqual 1 - contents(4).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.melee_ammo - contents(4).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3279) - pc.drawn_slot mustEqual DrawnSlot.Rifle1 + data match { + case Some(PlayerData(Some(pos), basic, char, inv, hand)) => + pos.coord mustEqual Vector3(3674.8438f, 2726.789f, 91.15625f) + pos.orient mustEqual Vector3(0f, 0f, 64.6875f) + pos.vel.isDefined mustEqual true + pos.vel.get mustEqual Vector3(1.4375f, -0.4375f, 0f) + + basic.app.name mustEqual "ScrawnyRonnie" + basic.app.faction mustEqual PlanetSideEmpire.TR + basic.app.sex mustEqual CharacterGender.Male + basic.app.head mustEqual 5 + basic.app.voice mustEqual CharacterVoice.Voice5 + basic.voice2 mustEqual 3 + basic.black_ops mustEqual false + basic.jammered mustEqual false + basic.exosuit mustEqual ExoSuitType.Reinforced + basic.outfit_name mustEqual "Black Beret Armoured Corps" + basic.outfit_logo mustEqual 23 + basic.facingPitch mustEqual 340.3125f + basic.facingYawUpper mustEqual 0 + basic.lfs mustEqual false + basic.grenade_state mustEqual GrenadeState.None + basic.is_cloaking mustEqual false + basic.charging_pose mustEqual false + basic.on_zipline mustEqual false + basic.ribbons.upper mustEqual MeritCommendation.MarkovVeteran + basic.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4 + basic.ribbons.lower mustEqual MeritCommendation.TankBuster7 + basic.ribbons.tos mustEqual MeritCommendation.SixYearTR + + char.health mustEqual 255 + char.armor mustEqual 253 + char.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade + char.command_rank mustEqual 5 + char.implant_effects.isDefined mustEqual true + char.implant_effects.get mustEqual ImplantEffects.NoEffects + char.cosmetics.isDefined mustEqual true + char.cosmetics.get.no_helmet mustEqual true + char.cosmetics.get.beret mustEqual true + char.cosmetics.get.sunglasses mustEqual true + char.cosmetics.get.earpiece mustEqual true + char.cosmetics.get.brimmed_cap mustEqual false + //short test of inventory items + inv.isDefined mustEqual true + val contents = inv.get.contents + contents.size mustEqual 5 + //0 + contents.head.objectClass mustEqual ObjectClass.plasma_grenade + contents.head.guid mustEqual PlanetSideGUID(3662) + contents.head.parentSlot mustEqual 0 + contents.head.obj.asInstanceOf[WeaponData].fire_mode mustEqual 0 + contents.head.obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.plasma_grenade_ammo + contents.head.obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3751) + //1 + contents(1).objectClass mustEqual ObjectClass.bank + contents(1).guid mustEqual PlanetSideGUID(3908) + contents(1).parentSlot mustEqual 1 + contents(1).obj.asInstanceOf[WeaponData].fire_mode mustEqual 1 + contents(1).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.armor_canister + contents(1).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(4143) + //2 + contents(2).objectClass mustEqual ObjectClass.mini_chaingun + contents(2).guid mustEqual PlanetSideGUID(4164) + contents(2).parentSlot mustEqual 2 + contents(2).obj.asInstanceOf[WeaponData].fire_mode mustEqual 0 + contents(2).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.bullet_9mm + contents(2).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3728) + //3 + contents(3).objectClass mustEqual ObjectClass.phoenix //actually, a decimator + contents(3).guid mustEqual PlanetSideGUID(3603) + contents(3).parentSlot mustEqual 3 + contents(3).obj.asInstanceOf[WeaponData].fire_mode mustEqual 0 + contents(3).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.phoenix_missile + contents(3).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3056) + //4 + contents(4).objectClass mustEqual ObjectClass.chainblade + contents(4).guid mustEqual PlanetSideGUID(4088) + contents(4).parentSlot mustEqual 4 + contents(4).obj.asInstanceOf[WeaponData].fire_mode mustEqual 1 + contents(4).obj.asInstanceOf[WeaponData].ammo.head.objectClass mustEqual ObjectClass.melee_ammo + contents(4).obj.asInstanceOf[WeaponData].ammo.head.guid mustEqual PlanetSideGUID(3279) + + hand mustEqual DrawnSlot.Rifle1 + case _ => + ko + } + case _ => + ko + } + } + + "decode (seated)" in { + PacketCoding.DecodePacket(string_seated).require match { + case ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 1795 + cls mustEqual ObjectClass.avatar + guid mustEqual PlanetSideGUID(3902) + parent mustEqual Some(ObjectCreateMessageParent(PlanetSideGUID(1234), 0)) + data match { + case Some(PlayerData(None, basic, char, inv, hand)) => + basic.app.name mustEqual "ScrawnyRonnie" + basic.app.faction mustEqual PlanetSideEmpire.TR + basic.app.sex mustEqual CharacterGender.Male + basic.app.head mustEqual 5 + basic.app.voice mustEqual CharacterVoice.Voice5 + basic.voice2 mustEqual 3 + basic.black_ops mustEqual false + basic.jammered mustEqual false + basic.exosuit mustEqual ExoSuitType.Reinforced + basic.outfit_name mustEqual "Black Beret Armoured Corps" + basic.outfit_logo mustEqual 23 + basic.facingPitch mustEqual 340.3125f + basic.facingYawUpper mustEqual 0 + basic.lfs mustEqual false + basic.grenade_state mustEqual GrenadeState.None + basic.is_cloaking mustEqual false + basic.charging_pose mustEqual false + basic.on_zipline mustEqual false + basic.ribbons.upper mustEqual MeritCommendation.MarkovVeteran + basic.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4 + basic.ribbons.lower mustEqual MeritCommendation.TankBuster7 + basic.ribbons.tos mustEqual MeritCommendation.SixYearTR + //etc.. + case _ => + ko + } case _ => ko } } "decode (backpack)" in { - PacketCoding.DecodePacket(string_character_backpack).require match { + PacketCoding.DecodePacket(string_backpack).require match { case ObjectCreateMessage(len, cls, guid, parent, data) => len mustEqual 924L cls mustEqual ObjectClass.avatar guid mustEqual PlanetSideGUID(3380) parent.isDefined mustEqual false - data.isDefined mustEqual true - data.get.isInstanceOf[CharacterData] mustEqual true - val pc = data.get.asInstanceOf[CharacterData] - pc.appearance.pos.coord.x mustEqual 4629.8906f - pc.appearance.pos.coord.y mustEqual 6316.4453f - pc.appearance.pos.coord.z mustEqual 54.734375f - pc.appearance.pos.orient.x mustEqual 0f - pc.appearance.pos.orient.y mustEqual 0f - pc.appearance.pos.orient.z mustEqual 126.5625f - pc.appearance.pos.vel.isDefined mustEqual false - pc.appearance.basic_appearance.name mustEqual "Angello" - pc.appearance.basic_appearance.faction mustEqual PlanetSideEmpire.VS - pc.appearance.basic_appearance.sex mustEqual CharacterGender.Male - pc.appearance.basic_appearance.head mustEqual 10 - pc.appearance.basic_appearance.voice mustEqual 2 - pc.appearance.voice2 mustEqual 0 - pc.appearance.black_ops mustEqual false - pc.appearance.jammered mustEqual false - pc.appearance.exosuit mustEqual ExoSuitType.MAX - pc.appearance.outfit_name mustEqual "Original District" - pc.appearance.outfit_logo mustEqual 23 - pc.appearance.facingPitch mustEqual 0 - pc.appearance.facingYawUpper mustEqual 180.0f - pc.appearance.lfs mustEqual false - pc.appearance.grenade_state mustEqual GrenadeState.None - pc.appearance.is_cloaking mustEqual false - pc.appearance.charging_pose mustEqual false - pc.appearance.on_zipline mustEqual false - pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking2 - pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerVS1 - pc.appearance.ribbons.lower mustEqual MeritCommendation.AMSSupport4 - pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearVS - pc.health mustEqual 0 - pc.armor mustEqual 0 - pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade - pc.command_rank mustEqual 2 - pc.implant_effects.isDefined mustEqual false - pc.cosmetics.isDefined mustEqual true - pc.cosmetics.get.no_helmet mustEqual true - pc.cosmetics.get.beret mustEqual true - pc.cosmetics.get.sunglasses mustEqual true - pc.cosmetics.get.earpiece mustEqual true - pc.cosmetics.get.brimmed_cap mustEqual false - pc.inventory.isDefined mustEqual false - pc.drawn_slot mustEqual DrawnSlot.Pistol1 + data match { + case Some(PlayerData(Some(pos), basic, char, None, hand)) => + pos.coord mustEqual Vector3(4629.8906f, 6316.4453f, 54.734375f) + pos.orient mustEqual Vector3(0, 0, 126.5625f) + pos.vel.isDefined mustEqual false + + basic.app.name mustEqual "Angello" + basic.app.faction mustEqual PlanetSideEmpire.VS + basic.app.sex mustEqual CharacterGender.Male + basic.app.head mustEqual 10 + basic.app.voice mustEqual CharacterVoice.Voice2 + basic.voice2 mustEqual 0 + basic.black_ops mustEqual false + basic.jammered mustEqual false + basic.exosuit mustEqual ExoSuitType.MAX + basic.outfit_name mustEqual "Original District" + basic.outfit_logo mustEqual 23 + basic.facingPitch mustEqual 0 + basic.facingYawUpper mustEqual 180.0f + basic.lfs mustEqual false + basic.grenade_state mustEqual GrenadeState.None + basic.is_cloaking mustEqual false + basic.charging_pose mustEqual false + basic.on_zipline mustEqual false + basic.ribbons.upper mustEqual MeritCommendation.Jacking2 + basic.ribbons.middle mustEqual MeritCommendation.ScavengerVS1 + basic.ribbons.lower mustEqual MeritCommendation.AMSSupport4 + basic.ribbons.tos mustEqual MeritCommendation.SixYearVS + + char.health mustEqual 0 + char.armor mustEqual 0 + char.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade + char.command_rank mustEqual 2 + char.implant_effects.isDefined mustEqual false + char.cosmetics.isDefined mustEqual true + char.cosmetics.get.no_helmet mustEqual true + char.cosmetics.get.beret mustEqual true + char.cosmetics.get.sunglasses mustEqual true + char.cosmetics.get.earpiece mustEqual true + char.cosmetics.get.brimmed_cap mustEqual false + + hand mustEqual DrawnSlot.Pistol1 + case _ => + ko + } case _ => ko } } "encode" in { - val obj = CharacterData( - CharacterAppearanceData( - PlacementData( - Vector3(3674.8438f, 2726.789f, 91.15625f), - Vector3(0f, 0f, 64.6875f), - Some(Vector3(1.4375f, -0.4375f, 0f)) - ), - BasicCharacterData( - "ScrawnyRonnie", - PlanetSideEmpire.TR, - CharacterGender.Male, - 5, - 5 - ), - 3, - false, - false, - ExoSuitType.Reinforced, - "Black Beret Armoured Corps", - 23, - false, - 340.3125f, 0f, - false, - GrenadeState.None, - false, false, false, - RibbonBars( - MeritCommendation.MarkovVeteran, - MeritCommendation.HeavyInfantry4, - MeritCommendation.TankBuster7, - MeritCommendation.SixYearTR - ) + val pos : PlacementData = PlacementData( + Vector3(3674.8438f, 2726.789f, 91.15625f), + Vector3(0f, 0f, 64.6875f), + Some(Vector3(1.4375f, -0.4375f, 0f)) + ) + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData( + "ScrawnyRonnie", + PlanetSideEmpire.TR, + CharacterGender.Male, + 5, + CharacterVoice.Voice5 ), + 3, + false, + false, + ExoSuitType.Reinforced, + "Black Beret Armoured Corps", + 23, + false, + 340.3125f, 0f, + false, + GrenadeState.None, + false, false, false, + RibbonBars( + MeritCommendation.MarkovVeteran, + MeritCommendation.HeavyInfantry4, + MeritCommendation.TankBuster7, + MeritCommendation.SixYearTR + ) + ) + val char : (Boolean,Boolean)=>CharacterData = CharacterData( 255, 253, UniformStyle.ThirdUpgrade, 5, Some(ImplantEffects.NoEffects), - Some(Cosmetics(true, true, true, true, false)), - InventoryData( - InventoryItemData(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) :: - InventoryItemData(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) :: - InventoryItemData(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) :: - InventoryItemData(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) :: - InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) :: - Nil - ), - DrawnSlot.Rifle1 + Some(Cosmetics(true, true, true, true, false)) ) + val inv = InventoryData( + InventoryItemData(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) :: + Nil + ) + val obj = PlayerData(pos, app, char, inv, DrawnSlot.Rifle1) + val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3902), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt_bitv = pkt.toBitVector - val ori_bitv = string_character.toBitVector + val ori_bitv = string.toBitVector pkt_bitv.take(452) mustEqual ori_bitv.take(452) //skip 126 pkt_bitv.drop(578).take(438) mustEqual ori_bitv.drop(578).take(438) //skip 2 pkt_bitv.drop(1018).take(17) mustEqual ori_bitv.drop(1018).take(17) //skip 11 @@ -229,47 +275,99 @@ class CharacterDataTest extends Specification { //TODO work on CharacterData to make this pass as a single stream } - "encode (backpack)" in { - val obj = CharacterData( - CharacterAppearanceData( - PlacementData(4629.8906f, 6316.4453f, 54.734375f, 0f, 0f, 126.5625f), - BasicCharacterData( - "Angello", - PlanetSideEmpire.VS, - CharacterGender.Male, - 10, - 2 - ), - 0, - false, - false, - ExoSuitType.MAX, - "Original District", - 23, - true, //backpack - 0f, 180.0f, - false, - GrenadeState.None, - false, false, false, - RibbonBars( - MeritCommendation.Jacking2, - MeritCommendation.ScavengerVS1, - MeritCommendation.AMSSupport4, - MeritCommendation.SixYearVS - ) + "encode (seated)" in { + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData( + "ScrawnyRonnie", + PlanetSideEmpire.TR, + CharacterGender.Male, + 5, + CharacterVoice.Voice5 ), + 3, + false, + false, + ExoSuitType.Reinforced, + "Black Beret Armoured Corps", + 23, + false, + 340.3125f, 0f, + false, + GrenadeState.None, + false, false, false, + RibbonBars( + MeritCommendation.MarkovVeteran, + MeritCommendation.HeavyInfantry4, + MeritCommendation.TankBuster7, + MeritCommendation.SixYearTR + ) + ) + val char : (Boolean,Boolean)=>CharacterData = CharacterData( + 255, 253, + UniformStyle.ThirdUpgrade, + 5, + Some(ImplantEffects.NoEffects), + Some(Cosmetics(true, true, true, true, false)) + ) + val inv = InventoryData( + InventoryItemData(ObjectClass.plasma_grenade, PlanetSideGUID(3662), 0, WeaponData(0, 0, ObjectClass.plasma_grenade_ammo, PlanetSideGUID(3751), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.bank, PlanetSideGUID(3908), 1, WeaponData(0, 0, 1, ObjectClass.armor_canister, PlanetSideGUID(4143), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.mini_chaingun, PlanetSideGUID(4164), 2, WeaponData(0, 0, ObjectClass.bullet_9mm, PlanetSideGUID(3728), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.phoenix, PlanetSideGUID(3603), 3, WeaponData(0, 0, ObjectClass.phoenix_missile, PlanetSideGUID(3056), 0, AmmoBoxData())) :: + InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) :: + Nil + ) + val obj = PlayerData(app, char, inv, DrawnSlot.Rifle1) + + val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3902), ObjectCreateMessageParent(PlanetSideGUID(1234), 0), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + pkt mustEqual string_seated + } + + "encode (backpack)" in { + val pos = PlacementData( + Vector3(4629.8906f, 6316.4453f, 54.734375f), + Vector3(0, 0, 126.5625f) + ) + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData( + "Angello", + PlanetSideEmpire.VS, + CharacterGender.Male, + 10, + CharacterVoice.Voice2 + ), + 0, + false, + false, + ExoSuitType.MAX, + "Original District", + 23, + true, //backpack + 0f, 180.0f, + false, + GrenadeState.None, + false, false, false, + RibbonBars( + MeritCommendation.Jacking2, + MeritCommendation.ScavengerVS1, + MeritCommendation.AMSSupport4, + MeritCommendation.SixYearVS + ) + ) + val char : (Boolean,Boolean)=>CharacterData = CharacterData( 0, 0, UniformStyle.ThirdUpgrade, 2, None, - Some(Cosmetics(true, true, true, true, false)), - None, - DrawnSlot.Pistol1 + Some(Cosmetics(true, true, true, true, false)) ) + val obj = PlayerData(pos, app, char, DrawnSlot.Pistol1) + val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3380), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt_bitv = pkt.toBitVector - val ori_bitv = string_character_backpack.toBitVector + val ori_bitv = string_backpack.toBitVector pkt_bitv.take(300) mustEqual ori_bitv.take(300) //skip 2 pkt_bitv.drop(302).take(14) mustEqual ori_bitv.drop(302).take(14) //skip 126 pkt_bitv.drop(442).take(305) mustEqual ori_bitv.drop(442).take(305) //skip 1 diff --git a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala index 38a31b52d..b571749be 100644 --- a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala +++ b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala @@ -9,228 +9,378 @@ import net.psforever.types._ import scodec.bits._ class DetailedCharacterDataTest extends Specification { - val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" - val string_testchar_br32 = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0" + val string = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + val string_seated = + hex"181f0c000066d5bc84b00808000012e049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c004900" ++ + hex"6c006c0049008452700000000000000000000000000000002000000fe6a703fffffffffffffffffffffffffffffffc000000000000000000" ++ + hex"00000000000000000000019001900064000001007ec800c80000000000000000000000000000000000000001c00042c54686c70000008000" ++ + hex"0012407870655f73616e6374756172795f68656c70907870655f74685f666972656d6f6465738b757365645f6265616d6572856d61703133" ++ + hex"0000000000000000000000000000000000000000000000000000000000010a2302600404400000100006020814d0080c80000200026b4e00" ++ + hex"82880000020000c041c09e01019000006400442a001091000000400018083894402032000000801905480217200000080070298043640000" ++ + hex"32000e0540089c8000064001c0aa0119900000c8003a1580287200001900040ab805264000032006c25800a7880000020000800000" + val string_br32 = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0" "DetailedCharacterData" should { "decode" in { - PacketCoding.DecodePacket(string_testchar).require match { + PacketCoding.DecodePacket(string).require match { case ObjectCreateDetailedMessage(len, cls, guid, parent, data) => len mustEqual 3159 cls mustEqual ObjectClass.avatar guid mustEqual PlanetSideGUID(75) parent.isDefined mustEqual false - data.isDefined mustEqual true - val char = data.get.asInstanceOf[DetailedCharacterData] - char.appearance.pos.coord.x mustEqual 3674.8438f - char.appearance.pos.coord.y mustEqual 2726.789f - char.appearance.pos.coord.z mustEqual 91.15625f - char.appearance.pos.orient.x mustEqual 0 - char.appearance.pos.orient.y mustEqual 0f - char.appearance.pos.orient.z mustEqual 36.5625f - char.appearance.basic_appearance.name mustEqual "IlllIIIlllIlIllIlllIllI" - char.appearance.basic_appearance.faction mustEqual PlanetSideEmpire.VS - char.appearance.basic_appearance.sex mustEqual CharacterGender.Female - char.appearance.basic_appearance.head mustEqual 41 - char.appearance.basic_appearance.voice mustEqual 1 //female 1 - char.appearance.voice2 mustEqual 3 - char.appearance.black_ops mustEqual false - char.appearance.jammered mustEqual false - char.appearance.exosuit mustEqual ExoSuitType.Standard - char.appearance.outfit_name mustEqual "" - char.appearance.outfit_logo mustEqual 0 - char.appearance.backpack mustEqual false - char.appearance.facingPitch mustEqual 2.8125f - char.appearance.facingYawUpper mustEqual 210.9375f - char.appearance.lfs mustEqual true - char.appearance.grenade_state mustEqual GrenadeState.None - char.appearance.is_cloaking mustEqual false - char.appearance.charging_pose mustEqual false - char.appearance.on_zipline mustEqual false - char.appearance.ribbons.upper mustEqual MeritCommendation.None - char.appearance.ribbons.middle mustEqual MeritCommendation.None - char.appearance.ribbons.lower mustEqual MeritCommendation.None - char.appearance.ribbons.tos mustEqual MeritCommendation.None - char.bep mustEqual 0 - char.cep mustEqual 0 - char.healthMax mustEqual 100 - char.health mustEqual 100 - char.armor mustEqual 50 //standard exosuit value - char.unk1 mustEqual 1 - char.unk2 mustEqual 7 - char.unk3 mustEqual 7 - char.staminaMax mustEqual 100 - char.stamina mustEqual 100 - char.certs.length mustEqual 7 - char.certs.head mustEqual CertificationType.StandardAssault - char.certs(1) mustEqual CertificationType.MediumAssault - char.certs(2) mustEqual CertificationType.ATV - char.certs(3) mustEqual CertificationType.Harasser - char.certs(4) mustEqual CertificationType.StandardExoSuit - char.certs(5) mustEqual CertificationType.AgileExoSuit - char.certs(6) mustEqual CertificationType.ReinforcedExoSuit - char.implants.length mustEqual 0 - char.firstTimeEvents.size mustEqual 4 - char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" - char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" - char.firstTimeEvents(2) mustEqual "used_beamer" - char.firstTimeEvents(3) mustEqual "map13" - char.tutorials.size mustEqual 0 - char.cosmetics.isDefined mustEqual false - char.inventory.isDefined mustEqual true - val inventory = char.inventory.get.contents - inventory.size mustEqual 10 - //0 - inventory.head.objectClass mustEqual ObjectClass.beamer - inventory.head.guid mustEqual PlanetSideGUID(76) - inventory.head.parentSlot mustEqual 0 - var wep = inventory.head.obj.asInstanceOf[DetailedWeaponData] - wep.ammo.head.objectClass mustEqual ObjectClass.energy_cell - wep.ammo.head.guid mustEqual PlanetSideGUID(77) - wep.ammo.head.parentSlot mustEqual 0 - wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 16 - //1 - inventory(1).objectClass mustEqual ObjectClass.suppressor - inventory(1).guid mustEqual PlanetSideGUID(78) - inventory(1).parentSlot mustEqual 2 - wep = inventory(1).obj.asInstanceOf[DetailedWeaponData] - wep.ammo.head.objectClass mustEqual ObjectClass.bullet_9mm - wep.ammo.head.guid mustEqual PlanetSideGUID(79) - wep.ammo.head.parentSlot mustEqual 0 - wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 25 - //2 - inventory(2).objectClass mustEqual ObjectClass.forceblade - inventory(2).guid mustEqual PlanetSideGUID(80) - inventory(2).parentSlot mustEqual 4 - wep = inventory(2).obj.asInstanceOf[DetailedWeaponData] - wep.ammo.head.objectClass mustEqual ObjectClass.melee_ammo - wep.ammo.head.guid mustEqual PlanetSideGUID(81) - wep.ammo.head.parentSlot mustEqual 0 - wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1 - //3 - inventory(3).objectClass mustEqual ObjectClass.locker_container - inventory(3).guid mustEqual PlanetSideGUID(82) - inventory(3).parentSlot mustEqual 5 - inventory(3).obj.isInstanceOf[DetailedLockerContainerData] mustEqual true - inventory(3).obj.asInstanceOf[DetailedLockerContainerData].inventory.isDefined mustEqual false - //4 - inventory(4).objectClass mustEqual ObjectClass.bullet_9mm - inventory(4).guid mustEqual PlanetSideGUID(83) - inventory(4).parentSlot mustEqual 6 - inventory(4).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 - //5 - inventory(5).objectClass mustEqual ObjectClass.bullet_9mm - inventory(5).guid mustEqual PlanetSideGUID(84) - inventory(5).parentSlot mustEqual 9 - inventory(5).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 - //6 - inventory(6).objectClass mustEqual ObjectClass.bullet_9mm - inventory(6).guid mustEqual PlanetSideGUID(85) - inventory(6).parentSlot mustEqual 12 - inventory(6).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 - //7 - inventory(7).objectClass mustEqual ObjectClass.bullet_9mm_AP - inventory(7).guid mustEqual PlanetSideGUID(86) - inventory(7).parentSlot mustEqual 33 - inventory(7).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 - //8 - inventory(8).objectClass mustEqual ObjectClass.energy_cell - inventory(8).guid mustEqual PlanetSideGUID(87) - inventory(8).parentSlot mustEqual 36 - inventory(8).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 - //9 - inventory(9).objectClass mustEqual ObjectClass.remote_electronics_kit - inventory(9).guid mustEqual PlanetSideGUID(88) - inventory(9).parentSlot mustEqual 39 - //the rek has data but none worth testing here - char.drawn_slot mustEqual DrawnSlot.Pistol1 + data match { + case Some(DetailedPlayerData(Some(pos), basic, char, inv, hand)) => + pos.coord mustEqual Vector3(3674.8438f, 2726.789f, 91.15625f) + pos.orient mustEqual Vector3(0, 0, 36.5625f) + pos.vel.isDefined mustEqual false + + basic.app.name mustEqual "IlllIIIlllIlIllIlllIllI" + basic.app.faction mustEqual PlanetSideEmpire.VS + basic.app.sex mustEqual CharacterGender.Female + basic.app.head mustEqual 41 + basic.app.voice mustEqual CharacterVoice.Voice1 + basic.voice2 mustEqual 3 + basic.black_ops mustEqual false + basic.jammered mustEqual false + basic.exosuit mustEqual ExoSuitType.Standard + basic.outfit_name mustEqual "" + basic.outfit_logo mustEqual 0 + basic.backpack mustEqual false + basic.facingPitch mustEqual 2.8125f + basic.facingYawUpper mustEqual 210.9375f + basic.lfs mustEqual true + basic.grenade_state mustEqual GrenadeState.None + basic.is_cloaking mustEqual false + basic.charging_pose mustEqual false + basic.on_zipline mustEqual false + basic.ribbons.upper mustEqual MeritCommendation.None + basic.ribbons.middle mustEqual MeritCommendation.None + basic.ribbons.lower mustEqual MeritCommendation.None + basic.ribbons.tos mustEqual MeritCommendation.None + + char.bep mustEqual 0 + char.cep mustEqual 0 + char.healthMax mustEqual 100 + char.health mustEqual 100 + char.armor mustEqual 50 //standard exosuit value + char.unk1 mustEqual 1 + char.unk2 mustEqual 7 + char.unk3 mustEqual 7 + char.staminaMax mustEqual 100 + char.stamina mustEqual 100 + char.certs.length mustEqual 7 + char.certs.head mustEqual CertificationType.StandardAssault + char.certs(1) mustEqual CertificationType.MediumAssault + char.certs(2) mustEqual CertificationType.ATV + char.certs(3) mustEqual CertificationType.Harasser + char.certs(4) mustEqual CertificationType.StandardExoSuit + char.certs(5) mustEqual CertificationType.AgileExoSuit + char.certs(6) mustEqual CertificationType.ReinforcedExoSuit + char.implants.length mustEqual 0 + char.firstTimeEvents.size mustEqual 4 + char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" + char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" + char.firstTimeEvents(2) mustEqual "used_beamer" + char.firstTimeEvents(3) mustEqual "map13" + char.tutorials.size mustEqual 0 + char.cosmetics.isDefined mustEqual false + inv.isDefined mustEqual true + val inventory = inv.get.contents + inventory.size mustEqual 10 + //0 + inventory.head.objectClass mustEqual ObjectClass.beamer + inventory.head.guid mustEqual PlanetSideGUID(76) + inventory.head.parentSlot mustEqual 0 + var wep = inventory.head.obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.energy_cell + wep.ammo.head.guid mustEqual PlanetSideGUID(77) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 16 + //1 + inventory(1).objectClass mustEqual ObjectClass.suppressor + inventory(1).guid mustEqual PlanetSideGUID(78) + inventory(1).parentSlot mustEqual 2 + wep = inventory(1).obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.bullet_9mm + wep.ammo.head.guid mustEqual PlanetSideGUID(79) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 25 + //2 + inventory(2).objectClass mustEqual ObjectClass.forceblade + inventory(2).guid mustEqual PlanetSideGUID(80) + inventory(2).parentSlot mustEqual 4 + wep = inventory(2).obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.melee_ammo + wep.ammo.head.guid mustEqual PlanetSideGUID(81) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1 + //3 + inventory(3).objectClass mustEqual ObjectClass.locker_container + inventory(3).guid mustEqual PlanetSideGUID(82) + inventory(3).parentSlot mustEqual 5 + inventory(3).obj.isInstanceOf[DetailedLockerContainerData] mustEqual true + inventory(3).obj.asInstanceOf[DetailedLockerContainerData].inventory.isDefined mustEqual false + //4 + inventory(4).objectClass mustEqual ObjectClass.bullet_9mm + inventory(4).guid mustEqual PlanetSideGUID(83) + inventory(4).parentSlot mustEqual 6 + inventory(4).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //5 + inventory(5).objectClass mustEqual ObjectClass.bullet_9mm + inventory(5).guid mustEqual PlanetSideGUID(84) + inventory(5).parentSlot mustEqual 9 + inventory(5).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //6 + inventory(6).objectClass mustEqual ObjectClass.bullet_9mm + inventory(6).guid mustEqual PlanetSideGUID(85) + inventory(6).parentSlot mustEqual 12 + inventory(6).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //7 + inventory(7).objectClass mustEqual ObjectClass.bullet_9mm_AP + inventory(7).guid mustEqual PlanetSideGUID(86) + inventory(7).parentSlot mustEqual 33 + inventory(7).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //8 + inventory(8).objectClass mustEqual ObjectClass.energy_cell + inventory(8).guid mustEqual PlanetSideGUID(87) + inventory(8).parentSlot mustEqual 36 + inventory(8).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //9 + inventory(9).objectClass mustEqual ObjectClass.remote_electronics_kit + inventory(9).guid mustEqual PlanetSideGUID(88) + inventory(9).parentSlot mustEqual 39 + //the rek has data but none worth testing here + hand mustEqual DrawnSlot.Pistol1 + case _ => + ko + } + case _ => + ko + } + } + + "decode (character, seated)" in { + PacketCoding.DecodePacket(string_seated).require match { + case ObjectCreateDetailedMessage(len, cls, guid, parent, data) => + len mustEqual 3103 + cls mustEqual ObjectClass.avatar + guid mustEqual PlanetSideGUID(75) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(43981) + parent.get.slot mustEqual 0 + data match { + case Some(DetailedPlayerData(None, basic, char, inv, hand)) => + basic.app.name mustEqual "IlllIIIlllIlIllIlllIllI" + basic.app.faction mustEqual PlanetSideEmpire.VS + basic.app.sex mustEqual CharacterGender.Female + basic.app.head mustEqual 41 + basic.app.voice mustEqual CharacterVoice.Voice1 + basic.voice2 mustEqual 3 + basic.black_ops mustEqual false + basic.jammered mustEqual false + basic.exosuit mustEqual ExoSuitType.Standard + basic.outfit_name mustEqual "" + basic.outfit_logo mustEqual 0 + basic.backpack mustEqual false + basic.facingPitch mustEqual 2.8125f + basic.facingYawUpper mustEqual 210.9375f + basic.lfs mustEqual true + basic.grenade_state mustEqual GrenadeState.None + basic.is_cloaking mustEqual false + basic.charging_pose mustEqual false + basic.on_zipline mustEqual false + basic.ribbons.upper mustEqual MeritCommendation.None + basic.ribbons.middle mustEqual MeritCommendation.None + basic.ribbons.lower mustEqual MeritCommendation.None + basic.ribbons.tos mustEqual MeritCommendation.None + + char.bep mustEqual 0 + char.cep mustEqual 0 + char.healthMax mustEqual 100 + char.health mustEqual 100 + char.armor mustEqual 50 //standard exosuit value + char.unk1 mustEqual 1 + char.unk2 mustEqual 7 + char.unk3 mustEqual 7 + char.staminaMax mustEqual 100 + char.stamina mustEqual 100 + char.certs.length mustEqual 7 + char.certs.head mustEqual CertificationType.StandardAssault + char.certs(1) mustEqual CertificationType.MediumAssault + char.certs(2) mustEqual CertificationType.ATV + char.certs(3) mustEqual CertificationType.Harasser + char.certs(4) mustEqual CertificationType.StandardExoSuit + char.certs(5) mustEqual CertificationType.AgileExoSuit + char.certs(6) mustEqual CertificationType.ReinforcedExoSuit + char.implants.length mustEqual 0 + char.firstTimeEvents.size mustEqual 4 + char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" + char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" + char.firstTimeEvents(2) mustEqual "used_beamer" + char.firstTimeEvents(3) mustEqual "map13" + char.tutorials.size mustEqual 0 + char.cosmetics.isDefined mustEqual false + inv.isDefined mustEqual true + val inventory = inv.get.contents + inventory.size mustEqual 10 + //0 + inventory.head.objectClass mustEqual ObjectClass.beamer + inventory.head.guid mustEqual PlanetSideGUID(76) + inventory.head.parentSlot mustEqual 0 + var wep = inventory.head.obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.energy_cell + wep.ammo.head.guid mustEqual PlanetSideGUID(77) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 16 + //1 + inventory(1).objectClass mustEqual ObjectClass.suppressor + inventory(1).guid mustEqual PlanetSideGUID(78) + inventory(1).parentSlot mustEqual 2 + wep = inventory(1).obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.bullet_9mm + wep.ammo.head.guid mustEqual PlanetSideGUID(79) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 25 + //2 + inventory(2).objectClass mustEqual ObjectClass.forceblade + inventory(2).guid mustEqual PlanetSideGUID(80) + inventory(2).parentSlot mustEqual 4 + wep = inventory(2).obj.asInstanceOf[DetailedWeaponData] + wep.ammo.head.objectClass mustEqual ObjectClass.melee_ammo + wep.ammo.head.guid mustEqual PlanetSideGUID(81) + wep.ammo.head.parentSlot mustEqual 0 + wep.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1 + //3 + inventory(3).objectClass mustEqual ObjectClass.locker_container + inventory(3).guid mustEqual PlanetSideGUID(82) + inventory(3).parentSlot mustEqual 5 + inventory(3).obj.isInstanceOf[DetailedLockerContainerData] mustEqual true + inventory(3).obj.asInstanceOf[DetailedLockerContainerData].inventory.isDefined mustEqual false + //4 + inventory(4).objectClass mustEqual ObjectClass.bullet_9mm + inventory(4).guid mustEqual PlanetSideGUID(83) + inventory(4).parentSlot mustEqual 6 + inventory(4).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //5 + inventory(5).objectClass mustEqual ObjectClass.bullet_9mm + inventory(5).guid mustEqual PlanetSideGUID(84) + inventory(5).parentSlot mustEqual 9 + inventory(5).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //6 + inventory(6).objectClass mustEqual ObjectClass.bullet_9mm + inventory(6).guid mustEqual PlanetSideGUID(85) + inventory(6).parentSlot mustEqual 12 + inventory(6).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //7 + inventory(7).objectClass mustEqual ObjectClass.bullet_9mm_AP + inventory(7).guid mustEqual PlanetSideGUID(86) + inventory(7).parentSlot mustEqual 33 + inventory(7).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //8 + inventory(8).objectClass mustEqual ObjectClass.energy_cell + inventory(8).guid mustEqual PlanetSideGUID(87) + inventory(8).parentSlot mustEqual 36 + inventory(8).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50 + //9 + inventory(9).objectClass mustEqual ObjectClass.remote_electronics_kit + inventory(9).guid mustEqual PlanetSideGUID(88) + inventory(9).parentSlot mustEqual 39 + //the rek has data but none worth testing here + hand mustEqual DrawnSlot.Pistol1 + case _ => + ko + } case _ => ko } } "decode (BR32)" in { - PacketCoding.DecodePacket(string_testchar_br32).require match { + PacketCoding.DecodePacket(string_br32).require match { case ObjectCreateDetailedMessage(len, cls, guid, parent, data) => //this test is mainly for an alternate bitstream parsing order //the object produced is massive and most of it is already covered in other tests //only certain details towards the end of the stream will be checked - data.isDefined mustEqual true - val char = data.get.asInstanceOf[DetailedCharacterData] - DetailedCharacterData.isBR24(char.bep) mustEqual true - char.certs.size mustEqual 15 - char.certs.head mustEqual CertificationType.StandardAssault - char.certs(14) mustEqual CertificationType.CombatEngineering - char.implants.size mustEqual 3 - char.implants.head.implant mustEqual ImplantType.AudioAmplifier - char.implants.head.activation mustEqual None - char.implants(1).implant mustEqual ImplantType.Targeting - char.implants(1).activation mustEqual None - char.implants(2).implant mustEqual ImplantType.Surge - char.implants(2).activation mustEqual None - char.firstTimeEvents.size mustEqual 298 - char.firstTimeEvents.head mustEqual "xpe_overhead_map" - char.firstTimeEvents(297) mustEqual "map10" - char.tutorials.size mustEqual 3 - char.tutorials.head mustEqual "training_start_nc" - char.tutorials(1) mustEqual "training_ui" - char.tutorials(2) mustEqual "training_map" - char.cosmetics.isDefined mustEqual true - char.cosmetics.get.no_helmet mustEqual true - char.cosmetics.get.beret mustEqual true - char.cosmetics.get.earpiece mustEqual true - char.cosmetics.get.sunglasses mustEqual true - char.cosmetics.get.brimmed_cap mustEqual false - //inventory - char.inventory.isDefined mustEqual true - char.inventory.get.contents.size mustEqual 12 - //0 - char.inventory.get.contents.head.objectClass mustEqual 531 - char.inventory.get.contents.head.guid mustEqual PlanetSideGUID(4202) - char.inventory.get.contents.head.parentSlot mustEqual 0 - val wep1 = char.inventory.get.contents.head.obj.asInstanceOf[DetailedWeaponData] - wep1.unk1 mustEqual 2 - wep1.unk2 mustEqual 8 - wep1.ammo.head.objectClass mustEqual 389 - wep1.ammo.head.guid mustEqual PlanetSideGUID(3942) - wep1.ammo.head.parentSlot mustEqual 0 - wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8 - wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 100 - //4 - char.inventory.get.contents(4).objectClass mustEqual 456 - char.inventory.get.contents(4).guid mustEqual PlanetSideGUID(5374) - char.inventory.get.contents(4).parentSlot mustEqual 5 - char.inventory.get.contents(4).obj.asInstanceOf[DetailedLockerContainerData].inventory.get.contents.size mustEqual 61 - //11 - char.inventory.get.contents(11).objectClass mustEqual 673 - char.inventory.get.contents(11).guid mustEqual PlanetSideGUID(3661) - char.inventory.get.contents(11).parentSlot mustEqual 60 - val wep2 = char.inventory.get.contents(11).obj.asInstanceOf[DetailedWeaponData] - wep2.unk1 mustEqual 2 - wep2.unk2 mustEqual 8 - wep2.ammo.head.objectClass mustEqual 674 - wep2.ammo.head.guid mustEqual PlanetSideGUID(8542) - wep2.ammo.head.parentSlot mustEqual 0 - wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8 - wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 3 - char.drawn_slot mustEqual DrawnSlot.None + data match { + case Some(DetailedPlayerData(Some(_), _, char, inv, hand)) => + DetailedCharacterData.isBR24(char.bep) mustEqual true + char.certs.size mustEqual 15 + char.certs.head mustEqual CertificationType.StandardAssault + char.certs(14) mustEqual CertificationType.CombatEngineering + char.implants.size mustEqual 3 + char.implants.head.implant mustEqual ImplantType.AudioAmplifier + char.implants.head.activation mustEqual None + char.implants(1).implant mustEqual ImplantType.Targeting + char.implants(1).activation mustEqual None + char.implants(2).implant mustEqual ImplantType.Surge + char.implants(2).activation mustEqual None + char.firstTimeEvents.size mustEqual 298 + char.firstTimeEvents.head mustEqual "xpe_overhead_map" + char.firstTimeEvents(297) mustEqual "map10" + char.tutorials.size mustEqual 3 + char.tutorials.head mustEqual "training_start_nc" + char.tutorials(1) mustEqual "training_ui" + char.tutorials(2) mustEqual "training_map" + char.cosmetics.isDefined mustEqual true + char.cosmetics.get.no_helmet mustEqual true + char.cosmetics.get.beret mustEqual true + char.cosmetics.get.earpiece mustEqual true + char.cosmetics.get.sunglasses mustEqual true + char.cosmetics.get.brimmed_cap mustEqual false + //inventory + inv.isDefined mustEqual true + inv.get.contents.size mustEqual 12 + //0 + inv.get.contents.head.objectClass mustEqual 531 + inv.get.contents.head.guid mustEqual PlanetSideGUID(4202) + inv.get.contents.head.parentSlot mustEqual 0 + val wep1 = inv.get.contents.head.obj.asInstanceOf[DetailedWeaponData] + wep1.unk1 mustEqual 2 + wep1.unk2 mustEqual 8 + wep1.ammo.head.objectClass mustEqual 389 + wep1.ammo.head.guid mustEqual PlanetSideGUID(3942) + wep1.ammo.head.parentSlot mustEqual 0 + wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8 + wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 100 + //4 + inv.get.contents(4).objectClass mustEqual 456 + inv.get.contents(4).guid mustEqual PlanetSideGUID(5374) + inv.get.contents(4).parentSlot mustEqual 5 + inv.get.contents(4).obj.asInstanceOf[DetailedLockerContainerData].inventory.get.contents.size mustEqual 61 + //11 + inv.get.contents(11).objectClass mustEqual 673 + inv.get.contents(11).guid mustEqual PlanetSideGUID(3661) + inv.get.contents(11).parentSlot mustEqual 60 + val wep2 = inv.get.contents(11).obj.asInstanceOf[DetailedWeaponData] + wep2.unk1 mustEqual 2 + wep2.unk2 mustEqual 8 + wep2.ammo.head.objectClass mustEqual 674 + wep2.ammo.head.guid mustEqual PlanetSideGUID(8542) + wep2.ammo.head.parentSlot mustEqual 0 + wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8 + wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 3 + + hand mustEqual DrawnSlot.None + case _ => + ko + } case _ => ko } } - "encode (character)" in { - val app = CharacterAppearanceData( - PlacementData( - Vector3(3674.8438f, 2726.789f, 91.15625f), - Vector3(0f, 0f, 36.5625f) - ), + "encode" in { + val pos : PlacementData = PlacementData( + 3674.8438f, 2726.789f, 91.15625f, + 0, 0, 36.5625f + ) + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( BasicCharacterData( "IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, - 1 + CharacterVoice.Voice1 ), 3, false, @@ -247,19 +397,7 @@ class DetailedCharacterDataTest extends Specification { false, RibbonBars() ) - val inv = InventoryItemData(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: - InventoryItemData(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: - InventoryItemData(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: - InventoryItemData(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedLockerContainerData(8)) :: - InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: - InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: - InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: - InventoryItemData(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: - InventoryItemData(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: - InventoryItemData(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: - Nil - val obj = DetailedCharacterData( - app, + val char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData( 0, 0, 100, 100, @@ -278,15 +416,27 @@ class DetailedCharacterDataTest extends Specification { List(), "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, List.empty, - None, - Some(InventoryData(inv)), - DrawnSlot.Pistol1 + None ) + val inv = InventoryData( + InventoryItemData(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: + InventoryItemData(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: + InventoryItemData(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: + InventoryItemData(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedLockerContainerData(8)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: + Nil + ) + val obj = DetailedPlayerData.apply(pos, app, char, inv, DrawnSlot.Pistol1) + val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - val pkt_bitv = pkt.toBitVector - val ori_bitv = string_testchar.toBitVector + val ori_bitv = string.toBitVector pkt_bitv.take(153) mustEqual ori_bitv.take(153) //skip 1 pkt_bitv.drop(154).take(422) mustEqual ori_bitv.drop(154).take(422) //skip 126 pkt_bitv.drop(702).take(29) mustEqual ori_bitv.drop(702).take(29) //skip 1 @@ -294,27 +444,100 @@ class DetailedCharacterDataTest extends Specification { //TODO work on DetailedCharacterData to make this pass as a single stream } - "encode (character, br32)" in { - val obj = DetailedCharacterData( - CharacterAppearanceData( - PlacementData( - Vector3(5500.0f, 3800.0f, 71.484375f), - Vector3(0.0f, 0.0f, 90.0f), - None - ), - BasicCharacterData("KiCkJr", PlanetSideEmpire.NC, CharacterGender.Male, 24, 4), - 3, - false, false, - ExoSuitType.Agile, - "", - 14, - false, - 354.375f, 354.375f, - false, - GrenadeState.None, - false, false, false, - RibbonBars(MeritCommendation.Loser4, MeritCommendation.EventNCElite, MeritCommendation.HeavyAssault6, MeritCommendation.SixYearNC) + "encode (character, seated)" in { + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData( + "IlllIIIlllIlIllIlllIllI", + PlanetSideEmpire.VS, + CharacterGender.Female, + 41, + CharacterVoice.Voice1 ), + 3, + false, + false, + ExoSuitType.Standard, + "", + 0, + false, + 2.8125f, 210.9375f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + val char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData( + 0, + 0, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + List( + CertificationType.StandardAssault, + CertificationType.MediumAssault, + CertificationType.ATV, + CertificationType.Harasser, + CertificationType.StandardExoSuit, + CertificationType.AgileExoSuit, + CertificationType.ReinforcedExoSuit + ), + List(), + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + None + ) + val inv = InventoryData( + InventoryItemData(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(4, 8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) :: + InventoryItemData(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(4, 8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) :: + InventoryItemData(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(4, 8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) :: + InventoryItemData(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedLockerContainerData(8)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) :: + InventoryItemData(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) :: + Nil + ) + val obj = DetailedPlayerData.apply(app, char, inv, DrawnSlot.Pistol1) + //it shouldn't be Pistol1 if he's seated but it's fine for the test + + val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), ObjectCreateMessageParent(PlanetSideGUID(43981), 0), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + val pkt_bitv = pkt.toBitVector + val ori_bitv = string_seated.toBitVector + pkt_bitv mustEqual ori_bitv + } + + "encode (character, br32)" in { + val pos : PlacementData = PlacementData( + Vector3(5500.0f, 3800.0f, 71.484375f), + Vector3(0, 0, 90.0f), + None + ) + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData("KiCkJr", PlanetSideEmpire.NC, CharacterGender.Male, 24, CharacterVoice.Voice4), + 3, + false, false, + ExoSuitType.Agile, + "", + 14, + false, + 354.375f, 354.375f, + false, + GrenadeState.None, + false, false, false, + RibbonBars( + MeritCommendation.Loser4, + MeritCommendation.EventNCElite, + MeritCommendation.HeavyAssault6, + MeritCommendation.SixYearNC + ) + ) + val char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData( 6366766, 694787, 100, 100, 100, @@ -647,200 +870,199 @@ class DetailedCharacterDataTest extends Specification { "training_ui", "training_map" ), - Some(Cosmetics(true, true, true, true, false)), - Some( - InventoryData( - List( - InternalSlot(531, PlanetSideGUID(4202), 0, - DetailedWeaponData(2, 8, 0, List(InternalSlot(389, PlanetSideGUID(3942), 0,DetailedAmmoBoxData(8, 100)))) - ), - InternalSlot(132, PlanetSideGUID(6924), 1, - DetailedWeaponData(2, 8, 0, List(InternalSlot(111, PlanetSideGUID(9157), 0, DetailedAmmoBoxData(8, 100)))) - ), - InternalSlot(714, PlanetSideGUID(8498), 2, - DetailedWeaponData(2, 8, 0, List(InternalSlot(755, PlanetSideGUID(5356), 0, DetailedAmmoBoxData(8, 16)))) - ), - InternalSlot(468, PlanetSideGUID(7198), 4, - DetailedWeaponData(2, 8, 0, List(InternalSlot(540, PlanetSideGUID(5009), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(456, PlanetSideGUID(5374), 5, - DetailedLockerContainerData(8, Some(InventoryData(List( - InternalSlot(429, PlanetSideGUID(3021), 0, - DetailedWeaponData(6, 8, 0, List(InternalSlot(272, PlanetSideGUID(8729), 0, DetailedAmmoBoxData(8, 0)))) - ), - InternalSlot(838, PlanetSideGUID(8467), 9, - DetailedWeaponData(6, 8, 0, List(InternalSlot(839, PlanetSideGUID(8603), 0, DetailedAmmoBoxData(8, 5)))) - ), - InternalSlot(272, PlanetSideGUID(3266), 18, DetailedAmmoBoxData(8, 27)), - InternalSlot(577, PlanetSideGUID(2934), 22, - DetailedWeaponData(6, 8, 0, List(InternalSlot(111, PlanetSideGUID(4682), 0, DetailedAmmoBoxData(8, 100)))) - ), - InternalSlot(839, PlanetSideGUID(3271), 90, DetailedAmmoBoxData(8, 15)), - InternalSlot(839, PlanetSideGUID(7174), 94, DetailedAmmoBoxData(8, 6)), - InternalSlot(429, PlanetSideGUID(6084), 98, - DetailedWeaponData(6, 8, 0, List(InternalSlot(272, PlanetSideGUID(5928), 0, DetailedAmmoBoxData(8, 35)))) - ), - InternalSlot(462, PlanetSideGUID(5000), 108, - DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(6277), 0, DetailedAmmoBoxData(8, 150)))) - ), - InternalSlot(429, PlanetSideGUID(4341), 189, - DetailedWeaponData(6, 8, 0, List(InternalSlot(272, PlanetSideGUID(7043), 0, DetailedAmmoBoxData(8, 35)))) - ), - InternalSlot(556, PlanetSideGUID(4168), 198, - DetailedWeaponData(6, 8, 0, List(InternalSlot(28, PlanetSideGUID(8937), 0, DetailedAmmoBoxData(8, 100)))) - ), - InternalSlot(272, PlanetSideGUID(3173), 207, DetailedAmmoBoxData(8, 50)), - InternalSlot(462, PlanetSideGUID(3221), 210, - DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(4031), 0, DetailedAmmoBoxData(8, 150)))) - ), - InternalSlot(556, PlanetSideGUID(6853), 280, - DetailedWeaponData(6, 8, 0, List(InternalSlot(29, PlanetSideGUID(8524), 0, DetailedAmmoBoxData(8, 67)))) - ), - InternalSlot(556, PlanetSideGUID(4569), 290, - DetailedWeaponData(6, 8, 0, List(InternalSlot(28, PlanetSideGUID(5584), 0, DetailedAmmoBoxData(8, 100)))) - ), - InternalSlot(462, PlanetSideGUID(9294), 300, - DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(3118), 0, DetailedAmmoBoxData(8, 150)))) - ), - InternalSlot(272, PlanetSideGUID(4759), 387, DetailedAmmoBoxData(8, 50)), - InternalSlot(462, PlanetSideGUID(7377), 390, - DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(8155), 0, DetailedAmmoBoxData(8, 150)))) - ), - InternalSlot(843, PlanetSideGUID(6709), 480, DetailedAmmoBoxData(8, 1)), - InternalSlot(843, PlanetSideGUID(5276), 484, DetailedAmmoBoxData(8, 1)), - InternalSlot(843, PlanetSideGUID(7769), 488, DetailedAmmoBoxData(8, 1)), - InternalSlot(844, PlanetSideGUID(5334), 492, DetailedAmmoBoxData(8, 1)), - InternalSlot(844, PlanetSideGUID(6219), 496, DetailedAmmoBoxData(8, 1)), - InternalSlot(842, PlanetSideGUID(7279), 500, DetailedAmmoBoxData(8, 1)), - InternalSlot(842, PlanetSideGUID(5415), 504, DetailedAmmoBoxData(8, 1)), - InternalSlot(175, PlanetSideGUID(5741), 540, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5183), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(6208), 541, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5029), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(8589), 542, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(9217), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(8901), 543, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7633), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(8419), 544, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6546), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(4715), 545, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(8453), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(3577), 546, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(9202), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(6003), 547, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(3260), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(9140), 548, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(3815),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(4913), 549, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(7222),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(6954), 550, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(2953),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(6405), 551, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(4676),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(8915), 552, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(4018),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(4993), 553, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(6775),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(5053), 554, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(6418),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(9244), 555, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(3327),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(6292), 556, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(6918),0,DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(842, PlanetSideGUID(5357), 558, DetailedAmmoBoxData(8, 1)), - InternalSlot(844, PlanetSideGUID(4435), 562, DetailedAmmoBoxData(8, 1)), - InternalSlot(843, PlanetSideGUID(7242), 566, DetailedAmmoBoxData(8, 1)), - InternalSlot(175, PlanetSideGUID(7330), 570, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4786), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(7415), 571, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6536), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(3949), 572, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7526), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(3805), 573, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7358), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(4493), 574, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6852), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(5762), 575, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(3463), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(3315), 576, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7619), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(324, PlanetSideGUID(6263), 577, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5912), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(4028), 578, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(8021), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(2843), 579, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7250), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(9143), 580, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5195), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(5024), 581, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4287), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(6582), 582, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4915), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(6425), 583, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(8872), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(468, PlanetSideGUID(4431), 584, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4191), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(8339), 585, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7317), 0, DetailedAmmoBoxData(8, 1)))) - ), - InternalSlot(175, PlanetSideGUID(3277), 586, - DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6469), 0, DetailedAmmoBoxData(8, 1)))) - ) - )))) - ), - InternalSlot(213, PlanetSideGUID(6877), 6, DetailedCommandDetonaterData(4, 8)), - InternalSlot(755, PlanetSideGUID(6227), 9, DetailedAmmoBoxData(8, 16)), - InternalSlot(728, PlanetSideGUID(7181), 12, DetailedREKData(4, 16)), - InternalSlot(536, PlanetSideGUID(4077), 33, DetailedAmmoBoxData(8, 1)), - InternalSlot(680, PlanetSideGUID(4377), 37, - DetailedWeaponData(2, 8, 0, List(InternalSlot(681, PlanetSideGUID(8905), 0, DetailedAmmoBoxData(8, 3)))) - ), - InternalSlot(32, PlanetSideGUID(5523), 39, DetailedACEData(4)), - InternalSlot(673, PlanetSideGUID(3661), 60, - DetailedWeaponData(2, 8, 0, List(InternalSlot(674, PlanetSideGUID(8542), 0, DetailedAmmoBoxData(8, 3)))) - ) - ) - ) - ), - DrawnSlot.None + Some(Cosmetics(true, true, true, true, false)) ) + val inv = InventoryData( + List( + InternalSlot(531, PlanetSideGUID(4202), 0, + DetailedWeaponData(2, 8, 0, List(InternalSlot(389, PlanetSideGUID(3942), 0,DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(132, PlanetSideGUID(6924), 1, + DetailedWeaponData(2, 8, 0, List(InternalSlot(111, PlanetSideGUID(9157), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(714, PlanetSideGUID(8498), 2, + DetailedWeaponData(2, 8, 0, List(InternalSlot(755, PlanetSideGUID(5356), 0, DetailedAmmoBoxData(8, 16)))) + ), + InternalSlot(468, PlanetSideGUID(7198), 4, + DetailedWeaponData(2, 8, 0, List(InternalSlot(540, PlanetSideGUID(5009), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(456, PlanetSideGUID(5374), 5, + DetailedLockerContainerData(8, Some(InventoryData(List( + InternalSlot(429, PlanetSideGUID(3021), 0, + DetailedWeaponData(6, 8, 0, List(InternalSlot(272, PlanetSideGUID(8729), 0, DetailedAmmoBoxData(8, 0)))) + ), + InternalSlot(838, PlanetSideGUID(8467), 9, + DetailedWeaponData(6, 8, 0, List(InternalSlot(839, PlanetSideGUID(8603), 0, DetailedAmmoBoxData(8, 5)))) + ), + InternalSlot(272, PlanetSideGUID(3266), 18, DetailedAmmoBoxData(8, 27)), + InternalSlot(577, PlanetSideGUID(2934), 22, + DetailedWeaponData(6, 8, 0, List(InternalSlot(111, PlanetSideGUID(4682), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(839, PlanetSideGUID(3271), 90, DetailedAmmoBoxData(8, 15)), + InternalSlot(839, PlanetSideGUID(7174), 94, DetailedAmmoBoxData(8, 6)), + InternalSlot(429, PlanetSideGUID(6084), 98, + DetailedWeaponData(6, 8, 0, List(InternalSlot(272, PlanetSideGUID(5928), 0, DetailedAmmoBoxData(8, 35)))) + ), + InternalSlot(462, PlanetSideGUID(5000), 108, + DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(6277), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(429, PlanetSideGUID(4341), 189, + DetailedWeaponData(6, 8, 0, List(InternalSlot(272, PlanetSideGUID(7043), 0, DetailedAmmoBoxData(8, 35)))) + ), + InternalSlot(556, PlanetSideGUID(4168), 198, + DetailedWeaponData(6, 8, 0, List(InternalSlot(28, PlanetSideGUID(8937), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(272, PlanetSideGUID(3173), 207, DetailedAmmoBoxData(8, 50)), + InternalSlot(462, PlanetSideGUID(3221), 210, + DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(4031), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(556, PlanetSideGUID(6853), 280, + DetailedWeaponData(6, 8, 0, List(InternalSlot(29, PlanetSideGUID(8524), 0, DetailedAmmoBoxData(8, 67)))) + ), + InternalSlot(556, PlanetSideGUID(4569), 290, + DetailedWeaponData(6, 8, 0, List(InternalSlot(28, PlanetSideGUID(5584), 0, DetailedAmmoBoxData(8, 100)))) + ), + InternalSlot(462, PlanetSideGUID(9294), 300, + DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(3118), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(272, PlanetSideGUID(4759), 387, DetailedAmmoBoxData(8, 50)), + InternalSlot(462, PlanetSideGUID(7377), 390, + DetailedWeaponData(6, 8, 0, List(InternalSlot(463, PlanetSideGUID(8155), 0, DetailedAmmoBoxData(8, 150)))) + ), + InternalSlot(843, PlanetSideGUID(6709), 480, DetailedAmmoBoxData(8, 1)), + InternalSlot(843, PlanetSideGUID(5276), 484, DetailedAmmoBoxData(8, 1)), + InternalSlot(843, PlanetSideGUID(7769), 488, DetailedAmmoBoxData(8, 1)), + InternalSlot(844, PlanetSideGUID(5334), 492, DetailedAmmoBoxData(8, 1)), + InternalSlot(844, PlanetSideGUID(6219), 496, DetailedAmmoBoxData(8, 1)), + InternalSlot(842, PlanetSideGUID(7279), 500, DetailedAmmoBoxData(8, 1)), + InternalSlot(842, PlanetSideGUID(5415), 504, DetailedAmmoBoxData(8, 1)), + InternalSlot(175, PlanetSideGUID(5741), 540, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5183), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6208), 541, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5029), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(8589), 542, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(9217), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(8901), 543, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7633), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(8419), 544, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6546), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(4715), 545, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(8453), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(3577), 546, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(9202), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6003), 547, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(3260), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(9140), 548, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(3815),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(4913), 549, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(7222),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6954), 550, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(2953),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6405), 551, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(4676),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(8915), 552, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(4018),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(4993), 553, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(6775),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(5053), 554, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(6418),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(9244), 555, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(3327),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(6292), 556, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540,PlanetSideGUID(6918),0,DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(842, PlanetSideGUID(5357), 558, DetailedAmmoBoxData(8, 1)), + InternalSlot(844, PlanetSideGUID(4435), 562, DetailedAmmoBoxData(8, 1)), + InternalSlot(843, PlanetSideGUID(7242), 566, DetailedAmmoBoxData(8, 1)), + InternalSlot(175, PlanetSideGUID(7330), 570, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4786), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(7415), 571, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6536), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3949), 572, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7526), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3805), 573, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7358), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(4493), 574, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6852), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(5762), 575, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(3463), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3315), 576, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7619), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(324, PlanetSideGUID(6263), 577, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5912), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(4028), 578, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(8021), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(2843), 579, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7250), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(9143), 580, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(5195), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(5024), 581, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4287), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(6582), 582, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4915), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(6425), 583, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(8872), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(468, PlanetSideGUID(4431), 584, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(4191), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(8339), 585, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(7317), 0, DetailedAmmoBoxData(8, 1)))) + ), + InternalSlot(175, PlanetSideGUID(3277), 586, + DetailedWeaponData(6, 8, 0, List(InternalSlot(540, PlanetSideGUID(6469), 0, DetailedAmmoBoxData(8, 1)))) + ) + )))) + ), + InternalSlot(213, PlanetSideGUID(6877), 6, DetailedCommandDetonaterData(4, 8)), + InternalSlot(755, PlanetSideGUID(6227), 9, DetailedAmmoBoxData(8, 16)), + InternalSlot(728, PlanetSideGUID(7181), 12, DetailedREKData(4, 16)), + InternalSlot(536, PlanetSideGUID(4077), 33, DetailedAmmoBoxData(8, 1)), + InternalSlot(680, PlanetSideGUID(4377), 37, + DetailedWeaponData(2, 8, 0, List(InternalSlot(681, PlanetSideGUID(8905), 0, DetailedAmmoBoxData(8, 3)))) + ), + InternalSlot(32, PlanetSideGUID(5523), 39, DetailedACEData(4)), + InternalSlot(673, PlanetSideGUID(3661), 60, + DetailedWeaponData(2, 8, 0, List(InternalSlot(674, PlanetSideGUID(8542), 0, DetailedAmmoBoxData(8, 3)))) + ) + ) + ) + val obj = DetailedPlayerData(pos, app, char, inv, DrawnSlot.None) + val msg = ObjectCreateDetailedMessage(ObjectClass.avatar, PlanetSideGUID(75), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - val pkt_bitv = pkt.toBitVector - val ori_bitv = string_testchar_br32.toBitVector + val ori_bitv = string_br32.toBitVector + //pkt_bitv mustEqual ori_bitv pkt_bitv.take(153) mustEqual ori_bitv.take(153) //skip 1 pkt_bitv.drop(154).take(144) mustEqual ori_bitv.drop(154).take(144) //skip 24 pkt_bitv.drop(322).take(72) mustEqual ori_bitv.drop(322).take(72) //skip 24 diff --git a/common/src/test/scala/game/objectcreatevehicle/DestroyedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/DestroyedVehiclesTest.scala index fe51669fe..84b243c70 100644 --- a/common/src/test/scala/game/objectcreatevehicle/DestroyedVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/DestroyedVehiclesTest.scala @@ -4,7 +4,6 @@ package game.objectcreatevehicle import net.psforever.packet._ import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} import net.psforever.packet.game.objectcreate._ -import net.psforever.types._ import org.specs2.mutable._ import scodec.bits._ diff --git a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala new file mode 100644 index 000000000..ca939ebea --- /dev/null +++ b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -0,0 +1,192 @@ +// Copyright (c) 2017 PSForever +package game.objectcreatevehicle + +import net.psforever.packet._ +import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID} +import net.psforever.packet.game.objectcreate._ +import net.psforever.types._ +import org.specs2.mutable._ +import scodec.bits._ + +class MountedVehiclesTest extends Specification { + val string_mosquito_seated = + hex"17c70700009e2d410d8ed818f1a4017047f7ffbc6390ffbe01801cff00003c08791801d00000002340530063007200610077006e00790052" ++ + hex"006f006e006e0069006500020b7e67b540404001000000000022b50100268042006c00610063006b00200042006500720065007400200041" ++ + hex"0072006d006f007500720065006400200043006f00720070007300170040030050040003bc00000234040001a00400027a7a0809a6910800" ++ + hex"00000008090a6403603000001082202e040000000202378ae0e80c00000162710b82000000008083837032030000015e2583210000000020" ++ + hex"20e21c0c80c000007722120e81c0000000808063483603000000" + + "decode (Scrawny Ronnie's mosquito)" in { + PacketCoding.DecodePacket(string_mosquito_seated).require match { + case ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 1991 + cls mustEqual ObjectClass.mosquito + guid mustEqual PlanetSideGUID(4308) + parent mustEqual None + data match { + case Some(vdata : VehicleData) => + vdata.pos.coord mustEqual Vector3(4571.6875f, 5602.1875f, 93) + vdata.pos.orient mustEqual Vector3(11.25f, 2.8125f, 92.8125f) + vdata.pos.vel mustEqual Some(Vector3(31.71875f, 8.875f, -0.03125f)) + vdata.faction mustEqual PlanetSideEmpire.TR + vdata.bops mustEqual false + vdata.destroyed mustEqual false + vdata.jammered mustEqual false + vdata.owner_guid mustEqual PlanetSideGUID(3776) + vdata.health mustEqual 255 + vdata.no_mount_points mustEqual false + vdata.driveState mustEqual DriveState.Mobile + vdata.cloak mustEqual false + vdata.unk1 mustEqual 0 + vdata.unk2 mustEqual false + vdata.unk3 mustEqual false + vdata.unk4 mustEqual false + vdata.unk5 mustEqual false + vdata.unk6 mustEqual false + vdata.vehicle_format_data mustEqual Some(VariantVehicleData(7)) + vdata.inventory match { + case Some(InventoryData(list)) => + list.head.objectClass mustEqual ObjectClass.avatar + list.head.guid mustEqual PlanetSideGUID(3776) + list.head.parentSlot mustEqual 0 + list.head.obj match { + case PlayerData(pos, app, char, Some(InventoryData(inv)), hand) => + pos mustEqual None + app.app.name mustEqual "ScrawnyRonnie" + app.app.faction mustEqual PlanetSideEmpire.TR + app.app.sex mustEqual CharacterGender.Male + app.app.head mustEqual 5 + app.app.voice mustEqual CharacterVoice.Voice5 + app.voice2 mustEqual 3 + app.black_ops mustEqual false + app.lfs mustEqual false + app.outfit_name mustEqual "Black Beret Armoured Corps" + app.outfit_logo mustEqual 23 + app.facingPitch mustEqual 354.375f + app.facingYawUpper mustEqual 0.0f + app.altModelBit mustEqual None + app.charging_pose mustEqual false + app.on_zipline mustEqual false + app.backpack mustEqual false + app.ribbons.upper mustEqual MeritCommendation.MarkovVeteran + app.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4 + app.ribbons.lower mustEqual MeritCommendation.TankBuster7 + app.ribbons.tos mustEqual MeritCommendation.SixYearTR + char.health mustEqual 100 + char.armor mustEqual 0 + char.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade + char.command_rank mustEqual 5 + char.implant_effects mustEqual None + char.cosmetics mustEqual Some(Cosmetics(true, true, true, true, false)) + inv.size mustEqual 4 + inv.head.objectClass mustEqual ObjectClass.medicalapplicator + inv.head.parentSlot mustEqual 0 + inv(1).objectClass mustEqual ObjectClass.bank + inv(1).parentSlot mustEqual 1 + inv(2).objectClass mustEqual ObjectClass.mini_chaingun + inv(2).parentSlot mustEqual 2 + inv(3).objectClass mustEqual ObjectClass.chainblade + inv(3).parentSlot mustEqual 4 + hand mustEqual DrawnSlot.None + case _ => + ko + } + list(1).objectClass mustEqual ObjectClass.rotarychaingun_mosquito + list(1).parentSlot mustEqual 1 + case None => + ko + } + case _ => + ko + } + case _ => + ko + } + } + + "encode (Scrawny Ronnie's mosquito)" in { + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData("ScrawnyRonnie", PlanetSideEmpire.TR, CharacterGender.Male, 5, CharacterVoice.Voice5), + 3, + false, false, + ExoSuitType.Agile, + "Black Beret Armoured Corps", + 23, + false, + 354.375f, 0.0f, + false, + GrenadeState.None, false, false, false, + RibbonBars( + MeritCommendation.MarkovVeteran, + MeritCommendation.HeavyInfantry4, + MeritCommendation.TankBuster7, + MeritCommendation.SixYearTR + ) + ) + val char : (Boolean,Boolean)=>CharacterData = CharacterData( + 100, 0, + UniformStyle.ThirdUpgrade, + 0, + 5, + None, + Some(Cosmetics(true, true, true, true, false)) + ) + val inv : InventoryData = InventoryData( + List( + InternalSlot(ObjectClass.medicalapplicator, PlanetSideGUID(4201), 0, + WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.health_canister, PlanetSideGUID(3472), 0, AmmoBoxData(0)))) + ), + InternalSlot(ObjectClass.bank, PlanetSideGUID(2952), 1, + WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.armor_canister, PlanetSideGUID(3758), 0, AmmoBoxData(0)))) + ), + InternalSlot(ObjectClass.mini_chaingun, PlanetSideGUID(2929), 2, + WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.bullet_9mm, PlanetSideGUID(3292), 0, AmmoBoxData(0)))) + ), + InternalSlot(ObjectClass.chainblade, PlanetSideGUID(3222), 4, + WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.melee_ammo, PlanetSideGUID(3100), 0, AmmoBoxData(0)))) + ) + ) + ) + val player = VehicleData.PlayerData(app, char, inv, DrawnSlot.None, VehicleData.InitialStreamLengthToSeatEntries(true, VehicleFormat.Variant)) + val obj = VehicleData( + PlacementData( + Vector3(4571.6875f, 5602.1875f, 93), + Vector3(11.25f, 2.8125f, 92.8125f), + Some(Vector3(31.71875f, 8.875f, -0.03125f)) + ), + PlanetSideEmpire.TR, + false, false, + 0, + false, false, + PlanetSideGUID(3776), + false, + 255, + false, false, + DriveState.Mobile, + false, false, false, + Some(VariantVehicleData(7)), + Some( + InventoryData( + List( + InternalSlot(ObjectClass.avatar, PlanetSideGUID(3776), 0, player), + InternalSlot(ObjectClass.rotarychaingun_mosquito, PlanetSideGUID(3602), 1, + WeaponData(6, 0, 0, List(InternalSlot(ObjectClass.bullet_12mm, PlanetSideGUID(3538), 0, AmmoBoxData(0)))) + ) + ) + ) + ) + )(VehicleFormat.Variant) + val msg = ObjectCreateMessage(ObjectClass.mosquito, PlanetSideGUID(4308), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + val pkt_bitv = pkt.toBitVector + val ori_bitv = string_mosquito_seated.toBitVector + pkt_bitv.take(555) mustEqual ori_bitv.take(555) //skip 126 + pkt_bitv.drop(681).take(512) mustEqual ori_bitv.drop(681).take(512) //renew + pkt_bitv.drop(1193).take(88) mustEqual ori_bitv.drop(1193).take(88) //skip 3 + pkt_bitv.drop(1284).take(512) mustEqual ori_bitv.drop(1284).take(512) //renew + pkt_bitv.drop(1796) mustEqual ori_bitv.drop(1796) + //TODO work on CharacterData to make this pass as a single stream + } +} + diff --git a/common/src/test/scala/game/objectcreatevehicle/NormalVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/NormalVehiclesTest.scala index 9dd75b48c..5d537227c 100644 --- a/common/src/test/scala/game/objectcreatevehicle/NormalVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/NormalVehiclesTest.scala @@ -24,16 +24,12 @@ class NormalVehiclesTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[VehicleData] mustEqual true val fury = data.get.asInstanceOf[VehicleData] - fury.basic.pos.coord.x mustEqual 6531.961f - fury.basic.pos.coord.y mustEqual 1872.1406f - fury.basic.pos.coord.z mustEqual 24.734375f - fury.basic.pos.orient.x mustEqual 0f - fury.basic.pos.orient.y mustEqual 0f - fury.basic.pos.orient.z mustEqual 357.1875f - fury.basic.pos.vel.isDefined mustEqual false - fury.basic.faction mustEqual PlanetSideEmpire.VS - fury.basic.unk mustEqual 2 - fury.basic.player_guid mustEqual PlanetSideGUID(0) + fury.pos.coord mustEqual Vector3(6531.961f, 1872.1406f,24.734375f) + fury.pos.orient mustEqual Vector3(0, 0, 357.1875f) + fury.pos.vel mustEqual None + fury.faction mustEqual PlanetSideEmpire.VS + fury.unk1 mustEqual 2 + fury.owner_guid mustEqual PlanetSideGUID(0) fury.health mustEqual 255 // fury.inventory.isDefined mustEqual true @@ -69,16 +65,14 @@ class NormalVehiclesTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[VehicleData] mustEqual true val lightning = data.get.asInstanceOf[VehicleData] - lightning.basic.pos.coord.x mustEqual 3674.8438f - lightning.basic.pos.coord.y mustEqual 2726.789f - lightning.basic.pos.coord.z mustEqual 91.15625f - lightning.basic.pos.orient.x mustEqual 0f - lightning.basic.pos.orient.y mustEqual 0f - lightning.basic.pos.orient.z mustEqual 90.0f - lightning.basic.faction mustEqual PlanetSideEmpire.VS - lightning.basic.unk mustEqual 2 - lightning.basic.player_guid mustEqual PlanetSideGUID(0) + lightning.pos.coord mustEqual Vector3(3674.8438f, 2726.789f, 91.15625f) + lightning.pos.orient mustEqual Vector3(0, 0, 90) + lightning.pos.vel mustEqual None + lightning.faction mustEqual PlanetSideEmpire.VS + lightning.unk1 mustEqual 2 + lightning.owner_guid mustEqual PlanetSideGUID(0) lightning.health mustEqual 255 + lightning.inventory.isDefined mustEqual true lightning.inventory.get.contents.size mustEqual 1 val mounting = lightning.inventory.get.contents.head @@ -120,22 +114,23 @@ class NormalVehiclesTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[VehicleData] mustEqual true val deliverer = data.get.asInstanceOf[VehicleData] - deliverer.basic.pos.coord.x mustEqual 6531.961f - deliverer.basic.pos.coord.y mustEqual 1872.1406f - deliverer.basic.pos.coord.z mustEqual 24.734375f - deliverer.basic.pos.orient.x mustEqual 0f - deliverer.basic.pos.orient.y mustEqual 0f - deliverer.basic.pos.orient.z mustEqual 357.1875f - deliverer.basic.faction mustEqual PlanetSideEmpire.NC - deliverer.basic.unk mustEqual 2 - deliverer.basic.player_guid mustEqual PlanetSideGUID(0) - deliverer.unk1 mustEqual 0 + deliverer.pos.coord mustEqual Vector3(6531.961f, 1872.1406f, 24.734375f) + deliverer.pos.orient mustEqual Vector3(0, 0, 357.1875f) + deliverer.pos.vel mustEqual None + deliverer.faction mustEqual PlanetSideEmpire.NC + deliverer.owner_guid mustEqual PlanetSideGUID(0) deliverer.health mustEqual 255 - deliverer.unk2 mustEqual false deliverer.driveState mustEqual DriveState.State7 - deliverer.unk3 mustEqual true - deliverer.unk4 mustEqual None - deliverer.unk5 mustEqual false + deliverer.jammered mustEqual false + deliverer.destroyed mustEqual false + deliverer.cloak mustEqual false + deliverer.unk1 mustEqual 2 + deliverer.unk2 mustEqual false + deliverer.unk3 mustEqual false + deliverer.unk4 mustEqual false + deliverer.unk5 mustEqual true + deliverer.unk6 mustEqual false + deliverer.vehicle_format_data mustEqual None deliverer.inventory.isDefined mustEqual true deliverer.inventory.get.contents.size mustEqual 2 //0 @@ -179,11 +174,13 @@ class NormalVehiclesTest extends Specification { "encode (fury)" in { val obj = VehicleData( - CommonFieldData( - PlacementData(6531.961f, 1872.1406f, 24.734375f, 0f, 0f, 357.1875f), - PlanetSideEmpire.VS, 2 - ), - 0, + PlacementData(6531.961f, 1872.1406f, 24.734375f, 0f, 0f, 357.1875f), + PlanetSideEmpire.VS, + false, false, + 2, + false, false, + PlanetSideGUID(0), + false, 255, false, false, DriveState.Mobile, @@ -203,11 +200,13 @@ class NormalVehiclesTest extends Specification { "encode (lightning)" in { val obj = VehicleData( - CommonFieldData( - PlacementData(3674.8438f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), - PlanetSideEmpire.VS, 2 - ), - 0, + PlacementData(3674.8438f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), + PlanetSideEmpire.VS, + false, false, + 2, + false, false, + PlanetSideGUID(0), + false, 255, false, false, DriveState.Mobile, @@ -227,11 +226,13 @@ class NormalVehiclesTest extends Specification { "encode (medium transport)" in { val obj = VehicleData( - CommonFieldData( - PlacementData(6531.961f, 1872.1406f, 24.734375f, 0f, 0f, 357.1875f), - PlanetSideEmpire.NC, 2 - ), - 0, + PlacementData(6531.961f, 1872.1406f, 24.734375f, 0f, 0f, 357.1875f), + PlanetSideEmpire.NC, + false, false, + 2, + false, false, + PlanetSideGUID(0), + false, 255, false, false, DriveState.State7, diff --git a/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala index 930c7e3a6..0c081a16a 100644 --- a/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/UtilityVehiclesTest.scala @@ -11,6 +11,8 @@ import scodec.bits._ class UtilityVehiclesTest extends Specification { val string_ant = hex"17 C2000000 9E0 7C01 6C2D7 65535 CA16 00 00 00 4400003FC000000" val string_ams = hex"17 B8010000 970 3D10 002D765535CA16000000 402285BB0037E4100749E1D03000000620D83A0A00000195798741C00000332E40D84800000" +// val string_ams_seated = +// hex"17ec060000970fe0f030898abda28127f007ff9c1f2f80c0001e18ff00001051e40786400000008c50004c0041006d0069006e006700790075006500540052007c00000304217c859e8080000000000000002503420022c02a002a002a002a0050004c0041002a002a002a002a00010027e300940000016c0400023c040002285a086c2f00c80000000000300210288740800000004046f17423018000002c4d6190400000001010704a86406000002bc770842000000004041c5f21d01800000e075821902000000623e84208000001950588c1800000332ea0f840000000" "Utility vehicles" should { "decode (ant)" in { @@ -23,17 +25,21 @@ class UtilityVehiclesTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[VehicleData] mustEqual true val ant = data.get.asInstanceOf[VehicleData] - ant.basic.pos.coord.x mustEqual 3674.8438f - ant.basic.pos.coord.y mustEqual 2726.789f - ant.basic.pos.coord.z mustEqual 91.15625f - ant.basic.pos.orient.x mustEqual 0f - ant.basic.pos.orient.y mustEqual 0f - ant.basic.pos.orient.z mustEqual 90.0f - ant.basic.faction mustEqual PlanetSideEmpire.VS - ant.basic.unk mustEqual 2 - ant.basic.player_guid mustEqual PlanetSideGUID(0) - ant.health mustEqual 255 + ant.pos.coord mustEqual Vector3(3674.8438f, 2726.789f, 91.15625f) + ant.pos.orient mustEqual Vector3(0, 0, 90) + ant.faction mustEqual PlanetSideEmpire.VS + ant.owner_guid mustEqual PlanetSideGUID(0) ant.driveState mustEqual DriveState.Mobile + ant.health mustEqual 255 + ant.jammered mustEqual false + ant.destroyed mustEqual false + ant.cloak mustEqual false + ant.unk1 mustEqual 2 + ant.unk2 mustEqual false + ant.unk3 mustEqual false + ant.unk4 mustEqual false + ant.unk5 mustEqual false + ant.unk6 mustEqual false case _ => ko } @@ -49,19 +55,23 @@ class UtilityVehiclesTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[VehicleData] mustEqual true val ams = data.get.asInstanceOf[VehicleData] - ams.basic.pos.coord.x mustEqual 3674.0f - ams.basic.pos.coord.y mustEqual 2726.789f - ams.basic.pos.coord.z mustEqual 91.15625f - ams.basic.pos.orient.x mustEqual 0f - ams.basic.pos.orient.y mustEqual 0f - ams.basic.pos.orient.z mustEqual 90.0f - ams.basic.faction mustEqual PlanetSideEmpire.VS - ams.basic.unk mustEqual 0 - ams.basic.player_guid mustEqual PlanetSideGUID(34082) - ams.unk1 mustEqual 2 - ams.health mustEqual 236 - ams.unk2 mustEqual false + ams.pos.coord mustEqual Vector3(3674, 2726.789f, 91.15625f) + ams.pos.orient mustEqual Vector3(0, 0, 90) + ams.pos.vel mustEqual None + ams.faction mustEqual PlanetSideEmpire.VS + ams.owner_guid mustEqual PlanetSideGUID(2885) ams.driveState mustEqual DriveState.Deployed + ams.vehicle_format_data mustEqual Some(UtilityVehicleData(60)) + ams.health mustEqual 236 + ams.jammered mustEqual false + ams.destroyed mustEqual false + ams.cloak mustEqual true + ams.unk1 mustEqual 0 + ams.unk2 mustEqual false + ams.unk3 mustEqual false + ams.unk4 mustEqual false + ams.unk5 mustEqual false + ams.unk6 mustEqual true ams.inventory.isDefined mustEqual true val inv = ams.inventory.get.contents @@ -88,11 +98,13 @@ class UtilityVehiclesTest extends Specification { "encode (ant)" in { val obj = VehicleData( - CommonFieldData( - PlacementData(3674.8438f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), - PlanetSideEmpire.VS, 2 - ), - 0, + PlacementData(3674.8438f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), + PlanetSideEmpire.VS, + false, false, + 2, + false, false, + PlanetSideGUID(0), + false, 255, false, false, DriveState.Mobile, @@ -108,12 +120,13 @@ class UtilityVehiclesTest extends Specification { "encode (ams)" in { val obj = VehicleData( - CommonFieldData( - PlacementData(3674.0f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), - PlanetSideEmpire.VS, 0, - PlanetSideGUID(34082) - ), - 2, + PlacementData(3674.0f, 2726.789f, 91.15625f, 0f, 0f, 90.0f), + PlanetSideEmpire.VS, + false, false, + 0, + false, false, + PlanetSideGUID(2885), + false, 236, false, false, DriveState.Deployed, diff --git a/common/src/test/scala/game/objectcreatevehicle/VariantVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/VariantVehiclesTest.scala index c1203d6ce..5924cda1a 100644 --- a/common/src/test/scala/game/objectcreatevehicle/VariantVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/VariantVehiclesTest.scala @@ -22,14 +22,14 @@ class VariantVehiclesTest extends Specification { data.isDefined mustEqual true data.get.isInstanceOf[VehicleData] mustEqual true val switchblade = data.get.asInstanceOf[VehicleData] - switchblade.basic.pos.coord.x mustEqual 6531.961f - switchblade.basic.pos.coord.y mustEqual 1872.1406f - switchblade.basic.pos.coord.z mustEqual 24.734375f - switchblade.basic.pos.orient.x mustEqual 0f - switchblade.basic.pos.orient.y mustEqual 0f - switchblade.basic.pos.orient.z mustEqual 357.1875f - switchblade.basic.faction mustEqual PlanetSideEmpire.VS - switchblade.basic.unk mustEqual 2 + switchblade.pos.coord.x mustEqual 6531.961f + switchblade.pos.coord.y mustEqual 1872.1406f + switchblade.pos.coord.z mustEqual 24.734375f + switchblade.pos.orient.x mustEqual 0f + switchblade.pos.orient.y mustEqual 0f + switchblade.pos.orient.z mustEqual 357.1875f + switchblade.faction mustEqual PlanetSideEmpire.VS + switchblade.unk1 mustEqual 2 switchblade.health mustEqual 255 switchblade.driveState mustEqual DriveState.Mobile switchblade.inventory.isDefined mustEqual true @@ -61,12 +61,13 @@ class VariantVehiclesTest extends Specification { "encode (switchblade)" in { val obj = VehicleData( - CommonFieldData( - PlacementData(6531.961f, 1872.1406f, 24.734375f, 0f, 0f, 357.1875f), - PlanetSideEmpire.VS, - 2 - ), - 0, + PlacementData(6531.961f, 1872.1406f, 24.734375f, 0f, 0f, 357.1875f), + PlanetSideEmpire.VS, + false, false, + 2, + false, false, + PlanetSideGUID(0), + false, 255, false, false, DriveState.Mobile, diff --git a/common/src/test/scala/objects/AutoDriveControlsTest.scala b/common/src/test/scala/objects/AutoDriveControlsTest.scala index e26c7d9bd..e16b701eb 100644 --- a/common/src/test/scala/objects/AutoDriveControlsTest.scala +++ b/common/src/test/scala/objects/AutoDriveControlsTest.scala @@ -7,7 +7,7 @@ import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawn import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} import net.psforever.objects.serverobject.pad.process.{AutoDriveControls, VehicleSpawnControlGuided} import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification import scala.concurrent.duration._ @@ -389,7 +389,7 @@ class GuidedControlTest1 extends ActorTest { "unguided" in { val vehicle = Vehicle(GlobalDefinitions.mediumtransport) vehicle.GUID = PlanetSideGUID(1) - val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) driver.VehicleSeated = vehicle.GUID val sendTo = TestProbe() val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) @@ -411,7 +411,7 @@ class GuidedControlTest2 extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.mediumtransport) vehicle.GUID = PlanetSideGUID(1) vehicle.Velocity = Vector3(1,1,1) - val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) driver.VehicleSeated = vehicle.GUID val sendTo = TestProbe() val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) @@ -436,7 +436,7 @@ class GuidedControlTest3 extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.mediumtransport) vehicle.GUID = PlanetSideGUID(1) vehicle.Velocity = Vector3(1,1,1) - val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) driver.VehicleSeated = vehicle.GUID val sendTo = TestProbe() val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) @@ -457,7 +457,7 @@ class GuidedControlTest3 extends ActorTest { assert(msg2.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) assert(msg2.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Wait) sendTo.expectNoMsg(1000 milliseconds) - val msg3 = sendTo.receiveOne(100 milliseconds) + val msg3 = sendTo.receiveOne(300 milliseconds) assert(msg3.isInstanceOf[VehicleSpawnControlGuided.GuidedControl]) assert(msg3.asInstanceOf[VehicleSpawnControlGuided.GuidedControl].command == AutoDriveControls.State.Drive) val msg4 = sendTo.receiveOne(200 milliseconds) @@ -474,7 +474,7 @@ class GuidedControlTest4 extends ActorTest { val vehicle = Vehicle(GlobalDefinitions.mediumtransport) vehicle.GUID = PlanetSideGUID(1) vehicle.Velocity = Vector3(1,1,1) - val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0,0)) + val driver = Player(Avatar("", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) driver.VehicleSeated = vehicle.GUID val sendTo = TestProbe() val order = VehicleSpawnControl.Order(driver, vehicle, sendTo.ref) diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala index 6c3317989..c2c482c06 100644 --- a/common/src/test/scala/objects/AvatarTest.scala +++ b/common/src/test/scala/objects/AvatarTest.scala @@ -5,12 +5,12 @@ import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects._ import net.psforever.objects.loadouts._ import net.psforever.objects.definition.ImplantDefinition -import net.psforever.types.{CharacterGender, ImplantType, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, ImplantType, PlanetSideEmpire} import org.specs2.mutable._ class AvatarTest extends Specification { def CreatePlayer() : (Player, Avatar) = { - val avatar = Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + val avatar = Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) val player = Player(avatar) player.Slot(0).Equipment = Tool(beamer) @@ -26,12 +26,12 @@ class AvatarTest extends Specification { } "construct" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.name mustEqual "Chord" av.faction mustEqual PlanetSideEmpire.TR av.sex mustEqual CharacterGender.Male av.head mustEqual 0 - av.voice mustEqual 5 + av.voice mustEqual CharacterVoice.Voice5 av.BEP mustEqual 0 av.CEP mustEqual 0 av.Certifications mustEqual Set.empty @@ -39,7 +39,7 @@ class AvatarTest extends Specification { } "can maintain cumulative battle experience point values" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.BEP mustEqual 0 av.BEP = 100 av.BEP mustEqual 100 @@ -48,14 +48,14 @@ class AvatarTest extends Specification { } "can maintain battle experience point values up to a maximum (Long.MaxValue)" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.BEP mustEqual 0 av.BEP = 4294967295L av.BEP mustEqual 4294967295L } "can not maintain battle experience point values below zero" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.BEP mustEqual 0 av.BEP = -1 av.BEP mustEqual 0 @@ -66,7 +66,7 @@ class AvatarTest extends Specification { } "can maintain cumulative command experience point values" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.CEP mustEqual 0 av.CEP = 100 av.CEP mustEqual 100 @@ -75,14 +75,14 @@ class AvatarTest extends Specification { } "can maintain command experience point values up to a maximum (Long.MaxValue)" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.CEP mustEqual 0 av.CEP = 4294967295L av.CEP mustEqual 4294967295L } "can not maintain command experience point values below zero" in { - val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val av = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) av.CEP mustEqual 0 av.CEP = -1 av.CEP mustEqual 0 @@ -93,28 +93,28 @@ class AvatarTest extends Specification { } "can tell the difference between avatars" in { - (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual true + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) mustEqual true - (Avatar("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Avatar("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual false + (Avatar("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + Avatar("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) mustEqual false - (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Avatar("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5)) mustEqual false + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + Avatar("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Voice5)) mustEqual false - (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Female, 0, 5)) mustEqual false + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Female, 0, CharacterVoice.Voice5)) mustEqual false - (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 1, 5)) mustEqual false + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 1, CharacterVoice.Voice5)) mustEqual false - (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 6)) mustEqual false + (Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice4)) mustEqual false } //refer to ImplantTest.scala for more tests "maximum of three implant slots" in { - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants.length mustEqual 3 obj.Implants(0).Unlocked mustEqual false obj.Implants(0).Initialized mustEqual false @@ -140,7 +140,7 @@ class AvatarTest extends Specification { "can install an implant" in { val testplant : ImplantDefinition = ImplantDefinition(1) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.InstallImplant(testplant) mustEqual Some(0) obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant @@ -155,7 +155,7 @@ class AvatarTest extends Specification { "can install implants in sequential slots" in { val testplant1 : ImplantDefinition = ImplantDefinition(1) val testplant2 : ImplantDefinition = ImplantDefinition(2) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true @@ -166,7 +166,7 @@ class AvatarTest extends Specification { "can not install the same type of implant twice" in { val testplant1 : ImplantDefinition = ImplantDefinition(1) val testplant2 : ImplantDefinition = ImplantDefinition(1) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true @@ -178,7 +178,7 @@ class AvatarTest extends Specification { val testplant1 : ImplantDefinition = ImplantDefinition(1) val testplant2 : ImplantDefinition = ImplantDefinition(2) val testplant3 : ImplantDefinition = ImplantDefinition(3) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true @@ -192,7 +192,7 @@ class AvatarTest extends Specification { val testplant2 : ImplantDefinition = ImplantDefinition(2) val testplant3 : ImplantDefinition = ImplantDefinition(3) val testplant4 : ImplantDefinition = ImplantDefinition(4) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true obj.Implants(2).Unlocked = true @@ -205,7 +205,7 @@ class AvatarTest extends Specification { "can uninstall an implant" in { val testplant : ImplantDefinition = ImplantDefinition(1) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.InstallImplant(testplant) mustEqual Some(0) obj.Implants(0).Installed mustEqual Some(testplant) @@ -218,7 +218,7 @@ class AvatarTest extends Specification { val testplant1 : ImplantDefinition = ImplantDefinition(1) val testplant2 : ImplantDefinition = ImplantDefinition(2) val testplant3 : ImplantDefinition = ImplantDefinition(3) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true obj.Implants(2).Unlocked = true @@ -239,7 +239,7 @@ class AvatarTest extends Specification { val testplant1 : ImplantDefinition = ImplantDefinition(1) val testplant2 : ImplantDefinition = ImplantDefinition(2) val testplant3 : ImplantDefinition = ImplantDefinition(3) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true obj.Implants(2).Unlocked = true @@ -261,7 +261,7 @@ class AvatarTest extends Specification { "can reset implants to uninitialized state" in { val testplant1 : ImplantDefinition = ImplantDefinition(1) val testplant2 : ImplantDefinition = ImplantDefinition(2) - val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true obj.InstallImplant(testplant1) mustEqual Some(0) @@ -393,6 +393,6 @@ class AvatarTest extends Specification { } "toString" in { - Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5).toString mustEqual "TR Chord" + Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5).toString mustEqual "TR Chord" } } diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala index aad737ddd..b863499a2 100644 --- a/common/src/test/scala/objects/ConverterTest.scala +++ b/common/src/test/scala/objects/ConverterTest.scala @@ -11,7 +11,7 @@ import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.objectcreate._ -import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification import scala.util.{Failure, Success} @@ -154,7 +154,7 @@ class ConverterTest extends Specification { } "Player" should { - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val obj : Player = { /* Create an AmmoBoxDefinition with which to build two AmmoBoxes diff --git a/common/src/test/scala/objects/DoorTest.scala b/common/src/test/scala/objects/DoorTest.scala index 73e55c00d..76184282f 100644 --- a/common/src/test/scala/objects/DoorTest.scala +++ b/common/src/test/scala/objects/DoorTest.scala @@ -7,13 +7,13 @@ import net.psforever.objects.serverobject.doors.{Door, DoorControl} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.Zone import net.psforever.packet.game.{PlanetSideGUID, UseItemMessage} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification import scala.concurrent.duration.Duration class DoorTest extends Specification { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) "Door" should { "construct" in { @@ -123,6 +123,6 @@ object DoorControlTest { door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door") door.Owner = new Building(0, Zone.Nowhere, StructureType.Building) door.Owner.Faction = faction - (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), door) + (Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), door) } } diff --git a/common/src/test/scala/objects/IFFLockTest.scala b/common/src/test/scala/objects/IFFLockTest.scala index 5cd66613c..9ac7044d6 100644 --- a/common/src/test/scala/objects/IFFLockTest.scala +++ b/common/src/test/scala/objects/IFFLockTest.scala @@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import org.specs2.mutable.Specification class IFFLockTest extends Specification { @@ -69,6 +69,6 @@ object IFFLockControlTest { lock.Actor = system.actorOf(Props(classOf[IFFLockControl], lock), "lock-control") lock.Owner = new Building(0, Zone.Nowhere, StructureType.Building) lock.Owner.Faction = faction - (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), lock) + (Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), lock) } } diff --git a/common/src/test/scala/objects/InventoryTest.scala b/common/src/test/scala/objects/InventoryTest.scala index fb6410b75..9236ff246 100644 --- a/common/src/test/scala/objects/InventoryTest.scala +++ b/common/src/test/scala/objects/InventoryTest.scala @@ -349,5 +349,42 @@ class InventoryTest extends Specification { out(1).Definition.Tile mustEqual InventoryTile.Tile22 //did not fit ok } + + "insert items quickly (risk overwriting entries)" in { + val obj : GridInventory = GridInventory(6, 6) + (obj += 0 -> bullet9mmBox1) mustEqual true + val collision1 = obj.CheckCollisions(0,1,1) + obj.CheckCollisions(1,1,1) mustEqual collision1 + obj.CheckCollisions(2,1,1) mustEqual collision1 + obj.CheckCollisions(6,1,1) mustEqual collision1 + obj.CheckCollisions(7,1,1) mustEqual collision1 + obj.CheckCollisions(8,1,1) mustEqual collision1 + obj.CheckCollisions(12,1,1) mustEqual collision1 + obj.CheckCollisions(13,1,1) mustEqual collision1 + obj.CheckCollisions(14,1,1) mustEqual collision1 + + (obj += 7 -> bullet9mmBox2) mustEqual false //can not insert overlapping object + obj.CheckCollisions(0,1,1) mustEqual collision1 + obj.CheckCollisions(1,1,1) mustEqual collision1 + obj.CheckCollisions(2,1,1) mustEqual collision1 + obj.CheckCollisions(6,1,1) mustEqual collision1 + obj.CheckCollisions(7,1,1) mustEqual collision1 + obj.CheckCollisions(8,1,1) mustEqual collision1 + obj.CheckCollisions(12,1,1) mustEqual collision1 + obj.CheckCollisions(13,1,1) mustEqual collision1 + obj.CheckCollisions(14,1,1) mustEqual collision1 + + obj.InsertQuickly(7, bullet9mmBox2) mustEqual true //overwrite + val collision2 = obj.CheckCollisions(7,1,1) + obj.CheckCollisions(0,1,1) mustEqual collision1 + obj.CheckCollisions(1,1,1) mustEqual collision1 + obj.CheckCollisions(2,1,1) mustEqual collision1 + obj.CheckCollisions(6,1,1) mustEqual collision1 + obj.CheckCollisions(7,1,1) mustEqual collision2 + obj.CheckCollisions(8,1,1) mustEqual collision2 + obj.CheckCollisions(12,1,1) mustEqual collision1 + obj.CheckCollisions(13,1,1) mustEqual collision2 + obj.CheckCollisions(14,1,1) mustEqual collision2 + } } } diff --git a/common/src/test/scala/objects/LoadoutTest.scala b/common/src/test/scala/objects/LoadoutTest.scala index 5ddb448e2..a5b98381c 100644 --- a/common/src/test/scala/objects/LoadoutTest.scala +++ b/common/src/test/scala/objects/LoadoutTest.scala @@ -3,12 +3,12 @@ package objects import net.psforever.objects._ import net.psforever.objects.loadouts._ -import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, ExoSuitType, PlanetSideEmpire} import net.psforever.objects.GlobalDefinitions._ import org.specs2.mutable._ class LoadoutTest extends Specification { - val avatar = Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + val avatar = Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) def CreatePlayer() : Player = { new Player(avatar) { diff --git a/common/src/test/scala/objects/MountableTest.scala b/common/src/test/scala/objects/MountableTest.scala index 56fe1f831..841069d69 100644 --- a/common/src/test/scala/objects/MountableTest.scala +++ b/common/src/test/scala/objects/MountableTest.scala @@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.vehicles.Seat import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import scala.concurrent.duration.Duration @@ -25,7 +25,7 @@ class MountableControl1Test extends ActorTest() { class MountableControl2Test extends ActorTest() { "MountableControl" should { "let a player mount" in { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj = new MountableTest.MountableTestObject obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") val msg = Mountable.TryMount(player, 0) @@ -46,8 +46,8 @@ class MountableControl2Test extends ActorTest() { class MountableControl3Test extends ActorTest() { "MountableControl" should { "block a player from mounting" in { - val player1 = Player(Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) - val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player1 = Player(Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) + val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj = new MountableTest.MountableTestObject obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") obj.Actor ! Mountable.TryMount(player1, 0) diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index 560b1c524..4e4d367ac 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -6,19 +6,19 @@ import net.psforever.objects._ import net.psforever.objects.definition.{ImplantDefinition, SimpleItemDefinition} import net.psforever.objects.equipment.EquipmentSize import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire} +import net.psforever.types._ import org.specs2.mutable._ import scala.util.Success class PlayerTest extends Specification { - def TestPlayer(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = { + def TestPlayer(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : CharacterVoice.Value) : Player = { new Player(Avatar(name, faction, sex, head, voice)) } "Player" should { "construct" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.isAlive mustEqual false obj.FacingYawUpper mustEqual 0 obj.Jumping mustEqual false @@ -36,27 +36,27 @@ class PlayerTest extends Specification { } "different players" in { - (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual true + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) mustEqual true - (TestPlayer("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - TestPlayer("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) mustEqual false + (TestPlayer("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + TestPlayer("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) mustEqual false - (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5)) mustEqual false + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Voice5)) mustEqual false - (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Female, 0, 5)) mustEqual false + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Female, 0, CharacterVoice.Voice5)) mustEqual false - (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 1, 5)) mustEqual false + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 1, CharacterVoice.Voice5)) mustEqual false - (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) == - TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 6)) mustEqual false + (TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) == + TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice4)) mustEqual false } "(re)spawn" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.isAlive mustEqual false obj.Health mustEqual 0 obj.Stamina mustEqual 0 @@ -72,7 +72,7 @@ class PlayerTest extends Specification { } "will not (re)spawn if not dead" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Spawn obj.Health mustEqual 100 obj.Armor mustEqual 50 @@ -88,7 +88,7 @@ class PlayerTest extends Specification { } "can die" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Spawn obj.Armor = 35 //50 -> 35 obj.isAlive mustEqual true @@ -103,7 +103,7 @@ class PlayerTest extends Specification { } "can not become a backpack if alive" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Spawn obj.isAlive mustEqual true obj.isBackpack mustEqual false @@ -113,7 +113,7 @@ class PlayerTest extends Specification { } "can become a backpack" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.isAlive mustEqual false obj.isBackpack mustEqual false obj.Release @@ -122,7 +122,7 @@ class PlayerTest extends Specification { } "set new maximum values (health, stamina)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.MaxHealth mustEqual 100 obj.MaxStamina mustEqual 100 obj.MaxHealth = 123 @@ -133,7 +133,7 @@ class PlayerTest extends Specification { } "set new values (health, armor, stamina) but only when alive" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Health = 23 obj.Armor = 34 obj.Stamina = 45 @@ -154,14 +154,20 @@ class PlayerTest extends Specification { } "has visible slots" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) + obj.VisibleSlots mustEqual Set(0,2,4) //Standard + obj.ExoSuit = ExoSuitType.Agile + obj.VisibleSlots mustEqual Set(0,1,2,4) + obj.ExoSuit = ExoSuitType.Reinforced obj.VisibleSlots mustEqual Set(0,1,2,3,4) + obj.ExoSuit = ExoSuitType.Infiltration + obj.VisibleSlots mustEqual Set(0,4) obj.ExoSuit = ExoSuitType.MAX obj.VisibleSlots mustEqual Set(0) } "init (Standard Exo-Suit)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.ExoSuit mustEqual ExoSuitType.Standard obj.Slot(0).Size mustEqual EquipmentSize.Pistol obj.Slot(1).Size mustEqual EquipmentSize.Blocked @@ -175,7 +181,7 @@ class PlayerTest extends Specification { "draw equipped holsters only" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Slot(1).Size = EquipmentSize.Pistol obj.Slot(1).Equipment = wep obj.DrawnSlot mustEqual Player.HandsDownSlot @@ -188,7 +194,7 @@ class PlayerTest extends Specification { "remember the last drawn holster" in { val wep1 = SimpleItem(SimpleItemDefinition(149)) val wep2 = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Slot(0).Size = EquipmentSize.Pistol obj.Slot(0).Equipment = wep1 obj.Slot(1).Size = EquipmentSize.Pistol @@ -227,7 +233,7 @@ class PlayerTest extends Specification { "hold something in their free hand" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Slot(Player.FreeHandSlot).Equipment = wep obj.Slot(Player.FreeHandSlot).Equipment.get.Definition.ObjectId mustEqual 149 @@ -235,14 +241,14 @@ class PlayerTest extends Specification { "provide an invalid hand that can not hold anything" in { val wep = SimpleItem(SimpleItemDefinition(149)) - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Slot(-1).Equipment = wep obj.Slot(-1).Equipment mustEqual None } "search for the smallest available slot in which to store equipment" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Inventory.Resize(3,3) //fits one item obj.Fit(Tool(GlobalDefinitions.beamer)) mustEqual Some(0) @@ -260,7 +266,7 @@ class PlayerTest extends Specification { } "can use their free hand to hold things" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val ammo = AmmoBox(GlobalDefinitions.bullet_9mm) obj.FreeHand.Equipment mustEqual None @@ -269,12 +275,12 @@ class PlayerTest extends Specification { } "can access the player's locker-space" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Slot(5).Equipment.get.isInstanceOf[LockerContainer] mustEqual true } "can find equipment" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Slot(0).Equipment = { val item = Tool(beamer) item.GUID = PlanetSideGUID(1) @@ -310,7 +316,7 @@ class PlayerTest extends Specification { } "does equipment collision checking (are we already holding something there?)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val item1 = Tool(beamer) val item2 = Kit(medkit) val item3 = AmmoBox(GlobalDefinitions.bullet_9mm) @@ -350,7 +356,7 @@ class PlayerTest extends Specification { } "battle experience point values of the avatar" in { - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.BEP mustEqual avatar.BEP @@ -359,7 +365,7 @@ class PlayerTest extends Specification { } "command experience point values of the avatar" in { - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.CEP mustEqual avatar.CEP @@ -368,14 +374,14 @@ class PlayerTest extends Specification { } "can get a quick summary of implant slots (default)" in { - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) player.Implants mustEqual Array.empty } "can get a quick summary of implant slots (two unlocked, one installed)" in { - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) val temp = new ImplantDefinition(1) avatar.Implants(0).Unlocked = true @@ -398,7 +404,7 @@ class PlayerTest extends Specification { } "can get a quick summary of implant slots (all unlocked, first two installed)" in { - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) avatar.Implants(0).Unlocked = true avatar.InstallImplant(new ImplantDefinition(1)) @@ -429,7 +435,7 @@ class PlayerTest extends Specification { } "seat in a vehicle" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.VehicleSeated mustEqual None obj.VehicleSeated = PlanetSideGUID(65) obj.VehicleSeated mustEqual Some(PlanetSideGUID(65)) @@ -438,7 +444,7 @@ class PlayerTest extends Specification { } "own in a vehicle" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.VehicleOwned mustEqual None obj.VehicleOwned = PlanetSideGUID(65) obj.VehicleOwned mustEqual Some(PlanetSideGUID(65)) @@ -447,21 +453,21 @@ class PlayerTest extends Specification { } "remember what zone he is in" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.Continent mustEqual "home2" obj.Continent = "ugd01" obj.Continent mustEqual "ugd01" } "special is typically normal and can not be changed from normal" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal } "a TR MAX can change its special to Overdrive or Anchored" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.ExoSuit = ExoSuitType.MAX obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored @@ -476,7 +482,7 @@ class PlayerTest extends Specification { } "an NC MAX can change its special to Shielded" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.ExoSuit = ExoSuitType.MAX obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded @@ -486,13 +492,13 @@ class PlayerTest extends Specification { } "one faction can not use the other's specials" in { - val objtr = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val objtr = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) objtr.ExoSuit = ExoSuitType.MAX objtr.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal objtr.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded objtr.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal - val objnc = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, 5) + val objnc = TestPlayer("Chord", PlanetSideEmpire.NC, CharacterGender.Male, 0, CharacterVoice.Voice5) objnc.ExoSuit = ExoSuitType.MAX objnc.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal objnc.UsingSpecial = SpecialExoSuitDefinition.Mode.Overdrive @@ -502,7 +508,7 @@ class PlayerTest extends Specification { } "changing exo-suit type resets the special to Normal (and changing back does not revert it again)" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.ExoSuit = ExoSuitType.MAX obj.UsingSpecial mustEqual SpecialExoSuitDefinition.Mode.Normal obj.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored @@ -516,7 +522,7 @@ class PlayerTest extends Specification { } "toString" in { - val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) obj.toString mustEqual "TR Chord 0/100 0/50" obj.GUID = PlanetSideGUID(455) diff --git a/common/src/test/scala/objects/VehicleSpawnPadTest.scala b/common/src/test/scala/objects/VehicleSpawnPadTest.scala index 2c9e1ba78..e2693030a 100644 --- a/common/src/test/scala/objects/VehicleSpawnPadTest.scala +++ b/common/src/test/scala/objects/VehicleSpawnPadTest.scala @@ -9,7 +9,7 @@ import net.psforever.objects.serverobject.structures.StructureType import net.psforever.objects.{Avatar, GlobalDefinitions, Player, Vehicle} import net.psforever.objects.zones.Zone import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterVoice, PlanetSideEmpire, Vector3} import org.specs2.mutable.Specification import scala.concurrent.duration._ @@ -49,7 +49,7 @@ class VehicleSpawnControl1Test extends ActorTest() { } class VehicleSpawnControl2aTest extends ActorTest() { - // This long runs for a long time. + // This runs for a long time. "VehicleSpawnControl" should { "complete on a vehicle order (block a second one until the first is done and the spawn pad is cleared)" in { val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) @@ -102,18 +102,18 @@ class VehicleSpawnControl2aTest extends ActorTest() { //if we move the vehicle more than 25m away from the pad, we should receive a ResetSpawnPad, and a second ConcealPlayer message //that means that the first order has cleared and the spawn pad is now working on the second order successfully - vehicle.Position = Vector3(11,0,0) player.VehicleSeated = None //since shared between orders, is necessary + vehicle.Position = Vector3(12,0,0) val probe3Msg5 = probe3.receiveOne(4 seconds) assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) - val probe3Msg6 = probe3.receiveOne(5 seconds) + val probe3Msg6 = probe3.receiveOne(4 seconds) assert(probe3Msg6.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) } } } class VehicleSpawnControl2bTest extends ActorTest() { - // This long runs for a long time. + // This runs for a long time. "VehicleSpawnControl" should { "complete on a vehicle order (railless)" in { val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) @@ -144,7 +144,7 @@ class VehicleSpawnControl2bTest extends ActorTest() { assert(probe1Msg2.isInstanceOf[Mountable.MountMessages]) val probe1Msg2Contents = probe1Msg2.asInstanceOf[Mountable.MountMessages] assert(probe1Msg2Contents.response.isInstanceOf[Mountable.CanMount]) - val probe1Msg3 = probe1.receiveOne(3 seconds) + val probe1Msg3 = probe1.receiveOne(4 seconds) assert(probe1Msg3.isInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle]) val probe1Msg4 = probe1.receiveOne(1 seconds) @@ -161,9 +161,9 @@ class VehicleSpawnControl2bTest extends ActorTest() { //if we move the vehicle more than 10m away from the pad, we should receive a second ConcealPlayer message //that means that the first order has cleared and the spawn pad is now working on the second order successfully - vehicle.Position = Vector3(11,0,0) player.VehicleSeated = None //since shared between orders, is necessary - val probe3Msg6 = probe3.receiveOne(4 seconds) + vehicle.Position = Vector3(12,0,0) + val probe3Msg6 = probe3.receiveOne(10 seconds) assert(probe3Msg6.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) } } @@ -263,6 +263,9 @@ class VehicleSpawnControl5Test extends ActorTest() { val probe3Msg4 = probe3.receiveOne(3 seconds) assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) + val probe3Msg5 = probe3.receiveOne(1 seconds) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.RevealPlayer]) + val probe1Msg = probe1.receiveOne(12 seconds) assert(probe1Msg.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) assert(probe1Msg.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) @@ -292,59 +295,17 @@ class VehicleSpawnControl6Test extends ActorTest() { val probe1Msg1 = probe1.receiveOne(200 milliseconds) assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) - player.Continent = "problem" //problem 1 + player.Continent = "problem" //problem probe1.receiveOne(200 milliseconds) //Mountable.MountMessage val probe3Msg4 = probe3.receiveOne(3 seconds) assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) val probe3Msg5 = probe3.receiveOne(3 seconds) - assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) + assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.RevealPlayer]) - val probe1Msg2 = probe1.receiveOne(12 seconds) - assert(probe1Msg2.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) - assert(probe1Msg2.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) - } - } -} - -class VehicleSpawnControl7Test extends ActorTest() { - "VehicleSpawnControl" should { - "player dies after getting in driver seat; the vehicle blocks the pad" in { - val (vehicle, player, pad, zone) = VehicleSpawnPadControlTest.SetUpAgents(PlanetSideEmpire.TR) - //we can recycle the vehicle and the player for each order - val probe1 = new TestProbe(system, "first-order") - val probe3 = new TestProbe(system, "zone-events") - zone.VehicleEvents = probe3.ref - - pad.Actor.tell(VehicleSpawnPad.VehicleOrder(player, vehicle), probe1.ref) - - val probe3Msg1 = probe3.receiveOne(3 seconds) - assert(probe3Msg1.isInstanceOf[VehicleSpawnPad.ConcealPlayer]) - - val probe3Msg2 = probe3.receiveOne(3 seconds) - assert(probe3Msg2.isInstanceOf[VehicleSpawnPad.LoadVehicle]) - - val probe3Msg3 = probe3.receiveOne(3 seconds) - assert(probe3Msg3.isInstanceOf[VehicleSpawnPad.AttachToRails]) - - val probe1Msg1 = probe1.receiveOne(200 milliseconds) - assert(probe1Msg1.isInstanceOf[VehicleSpawnPad.StartPlayerSeatedInVehicle]) - val probe1Msg2 = probe1.receiveOne(200 milliseconds) - assert(probe1Msg2.isInstanceOf[Mountable.MountMessages]) - val probe1Msg2Contents = probe1Msg2.asInstanceOf[Mountable.MountMessages] - assert(probe1Msg2Contents.response.isInstanceOf[Mountable.CanMount]) - val probe1Msg3 = probe1.receiveOne(3 seconds) - assert(probe1Msg3.isInstanceOf[VehicleSpawnPad.PlayerSeatedInVehicle]) - player.Die //problem - - val probe3Msg4 = probe3.receiveOne(3 seconds) - assert(probe3Msg4.isInstanceOf[VehicleSpawnPad.DetachFromRails]) - val probe3Msg5 = probe3.receiveOne(100 milliseconds) - assert(probe3Msg5.isInstanceOf[VehicleSpawnPad.ResetSpawnPad]) - - val probe1Msg4 = probe1.receiveOne(12 seconds) - assert(probe1Msg4.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) - assert(probe1Msg4.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) + val probe1Msg3 = probe1.receiveOne(12 seconds) + assert(probe1Msg3.isInstanceOf[VehicleSpawnPad.PeriodicReminder]) + assert(probe1Msg3.asInstanceOf[VehicleSpawnPad.PeriodicReminder].reason == VehicleSpawnPad.Reminders.Blocked) } } } @@ -379,11 +340,13 @@ object VehicleSpawnPadControlTest { pad.Actor = system.actorOf(Props(classOf[VehicleSpawnControl], pad), s"test-pad-${System.nanoTime()}") pad.Owner = new Building(0, zone, StructureType.Building) pad.Owner.Faction = faction - val player = Player(Avatar("test", faction, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)) player.GUID = PlanetSideGUID(10) player.Continent = zone.Id player.Spawn - //note: pad and vehicle are both at Vector3(0,0,0) so they count as blocking + //note: pad and vehicle are both at Vector3(1,0,0) so they count as blocking + pad.Position = Vector3(1,0,0) + vehicle.Position = Vector3(1,0,0) (vehicle, player, pad, zone) } } diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index e0495c4ad..42a581acf 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -1,13 +1,13 @@ // Copyright (c) 2017 PSForever package objects -import akka.actor.Props +import akka.actor.{ActorSystem, Props} import net.psforever.objects._ import net.psforever.objects.definition.{SeatDefinition, VehicleDefinition} import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles._ import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.ExoSuitType +import net.psforever.types.{CharacterVoice, ExoSuitType} import org.specs2.mutable._ import scala.concurrent.duration.Duration @@ -312,7 +312,7 @@ class VehicleTest extends Specification { } } -class VehicleControl1Test extends ActorTest { +class VehicleControlStopMountingTest extends ActorTest { "Vehicle Control" should { "deactivate and stop handling mount messages" in { val player1 = Player(VehicleTest.avatar1) @@ -333,7 +333,7 @@ class VehicleControl1Test extends ActorTest { } } -class VehicleControl2Test extends ActorTest { +class VehicleControlRestartMountingTest extends ActorTest { "Vehicle Control" should { "reactivate and resume handling mount messages" in { val player1 = Player(VehicleTest.avatar1) @@ -358,9 +358,261 @@ class VehicleControl2Test extends ActorTest { } } +class VehicleControlAlwaysDismountTest extends ActorTest { + "Vehicle Control" should { + "always allow dismount messages" in { + val player1 = Player(VehicleTest.avatar1) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar2) + player2.GUID = PlanetSideGUID(2) + val vehicle = Vehicle(GlobalDefinitions.two_man_assault_buggy) + vehicle.GUID = PlanetSideGUID(3) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + vehicle.Actor ! Mountable.TryMount(player1, 0) + receiveOne(Duration.create(100, "ms")) //discard + vehicle.Actor ! Mountable.TryMount(player2, 1) + receiveOne(Duration.create(100, "ms")) //discard + + vehicle.Actor ! Mountable.TryDismount(player2, 1) //player2 requests dismount + val reply1 = receiveOne(Duration.create(100, "ms")) + assert(reply1.isInstanceOf[Mountable.MountMessages]) + assert(reply1.asInstanceOf[Mountable.MountMessages].response.isInstanceOf[Mountable.CanDismount]) //player2 dismounts + vehicle.Actor ! Vehicle.PrepareForDeletion + + vehicle.Actor ! Mountable.TryDismount(player1, 0) //player1 requests dismount + val reply2 = receiveOne(Duration.create(100, "ms")) + assert(reply2.isInstanceOf[Mountable.MountMessages]) + assert(reply2.asInstanceOf[Mountable.MountMessages].response.isInstanceOf[Mountable.CanDismount]) //player1 dismounts + } + } +} + +class VehicleControlMountingBlockedExosuitTest extends ActorTest { + def checkCanNotMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + case _ => + assert(false) + } + } + + def checkCanMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + + "Vehicle Control" should { + "block players from sitting if their exo-suit is not allowed by the seat" in { + val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + + val player1 = Player(VehicleTest.avatar1) + player1.ExoSuit = ExoSuitType.Reinforced + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.ExoSuit = ExoSuitType.MAX + player2.GUID = PlanetSideGUID(2) + val player3 = Player(VehicleTest.avatar1) + player3.ExoSuit = ExoSuitType.Agile + player3.GUID = PlanetSideGUID(3) + + //disallow + vehicle.Actor ! Mountable.TryMount(player1, 0) //Reinforced in non-MAX seat + checkCanNotMount() + vehicle.Actor ! Mountable.TryMount(player2, 0) //MAX in non-Reinforced seat + checkCanNotMount() + vehicle.Actor ! Mountable.TryMount(player2, 1) //MAX in non-MAX seat + checkCanNotMount() + vehicle.Actor ! Mountable.TryMount(player1, 9) //Reinforced in MAX-only seat + checkCanNotMount() + vehicle.Actor ! Mountable.TryMount(player3, 9) //Agile in MAX-only seat + checkCanNotMount() + + //allow + vehicle.Actor ! Mountable.TryMount(player1, 1) + checkCanMount() + vehicle.Actor ! Mountable.TryMount(player2, 9) + checkCanMount() + vehicle.Actor ! Mountable.TryMount(player3, 0) + checkCanMount() + } + } +} + +class VehicleControlMountingBlockedSeatPermissionTest extends ActorTest { + def checkCanNotMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + case _ => + assert(false) + } + } + + def checkCanMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + + "Vehicle Control" should { + //11 June 2018: Group is not supported yet so do not bother testing it + "block players from sitting if the seat does not allow it" in { + val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + + val player1 = Player(VehicleTest.avatar1) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.GUID = PlanetSideGUID(2) + + vehicle.PermissionGroup(2,3) //passenger group -> empire + vehicle.Actor ! Mountable.TryMount(player1, 3) //passenger seat + checkCanMount() + vehicle.PermissionGroup(2,0) //passenger group -> locked + vehicle.Actor ! Mountable.TryMount(player2, 4) //passenger seat + checkCanNotMount() + } + } +} + +class VehicleControlMountingDriverSeatTest extends ActorTest { + def checkCanMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + + "Vehicle Control" should { + "allow players to sit in the driver seat, even if it is locked, if the vehicle is unowned" in { + val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + val player1 = Player(VehicleTest.avatar1) + player1.GUID = PlanetSideGUID(1) + + assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked + assert(vehicle.Seats(0).Occupant.isEmpty) + assert(vehicle.Owner.isEmpty) + vehicle.Actor ! Mountable.TryMount(player1, 0) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + } + } +} + +class VehicleControlMountingOwnedLockedDriverSeatTest extends ActorTest { + def checkCanNotMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanNotMount]) + case _ => + assert(false) + } + } + + def checkCanMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + + "Vehicle Control" should { + "block players that are not the current owner from sitting in the driver seat (locked)" in { + val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + + val player1 = Player(VehicleTest.avatar1) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.GUID = PlanetSideGUID(2) + + assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Locked)) //driver group -> locked + assert(vehicle.Seats(0).Occupant.isEmpty) + vehicle.Owner = player1.GUID + + vehicle.Actor ! Mountable.TryMount(player1, 0) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor ! Mountable.TryDismount(player1, 0) + receiveOne(Duration.create(100, "ms")) //discard + assert(vehicle.Seats(0).Occupant.isEmpty) + + vehicle.Actor ! Mountable.TryMount(player2, 0) + checkCanNotMount() + assert(vehicle.Seats(0).Occupant.isEmpty) + } + } +} + +class VehicleControlMountingOwnedUnlockedDriverSeatTest extends ActorTest { + def checkCanMount() : Unit = { + val reply = receiveOne(Duration.create(100, "ms")) + reply match { + case msg : Mountable.MountMessages => + assert(msg.response.isInstanceOf[Mountable.CanMount]) + case _ => + assert(false) + } + } + + "Vehicle Control" should { + "allow players that are not the current owner to sit in the driver seat (empire)" in { + val vehicle = Vehicle(GlobalDefinitions.apc_tr) + vehicle.GUID = PlanetSideGUID(10) + vehicle.Actor = system.actorOf(Props(classOf[VehicleControl], vehicle), "vehicle-test") + + val player1 = Player(VehicleTest.avatar1) + player1.GUID = PlanetSideGUID(1) + val player2 = Player(VehicleTest.avatar1) + player2.GUID = PlanetSideGUID(2) + + vehicle.PermissionGroup(0,3) //passenger group -> empire + assert(vehicle.PermissionGroup(0).contains(VehicleLockState.Empire)) //driver group -> empire + assert(vehicle.Seats(0).Occupant.isEmpty) + vehicle.Owner = player1.GUID //owner set + + vehicle.Actor ! Mountable.TryMount(player1, 0) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + vehicle.Actor ! Mountable.TryDismount(player1, 0) + receiveOne(Duration.create(100, "ms")) //discard + assert(vehicle.Seats(0).Occupant.isEmpty) + + vehicle.Actor ! Mountable.TryMount(player2, 0) + checkCanMount() + assert(vehicle.Seats(0).Occupant.nonEmpty) + } + } +} + object VehicleTest { import net.psforever.objects.Avatar import net.psforever.types.{CharacterGender, PlanetSideEmpire} - val avatar1 = Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) - val avatar2 = Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar1 = Avatar("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) + val avatar2 = Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) } diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 6137b9938..3804825de 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -12,13 +12,13 @@ import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects._ import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType} import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} import net.psforever.objects.Vehicle import org.specs2.mutable.Specification -import scala.concurrent.duration.Duration +import scala.concurrent.duration._ class ZoneTest extends Specification { def test(a: Int, b : Zone, c : ActorContext) : Building = { Building.NoBuilding } @@ -185,7 +185,7 @@ class ZoneActorTest extends ActorTest { zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-spawn") zone.Actor ! Zone.Init() expectNoMsg(Duration.create(300, "ms")) - val player = Player(Avatar("Chord", PlanetSideEmpire.NEUTRAL, CharacterGender.Male, 0, 5)) + val player = Player(Avatar("Chord", PlanetSideEmpire.NEUTRAL, CharacterGender.Male, 0, CharacterVoice.Voice5)) val bldg1 = zone.Building(1).get val bldg3 = zone.Building(3).get @@ -216,7 +216,7 @@ class ZoneActorTest extends ActorTest { zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "test-no-spawn") zone.Actor ! Zone.Init() expectNoMsg(Duration.create(300, "ms")) - val player = Player(Avatar("Chord", PlanetSideEmpire.NEUTRAL, CharacterGender.Male, 0, 5)) + val player = Player(Avatar("Chord", PlanetSideEmpire.NEUTRAL, CharacterGender.Male, 0, CharacterVoice.Voice5)) zone.Actor ! Zone.Lattice.RequestSpawnPoint(1, player, 7) val reply = receiveOne(Duration.create(200, "ms")) @@ -234,7 +234,7 @@ class ZonePopulationTest extends ActorTest { "ZonePopulationActor" should { "add new user to zones" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -249,7 +249,7 @@ class ZonePopulationTest extends ActorTest { "remove user from zones" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume zone.Population ! Zone.Population.Join(avatar) @@ -264,7 +264,7 @@ class ZonePopulationTest extends ActorTest { "associate user with a character" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -284,7 +284,7 @@ class ZonePopulationTest extends ActorTest { "disassociate character from a user" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -306,7 +306,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Leave, but still has an associated character" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(500, "ms")) //consume @@ -330,7 +330,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Spawn a character, but an associated character already exists" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player1 = Player(avatar) val player2 = Player(avatar) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" @@ -356,7 +356,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Spawn a character, but did not Join first" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) val player = Player(avatar) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -374,7 +374,7 @@ class ZonePopulationTest extends ActorTest { "user tries to Release a character, but did not Spawn a character first" in { val zone = new Zone("test", new ZoneMap(""), 0) - val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + val avatar = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5) system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume zone.Population ! Zone.Population.Join(avatar) @@ -395,7 +395,7 @@ class ZonePopulationTest extends ActorTest { "user adds character to list of retired characters" in { val zone = new Zone("test", new ZoneMap(""), 0) - val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) player.Release system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -409,7 +409,7 @@ class ZonePopulationTest extends ActorTest { "user removes character from the list of retired characters" in { val zone = new Zone("test", new ZoneMap(""), 0) - val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) player.Release system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -425,11 +425,11 @@ class ZonePopulationTest extends ActorTest { "user removes THE CORRECT character from the list of retired characters" in { val zone = new Zone("test", new ZoneMap(""), 0) - val player1 = Player(Avatar("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + val player1 = Player(Avatar("Chord1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) player1.Release - val player2 = Player(Avatar("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + val player2 = Player(Avatar("Chord2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) player2.Release - val player3 = Player(Avatar("Chord3", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + val player3 = Player(Avatar("Chord3", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) player3.Release system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), TestName) ! "!" receiveOne(Duration.create(200, "ms")) //consume @@ -451,7 +451,7 @@ class ZonePopulationTest extends ActorTest { "user tries to add character to list of retired characters, but is not in correct state" in { val zone = new Zone("test", new ZoneMap(""), 0) - val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) + val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)) //player.Release !!important system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "testC") ! "!" receiveOne(Duration.create(500, "ms")) //consume @@ -464,56 +464,183 @@ class ZonePopulationTest extends ActorTest { } } -class ZoneGroundTest extends ActorTest { +class ZoneGroundDropItemTest extends ActorTest { val item = AmmoBox(GlobalDefinitions.bullet_9mm) - item.GUID = PlanetSideGUID(10) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + hub.register(item, 10) + val zone = new Zone("test", new ZoneMap("test-map"), 0) + zone.GUID(hub) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" - "ZoneGroundActor" should { + "DropItem" should { "drop item on ground" in { - val zone = new Zone("test", new ZoneMap(""), 0) - system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-item-test") ! "!" - receiveOne(Duration.create(200, "ms")) //consume + receiveOne(1 second) //consume + assert(!zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.DropItem(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f)) - assert(zone.EquipmentOnGround.isEmpty) - assert(item.Position == Vector3.Zero) - assert(item.Orientation == Vector3.Zero) - zone.Ground ! Zone.DropItemOnGround(item, Vector3(1.1f, 2.2f, 3.3f), Vector3(4.4f, 5.5f, 6.6f)) - expectNoMsg(Duration.create(100, "ms")) - - assert(zone.EquipmentOnGround == List(item)) - assert(item.Position == Vector3(1.1f, 2.2f, 3.3f)) - assert(item.Orientation == Vector3(4.4f, 5.5f, 6.6f)) + val reply = receiveOne(200 milliseconds) + assert(reply.isInstanceOf[Zone.Ground.ItemOnGround]) + assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].item == item) + assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].pos == Vector3(1.1f, 2.2f, 3.3f)) + assert(reply.asInstanceOf[Zone.Ground.ItemOnGround].orient == Vector3(4.4f, 5.5f, 6.6f)) + assert(zone.EquipmentOnGround.contains(item)) } + } +} - "get item from ground (success)" in { - val zone = new Zone("test", new ZoneMap(""), 0) - val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) - system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-good") ! "!" - receiveOne(Duration.create(200, "ms")) //consume - zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) - expectNoMsg(Duration.create(100, "ms")) +class ZoneGroundCanNotDropItem1Test extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + //hub.register(item, 10) //!important + val zone = new Zone("test", new ZoneMap("test-map"), 0) + zone.GUID(hub) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" - assert(zone.EquipmentOnGround == List(item)) - zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(10)) - val reply = receiveOne(Duration.create(100, "ms")) + "DropItem" should { + "not drop an item that is not registered" in { + receiveOne(1 second) //consume + assert(!zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) - assert(zone.EquipmentOnGround.isEmpty) - assert(reply.isInstanceOf[Zone.ItemFromGround]) - assert(reply.asInstanceOf[Zone.ItemFromGround].player == player) - assert(reply.asInstanceOf[Zone.ItemFromGround].item == item) + val reply = receiveOne(300 milliseconds) + assert(reply.isInstanceOf[Zone.Ground.CanNotDropItem]) + assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].item == item) + assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].zone == zone) + assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].reason == "not registered yet") + assert(!zone.EquipmentOnGround.contains(item)) } + } +} - "get item from ground (failure)" in { - val zone = new Zone("test", new ZoneMap(""), 0) - val player = Player(Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)) - system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "get-item-test-fail") ! "!" - receiveOne(Duration.create(200, "ms")) //consume - zone.Ground ! Zone.DropItemOnGround(item, Vector3.Zero, Vector3.Zero) - expectNoMsg(Duration.create(100, "ms")) +class ZoneGroundCanNotDropItem2Test extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + hub.register(item, 10) //!important + val zone = new Zone("test", new ZoneMap("test-map"), 0) + //zone.GUID(hub) //!important + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" - assert(zone.EquipmentOnGround == List(item)) - zone.Ground ! Zone.GetItemOnGround(player, PlanetSideGUID(11)) //wrong guid - expectNoMsg(Duration.create(500, "ms")) + "DropItem" should { + "not drop an item that is not registered to the zone" in { + receiveOne(1 second) //consume + assert(!zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) + + val reply = receiveOne(300 milliseconds) + assert(reply.isInstanceOf[Zone.Ground.CanNotDropItem]) + assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].item == item) + assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].zone == zone) + assert(reply.asInstanceOf[Zone.Ground.CanNotDropItem].reason == "registered to some other zone") + assert(!zone.EquipmentOnGround.contains(item)) + } + } +} + +class ZoneGroundCanNotDropItem3Test extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + hub.register(item, 10) //!important + val zone = new Zone("test", new ZoneMap("test-map"), 0) + zone.GUID(hub) //!important + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" + + "DropItem" should { + "not drop an item that has already been dropped" in { + receiveOne(1 second) //consume + assert(!zone.EquipmentOnGround.contains(item)) + assert(zone.EquipmentOnGround.isEmpty) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) + + val reply1 = receiveOne(300 milliseconds) + assert(reply1.isInstanceOf[Zone.Ground.ItemOnGround]) + assert(reply1.asInstanceOf[Zone.Ground.ItemOnGround].item == item) + assert(zone.EquipmentOnGround.contains(item)) + assert(zone.EquipmentOnGround.size == 1) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) + + val reply2 = receiveOne(300 milliseconds) + assert(reply2.isInstanceOf[Zone.Ground.CanNotDropItem]) + assert(reply2.asInstanceOf[Zone.Ground.CanNotDropItem].item == item) + assert(reply2.asInstanceOf[Zone.Ground.CanNotDropItem].zone == zone) + assert(reply2.asInstanceOf[Zone.Ground.CanNotDropItem].reason == "already dropped") + assert(zone.EquipmentOnGround.size == 1) + } + } +} + +class ZoneGroundPickupItemTest extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + hub.register(item, 10) + val zone = new Zone("test", new ZoneMap("test-map"), 0) + zone.GUID(hub) + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" + + "PickupItem" should { + "pickup an item from ground" in { + receiveOne(1 second) //consume + assert(!zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) + + val reply1 = receiveOne(200 milliseconds) + assert(reply1.isInstanceOf[Zone.Ground.ItemOnGround]) + assert(zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.PickupItem(item.GUID) + + val reply2 = receiveOne(200 milliseconds) + assert(reply2.isInstanceOf[Zone.Ground.ItemInHand]) + assert(reply2.asInstanceOf[Zone.Ground.ItemInHand].item == item) + assert(!zone.EquipmentOnGround.contains(item)) + } + } +} + +class ZoneGroundCanNotPickupItemTest extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + hub.register(item, 10) + val zone = new Zone("test", new ZoneMap("test-map"), 0) + zone.GUID(hub) //still registered to this zone + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" + + "PickupItem" should { + "not pickup an item if it can not be found" in { + receiveOne(1 second) //consume + assert(!zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.PickupItem(item.GUID) + + val reply2 = receiveOne(200 milliseconds) + assert(reply2.isInstanceOf[Zone.Ground.CanNotPickupItem]) + assert(reply2.asInstanceOf[Zone.Ground.CanNotPickupItem].item_guid == item.GUID) + assert(reply2.asInstanceOf[Zone.Ground.CanNotPickupItem].zone == zone) + assert(reply2.asInstanceOf[Zone.Ground.CanNotPickupItem].reason == "can not find") + } + } +} + +class ZoneGroundRemoveItemTest extends ActorTest { + val item = AmmoBox(GlobalDefinitions.bullet_9mm) + val hub = new NumberPoolHub(new LimitedNumberSource(20)) + hub.register(item, 10) + val zone = new Zone("test", new ZoneMap("test-map"), 0) + zone.GUID(hub) //still registered to this zone + system.actorOf(Props(classOf[ZoneTest.ZoneInitActor], zone), "drop-test-zone") ! "!" + + "RemoveItem" should { + "remove an item from the ground without callback (even if the item is not found)" in { + receiveOne(1 second) + assert(!zone.EquipmentOnGround.contains(item)) + zone.Ground ! Zone.Ground.DropItem(item, Vector3.Zero, Vector3.Zero) + receiveOne(200 milliseconds) + assert(zone.EquipmentOnGround.contains(item)) //dropped + + zone.Ground ! Zone.Ground.RemoveItem(item.GUID) + expectNoMsg(500 milliseconds) + assert(!zone.EquipmentOnGround.contains(item)) + + zone.Ground ! Zone.Ground.RemoveItem(item.GUID) //repeat + expectNoMsg(500 milliseconds) + assert(!zone.EquipmentOnGround.contains(item)) } } } diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala index 0a19c67df..c18987ef2 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegister5Test.scala @@ -3,13 +3,13 @@ package objects.guidtask import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import objects.ActorTest class GUIDTaskRegister5Test extends ActorTest() { "RegisterAvatar" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala index 091ea62fc..e6806cbd0 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskRegister6Test.scala @@ -3,13 +3,13 @@ package objects.guidtask import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import objects.ActorTest class GUIDTaskRegister6Test extends ActorTest() { "RegisterPlayer" in { val (_, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala index 15cef74e2..a9bba7173 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister5Test.scala @@ -3,13 +3,13 @@ package objects.guidtask import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import objects.ActorTest class GUIDTaskUnregister5Test extends ActorTest() { "UnregisterAvatar" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala index 718f460f0..26e8ccfb3 100644 --- a/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala +++ b/common/src/test/scala/objects/guidtask/GUIDTaskUnregister6Test.scala @@ -3,13 +3,13 @@ package objects.guidtask import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import objects.ActorTest class GUIDTaskUnregister6Test extends ActorTest() { "UnregisterPlayer" in { val (guid, uns, taskResolver, probe) = GUIDTaskTest.CommonTestSetup - val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val obj = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj_wep = Tool(GlobalDefinitions.beamer) obj.Slot(0).Equipment = obj_wep val obj_wep_ammo = AmmoBox(GlobalDefinitions.energy_cell) diff --git a/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala b/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala index b635ad5de..8ea5ae6a5 100644 --- a/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/AirVehicleTerminalTest.scala @@ -7,12 +7,12 @@ import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class AirVehicleTerminalTest extends Specification { "Air_Vehicle_Terminal" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.air_vehicle_terminal) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR diff --git a/common/src/test/scala/objects/terminal/CertTerminalTest.scala b/common/src/test/scala/objects/terminal/CertTerminalTest.scala index c9791c9ec..67682396d 100644 --- a/common/src/test/scala/objects/terminal/CertTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/CertTerminalTest.scala @@ -12,7 +12,7 @@ import org.specs2.mutable.Specification class CertTerminalTest extends Specification { "Cert_Terminal" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.cert_terminal) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR diff --git a/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala b/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala index ef6c621b8..3ad3df334 100644 --- a/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/DropshipVehicleTerminalTest.scala @@ -7,12 +7,12 @@ import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class DropshipVehicleTerminalTest extends Specification { "Dropship_Vehicle_Terminal" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.dropship_vehicle_terminal) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR diff --git a/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala b/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala index ee88b6027..680d4752e 100644 --- a/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/GroundVehicleTerminalTest.scala @@ -7,12 +7,12 @@ import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class GroundVehicleTerminalTest extends Specification { "Ground_Vehicle_Terminal" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.ground_vehicle_terminal) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala index 272815f78..461fef8f9 100644 --- a/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala +++ b/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala @@ -7,12 +7,12 @@ import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class ImplantTerminalInterfaceTest extends Specification { "Implant_Terminal_Interface" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala index 91993769f..6bde119a2 100644 --- a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala +++ b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala @@ -8,7 +8,7 @@ import net.psforever.objects.serverobject.implantmech.{ImplantTerminalMech, Impl import net.psforever.objects.serverobject.structures.StructureType import net.psforever.objects.vehicles.Seat import net.psforever.objects.{Avatar, GlobalDefinitions, Player} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, Vector3} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, Vector3} import objects.ActorTest import org.specs2.mutable.Specification @@ -45,7 +45,7 @@ class ImplantTerminalMechTest extends Specification { } "get passenger in a seat" in { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) obj.PassengerInSeat(player) mustEqual None obj.Seats(0).Occupant = player @@ -90,7 +90,7 @@ class ImplantTerminalMechControl3Test extends ActorTest() { "ImplantTerminalMechControl" should { "block a player from mounting" in { val (player1, mech) = ImplantTerminalMechTest.SetUpAgents(PlanetSideEmpire.TR) - val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) mech.Actor ! Mountable.TryMount(player1, 0) receiveOne(Duration.create(100, "ms")) //consume reply @@ -164,6 +164,6 @@ object ImplantTerminalMechTest { terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = faction terminal.GUID = PlanetSideGUID(1) - (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), terminal) + (Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), terminal) } } diff --git a/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala b/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala index fbbb4d6e6..15c3e7e45 100644 --- a/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/MatrixTerminalTest.scala @@ -60,7 +60,7 @@ class MatrixTerminalTest extends Specification { } "player can not buy (anything)" in { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.NoDeal() diff --git a/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala b/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala index 86b39fe3f..b27de07b8 100644 --- a/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala @@ -5,7 +5,7 @@ import akka.actor.ActorRef import net.psforever.objects.serverobject.terminals.{MedicalTerminalDefinition, ProximityTerminal, Terminal} import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class MedicalTerminalTest extends Specification { @@ -81,7 +81,7 @@ class MedicalTerminalTest extends Specification { "player can not interact with the proximity terminal normally (buy)" in { val terminal = ProximityTerminal(GlobalDefinitions.medical_terminal) - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.NoDeal() diff --git a/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala index ee4b4a476..fcdaa4987 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalABTest.scala @@ -47,14 +47,14 @@ class OrderTerminalABTest extends Specification { } "player can buy different armor ('lite_armor')" in { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.BuyExosuit(ExoSuitType.Agile) } "player can buy max armor ('trhev_antiaircraft')" in { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "trhev_antiaircraft", 0, PlanetSideGUID(0)) terminal.Request(player, msg) mustEqual Terminal.NoDeal() @@ -62,7 +62,7 @@ class OrderTerminalABTest extends Specification { //TODO loudout tests "player can not load max loadout" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player = Player(avatar) avatar.SaveLoadout(player, "test1", 0) player.ExoSuit = ExoSuitType.MAX diff --git a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala index 76e30ba1a..ccdc75310 100644 --- a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala +++ b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala @@ -12,7 +12,7 @@ import org.specs2.mutable.Specification class OrderTerminalTest extends Specification { "Order_Terminal" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.order_terminal) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR @@ -79,7 +79,7 @@ class OrderTerminalTest extends Specification { } "player can retrieve an infantry loadout" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player2 = Player(avatar) player2.ExoSuit = ExoSuitType.Agile player2.Slot(0).Equipment = Tool(GlobalDefinitions.beamer) @@ -99,7 +99,7 @@ class OrderTerminalTest extends Specification { } "player can not retrieve an infantry loadout from the wrong page" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player2 = Player(avatar) player2.ExoSuit = ExoSuitType.Agile player2.Slot(0).Equipment = Tool(GlobalDefinitions.beamer) @@ -111,7 +111,7 @@ class OrderTerminalTest extends Specification { } "player can not retrieve an infantry loadout from the wrong line" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player2 = Player(avatar) player2.ExoSuit = ExoSuitType.Agile player2.Slot(0).Equipment = Tool(GlobalDefinitions.beamer) diff --git a/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala b/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala index a28235fe0..bfd00c033 100644 --- a/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala +++ b/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala @@ -6,7 +6,7 @@ import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals._ import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import objects.ActorTest import scala.concurrent.duration.Duration @@ -34,7 +34,7 @@ class MedicalTerminalControl1Test extends ActorTest() { "ProximityTerminalControl sends a message to the first new user only" in { val (player, terminal) = ProximityTerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) player.GUID = PlanetSideGUID(10) - val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(11) terminal.Actor ! CommonMessages.Use(player) @@ -57,7 +57,7 @@ class MedicalTerminalControl2Test extends ActorTest() { "ProximityTerminalControl sends a message to the last user only" in { val (player, terminal) : (Player, ProximityTerminal) = ProximityTerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) player.GUID = PlanetSideGUID(10) - val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(11) terminal.Actor ! CommonMessages.Use(player) @@ -86,7 +86,7 @@ class MedicalTerminalControl3Test extends ActorTest() { "ProximityTerminalControl sends a message to the last user only (confirmation of test #2)" in { val (player, terminal) : (Player, ProximityTerminal) = ProximityTerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) player.GUID = PlanetSideGUID(10) - val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(11) terminal.Actor ! CommonMessages.Use(player) @@ -115,6 +115,6 @@ object ProximityTerminalControlTest { def SetUpAgents(tdef : MedicalTerminalDefinition, faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, ProximityTerminal) = { val terminal = ProximityTerminal(tdef) terminal.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], terminal), "test-term") - (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), terminal) + (Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), terminal) } } diff --git a/common/src/test/scala/objects/terminal/ProximityTest.scala b/common/src/test/scala/objects/terminal/ProximityTest.scala index 7b799d1d3..449feb316 100644 --- a/common/src/test/scala/objects/terminal/ProximityTest.scala +++ b/common/src/test/scala/objects/terminal/ProximityTest.scala @@ -7,7 +7,7 @@ import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.terminals.{ProximityTerminal, ProximityTerminalControl, ProximityUnit, Terminal} import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.packet.game.PlanetSideGUID -import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire} import objects.ActorTest import org.specs2.mutable.Specification @@ -70,7 +70,7 @@ class ProximityTerminalControl1bTest extends ActorTest { "send out a start message" in { val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") - val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player.GUID = PlanetSideGUID(10) assert(obj.NumberUsers == 0) @@ -91,9 +91,9 @@ class ProximityTerminalControl2bTest extends ActorTest { "will not send out one start message unless first user" in { val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") - val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player1.GUID = PlanetSideGUID(10) - val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(11) assert(obj.NumberUsers == 0) @@ -114,7 +114,7 @@ class ProximityTerminalControl3bTest extends ActorTest { "send out a stop message" in { val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") - val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("TestCharacter", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player.GUID = PlanetSideGUID(10) assert(obj.NumberUsers == 0) @@ -138,9 +138,9 @@ class ProximityTerminalControl4bTest extends ActorTest { "will not send out one stop message until last user" in { val obj = ProximityTerminal(GlobalDefinitions.medical_terminal) obj.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], obj), "prox-ctrl") - val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player1 = Player(Avatar("TestCharacter1", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player1.GUID = PlanetSideGUID(10) - val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player2 = Player(Avatar("TestCharacter2", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) player2.GUID = PlanetSideGUID(11) assert(obj.NumberUsers == 0) diff --git a/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala b/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala index 516e834c0..ddc277948 100644 --- a/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala +++ b/common/src/test/scala/objects/terminal/RepairRearmSiloTest.scala @@ -7,12 +7,12 @@ import net.psforever.objects._ import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class RepairRearmSiloTest extends Specification { "RepairRearmSilo" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val silo = Terminal(GlobalDefinitions.repair_silo) silo.Owner = new Building(0, Zone.Nowhere, StructureType.Building) silo.Owner.Faction = PlanetSideEmpire.TR @@ -49,7 +49,7 @@ class RepairRearmSiloTest extends Specification { } "player can retrieve a vehicle loadout" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player2 = Player(avatar) val vehicle = Vehicle(GlobalDefinitions.fury) vehicle.Slot(30).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) @@ -67,7 +67,7 @@ class RepairRearmSiloTest extends Specification { } "player can not retrieve a vehicle loadout from the wrong line" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player2 = Player(avatar) val vehicle = Vehicle(GlobalDefinitions.fury) vehicle.Slot(30).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) @@ -78,7 +78,7 @@ class RepairRearmSiloTest extends Specification { } "player can not retrieve a vehicle loadout from the wrong line" in { - val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val avatar = Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute) val player2 = Player(avatar) val vehicle = Vehicle(GlobalDefinitions.fury) vehicle.Slot(30).Equipment = AmmoBox(GlobalDefinitions.bullet_9mm) diff --git a/common/src/test/scala/objects/terminal/TerminalControlTest.scala b/common/src/test/scala/objects/terminal/TerminalControlTest.scala index e8432d3d6..784779fb6 100644 --- a/common/src/test/scala/objects/terminal/TerminalControlTest.scala +++ b/common/src/test/scala/objects/terminal/TerminalControlTest.scala @@ -123,6 +123,6 @@ object TerminalControlTest { terminal.Actor = system.actorOf(Props(classOf[TerminalControl], terminal), "test-term") terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = faction - (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), terminal) + (Player(Avatar("test", faction, CharacterGender.Male, 0, CharacterVoice.Mute)), terminal) } } diff --git a/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala b/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala index 0233dc9d6..b6edd7419 100644 --- a/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala +++ b/common/src/test/scala/objects/terminal/VehicleTerminalCombinedTest.scala @@ -7,12 +7,12 @@ import net.psforever.objects.{Avatar, GlobalDefinitions, Player} import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.zones.Zone import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} -import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import net.psforever.types.{CharacterGender, CharacterVoice, PlanetSideEmpire, TransactionType} import org.specs2.mutable.Specification class VehicleTerminalCombinedTest extends Specification { "Ground_Vehicle_Terminal" should { - val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Mute)) val terminal = Terminal(GlobalDefinitions.vehicle_terminal_combined) terminal.Owner = new Building(0, Zone.Nowhere, StructureType.Building) terminal.Owner.Faction = PlanetSideEmpire.TR diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 64f8e22f8..911979b8b 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals.{ProximityTerminal, Terminal} import net.psforever.objects.serverobject.tube.SpawnTube +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.types.Vector3 object Maps { @@ -125,6 +126,7 @@ object Maps { LocalObject(2050, Terminal.Constructor(repair_silo)) //rearm terminal A LocalObject(2061, ProximityTerminal.Constructor(repair_silo)) //repair terminal B LocalObject(2062, Terminal.Constructor(repair_silo)) //rearm terminal B + LocalObject(2094, ResourceSilo.Constructor) // NTU Silo LocalObject(2239, Terminal.Constructor(spawn_terminal)) LocalObject(2244, Terminal.Constructor(spawn_terminal)) LocalObject(2245, Terminal.Constructor(spawn_terminal)) @@ -238,6 +240,7 @@ object Maps { ObjectToBuilding(2050, 2) ObjectToBuilding(2061, 2) ObjectToBuilding(2062, 2) + ObjectToBuilding(2094, 2) ObjectToBuilding(2145, 2) ObjectToBuilding(2146, 2) ObjectToBuilding(2147, 2) @@ -473,6 +476,7 @@ object Maps { LocalObject(396, Door.Constructor) LocalObject(397, Door.Constructor) LocalObject(398, Door.Constructor) + LocalObject(399, Door.Constructor) LocalObject(462, Door.Constructor) LocalObject(463, Door.Constructor) LocalObject(522, ImplantTerminalMech.Constructor) @@ -520,6 +524,7 @@ object Maps { ObjectToBuilding(396, 2) ObjectToBuilding(397, 2) ObjectToBuilding(398, 2) + ObjectToBuilding(399, 2) ObjectToBuilding(462, 2) ObjectToBuilding(463, 2) ObjectToBuilding(522, 2) diff --git a/pslogin/src/main/scala/PacketCodingActor.scala b/pslogin/src/main/scala/PacketCodingActor.scala index 5a5b06b6f..2dc2687fc 100644 --- a/pslogin/src/main/scala/PacketCodingActor.scala +++ b/pslogin/src/main/scala/PacketCodingActor.scala @@ -331,8 +331,8 @@ class PacketCodingActor extends Actor with MDCContextAware { /** * Accept a series of packets and transform it into a series of packet encodings. - * Packets that do not encode properly are simply excluded for the product. - * This is not treated as an error or exception; a warning will mrely be logged. + * Packets that do not encode properly are simply excluded from the product. + * This is not treated as an error or exception; a warning will merely be logged. * @param iter the `Iterator` for a series of packets * @param out updated series of byte stream data produced through successful packet encoding; * defaults to an empty list diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index e689aa9ef..869ff6cbb 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -19,6 +19,7 @@ import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi.Color._ import services.ServiceManager import services.avatar._ +import services.galaxy.GalaxyService import services.local._ import services.vehicle.VehicleService @@ -209,7 +210,8 @@ object PsLogin { serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar") serviceManager ! ServiceManager.Register(Props[LocalService], "local") serviceManager ! ServiceManager.Register(Props[VehicleService], "vehicle") - serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "galaxy") + serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy") + serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster") //attach event bus entry point to each zone import akka.pattern.ask @@ -217,7 +219,7 @@ object PsLogin { import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.util.{Failure, Success} - implicit val timeout = Timeout(200 milliseconds) + implicit val timeout = Timeout(500 milliseconds) val requestVehicleEventBus : Future[ServiceManager.LookupResult] = (ServiceManager.serviceManager ask ServiceManager.Lookup("vehicle")).mapTo[ServiceManager.LookupResult] requestVehicleEventBus.onComplete { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 4491654d9..3897d94e6 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -24,11 +24,13 @@ import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} import net.psforever.objects.serverobject.pad.process.{AutoDriveControls, VehicleSpawnControlGuided} +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage @@ -37,15 +39,20 @@ import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, Ve import net.psforever.objects.zones.{InterstellarCluster, Zone} import net.psforever.packet.game.objectcreate._ import net.psforever.types._ -import services._ +import services.{RemoverActor, _} import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} +import services.galaxy.{GalaxyResponse, GalaxyServiceResponse} import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import services.vehicle.VehicleAction.UnstowEquipment import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} -import scala.annotation.tailrec -import scala.util.Success import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.annotation.tailrec +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.util.Success +import akka.pattern.ask class WorldSessionActor extends Actor with MDCContextAware { import WorldSessionActor._ @@ -57,8 +64,9 @@ class WorldSessionActor extends Actor with MDCContextAware { var avatarService : ActorRef = ActorRef.noSender var localService : ActorRef = ActorRef.noSender var vehicleService : ActorRef = ActorRef.noSender + var galaxyService : ActorRef = ActorRef.noSender var taskResolver : ActorRef = Actor.noSender - var galaxy : ActorRef = Actor.noSender + var cluster : ActorRef = Actor.noSender var continent : Zone = Zone.Nowhere var player : Player = null var avatar : Avatar = null @@ -86,6 +94,8 @@ class WorldSessionActor extends Actor with MDCContextAware { var cargoMountTimer : Cancellable = DefaultCancellable.obj var cargoDismountTimer : Cancellable = DefaultCancellable.obj + var antChargingTick : Cancellable = DefaultCancellable.obj + var antDischargingTick : Cancellable = DefaultCancellable.obj /** * Convert a boolean value into an integer value. @@ -103,6 +113,7 @@ class WorldSessionActor extends Actor with MDCContextAware { localService ! Service.Leave() vehicleService ! Service.Leave() avatarService ! Service.Leave() + galaxyService ! Service.Leave() LivePlayerList.Remove(sessionId) if(player != null && player.HasGUID) { @@ -144,31 +155,20 @@ class WorldSessionActor extends Actor with MDCContextAware { else { //no items in inventory; leave no corpse val player_guid = player.GUID player.Position = Vector3.Zero //save character before doing this - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) } case Some(vehicle_guid) => val player_guid = player.GUID player.Position = Vector3.Zero //save character before doing this - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid)) taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) DismountVehicleOnLogOut() } } - player.VehicleOwned match { - case Some(vehicle_guid) => - continent.GUID(vehicle_guid) match { - case Some(vehicle : Vehicle) => - vehicle.Owner = None - //TODO temporary solution; to un-own, permit driver seat to Empire access level - vehicle.PermissionGroup(10, VehicleLockState.Empire.id) - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.SeatPermissions(player_guid, vehicle_guid, 10, VehicleLockState.Empire.id)) - case _ => ; - } - case None => ; - } + DisownVehicle() continent.Population ! Zone.Population.Leave(avatar) } } @@ -187,7 +187,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(vehicle : Vehicle) => vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None if(vehicle.Seats.values.count(_.isOccupied) == 0) { - vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 600L) //start vehicle decay (10m) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent), vehicle.Definition.DeconstructionTime) //start vehicle decay } vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) @@ -216,6 +216,7 @@ class WorldSessionActor extends Actor with MDCContextAware { ServiceManager.serviceManager ! Lookup("local") ServiceManager.serviceManager ! Lookup("vehicle") ServiceManager.serviceManager ! Lookup("taskResolver") + ServiceManager.serviceManager ! Lookup("cluster") ServiceManager.serviceManager ! Lookup("galaxy") case _ => @@ -237,8 +238,11 @@ class WorldSessionActor extends Actor with MDCContextAware { taskResolver = endpoint log.info("ID: " + sessionId + " Got task resolver service " + endpoint) case ServiceManager.LookupResult("galaxy", endpoint) => - galaxy = endpoint + galaxyService = endpoint log.info("ID: " + sessionId + " Got galaxy service " + endpoint) + case ServiceManager.LookupResult("cluster", endpoint) => + cluster = endpoint + log.info("ID: " + sessionId + " Got cluster service " + endpoint) case ControlPacket(_, ctrl) => handleControlPkt(ctrl) @@ -354,7 +358,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) => if(tplayer_guid != guid) { - sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3(0,0,0), 0f, 0f, 0f)) + sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) sendResponse( ObjectCreateMessage( ammo_id, @@ -386,34 +390,19 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(GenericObjectActionMessage(guid, 36)) } - case AvatarResponse.EquipmentInHand(target, slot, item) => + case msg @ AvatarResponse.DropItem(pkt) => if(tplayer_guid != guid) { - val definition = item.Definition - sendResponse( - ObjectCreateMessage( - definition.ObjectId, - item.GUID, - ObjectCreateMessageParent(target, slot), - definition.Packet.ConstructorData(item).get - ) - ) + sendResponse(pkt) } - case msg @ AvatarResponse.EquipmentOnGround(pos, orient, item_id, item_guid, item_data) => + case AvatarResponse.EquipmentInHand(pkt) => if(tplayer_guid != guid) { - log.info(s"now dropping a $msg") - sendResponse( - ObjectCreateMessage( - item_id, - item_guid, - DroppedItemData(PlacementData(pos, Vector3(0f, 0f, orient.z)), item_data) - ) - ) + sendResponse(pkt) } - case AvatarResponse.LoadPlayer(pdata) => + case AvatarResponse.LoadPlayer(pkt) => if(tplayer_guid != guid) { - sendResponse(ObjectCreateMessage(ObjectClass.avatar, guid, pdata)) + sendResponse(pkt) } case AvatarResponse.ObjectDelete(item_guid, unk) => @@ -461,7 +450,7 @@ class WorldSessionActor extends Actor with MDCContextAware { msg.facingYaw, msg.facingPitch, msg.facingYawUpper, - 0, + unk1 = 0, msg.is_crouching, msg.is_jumping, msg.jump_thrust, @@ -503,8 +492,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } + case GalaxyServiceResponse(_, reply) => + reply match { + case GalaxyResponse.MapUpdate(msg) => + sendResponse(msg) + } + case LocalServiceResponse(_, guid, reply) => - val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } + val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(-1) } reply match { case LocalResponse.DoorOpens(door_guid) => if(tplayer_guid != guid) { @@ -515,15 +510,25 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(GenericObjectStateMsg(door_guid, 17)) case LocalResponse.HackClear(target_guid, unk1, unk2) => + // Reset hack state for all players sendResponse(HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)) + // Set the object faction displayed back to it's original owner faction + sendResponse(SetEmpireMessage(target_guid, continent.GUID(target_guid).get.asInstanceOf[FactionAffinity].Faction)) case LocalResponse.HackObject(target_guid, unk1, unk2) => - if(player.GUID != guid) { + if(tplayer_guid != guid && continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction != player.Faction) { + // If the player is not in the faction that hacked this object then send the packet that it's been hacked, so they can either unhack it or use the hacked object + // Don't send this to the faction that hacked the object, otherwise it will interfere with the new SetEmpireMessage QoL change that changes the object colour to their faction (but only visible to that faction) sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) } + if(continent.GUID(target_guid).get.asInstanceOf[Hackable].HackedBy.get._1.Faction == player.Faction){ + // Make the hacked object look like it belongs to the hacking empire, but only for that empire's players (so that infiltrators on stealth missions won't be given away to opposing factions) + sendResponse(SetEmpireMessage(target_guid, player.Faction)) + } + case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => - if(player.GUID != guid) { + if(tplayer_guid != guid) { sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) } @@ -536,8 +541,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleServiceResponse(_, guid, reply) => val tplayer_guid = if(player.HasGUID) { player.GUID} else { PlanetSideGUID(0) } reply match { - case VehicleResponse.Awareness(vehicle_guid) => - //resets exclamation point fte marker (once) + case VehicleResponse.Ownership(vehicle_guid) => sendResponse(PlanetsideAttributeMessage(guid, 21, vehicle_guid.guid.toLong)) case VehicleResponse.AttachToRails(vehicle_guid, pad_guid) => @@ -564,7 +568,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } case VehicleResponse.DetachFromRails(vehicle_guid, pad_guid, pad_position, pad_orientation_z) => - sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0,0,0.5f), 0, 0, pad_orientation_z)) + sendResponse(ObjectDetachMessage(pad_guid, vehicle_guid, pad_position + Vector3(0,0,0.5f), pad_orientation_z)) case VehicleResponse.InventoryState(obj, parent_guid, start, con_data) => if(tplayer_guid != guid) { @@ -686,7 +690,6 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) DeploymentActivities(obj) - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global context.system.scheduler.scheduleOnce(obj.DeployTime milliseconds, obj.Actor, Deployment.TryDeploy(DriveState.Deployed)) } @@ -708,7 +711,6 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(DeployRequestMessage(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.DeployRequest(player.GUID, vehicle_guid, state, 0, false, Vector3.Zero)) DeploymentActivities(obj) - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global context.system.scheduler.scheduleOnce(obj.UndeployTime milliseconds, obj.Actor, Deployment.TryUndeploy(DriveState.Mobile)) } @@ -726,6 +728,17 @@ class WorldSessionActor extends Actor with MDCContextAware { case Deployment.CanNotChangeDeployment(obj, state, reason) => CanNotChangeDeployment(obj, state, reason) + case ResourceSilo.ResourceSiloMessage(tplayer, msg, order) => + val vehicle_guid = msg.avatar_guid + val silo_guid = msg.object_guid + order match { + case ResourceSilo.ChargeEvent() => + antChargingTick.cancel() // If an ANT is refilling a NTU silo it isn't in a warpgate, so disable NTU regeneration + antDischargingTick.cancel() + + antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle], silo_guid)) + } + case Door.DoorMessage(tplayer, msg, order) => val door_guid = msg.object_guid order match { @@ -761,7 +774,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val obj_guid : PlanetSideGUID = obj.GUID val player_guid : PlanetSideGUID = tplayer.GUID log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num") - vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(obj_guid) //clear all deconstruction timers + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(obj), continent)) //clear timer PlayerActionsToCancel() if(seat_num == 0) { //simplistic vehicle ownership management obj.Owner match { @@ -815,13 +828,19 @@ class WorldSessionActor extends Actor with MDCContextAware { vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID)) } if(obj.Seats.values.count(_.isOccupied) == 0) { - vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj, continent, 600L) //start vehicle decay (10m) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, obj.Definition.DeconstructionTime)) //start vehicle decay } case Mountable.CanDismount(obj : Mountable, _) => log.warn(s"DismountVehicleMsg: $obj is some generic mountable object and nothing will happen") - case Mountable.CanNotMount(obj, seat_num) => + case Mountable.CanNotMount(obj : Vehicle, seat_num) => + log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") + if(obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) { + sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None)) + } + + case Mountable.CanNotMount(obj : Mountable, seat_num) => log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") case Mountable.CanNotDismount(obj, seat_num) => @@ -945,12 +964,10 @@ class WorldSessionActor extends Actor with MDCContextAware { }) //drop items on ground val pos = tplayer.Position - val orient = tplayer.Orientation + val orient = Vector3(0,0, tplayer.Orientation.z) ((dropHolsters ++ dropInventory).map(_.obj) ++ drop).foreach(obj => { - continent.Ground ! Zone.DropItemOnGround(obj, pos, Vector3(0f, 0f, orient.z)) - sendResponse(ObjectDetachMessage(tplayer.GUID, obj.GUID, pos, 0f, 0f, orient.z)) - val objDef = obj.Definition - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get)) + //TODO make a sound when dropping stuff + continent.Ground ! Zone.Ground.DropItem(obj, pos, orient) }) sendResponse(ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)) } @@ -1039,12 +1056,9 @@ class WorldSessionActor extends Actor with MDCContextAware { }) //drop stuff on ground val pos = tplayer.Position - val orient = tplayer.Orientation + val orient = Vector3(0,0, tplayer.Orientation.z) ((dropHolsters ++ dropInventory).map(_.obj)).foreach(obj => { - continent.Ground ! Zone.DropItemOnGround(obj, pos, Vector3(0f, 0f, orient.z)) - sendResponse(ObjectDetachMessage(tplayer.GUID, obj.GUID, pos, 0f, 0f, orient.z)) - val objDef = obj.Definition - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, objDef.ObjectId, obj.GUID, objDef.Packet.ConstructorData(obj).get)) + continent.Ground ! Zone.Ground.DropItem(obj, pos, orient) }) case Terminal.VehicleLoadout(definition, weapons, inventory) => @@ -1260,25 +1274,22 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)) } - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off? - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player.GUID.guid)) //fte and ownership? + sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off + sendResponse(PlanetsideAttributeMessage(vehicle_guid, 21, player.GUID.guid)) //ownership case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle, pad) => val vehicle_guid = vehicle.GUID - if(player.VehicleSeated.nonEmpty) { - vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) - } - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on? + sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 0L)) //mount points on //sendResponse(PlanetsideAttributeMessage(vehicle_guid, 0, 10))//vehicle.Definition.MaxHealth)) - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) //??? - sendResponse(PlanetsideAttributeMessage(vehicle_guid, 113, 0L)) //??? + sendResponse(PlanetsideAttributeMessage(vehicle_guid, 68, 0L)) // Shield health + sendResponse(PlanetsideAttributeMessage(vehicle_guid, 113, 0L)) // Capacitor (EMP) ReloadVehicleAccessPermissions(vehicle) ServerVehicleLock(vehicle) case VehicleSpawnPad.ServerVehicleOverrideStart(vehicle, pad) => val vdef = vehicle.Definition if(vehicle.Seats(0).isOccupied) { - sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3(0, 0, 0.5f), 0, 0, pad.Orientation.z)) + sendResponse(ObjectDetachMessage(pad.GUID, vehicle.GUID, pad.Position + Vector3(0, 0, 0.5f), pad.Orientation.z)) } ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef):Int) @@ -1341,6 +1352,7 @@ class WorldSessionActor extends Actor with MDCContextAware { RemoveCharacterSelectScreenGUID(player) sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)) + sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0)) case VehicleLoaded(_/*vehicle*/) => ; //currently being handled by VehicleSpawnPad.LoadVehicle during testing phase @@ -1443,6 +1455,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } } else { + DisownVehicle() continent.Population ! Zone.Population.Leave(avatar) val original = player //TODO check player orientation upon spawn not polluted @@ -1456,7 +1469,6 @@ class WorldSessionActor extends Actor with MDCContextAware { (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterAvatar(original)(continent.GUID), zone_id)) } } - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global respawnTimer = context.system.scheduler.scheduleOnce(respawnTime seconds, target, msg) @@ -1470,23 +1482,67 @@ class WorldSessionActor extends Actor with MDCContextAware { reviveTimer.cancel if(spawn_group == 2) { sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "No friendly AMS is deployed in this region.", None)) - galaxy ! Zone.Lattice.RequestSpawnPoint(zone_number, player, 0) + cluster ! Zone.Lattice.RequestSpawnPoint(zone_number, player, 0) } else { RequestSanctuaryZoneSpawn(player, zone_number) } + case Zone.Ground.ItemOnGround(item, pos, orient) => + item.Position = pos + item.Orientation = Vector3(0,0, orient.z) //dropped items rotate towards the user's standing direction + val exclusionId = player.Find(item) match { + case Some(slotNum) => + player.Slot(slotNum).Equipment = None + sendResponse(ObjectDetachMessage(player.GUID, item.GUID, pos, orient.z)) + sendResponse(ActionResultMessage.Pass) + player.GUID //we're dropping it; don't need to see it dropped again + case None => + PlanetSideGUID(0) //object is being introduced into the world upon drop + } + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(exclusionId, item, continent)) + + case Zone.Ground.CanNotDropItem(zone, item, reason) => + log.warn(s"DropItem: $player tried to drop a $item on the ground, but $reason") + + case Zone.Ground.ItemInHand(item) => + player.Fit(item) match { + case Some(slotNum) => + val item_guid = item.GUID + val player_guid = player.GUID + player.Slot(slotNum).Equipment = item + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item_guid, + ObjectCreateMessageParent(player_guid, slotNum), + definition.Packet.DetailedConstructorData(item).get + ) + ) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PickupItem(player_guid, continent, player, slotNum, item)) + case None => + continent.Ground ! Zone.Ground.DropItem(item, item.Position, item.Orientation) //restore previous state + } + + case Zone.Ground.CanNotPickupItem(zone, item_guid, _) => + zone.GUID(item_guid) match { + case Some(item) => + log.warn(s"DropItem: finding a $item on the ground was suggested, but $player can not reach it") + case None => + log.warn(s"DropItem: finding an item ($item_guid) on the ground was suggested, but $player can not see it") + } + case InterstellarCluster.ClientInitializationComplete() => StopBundlingPackets() LivePlayerList.Add(sessionId, avatar) traveler = new Traveler(self, continent.Id) //PropertyOverrideMessage - sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1)) sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil)) - galaxy ! InterstellarCluster.GetWorld("z6") + cluster ! InterstellarCluster.GetWorld("z6") case InterstellarCluster.GiveWorld(zoneId, zone) => log.info(s"Zone $zoneId will now load") @@ -1521,7 +1577,6 @@ class WorldSessionActor extends Actor with MDCContextAware { if(!corpse.isAlive && corpse.HasGUID) { corpse.VehicleSeated match { case Some(_) => - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(corpse)) case None => @@ -1533,18 +1588,29 @@ class WorldSessionActor extends Actor with MDCContextAware { player = tplayer val guid = tplayer.GUID StartBundlingPackets() - sendResponse(SetCurrentAvatarMessage(guid,0,0)) + sendResponse(SetCurrentAvatarMessage(guid, 0, 0)) + sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on //TODO once per respawn? sendResponse(PlayerStateShiftMessage(ShiftState(1, tplayer.Position, tplayer.Orientation.z))) + //transfer vehicle ownership + player.VehicleOwned match { + case Some(vehicle_guid) => + continent.GUID(vehicle_guid) match { + case Some(vehicle : Vehicle) => + vehicle.Owner = player + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.Ownership(guid, vehicle_guid)) + case _ => + player.VehicleOwned = None + } + case None => ; + } if(spectator) { sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, false, "", "on", None)) } - (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => { sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant //TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63 }) - sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0)) //TODO if Medkit does not have shortcut, add to a free slot or write over slot 64 sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)) @@ -1552,45 +1618,97 @@ class WorldSessionActor extends Actor with MDCContextAware { //FavoritesMessage sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this deadState = DeadState.Alive - sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0,0, tplayer.Position, player.Faction, true)) + sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true)) sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) - sendResponse(AvatarSearchCriteriaMessage(guid, List(0,0,0,0,0,0))) + sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) (1 to 73).foreach(i => { sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0)) }) - (0 to 30).foreach(i => { //TODO 30 for a new character only? + (0 to 30).foreach(i => { + //TODO 30 for a new character only? sendResponse(AvatarStatisticsMessage(2, Statistics(0L))) }) //AvatarAwardMessage //DisplayAwardMessage //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage - //MapObjectStateBlockMessage and ObjectCreateMessage - //TacticsMessage + //MapObjectStateBlockMessage and ObjectCreateMessage? + //TacticsMessage? StopBundlingPackets() + case NtuCharging(tplayer, vehicle) => + log.trace(s"NtuCharging: Vehicle ${vehicle.GUID} is charging NTU capacitor.") - case Zone.ItemFromGround(tplayer, item) => - val obj_guid = item.GUID - val player_guid = tplayer.GUID - tplayer.Fit(item) match { - case Some(slot) => - tplayer.Slot(slot).Equipment = item - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.ObjectDelete(player_guid, obj_guid)) - val definition = item.Definition - sendResponse( - ObjectCreateDetailedMessage( - definition.ObjectId, - obj_guid, - ObjectCreateMessageParent(player_guid, slot), - definition.Packet.DetailedConstructorData(item).get - ) - ) - sendResponse(ActionResultMessage()) - if(tplayer.VisibleSlots.contains(slot)) { - avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentInHand(player_guid, player_guid, slot, item)) + if(vehicle.Capacitor < vehicle.Definition.MaximumCapacitor) { + // Charging + vehicle.Capacitor += 100 + + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10) )) // set ntu on vehicle UI + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 1L)) // orb particle effect on + + antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) // Repeat until fully charged + } else { + // Fully charged + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10).toInt)) // set ntu on vehicle UI + + // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side + context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) + } + + case NtuDischarging(tplayer, vehicle, silo_guid) => + log.trace(s"NtuDischarging: Vehicle ${vehicle.GUID} is discharging NTU into silo $silo_guid") + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 49, 0L)) // orb particle effect off + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 1L)) // panel glow on + + var silo = continent.GUID(silo_guid).get.asInstanceOf[ResourceSilo] + + // Check vehicle is still deployed before continuing. User can undeploy manually or vehicle may not longer be present. + if(vehicle.DeploymentState == DriveState.Deployed) { + if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) { + + // Make sure we don't exceed the silo maximum charge or remove much NTU from ANT if maximum is reached, or try to make ANT go below 0 NTU + var chargeToDeposit = Math.min(Math.min(vehicle.Capacitor, 100), (silo.MaximumCharge - silo.ChargeLevel)) + vehicle.Capacitor -= chargeToDeposit + silo.Actor ! ResourceSilo.UpdateChargeLevel(chargeToDeposit) + + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 1L)) // panel glow on & orb particles on + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 45, scala.math.round((vehicle.Capacitor.toFloat / vehicle.Definition.MaximumCapacitor.toFloat) * 10))) // set ntu on vehicle UI + + //todo: grant BEP to user + //todo: grant BEP to squad in range + //todo: notify map service to update ntu % on map for all users + + //todo: handle silo orb / panel glow properly if more than one person is refilling silo and one player stops. effects should stay on until all players stop + + if(vehicle.Capacitor > 0 && silo.ChargeLevel < silo.MaximumCharge) { + log.trace(s"NtuDischarging: ANT not empty and Silo not full. Scheduling another discharge") + // Silo still not full and ant still has charge left - keep rescheduling ticks + antDischargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuDischarging(player, vehicle, silo_guid)) + } else { + log.trace(s"NtuDischarging: ANT NTU empty or Silo NTU full.") + + // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side + context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) + + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off + antDischargingTick.cancel() } - case None => - continent.Ground ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore + } else { + // This shouldn't normally be run, only if the client thinks the ANT has capacitor charge when it doesn't, or thinks the silo isn't full when it is. + log.warn(s"NtuDischarging: Invalid discharge state. ANT Capacitor: ${vehicle.Capacitor} Silo Capacitor: ${silo.ChargeLevel}") + + // Turning off glow/orb effects on ANT doesn't seem to work when deployed. Try to undeploy ANT from server side + context.system.scheduler.scheduleOnce(vehicle.UndeployTime milliseconds, vehicle.Actor, Deployment.TryUndeploy(DriveState.Undeploying)) + + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off + antDischargingTick.cancel() + } + } else { + log.trace(s"NtuDischarging: Vehicle is no longer deployed. Removing effects") + // Vehicle has changed from deployed and this should be the last timer tick sent + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 52, 0L)) // panel glow off + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(silo_guid, 49, 0L)) // panel glow off & orb particles off + antDischargingTick.cancel() } case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) => @@ -1610,13 +1728,12 @@ class WorldSessionActor extends Actor with MDCContextAware { if(progressBarVal > 100) { //done progressBarValue = None log.info(s"Hacked a $target") - sendResponse(HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)) +// sendResponse(HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)) completeAction() } else { //continue next tick tickAction.getOrElse(() => Unit)() progressBarValue = Some(progressBarVal) - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta, completeAction)) } @@ -1655,7 +1772,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //TODO begin temp player character auto-loading; remove later import net.psforever.objects.GlobalDefinitions._ import net.psforever.types.CertificationType._ - avatar = Avatar("TestCharacter" + sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) + avatar = Avatar("TestCharacter" + sessionId.toString, PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1) avatar.Certifications += StandardAssault avatar.Certifications += MediumAssault avatar.Certifications += StandardExoSuit @@ -1704,11 +1821,9 @@ class WorldSessionActor extends Actor with MDCContextAware { player.Locker.Inventory += 0 -> SimpleItem(remote_electronics_kit) //TODO end temp player character auto-loading self ! ListAccountCharacters - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global clientKeepAlive.cancel clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient()) - log.warn(PacketCoding.DecodePacket(hex"d2327e7b8a972b95113881003710").toString) case msg @ DismountVehicleCargoMsg(player_guid, vehicle_guid, bailed, requestedByPassenger, kicked) => log.info(msg.toString) @@ -1803,19 +1918,19 @@ class WorldSessionActor extends Actor with MDCContextAware { cargoMountTimer = context.system.scheduler.scheduleOnce(1 second, self, CheckCargoMounting(vehicle_guid, cargo_vehicle_guid, cargo_mountpoint, iteration = 0)) case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) => log.info("Handling " + msg) - sendResponse(ActionResultMessage(true, None)) + sendResponse(ActionResultMessage.Pass) self ! ListAccountCharacters case msg @ CharacterRequestMessage(charId, action) => log.info("Handling " + msg) action match { case CharacterRequestAction.Delete => - sendResponse(ActionResultMessage(false, Some(1))) + sendResponse(ActionResultMessage.Fail(1)) case CharacterRequestAction.Select => //TODO check if can spawn on last continent/location from player? //TODO if yes, get continent guid accessors //TODO if no, get sanctuary guid accessors and reset the player's expectations - galaxy ! InterstellarCluster.RequestClientInitialization() + cluster ! InterstellarCluster.RequestClientInitialization() case default => log.error("Unsupported " + default + " in " + msg) } @@ -1830,6 +1945,7 @@ class WorldSessionActor extends Actor with MDCContextAware { avatarService ! Service.Join(continent.Id) localService ! Service.Join(continent.Id) vehicleService ! Service.Join(continent.Id) + galaxyService ! Service.Join("galaxy") configZone(continent) sendResponse(TimeOfDayMessage(1191182336)) //custom @@ -1850,34 +1966,39 @@ class WorldSessionActor extends Actor with MDCContextAware { ) }) //load active players in zone - continent.LivePlayers.filterNot(_.GUID == player.GUID).foreach(char => { - sendResponse(ObjectCreateMessage(ObjectClass.avatar, char.GUID, char.Definition.Packet.ConstructorData(char).get)) - if(char.UsingSpecial == SpecialExoSuitDefinition.Mode.Anchored) { - sendResponse(PlanetsideAttributeMessage(char.GUID, 19, 1)) - } - }) + continent.LivePlayers + .filterNot(tplayer => { tplayer.GUID == player.GUID || tplayer.VehicleSeated.nonEmpty }) + .foreach(char => { + val tdefintion = char.Definition + sendResponse(ObjectCreateMessage(tdefintion.ObjectId, char.GUID, char.Definition.Packet.ConstructorData(char).get)) + if(char.UsingSpecial == SpecialExoSuitDefinition.Mode.Anchored) { + sendResponse(PlanetsideAttributeMessage(char.GUID, 19, 1)) + } + }) //load corpses in zone continent.Corpses.foreach { TurnPlayerIntoCorpse } //load active vehicles in zone continent.Vehicles.foreach(vehicle => { - val definition = vehicle.Definition - sendResponse(ObjectCreateMessage(definition.ObjectId, vehicle.GUID, definition.Packet.ConstructorData(vehicle).get)) - //seat vehicle occupants - definition.MountPoints.values.foreach(seat_num => { - vehicle.Seat(seat_num) match { - case Some(seat) => - seat.Occupant match { - case Some(tplayer) => - if(tplayer.HasGUID) { - sendResponse(ObjectAttachMessage(vehicle.GUID, tplayer.GUID, seat_num)) - } - case None => ; // No player seated - } - case None => ; // Not a seat mounting point - } - }) + val vehicle_guid = vehicle.GUID + val vdefinition = vehicle.Definition + sendResponse(ObjectCreateMessage(vdefinition.ObjectId, vehicle_guid, vdefinition.Packet.ConstructorData(vehicle).get)) + //occupants other than driver + vehicle.Seats + .filter({ case(index, seat) => seat.isOccupied && index > 0 }) + .foreach({ case(index, seat) => + val tplayer = seat.Occupant.get + val tdefintion = tplayer.Definition + sendResponse( + ObjectCreateMessage( + tdefintion.ObjectId, + tplayer.GUID, + ObjectCreateMessageParent(vehicle_guid, index), + tdefintion.Packet.ConstructorData(tplayer).get + ) + ) + }) ReloadVehicleAccessPermissions(vehicle) }) @@ -1906,7 +2027,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val objDef = obj.Definition sendResponse( ObjectCreateMessage( - ObjectClass.implant_terminal_interface, + objDef.ObjectId, PlanetSideGUID(interface_guid), ObjectCreateMessageParent(parent_guid, 1), objDef.Packet.ConstructorData(obj).get @@ -1917,15 +2038,19 @@ class WorldSessionActor extends Actor with MDCContextAware { //seat terminal occupants continent.GUID(terminal_guid) match { case Some(obj : Mountable) => - obj.MountPoints.foreach({ case ((_, seat_num)) => - obj.Seat(seat_num).get.Occupant match { - case Some(tplayer) => - if(tplayer.HasGUID) { - sendResponse(ObjectAttachMessage(parent_guid, tplayer.GUID, seat_num)) - } - case None => ; - } - }) + obj.Seats(0).Occupant match { + case Some(tplayer) => + val tdefintion = tplayer.Definition + sendResponse( + ObjectCreateMessage( + tdefintion.ObjectId, + tplayer.GUID, + ObjectCreateMessageParent(parent_guid, 0), + tdefintion.Packet.ConstructorData(tplayer).get + ) + ) + case None => ; + } case _ => ; } }) @@ -2039,20 +2164,19 @@ class WorldSessionActor extends Actor with MDCContextAware { avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) self ! PacketCoding.CreateGamePacket(0, DismountVehicleMsg(player_guid, BailType.Normal, true)) //let vehicle try to clean up its fields - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global context.system.scheduler.scheduleOnce(50 milliseconds, self, UnregisterCorpseOnVehicleDisembark(player)) - //sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0, 0, 0)) + //sendResponse(ObjectDetachMessage(vehicle_guid, player.GUID, Vector3.Zero, 0)) //sendResponse(PlayerStateShiftMessage(ShiftState(1, Vector3.Zero, 0))) } case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) => log.info(s"SpawnRequestMessage: $msg") //TODO just focus on u5 and u2 for now - galaxy ! Zone.Lattice.RequestSpawnPoint(u5.toInt, player, u2.toInt) + cluster ! Zone.Lattice.RequestSpawnPoint(u5.toInt, player, u2.toInt) case msg @ SetChatFilterMessage(send_channel, origin, whitelist) => - log.info("SetChatFilters: " + msg) + //log.info("SetChatFilters: " + msg) case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) => var makeReply : Boolean = true @@ -2114,7 +2238,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case (false, _) => ; } - + // TODO: Prevents log spam, but should be handled correctly if(messagetype != ChatMessageType.CMT_TOGGLE_GM) { log.info("Chat: " + msg) @@ -2152,7 +2276,7 @@ class WorldSessionActor extends Actor with MDCContextAware { else if(trimContents.equals("!ams")) { makeReply = false if(player.isBackpack) { //player is on deployment screen (either dead or deconstructed) - galaxy ! Zone.Lattice.RequestSpawnPoint(continent.Number, player, 2) + cluster ! Zone.Lattice.RequestSpawnPoint(continent.Number, player, 2) } } // TODO: Depending on messagetype, may need to prepend sender's name to contents with proper spacing @@ -2203,8 +2327,8 @@ class WorldSessionActor extends Actor with MDCContextAware { val tailReloadValue : Int = if(xs.isEmpty) { 0 } else { xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).reduceLeft(_ + _) } val sumReloadValue : Int = originalBoxCapacity + tailReloadValue val previousBox = tool.AmmoSlot.Box //current magazine in tool - sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f)) - sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3(0f, 0f, 0f), 0f, 0f, 0f)) + sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f)) + sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f)) obj.Inventory -= x.start //remove replacement ammo from inventory val ammoSlotIndex = tool.FireMode.AmmoSlotIndex tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool @@ -2236,7 +2360,6 @@ class WorldSessionActor extends Actor with MDCContextAware { //divide capacity across other existing and not full boxes of that ammo type var capacity = previousBox.Capacity val iter = obj.Inventory.Items - .map({case(_, entry) => entry }) .filter(entry => { entry.obj match { case (item : AmmoBox) => @@ -2372,27 +2495,37 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(EmoteMsg(avatar_guid, emote)) case msg @ DropItemMessage(item_guid) => - log.info("DropItem: " + msg) - player.FreeHand.Equipment match { - case Some(item) => - if(item.GUID == item_guid) { - val orient : Vector3 = Vector3(0f, 0f, player.Orientation.z) - player.FreeHand.Equipment = None - continent.Ground ! Zone.DropItemOnGround(item, player.Position, orient) - sendResponse(ObjectDetachMessage(player.GUID, item.GUID, player.Position, 0f, 0f, player.Orientation.z)) - val objDef = item.Definition - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.EquipmentOnGround(player.GUID, player.Position, orient, objDef.ObjectId, item.GUID, objDef.Packet.ConstructorData(item).get)) - } - else { - log.warn(s"item in hand was ${item.GUID} but trying to drop $item_guid; nothing will be dropped") + log.info(s"DropItem: $msg") + continent.GUID(item_guid) match { + case Some(item : Equipment) => + player.FreeHand.Equipment match { + case Some(_) => + if(item.GUID == item_guid) { + continent.Ground ! Zone.Ground.DropItem(item, player.Position, player.Orientation) + } + case None => + log.warn(s"DropItem: $player wanted to drop a $item, but it wasn't at hand") } + case Some(obj) => //TODO LLU + log.warn(s"DropItem: $player wanted to drop a $obj, but that isn't possible") case None => - log.error(s"$player wanted to drop an item, but it was not in hand") + log.warn(s"DropItem: $player wanted to drop an item ($item_guid), but it was nowhere to be found") } case msg @ PickupItemMessage(item_guid, player_guid, unk1, unk2) => - log.info("PickupItem: " + msg) - continent.Ground ! Zone.GetItemOnGround(player, item_guid) + log.info(s"PickupItem: $msg") + continent.GUID(item_guid) match { + case Some(item : Equipment) => + player.Fit(item) match { + case Some(_) => + continent.Ground ! Zone.Ground.PickupItem(item_guid) + case None => //skip + sendResponse(ActionResultMessage.Fail(16)) //error code? + } + case _ => + log.warn(s"PickupItem: $player requested an item that doesn't exist in this zone; assume client-side garbage data") + sendResponse(ObjectDeleteMessage(item_guid, 0)) + } case msg @ ReloadMessage(item_guid, ammo_clip, unk1) => log.info("Reload: " + msg) @@ -2452,6 +2585,23 @@ class WorldSessionActor extends Actor with MDCContextAware { } else if((player.DrawnSlot = held_holsters) != before) { avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)) + + + // Ignore non-equipment holsters + //todo: check current suit holster slots? + if(held_holsters >= 0 && held_holsters < 5) { + player.Holsters()(held_holsters).Equipment match { + case Some(unholsteredItem : Equipment) => + if(unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { + // Player has ulholstered a REK - we need to set an atttribute on the REK itself to change the beam/icon colour to the correct one for the player's hack level + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, GetPlayerHackData().hackLevel)) + } + case None => ; + } + + } + + // Stop using proximity terminals if player unholsters a weapon (which should re-trigger the proximity effect and re-holster the weapon) if(player.VisibleSlots.contains(held_holsters)) { usingMedicalTerminal match { case Some(term_guid) => @@ -2490,12 +2640,12 @@ class WorldSessionActor extends Actor with MDCContextAware { if((player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) || (player.Faction == vehicle.Faction && ((vehicle.Owner.isEmpty || continent.GUID(vehicle.Owner.get).isEmpty) || vehicle.Health == 0))) { - vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(object_guid) - vehicleService ! VehicleServiceMessage.RequestDeleteVehicle(vehicle, continent) - log.info(s"RequestDestroy: vehicle $object_guid") + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.ClearSpecific(List(vehicle), continent)) + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(vehicle, continent, Some(0 seconds))) + log.info(s"RequestDestroy: vehicle $vehicle") } else { - log.info(s"RequestDestroy: must own vehicle $object_guid in order to deconstruct it") + log.info(s"RequestDestroy: must own vehicle in order to deconstruct it") } case Some(obj : Equipment) => @@ -2517,20 +2667,28 @@ class WorldSessionActor extends Actor with MDCContextAware { }) match { case Some((parent, Some(slot))) => + obj.Position = Vector3.Zero taskResolver ! RemoveEquipmentFromSlot(parent, obj, slot) - log.info(s"RequestDestroy: equipment $object_guid") + log.info(s"RequestDestroy: equipment $obj") case _ => - //TODO search for item on ground - sendResponse(ObjectDeleteMessage(object_guid, 0)) - log.warn(s"RequestDestroy: object $object_guid not found") + if(continent.EquipmentOnGround.contains(obj)) { + obj.Position = Vector3.Zero + continent.Ground ! Zone.Ground.RemoveItem(object_guid) + avatarService ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(PlanetSideGUID(0), object_guid)) + log.info(s"RequestDestroy: equipment $obj on ground") + } + else { + log.warn(s"RequestDestroy: equipment $obj exists, but can not be reached") + } } + case Some(thing) => + log.warn(s"RequestDestroy: not allowed to delete object $thing") + case None => log.warn(s"RequestDestroy: object $object_guid not found") - - case _ => - log.warn(s"RequestDestroy: not allowed to delete object $object_guid") } case msg @ ObjectDeleteMessage(object_guid, unk1) => @@ -2624,7 +2782,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ AvatarImplantMessage(_, _, _, _) => //(player_guid, unk1, unk2, implant) => log.info("AvatarImplantMessage: " + msg) - case msg @ UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) => + case msg @ UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) => log.info("UseItem: " + msg) // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok) // TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg) @@ -2641,14 +2799,27 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(GenericObjectStateMsg(object_guid, 16)) } + case Some(resourceSilo : ResourceSilo) => + log.info(s"UseItem: Vehicle $avatar_guid is refilling resource silo $object_guid") + val vehicle = continent.GUID(avatar_guid).get.asInstanceOf[Vehicle] + + if(resourceSilo.Faction == PlanetSideEmpire.NEUTRAL || player.Faction == resourceSilo.Faction) { + if(vehicle.Seat(0).get.Occupant.contains(player)) { + log.trace("UseItem: Player matches vehicle driver. Calling ResourceSilo.Use") + resourceSilo.Actor ! ResourceSilo.Use(player, msg) + } + } else { + log.warn(s"Player ${player.GUID} - ${player.Faction} tried to refill silo ${resourceSilo.GUID} - ${resourceSilo.Faction} belonging to another empire") + } + + case Some(panel : IFFLock) => if(panel.Faction != player.Faction && panel.HackedBy.isEmpty) { player.Slot(player.DrawnSlot).Equipment match { case Some(tool : SimpleItem) => if(tool.Definition == GlobalDefinitions.remote_electronics_kit) { - //TODO get player hack level (for now, presume 15s in intervals of 4/s) - progressBarValue = Some(-2.66f) - self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, FinishHackingDoor(panel, 1114636288L)) + progressBarValue = Some(-GetPlayerHackSpeed()) + self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, GetPlayerHackSpeed(), FinishHacking(panel, 1114636288L)) log.info("Hacking a door~") } case _ => ; @@ -2658,11 +2829,11 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(obj : Player) => if(obj.isBackpack) { log.info(s"UseItem: $player looting the corpse of $obj") - sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) accessedContainer = Some(obj) } else if(!unk3) { //potential kit use - continent.GUID(unk1) match { + continent.GUID(item_used_guid) match { case Some(kit : Kit) => player.Find(kit) match { case Some(index) => @@ -2678,7 +2849,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(index) => whenUsedLastKit = System.currentTimeMillis player.Slot(index).Equipment = None //remove from slot immediately; must exist on client for next packet - sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) sendResponse(ObjectDeleteMessage(kit.GUID, 0)) taskResolver ! GUIDTask.UnregisterEquipment(kit)(continent.GUID) //TODO better health/damage control workflow @@ -2700,16 +2871,26 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(item) => log.warn(s"UseItem: looking for Kit to use, but found $item instead") case None => - log.warn(s"UseItem: anticipated a Kit $unk1, but can't find it") + log.warn(s"UseItem: anticipated a Kit $item_used_guid, but can't find it") } } case Some(obj : Locker) => - if(player.Faction == obj.Faction) { + if(obj.Faction != player.Faction && obj.HackedBy.isEmpty) { + player.Slot(player.DrawnSlot).Equipment match { + case Some(tool: SimpleItem) => + if (tool.Definition == GlobalDefinitions.remote_electronics_kit) { + progressBarValue = Some(-GetPlayerHackSpeed()) + self ! WorldSessionActor.ItemHacking(player, obj, tool.GUID, GetPlayerHackSpeed(), FinishHacking(obj, 3212836864L)) + log.info("Hacking a locker") + } + case _ => ; + } + } else if(player.Faction == obj.Faction || !obj.HackedBy.isEmpty) { log.info(s"UseItem: $player accessing a locker") val container = player.Locker accessedContainer = Some(container) - sendResponse(UseItemMessage(avatar_guid, unk1, container.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, 456)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, container.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, 456)) } else { log.info(s"UseItem: not $player's locker") @@ -2727,14 +2908,15 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => true }) { //access to trunk - if(obj.AccessingTrunk.isEmpty) { + if(obj.AccessingTrunk.isEmpty && + (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.Owner.contains(player.GUID))) { obj.AccessingTrunk = player.GUID accessedContainer = Some(obj) AccessContents(obj) - sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) } else { - log.info(s"UseItem: $player can not cut in line while player ${obj.AccessingTrunk.get} is using $obj's trunk") + log.info(s"UseItem: $obj's trunk is not currently accessible for $player") } } else if(equipment.isDefined) { @@ -2764,14 +2946,29 @@ class WorldSessionActor extends Actor with MDCContextAware { else if(obj.Definition.isInstanceOf[RepairRearmSiloDefinition]) { FindLocalVehicle match { case Some(vehicle) => - sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) - sendResponse(UseItemMessage(avatar_guid, unk1, vehicle.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, vehicle.Definition.ObjectId)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, vehicle.GUID, unk2, unk3, unk4, unk5, unk6, unk7, unk8, vehicle.Definition.ObjectId)) case None => log.error("UseItem: expected seated vehicle, but found none") } } else { - sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + if(obj.Faction != player.Faction && obj.HackedBy.isEmpty) { + player.Slot(player.DrawnSlot).Equipment match { + case Some(tool: SimpleItem) => + if (tool.Definition == GlobalDefinitions.remote_electronics_kit) { + progressBarValue = Some(-GetPlayerHackSpeed()) + self ! WorldSessionActor.ItemHacking(player, obj, tool.GUID, GetPlayerHackSpeed(), FinishHacking(obj, 3212836864L)) + log.info("Hacking a terminal") + } + case _ => ; + } + } else if (obj.Faction == player.Faction || !obj.HackedBy.isEmpty) { + // If hacked only allow access to the faction that hacked it + // Otherwise allow the faction that owns the terminal to use it + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + } + } case Some(obj : SpawnTube) => @@ -2785,7 +2982,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(obj) => log.warn(s"UseItem: don't know how to handle $obj; taking a shot in the dark") - sendResponse(UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) + sendResponse(UseItemMessage(avatar_guid, item_used_guid, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)) case None => log.error(s"UseItem: can not find object $object_guid") @@ -3008,7 +3205,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. //todo: kick cargo passengers out. To be added after PR #216 is merged if(bailType == BailType.Bailed && seat_num == 0 && GlobalDefinitions.isFlightVehicle(obj.asInstanceOf[Vehicle].Definition)) { - vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj.asInstanceOf[Vehicle], continent, 0L) // Immediately deconstruct vehicle + vehicleService ! VehicleServiceMessage.Decon(RemoverActor.AddTask(obj, continent, Some(0 seconds))) // Immediately deconstruct vehicle } case None => @@ -3053,7 +3250,7 @@ class WorldSessionActor extends Actor with MDCContextAware { obj.Actor ! Deployment.TryDeploymentChange(deploy_state) case _ => - log.error(s"DeployRequest: can not find $vehicle_guid in scope; removing ownership to mitigate confusion") + log.error(s"DeployRequest: can not find $vehicle_guid in scope") player.VehicleOwned = None } } @@ -3094,7 +3291,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(seat) => seat.Occupant match { case Some(tplayer) => - if(vehicle.SeatPermissionGroup(mountpoint_num).contains(group) && tplayer != player) { + if(vehicle.SeatPermissionGroup(mountpoint_num).contains(group) && tplayer != player) { //can not kick self seat.Occupant = None tplayer.VehicleSeated = None vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(tplayer.GUID, 4, false, object_guid)) @@ -3439,6 +3636,29 @@ class WorldSessionActor extends Actor with MDCContextAware { }, List(RegisterVehicle(obj))) } + //TODO this may be useful for vehicle gating + def RegisterDrivenVehicle(obj : Vehicle, driver : Player) : TaskResolver.GiveTask = { + TaskResolver.GiveTask( + new Task() { + private val localVehicle = obj + private val localDriver = driver + + override def isComplete : Task.Resolution.Value = { + if(localVehicle.HasGUID && localDriver.HasGUID) { + Task.Resolution.Success + } + else { + Task.Resolution.Incomplete + } + } + + def Execute(resolver : ActorRef) : Unit = { + //TODO some kind of callback ... + resolver ! scala.util.Success(this) + } + }, List(RegisterAvatar(driver), RegisterVehicle(obj))) + } + /** * Construct tasking that removes the `Equipment` to `target`. * @param target what object that contains the `Equipment` @@ -3529,7 +3749,7 @@ class WorldSessionActor extends Actor with MDCContextAware { def TaskBeforeZoneChange(priorTask : TaskResolver.GiveTask, zoneId : String) : TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { - private val localService = galaxy + private val localService = cluster private val localMsg = InterstellarCluster.GetWorld(zoneId) override def isComplete : Task.Resolution.Value = priorTask.task.isComplete @@ -3606,20 +3826,25 @@ class WorldSessionActor extends Actor with MDCContextAware { } /** - * The process of hacking the `Door` `IFFLock` is completed. - * Pass the message onto the lock and onto the local events system. - * @param target the `IFFLock` belonging to the door that is being hacked + * The process of hacking an object is completed + * Pass the message onto the hackable object and onto the local events system. + * @param target the `Hackable` object that has been hacked * @param unk na; - * used by `HackingMessage` as `unk5` + * used by `HackMessage` as `unk5` * @see `HackMessage` */ //TODO add params here depending on which params in HackMessage are important - //TODO sound should be centered on IFFLock, not on player - private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = { - target.Actor ! CommonMessages.Hack(player) - localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackDoor, player.Position, 30, 0.49803925f)) + private def FinishHacking(target : PlanetSideServerObject with Hackable, unk : Long)() : Unit = { + // Wait for the target actor to set the HackedBy property, otherwise LocalAction.HackTemporarily will not complete properly + import scala.concurrent.ExecutionContext.Implicits.global + ask(target.Actor, CommonMessages.Hack(player))(1 second).mapTo[Boolean].onComplete { + case Success(_) => + localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, target.HackSound, player.Position, 30, 0.49803925f)) localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk)) + case scala.util.Failure(_) => log.warn(s"Hack message failed on target guid: ${target.GUID}") } + } + /** * Temporary function that iterates over vehicle permissions and turns them into `PlanetsideAttributeMessage` packets.
@@ -3647,10 +3872,50 @@ class WorldSessionActor extends Actor with MDCContextAware { }) } + /** + * Disassociate this client's player (oneself) from a vehicle that he owns. + */ + def DisownVehicle() : Unit = DisownVehicle(player) + + /** + * Disassociate a player from a vehicle that he owns. + * The vehicle must exist in the game world on the current continent. + * This is similar but unrelated to the natural exchange of ownership when someone else sits in the vehicle's driver seat. + * This is the player side of vehicle ownership removal. + * @see `DisownVehicle(Player, Vehicle)` + * @param tplayer the player + */ + def DisownVehicle(tplayer : Player) : Unit = { + tplayer.VehicleOwned match { + case Some(vehicle_guid) => + continent.GUID(vehicle_guid) match { + case Some(vehicle : Vehicle) => + DisownVehicle(tplayer, vehicle) + case _ => ; + } + tplayer.VehicleOwned = None + case None => ; + } + } + + /** + * Disassociate a vehicle from the player that owns it. + * When a vehicle is disowned + * This is the vehicle side of vehicle ownership removal. + * @see `DisownVehicle(Player)` + * @param tplayer the player + * @param vehicle the discovered vehicle + */ + private def DisownVehicle(tplayer : Player, vehicle : Vehicle) : Unit = { + if(vehicle.Owner.contains(tplayer.GUID)) { + vehicle.Owner = None + } + } + /** * Gives a target player positive battle experience points only. * If the player has access to more implant slots as a result of changing battle experience points, unlock those slots. - * @param tplayer the player + * @param avatar the player * @param bep the change in experience points, positive by assertion * @return the player's current battle experience points */ @@ -3684,18 +3949,17 @@ class WorldSessionActor extends Actor with MDCContextAware { def AccessContents(vehicle : Vehicle) : Unit = { vehicleService ! Service.Join(s"${vehicle.Actor}") val parent_guid = vehicle.GUID - vehicle.Trunk.Items.foreach({ - case ((_, entry)) => - val obj = entry.obj - val objDef = obj.Definition - sendResponse( - ObjectCreateDetailedMessage( - objDef.ObjectId, - obj.GUID, - ObjectCreateMessageParent(parent_guid, entry.start), - objDef.Packet.DetailedConstructorData(obj).get - ) + vehicle.Trunk.Items.foreach(entry => { + val obj = entry.obj + val objDef = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + objDef.ObjectId, + obj.GUID, + ObjectCreateMessageParent(parent_guid, entry.start), + objDef.Packet.DetailedConstructorData(obj).get ) + ) }) } @@ -3707,8 +3971,7 @@ class WorldSessionActor extends Actor with MDCContextAware { */ def UnAccessContents(vehicle : Vehicle) : Unit = { vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) - vehicle.Trunk.Items.foreach({ - case ((_, entry)) => + vehicle.Trunk.Items.foreach(entry =>{ sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0)) }) } @@ -3797,7 +4060,6 @@ class WorldSessionActor extends Actor with MDCContextAware { counting : (Equipment)=>Int = DefaultCount) : List[InventoryItem] = { var currentAmount : Int = 0 obj.Inventory.Items - .map({ case ((_, item)) => item }) .filter(item => filterTest(item.obj)) .toList .sortBy(_.start) @@ -4080,14 +4342,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(InventoryItem(item2, destIndex)) => //yes, swap //cleanly shuffle items around to avoid losing icons //the next ObjectDetachMessage is necessary to avoid icons being lost, but only as part of this swap - sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f, 0f, 0f)) + sendResponse(ObjectDetachMessage(source_guid, item_guid, Vector3.Zero, 0f)) val item2_guid = item2.GUID destination.Slot(destIndex).Equipment = None //remove the swap item from destination (indexSlot.Equipment = item2) match { case Some(_) => //item and item2 swapped places successfully log.info(s"MoveItem: $item2 swapped to $source @ $index") //remove item2 from destination - sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f, 0f, 0f)) + sendResponse(ObjectDetachMessage(destination_guid, item2_guid, Vector3.Zero, 0f)) destination match { case obj : Vehicle => vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.UnstowEquipment(player_guid, item2_guid)) @@ -4130,8 +4392,8 @@ class WorldSessionActor extends Actor with MDCContextAware { val pos = source.Position val sourceOrientZ = source.Orientation.z val orient : Vector3 = Vector3(0f, 0f, sourceOrientZ) - continent.Actor ! Zone.DropItemOnGround(item2, pos, orient) - sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, 0f, 0f, sourceOrientZ)) //ground + continent.Ground ! Zone.Ground.DropItem(item2, pos, orient) + sendResponse(ObjectDetachMessage(destination_guid, item2_guid, pos, sourceOrientZ)) //ground val objDef = item2.Definition destination match { case obj : Vehicle => @@ -4139,7 +4401,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; //Player does not require special case; the act of dropping forces the item and icon to change } - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.EquipmentOnGround(player_guid, pos, orient, objDef.ObjectId, item2_guid, objDef.Packet.ConstructorData(item2).get)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.DropItem(player_guid, item2, continent)) } case None => ; @@ -4189,15 +4451,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param item the item */ def NormalItemDrop(obj : PlanetSideGameObject with Container, zone : Zone, service : ActorRef)(item : Equipment) : Unit = { - val itemGUID = item.GUID - val ang = obj.Orientation.z - val pos = obj.Position - val orient = Vector3(0f, 0f, ang) - item.Position = pos - item.Orientation = orient - zone.Ground ! Zone.DropItemOnGround(item, pos, orient) - val itemDef = item.Definition - service ! AvatarServiceMessage(zone.Id, AvatarAction.EquipmentOnGround(Service.defaultPlayerGUID, pos, orient, itemDef.ObjectId, itemGUID, itemDef.Packet.ConstructorData(item).get)) + continent.Ground ! Zone.Ground.DropItem(item, obj.Position, Vector3(0f, 0f, obj.Orientation.z)) } /** @@ -4302,7 +4556,7 @@ class WorldSessionActor extends Actor with MDCContextAware { obj match { case vehicle : Vehicle => ReloadVehicleAccessPermissions(vehicle) //TODO we should not have to do this imho - // + if(obj.Definition == GlobalDefinitions.ams) { obj.DeploymentState match { case DriveState.Deployed => @@ -4315,6 +4569,27 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } } + if(obj.Definition == GlobalDefinitions.ant) { + obj.DeploymentState match { + case DriveState.Deployed => + // We only want this WSA (not other player's WSA) to manage timers + if(vehicle.Seat(0).get.Occupant.contains(player)){ + // Start ntu regeneration + // If vehicle sends UseItemMessage with silo as target NTU regeneration will be disabled and orb particles will be disabled + antChargingTick = context.system.scheduler.scheduleOnce(1000 milliseconds, self, NtuCharging(player, vehicle)) + } + case DriveState.Undeploying => + // We only want this WSA (not other player's WSA) to manage timers + if(vehicle.Seat(0).get.Occupant.contains(player)){ + antChargingTick.cancel() // Stop charging NTU if charging + } + + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(obj.GUID, 52, 0L)) // panel glow off + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(obj.GUID, 49, 0L)) // orb particles off + case DriveState.Mobile | DriveState.State7 | DriveState.Deploying => + case _ => ; + } + } case _ => ; } } @@ -4374,29 +4649,36 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param building the building object */ def initFacility(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = { + var ntuLevel = 0 + building.Amenities.filter(x => (x.Definition == GlobalDefinitions.resource_silo)).headOption.asInstanceOf[Option[ResourceSilo]] match { + case Some(obj: ResourceSilo) => + ntuLevel = obj.CapacitorDisplay.toInt + case _ => ; + } + sendResponse( BuildingInfoUpdateMessage( - continentNumber, //Zone - buildingNumber, //Facility - 8, //NTU% - false, //Hacked - PlanetSideEmpire.NEUTRAL, //Base hacked by - 0, //Time remaining for hack (ms) - building.Faction, //Base owned by - 0, //!! Field != 0 will cause malformed packet. See class def. - None, - PlanetSideGeneratorState.Normal, //Generator state - true, //Respawn tubes operating state - false, //Force dome state - 0, //Lattice benefits - 0, //!! Field > 0 will cause malformed packet. See class def. - Nil, - 0, - false, - 8, //!! Field != 8 will cause malformed packet. See class def. - None, - false, //Boosted spawn room pain field - false //Boosted generator room pain field + continent_id = continentNumber, + building_id = buildingNumber, + ntu_level = ntuLevel, + is_hacked = false, + empire_hack = PlanetSideEmpire.NEUTRAL, + hack_time_remaining = 0, // milliseconds + empire_own = building.Faction, + unk1 = 0, //!! Field != 0 will cause malformed packet. See class def. + unk1x = None, + generator_state = PlanetSideGeneratorState.Normal, + spawn_tubes_normal = true, + force_dome_active = false, + lattice_benefit = 0, + cavern_benefit = 0, //!! Field > 0 will cause malformed packet. See class def. + unk4 = Nil, + unk5 = 0, + unk6 = false, + unk7 = 8, //!! Field != 8 will cause malformed packet. See class def. + unk7x = None, + boost_spawn_pain = false, + boost_generator_pain = false ) ) sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0,0, 0,0, 0,0, 0,0))) @@ -4416,26 +4698,27 @@ class WorldSessionActor extends Actor with MDCContextAware { def initGate(continentNumber : Int, buildingNumber : Int, building : Building) : Unit = { sendResponse( BuildingInfoUpdateMessage( - continentNumber, buildingNumber, - 0, - false, - PlanetSideEmpire.NEUTRAL, - 0, + continentNumber, + buildingNumber, + ntu_level = 0, + is_hacked = false, + empire_hack = PlanetSideEmpire.NEUTRAL, + hack_time_remaining = 0, building.Faction, - 0, - None, + unk1 = 0, + unk1x = None, PlanetSideGeneratorState.Normal, - true, - false, - 0, - 0, - Nil, - 0, - false, - 8, - None, - false, - false + spawn_tubes_normal = true, + force_dome_active = false, + lattice_benefit = 0, + cavern_benefit = 0, + unk4 = Nil, + unk5 = 0, + unk6 = false, + unk7 = 8, + unk7x = None, + boost_spawn_pain = false, + boost_generator_pain = false ) ) sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0,0, 0,0, 0,0, 0,0))) @@ -4457,6 +4740,20 @@ class WorldSessionActor extends Actor with MDCContextAware { val amenityId = amenity.GUID sendResponse(PlanetsideAttributeMessage(amenityId, 50, 0)) sendResponse(PlanetsideAttributeMessage(amenityId, 51, 0)) + + amenity.Definition match { + case GlobalDefinitions.resource_silo => + // Synchronise warning light & silo capacity + var silo = amenity.asInstanceOf[ResourceSilo] + sendResponse(PlanetsideAttributeMessage(amenityId, 45, silo.CapacitorDisplay)) + sendResponse(PlanetsideAttributeMessage(amenityId, 47, silo.LowNtuWarningOn)) + + if(silo.ChargeLevel == 0) { + // temporarily disabled until warpgates can bring ANTs from sanctuary, otherwise we'd be stuck in a situation with an unpowered base and no way to get an ANT to refill it. + // sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(silo.Owner.asInstanceOf[Building].ModelId), 48, 1)) + } + case _ => ; + } }) sendResponse(HackMessage(3, PlanetSideGUID(building.ModelId), PlanetSideGUID(0), 0, 3212836864L, HackState.HackCleared, 8)) }) @@ -4474,7 +4771,7 @@ class WorldSessionActor extends Actor with MDCContextAware { *
* A maximum revive waiting timer is started. * When this timer reaches zero, the avatar will attempt to spawn back on its faction-specific sanctuary continent. - * @pararm tplayer the player to be killed + * @param tplayer the player to be killed */ def KillPlayer(tplayer : Player) : Unit = { val player_guid = tplayer.GUID @@ -4500,9 +4797,8 @@ class WorldSessionActor extends Actor with MDCContextAware { PlayerActionsToCancel() CancelAllProximityUnits() - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global - reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, galaxy, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7)) + reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(tplayer.Faction), tplayer, 7)) } /** @@ -4553,14 +4849,16 @@ class WorldSessionActor extends Actor with MDCContextAware { * It adds the `WSA`-current `Player` to the current zone and sends out the expected packets. */ def AvatarCreate() : Unit = { + player.VehicleSeated = None //TODO temp, until vehicle gating; unseat player else constructor data is messed up player.Spawn player.Health = 50 //TODO temp player.Armor = 25 val packet = player.Definition.Packet val dcdata = packet.DetailedConstructorData(player).get - sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, dcdata)) - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.LoadPlayer(player.GUID, packet.ConstructorData(player).get)) + val player_guid = player.GUID + sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player_guid, dcdata)) continent.Population ! Zone.Population.Spawn(avatar, player) + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.LoadPlayer(player_guid, ObjectClass.avatar, player_guid, packet.ConstructorData(player).get, None)) log.debug(s"ObjectCreateDetailedMessage: $dcdata") } @@ -4618,8 +4916,9 @@ class WorldSessionActor extends Actor with MDCContextAware { * @param tplayer the player */ def TurnPlayerIntoCorpse(tplayer : Player) : Unit = { + val guid = tplayer.GUID sendResponse( - ObjectCreateDetailedMessage(ObjectClass.avatar, tplayer.GUID, CorpseConverter.converter.DetailedConstructorData(tplayer).get) + ObjectCreateDetailedMessage(ObjectClass.avatar, guid, CorpseConverter.converter.DetailedConstructorData(tplayer).get) ) } @@ -4641,9 +4940,7 @@ class WorldSessionActor extends Actor with MDCContextAware { */ def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { if(WellLootedCorpse(obj)) { - import scala.concurrent.duration._ - import scala.concurrent.ExecutionContext.Implicits.global - context.system.scheduler.scheduleOnce(1 second, avatarService, AvatarServiceMessage.RemoveSpecificCorpse(List(obj))) + avatarService ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent)) true } else { @@ -4667,7 +4964,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(DisconnectMessage("Player failed to load on faction's sanctuary continent. Please relog.")) } else { - galaxy ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) + cluster ! Zone.Lattice.RequestSpawnPoint(sanctNumber, tplayer, 7) } } @@ -4719,7 +5016,6 @@ class WorldSessionActor extends Actor with MDCContextAware { def SetDelayedProximityUnitReset(terminal : Terminal with ProximityUnit) : Unit = { val terminal_guid = terminal.GUID ClearDelayedProximityUnitReset(terminal_guid) - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global delayedProximityTerminalResets += terminal_guid -> context.system.scheduler.scheduleOnce(3000 milliseconds, self, DelayedProximityUnitStop(terminal)) @@ -4728,7 +5024,7 @@ class WorldSessionActor extends Actor with MDCContextAware { /** * For pure proximity-based units and services, disable any manual attempt at cutting off the functionality. * If an existing timer can be found, cancel it. - * @param terminal the proximity-based unit + * @param terminal_guid the proximity-based unit */ def ClearDelayedProximityUnitReset(terminal_guid : PlanetSideGUID) : Unit = { delayedProximityTerminalResets.get(terminal_guid) match { @@ -4825,7 +5121,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * Restore, at most, a specific amount of health points on a player. * Send messages to connected client and to events system. * @param tplayer the player - * @param repairValue the amount to heal; + * @param healValue the amount to heal; * 10 by default * @return whether the player can be repaired for any more health points */ @@ -5028,7 +5324,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case None => ; } } - + def sendResponse(cont : PlanetSidePacketContainer) : Unit = { log.trace("WORLD SEND: " + cont) sendResponse(cont.asInstanceOf[Any]) @@ -5047,6 +5343,30 @@ class WorldSessionActor extends Actor with MDCContextAware { log.trace("WORLD SEND RAW: " + pkt) sendResponse(RawPacket(pkt)) } + + def GetPlayerHackSpeed(): Float = { + if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) { + 10.64f + } else if(player.Certifications.contains(CertificationType.AdvancedHacking)) { + 5.32f + } else { + 2.66f + } + } + + def GetPlayerHackData(): PlayerHackData = { + if(player.Certifications.contains(CertificationType.ExpertHacking) || player.Certifications.contains(CertificationType.ElectronicsExpert)) { + PlayerHackData(3, 10.64f) + } else if(player.Certifications.contains(CertificationType.AdvancedHacking)) { + PlayerHackData(2, 7.98f) + } else if (player.Certifications.contains(CertificationType.Hacking)) { + PlayerHackData(1, 5.32f) + } else { + PlayerHackData(0, 2.66f) + } + } + + case class PlayerHackData(hackLevel: Int, hackSpeed: Float) } object WorldSessionActor { @@ -5083,4 +5403,8 @@ object WorldSessionActor { delta : Float, completeAction : () => Unit, tickAction : Option[() => Unit] = None) + + private final case class NtuCharging(tplayer: Player, + vehicle: Vehicle) + private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID) } diff --git a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala b/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala deleted file mode 100644 index 1486fb5be..000000000 --- a/pslogin/src/main/scala/services/avatar/support/CorpseRemovalActor.scala +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2017 PSForever -package services.avatar.support - -import akka.actor.{Actor, ActorRef, Cancellable} -import net.psforever.objects.guid.TaskResolver -import net.psforever.objects.{DefaultCancellable, Player} -import net.psforever.objects.zones.Zone -import net.psforever.types.Vector3 -import services.{Service, ServiceManager} -import services.ServiceManager.Lookup -import services.avatar.{AvatarAction, AvatarServiceMessage} - -import scala.annotation.tailrec -import scala.concurrent.duration._ - -class CorpseRemovalActor extends Actor { - private var burial : Cancellable = DefaultCancellable.obj - private var corpses : List[CorpseRemovalActor.Entry] = List() - - private var decomposition : Cancellable = DefaultCancellable.obj - private var buriedCorpses : List[CorpseRemovalActor.Entry] = List() - - private var taskResolver : ActorRef = Actor.noSender - - private[this] val log = org.log4s.getLogger - - override def postStop() = { - //Cart Master: See you on Thursday. - super.postStop() - burial.cancel - decomposition.cancel - - corpses.foreach(corpse => { - BurialTask(corpse) - LastRitesTask(corpse) - }) - buriedCorpses.foreach { LastRitesTask } - } - - def receive : Receive = { - case "startup" => - ServiceManager.serviceManager ! Lookup("taskResolver") //ask for a resolver to deal with the GUID system - - case ServiceManager.LookupResult("taskResolver", endpoint) => - //Cart Master: Bring out your dead! - taskResolver = endpoint - context.become(Processing) - - case _ => ; - } - - def Processing : Receive = { - case CorpseRemovalActor.AddCorpse(corpse, zone, time) => - import CorpseRemovalActor.SimilarCorpses - if(corpse.isBackpack && !buriedCorpses.exists(entry => SimilarCorpses(entry.corpse, corpse) )) { - if(corpses.isEmpty) { - //we were the only entry so the event must be started from scratch - corpses = List(CorpseRemovalActor.Entry(corpse, zone, time)) - RetimeFirstTask() - } - else { - //unknown number of entries; append, sort, then re-time tasking - val oldHead = corpses.head - if(!corpses.exists(entry => SimilarCorpses(entry.corpse, corpse))) { - corpses = (corpses :+ CorpseRemovalActor.Entry(corpse, zone, time)).sortBy(_.timeAlive) - if(oldHead != corpses.head) { - RetimeFirstTask() - } - } - } - } - else { - //Cart Master: 'Ere. He says he's not dead! - log.warn(s"$corpse does not qualify as a corpse; ignored queueing request") - } - - case AvatarServiceMessage.RemoveSpecificCorpse(targets) => - if(targets.nonEmpty) { - //Cart Master: No, I've got to go to the Robinsons'. They've lost nine today. - burial.cancel - if(targets.size == 1) { - log.debug(s"a target corpse submitted for early cleanup: ${targets.head}") - //simple selection - CorpseRemovalActor.recursiveFindCorpse(corpses.iterator, targets.head) match { - case None => ; - case Some(index) => - decomposition.cancel - BurialTask(corpses(index)) - buriedCorpses = buriedCorpses :+ corpses(index) - corpses = (corpses.take(index) ++ corpses.drop(index + 1)).sortBy(_.timeAlive) - import scala.concurrent.ExecutionContext.Implicits.global - decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete()) - } - } - else { - log.debug(s"multiple target corpses submitted for early cleanup: $targets") - import CorpseRemovalActor.SimilarCorpses - decomposition.cancel - //cumbersome partition - //a - find targets from corpses - val locatedTargets = for { - a <- targets - b <- corpses - if b.corpse.HasGUID && a.HasGUID && SimilarCorpses(b.corpse, a) - } yield b - if(locatedTargets.nonEmpty) { - decomposition.cancel - locatedTargets.foreach { BurialTask } - buriedCorpses = locatedTargets ++ buriedCorpses - import scala.concurrent.ExecutionContext.Implicits.global - decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete()) - //b - corpses, after the found targets are removed (cull any non-GUID entries while at it) - corpses = (for { - a <- locatedTargets - b <- corpses - if b.corpse.HasGUID && a.corpse.HasGUID && !SimilarCorpses(b.corpse, a.corpse) - } yield b).sortBy(_.timeAlive) - } - } - RetimeFirstTask() - } - - case CorpseRemovalActor.StartDelete() => - burial.cancel - decomposition.cancel - val now : Long = System.nanoTime - val (buried, rotting) = corpses.partition(entry => { now - entry.time >= entry.timeAlive }) - corpses = rotting - buriedCorpses = buriedCorpses ++ buried - buried.foreach { BurialTask } - RetimeFirstTask() - if(buriedCorpses.nonEmpty) { - import scala.concurrent.ExecutionContext.Implicits.global - burial = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete()) - } - - case CorpseRemovalActor.TryDelete() => - decomposition.cancel - val (decomposed, rotting) = buriedCorpses.partition(entry => { - !entry.zone.Corpses.contains(entry.corpse) - }) - buriedCorpses = rotting - decomposed.foreach { LastRitesTask } - if(rotting.nonEmpty) { - import scala.concurrent.ExecutionContext.Implicits.global - decomposition = context.system.scheduler.scheduleOnce(500 milliseconds, self, CorpseRemovalActor.TryDelete()) - } - - case CorpseRemovalActor.FailureToWork(target, zone, ex) => - //Cart Master: Oh, I can't take him like that. It's against regulations. - log.error(s"corpse $target from $zone not properly unregistered - $ex") - - case _ => ; - } - - def RetimeFirstTask(now : Long = System.nanoTime) : Unit = { - //Cart Master: Thursday. - burial.cancel - if(corpses.nonEmpty) { - val short_timeout : FiniteDuration = math.max(1, corpses.head.timeAlive - (now - corpses.head.time)) nanoseconds - import scala.concurrent.ExecutionContext.Implicits.global - burial = context.system.scheduler.scheduleOnce(short_timeout, self, CorpseRemovalActor.StartDelete()) - } - } - - def BurialTask(entry : CorpseRemovalActor.Entry) : Unit = { - val target = entry.corpse - entry.zone.Population ! Zone.Corpse.Remove(target) - context.parent ! AvatarServiceMessage(entry.zone.Id, AvatarAction.ObjectDelete(Service.defaultPlayerGUID, target.GUID)) - } - - def LastRitesTask(entry : CorpseRemovalActor.Entry) : Unit = { - //Cart master: Nine pence. - val target = entry.corpse - target.Position = Vector3.Zero //somewhere it will not disturb anything - taskResolver ! LastRitesTask(target, entry.zone) - } - - def LastRitesTask(corpse : Player, zone : Zone) : TaskResolver.GiveTask = { - import net.psforever.objects.guid.{GUIDTask, Task} - TaskResolver.GiveTask ( - new Task() { - private val localCorpse = corpse - private val localZone = zone - private val localAnnounce = self - - override def isComplete : Task.Resolution.Value = if(!localCorpse.HasGUID) { - Task.Resolution.Success - } - else { - Task.Resolution.Incomplete - } - - def Execute(resolver : ActorRef) : Unit = { - resolver ! scala.util.Success(this) - } - - override def onFailure(ex : Throwable): Unit = { - localAnnounce ! CorpseRemovalActor.FailureToWork(localCorpse, localZone, ex) - } - }, List(GUIDTask.UnregisterPlayer(corpse)(zone.GUID)) - ) - } -} - -object CorpseRemovalActor { - final val time : Long = 180000000000L //3 min (180s) - - final case class AddCorpse(corpse : Player, zone : Zone, time : Long = CorpseRemovalActor.time) - - final case class Entry(corpse : Player, zone : Zone, timeAlive : Long = CorpseRemovalActor.time, time : Long = System.nanoTime()) - - private final case class FailureToWork(corpse : Player, zone : Zone, ex : Throwable) - - private final case class StartDelete() - - private final case class TryDelete() - - private def SimilarCorpses(obj1 : Player, obj2 : Player) : Boolean = { - obj1 == obj2 && obj1.Continent.equals(obj2.Continent) && obj1.GUID == obj2.GUID - } - - /** - * A recursive function that finds and removes a specific player from a list of players. - * @param iter an `Iterator` of `CorpseRemovalActor.Entry` objects - * @param player the target `Player` - * @param index the index of the discovered `Player` object - * @return the index of the `Player` object in the list to be removed; - * `None`, otherwise - */ - @tailrec final def recursiveFindCorpse(iter : Iterator[CorpseRemovalActor.Entry], player : Player, index : Int = 0) : Option[Int] = { - if(!iter.hasNext) { - None - } - else { - val corpse = iter.next.corpse - if(corpse.HasGUID && player.HasGUID && SimilarCorpses(corpse, player)) { - Some(index) - } - else { - recursiveFindCorpse(iter, player, index + 1) - } - } - } -} diff --git a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala index 07e438525..e69de29bb 100644 --- a/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala +++ b/pslogin/src/main/scala/services/vehicle/support/DeconstructionActor.scala @@ -1,292 +0,0 @@ -// Copyright (c) 2017 PSForever -package services.vehicle.support - -import akka.actor.{Actor, ActorRef, Cancellable} -import net.psforever.objects.{DefaultCancellable, GlobalDefinitions, Vehicle} -import net.psforever.objects.guid.TaskResolver -import net.psforever.objects.vehicles.Seat -import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{DismountVehicleCargoMsg, PlanetSideGUID} -import net.psforever.types.Vector3 -import services.ServiceManager -import services.ServiceManager.Lookup -import services.vehicle.{VehicleAction, VehicleServiceMessage} - -import scala.annotation.tailrec -import scala.concurrent.duration._ - -/** - * Manage a previously-functioning vehicle as it is being deconstructed.
- *
- * A reference to a vehicle should be passed to this object as soon as it is going to be cleaned-up from the game world. - * Once accepted, only a few seconds will remain before the vehicle is deleted. - * To ensure that no players are lost in the deletion, all occupants of the vehicle are kicked out. - * Furthermore, the vehicle is rendered "dead" and inaccessible right up to the point where it is removed.
- *
- * This `Actor` is intended to sit on top of the event system that handles broadcast messaging. - */ -class DeconstructionActor extends Actor { - /** The periodic `Executor` that scraps the next vehicle on the list */ - private var scrappingProcess : Cancellable = DefaultCancellable.obj - /** A `List` of currently doomed vehicles */ - private var vehicles : List[DeconstructionActor.VehicleEntry] = Nil - /** The periodic `Executor` that cleans up the next vehicle on the list */ - private var heapEmptyProcess : Cancellable = DefaultCancellable.obj - /** A `List` of vehicles that have been removed from the game world and are awaiting deconstruction. */ - private var vehicleScrapHeap : List[DeconstructionActor.VehicleEntry] = Nil - /** The manager that helps unregister the vehicle from its current GUID scope */ - private var taskResolver : ActorRef = Actor.noSender - //private[this] val log = org.log4s.getLogger - - override def postStop() : Unit = { - super.postStop() - scrappingProcess.cancel - heapEmptyProcess.cancel - - vehicles.foreach(entry => { - RetirementTask(entry) - DestructionTask(entry) - }) - vehicleScrapHeap.foreach { DestructionTask } - } - - def receive : Receive = { - /* - ask for a resolver to deal with the GUID system - when the TaskResolver is finally delivered, switch over to a behavior that actually deals with submitted vehicles - */ - case DeconstructionActor.RequestTaskResolver => - ServiceManager.serviceManager ! Lookup("taskResolver") - - case ServiceManager.LookupResult("taskResolver", endpoint) => - taskResolver = endpoint - context.become(Processing) - - case _ => ; - } - - def Processing : Receive = { - case DeconstructionActor.RequestDeleteVehicle(vehicle, zone, time) => - if(!vehicles.exists(_.vehicle == vehicle) && !vehicleScrapHeap.exists(_.vehicle == vehicle)) { - vehicles = vehicles :+ DeconstructionActor.VehicleEntry(vehicle, zone, time) - vehicle.Actor ! Vehicle.PrepareForDeletion - //kick everyone out; this is a no-blocking manual form of MountableBehavior ! Mountable.TryDismount - vehicle.Definition.MountPoints.values.foreach(seat_num => { - val zone_id : String = zone.Id - vehicle.Seat(seat_num) match { - case Some(seat) => - seat.Occupant match { - case Some(tplayer) => - seat.Occupant = None - tplayer.VehicleSeated = None - if(tplayer.HasGUID) { - context.parent ! VehicleServiceMessage(zone_id, VehicleAction.KickPassenger(tplayer.GUID, 4, false, vehicle.GUID)) - } - case None => ; // No player seated - } - case None => ; // Not a seat mount point - } - vehicle.CargoHold(seat_num) match { - case Some(cargo) => - cargo.Occupant match { - case Some(vehicle) => - //todo: this probably doesn't work for passengers within the cargo vehicle - // Instruct client to start bail dismount procedure - // player_num set to 0 as it's not easily available in the context and currently isn't used by DismountVehicleCargoMsg - context.parent ! DismountVehicleCargoMsg(PlanetSideGUID(0), vehicle.GUID, true, false, false) - case None => ; // No vehicle in cargo - } - case None => ; // Not a cargo mounting point - } - - }) - if(vehicles.size == 1) { - //we were the only entry so the event must be started from scratch - import scala.concurrent.ExecutionContext.Implicits.global - scrappingProcess = context.system.scheduler.scheduleOnce(DeconstructionActor.timeout, self, DeconstructionActor.StartDeleteVehicle()) - } - } - - case DeconstructionActor.StartDeleteVehicle() => - scrappingProcess.cancel - heapEmptyProcess.cancel - val now : Long = System.nanoTime - val (vehiclesToScrap, vehiclesRemain) = PartitionEntries(vehicles, now) - vehicles = vehiclesRemain - vehicleScrapHeap = vehicleScrapHeap ++ vehiclesToScrap //may include existing entries - vehiclesToScrap.foreach(entry => { - val vehicle = entry.vehicle - val zone = entry.zone - RetirementTask(entry) - if(vehicle.Definition == GlobalDefinitions.ams) { - import net.psforever.types.DriveState - vehicle.DeploymentState = DriveState.Mobile //internally undeployed //TODO this should be temporary? - context.parent ! VehicleServiceMessage.AMSDeploymentChange(zone) - } - taskResolver ! DeconstructionTask(vehicle, zone) - }) - - if(vehiclesRemain.nonEmpty) { - val short_timeout : FiniteDuration = math.max(1, DeconstructionActor.timeout_time - (now - vehiclesRemain.head.time)) nanoseconds - import scala.concurrent.ExecutionContext.Implicits.global - scrappingProcess = context.system.scheduler.scheduleOnce(short_timeout, self, DeconstructionActor.StartDeleteVehicle()) - } - if(vehicleScrapHeap.nonEmpty) { - import scala.concurrent.ExecutionContext.Implicits.global - heapEmptyProcess = context.system.scheduler.scheduleOnce(500 milliseconds, self, DeconstructionActor.TryDeleteVehicle()) - } - - case DeconstructionActor.TryDeleteVehicle() => - heapEmptyProcess.cancel - val (vehiclesToScrap, vehiclesRemain) = vehicleScrapHeap.partition(entry => !entry.zone.Vehicles.contains(entry.vehicle)) - vehicleScrapHeap = vehiclesRemain - vehiclesToScrap.foreach { DestructionTask } - if(vehiclesRemain.nonEmpty) { - import scala.concurrent.ExecutionContext.Implicits.global - heapEmptyProcess = context.system.scheduler.scheduleOnce(500 milliseconds, self, DeconstructionActor.TryDeleteVehicle()) - } - - case DeconstructionActor.FailureToDeleteVehicle(localVehicle, localZone, ex) => - org.log4s.getLogger.error(s"vehicle deconstruction: $localVehicle failed to be properly cleaned up from zone $localZone - $ex") - - case _ => ; - } - - def RetirementTask(entry : DeconstructionActor.VehicleEntry) : Unit = { - val vehicle = entry.vehicle - val zone = entry.zone - vehicle.Position = Vector3.Zero //somewhere it will not disturb anything - zone.Transport ! Zone.Vehicle.Despawn(vehicle) - context.parent ! DeconstructionActor.DeleteVehicle(vehicle.GUID, zone.Id) //call up to the main event system - } - - def DestructionTask(entry : DeconstructionActor.VehicleEntry) : Unit = { - val vehicle = entry.vehicle - val zone = entry.zone - taskResolver ! DeconstructionTask(vehicle, zone) - } - - /** - * Construct a middleman `Task` intended to return error messages to the `DeconstructionActor`. - * @param vehicle the `Vehicle` object - * @param zone the `Zone` in which the vehicle resides - * @return a `TaskResolver.GiveTask` message - */ - def DeconstructionTask(vehicle : Vehicle, zone : Zone) : TaskResolver.GiveTask = { - import net.psforever.objects.guid.{GUIDTask, Task} - TaskResolver.GiveTask ( - new Task() { - private val localVehicle = vehicle - private val localZone = zone - private val localAnnounce = self - - override def isComplete : Task.Resolution.Value = Task.Resolution.Success - - def Execute(resolver : ActorRef) : Unit = { - resolver ! scala.util.Success(this) - } - - override def onFailure(ex : Throwable): Unit = { - localAnnounce ! DeconstructionActor.FailureToDeleteVehicle(localVehicle, localZone, ex) - } - }, List(GUIDTask.UnregisterVehicle(vehicle)(zone.GUID)) - ) - } - - /** - * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered. - * Separate the original `List` into two: - * a `List` of elements that have exceeded the time limit, - * and a `List` of elements that still satisfy the time limit. - * As newer entries to the `List` will always resolve later than old ones, - * and newer entries are always added to the end of the main `List`, - * processing in order is always correct. - * @param list the `List` of entries to divide - * @param now the time right now (in nanoseconds) - * @see `List.partition` - * @return a `Tuple` of two `Lists`, whose qualifications are explained above - */ - private def PartitionEntries(list : List[DeconstructionActor.VehicleEntry], now : Long) : (List[DeconstructionActor.VehicleEntry], List[DeconstructionActor.VehicleEntry]) = { - val n : Int = recursivePartitionEntries(list.iterator, now) - (list.take(n), list.drop(n)) //take and drop so to always return new lists - } - - /** - * Mark the index where the `List` of elements can be divided into two: - * a `List` of elements that have exceeded the time limit, - * and a `List` of elements that still satisfy the time limit. - * @param iter the `Iterator` of entries to divide - * @param now the time right now (in nanoseconds) - * @param index a persistent record of the index where list division should occur; - * defaults to 0 - * @return the index where division will occur - */ - @tailrec private def recursivePartitionEntries(iter : Iterator[DeconstructionActor.VehicleEntry], now : Long, index : Int = 0) : Int = { - if(!iter.hasNext) { - index - } - else { - val entry = iter.next() - if(now - entry.time >= DeconstructionActor.timeout_time) { - recursivePartitionEntries(iter, now, index + 1) - } - else { - index - } - } - } -} - -object DeconstructionActor { - /** The wait before completely deleting a vehicle; as a Long for calculation simplicity */ - private final val timeout_time : Long = 5000000000L //nanoseconds (5s) - /** The wait before completely deleting a vehicle; as a `FiniteDuration` for `Executor` simplicity */ - private final val timeout : FiniteDuration = timeout_time nanoseconds - - final case class RequestTaskResolver() - - /** - * Message that carries information about a vehicle to be deconstructed. - * @param vehicle the `Vehicle` object - * @param zone the `Zone` in which the vehicle resides - * @param time when the vehicle was doomed - * @see `VehicleEntry` - */ - final case class RequestDeleteVehicle(vehicle : Vehicle, zone : Zone, time : Long = System.nanoTime()) - /** - * Message that carries information about a vehicle to be deconstructed. - * Prompting, as compared to `RequestDeleteVehicle` which is reactionary. - * @param vehicle_guid the vehicle - * @param zone_id the `Zone` in which the vehicle resides - */ - final case class DeleteVehicle(vehicle_guid : PlanetSideGUID, zone_id : String) - /** - * Internal message used to signal a test of the queued vehicle information. - * Remove all deconstructing vehicles from the game world. - */ - private final case class StartDeleteVehicle() - /** - * Internal message used to signal a test of the queued vehicle information. - * Remove all deconstructing vehicles from the zone's globally unique identifier system. - */ - private final case class TryDeleteVehicle() - - /** - * Error-passing message carrying information out of the final deconstruction GUID unregistering task. - * @param vehicle the `Vehicle` object - * @param zone the `Zone` in which the vehicle may or may not reside - * @param ex information regarding what happened - */ - private final case class FailureToDeleteVehicle(vehicle : Vehicle, zone : Zone, ex : Throwable) - - /** - * Entry of vehicle information. - * The `zone` is maintained separately as a necessity, required to complete the deletion of the vehicle - * via unregistering of the vehicle and all related, registered objects. - * @param vehicle the `Vehicle` object - * @param zone the `Zone` in which the vehicle resides - * @param time when the vehicle was doomed - * @see `RequestDeleteVehicle` - */ - private final case class VehicleEntry(vehicle : Vehicle, zone : Zone, time : Long) -} diff --git a/pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala b/pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala deleted file mode 100644 index 7756b4153..000000000 --- a/pslogin/src/main/scala/services/vehicle/support/DelayedDeconstructionActor.scala +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2017 PSForever -package services.vehicle.support - -import akka.actor.{Actor, Cancellable} -import net.psforever.objects.{DefaultCancellable, Vehicle} -import net.psforever.objects.zones.Zone -import net.psforever.packet.game.PlanetSideGUID -import services.vehicle.VehicleServiceMessage - -import scala.concurrent.duration._ - -/** - * Maintain and curate a list of timed `vehicle` object deconstruction tasks.
- *
- * These tasks are queued or dismissed by player activity but they are executed independent of player activity. - * A common disconnected cause of deconstruction is neglect for an extended period of time. - * At that point, the original owner of the vehicle no longer matters. - * Deconstruction neglect, however, is averted by having someone become seated. - * A realized deconstruction is entirely based on a fixed interval after an unresolved request has been received. - * The actual process of deconstructing the vehicle and cleaning up its resources is performed by an external agent.
- *
- * This `Actor` is intended to sit on top of the event system that handles broadcast messaging. - */ -class DelayedDeconstructionActor extends Actor { - /** The periodic `Executor` that scraps the next vehicle on the list */ - private var monitor : Cancellable = DefaultCancellable.obj - /** A `List` of currently doomed vehicles */ - private var vehicles : List[DelayedDeconstructionActor.VehicleEntry] = Nil - private[this] val log = org.log4s.getLogger - private[this] def trace(msg : String) : Unit = log.trace(msg) - - - def receive : Receive = { - case DelayedDeconstructionActor.ScheduleDeconstruction(vehicle, zone, timeAlive) => - trace(s"delayed deconstruction order for $vehicle in $timeAlive") - val oldHead = vehicles.headOption - val now : Long = System.nanoTime - vehicles = (vehicles :+ DelayedDeconstructionActor.VehicleEntry(vehicle, zone, timeAlive * 1000000000L)) - .sortBy(entry => entry.survivalTime - (now - entry.logTime)) - if(vehicles.size == 1 || oldHead != vehicles.headOption) { //we were the only entry so the event must be started from scratch - RetimePeriodicTest() - } - - case DelayedDeconstructionActor.UnscheduleDeconstruction(vehicle_guid) => - //all tasks for this vehicle are cleared from the queue - //clear any task that is no longer valid by determination of unregistered GUID - val before = vehicles.length - val now : Long = System.nanoTime - vehicles = vehicles.filter(entry => { entry.vehicle.HasGUID && entry.vehicle.GUID != vehicle_guid }) - .sortBy(entry => entry.survivalTime - (now - entry.logTime)) - trace(s"attempting to clear deconstruction order for vehicle $vehicle_guid, found ${before - vehicles.length}") - RetimePeriodicTest() - - case DelayedDeconstructionActor.PeriodicTaskCulling => - //filter the list of deconstruction tasks for any that are need to be triggered - monitor.cancel - val now : Long = System.nanoTime - val (vehiclesToDecon, vehiclesRemain) = vehicles.partition(entry => { now - entry.logTime >= entry.survivalTime }) - vehicles = vehiclesRemain.sortBy(_.survivalTime) - trace(s"vehicle culling - ${vehiclesToDecon.length} deconstruction tasks found; ${vehiclesRemain.length} tasks remain") - vehiclesToDecon.foreach(entry => { context.parent ! VehicleServiceMessage.RequestDeleteVehicle(entry.vehicle, entry.zone) }) - RetimePeriodicTest() - - case _ => ; - } - - def RetimePeriodicTest() : Unit = { - monitor.cancel - vehicles.headOption match { - case None => ; - case Some(entry) => - val retime = math.max(1, entry.survivalTime - (System.nanoTime - entry.logTime)) nanoseconds - import scala.concurrent.ExecutionContext.Implicits.global - monitor = context.system.scheduler.scheduleOnce(retime, self, DelayedDeconstructionActor.PeriodicTaskCulling) - } - } -} - -object DelayedDeconstructionActor { - /** - * Timer for the repeating executor. - */ - private final val periodicTest : FiniteDuration = 5000000000L nanoseconds //5s - - /** - * Queue a future vehicle deconstruction action. - * @param vehicle the `Vehicle` object - * @param zone the `Zone` that the vehicle currently occupies - * @param survivalTime how long until the vehicle will be deconstructed in seconds - */ - final case class ScheduleDeconstruction(vehicle : Vehicle, zone : Zone, survivalTime : Long) - - /** - * Dequeue a vehicle from being deconstructed. - * @param vehicle_guid the vehicle - */ - final case class UnscheduleDeconstruction(vehicle_guid : PlanetSideGUID) - - /** - * A message the `Actor` sends to itself. - * The trigger for the periodic deconstruction task. - */ - private final case class PeriodicTaskCulling() - - /** - * An entry that stores vehicle deconstruction tasks. - * @param vehicle the `Vehicle` object - * @param zone the `Zone` that the vehicle currently occupies - * @param survivalTime how long until the vehicle will be deconstructed in nanoseconds - * @param logTime when this deconstruction request was initially created in nanoseconds; - * initialized by default to a "now" - */ - private final case class VehicleEntry(vehicle : Vehicle, zone : Zone, survivalTime : Long, logTime : Long = System.nanoTime()) -} diff --git a/pslogin/src/test/scala/AvatarServiceTest.scala b/pslogin/src/test/scala/AvatarServiceTest.scala index c122dcdf2..9d521c1bd 100644 --- a/pslogin/src/test/scala/AvatarServiceTest.scala +++ b/pslogin/src/test/scala/AvatarServiceTest.scala @@ -4,9 +4,10 @@ import akka.routing.RandomPool import net.psforever.objects._ import net.psforever.objects.guid.{GUIDTask, TaskResolver} import net.psforever.objects.zones.{Zone, ZoneActor, ZoneMap} -import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream} -import net.psforever.types.{CharacterGender, ExoSuitType, PlanetSideEmpire, Vector3} -import services.{Service, ServiceManager} +import net.psforever.packet.game.objectcreate.{DroppedItemData, ObjectClass, ObjectCreateMessageParent, PlacementData} +import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID, PlayerStateMessageUpstream} +import net.psforever.types._ +import services.{RemoverActor, Service, ServiceManager} import services.avatar._ import scala.concurrent.duration._ @@ -93,49 +94,89 @@ class ConcealPlayerTest extends ActorTest { } class EquipmentInHandTest extends ActorTest { - val tool = Tool(GlobalDefinitions.beamer) + ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") + val service = system.actorOf(Props[AvatarService], "release-test-service") + val zone = new Zone("test", new ZoneMap("test-map"), 0) + val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") + + val toolDef = GlobalDefinitions.beamer + val tool = Tool(toolDef) + tool.GUID = PlanetSideGUID(40) + tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(41) + val pkt = ObjectCreateMessage( + toolDef.ObjectId, + tool.GUID, + ObjectCreateMessageParent(PlanetSideGUID(11), 2), + toolDef.Packet.ConstructorData(tool).get + ) "AvatarService" should { "pass EquipmentInHand" in { - ServiceManager.boot(system) - val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") service ! AvatarServiceMessage("test", AvatarAction.EquipmentInHand(PlanetSideGUID(10), PlanetSideGUID(11), 2, tool)) - expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(PlanetSideGUID(11), 2, tool))) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(pkt))) } } } -class EquipmentOnGroundTest extends ActorTest { +class DroptItemTest extends ActorTest { + ServiceManager.boot(system) ! ServiceManager.Register(RandomPool(1).props(Props[TaskResolver]), "taskResolver") + val service = system.actorOf(Props[AvatarService], "release-test-service") + val zone = new Zone("test", new ZoneMap("test-map"), 0) + val taskResolver = system.actorOf(Props[TaskResolver], "drop-item-test-resolver") + zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "drop-item-test-zone") + zone.Actor ! Zone.Init() + val toolDef = GlobalDefinitions.beamer val tool = Tool(toolDef) - tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(1) - val cdata = toolDef.Packet.ConstructorData(tool).get + tool.Position = Vector3(1,2,3) + tool.Orientation = Vector3(4,5,6) + tool.GUID = PlanetSideGUID(40) + tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(41) + val pkt = ObjectCreateMessage( + toolDef.ObjectId, + tool.GUID, + DroppedItemData( + PlacementData(tool.Position, tool.Orientation), + toolDef.Packet.ConstructorData(tool).get + ) + ) "AvatarService" should { - "pass EquipmentOnGround" in { - ServiceManager.boot(system) - val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) + "pass DropItem" in { service ! Service.Join("test") - service ! AvatarServiceMessage("test", AvatarAction.EquipmentOnGround(PlanetSideGUID(10), Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata)) - expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentOnGround(Vector3(300f, 200f, 100f), Vector3(450f, 300f, 150f), toolDef.ObjectId, PlanetSideGUID(11), cdata))) + service ! AvatarServiceMessage("test", AvatarAction.DropItem(PlanetSideGUID(10), tool, zone)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.DropItem(pkt))) } } } class LoadPlayerTest extends ActorTest { - val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) obj.GUID = PlanetSideGUID(10) obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(11) - val pdata = obj.Definition.Packet.DetailedConstructorData(obj).get + val c1data = obj.Definition.Packet.DetailedConstructorData(obj).get + val pkt1 = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(10), c1data) + val parent = ObjectCreateMessageParent(PlanetSideGUID(12), 0) + obj.VehicleSeated = PlanetSideGUID(12) + val c2data = obj.Definition.Packet.DetailedConstructorData(obj).get + val pkt2 = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(10), parent, c2data) "AvatarService" should { "pass LoadPlayer" in { ServiceManager.boot(system) val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) service ! Service.Join("test") - service ! AvatarServiceMessage("test", AvatarAction.LoadPlayer(PlanetSideGUID(10), pdata)) - expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.LoadPlayer(pdata))) + //no parent data + service ! AvatarServiceMessage("test", AvatarAction.LoadPlayer( + PlanetSideGUID(20), ObjectClass.avatar, PlanetSideGUID(10), c1data, None) + ) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(20), AvatarResponse.LoadPlayer(pkt1))) + //parent data + service ! AvatarServiceMessage("test", AvatarAction.LoadPlayer( + PlanetSideGUID(20), ObjectClass.avatar, PlanetSideGUID(10), c2data, Some(parent)) + ) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(20), AvatarResponse.LoadPlayer(pkt2))) } } } @@ -193,6 +234,45 @@ class PlayerStateTest extends ActorTest { } } +class PickupItemATest extends ActorTest { + val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + obj.GUID = PlanetSideGUID(10) + obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(11) + + val toolDef = GlobalDefinitions.beamer + val tool = Tool(toolDef) + tool.GUID = PlanetSideGUID(40) + tool.AmmoSlots.head.Box.GUID = PlanetSideGUID(41) + val pkt = ObjectCreateMessage( + toolDef.ObjectId, + tool.GUID, + ObjectCreateMessageParent(PlanetSideGUID(10), 0), + toolDef.Packet.ConstructorData(tool).get + ) + + "pass PickUpItem as EquipmentInHand (visible pistol slot)" in { + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), Zone.Nowhere, obj, 0, tool)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.EquipmentInHand(pkt))) + } +} + +class PickupItemBTest extends ActorTest { + val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) + val tool = Tool(GlobalDefinitions.beamer) + tool.GUID = PlanetSideGUID(40) + + "pass PickUpItem as ObjectDelete (not visible inventory space)" in { + ServiceManager.boot(system) + val service = system.actorOf(Props[AvatarService], AvatarServiceTest.TestName) + service ! Service.Join("test") + service ! AvatarServiceMessage("test", AvatarAction.PickupItem(PlanetSideGUID(10), Zone.Nowhere, obj, 6, tool)) + expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectDelete(tool.GUID, 0))) + } +} + class ReloadTest extends ActorTest { "AvatarService" should { "pass Reload" in { @@ -308,7 +388,7 @@ class AvatarReleaseTest extends ActorTest { val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") zone.Actor ! Zone.Init() - val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + val obj = Player(Avatar("TestCharacter", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) obj.Continent = "test" obj.Release @@ -320,14 +400,14 @@ class AvatarReleaseTest extends ActorTest { taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) assert(zone.Corpses.isEmpty) zone.Population ! Zone.Corpse.Add(obj) - expectNoMsg(100 milliseconds) //spacer + expectNoMsg(200 milliseconds) //spacer assert(zone.Corpses.size == 1) assert(obj.HasGUID) val guid = obj.GUID - service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone, Some(1000000000))) //alive for one second + service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone, Some(1 second))) //alive for one second - val reply1 = receiveOne(100 milliseconds) + val reply1 = receiveOne(200 milliseconds) assert(reply1.isInstanceOf[AvatarServiceResponse]) val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] assert(reply1msg.toChannel == "/test/Avatar") @@ -343,7 +423,7 @@ class AvatarReleaseTest extends ActorTest { assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) - expectNoMsg(1000 milliseconds) + expectNoMsg(1 seconds) assert(zone.Corpses.isEmpty) assert(!obj.HasGUID) } @@ -357,7 +437,7 @@ class AvatarReleaseEarly1Test extends ActorTest { val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") zone.Actor ! Zone.Init() - val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) obj.Continent = "test" obj.Release @@ -369,14 +449,14 @@ class AvatarReleaseEarly1Test extends ActorTest { taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) assert(zone.Corpses.isEmpty) zone.Population ! Zone.Corpse.Add(obj) - expectNoMsg(100 milliseconds) //spacer + expectNoMsg(200 milliseconds) //spacer assert(zone.Corpses.size == 1) assert(obj.HasGUID) val guid = obj.GUID service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes! - val reply1 = receiveOne(100 milliseconds) + val reply1 = receiveOne(200 milliseconds) assert(reply1.isInstanceOf[AvatarServiceResponse]) val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] assert(reply1msg.toChannel == "/test/Avatar") @@ -384,8 +464,8 @@ class AvatarReleaseEarly1Test extends ActorTest { assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) - service ! AvatarServiceMessage.RemoveSpecificCorpse(List(obj)) //IMPORTANT: ONE ENTRY - val reply2 = receiveOne(100 milliseconds) + service ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), zone)) //IMPORTANT: ONE ENTRY + val reply2 = receiveOne(200 milliseconds) assert(reply2.isInstanceOf[AvatarServiceResponse]) val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] assert(reply2msg.toChannel.equals("/test/Avatar")) @@ -393,7 +473,7 @@ class AvatarReleaseEarly1Test extends ActorTest { assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) - expectNoMsg(600 milliseconds) + expectNoMsg(1 seconds) assert(zone.Corpses.isEmpty) assert(!obj.HasGUID) } @@ -407,8 +487,8 @@ class AvatarReleaseEarly2Test extends ActorTest { val taskResolver = system.actorOf(Props[TaskResolver], "release-test-resolver") zone.Actor = system.actorOf(Props(classOf[ZoneActor], zone), "release-test-zone") zone.Actor ! Zone.Init() - val objAlt = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 1, 1)) //necessary clutter - val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, 1)) + val objAlt = Player(Avatar("TestCharacter2", PlanetSideEmpire.NC, CharacterGender.Male, 1, CharacterVoice.Voice1)) //necessary clutter + val obj = Player(Avatar("TestCharacter1", PlanetSideEmpire.VS, CharacterGender.Female, 1, CharacterVoice.Voice1)) obj.Continent = "test" obj.Release @@ -420,14 +500,14 @@ class AvatarReleaseEarly2Test extends ActorTest { taskResolver ! GUIDTask.RegisterObjectTask(obj)(zone.GUID) assert(zone.Corpses.isEmpty) zone.Population ! Zone.Corpse.Add(obj) - expectNoMsg(100 milliseconds) //spacer + expectNoMsg(200 milliseconds) //spacer assert(zone.Corpses.size == 1) assert(obj.HasGUID) val guid = obj.GUID service ! AvatarServiceMessage("test", AvatarAction.Release(obj, zone)) //3+ minutes! - val reply1 = receiveOne(100 milliseconds) + val reply1 = receiveOne(200 milliseconds) assert(reply1.isInstanceOf[AvatarServiceResponse]) val reply1msg = reply1.asInstanceOf[AvatarServiceResponse] assert(reply1msg.toChannel == "/test/Avatar") @@ -435,7 +515,7 @@ class AvatarReleaseEarly2Test extends ActorTest { assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release]) assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj) - service ! AvatarServiceMessage.RemoveSpecificCorpse(List(objAlt, obj)) //IMPORTANT: TWO ENTRIES + service ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(objAlt, obj), zone)) //IMPORTANT: TWO ENTRIES val reply2 = receiveOne(100 milliseconds) assert(reply2.isInstanceOf[AvatarServiceResponse]) val reply2msg = reply2.asInstanceOf[AvatarServiceResponse] @@ -444,7 +524,7 @@ class AvatarReleaseEarly2Test extends ActorTest { assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete]) assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid) - expectNoMsg(600 milliseconds) + expectNoMsg(1 seconds) assert(zone.Corpses.isEmpty) assert(!obj.HasGUID) } diff --git a/pslogin/src/test/scala/PacketCodingActorTest.scala b/pslogin/src/test/scala/PacketCodingActorTest.scala index a2096f014..9922f93fa 100644 --- a/pslogin/src/test/scala/PacketCodingActorTest.scala +++ b/pslogin/src/test/scala/PacketCodingActorTest.scala @@ -452,45 +452,44 @@ class PacketCodingActorHTest extends ActorTest { } class PacketCodingActorITest extends ActorTest { + import net.psforever.packet.game.objectcreate._ + val pos : PlacementData = PlacementData(Vector3.Zero, Vector3.Zero) + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1), + 3, + false, + false, + ExoSuitType.Standard, + "", + 0, + false, + 2.8125f, 210.9375f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + var char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData( + 0, + 0, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + List(CertificationType.StandardAssault, CertificationType.MediumAssault, CertificationType.ATV, CertificationType.Harasser, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit, CertificationType.ReinforcedExoSuit), + List(), + List(), + List.empty, + None + ) + val obj = DetailedPlayerData(pos, app, char, InventoryData(Nil), DrawnSlot.None) + val pkt = MultiPacketBundle(List(ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj))) + val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c0049008452700000000000000000000000000000002000000fe6a703fffffffffffffffffffffffffffffffc00000000000000000000000000000000000000019001900064000001007ec800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000000200700" + "PacketCodingActor" should { "bundle an r-originating packet into an l-facing SlottedMetaPacket byte stream data (SlottedMetaPacket)" in { - import net.psforever.packet.game.objectcreate._ - val obj = DetailedCharacterData( - CharacterAppearanceData( - PlacementData(Vector3.Zero, Vector3.Zero), - BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1), - 3, - false, - false, - ExoSuitType.Standard, - "", - 0, - false, - 2.8125f, 210.9375f, - true, - GrenadeState.None, - false, - false, - false, - RibbonBars() - ), - 0, - 0, - 100, 100, - 50, - 1, 7, 7, - 100, 100, - List(CertificationType.StandardAssault,CertificationType.MediumAssault,CertificationType.ATV,CertificationType.Harasser,CertificationType.StandardExoSuit,CertificationType.AgileExoSuit,CertificationType.ReinforcedExoSuit), - List(), - List(), - List.empty, - None, - Some(InventoryData(Nil)), - DrawnSlot.None - ) - val pkt = MultiPacketBundle(List(ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj))) - val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c0049008452700000000000000000000000000000002000000fe6a703fffffffffffffffffffffffffffffffc00000000000000000000000000000000000000019001900064000001007ec800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000000200700" - val probe1 = TestProbe() val probe2 = system.actorOf(Props(classOf[ActorTest.MDCTestProbe], probe1), "mdc-probe") val pca : ActorRef = system.actorOf(Props[PacketCodingActor], "pca") @@ -547,25 +546,25 @@ class PacketCodingActorJTest extends ActorTest { class PacketCodingActorKTest extends ActorTest { import net.psforever.packet.game.objectcreate._ - val obj = DetailedCharacterData( - CharacterAppearanceData( - PlacementData(Vector3.Zero, Vector3.Zero), - BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1), - 3, - false, - false, - ExoSuitType.Standard, - "", - 0, - false, - 2.8125f, 210.9375f, - true, - GrenadeState.None, - false, - false, - false, - RibbonBars() - ), + val pos : PlacementData = PlacementData(Vector3.Zero, Vector3.Zero) + val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData( + BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1), + 3, + false, + false, + ExoSuitType.Standard, + "", + 0, + false, + 2.8125f, 210.9375f, + true, + GrenadeState.None, + false, + false, + false, + RibbonBars() + ) + var char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData( 0, 0, 100, 100, @@ -576,10 +575,9 @@ class PacketCodingActorKTest extends ActorTest { List(), List("xpe_sanctuary_help", "xpe_th_firemodes", "used_beamer", "map13"), List.empty, - None, - Some(InventoryData(Nil)), - DrawnSlot.None + None ) + val obj = DetailedPlayerData(pos, app, char, InventoryData(Nil), DrawnSlot.None) val list = List( ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj), ObjectDeleteMessage(PlanetSideGUID(1103), 2), diff --git a/pslogin/src/test/scala/RemoverActorTest.scala b/pslogin/src/test/scala/RemoverActorTest.scala new file mode 100644 index 000000000..2d0704d3e --- /dev/null +++ b/pslogin/src/test/scala/RemoverActorTest.scala @@ -0,0 +1,537 @@ +//// Copyright (c) 2017 PSForever +//import akka.actor.{ActorRef, Props} +//import akka.routing.RandomPool +//import akka.testkit.TestProbe +//import net.psforever.objects.PlanetSideGameObject +//import net.psforever.objects.definition.{EquipmentDefinition, ObjectDefinition} +//import net.psforever.objects.equipment.Equipment +//import net.psforever.objects.guid.TaskResolver +//import net.psforever.objects.zones.{Zone, ZoneMap} +//import net.psforever.packet.game.PlanetSideGUID +//import services.{RemoverActor, ServiceManager} +// +//import scala.concurrent.duration._ +// +//class StandardRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// +// "RemoverActor" should { +// "handle a simple task" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) +// +// val reply1 = probe.receiveOne(200 milliseconds) +// assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) +// val reply2 = probe.receiveOne(200 milliseconds) +// assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert]) +// probe.expectNoMsg(1 seconds) //delay +// val reply3 = probe.receiveOne(300 milliseconds) +// assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4 = probe.receiveOne(300 milliseconds) +// assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5 = probe.receiveOne(300 milliseconds) +// assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6 = probe.receiveOne(500 milliseconds) +// assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7 = probe.receiveOne(500 milliseconds) +// assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// } +// } +//} +// +//class DelayedRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// +// "RemoverActor" should { +// "handle a simple task (timed)" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(100 milliseconds)) +// +// val reply1 = probe.receiveOne(200 milliseconds) +// assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) +// val reply2 = probe.receiveOne(200 milliseconds) +// assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert]) +// //no delay +// val reply3 = probe.receiveOne(300 milliseconds) +// assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4 = probe.receiveOne(300 milliseconds) +// assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5 = probe.receiveOne(300 milliseconds) +// assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6 = probe.receiveOne(300 milliseconds) +// assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7 = probe.receiveOne(300 milliseconds) +// assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// } +// } +//} +// +//class ExcludedRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// val AlternateTestObject = new PlanetSideGameObject() { def Definition = new ObjectDefinition(0) { } } +// +// "RemoverActor" should { +// "allow only specific objects" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(AlternateTestObject, Zone.Nowhere) +// +// val reply1 = probe.receiveOne(200 milliseconds) +// assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) +// expectNoMsg(2 seconds) +// //RemoverActor is stalled because it received an object that it was not allowed to act upon +// } +// } +//} +// +//class MultipleRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// +// "RemoverActor" should { +// "work on parallel tasks" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere) +// +// val replies = probe.receiveN(14, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// var fja : Int = 0 +// var cta : Int = 0 +// var sja : Int = 0 +// var dta : Int = 0 +// var dtr : Int = 0 +// replies.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case RemoverActorTest.FirstJobAlert() => fja += 1 +// case RemoverActorTest.ClearanceTestAlert() => cta += 1 +// case RemoverActorTest.SecondJobAlert() => sja += 1 +// case RemoverActorTest.DeletionTaskAlert() => dta += 1 +// case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 2 && ija == 2 && fja == 2 && cta == 2 && sja == 2 && dta == 2 && dtr == 2) +// } +// } +//} +// +//class HurrySpecificRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// +// "RemoverActor" should { +// "be able to hurry certain tasks" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(10 minutes)) //TEN MINUTE WAIT +// +// val reply1 = probe.receiveOne(200 milliseconds) +// assert(reply1.isInstanceOf[RemoverActorTest.InclusionTestAlert]) +// val reply2 = probe.receiveOne(200 milliseconds) +// assert(reply2.isInstanceOf[RemoverActorTest.InitialJobAlert]) +// probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 10 minutes +// remover ! RemoverActor.HurrySpecific(List(RemoverActorTest.TestObject), Zone.Nowhere) //hurried +// val reply3 = probe.receiveOne(300 milliseconds) +// assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4 = probe.receiveOne(300 milliseconds) +// assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5 = probe.receiveOne(300 milliseconds) +// assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6 = probe.receiveOne(500 milliseconds) +// assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7 = probe.receiveOne(500 milliseconds) +// assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// } +// } +//} +// +//class HurrySelectionRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// +// "RemoverActor" should { +// "be able to hurry certain tasks, but let others finish normally" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(10 seconds)) +// +// val replies = probe.receiveN(4, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 2 && ija == 2) +// probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 5 seconds +// remover ! RemoverActor.HurrySpecific(List(RemoverActorTest.TestObject), Zone.Nowhere) //hurried +// //first +// val reply3a = probe.receiveOne(300 milliseconds) +// assert(reply3a.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4a = probe.receiveOne(300 milliseconds) +// assert(reply4a.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5a = probe.receiveOne(300 milliseconds) +// assert(reply5a.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6a = probe.receiveOne(500 milliseconds) +// assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7a = probe.receiveOne(500 milliseconds) +// assert(reply7a.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// //second +// remover ! RemoverActor.HurrySpecific(List(TestObject2), Zone.Nowhere) //hurried +// val reply3b = probe.receiveOne(300 milliseconds) +// assert(reply3b.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4b = probe.receiveOne(300 milliseconds) +// assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5b = probe.receiveOne(300 milliseconds) +// assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6b = probe.receiveOne(500 milliseconds) +// assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7b = probe.receiveOne(500 milliseconds) +// assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// } +// } +//} +// +//class HurryMultipleRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// final val TestObject3 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(3) } } +// +// "RemoverActor" should { +// "be able to hurry certain tasks, but only valid ones" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) +// +// val replies = probe.receiveN(4, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 2 && ija == 2) +// probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 5 seconds +// remover ! RemoverActor.HurrySpecific(List(RemoverActorTest.TestObject, TestObject3), Zone.Nowhere) //multiple hurried, only one valid +// //first +// val reply3a = probe.receiveOne(300 milliseconds) +// assert(reply3a.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4a = probe.receiveOne(300 milliseconds) +// assert(reply4a.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5a = probe.receiveOne(300 milliseconds) +// assert(reply5a.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6a = probe.receiveOne(500 milliseconds) +// assert(reply6a.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7a = probe.receiveOne(500 milliseconds) +// assert(reply7a.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// //second +// remover ! RemoverActor.HurrySpecific(List(TestObject2), Zone.Nowhere) //hurried +// val reply3b = probe.receiveOne(300 milliseconds) +// assert(reply3b.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4b = probe.receiveOne(300 milliseconds) +// assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5b = probe.receiveOne(300 milliseconds) +// assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6b = probe.receiveOne(500 milliseconds) +// assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7b = probe.receiveOne(500 milliseconds) +// assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// } +// } +//} +// +//class HurryByZoneRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// final val TestObject3 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(3) } } +// final val zone = new Zone("test", new ZoneMap("test-map"), 11) +// +// "RemoverActor" should { +// "be able to hurry certain tasks by their zone" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, zone, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject3, Zone.Nowhere, Some(5 seconds)) +// +// val replies1 = probe.receiveN(6, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies1.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 3 && ija == 3) +// probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet 5 seconds +// remover ! RemoverActor.HurrySpecific(List(), Zone.Nowhere) //multiple hurried, only the two entries with Zone.Nowhere +// // +// val replies2 = probe.receiveN(10, 5 seconds) +// var fja : Int = 0 +// var cta : Int = 0 +// var sja : Int = 0 +// var dta : Int = 0 +// var dtr : Int = 0 +// replies2.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case RemoverActorTest.FirstJobAlert() => fja += 1 +// case RemoverActorTest.ClearanceTestAlert() => cta += 1 +// case RemoverActorTest.SecondJobAlert() => sja += 1 +// case RemoverActorTest.DeletionTaskAlert() => dta += 1 +// case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1 +// case msg => assert(false, s"$msg") +// } +// assert(fja == 2 && cta == 2 && sja == 2 && dta == 2 && dtr == 2) +// //final +// remover ! RemoverActor.HurrySpecific(List(), zone) //hurried +// val reply3b = probe.receiveOne(300 milliseconds) +// assert(reply3b.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4b = probe.receiveOne(300 milliseconds) +// assert(reply4b.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5b = probe.receiveOne(300 milliseconds) +// assert(reply5b.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6b = probe.receiveOne(500 milliseconds) +// assert(reply6b.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7b = probe.receiveOne(500 milliseconds) +// assert(reply7b.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// } +// } +//} +// +//class HurryAllRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// final val TestObject3 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(3) } } +// +// "RemoverActor" should { +// "be able to hurry all tasks to completion" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(20 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(15 seconds)) +// remover ! RemoverActor.AddTask(TestObject3, Zone.Nowhere, Some(10 seconds)) +// +// val replies1 = probe.receiveN(6, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies1.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 3 && ija == 3) +// probe.expectNoMsg(3 seconds) //long delay, longer than standard but not yet longer than any of the tasks +// remover ! RemoverActor.HurryAll() //all hurried +// // +// val replies2 = probe.receiveN(15, 5 seconds) +// var fja : Int = 0 +// var cta : Int = 0 +// var sja : Int = 0 +// var dta : Int = 0 +// var dtr : Int = 0 +// replies2.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case RemoverActorTest.FirstJobAlert() => fja += 1 +// case RemoverActorTest.ClearanceTestAlert() => cta += 1 +// case RemoverActorTest.SecondJobAlert() => sja += 1 +// case RemoverActorTest.DeletionTaskAlert() => dta += 1 +// case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1 +// case msg => assert(false, s"$msg") +// } +// assert(fja == 3 && cta == 3 && sja == 3 && dta == 3 && dtr == 3) +// } +// } +//} +// +//class ClearSelectionRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// +// "RemoverActor" should { +// "be able to clear certain tasks" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) +// +// val replies = probe.receiveN(4, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 2 && ija == 2) +// probe.expectNoMsg(4 seconds) //long delay, longer than standard but not yet 5 seconds +// remover ! RemoverActor.ClearSpecific(List(RemoverActorTest.TestObject), Zone.Nowhere) //cleared +// // +// val reply3 = probe.receiveOne(2 seconds) +// assert(reply3.isInstanceOf[RemoverActorTest.FirstJobAlert]) +// val reply4 = probe.receiveOne(300 milliseconds) +// assert(reply4.isInstanceOf[RemoverActorTest.ClearanceTestAlert]) +// val reply5 = probe.receiveOne(300 milliseconds) +// assert(reply5.isInstanceOf[RemoverActorTest.SecondJobAlert]) +// val reply6 = probe.receiveOne(500 milliseconds) +// assert(reply6.isInstanceOf[RemoverActorTest.DeletionTaskAlert]) +// val reply7 = probe.receiveOne(500 milliseconds) +// assert(reply7.isInstanceOf[RemoverActorTest.DeletionTaskRunAlert]) +// //wait +// probe.expectNoMsg(2 seconds) //nothing more to do +// } +// } +//} +// +//class ClearAllRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// +// "RemoverActor" should { +// "be able to clear all tasks, with no more work on them" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) +// +// val replies = probe.receiveN(4, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 2 && ija == 2) +// probe.expectNoMsg(4 seconds) //long delay, longer than standard but not yet 5 seconds +// remover ! RemoverActor.ClearAll() //cleared +// //wait +// probe.expectNoMsg(3 seconds) //nothing more to do +// } +// } +//} +// +//class EarlyDeathRemoverActorTest extends ActorTest { +// ServiceManager.boot ! ServiceManager.Register(RandomPool(2).props(Props[TaskResolver]), "taskResolver") +// final val TestObject2 = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(2) } } +// +// "RemoverActor" should { +// "be able to hurry certain tasks" in { +// expectNoMsg(500 milliseconds) +// val probe = TestProbe() +// val remover = system.actorOf(Props(classOf[RemoverActorTest.TestRemover], probe), "test-remover") +// remover ! RemoverActor.AddTask(RemoverActorTest.TestObject, Zone.Nowhere, Some(5 seconds)) +// remover ! RemoverActor.AddTask(TestObject2, Zone.Nowhere, Some(5 seconds)) +// +// val replies = probe.receiveN(4, 5 seconds) +// var ita : Int = 0 +// var ija : Int = 0 +// replies.collect { +// case RemoverActorTest.InclusionTestAlert() => ita += 1 +// case RemoverActorTest.InitialJobAlert() => ija += 1 +// case msg => assert(false, s"$msg") +// } +// assert(ita == 2 && ija == 2) +// probe.expectNoMsg(2 seconds) +// remover ! akka.actor.PoisonPill +// // +// val replies2 = probe.receiveN(8, 5 seconds) +// var fja : Int = 0 +// var cta : Int = 0 +// var sja : Int = 0 +// var dta : Int = 0 +// var dtr : Int = 0 +// replies2.collect { +// case RemoverActorTest.FirstJobAlert() => fja += 1 +// case RemoverActorTest.ClearanceTestAlert() => cta += 1 +// case RemoverActorTest.SecondJobAlert() => sja += 1 +// case RemoverActorTest.DeletionTaskAlert() => dta += 1 +// case RemoverActorTest.DeletionTaskRunAlert() => dtr += 1 +// case msg => assert(false, s"$msg") +// } +// assert(fja == 2 && cta == 0 && sja == 2 && dta == 2 && dtr == 2) //no clearance tests +// } +// } +//} +// +//object RemoverActorTest { +// final val TestObject = new Equipment() { def Definition = new EquipmentDefinition(0) { GUID = PlanetSideGUID(1) } } +// +// final case class InclusionTestAlert() +// +// final case class InitialJobAlert() +// +// final case class FirstJobAlert() +// +// final case class SecondJobAlert() +// +// final case class ClearanceTestAlert() +// +// final case class DeletionTaskAlert() +// +// final case class DeletionTaskRunAlert() +// +// class TestRemover(probe : TestProbe) extends RemoverActor { +// import net.psforever.objects.guid.{Task, TaskResolver} +// val FirstStandardDuration = 1 seconds +// +// val SecondStandardDuration = 100 milliseconds +// +// def InclusionTest(entry : RemoverActor.Entry) : Boolean = { +// probe.ref ! InclusionTestAlert() +// entry.obj.isInstanceOf[Equipment] +// } +// +// def InitialJob(entry : RemoverActor.Entry) : Unit = { +// probe.ref ! InitialJobAlert() +// } +// +// def FirstJob(entry : RemoverActor.Entry) : Unit = { +// probe.ref ! FirstJobAlert() +// } +// +// override def SecondJob(entry : RemoverActor.Entry) : Unit = { +// probe.ref ! SecondJobAlert() +// super.SecondJob(entry) +// } +// +// def ClearanceTest(entry : RemoverActor.Entry) : Boolean = { +// probe.ref ! ClearanceTestAlert() +// true +// } +// +// def DeletionTask(entry : RemoverActor.Entry) : TaskResolver.GiveTask = { +// probe.ref ! DeletionTaskAlert() +// TaskResolver.GiveTask(new Task() { +// private val localProbe = probe +// +// override def isComplete = Task.Resolution.Success +// +// def Execute(resolver : ActorRef) : Unit = { +// localProbe.ref ! DeletionTaskRunAlert() +// resolver ! scala.util.Success(this) +// } +// }) +// } +// } +//} diff --git a/pslogin/src/test/scala/VehicleServiceTest.scala b/pslogin/src/test/scala/VehicleServiceTest.scala index 261556c7f..e2d48c97d 100644 --- a/pslogin/src/test/scala/VehicleServiceTest.scala +++ b/pslogin/src/test/scala/VehicleServiceTest.scala @@ -68,15 +68,15 @@ class VehicleService5Test extends ActorTest { } } -class AwarenessTest extends ActorTest { +class OwnershipTest extends ActorTest { ServiceManager.boot(system) "VehicleService" should { "pass Awareness" in { val service = system.actorOf(Props[VehicleService], "v-service") service ! Service.Join("test") - service ! VehicleServiceMessage("test", VehicleAction.Awareness(PlanetSideGUID(10), PlanetSideGUID(11))) - expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.Awareness(PlanetSideGUID(11)))) + service ! VehicleServiceMessage("test", VehicleAction.Ownership(PlanetSideGUID(10), PlanetSideGUID(11))) + expectMsg(VehicleServiceResponse("/test/Vehicle", PlanetSideGUID(10), VehicleResponse.Ownership(PlanetSideGUID(11)))) } } }