diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..ed4904db --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +# Too spammy for us +comment: off diff --git a/common/src/main/scala/net/psforever/crypto/CryptoInterface.scala b/common/src/main/scala/net/psforever/crypto/CryptoInterface.scala index c6e7e300..24e695cd 100644 --- a/common/src/main/scala/net/psforever/crypto/CryptoInterface.scala +++ b/common/src/main/scala/net/psforever/crypto/CryptoInterface.scala @@ -13,6 +13,20 @@ object CryptoInterface { final val PSCRYPTO_VERSION_MAJOR = 1 final val PSCRYPTO_VERSION_MINOR = 1 + /** + NOTE: this is a single, global shared library for the entire server's crypto needs + + Unfortunately, access to this object didn't used to be synchronized. I noticed that + tests for this module were hanging ("arrive at a shared secret" & "must fail to agree on + a secret..."). This heisenbug was responsible for failed Travis test runs and developer + issues as well. Using Windows minidumps, I tracked the issue to a single thread deep in + pscrypto.dll. It appeared to be executing an EB FE instruction (on Intel x86 this is + `jmp $-2` or jump to self), which is an infinite loop. The stack trace made little to no + sense and after banging my head on the wall for many hours, I assumed that something deep + in CryptoPP, the libgcc libraries, or MSVC++ was the cause (or myself). Now all access to + pscrypto functions that allocate and deallocate memory (DH_Start, RC5_Init) are synchronized. + This *appears* to have fixed the problem. + */ final val psLib = new Library(libName) final val RC5_BLOCK_SIZE = 8 @@ -48,7 +62,7 @@ object CryptoInterface { if(!psLib.PSCrypto_Init(PSCRYPTO_VERSION_MAJOR, PSCRYPTO_VERSION_MINOR)[Boolean]) { throw new IllegalArgumentException(s"Invalid PSCrypto library version ${libraryMajor.getValue}.${libraryMinor.getValue}. Expected " + - s"${PSCRYPTO_VERSION_MAJOR}.${PSCRYPTO_VERSION_MINOR}") + s"$PSCRYPTO_VERSION_MAJOR.$PSCRYPTO_VERSION_MINOR") } } @@ -121,7 +135,9 @@ object CryptoInterface { if(started) throw new IllegalStateException("DH state has already been started") - dhHandle = psLib.DH_Start(modulus.toArray, generator.toArray, privateKey, publicKey)[Pointer] + psLib.synchronized { + dhHandle = psLib.DH_Start(modulus.toArray, generator.toArray, privateKey, publicKey)[Pointer] + } if(dhHandle == Pointer.NULL) throw new Exception("DH initialization failed!") @@ -138,7 +154,9 @@ object CryptoInterface { if(started) throw new IllegalStateException("DH state has already been started") - dhHandle = psLib.DH_Start_Generate(privateKey, publicKey, p, g)[Pointer] + psLib.synchronized { + dhHandle = psLib.DH_Start_Generate(privateKey, publicKey, p, g)[Pointer] + } if(dhHandle == Pointer.NULL) throw new Exception("DH initialization failed!") @@ -185,7 +203,9 @@ object CryptoInterface { override def close = { if(started) { // TODO: zero private key material - psLib.Free_DH(dhHandle)[Unit] + psLib.synchronized { + psLib.Free_DH(dhHandle)[Unit] + } started = false } @@ -196,8 +216,13 @@ object CryptoInterface { class CryptoState(val decryptionKey : ByteVector, val encryptionKey : ByteVector) extends IFinalizable { // Note that the keys must be returned as primitive Arrays for JNA to work - val encCryptoHandle = psLib.RC5_Init(encryptionKey.toArray, encryptionKey.length, true)[Pointer] - val decCryptoHandle = psLib.RC5_Init(decryptionKey.toArray, decryptionKey.length, false)[Pointer] + var encCryptoHandle : Pointer = Pointer.NULL + var decCryptoHandle : Pointer = Pointer.NULL + + psLib.synchronized { + encCryptoHandle = psLib.RC5_Init(encryptionKey.toArray, encryptionKey.length, true)[Pointer] + decCryptoHandle = psLib.RC5_Init(decryptionKey.toArray, decryptionKey.length, false)[Pointer] + } if(encCryptoHandle == Pointer.NULL) throw new Exception("Encryption initialization failed!") @@ -234,8 +259,10 @@ object CryptoInterface { } override def close = { - psLib.Free_RC5(encCryptoHandle)[Unit] - psLib.Free_RC5(decCryptoHandle)[Unit] + psLib.synchronized { + psLib.Free_RC5(encCryptoHandle)[Unit] + psLib.Free_RC5(decCryptoHandle)[Unit] + } super.close } } @@ -246,8 +273,8 @@ object CryptoInterface { val encryptionMACKey : ByteVector) extends CryptoState(decryptionKey, encryptionKey) { /** * Performs a MAC operation over the message. Used when encrypting packets - * - * @param message + * + * @param message the input message * @return ByteVector */ def macForEncrypt(message : ByteVector) : ByteVector = { @@ -256,8 +283,8 @@ object CryptoInterface { /** * Performs a MAC operation over the message. Used when verifying decrypted packets - * - * @param message + * + * @param message the input message * @return ByteVector */ def macForDecrypt(message : ByteVector) : ByteVector = { diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index b697b677..0ebb81cf 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -544,13 +544,13 @@ object GamePacketOpcode extends Enumeration { case 0xbc => noDecoder(SnoopMsg) case 0xbd => game.PlayerStateMessageUpstream.decode case 0xbe => game.PlayerStateShiftMessage.decode - case 0xbf => noDecoder(ZipLineMessage) + case 0xbf => game.ZipLineMessage.decode // OPCODES 0xc0-cf case 0xc0 => noDecoder(CaptureFlagUpdateMessage) case 0xc1 => noDecoder(VanuModuleUpdateMessage) case 0xc2 => noDecoder(FacilityBenefitShieldChargeRequestMessage) - case 0xc3 => noDecoder(ProximityTerminalUseMessage) + case 0xc3 => game.ProximityTerminalUseMessage.decode case 0xc4 => game.QuantityDeltaUpdateMessage.decode case 0xc5 => noDecoder(ChainLashMessage) case 0xc6 => game.ZoneInfoMessage.decode @@ -591,7 +591,7 @@ object GamePacketOpcode extends Enumeration { case 0xe3 => noDecoder(ZoneForcedCavernConnectionsMessage) case 0xe4 => noDecoder(MissionActionMessage) case 0xe5 => noDecoder(MissionKillTriggerMessage) - case 0xe6 => noDecoder(ReplicationStreamMessage) + case 0xe6 => game.ReplicationStreamMessage.decode case 0xe7 => game.SquadDefinitionActionMessage.decode // 0xe8 case 0xe8 => noDecoder(SquadDetailDefinitionUpdateMessage) diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarFirstTimeEventMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarFirstTimeEventMessage.scala index 0fd5bc10..29c5b3af 100644 --- a/common/src/main/scala/net/psforever/packet/game/AvatarFirstTimeEventMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/AvatarFirstTimeEventMessage.scala @@ -5,9 +5,29 @@ import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, Plan import scodec.Codec import scodec.codecs._ +/** + * Dispatched to the server when the player encounters something for the very first time in their campaign. + * For example, the first time the player rubs up against a game object with a yellow exclamation point. + * For example, the first time the player draws a specific weapon.
+ *
+ * When the first time events (FTE's) are received, battle experience is awarded. + * Text information about the object will be displayed. + * A certain itemized checkbox under the "Training" tab that corresponds is marked off. + * The latter list indicates all "encounter-able" game objects for which a FTE exists. + * These effects only happen once per character/campaign. + * (The Motion Sensor will occasionally erroneously display the information window on repeat encounters. + * No additional experience is given, though.)
+ *
+ * FTE's are recorded in a great `List` of `String`s in the middle of player `ObjectCreateMessage` data. + * Tutorial complete events are enqueued nearby. + * @param avatar_guid the player + * @param object_id the game object that triggers the event + * @param unk na + * @param event_name the string name of the event + */ final case class AvatarFirstTimeEventMessage(avatar_guid : PlanetSideGUID, - object_guid : PlanetSideGUID, - unk1 : Long, + object_id : PlanetSideGUID, + unk : Long, event_name : String) extends PlanetSideGamePacket { type Packet = AvatarFirstTimeEventMessage @@ -18,8 +38,8 @@ final case class AvatarFirstTimeEventMessage(avatar_guid : PlanetSideGUID, object AvatarFirstTimeEventMessage extends Marshallable[AvatarFirstTimeEventMessage] { implicit val codec : Codec[AvatarFirstTimeEventMessage] = ( ("avatar_guid" | PlanetSideGUID.codec) :: - ("object_guid" | PlanetSideGUID.codec) :: - ("unk1" | uint32L ) :: + ("object_id" | PlanetSideGUID.codec) :: + ("unk" | uint32L ) :: ("event_name" | PacketHelpers.encodedString) ).as[AvatarFirstTimeEventMessage] } diff --git a/common/src/main/scala/net/psforever/packet/game/BugReportMessage.scala b/common/src/main/scala/net/psforever/packet/game/BugReportMessage.scala index f444591e..b603eaec 100644 --- a/common/src/main/scala/net/psforever/packet/game/BugReportMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/BugReportMessage.scala @@ -34,10 +34,9 @@ object BugType extends Enumeration { * @param version_date the date the client was compiled * @param bug_type the kind of bug that took place * @param repeatable whether the bug is repeatable - * @param unk na; - * always 0? + * @param location 0 when "other location", 2 when "current location" * @param zone which zone the bug took place - * @param pos the location where ther bug took place + * @param pos the x y z location where the bug took place * @param summary a short explanation of the bug * @param desc a detailed explanation of the bug */ @@ -46,7 +45,7 @@ final case class BugReportMessage(version_major : Long, version_date : String, bug_type : BugType.Value, repeatable : Boolean, - unk : Int, + location : Int, zone : Int, pos : Vector3, summary : String, @@ -65,7 +64,7 @@ object BugReportMessage extends Marshallable[BugReportMessage] { ("bug_type" | BugType.codec) :: ignore(3) :: ("repeatable" | bool) :: - ("unk" | uint4L) :: + ("location" | uint4L) :: ("zone" | uint8L) :: ("pos" | Vector3.codec_pos) :: ("summary" | PacketHelpers.encodedWideStringAligned(4)) :: diff --git a/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala index 205adf29..28ffc71a 100644 --- a/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/BuildingInfoUpdateMessage.scala @@ -2,6 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideEmpire import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} 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 b616493d..ed5b4528 100644 --- a/common/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/CharacterCreateRequestMessage.scala @@ -2,8 +2,10 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.Codec +import net.psforever.types.PlanetSideEmpire +import scodec.{Attempt, Codec, Err} import scodec.codecs._ +import shapeless.{::, HNil} object CharacterGender extends Enumeration(1) { type Type = Value @@ -34,5 +36,17 @@ object CharacterCreateRequestMessage extends Marshallable[CharacterCreateRequest ("voiceId" | uint8L) :: ("gender" | CharacterGender.codec) :: ("empire" | PlanetSideEmpire.codec) - ).as[CharacterCreateRequestMessage] + ).exmap[CharacterCreateRequestMessage] ( + { + case name :: headId :: voiceId :: gender :: empire :: HNil => + Attempt.successful(CharacterCreateRequestMessage(name, headId, voiceId, gender, empire)) + }, + { + case CharacterCreateRequestMessage(name, _, _, _, PlanetSideEmpire.NEUTRAL) => + Attempt.failure(Err(s"character $name's faction can not declare as neutral")) + + case CharacterCreateRequestMessage(name, headId, voiceId, gender, empire) => + Attempt.successful(name :: headId :: voiceId :: gender :: empire :: HNil) + } + ) } \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/ContinentalLockUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ContinentalLockUpdateMessage.scala index 0b119347..24b0f319 100644 --- a/common/src/main/scala/net/psforever/packet/game/ContinentalLockUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ContinentalLockUpdateMessage.scala @@ -2,21 +2,21 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideEmpire import scodec.Codec import scodec.codecs._ /** - * Create a dispatched game packet that instructs the client to update the user about continents that are conquered. - * + * Create a dispatched game packet that instructs the client to update the user about continents that are conquered.
+ *
* This generates the event message "The [empire] have captured [continent]." * If the continent_guid is not a valid zone, no message is displayed. - * If empire is not a valid empire, no message is displayed. - * + * If empire is not a valid empire, or refers to the neutral or Black Ops forces, no message is displayed. * @param continent_guid identifies the zone (continent) - * @param empire identifies the empire; this value is matchable against PlanetSideEmpire + * @param empire identifies the empire */ final case class ContinentalLockUpdateMessage(continent_guid : PlanetSideGUID, - empire : PlanetSideEmpire.Value) // 00 for TR, 40 for NC, 80 for VS; C0 generates no message + empire : PlanetSideEmpire.Value) extends PlanetSideGamePacket { type Packet = ContinentalLockUpdateMessage def opcode = GamePacketOpcode.ContinentalLockUpdateMessage diff --git a/common/src/main/scala/net/psforever/packet/game/DestroyDisplayMessage.scala b/common/src/main/scala/net/psforever/packet/game/DestroyDisplayMessage.scala index 77dcc41e..5adfa168 100644 --- a/common/src/main/scala/net/psforever/packet/game/DestroyDisplayMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/DestroyDisplayMessage.scala @@ -2,6 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideEmpire import scodec.Codec import scodec.codecs._ @@ -24,8 +25,8 @@ import scodec.codecs._ * In the case of absentee kills, for example, where there is no killer listed, this field has been zero'd (`00000000`).
*
* The faction affiliation is different from the normal way `PlanetSideEmpire` values are recorded. - * The higher nibble will reflect the first part of the `PlanetSideEmpire` value - `0` for TR, `4` for NC `8` for TR, `C` for Neutral/BOPs. - * An extra `20` will be added if the player is in a vehicle or turret at the time - `2` for TR, `6` for NC, `A` for VS, `E` for Neutral/BOPs. + * The higher nibble will reflect the first part of the `PlanetSideEmpire` value. + * An extra `20` will be added if the player is in a vehicle or turret at the time. * When marked as being in a vehicle or turret, the player's name will be enclosed within square brackets. * The length of the player's name found at the start of the wide character string does not reflect whether or not there will be square brackets (fortunately).
*
@@ -36,15 +37,13 @@ import scodec.codecs._ * It is also unknown what the two bytes preceding `method` specify, as changing them does nothing to the displayed message. * @param killer the name of the player who did the killing * @param killer_unk See above - * @param killer_empire the empire affiliation of the killer: - * 0 - TR, 1 - NC, 2 - VS, 3 - Neutral/BOPs + * @param killer_empire the empire affiliation of the killer * @param killer_inVehicle true, if the killer was in a vehicle at the time of the kill; false, otherwise * @param unk na; but does not like being set to 0 * @param method modifies the icon in the message, related to the way the victim was killed * @param victim the name of the player who was killed * @param victim_unk See above - * @param victim_empire the empire affiliation of the victim: - * 0 - TR, 1 - NC, 2 - VS, 3 - Neutral/BOPs + * @param victim_empire the empire affiliation of the victim * @param victim_inVehicle true, if the victim was in a vehicle when he was killed; false, otherwise */ final case class DestroyDisplayMessage(killer : String, diff --git a/common/src/main/scala/net/psforever/packet/game/ProximityTerminalUseMessage.scala b/common/src/main/scala/net/psforever/packet/game/ProximityTerminalUseMessage.scala new file mode 100644 index 00000000..037e5ce3 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ProximityTerminalUseMessage.scala @@ -0,0 +1,35 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * The player's avatar has moved in relation to a set piece that reacts with the player due to his proximity.
+ *
+ * Elements that exhibit this behavior include Repair/Rearm Silos in facility courtyards and various cavern crystals. + * The packets are only dispatched when it is appropriate for the player to be affected.
+ *
+ * Exploration:
+ * Packets where the bytes for the player's GUID are blank exist. + * @param player_guid the player + * @param object_guid the object whose functionality is triggered + * @param unk na + */ +final case class ProximityTerminalUseMessage(player_guid : PlanetSideGUID, + object_guid : PlanetSideGUID, + unk : Boolean) + extends PlanetSideGamePacket { + type Packet = ProximityTerminalUseMessage + def opcode = GamePacketOpcode.ProximityTerminalUseMessage + def encode = ProximityTerminalUseMessage.encode(this) +} + +object ProximityTerminalUseMessage extends Marshallable[ProximityTerminalUseMessage] { + implicit val codec : Codec[ProximityTerminalUseMessage] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("object_guid" | PlanetSideGUID.codec) :: + ("unk" | bool) + ).as[ProximityTerminalUseMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala new file mode 100644 index 00000000..a36089ad --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala @@ -0,0 +1,660 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.newcodecs.newcodecs +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Maintains squad information changes performed by this listing. + * Only certain information will be transmitted depending on the purpose of the packet. + * @param leader the name of the squad leader as a wide character string, or `None` if not applicable + * @param task the task the squad is trying to perform as a wide character string, or `None` if not applicable + * @param zone_id the continent on which the squad is acting, or `None` if not applicable + * @param size the current size of the squad, or `None` if not applicable; + * "can" be greater than `capacity`, though with issues + * @param capacity the maximum number of members that the squad can tolerate, or `None` if not applicable; + * normal count is 10; + * maximum is 15 but naturally can not be assigned that many + * @param squad_guid a GUID associated with the squad, used to recover the squad definition, or `None` if not applicable; + * sometimes it is defined but is still not applicable + */ +final case class SquadInfo(leader : Option[String], + task : Option[String], + zone_id : Option[PlanetSideZoneID], + size : Option[Int], + capacity : Option[Int], + squad_guid : Option[PlanetSideGUID] = None) + +/** + * Define three fields determining the purpose of data in this listing.
+ *
+ * The third field `unk3` is not always be defined and will be supplanted by the squad (definition) GUID during initialization and a full update.
+ *
+ * Actions:
+ * `unk1 unk2  unk3`
+ * `0    true  4 -- `Remove a squad from listing
+ * `128  true  0 -- `Update a squad's leader
+ * `128  true  1 -- `Update a squad's task or continent
+ * `128  true  2 -- `Update a squad's size
+ * `129  false 0 -- `Update a squad's leader or size
+ * `129  false 1 -- `Update a squad's task and continent
+ * `131  false X -- `Add all squads during initialization / update all information pertaining to this squad + * @param unk1 na + * @param unk2 na + * @param unk3 na; + * not always defined + * @param info information pertaining to this squad listing + */ +//TODO when these unk# values are better understood, transform SquadHeader to streamline the actions to be performed +final case class SquadHeader(unk1 : Int, + unk2 : Boolean, + unk3 : Option[Int], + info : Option[SquadInfo] = None) + +/** + * An indexed entry in the listing of squads.
+ *
+ * Squad listing indices are not an arbitrary order. + * The server communicates changes to the client by referencing a squad's listing index, defined at the time of list initialization. + * Once initialized, each client may organize their squads however they wish, e.g., by leader, by task, etc., without compromising this index. + * During the list initialization process, the entries must always follow numerical order, increasing from `0`. + * During any other operation, the entries may be prefixed with whichever index is necessary to indicate the squad listing in question. + * @param index the index of this listing; + * first entry should be 0, and subsequent valid entries are sequentially incremental; + * last entry is always a placeholder with index 255 + * @param listing the data for this entry, defining both the actions and the pertinent squad information + */ +final case class SquadListing(index : Int = 255, + listing : Option[SquadHeader] = None) + +/** + * Modify the list of squads available to a given player. + * The squad list updates in real time rather than just whenever a player opens the squad information window.
+ *
+ * The four main operations are: initializing the list, updating entries in the list, removing entries from the list, and clearing the list. + * The process of initializing the list and clearing the list actually are performed by similar behavior. + * Squads would just not be added after the list clears. + * Moreover, removing entries from the list overrides the behavior to update entries in the list. + * The two-three codes per entry (see `SquadHeader`) are important for determining the effect of a specific entry. + * As of the moment, the important details of the behaviors is that they modify how the packet is encoded and padded.
+ *
+ * Referring to information in `SquadListing`, entries are identified by their index in the list. + * This is followed by a coded section that indicates what action the entry will execute on that squad listing. + * After the "coded action" section is the "general information" section where the data for the change is specified. + * In this manner, all the entries will have a knowable length.
+ *
+ * The total number of entries in a packet is not known until they have all been parsed. + * During the list initialization process, the entries must be in ascending order of index. + * Otherwise, the specific index of the squad listing is referenced. + * The minimum number of entries is "no entries." + * The maximum number of entries is supposedly 254. + * The last item is always the index 255 and this is interpreted as the end of the stream.
+ *
+ * When no updates are provided, the client loads a default (but invalid) selection of data comprising four squads:
+ * `0  Holeesh      another purpose  Desolation  6/7`
+ * `1  Korealis     another purpose  Drugaskan   10/10`
+ * `2  PsychoSanta  blah blah blah               10/10`
+ * `3  Squishling   another purpose  Cyssor      8/10`
+ * The last entry is entirely in green text.
+ *
+ * Behaviors:
+ * `behavior behavior2`
+ * `1        X         `Update where initial entry removes a squad from the list
+ * `5        6         `Clear squad list and initialize new squad list
+ * `5        6         `Clear squad list (ransitions directly into 255-entry)
+ * `6        X         `Update a squad in the list + * @param behavior a code that suggests the primary purpose of the data in this packet + * @param behavior2 during initialization, this code is read; + * it supplements the normal `behavior` and is typically is an "update" code + * @param entries a `Vector` of the squad listings + */ +final case class ReplicationStreamMessage(behavior : Int, + behavior2 : Option[Int], + entries : Vector[SquadListing]) + extends PlanetSideGamePacket { + type Packet = ReplicationStreamMessage + def opcode = GamePacketOpcode.ReplicationStreamMessage + def encode = ReplicationStreamMessage.encode(this) +} + +object SquadInfo { + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.
+ *
+ * This constructor is not actually used at the moment. + * @param leader the name of the squad leader + * @param task the task the squad is trying to perform + * @param continent_guid the continent on which the squad is acting + * @param size the current size of the squad + * @param capacity the maximum number of members that the squad can tolerate + * @return a `SquadInfo` object + */ + def apply(leader : String, task : String, continent_guid : PlanetSideZoneID, size : Int, capacity : Int) : SquadInfo = { + SquadInfo(Some(leader), Some(task), Some(continent_guid), Some(size), Some(capacity)) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.
+ *
+ * This constructor is used by the `initCodec`, `alt_initCodec`, and `allCodec`. + * @param leader the name of the squad leader + * @param task the task the squad is trying to perform + * @param continent_guid the continent on which the squad is acting + * @param size the current size of the squad + * @param capacity the maximum number of members that the squad can tolerate + * @param squad_guid a GUID associated with the squad, used to recover the squad definition + * @return a `SquadInfo` object + */ + def apply(leader : String, task : String, continent_guid : PlanetSideZoneID, size : Int, capacity : Int, squad_guid : PlanetSideGUID) : SquadInfo = { + SquadInfo(Some(leader), Some(task), Some(continent_guid), Some(size), Some(capacity), Some(squad_guid)) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
+ *
+ * This constructor is used by `leaderCodec`. + * Two of the fields normally are `Option[String]`s. + * Only the `leader` field in this packet is a `String`, giving the method a distinct signature. + * The other field - an `Option[String]` for `task` - can still be set if passed.
+ *
+ * Recommended use: `SquadInfo(leader, None)` + * @param leader the name of the squad leader + * @param task the task the squad is trying to perform, if not `None` + * @return a `SquadInfo` object + */ + def apply(leader : String, task : Option[String]) : SquadInfo = { + SquadInfo(Some(leader), task, None, None, None) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
+ *
+ * This constructor is used by `taskOrContinentCodec`. + * Two of the fields normally are `Option[String]`s. + * Only the `task` field in this packet is a `String`, giving the method a distinct signature. + * The other field - an `Option[String]` for `leader` - can still be set if passed.
+ *
+ * Recommended use: `SquadInfo(None, task)` + * @param leader the name of the squad leader, if not `None` + * @param task the task the squad is trying to perform + * @return a `SquadInfo` object + */ + def apply(leader : Option[String], task : String) : SquadInfo = { + SquadInfo(leader, Some(task), None, None, None) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the field.
+ *
+ * This constructor is used by `taskOrContinentCodec`. + * @param continent_guid the continent on which the squad is acting + * @return a `SquadInfo` object + */ + def apply(continent_guid : PlanetSideZoneID) : SquadInfo = { + SquadInfo(None, None, Some(continent_guid), None, None) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
+ *
+ * This constructor is used by `sizeCodec`. + * Two of the fields normally are `Option[Int]`s. + * Only the `size` field in this packet is an `Int`, giving the method a distinct signature.
+ *
+ * Recommended use: `SquadInfo(size, None)`
+ *
+ * Exploration:
+ * We do not currently know any `SquadHeader` action codes for adjusting `capacity`. + * @param size the current size of the squad + * @param capacity the maximum number of members that the squad can tolerate, if not `None` + * @return a `SquadInfo` object + */ + def apply(size : Int, capacity : Option[Int]) : SquadInfo = { + SquadInfo(None, None, None, Some(size), None) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
+ *
+ * This constructor is not actually used at the moment. + * Two of the fields normally are `Option[Int]`s. + * Only the `capacity` field in this packet is an `Int`, giving the method a distinct signature.
+ *
+ * Recommended use: `SquadInfo(None, capacity)`
+ *
+ * Exploration:
+ * We do not currently know any `SquadHeader` action codes for adjusting `capacity`. + * @param size the current size of the squad + * @param capacity the maximum number of members that the squad can tolerate, if not `None` + * @return a `SquadInfo` object + */ + def apply(size : Option[Int], capacity : Int) : SquadInfo = { + SquadInfo(None, None, None, None, Some(capacity)) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.
+ *
+ * This constructor is used by `leaderSizeCodec`. + * @param leader the name of the squad leader + * @param size the current size of the squad + * @return a `SquadInfo` object + */ + def apply(leader : String, size : Int) : SquadInfo = { + SquadInfo(Some(leader), None, None, Some(size), None) + } + + /** + * Alternate constructor for `SquadInfo` that ignores the Option requirement for the fields.
+ *
+ * This constructor is used by `taskAndContinentCodec`. + * @param task the task the squad is trying to perform + * @param continent_guid the continent on which the squad is acting + * @return a `SquadInfo` object + */ + def apply(task : String, continent_guid : PlanetSideZoneID) : SquadInfo = { + SquadInfo(None, Some(task), Some(continent_guid), None, None, None) + } +} + +object SquadHeader { + /** + * Alternate constructor for SquadInfo that ignores the Option requirement for the `info` field. + * @param unk1 na + * @param unk2 na + * @param unk3 na; not always defined + * @param info information pertaining to this squad listing + */ + def apply(unk1 : Int, unk2 : Boolean, unk3 : Option[Int], info : SquadInfo) : SquadHeader = { + SquadHeader(unk1, unk2, unk3, Some(info)) + } + + /** + * `squadPattern` completes the fields for the `SquadHeader` class. + * It translates an indeterminate number of bit regions into something that can be processed as an `Option[SquadInfo]`. + */ + private type squadPattern = Option[SquadInfo] :: HNil + + /** + * `Codec` for reading `SquadInfo` data from the first entry from a packet with squad list initialization entries. + */ + private val initCodec : Codec[squadPattern] = ( + ("squad_guid" | PlanetSideGUID.codec) :: + ("leader" | PacketHelpers.encodedWideString) :: + ("task" | PacketHelpers.encodedWideString) :: + ("continent_guid" | PlanetSideZoneID.codec) :: + ("size" | uint4L) :: + ("capacity" | uint4L) + ).exmap[squadPattern] ( + { + case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil => + Attempt.successful(Some(SquadInfo(lead, tsk, cguid, sz, cap, sguid)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for adding [A] a squad entry")) + }, + { + case Some(SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid))) :: HNil => + Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for adding [A] a squad entry")) + } + ) + + /** + * `Codec` for reading `SquadInfo` data from all entries other than the first from a packet with squad list initialization entries. + */ + private val alt_initCodec : Codec[squadPattern] = ( + ("squad_guid" | PlanetSideGUID.codec) :: + ("leader" | PacketHelpers.encodedWideStringAligned(7)) :: + ("task" | PacketHelpers.encodedWideString) :: + ("continent_guid" | PlanetSideZoneID.codec) :: + ("size" | uint4L) :: + ("capacity" | uint4L) + ).exmap[squadPattern] ( + { + case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil => + Attempt.successful(Some(SquadInfo(lead, tsk, cguid, sz, cap, sguid)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for adding [B] a squad entry")) + }, + { + case Some(SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid))) :: HNil => + Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for adding [B] a squad entry")) + } + ) + + /** + * `Codec` for reading the `SquadInfo` data in an "update all squad data" entry. + */ + private val allCodec : Codec[squadPattern] = ( + ("squad_guid" | PlanetSideGUID.codec) :: + ("leader" | PacketHelpers.encodedWideStringAligned(3)) :: + ("task" | PacketHelpers.encodedWideString) :: + ("continent_guid" | PlanetSideZoneID.codec) :: + ("size" | uint4L) :: + ("capacity" | uint4L) + ).exmap[squadPattern] ( + { + case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil => + Attempt.successful(Some(SquadInfo(lead, tsk, cguid, sz, cap, sguid)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for updating a squad entry")) + }, + { + case Some(SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid))) :: HNil => + Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for updating a squad entry")) + } + ) + + /** + * `Codec` for reading the `SquadInfo` data in an "update squad leader" entry. + */ + private val leaderCodec : Codec[squadPattern] = ( + bool :: + ("leader" | PacketHelpers.encodedWideStringAligned(7)) + ).exmap[squadPattern] ( + { + case true :: lead :: HNil => + Attempt.successful(Some(SquadInfo(lead, None)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for a leader name")) + }, + { + case Some(SquadInfo(Some(lead), _, _, _, _, _)) :: HNil => + Attempt.successful(true :: lead :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for a leader name")) + } + ) + + /** + * `Codec` for reading the `SquadInfo` data in an "update squad task or continent" entry. + */ + private val taskOrContinentCodec : Codec[squadPattern] = ( + bool >>:~ { path => + conditional(path, "continent_guid" | PlanetSideZoneID.codec) :: + conditional(!path, "task" | PacketHelpers.encodedWideStringAligned(7)) + } + ).exmap[squadPattern] ( + { + case true :: Some(cguid) :: _ :: HNil => + Attempt.successful(Some(SquadInfo(cguid)) :: HNil) + case true :: None :: _ :: HNil => + Attempt.failure(Err("failed to decode squad data for a task - no continent")) + case false :: _ :: Some(tsk) :: HNil => + Attempt.successful(Some(SquadInfo(None, tsk)) :: HNil) + case false :: _ :: None :: HNil => + Attempt.failure(Err("failed to decode squad data for a task - no task")) + }, + { + case Some(SquadInfo(_, None, Some(cguid), _, _, _)) :: HNil => + Attempt.successful(true :: Some(cguid) :: None :: HNil) + case Some(SquadInfo(_, Some(tsk), None, _, _, _)) :: HNil => + Attempt.successful(false :: None :: Some(tsk) :: HNil) + case Some(SquadInfo(_, Some(_), Some(_), _, _, _)) :: HNil => + Attempt.failure(Err("failed to encode squad data for either a task or a continent - multiple encodings reachable")) + case _ => + Attempt.failure(Err("failed to encode squad data for either a task or a continent")) + } + ) + + /** + * `Codec` for reading the `SquadInfo` data in an "update squad size" entry. + */ + private val sizeCodec : Codec[squadPattern] = ( + bool :: + ("size" | uint4L) + ).exmap[squadPattern] ( + { + case false :: sz :: HNil => + Attempt.successful(Some(SquadInfo(sz, None)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for a size")) + }, + { + case Some(SquadInfo(_, _, _, Some(sz), _, _)) :: HNil => + Attempt.successful(false :: sz :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for a size")) + } + ) + + /** + * `Codec` for reading the `SquadInfo` data in an "update squad leader and size" entry. + */ + private val leaderSizeCodec : Codec[squadPattern] = ( + bool :: + ("leader" | PacketHelpers.encodedWideStringAligned(7)) :: + uint4L :: + ("size" | uint4L) + ).exmap[squadPattern] ( + { + case true :: lead :: 4 :: sz :: HNil => + Attempt.successful(Some(SquadInfo(lead, sz)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for a leader and a size")) + }, + { + case Some(SquadInfo(Some(lead), _, _, Some(sz), _, _)) :: HNil => + Attempt.successful(true :: lead :: 4 :: sz :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for a leader and a size")) + } + ) + + /** + * `Codec` for reading the `SquadInfo` data in an "update squad task and continent" entry. + */ + private val taskAndContinentCodec : Codec[squadPattern] = ( + bool :: + ("task" | PacketHelpers.encodedWideStringAligned(7)) :: + uintL(3) :: + bool :: + ("continent_guid" | PlanetSideZoneID.codec) + ).exmap[squadPattern] ( + { + case false :: tsk :: 1 :: true :: cguid :: HNil => + Attempt.successful(Some(SquadInfo(tsk, cguid)) :: HNil) + case _ => + Attempt.failure(Err("failed to decode squad data for a task and a continent")) + }, + { + case Some(SquadInfo(_, Some(tsk), Some(cguid), _, _, _)) :: HNil => + Attempt.successful(false :: tsk :: 1 :: true :: cguid :: HNil) + case _ => + Attempt.failure(Err("failed to encode squad data for a task and a continent")) + } + ) + + /** + * Codec for reading the `SquadInfo` data in a "remove squad from list" entry. + * This `Codec` is unique because it is considered a valid `Codec` that does not read any bit data. + * The `conditional` will always return `None` because its determining conditional statement is explicitly `false`. + */ + private val removeCodec : Codec[squadPattern] = conditional(false, bool).exmap[squadPattern] ( + { + case None | _ => + Attempt.successful(None :: HNil) + }, + { + case None :: HNil | _ => + Attempt.successful(None) + } + ) + + /** + * `Codec` for failing to determine a valid `Codec` based on the entry data. + * This `Codec` is an invalid codec that does not read any bit data. + * The `conditional` will always return `None` because its determining conditional statement is explicitly `false`. + */ + private val failureCodec : Codec[squadPattern] = conditional(false, bool).exmap[squadPattern] ( + { + case None | _ => + Attempt.failure(Err("decoding with unhandled codec")) + }, + { + case None :: HNil | _ => + Attempt.failure(Err("encoding with unhandled codec")) + } + ) + + /** + * Select the `Codec` to translate bit data in this packet into an `Option[SquadInfo]` using other fields of the same packet. + * Refer to comments for the primary `case class` constructor for `SquadHeader` to explain how the conditions in this function path. + * @param a na + * @param b na + * @param c na; may be `None` + * @param optionalCodec a to-be-defined `Codec` that is determined by the suggested mood of the packet and listing of the squad; + * despite the name, actually a required parameter + * @return a `Codec` that corresponds to a `squadPattern` translation + */ + private def selectCodec(a : Int, b : Boolean, c : Option[Int], optionalCodec : Codec[squadPattern]) : Codec[squadPattern] = { + if(c.isDefined) { + val cVal = c.get + if(a == 0 && b) + if(cVal == 4) + return removeCodec + if(a == 128 && b) { + if(cVal == 0) + return leaderCodec + else if(cVal == 1) + return taskOrContinentCodec + else if(cVal == 2) + return sizeCodec + } + else if(a == 129 && !b) { + if(cVal == 0) + return leaderSizeCodec + else if(cVal == 1) + return taskAndContinentCodec + } + } + else { + if(a == 131 && !b) + return optionalCodec + } + //we've not encountered a valid Codec + failureCodec + } + + /** + * `Codec` for standard `SquadHeader` entries. + */ + val codec : Codec[SquadHeader] = ( + ("unk1" | uint8L) >>:~ { unk1 => + ("unk2" | bool) >>:~ { unk2 => + conditional(unk1 != 131, "unk3" | uintL(3)) >>:~ { unk3 => + selectCodec(unk1, unk2, unk3, allCodec) + } + } + }).as[SquadHeader] + + /** + * `Codec` for types of `SquadHeader` initializations. + */ + val init_codec : Codec[SquadHeader] = ( + ("unk1" | uint8L) >>:~ { unk1 => + ("unk2" | bool) >>:~ { unk2 => + conditional(unk1 != 131, "unk3" | uintL(3)) >>:~ { unk3 => + selectCodec(unk1, unk2, unk3, initCodec) + } + } + }).as[SquadHeader] + + /** + * Alternate `Codec` for types of `SquadHeader` initializations. + */ + val alt_init_codec : Codec[SquadHeader] = ( + ("unk1" | uint8L) >>:~ { unk1 => + ("unk2" | bool) >>:~ { unk2 => + conditional(unk1 != 131, "unk3" | uintL(3)) >>:~ { unk3 => + selectCodec(unk1, unk2, unk3, alt_initCodec) + } + } + }).as[SquadHeader] +} + +object SquadListing { + /** + * `Codec` for standard `SquadListing` entries. + */ + val codec : Codec[SquadListing] = ( + ("index" | uint8L) >>:~ { index => + conditional(index < 255, "listing" | SquadHeader.codec) :: + conditional(index == 255, bits) //consume n < 8 bits padding the tail entry, else vector will try to operate on invalid data + }).xmap[SquadListing] ( + { + case ndx :: lstng :: _ :: HNil => + SquadListing(ndx, lstng) + }, + { + case SquadListing(ndx, lstng) => + ndx :: lstng :: None :: HNil + } + ) + + /** + * `Codec` for branching types of `SquadListing` initializations. + */ + val init_codec : Codec[SquadListing] = ( + ("index" | uint8L) >>:~ { index => + conditional(index < 255, + newcodecs.binary_choice(index == 0, + "listing" | SquadHeader.init_codec, + "listing" | SquadHeader.alt_init_codec) + ) :: + conditional(index == 255, bits) //consume n < 8 bits padding the tail entry, else vector will try to operate on invalid data + }).xmap[SquadListing] ( + { + case ndx :: lstng :: _ :: HNil => + SquadListing(ndx, lstng) + }, + { + case SquadListing(ndx, lstng) => + ndx :: lstng :: None :: HNil + } + ) +} + +object ReplicationStreamMessage extends Marshallable[ReplicationStreamMessage] { + implicit val codec : Codec[ReplicationStreamMessage] = ( + ("behavior" | uintL(3)) >>:~ { behavior => + conditional(behavior == 5, "behavior2" | uintL(3)) :: + conditional(behavior != 1, bool) :: + newcodecs.binary_choice(behavior != 5, + "entries" | vector(SquadListing.codec), + "entries" | vector(SquadListing.init_codec) + ) + }).xmap[ReplicationStreamMessage] ( + { + case bhvr :: bhvr2 :: _ :: lst :: HNil => + ReplicationStreamMessage(bhvr, bhvr2, lst) + }, + { + case ReplicationStreamMessage(1, bhvr2, lst) => + 1 :: bhvr2 :: None :: lst :: HNil + case ReplicationStreamMessage(bhvr, bhvr2, lst) => + bhvr :: bhvr2 :: Some(false) :: lst :: HNil + } + ) +} + +/* + +-> SquadListing.codec -------> SquadHeader.codec ----------+ + | | + | | +ReplicationStream.codec -+ | + | | + | +-> SquadHeader.init_codec -----+-> SquadInfo + | | | + +-> SquadListing.initCodec -+ | + | | + +-> SquadHeader.alt_init_codec -+ +*/ diff --git a/common/src/main/scala/net/psforever/packet/game/SetEmpireMessage.scala b/common/src/main/scala/net/psforever/packet/game/SetEmpireMessage.scala index 0dcc7428..bb05ef05 100644 --- a/common/src/main/scala/net/psforever/packet/game/SetEmpireMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SetEmpireMessage.scala @@ -2,6 +2,7 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideEmpire import scodec.Codec import scodec.codecs._ diff --git a/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala b/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala index 32d5fc60..b3933422 100644 --- a/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/VNLWorldStatusMessage.scala @@ -4,6 +4,7 @@ package net.psforever.packet.game import java.net.{InetAddress, InetSocketAddress} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.PlanetSideEmpire import scodec._ import scodec.bits._ import scodec.codecs._ @@ -22,13 +23,6 @@ object ServerType extends Enumeration(1) { implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L) } -object PlanetSideEmpire extends Enumeration { - type Type = Value - val TR, NC, VS, NEUTRAL = Value - - implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) -} - final case class WorldConnectionInfo(address : InetSocketAddress) final case class WorldInformation(name : String, status : WorldStatus.Value, diff --git a/common/src/main/scala/net/psforever/packet/game/ZipLineMessage.scala b/common/src/main/scala/net/psforever/packet/game/ZipLineMessage.scala new file mode 100644 index 00000000..63cdce93 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ZipLineMessage.scala @@ -0,0 +1,74 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.newcodecs.newcodecs +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Dispatched by the client when the player is interacting with a zip line. + * Dispatched by the server to instruct the client to use the zip line. + * Cavern teleportation rings also count as "zip lines" as far as the game is concerned, in that they use this packet.
+ *
+ * Action:
+ * `0 - Attach to a node`
+ * `1 - Arrived at destination`
+ * `2 - Forcibly detach from zip line in mid-transit` + * @param player_guid the player + * @param origin_side whether this corresponds with the "entry" or the "exit" of the zip line, as per the direction of the light pulse visuals + * @param action how the player interacts with the zip line + * @param guid a number that is consistent to a terminus + * @param x the x-coordinate of the point where the player is interacting with the zip line + * @param y the y-coordinate of the point where the player is interacting with the zip line + * @param z the z-coordinate of the point where the player is interacting with the zip line + */ +final case class ZipLineMessage(player_guid : PlanetSideGUID, + origin_side : Boolean, + action : Int, + guid : Long, + x : Float, + y : Float, + z : Float) + extends PlanetSideGamePacket { + type Packet = ZipLineMessage + def opcode = GamePacketOpcode.ZipLineMessage + def encode = ZipLineMessage.encode(this) +} + +object ZipLineMessage extends Marshallable[ZipLineMessage] { + type threeFloatsPattern = Float :: Float :: Float :: HNil + + /** + * A `Codec` for when three `Float` values are to be read or written. + */ + val threeFloatValues : Codec[threeFloatsPattern] = ( + ("x" | floatL) :: + ("y" | floatL) :: + ("z" | floatL) + ).as[threeFloatsPattern] + + /** + * A `Codec` for when there are no extra `Float` values present. + */ + val noFloatValues : Codec[threeFloatsPattern] = ignore(0).xmap[threeFloatsPattern] ( + { + case () => + 0f :: 0f :: 0f :: HNil + }, + { + case _ => + () + } + ) + + implicit val codec : Codec[ZipLineMessage] = ( + ("player_guid" | PlanetSideGUID.codec) >>:~ { player => + ("origin_side" | bool) :: + ("action" | uint2) :: + ("id" | uint32L) :: + newcodecs.binary_choice(player.guid > 0, threeFloatValues, noFloatValues) // !(player.guid == 0) + } + ).as[ZipLineMessage] +} 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 b614fc70..7b252414 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 @@ -2,7 +2,7 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.{Marshallable, PacketHelpers} -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideEmpire, Vector3} import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} @@ -18,11 +18,6 @@ import shapeless.{::, HNil} * This base length of this stream is __430__ known bits, excluding the length of the name and the padding on that name. * Of that, __203__ bits are perfectly unknown in significance. *
- * Faction:
- * `0 - Terran Republic`
- * `1 - New Conglomerate`
- * `2 - Vanu Sovereignty`
- *
* Exo-suit:
* `0 - Agile`
* `1 - Refinforced`
@@ -91,7 +86,7 @@ import shapeless.{::, HNil} */ final case class CharacterAppearanceData(pos : Vector3, objYaw : Int, - faction : Int, + faction : PlanetSideEmpire.Value, bops : Boolean, unk1 : Int, name : String, @@ -138,7 +133,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ignore(16) :: ("objYaw" | uint8L) :: ignore(1) :: - ("faction" | uint2L) :: + ("faction" | PlanetSideEmpire.codec) :: ("bops" | bool) :: ("unk1" | uint4L) :: ignore(16) :: @@ -164,7 +159,23 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ("unk8" | uint4L) :: ignore(6) :: ("ribbons" | RibbonBars.codec) - ).as[CharacterAppearanceData] + ).exmap[CharacterAppearanceData] ( + { + case a :: _ :: b :: _ :: c :: d :: e :: _ :: f :: g :: _ :: h :: i :: j :: k :: l :: _ :: m :: n :: o :: _ :: p :: _ :: q :: _ :: r :: s :: t :: _ :: u :: HNil => + Attempt.successful( + CharacterAppearanceData(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) + ) + }, + { + case CharacterAppearanceData(_, _, PlanetSideEmpire.NEUTRAL, _, _, name, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => + Attempt.failure(Err(s"character $name's faction can not declare as neutral")) + + case CharacterAppearanceData(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) => + Attempt.successful( + a :: () :: b :: () :: c :: d :: e :: () :: f :: g :: () :: h :: i :: j :: k :: l :: () :: m :: n :: o :: () :: p :: () :: q :: () :: r :: s :: t :: () :: u :: HNil + ) + } + ) } /** 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 7e44ff7a..2daf870d 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 @@ -421,7 +421,6 @@ object ObjectClass { case ObjectClass.battlewagon_weapon_systemb => WeaponData.genericCodec case ObjectClass.battlewagon_weapon_systemc => WeaponData.genericCodec case ObjectClass.battlewagon_weapon_systemd => WeaponData.genericCodec - case ObjectClass.beamer => WeaponData.genericCodec case ObjectClass.bolt_driver => WeaponData.genericCodec case ObjectClass.chainblade => WeaponData.genericCodec case ObjectClass.chaingun_p => WeaponData.genericCodec diff --git a/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala b/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala new file mode 100644 index 00000000..9f4481c1 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala @@ -0,0 +1,15 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.types + +import net.psforever.packet.PacketHelpers +import scodec.codecs.uint2L + +/** + * Values for the three empires and the neutral/Black Ops group. + */ +object PlanetSideEmpire extends Enumeration { + type Type = Value + val TR, NC, VS, NEUTRAL = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L) +} diff --git a/common/src/test/scala/CryptoInterfaceTest.scala b/common/src/test/scala/CryptoInterfaceTest.scala index 7b75ea05..2bca544a 100644 --- a/common/src/test/scala/CryptoInterfaceTest.scala +++ b/common/src/test/scala/CryptoInterfaceTest.scala @@ -109,14 +109,12 @@ class CryptoInterfaceTest extends Specification { args(stopOnFail = true) "safely handle multiple starts" in { val dontCare = ByteVector.fill(16)(0x42) - var dh = new CryptoDHState() + val dh = new CryptoDHState() dh.start() dh.start() must throwA[IllegalStateException] dh.close - dh = new CryptoDHState() - ok } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 6eea405d..53221634 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -374,7 +374,7 @@ class GamePacketTest extends Specification { char.appearance.pos.y mustEqual 2726.789f char.appearance.pos.z mustEqual 91.15625f char.appearance.objYaw mustEqual 19 - char.appearance.faction mustEqual 2 //vs + char.appearance.faction mustEqual PlanetSideEmpire.VS char.appearance.bops mustEqual false char.appearance.unk1 mustEqual 4 char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI" @@ -529,7 +529,7 @@ class GamePacketTest extends Specification { val app = CharacterAppearanceData( Vector3(3674.8438f, 2726.789f, 91.15625f), 19, - 2, + PlanetSideEmpire.VS, false, 4, "IlllIIIlllIlIllIlllIllI", @@ -1139,6 +1139,32 @@ class GamePacketTest extends Specification { } } + "ZipLineMessage" should { + val string = hex"BF 4B00 19 80000010 5bb4089c 52116881 cf76e840" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ZipLineMessage(player_guid, origin_side, action, uid, x, y, z) => + player_guid mustEqual PlanetSideGUID(75) + origin_side mustEqual false + action mustEqual 0 + uid mustEqual 204 + x mustEqual 1286.9221f + y mustEqual 1116.5276f + z mustEqual 91.74034f + case _ => + ko + } + } + + "encode" in { + val msg = ZipLineMessage(PlanetSideGUID(75), false, 0, 204, 1286.9221f, 1116.5276f, 91.74034f) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } + } + "PlayerStateShiftMessage" should { val string_short = hex"BE 68" val string_pos = hex"BE 95 A0 89 13 91 B8 B0 BF F0" @@ -1212,6 +1238,25 @@ class GamePacketTest extends Specification { } } + "ProximityTerminalUseMessage" should { + val string = hex"C3 4B00 A700 80" + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ProximityTerminalUseMessage(player_guid, object_guid, unk) => + player_guid mustEqual PlanetSideGUID(75) + object_guid mustEqual PlanetSideGUID(167) + unk mustEqual true + case _ => + ko + } + } + "encode" in { + val msg = ProximityTerminalUseMessage(PlanetSideGUID(75), PlanetSideGUID(167), true) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + pkt mustEqual string + } + } + "UseItemMessage" should { val string = hex"10 4B00 0000 7401 FFFFFFFF 4001000000000000000000000000058C803600800000" @@ -1346,7 +1391,7 @@ class GamePacketTest extends Specification { } "encode" in { - val msg = DestroyDisplayMessage("Angello", 30981173,PlanetSideEmpire.VS, false, 121, 969, "HMFIC", 31035057, PlanetSideEmpire.TR, false) + val msg = DestroyDisplayMessage("Angello", 30981173, PlanetSideEmpire.VS, false, 121, 969, "HMFIC", 31035057, PlanetSideEmpire.TR, false) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string } @@ -1492,6 +1537,654 @@ class GamePacketTest extends Specification { } } + "ReplicationStreamMessage" should { + val stringListClear = hex"E6 B9 FE" + val stringListOne = hex"E6 B8 01 06 01 00 8B 46007200610067004C0041004E00640049004E004300 84 4600720061006700 0A00 00 00 0A FF" + val stringListTwo = hex"E6 B8 01 06 06 00 8E 470065006E006500720061006C0047006F0072006700750074007A00 A1 46004C0059002C0041006C006C002000770065006C0063006F006D0065002C0063006E0020006C0061007300740020006E0069006700680074002100210021002100 0400 00 00 7A 01 83 02 00 45 80 4B004F004B006B006900610073004D00460043004E00 87 5300710075006100640020003200 0400 00 00 6A FF" + val stringListThree = hex"E6 B8 01 06 06 00 8E 470065006E006500720061006C0047006F0072006700750074007A00 A1 46004C0059002C0041006C006C002000770065006C0063006F006D0065002C0063006E0020006C0061007300740020006E0069006700680074002100210021002100 0400 00 00 7A 01 83 01 80 4600 4E0049004700480054003800380052004100560045004E00 8B 41006C006C002000570065006C0063006F006D006500 0A 00 00 00 4A 02 83 02 00 45 80 4B004F004B006B006900610073004D00460043004E00 87 5300710075006100640020003200 0400 00 00 6A FF" + val stringListRemove = hex"E6 20 A0 19 FE" + val stringUpdateLeader = hex"E6 C0 28 08 C4 00 46006100740065004A0048004E004300 FF" + val stringUpdateTask = hex"E6 C0 58 094E00 52004900500020005000530031002C0020007600690073006900740020005000530046006F00720065007600650072002E006E0065007400 FF" + val stringUpdateContinent = hex"E6 C0 38 09 85000000 7F80" + val stringUpdateSize = hex"E6 C0 18 0A 37 F8" + val stringUpdateLeaderSize = hex"E6 C0 58 10 C3 00 4A0069006D006D0079006E00 43 FF" + val stringUpdateTaskContinent = hex"E6 C0 58 11 40 80 3200 3 04000000 FF0" + val stringUpdateAll = hex"E6 C0 78 30 58 0430 6D00610064006D0075006A00 80 040000000A FF" + //failing conditions + val stringCodecFail = hex"E6 20 A1 19 FE" + val stringListOneFail = hex"E6 B8 01 06 01 00 8B 46007200610067004C0041004E00640049004E004300 84 4600720061006700 0A00 00 01 0A FF" + val stringListTwoFail = hex"E6 B8 01 06 06 00 8E 470065006E006500720061006C0047006F0072006700750074007A00 A1 46004C0059002C0041006C006C002000770065006C0063006F006D0065002C0063006E0020006C0061007300740020006E0069006700680074002100210021002100 0400 00 00 7A 01 83 02 00 45 80 4B004F004B006B006900610073004D00460043004E00 87 5300710075006100640020003200 0400 00 01 6A FF" + val stringUpdateLeaderFail = hex"E6 C0 28 08 44 00 46006100740065004A0048004E004300 FF" + val stringUpdateTaskFail = hex"E6 C0 58 09CE00 52004900500020005000530031002C0020007600690073006900740020005000530046006F00720065007600650072002E006E0065007400 FF" + val stringUpdateContinentFail = hex"E6 C0 38 09 85000001 7F80" + val stringUpdateSizeFail = hex"E6 C0 18 0A B7 F8" + val stringUpdateLeaderSizeFail = hex"E6 C0 58 10 43 00 4A0069006D006D0079006E00 43 FF" + val stringUpdateTaskContinentFail = hex"E6 C0 58 11 C0 80 3200 3 04000000 FF0" + val stringUpdateAllFail = hex"E6 C0 78 30 58 0430 6D00610064006D0075006A00 80 04000001 0A FF" + + "SquadInfo (w/ squad_guid)" in { + val o = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10) + o.leader.isDefined mustEqual true + o.leader.get mustEqual "FragLANdINC" + o.task.isDefined mustEqual true + o.task.get mustEqual "Frag" + o.zone_id.isDefined mustEqual true + o.zone_id.get mustEqual PlanetSideZoneID(10) + o.size.isDefined mustEqual true + o.size.get mustEqual 0 + o.capacity.isDefined mustEqual true + o.capacity.get mustEqual 10 + } + + "SquadInfo (capacity)" in { + val o = SquadInfo(None, 7) + o.leader.isDefined mustEqual false + o.task.isDefined mustEqual false + o.zone_id.isDefined mustEqual false + o.size.isDefined mustEqual false + o.capacity.isDefined mustEqual true + o.capacity.get mustEqual 7 + } + + "decode (clear)" in { + PacketCoding.DecodePacket(stringListClear).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 5 + behavior2.isDefined mustEqual true + behavior2.get mustEqual 6 + entries.length mustEqual 1 + entries.head.index mustEqual 255 + entries.head.listing.isDefined mustEqual false + case _ => + ko + } + } + + "decode (one)" in { + PacketCoding.DecodePacket(stringListOne).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 5 + behavior2.get mustEqual 6 + entries.length mustEqual 2 + entries.head.index mustEqual 0 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 131 + entries.head.listing.get.unk2 mustEqual false + entries.head.listing.get.unk3.isDefined mustEqual false + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual true + entries.head.listing.get.info.get.leader.get mustEqual "FragLANdINC" + entries.head.listing.get.info.get.task.isDefined mustEqual true + entries.head.listing.get.info.get.task.get mustEqual "Frag" + entries.head.listing.get.info.get.zone_id.isDefined mustEqual true + entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10) + entries.head.listing.get.info.get.size.isDefined mustEqual true + entries.head.listing.get.info.get.size.get mustEqual 0 + entries.head.listing.get.info.get.capacity.isDefined mustEqual true + entries.head.listing.get.info.get.capacity.get mustEqual 10 + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual true + entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(1) + entries(1).index mustEqual 255 + entries(1).listing.isDefined mustEqual false + case _ => + ko + } + } + + "decode (two)" in { + PacketCoding.DecodePacket(stringListTwo).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 5 + behavior2.get mustEqual 6 + entries.length mustEqual 3 + entries.head.index mustEqual 0 + entries.head.listing.get.unk1 mustEqual 131 + entries.head.listing.get.unk2 mustEqual false + entries.head.listing.get.unk3.isDefined mustEqual false + entries.head.listing.get.info.get.leader.get mustEqual "GeneralGorgutz" + entries.head.listing.get.info.get.task.get mustEqual "FLY,All welcome,cn last night!!!!" + entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4) + entries.head.listing.get.info.get.size.get mustEqual 7 + entries.head.listing.get.info.get.capacity.get mustEqual 10 + entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(6) + entries(1).index mustEqual 1 + entries(1).listing.get.unk1 mustEqual 131 + entries(1).listing.get.unk2 mustEqual false + entries(1).listing.get.unk3.isDefined mustEqual false + entries(1).listing.get.info.get.leader.get mustEqual "KOKkiasMFCN" + entries(1).listing.get.info.get.task.get mustEqual "Squad 2" + entries(1).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4) + entries(1).listing.get.info.get.size.get mustEqual 6 + entries(1).listing.get.info.get.capacity.get mustEqual 10 + entries(1).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(4) + entries(2).index mustEqual 255 + case _ => + ko + } + } + + "decode (three)" in { + PacketCoding.DecodePacket(stringListThree).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 5 + behavior2.get mustEqual 6 + entries.length mustEqual 4 + entries.head.index mustEqual 0 + entries.head.listing.get.unk1 mustEqual 131 + entries.head.listing.get.unk2 mustEqual false + entries.head.listing.get.unk3.isDefined mustEqual false + entries.head.listing.get.info.get.leader.get mustEqual "GeneralGorgutz" + entries.head.listing.get.info.get.task.get mustEqual "FLY,All welcome,cn last night!!!!" + entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4) + entries.head.listing.get.info.get.size.get mustEqual 7 + entries.head.listing.get.info.get.capacity.get mustEqual 10 + entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(6) + entries(1).index mustEqual 1 + entries(1).listing.get.unk1 mustEqual 131 + entries(1).listing.get.unk2 mustEqual false + entries(1).listing.get.unk3.isDefined mustEqual false + entries(1).listing.get.info.get.leader.get mustEqual "NIGHT88RAVEN" + entries(1).listing.get.info.get.task.get mustEqual "All Welcome" + entries(1).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10) + entries(1).listing.get.info.get.size.get mustEqual 4 + entries(1).listing.get.info.get.capacity.get mustEqual 10 + entries(1).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(3) + entries(2).index mustEqual 2 + entries(2).listing.get.unk1 mustEqual 131 + entries(2).listing.get.unk2 mustEqual false + entries(2).listing.get.unk3.isDefined mustEqual false + entries(2).listing.get.info.get.leader.get mustEqual "KOKkiasMFCN" + entries(2).listing.get.info.get.task.get mustEqual "Squad 2" + entries(2).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4) + entries(2).listing.get.info.get.size.get mustEqual 6 + entries(2).listing.get.info.get.capacity.get mustEqual 10 + entries(2).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(4) + entries(3).index mustEqual 255 + case _ => + ko + } + } + + "decode (remove)" in { + PacketCoding.DecodePacket(stringListRemove).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 1 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 5 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 0 + entries.head.listing.get.unk2 mustEqual true + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 4 + entries.head.listing.get.info.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update leader)" in { + PacketCoding.DecodePacket(stringUpdateLeader).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 2 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 128 + entries.head.listing.get.unk2 mustEqual true + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 0 + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual true + entries.head.listing.get.info.get.leader.get mustEqual "FateJHNC" + entries.head.listing.get.info.get.task.isDefined mustEqual false + entries.head.listing.get.info.get.zone_id.isDefined mustEqual false + entries.head.listing.get.info.get.size.isDefined mustEqual false + entries.head.listing.get.info.get.capacity.isDefined mustEqual false + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update task)" in { + PacketCoding.DecodePacket(stringUpdateTask).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 5 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 128 + entries.head.listing.get.unk2 mustEqual true + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 1 + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual false + entries.head.listing.get.info.get.task.isDefined mustEqual true + entries.head.listing.get.info.get.task.get mustEqual "RIP PS1, visit PSForever.net" + entries.head.listing.get.info.get.zone_id.isDefined mustEqual false + entries.head.listing.get.info.get.size.isDefined mustEqual false + entries.head.listing.get.info.get.capacity.isDefined mustEqual false + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update continent)" in { + PacketCoding.DecodePacket(stringUpdateContinent).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 3 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 128 + entries.head.listing.get.unk2 mustEqual true + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 1 + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual false + entries.head.listing.get.info.get.task.isDefined mustEqual false + entries.head.listing.get.info.get.zone_id.isDefined mustEqual true + entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10) + entries.head.listing.get.info.get.size.isDefined mustEqual false + entries.head.listing.get.info.get.capacity.isDefined mustEqual false + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update size)" in { + PacketCoding.DecodePacket(stringUpdateSize).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 1 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 128 + entries.head.listing.get.unk2 mustEqual true + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 2 + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual false + entries.head.listing.get.info.get.task.isDefined mustEqual false + entries.head.listing.get.info.get.zone_id.isDefined mustEqual false + entries.head.listing.get.info.get.size.isDefined mustEqual true + entries.head.listing.get.info.get.size.get mustEqual 6 + entries.head.listing.get.info.get.capacity.isDefined mustEqual false + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update leader and size)" in { + PacketCoding.DecodePacket(stringUpdateLeaderSize).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 5 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 129 + entries.head.listing.get.unk2 mustEqual false + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 0 + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual true + entries.head.listing.get.info.get.leader.get mustEqual "Jimmyn" + entries.head.listing.get.info.get.task.isDefined mustEqual false + entries.head.listing.get.info.get.zone_id.isDefined mustEqual false + entries.head.listing.get.info.get.size.isDefined mustEqual true + entries.head.listing.get.info.get.size.get mustEqual 3 + entries.head.listing.get.info.get.capacity.isDefined mustEqual false + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update task and continent)" in { + PacketCoding.DecodePacket(stringUpdateTaskContinent).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 5 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 129 + entries.head.listing.get.unk2 mustEqual false + entries.head.listing.get.unk3.isDefined mustEqual true + entries.head.listing.get.unk3.get mustEqual 1 + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual false + entries.head.listing.get.info.get.task.isDefined mustEqual true + entries.head.listing.get.info.get.task.get mustEqual "2" + entries.head.listing.get.info.get.zone_id.isDefined mustEqual true + entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4) + entries.head.listing.get.info.get.size.isDefined mustEqual false + entries.head.listing.get.info.get.capacity.isDefined mustEqual false + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (update all)" in { + PacketCoding.DecodePacket(stringUpdateAll).require match { + case ReplicationStreamMessage(behavior, behavior2, entries) => + behavior mustEqual 6 + behavior2.isDefined mustEqual false + entries.length mustEqual 2 + entries.head.index mustEqual 7 + entries.head.listing.isDefined mustEqual true + entries.head.listing.get.unk1 mustEqual 131 + entries.head.listing.get.unk2 mustEqual false + entries.head.listing.get.unk3.isDefined mustEqual false + entries.head.listing.get.info.isDefined mustEqual true + entries.head.listing.get.info.get.leader.isDefined mustEqual true + entries.head.listing.get.info.get.leader.get mustEqual "madmuj" + entries.head.listing.get.info.get.task.isDefined mustEqual true + entries.head.listing.get.info.get.task.get mustEqual "" + entries.head.listing.get.info.get.zone_id.isDefined mustEqual true + entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4) + entries.head.listing.get.info.get.size.isDefined mustEqual true + entries.head.listing.get.info.get.size.get mustEqual 0 + entries.head.listing.get.info.get.capacity.isDefined mustEqual true + entries.head.listing.get.info.get.capacity.get mustEqual 10 + entries.head.listing.get.info.get.squad_guid.isDefined mustEqual true + entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(11) + entries(1).index mustEqual 255 + case _ => + ko + } + } + + "decode (fails)" in { + PacketCoding.DecodePacket(stringCodecFail).isFailure mustEqual true + //PacketCoding.DecodePacket(stringListOneFail).isFailure mustEqual true -> used to fail + //PacketCoding.DecodePacket(stringListTwoFail).isFailure mustEqual true -> used to fail + PacketCoding.DecodePacket(stringUpdateLeaderFail).isFailure mustEqual true + PacketCoding.DecodePacket(stringUpdateTaskFail).isFailure mustEqual true + //PacketCoding.DecodePacket(stringUpdateContinentFail).isFailure mustEqual true -> used to fail + PacketCoding.DecodePacket(stringUpdateSizeFail).isFailure mustEqual true + PacketCoding.DecodePacket(stringUpdateLeaderSizeFail).isFailure mustEqual true + PacketCoding.DecodePacket(stringUpdateTaskContinentFail).isFailure mustEqual true + //PacketCoding.DecodePacket(stringUpdateAllFail).isFailure mustEqual true -> used to fail + } + + "encode (clear)" in { + val msg = ReplicationStreamMessage(5, Some(6), + Vector( + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringListClear + } + + "encode (one)" in { + val msg = ReplicationStreamMessage(5, Some(6), + Vector( + SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10, PlanetSideGUID(1))))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringListOne + } + + "encode (two)" in { + val msg = ReplicationStreamMessage(5, Some(6), + Vector( + SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))), + SquadListing(1, Some(SquadHeader(131, false, None, SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringListTwo + } + + "encode (three)" in { + val msg = ReplicationStreamMessage(5, Some(6), + Vector( + SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))), + SquadListing(1, Some(SquadHeader(131, false, None, SquadInfo("NIGHT88RAVEN", "All Welcome", PlanetSideZoneID(10), 4, 10, PlanetSideGUID(3))))), + SquadListing(2, Some(SquadHeader(131, false, None, SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringListThree + } + + "encode (remove)" in { + val msg = ReplicationStreamMessage(1, None, + Vector( + SquadListing(5, Some(SquadHeader(0, true, Some(4)))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringListRemove + } + + "encode (update leader)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(2, Some(SquadHeader(128, true, Some(0), SquadInfo("FateJHNC", None)))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateLeader + } + + "encode (update task)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(5, Some(SquadHeader(128, true, Some(1), SquadInfo(None, "RIP PS1, visit PSForever.net")))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateTask + } + + "encode (update continent)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(3, Some(SquadHeader(128, true, Some(1), SquadInfo(PlanetSideZoneID(10))))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateContinent + } + + "encode (update size)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(1, Some(SquadHeader(128, true, Some(2), SquadInfo(6, None)))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateSize + } + + "encode (update leader and size)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(5, Some(SquadHeader(129, false, Some(0), SquadInfo("Jimmyn", 3)))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateLeaderSize + } + + "encode (update task and continent)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(5, Some(SquadHeader(129, false, Some(1), SquadInfo("2", PlanetSideZoneID(4))))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateTaskContinent + } + + "encode (update all)" in { + val msg = ReplicationStreamMessage(6, None, + Vector( + SquadListing(7, Some(SquadHeader(131, false, None, SquadInfo("madmuj", "", PlanetSideZoneID(4), 0, 10, PlanetSideGUID(11))))), + SquadListing(255) + ) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual stringUpdateAll + } + + "encode (fails)" in { + //encode codec fail + PacketCoding.EncodePacket( + ReplicationStreamMessage(1, None, + Vector( + SquadListing(5, Some(SquadHeader(0, false, Some(4)))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode one + PacketCoding.EncodePacket( + ReplicationStreamMessage(5, Some(6), + Vector( + SquadListing(0, Some(SquadHeader(131, false, None, Some(SquadInfo(Some("FragLANdINC"), Some("Frag"), None, Some(0),Some(10), Some(PlanetSideGUID(1))))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode two + PacketCoding.EncodePacket( + ReplicationStreamMessage(5, Some(6), + Vector( + SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))), + SquadListing(1, Some(SquadHeader(131, false, None, Some(SquadInfo(Some("KOKkiasMFCN"), Some("Squad 2"), None, Some(6), Some(10), Some(PlanetSideGUID(4))))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode leader + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(2, Some(SquadHeader(128, true, Some(0), Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode task + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(5, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode continent + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(3, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode task or continent + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(3, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, Some(""), Some(PlanetSideZoneID(10)), None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode size + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(1, Some(SquadHeader(128, true, Some(2), Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode leader and size + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(5, Some(SquadHeader(129, false, Some(0), Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode task and continent + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(5, Some(SquadHeader(129, false, Some(1), Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + + //encode all + PacketCoding.EncodePacket( + ReplicationStreamMessage(6, None, + Vector( + SquadListing(7, Some(SquadHeader(131, false, None, Some(SquadInfo(None, None, None, None, None, None))))), + SquadListing(255) + ) + ) + ).isFailure mustEqual true + } + } + "ZoneLockInfoMesage" should { val string = hex"DF 1B 00 40" diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala index 8131b89c..8fa98af4 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -9,6 +9,7 @@ import org.log4s.MDC import scodec.Attempt.{Failure, Successful} import scodec.bits._ import MDCContextAware.Implicits._ +import net.psforever.types.PlanetSideEmpire import scala.concurrent.duration._ import scala.util.Random diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 1f5792a8..0762f464 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -10,7 +10,7 @@ import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ import net.psforever.packet.game.objectcreate._ -import net.psforever.types.{ChatMessageType, Vector3} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3} class WorldSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger @@ -113,7 +113,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val app = CharacterAppearanceData( Vector3(3674.8438f, 2726.789f, 91.15625f), 19, - 2, + PlanetSideEmpire.VS, false, 4, "IlllIIIlllIlIllIlllIllI", @@ -216,6 +216,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0))) sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))) + sendResponse(PacketCoding.CreateGamePacket(0, ReplicationStreamMessage(5, Some(6), Vector(SquadListing(255))))) //clear squad list import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -286,6 +287,19 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ AvatarJumpMessage(state) => //log.info("AvatarJump: " + msg) + case msg @ ZipLineMessage(player_guid,origin_side,action,id,x,y,z) => + log.info("ZipLineMessage: " + msg) + if(action == 0) { + //doing this lets you use the zip line, but you can't get off + //sendResponse(PacketCoding.CreateGamePacket(0,ZipLineMessage(player_guid, origin_side, action, id, x,y,z))) + } + else if(action == 1) { + //disembark from zipline at destination? + } + else if(action == 2) { + //get off by force + } + case msg @ RequestDestroyMessage(object_guid) => log.info("RequestDestroy: " + msg) // TODO: Make sure this is the correct response in all cases @@ -335,6 +349,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ SquadDefinitionActionMessage(a, b, c, d, e, f, g, h, i) => log.info("SquadDefinitionAction: " + msg) + case msg @ BugReportMessage(version_major,version_minor,version_date,bug_type,repeatable,location,zone,pos,summary,desc) => + log.info("BugReportMessage: " + msg) + case default => log.debug(s"Unhandled GamePacket ${pkt}") }