From caf56c4e72c2f79e0ade0e2b754d199c02f89e6d Mon Sep 17 00:00:00 2001 From: FateJH Date: Sun, 3 Jun 2018 01:15:47 -0400 Subject: [PATCH] added a door in home3 HART C that I missed; modified AvatarConverter and VehicleConverter to correctly handle multiple players in vehicles at world join time; began implementation of this procedure in WSA, but independent creation of players in implant terminals is giving trouble; fixed a ludicrous oversight with the bitsize of players without outfits --- .../converter/AvatarConverter.scala | 47 +++++--- .../converter/CharacterSelectConverter.scala | 6 +- .../converter/VehicleConverter.scala | 24 +++- .../CharacterAppearanceData.scala | 19 +-- .../packet/game/objectcreate/PlayerData.scala | 31 +---- .../game/objectcreate/VehicleData.scala | 70 ++++++++++- .../game/objectcreate/CharacterDataTest.scala | 112 +++++++++++++++++- .../MountedVehiclesTest.scala | 2 +- pslogin/src/main/scala/Maps.scala | 2 + .../src/main/scala/WorldSessionActor.scala | 45 ++++--- 10 files changed, 267 insertions(+), 91 deletions(-) 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 10a332b5..2fb63bd1 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 @@ -11,26 +11,21 @@ import scala.util.{Success, Try} class AvatarConverter extends ObjectCreateConverter[Player]() { override def ConstructorData(obj : Player) : Try[PlayerData] = { + import AvatarConverter._ val MaxArmor = obj.MaxArmor - Success( + Success( PlayerData.apply( PlacementData(obj.Position, obj.Orientation, obj.Velocity), MakeAppearanceData(obj), - 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) - ), - InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), //TODO is sorting necessary? + MakeCharacterData(obj), + MakeInventoryData(obj), GetDrawnSlot(obj) ) ) } override def DetailedConstructorData(obj : Player) : Try[DetailedPlayerData] = { + import AvatarConverter._ Success( DetailedPlayerData.apply( PlacementData(obj.Position, obj.Orientation, obj.Velocity), @@ -54,13 +49,15 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { ) ) } +} +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) : (Int)=>CharacterAppearanceData = { + def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = { CharacterAppearanceData( BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice), 0, @@ -81,6 +78,27 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { ) } + 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 MakeInventoryData(obj : Player) : InventoryData = { + InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)) //TODO is sorting necessary? + } + /** * 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. @@ -187,7 +205,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)) } @@ -210,6 +228,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { 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, @@ -255,7 +274,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) } @@ -293,7 +312,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 bb00d7dc..7840e027 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 @@ -29,10 +29,10 @@ class CharacterSelectConverter extends AvatarConverter { Nil, MakeImplantEntries(obj), //necessary for correct stream length Nil, Nil, - MakeCosmetics(obj.BEP) + AvatarConverter.MakeCosmetics(obj.BEP) ), InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)), - GetDrawnSlot(obj) + AvatarConverter.GetDrawnSlot(obj) ) ) } @@ -92,7 +92,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/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 81f3f2b6..e354e8e8 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,7 +10,7 @@ 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] = { Success( @@ -29,10 +29,30 @@ class VehicleConverter extends ObjectCreateConverter[Vehicle]() { false, obj.Cloaked, SpecificFormatData(obj), - Some(InventoryData((MakeUtilities(obj) ++ MakeMountings(obj)).sortBy(_.parentSlot))) + Some(InventoryData((MakeSeats(obj) ++ MakeUtilities(obj) ++ MakeMountings(obj)).sortBy(_.parentSlot))) )(SpecificFormatModifier) ) } + + private def MakeSeats(obj : Vehicle) : List[InventoryItemData.InventoryItem] = { + var offset : Long = VehicleData.InitialStreamLengthToSeatEntries(true, 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 $index offset: $offset, size: ${entry.bitsize}") + 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/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala index e590def6..35ae248e 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 @@ -124,7 +124,8 @@ final case class CharacterAppearanceData(app : BasicCharacterData, override def bitsize : Long = { //factor guard bool values into the base size, not its corresponding optional field val nameStringSize : Long = StreamBitSize.stringBitSize(app.name, 16) + name_padding - val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding + val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + + (if(outfit_name.nonEmpty) { CharacterAppearanceData.outfitNamePadding } else { 0 }) val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0) 335L + nameStringSize + outfitStringSize + altModelSize } @@ -153,20 +154,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 namePaddingRule(pad : Int) : Int = - if(pad == 0) { - //note that this counts as 1 (local) + 4 (inherited from PlayerData OCM with parent) - 5 //normal alignment padding - } - else { - pad //custom padding value - } - /** * Get the padding of the outfit's name. * The padding will always be a number 0-7. @@ -234,7 +221,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { 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 : Boolean = false var alt_model_extrabit : Option[Boolean] = None if(zipline || bpack) { alt_model = true 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 index 636df525..ab9c0d30 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala @@ -65,7 +65,7 @@ object PlayerData extends Marshallable[PlayerData] { * @return a `PlayerData` object */ def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type, accumulative : Long) : PlayerData = { - val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) + val appearance = basic_appearance(1) PlayerData(None, appearance, character_data(appearance.backpack, true), Some(inventory), drawn_slot)(false) } /** @@ -80,7 +80,7 @@ object PlayerData extends Marshallable[PlayerData] { * @return a `PlayerData` object */ def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type, accumulative : Long) : PlayerData = { - val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative)) + val appearance = basic_appearance(1) PlayerData(None, appearance, character_data(appearance.backpack, true), None, drawn_slot)(false) } @@ -114,31 +114,6 @@ object PlayerData extends Marshallable[PlayerData] { PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), None, drawn_slot)(true) } - /** - * 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 - */ - def CumulativeSeatedPlayerNamePadding(accumulative : Long) : Int = { - val offset = accumulative + 23 + 35 - val pad = ((offset - math.floor(offset / 8) * 8) % 8).toInt - if(pad > 0) { - 8 - pad - } - else { - 0 - } - } - /** * Determine the padding offset for a subsequent field given the existence of `PlacementData`. * The padding will always be a number 0-7. @@ -151,7 +126,7 @@ object PlayerData extends Marshallable[PlayerData] { case Some(place) => if(place.vel.isDefined) { 2 } else { 4 } case None => - 0 + 1 } } 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 56753c64..c489ad9b 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 @@ -146,6 +146,41 @@ object VehicleData extends Marshallable[VehicleData] { new VehicleData(basic, unk1, health, unk2>0, false, driveState, unk3, unk5>0, false, Some(unk4), 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) /** @@ -273,12 +308,37 @@ object VehicleData extends Marshallable[VehicleData] { * @return the padding value, 0-7 bits */ def CumulativeSeatedPlayerNamePadding(base : Long, next : Option[StreamBitSize]) : Int = { - PlayerData.CumulativeSeatedPlayerNamePadding(base + (next match { + 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 = { + val offset = accumulative + 23 + 35 + val pad = ((offset - math.floor(offset / 8) * 8) % 8).toInt + if(pad > 0) { + 8 - pad + } + else { + 0 + } + } + /** * A special method of handling mounted players within the same inventory space as normal `Equipment` can be encountered. * Due to variable-length fields within `PlayerData` extracted from the input, @@ -299,7 +359,7 @@ object VehicleData extends Marshallable[VehicleData] { uint2 :: (inventory_seat_codec( length, //length of stream until current seat - PlayerData.CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat + CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat ) >>:~ { seats => PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist }) @@ -348,7 +408,7 @@ object VehicleData extends Marshallable[VehicleData] { case None => 0 }) }, - VehicleData.CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat + CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat )).hlist } } @@ -381,7 +441,7 @@ object VehicleData extends Marshallable[VehicleData] { * this padding value must recalculate for each represented seat * @see `CharacterAppearanceData`
* `VehicleData.InitialStreamLengthToSeatEntries`
- * `PlayerData.CumulativeSeatedPlayerNamePadding` + * `CumulativeSeatedPlayerNamePadding` * @return a `Codec` that translates `PlayerData` */ private def seat_codec(pad : Int) : Codec[InternalSlot] = { @@ -390,7 +450,7 @@ object VehicleData extends Marshallable[VehicleData] { ("objectClass" | uintL(11)) :: ("guid" | PlanetSideGUID.codec) :: ("parentSlot" | PacketHelpers.encodedStringSize) :: - ("obj" | PlayerData.codec(pad)) + ("obj" | Player_Data.codec(pad)) ).xmap[InternalSlot] ( { case objectClass :: guid :: parentSlot :: obj :: HNil => diff --git a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala index 06805d20..ae7f2826 100644 --- a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala +++ b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala @@ -9,12 +9,18 @@ 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"17ff06000069023c83e0f800000011a530063007200610077006e00790052006f006e006e0069006500220b7000000000000000000000000" ++ + hex"6800000268042006c00610063006b002000420065007200650074002000410072006d006f007500720065006400200043006f00720070007" ++ + hex"3001700e0030050040003bc00000234040001a00400020a8fa0a5424e0e800000000080952a9c3a03000001081103e040000000a023782f1" ++ + hex"080c0000016244108200000000808382403a030000014284c3a0c0000000202512f00b80c00000578f80f840000000280838b3c32030000008" + 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 @@ -111,8 +117,48 @@ class CharacterDataTest extends Specification { } } + "decode (seated)" in { + PacketCoding.DecodePacket(string_seated).require match { + case ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 1791 + 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 5 + 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 @@ -220,7 +266,7 @@ class CharacterDataTest extends Specification { 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,6 +275,60 @@ class CharacterDataTest extends Specification { //TODO work on CharacterData to make this pass as a single stream } + "encode (seated)" in { + 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, + 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 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, 0) + + 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), @@ -272,7 +372,7 @@ class CharacterDataTest extends Specification { 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/objectcreatevehicle/MountedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index 938d6e6a..eb15db39 100644 --- a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -145,7 +145,7 @@ class MountedVehiclesTest extends Specification { ) ) ) - val player = PlayerData.apply(app, char, inv, DrawnSlot.None, VehicleData.InitialStreamLengthToSeatEntries(true, VehicleFormat.Variant)) + val player = VehicleData.PlayerData(app, char, inv, DrawnSlot.None, VehicleData.InitialStreamLengthToSeatEntries(true, VehicleFormat.Variant)) val obj = VehicleData( CommonFieldData( PlacementData( diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index 64f8e22f..0a710e80 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -473,6 +473,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 +521,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/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 33e4ab6e..7b03a5eb 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -1649,30 +1649,23 @@ 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 => { + sendResponse(ObjectCreateMessage(ObjectClass.avatar, 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 } + var mountedPlayers : Set[Player] = Set.empty //players in vehicles //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).get.Occupant match { - case Some(tplayer) => - if(tplayer.HasGUID) { - sendResponse(ObjectAttachMessage(vehicle.GUID, tplayer.GUID, seat_num)) - } - case None => ; - } - }) ReloadVehicleAccessPermissions(vehicle) }) //implant terminals @@ -1692,8 +1685,28 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => ; } //seat terminal occupants + import net.psforever.objects.definition.converter.AvatarConverter continent.GUID(terminal_guid) match { case Some(obj : Mountable) => + obj.Seats + .filter({ case(_, seat) => seat.isOccupied }) + .foreach({ case(index, seat) => + val tplayer = seat.Occupant.get + val tdefintion = tplayer.Definition + sendResponse(ObjectCreateMessage( + tdefintion.ObjectId, + tplayer.GUID, + ObjectCreateMessageParent(parent_guid, index), + PlayerData( + AvatarConverter.MakeAppearanceData(tplayer), + AvatarConverter.MakeCharacterData(tplayer), + AvatarConverter.MakeInventoryData(tplayer), + AvatarConverter.GetDrawnSlot(tplayer), + 0 + ) + )) + }) + obj.MountPoints.foreach({ case ((_, seat_num)) => obj.Seat(seat_num).get.Occupant match { case Some(tplayer) =>