Merge branch 'master' into Updates

This commit is contained in:
SouNourS 2017-05-02 09:26:55 +02:00 committed by GitHub
commit ed9bc36b56
77 changed files with 6760 additions and 1489 deletions

View file

@ -345,15 +345,15 @@ object GamePacketOpcode extends Enumeration {
case 0x14 => game.CharacterInfoMessage.decode
case 0x15 => noDecoder(UnknownMessage21)
case 0x16 => game.BindPlayerMessage.decode
case 0x17 => noDecoder(ObjectCreateMessage_Duplicate)
case 0x17 => game.ObjectCreateMessage.decode
// 0x18
case 0x18 => game.ObjectCreateMessage.decode
case 0x18 => game.ObjectCreateDetailedMessage.decode
case 0x19 => game.ObjectDeleteMessage.decode
case 0x1a => game.PingMsg.decode
case 0x1b => noDecoder(VehicleStateMessage)
case 0x1c => noDecoder(FrameVehicleStateMessage)
case 0x1d => game.GenericObjectStateMsg.decode
case 0x1e => noDecoder(ChildObjectStateMessage)
case 0x1e => game.ChildObjectStateMessage.decode
case 0x1f => game.ActionResultMessage.decode
// OPCODES 0x20-2f
@ -398,7 +398,7 @@ object GamePacketOpcode extends Enumeration {
case 0x40 => noDecoder(MountVehicleCargoMsg)
case 0x41 => noDecoder(DismountVehicleCargoMsg)
case 0x42 => noDecoder(CargoMountPointStatusMessage)
case 0x43 => noDecoder(BeginZoningMessage)
case 0x43 => game.BeginZoningMessage.decode
case 0x44 => game.ItemTransactionMessage.decode
case 0x45 => game.ItemTransactionResultMessage.decode
case 0x46 => game.ChangeFireModeMessage.decode
@ -411,7 +411,7 @@ object GamePacketOpcode extends Enumeration {
case 0x4c => noDecoder(UnknownMessage76)
case 0x4d => game.RepairMessage.decode
case 0x4e => noDecoder(ServerVehicleOverrideMsg)
case 0x4f => noDecoder(LashMessage)
case 0x4f => game.LashMessage.decode
// OPCODES 0x50-5f
case 0x50 => noDecoder(TargetingInfoMessage)
@ -428,7 +428,7 @@ object GamePacketOpcode extends Enumeration {
case 0x5a => noDecoder(DelayedPathMountMsg)
case 0x5b => noDecoder(OrbitalShuttleTimeMsg)
case 0x5c => noDecoder(AIDamage)
case 0x5d => noDecoder(DeployObjectMessage)
case 0x5d => game.DeployObjectMessage.decode
case 0x5e => noDecoder(FavoritesRequest)
case 0x5f => noDecoder(FavoritesResponse)
@ -456,9 +456,9 @@ object GamePacketOpcode extends Enumeration {
case 0x71 => noDecoder(PlatoonEvent)
case 0x72 => game.FriendsRequest.decode
case 0x73 => game.FriendsResponse.decode
case 0x74 => noDecoder(TriggerEnvironmentalDamageMessage)
case 0x74 => game.TriggerEnvironmentalDamageMessage.decode
case 0x75 => game.TrainingZoneMessage.decode
case 0x76 => noDecoder(DeployableObjectsInfoMessage)
case 0x76 => game.DeployableObjectsInfoMessage.decode
case 0x77 => noDecoder(SquadState)
// 0x78
case 0x78 => noDecoder(OxygenStateMessage)
@ -477,7 +477,7 @@ object GamePacketOpcode extends Enumeration {
case 0x83 => noDecoder(SquadWaypointRequest)
case 0x84 => noDecoder(SquadWaypointEvent)
case 0x85 => noDecoder(OffshoreVehicleMessage)
case 0x86 => noDecoder(ObjectDeployedMessage)
case 0x86 => game.ObjectDeployedMessage.decode
case 0x87 => noDecoder(ObjectDeployedCountMessage)
// 0x88
case 0x88 => game.WeaponDelayFireMessage.decode
@ -522,8 +522,8 @@ object GamePacketOpcode extends Enumeration {
case 0xa9 => game.AvatarGrenadeStateMessage.decode
case 0xaa => noDecoder(UnknownMessage170)
case 0xab => noDecoder(UnknownMessage171)
case 0xac => noDecoder(ReleaseAvatarRequestMessage)
case 0xad => noDecoder(AvatarDeadStateMessage)
case 0xac => game.ReleaseAvatarRequestMessage.decode
case 0xad => game.AvatarDeadStateMessage.decode
case 0xae => noDecoder(CSAssistMessage)
case 0xaf => noDecoder(CSAssistCommentMessage)
@ -531,7 +531,7 @@ object GamePacketOpcode extends Enumeration {
case 0xb0 => game.VoiceHostRequest.decode
case 0xb1 => game.VoiceHostKill.decode
case 0xb2 => game.VoiceHostInfo.decode
case 0xb3 => noDecoder(BattleplanMessage)
case 0xb3 => game.BattleplanMessage.decode
case 0xb4 => game.BattleExperienceMessage.decode
case 0xb5 => noDecoder(TargetingImplantRequest)
case 0xb6 => game.ZonePopulationUpdateMessage.decode
@ -605,7 +605,7 @@ object GamePacketOpcode extends Enumeration {
// OPCODES 0xf0-f3
case 0xf0 => noDecoder(QueueTimedHelpMessage)
case 0xf1 => noDecoder(MailMessage)
case 0xf1 => game.MailMessage.decode
case 0xf2 => noDecoder(GameVarUpdate)
case 0xf3 => noDecoder(ClientCheatedMessage)
case default => noDecoder(opcode)

View file

@ -31,7 +31,7 @@ final case class ControlPacket(opcode : ControlPacketOpcode.Value,
object PacketCoding {
/// A lower bound on the packet size
final val PLANETSIDE_MIN_PACKET_SIZE = 2
final val PLANETSIDE_MIN_PACKET_SIZE = 1
/**
* Given a full and complete planetside packet as it would be sent on the wire, attempt to

View file

@ -2,6 +2,7 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.ExoSuitType
import scodec.Codec
import scodec.codecs._
@ -13,23 +14,21 @@ import scodec.codecs._
* Due to the way armor is handled internally, a player of one faction may not spawn in the exo-suit of another faction.
* That style of exo-suit is never available through this packet.
* As MAX units do not get their weapon by default, all the MAX values produce the same faction-appropriate mechanized exo-suit body visually.
* (The MAX weapons are supplied in subsequent packets.)
* (The MAX weapons are supplied in subsequent packets.)<br>
* <br>
* Mechanized Assault Subtypes:<br>
* `
* 0, 0 - Agile<br>
* 1, 0 - Reinforced<br>
* 2, 0 - MAX<br>
* 2, 1 - AI MAX<br>
* 2, 2 - AV MAX<br>
* 2, 3 - AA MAX<br>
* 3, 0 - Infiltration<br>
* 4, 0 - Standard
* 0 - na<br>
* 1 - AI MAX<br>
* 2 - AV MAX<br>
* 3 - AA MAX
* `
* @param player_guid the player
* @param armor the type of exo-suit
* @param subtype the exo-suit subtype, if any
*/
final case class ArmorChangedMessage(player_guid : PlanetSideGUID,
armor : Int,
armor : ExoSuitType.Value,
subtype : Int)
extends PlanetSideGamePacket {
type Packet = ArmorChangedMessage
@ -40,7 +39,7 @@ final case class ArmorChangedMessage(player_guid : PlanetSideGUID,
object ArmorChangedMessage extends Marshallable[ArmorChangedMessage] {
implicit val codec : Codec[ArmorChangedMessage] = (
("player_guid" | PlanetSideGUID.codec) ::
("armor" | uintL(3)) ::
("armor" | ExoSuitType.codec) ::
("subtype" | uintL(3))
).as[ArmorChangedMessage]
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.Vector3
import scodec.Codec
import scodec.codecs._
/**
* na
* @param unk1 0 = nothing, 1 = waiting for a rez, 2 = auto map to select spawn, 3 = respawn time
* @param unk2 na
* @param unk3 spawn penality
* @param pos last victim's position
* @param unk4 na
* @param unk5 na
*/
final case class AvatarDeadStateMessage(unk1 : Int,
unk2 : Long,
unk3 : Long,
pos : Vector3,
unk4 : Long,
unk5 : Boolean)
extends PlanetSideGamePacket {
type Packet = AvatarDeadStateMessage
def opcode = GamePacketOpcode.AvatarDeadStateMessage
def encode = AvatarDeadStateMessage.encode(this)
}
object AvatarDeadStateMessage extends Marshallable[AvatarDeadStateMessage] {
implicit val codec : Codec[AvatarDeadStateMessage] = (
("unk1" | uintL(3)) ::
("unk2" | uint32L) ::
("unk3" | uint32L) ::
("pos" | Vector3.codec_pos) ::
("unk4" | uint32L) ::
("unk5" | bool)
).as[AvatarDeadStateMessage]
}

View file

@ -1,23 +1,11 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.GrenadeState
import scodec.Codec
import scodec.codecs._
/**
* An `Enumeration` of the kinds of states applicable to the grenade animation.
*/
object GrenadeState extends Enumeration {
type Type = Value
val UNK0,
PRIMED, //avatars and other depicted player characters
THROWN //avatars only
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
}
/**
* Report the state of the grenade throw animation for this player.
* The default state is "held at side," though the client's avatar never has to announce this.<br>
@ -25,12 +13,12 @@ object GrenadeState extends Enumeration {
* The throwing animation has a minor timing glitch.
* Causing another player to raise his arm will always result in that arm being lowered a few seconds later.
* This is as opposed to the client's avatar, who can seem to hold a grenade in the "prepare to throw" state indefinitely.
* If the avatar looks away from a player whose grenade arm is up ("prepare to throw"), however, when they look back at the player
* If the avatar looks away from a player whose grenade arm is up ("prepare to throw"), however, when they look back at the player,
* his grenade arm will occasionally have been lowered ("held at side") again before it would normally be lowered.<br>
* <br>
* A client will dispatch state '1' and state '2' for the avatar's actions.
* A client will only react temporarily for another character other than the avatar when the given a state '1'.
* If that internal state is not changed, however, that other character will not respond to any subsequent '1' state.
* A client will dispatch state 'Primed' and state 'Thrown' for the avatar's actions.
* A client will only react temporarily for another character other than the avatar when the given a state 'Primed'.
* If that internal state is not changed, however, that other character will not respond to any subsequent 'Primed' state.
* (This may also be a glitch.)<br>
* <br>
* States:<br>

View file

@ -0,0 +1,411 @@
// Copyright (c) 2017 PSForever
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}
/**
* A `Codec` for the actions that each layer of the diagram performs.
* `Style`, `Vertex`, `Action5`, `DrawString`, and `Action7` have additional `DiagramStroke` input data.
*/
object DiagramActionCode extends Enumeration {
type Type = Value
val Action0,
Style,
Vertex,
Action3,
Action4,
Action5,
DrawString,
Action7,
Action8,
Action9,
ActionA,
ActionB,
ActionC, //clear?
ActionD, //opposite of clear?
StartDrawing,
StopDrawing
= Value //TODO replace all these with descriptive words
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}
/**
* A common ancestor of all the different "strokes" used to keep track of the data.
*/
sealed trait DiagramStroke
/**
* Set style properties for the line segemnt(s) to be drawn.
* Color and thickness can not vary within a given line and will only apply to the subsequent line segments.
* Attempting to list a change in between coordinate points will invalidate that segment.
* @param thickness the line width in pixels;
* 0.0f - 16.0f;
* 3.0f is about normal and 0.0f is smaller than the map grid lines
* @param color the color of the line;
* 0 is gray (default);
* 1 is red;
* 2 is green;
* 3 is blue
*/
final case class Style(thickness : Float,
color : Int) extends DiagramStroke
/**
* Indicate coordinates on the tactical map.
* Any adjacent sets of coordinates will be connected with a line segment.
* @param x the x-coordinate of this point
* @param y the y-coordinate of this point
*/
final case class Vertex(x : Float,
y : Float) extends DiagramStroke
/**
* na
* @param x the x-coordinate of this point
* @param y the y-coordinate of this point
* @param unk na;
* 1024.0f - 0.0f
*/
final case class StrokeFive(x : Float,
y : Float,
unk : Float) extends DiagramStroke
/**
* Draw a string message on the tactical map.
* String messages have their own color designation and will not inherit line properties.
* @param x the x-coordinate marking the bottom center of this message's text
* @param y the y-coordinate marking the bottom center of this message's text
* @param color the color of the message;
* 0 is gray (default);
* 1 is red;
* 2 is green;
* 3 is blue
* @param channel the available "slots" in which to display messages on the map;
* a maximum of 16 channels/messages (0-15) are available per player;
* no two messages may inhabit the same channel
* @param message the text to display
*/
final case class DrawString(x : Float,
y : Float,
color : Int,
channel : Int,
message : String) extends DiagramStroke
/**
* na
* @param unk na
*/
final case class StrokeSeven(unk : Int) extends DiagramStroke
/**
* A particular instruction in the rendering of this battleplan's diagram entry.
* @param action the behavior of this stroke;
* a hint to the kind of stroke data stored, if at all, and how to use it or incorporate prior data
* @param stroke the data;
* defaults to `None`
*/
final case class BattleDiagramAction(action : DiagramActionCode.Value,
stroke : Option[DiagramStroke] = None)
/**
* Share drawn images and words on the tactical map among a group of players.<br>
* <br>
* Each packet usually contains a small portion of an image, herein called a "diagram."
* `BattleplanMessage` packets are accumulative towards a full diagram.
* Moreover, rather than the `player_name`, each diagram is associated on a client by the `char_id` field.
* Only squad leaders and platoon leaders can draw on the map and share with other players in their squad or platoon.<br>
* <br>
* To start drawing, a would-be artist must have all clients who will receive their diagrams acknowledge a `StartDrawing` action.
* The `char_id` with this `StartDrawing` will associate all diagrams submitted with the same `char_id`'s portfolio.
* Multiple portfolio definitions may exist on a client at a given time and each will manage their own diagrams.
* When a given portfolio submits a `StopDrawing` action that is received, the previous diagrams associated with it will be cleared.
* That `char_id` will no longer accept diagrams on that client.
* Other portfolios will continue to accept diagrams as initialized.
* When no portfolios are being accepted, the "Toggle -> Battleplan" button on that client's tactical map will be disabled.
* When there is at least one portfolio accepted, the "Battleplan" button will be functional and can be toggled.<br>
* <br>
* To construct line segments, chain `StrokeTwo` diagrams in the given packet entry.
* Each defined point will act like a successive vertex in a chain of segments.
* Any non-vertex entry in between entries, e.g., a change of line color, will break the chain of line segments.
* For example:<br>
* RED-A-B-C will construct red lines segments A-B and B-C.<br>
* RED-A-B-GREEN-C will only construct a red line segement A-B.<br>
* RED-A-B-GREEN-C-D will construct a red line segement A-B and a green line segment C-D.<br>
* (Default line color, if none is declared specifically, is gray.)<br>
* <br>
* To construct a message, define a point to act as the center baseline for the text.
* The message will be written above and outwards from that point.
* Messages do not carry properties over from line segments - they set their own color and do not have line thickness.
* Any single portfolio may have only fifteen messages written to the tactical map at a time.
* @param char_id na;
* same as in `CharacterInfoMessage`
* @param player_name the player who contributed this battle plan
* @param zone_id on which continent the battle plan will be overlaid;
* can identify as "no zone" 0 when performing instructions not specific to drawing
* @param diagrams a list of the itemized actions that will construct this plan or are used to modify the plan
*/
final case class BattleplanMessage(char_id : Long,
player_name : String,
zone_id : PlanetSideGUID,
diagrams : List[BattleDiagramAction])
extends PlanetSideGamePacket {
type Packet = BattleplanMessage
def opcode = GamePacketOpcode.BattleplanMessage
def encode = BattleplanMessage.encode(this)
}
object BattleDiagramAction {
/**
* Create a `BattleDiagramAction` object containing `StrokeOne` data.
* @param thickness the line width in pixels
* @param color the color of the line
* @return a `BattleDiagramAction` object
*/
def style(thickness : Float, color : Int) : BattleDiagramAction =
BattleDiagramAction(DiagramActionCode.Style, Some(Style(thickness, color)))
/**
* Create a `BattleDiagramAction` object containing `StrokeTwo` vertex data.
* @param x the x-coordinate of this point
* @param y the y-coordinate of this point
* @return a `BattleDiagramAction` object
*/
def vertex(x : Float, y : Float) : BattleDiagramAction =
BattleDiagramAction(DiagramActionCode.Vertex, Some(Vertex(x, y)))
/**
* Create a `BattleDiagramAction` object containing `StrokeFive` data.
* @param x the x-coordinate of this point
* @param y the y-coordinate of this point
* @param unk na
* @return a `BattleDiagramAction` object
*/
def stroke5(x : Float, y : Float, unk : Float) : BattleDiagramAction =
BattleDiagramAction(DiagramActionCode.Action5, Some(StrokeFive(x, y, unk)))
/**
* Create a `BattleDiagramAction` object containing `StrokeSix` data.
* @param x the x-coordinate marking the bottom center of this message's text
* @param y the y-coordinate marking the bottom center of this message's text
* @param color the color of the message
* @param channel the available "slots" in which to display messages on the map
* @param message the text to display
*/
def drawString(x : Float, y : Float, color : Int, channel : Int, message : String) : BattleDiagramAction =
BattleDiagramAction(DiagramActionCode.DrawString, Some(DrawString(x, y, color, channel, message)))
/**
* Create a `BattleDiagramAction` object containing `StrokeSeven` data.
* @param unk na
* @return a `BattleDiagramAction` object
*/
def stroke7(unk : Int) : BattleDiagramAction =
BattleDiagramAction(DiagramActionCode.Action7, Some(StrokeSeven(unk)))
}
object BattleplanMessage extends Marshallable[BattleplanMessage] {
/**
* An intermediary object intended to temporarily store `BattleDiagramAction` objects.<br>
* <br>
* This hidden object is arranged like a linked list.
* During the decoding process, it is converted into an accessible formal `List`.
* During the encoding process, the `List` is transformed back into a linked list structure.
* Scala's own linked list `Collection` is deprecated, without substitution, so this custom one shall be used.
* @param diagram the contained object that maintains the data
* @param next the next `BattleDiagramChain`, if any
* @see scala.collection.mutable.LinkedList&#60;E&#62;
* @see java.util.LinkedList&#60;E&#62;
*/
private final case class BattleDiagramChain(diagram : BattleDiagramAction,
next : Option[BattleDiagramChain])
/**
* Parse data into a `Style` object.
*/
private val plan1_codec : Codec[Style] = ( //size: 8 (12)
("thickness" | newcodecs.q_float(16.0, 0.0, 5)) ::
("color" | uintL(3))
).as[Style]
/**
* Parse data into a `Vertex` object.
*/
private val plan2_codec : Codec[Vertex] = ( //size: 22 (26)
("x" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("y" | newcodecs.q_float(-4096.0, 12288.0, 11))
).as[Vertex]
/**
* Parse data into a `StrokeFive` object.
*/
private val plan5_codec : Codec[StrokeFive] = ( //size: 33 (37)
("unk1" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("unk2" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("unk3" | newcodecs.q_float(1024.0, 0.0, 11))
).as[StrokeFive]
/**
* Parse data into a `DrawString` object.<br>
* If we are on a byte boundary upon starting this entry, our message is padded by `5u` (always).
* If we are not on a byte boundary, we must use our current offset and this size (`31u + 4u`) to calculate the padding value.
* @param padOffset the current padding value for the `String` entry
*/
private def plan6_codec(padOffset : Int) : Codec[DrawString] = ( //size: irrelevant, pad value resets
("x" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("y" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("color" | uintL(3)) ::
("font_size" | uintL(6)) ::
("message" | PacketHelpers.encodedWideStringAligned( if(padOffset % 8 == 0) { 5 } else { 8 - (padOffset + 35) % 8 } ))
).as[DrawString]
/**
* Parse data into a `StrokeSeven` object.
*/
private val plan7_codec : Codec[StrokeSeven] = ("unk" | uintL(6)).as[StrokeSeven] // size: 6 (10)
/**
* Switch between different patterns to create a `BattleDiagramAction` for the following data.
* @param plan a hint to help parse the following data
* @param pad the current padding for any `String` entry stored within the parsed elements;
* when `plan == 6`, `plan6_codec` utilizes this value
* @return a `BattleDiagramAction` object
*/
private def diagram_codec(plan : DiagramActionCode.Value, pad : Int) : Codec[BattleDiagramAction] = (
conditional(plan == DiagramActionCode.Style, plan1_codec) ::
conditional(plan == DiagramActionCode.Vertex, plan2_codec) ::
conditional(plan == DiagramActionCode.Action5, plan5_codec) ::
conditional(plan == DiagramActionCode.DrawString, plan6_codec(pad)) ::
conditional(plan == DiagramActionCode.Action7, plan7_codec)
).exmap[BattleDiagramAction] (
{
case Some(stroke) :: None :: None :: None :: None :: HNil =>
Attempt.successful(BattleDiagramAction(plan, Some(stroke)))
case None :: Some(stroke) :: None :: None :: None :: HNil =>
Attempt.successful(BattleDiagramAction(plan, Some(stroke)))
case None :: None :: Some(stroke) :: None :: None :: HNil =>
Attempt.successful(BattleDiagramAction(plan, Some(stroke)))
case None :: None :: None :: Some(stroke) :: None :: HNil =>
Attempt.successful(BattleDiagramAction(plan, Some(stroke)))
case None :: None :: None :: None :: Some(stroke) :: HNil =>
Attempt.successful(BattleDiagramAction(plan, Some(stroke)))
case None :: None :: None :: None :: None :: HNil =>
Attempt.successful(BattleDiagramAction(plan, None))
case _:: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err(s"too many strokes for action $plan"))
},
{
case BattleDiagramAction(DiagramActionCode.Style, Some(stroke)) =>
Attempt.successful(Some(stroke.asInstanceOf[Style]) :: None :: None :: None :: None :: HNil)
case BattleDiagramAction(DiagramActionCode.Vertex, Some(stroke)) =>
Attempt.successful(None :: Some(stroke.asInstanceOf[Vertex]) :: None :: None :: None :: HNil)
case BattleDiagramAction(DiagramActionCode.Action5, Some(stroke)) =>
Attempt.successful(None :: None :: Some(stroke.asInstanceOf[StrokeFive]) :: None :: None :: HNil)
case BattleDiagramAction(DiagramActionCode.DrawString, Some(stroke)) =>
Attempt.successful(None :: None :: None :: Some(stroke.asInstanceOf[DrawString]) :: None :: HNil)
case BattleDiagramAction(DiagramActionCode.Action7, Some(stroke)) =>
Attempt.successful(None :: None :: None :: None :: Some(stroke.asInstanceOf[StrokeSeven]) :: HNil)
case BattleDiagramAction(_, None) =>
Attempt.successful(None :: None :: None :: None :: None :: HNil)
case BattleDiagramAction(n, _) =>
Attempt.failure(Err(s"unhandled stroke action number $n"))
}
)
/**
* Parse diagram instructions as a linked list.
* Maintain a `String` padding value that applies an appropriate offset value regardless of where in the elements it is required.
* @param remaining the number of elements remaining to parse
* @param padOffset the current padding for any `String` entry stored within the parsed elements;
* different elements add different padding offset to this field on subsequent passes
* @return a `Codec` for `BattleDiagramChain` segments
*/
private def parse_diagrams_codec(remaining : Int, padOffset : Int = 0) : Codec[BattleDiagramChain] = (
DiagramActionCode.codec >>:~ { plan =>
("diagram" | diagram_codec(plan, padOffset)) ::
conditional(remaining > 1,
"next" | parse_diagrams_codec(
remaining - 1,
padOffset + (if(plan == DiagramActionCode.DrawString) { -padOffset } else if(plan == DiagramActionCode.Action5) { 37 } else if(plan == DiagramActionCode.Vertex) { 26 } else if(plan == DiagramActionCode.Style) { 12 } else if(plan == DiagramActionCode.Action7) { 10 } else { 4 })
)
)
}).exmap[BattleDiagramChain] (
{
case _ :: diagram :: next :: HNil =>
Attempt.successful(BattleDiagramChain(diagram, next))
},
{
case BattleDiagramChain(BattleDiagramAction(num, stroke), next) =>
Attempt.successful(num :: BattleDiagramAction(num, stroke) :: next :: HNil)
}
)
import scala.collection.mutable.ListBuffer
/**
* Transform a linked list of `BattleDiagramChain` into a `List` of `BattleDiagramAction` objects.
* @param element the current link in a chain of `BattleDiagramChain` objects
* @param list a `List` of extracted `BattleDiagrams`;
* technically, the output
*/
private def rollDiagramLayers(element : Option[BattleDiagramChain], list : ListBuffer[BattleDiagramAction]) : Unit = {
if(element.isEmpty)
return
list += element.get.diagram
rollDiagramLayers(element.get.next, list) //tail call optimization
}
/**
* Transform a `List` of `BattleDiagramAction` objects into a linked list of `BattleDiagramChain` objects.
* @param revIter a reverse `List` `Iterator` for a `List` of `BattleDiagrams`
* @param layers the current head of a chain of `BattleDiagramChain` objects;
* defaults to `None`, so does not need to be defined during the initial pass;
* technically, the output
* @return a linked list of `BattleDiagramChain` objects
*/
private def unrollDiagramLayers(revIter : Iterator[BattleDiagramAction], layers : Option[BattleDiagramChain] = None) : Option[BattleDiagramChain] = {
if(!revIter.hasNext)
return layers
val elem : BattleDiagramAction = revIter.next
unrollDiagramLayers(revIter, Some(BattleDiagramChain(elem, layers))) //tail call optimization
}
implicit val codec : Codec[BattleplanMessage] = (
("char_id" | uint32L) ::
("player_name" | PacketHelpers.encodedWideString) ::
("zone_id" | PlanetSideGUID.codec) ::
(uint8L >>:~ { count =>
conditional(count > 0, "diagrams" | parse_diagrams_codec(count)).hlist
})
).exmap[BattleplanMessage] (
{
case char_id :: player :: zone_id :: _ :: diagramLayers :: HNil =>
val list : ListBuffer[BattleDiagramAction] = new ListBuffer()
if(diagramLayers.isDefined)
rollDiagramLayers(diagramLayers, list)
Attempt.successful(BattleplanMessage(char_id, player, zone_id, list.toList))
},
{
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
val layersOpt : Option[BattleDiagramChain] = unrollDiagramLayers(diagrams.reverseIterator)
Attempt.successful(char_id :: player_name :: zone_id :: diagrams.size :: layersOpt :: HNil)
}
)
}

View file

@ -0,0 +1,26 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec
/**
* Dispatched by the client after the current map has been fully loaded locally and its objects are ready to be initialized.<br>
* <br>
* When the server receives the packet, for each object on that map, it sends the packets to the client:<br>
* - `SetEmpireMessage`<br>
* - `HackMessage`<br>
* - `PlanetSideAttributeMessage`<br>
* - ... and so forth<br>
* Afterwards, an avatar POV is declared and the remaining details about the said avatar are assigned.
*/
final case class BeginZoningMessage()
extends PlanetSideGamePacket {
type Packet = BeginZoningMessage
def opcode = GamePacketOpcode.BeginZoningMessage
def encode = BeginZoningMessage.encode(this)
}
object BeginZoningMessage extends Marshallable[BeginZoningMessage] {
implicit val codec : Codec[BeginZoningMessage] = PacketHelpers.emptyCodec(BeginZoningMessage())
}

View file

@ -2,19 +2,11 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideEmpire
import net.psforever.types.{CharacterGender, PlanetSideEmpire}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
object CharacterGender extends Enumeration(1) {
type Type = Value
val Male, Female = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
}
/**
* Is sent by the PlanetSide client on character selection completion.
*/

View file

@ -0,0 +1,43 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
/**
* Dispatched from a client when its user is controlling a secondary object whose state must be updated.<br>
* <br>
* When `ChildObjectStateMessage` is being sent to the server, it replaces `PlayerStateMessage`.
* The packet frequently gets hidden in a `MultiPacket`, though it is not functionally essential to do that.<br>
* <br>
* Note the lack of position data.
* The secondary object in question is updated in position through another means or is stationary.
* The only concern is the direction the object is facing.
* The angles are relative to the object's normal forward-facing and typically begin tracking at 0, 0 (forward-facing).
* @param object_guid the object being manipulated (controlled)
* @param pitch the angle with respect to the sky and the ground towards which the object is directed;
* an 8-bit unsigned value;
* 0 is perfectly level and forward-facing and mapped to 255;
* positive rotation is downwards from forward-facing
* @param yaw the angle with respect to the horizon towards which the object is directed;
* an 8-bit unsigned value;
* 0 is forward-facing, wrapping around at 127;
* positive rotation is counter-clockwise of forward-facing
*/
final case class ChildObjectStateMessage(object_guid : PlanetSideGUID,
pitch : Int,
yaw : Int)
extends PlanetSideGamePacket {
type Packet = ChildObjectStateMessage
def opcode = GamePacketOpcode.ChildObjectStateMessage
def encode = ChildObjectStateMessage.encode(this)
}
object ChildObjectStateMessage extends Marshallable[ChildObjectStateMessage] {
implicit val codec : Codec[ChildObjectStateMessage] = (
("object_guid" | PlanetSideGUID.codec) ::
("pitch" | uint8L) ::
("yaw" | uint8L)
).as[ChildObjectStateMessage]
}

View file

@ -0,0 +1,43 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.Vector3
import scodec.Codec
import scodec.codecs._
/**
* For completion's sake.
* We've never actually sent or received this packet during session captures on Gemini Live.
* @param guid na
* @param unk1 na
* @param pos na
* @param unk2 na
* @param unk3 na
* @param unk4 na
* @param unk5 na
*/
final case class DeployObjectMessage(guid : PlanetSideGUID,
unk1 : Long,
pos : Vector3,
unk2 : Int,
unk3 : Int,
unk4 : Int,
unk5 : Long)
extends PlanetSideGamePacket {
type Packet = DeployObjectMessage
def opcode = GamePacketOpcode.DeployObjectMessage
def encode = DeployObjectMessage.encode(this)
}
object DeployObjectMessage extends Marshallable[DeployObjectMessage] {
implicit val codec : Codec[DeployObjectMessage] = (
("guid1" | PlanetSideGUID.codec) ::
("unk1" | uint32L) ::
("pos" | Vector3.codec_pos) ::
("unk2" | uint8L) ::
("unk3" | uint8L) ::
("unk4" | uint8L) ::
("unk5" | uint32L)
).as[DeployObjectMessage]
}

View file

@ -0,0 +1,109 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.Vector3
import scodec.Codec
import scodec.codecs._
/**
* An `Enumeration` of the actions that can be performed upon a deployable item.
*/
object DeploymentAction extends Enumeration {
type Type = Value
val Dismiss,
Build
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(1)) //no bool overload
}
/**
* An `Enumeration` of the map element icons that can be displayed based on the type of deployable item.
*/
object DeployableIcon extends Enumeration {
type Type = Value
val Boomer,
HEMine,
MotionAlarmSensor,
SpitfireTurret,
RouterTelepad,
DisruptorMine,
ShadowTurret,
CerebusTurret,
TRAP,
AegisShieldGenerator,
FieldTurret,
SensorDisruptor
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}
/**
* The entry of a deployable item.
* @param object_guid the deployable item
* @param icon the map element depicting the item
* @param pos the position of the deployable in the world (and on the map)
* @param player_guid the player who is the owner
*/
final case class DeployableInfo(object_guid : PlanetSideGUID,
icon : DeployableIcon.Value,
pos : Vector3,
player_guid : PlanetSideGUID)
/**
* Dispatched by the server to inform the client of a change in deployable items and that the map should be updated.<br>
* <br>
* When this packet defines a `Build` `action`, an icon of the deployable item is added to the avatar's map.
* The actual object referenced does not have to actually exist on the client for the map element to appear.
* The identity of the element is discerned from its icon rather than the actual object (if it exists).
* When this packet defines a `Deconstruct` `action`, the icon of the deployable item is removed from the avatar's map.
* (The map icon to be removed is located by searching for the matching UID.
* The item does not need to exist to remove its icon.)<br>
* <br>
* All deployables have a map-icon-menu that allows for control of and some feedback about the item.
* At the very least, the item can be dismissed.
* The type of icon indicating the type of deployable item determines the map-icon-menu.
* Normally, the icon of a random (but friendly) deployable is gray and the menu is unresponsive.
* If the `player_guid` matches the client's avatar, the icon is yellow and that marks that the avatar owns this item.
* The avatar is capable of accessing the item's map-icon-menu and manipulating the item from there.
* If the deployable item actually doesn't exist, feedback is disabled, e.g., Aegis Shield Generators lack upgrade information.
* @param action how the entries in the following `List` are affected
* @param deployables a `List` of information regarding deployable items
*/
final case class DeployableObjectsInfoMessage(action : DeploymentAction.Value,
deployables : List[DeployableInfo]
) extends PlanetSideGamePacket {
type Packet = DeployableObjectsInfoMessage
def opcode = GamePacketOpcode.DeployableObjectsInfoMessage
def encode = DeployableObjectsInfoMessage.encode(this)
}
object DeployableObjectsInfoMessage extends Marshallable[DeployableObjectsInfoMessage] {
/**
* Overloaded constructor that accepts a single `DeployableInfo` entry (and turns it into a `List`).
* @param action how the following entry is affected
* @param info the singular entry of a deployable item
* @return a `DeployableObjectsInfoMessage` object
*/
def apply(action : DeploymentAction.Type, info : DeployableInfo) : DeployableObjectsInfoMessage =
new DeployableObjectsInfoMessage(action, info :: Nil)
/**
* `Codec` for `DeployableInfo` data.
*/
private val info_codec : Codec[DeployableInfo] = (
("object_guid" | PlanetSideGUID.codec) ::
("icon" | DeployableIcon.codec) ::
("pos" | Vector3.codec_pos) ::
("player_guid" | PlanetSideGUID.codec)
).as[DeployableInfo]
implicit val codec : Codec[DeployableObjectsInfoMessage] = (
("action" | DeploymentAction.codec) ::
("deployables" | PacketHelpers.listOfNAligned(uint32L, 0, info_codec))
).as[DeployableObjectsInfoMessage]
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.Vector3
import scodec.Codec
import scodec.codecs._
/**
* na
* @param seq_time na
* @param player na
* @param victim na
* @param bullet na
* @param pos na
* @param unk1 na
*/
final case class LashMessage(seq_time : Int,
player : PlanetSideGUID,
victim : PlanetSideGUID,
bullet : PlanetSideGUID,
pos : Vector3,
unk1 : Int)
extends PlanetSideGamePacket {
type Packet = LashMessage
def opcode = GamePacketOpcode.LashMessage
def encode = LashMessage.encode(this)
}
object LashMessage extends Marshallable[LashMessage] {
implicit val codec : Codec[LashMessage] = (
("seq_time" | uintL(10)) ::
("player" | PlanetSideGUID.codec) ::
("victim" | PlanetSideGUID.codec) ::
("bullet" | PlanetSideGUID.codec) ::
("pos" | Vector3.codec_pos) ::
("unk1" | uintL(3))
).as[LashMessage]
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
/**
* Dispatched from the server, sending a "priority message" to the given client's avatar.
* The messaging inbox is generally accessible through the use of `alt`+`i`.
* It is also made accessible through use of an icon in the lower right corner when there is an outstanding message.<br>
* <br>
* Exploration:<br>
* How does the PlanetSide Classic mail system work?
* At the moment, it only seems possible to receive and read mail from the server.
* @param sender the name of the player who sent the mail
* @param subject the subject
* @param message the message
*/
final case class MailMessage(sender : String,
subject : String,
message : String
) extends PlanetSideGamePacket {
type Packet = MailMessage
def opcode = GamePacketOpcode.MailMessage
def encode = MailMessage.encode(this)
}
object MailMessage extends Marshallable[MailMessage] {
implicit val codec : Codec[MailMessage] = (
("sender" | PacketHelpers.encodedString) ::
("subject" | PacketHelpers.encodedString) ::
("message" | PacketHelpers.encodedString)
).as[MailMessage]
}

View file

@ -0,0 +1,113 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, ObjectCreateBase, ObjectCreateMessageParent}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* Communicate with the client that a certain object with certain properties is to be created.
* In general, `ObjectCreateMessage` and its counterpart `ObjectCreateDetailedMessage` should look similar.<br>
* <br>
* In normal packet data order, the parent object is specified before the actual object is specified.
* This is most likely a method of early correction.
* "Does this parent object exist?"
* "Is this new object something that can be attached to this parent?"
* "Does the parent have the appropriate attachment slot?"
* There is no fail-safe method for any of these circumstances being false, however, and the object will simply not be created.
* In instance where the parent data does not exist, the object-specific data is immediately encountered.<br>
* <br>
* The object's GUID is assigned by the server.
* The clients are required to adhere to this new GUID referring to the object.
* There is no fail-safe for a conflict between what the server thinks is a new GUID and what any client thinks is an already-assigned GUID.
* Likewise, there is no fail-safe between a client failing or refusing to create an object and the server thinking an object has been created.
* (The GM-level command `/sync` tests for objects that "do not match" between the server and the client.
* It's implementation and scope are undefined.)<br>
* <br>
* Knowing the object's type is essential for parsing the specific information passed by the `data` parameter.
* If the object does not have encoding information or is unknown, it will not translate between byte data and a game object.
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentInfo if defined, the relationship between this object and another object (its parent)
* @param data the data used to construct this type of object;
* on decoding, set to `None` if the process failed
*/
final case class ObjectCreateDetailedMessage(streamLength : Long,
objectClass : Int,
guid : PlanetSideGUID,
parentInfo : Option[ObjectCreateMessageParent],
data : Option[ConstructorData])
extends PlanetSideGamePacket {
type Packet = ObjectCreateDetailedMessage
def opcode = GamePacketOpcode.ObjectCreateMessage
def encode = ObjectCreateDetailedMessage.encode(this)
}
object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMessage] {
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields.
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentInfo the relationship between this object and another object (its parent)
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
*/
def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateDetailedMessage =
ObjectCreateDetailedMessage(0L, objectClass, guid, Some(parentInfo), Some(data))
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`.
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
*/
def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateDetailedMessage =
ObjectCreateDetailedMessage(0L, objectClass, guid, None, Some(data))
/**
* Take the important information of a game piece and transform it into bit data.
* This function is fail-safe because it catches errors involving bad parsing of the object data.
* Generally, the `Exception` messages themselves are not useful here.
* @param objClass the code for the type of object being deconstructed
* @param obj the object data
* @return the bitstream data
* @see ObjectClass.selectDataCodec
*/
def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = {
var out = BitVector.empty
try {
val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption
if(outOpt.isDefined)
out = outOpt.get
}
catch {
case _ : Exception =>
//catch and release, any sort of parse error
}
out
}
implicit val codec : Codec[ObjectCreateDetailedMessage] = ObjectCreateBase.baseCodec.exmap[ObjectCreateDetailedMessage] (
{
case _ :: _ :: _ :: _ :: BitVector.empty :: HNil =>
Attempt.failure(Err("no data to decode"))
case len :: cls :: guid :: par :: data :: HNil =>
val obj = ObjectCreateBase.decodeData(cls, data, ObjectClass.selectDataDetailedCodec)
Attempt.successful(ObjectCreateDetailedMessage(len, cls, guid, par, obj))
},
{
case ObjectCreateDetailedMessage(_ , _ , _, _, None) =>
Attempt.failure(Err("no object to encode"))
case ObjectCreateDetailedMessage(_, cls, guid, par, Some(obj)) =>
val len = ObjectCreateBase.streamLen(par, obj) //even if a stream length has been assigned, it can not be trusted during encoding
val bitvec = ObjectCreateBase.encodeData(cls, obj, ObjectClass.selectDataDetailedCodec)
Attempt.successful(len :: cls :: guid :: par :: bitvec :: HNil)
}
)
}

View file

@ -1,53 +1,50 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.packet.game.objectcreate._
import scodec.{Attempt, Codec, Err}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* The parent information of a created object.<br>
* <br>
* Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child.
* The parent is a pre-existing object into which the (created) child is attached.
* The slot is encoded as a string length integer, following PlanetSide Classic convention for slot numbering.
* It is either a 0-127 eight bit number, or a 128-32767 sixteen bit number.
* @param guid the GUID of the parent object
* @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent
*/
final case class ObjectCreateMessageParent(guid : PlanetSideGUID,
slot : Int)
/**
* Communicate with the client that a certain object with certain properties is to be created.
* The object may also have primitive assignment (attachment) properties.<br>
* In general, `ObjectCreateMessage` and its counterpart `ObjectCreateDetailedMessage` should look similar.<br>
* <br>
* In normal packet data order, the parent object is specified before the actual object is specified.
* This is most likely a method of early correction.
* "Does this parent object exist?"
* "Is this new object something that can be attached to this parent?"
* "Does the parent have the appropriate attachment slot?"
* There is no fail-safe method for any of these circumstances being false, however, and the object will simply not be created.
* In instance where the parent data does not exist, the object-specific data is immediately encountered.<br>
* `ObjectCreateMessage` is capable of creating every non-environmental object in the game through the use of encoding patterns.
* The objects produced by this packet generally do not always fully express all the complexities of the object class.
* With respect to a client's avatar, all of the items in his inventory are given thorough detail so that the client can account for their interaction.
* The "shallow" objects produced by this packet are not like that.
* They express only the essential information necessary for client interaction when the client interacts with them.
* For example, a weapon defined by this packet may not care internally what fire mode it is in or how much ammunition it has.
* Such a weapon is not in the client's player's holster or inventory.
* It is imperceptive information to which he would not currently have access.
* An `0x17` game object is, therefore, a game object with only the essential data exposed.<br>
* <br>
* The object's GUID is assigned by the server.
* The clients are required to adhere to this new GUID referring to the object.
* There is no fail-safe for a conflict between what the server thinks is a new GUID and what any client thinks is an already-assigned GUID.
* Likewise, there is no fail-safe between a client failing or refusing to create an object and the server thinking an object has been created.
* (The GM-level command `/sync` tests for objects that "do not match" between the server and the client.
* It's implementation and scope are undefined.)<br>
* When interacting with an `0x17` game object, the server will swap back and forth between it and an `0x18` object.
* (Or it will be removed when it is placed somewhere a given client will no longer be able to see it.)
* The purpose of this conversion is to control network traffic and object agency.
* It is not necessary to keep track of all objects on every player on every client individually.
* This relates to the goal of this packet exposing only "essential data."
* One player does not need to know how much ammunition remains in a weapon belonging to another player normally.
* One player also does not need to know how much ammunition is used up when another player reloads their weapon.
* The only way the first player will know is when the weapon is transferred into his own inventory.
* All other clients are spared micromanagement of the hypothetical other player's weapon.
* Updated information is only made available when and where it is needed.<br>
* <br>
* Knowing the object's class is essential for parsing the specific information passed by the `data` parameter.
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
* @param objectClass the code for the type of object being constructed
* Knowing the object's type is necessary for proper parsing.
* If the object does not have encoding information or is unknown, it will not translate between byte data and a game object.
* @param streamLength the total length of the data that composes this packet in bits;
* exclude the opcode (1 byte) and end padding (0-7 bits);
* when encoding, it will be calculated automatically
* @param objectClass the code for the type of object being constructed;
* always an 11-bit LE value
* @param guid the GUID this object will be assigned
* @param parentInfo if defined, the relationship between this object and another object (its parent)
* @param data the data used to construct this type of object;
* on decoding, set to `None` if the process failed
* @see ObjectClass.selectDataCodec
* @see ObjectCreateDetailedMessage
* @see ObjectCreateMessageParent
*/
final case class ObjectCreateMessage(streamLength : Long,
objectClass : Int,
@ -55,185 +52,67 @@ final case class ObjectCreateMessage(streamLength : Long,
parentInfo : Option[ObjectCreateMessageParent],
data : Option[ConstructorData])
extends PlanetSideGamePacket {
def opcode = GamePacketOpcode.ObjectCreateMessage
type Packet = ObjectCreateMessage
def opcode = GamePacketOpcode.ObjectCreateMessage_Duplicate
def encode = ObjectCreateMessage.encode(this)
}
object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields.
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
* An abbreviated constructor for creating `ObjectCreateMessage`s, ignoring the optional aspect of some fields.
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentInfo the relationship between this object and another object (its parent)
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
* @return an `ObjectCreateMessage`
*/
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data))
def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage = {
val parentInfoOpt : Option[ObjectCreateMessageParent] = Some(parentInfo)
ObjectCreateMessage(ObjectCreateBase.streamLen(parentInfoOpt, data), objectClass, guid, parentInfoOpt, Some(data))
}
/**
* An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`.
* @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
* An abbreviated constructor for creating `ObjectCreateMessage`s, calculating `streamLen` and ignoring `parentInfo`.
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param data the data used to construct this type of object
* @return an ObjectCreateMessage
* @return an `ObjectCreateMessage`
*/
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, None, Some(data))
type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil
/**
* Codec for formatting around the lack of parent data in the stream.
*/
private val noParent : Codec[Pattern] = (
("objectClass" | uintL(0xb)) :: //11u
("guid" | PlanetSideGUID.codec) //16u
).xmap[Pattern](
{
case cls :: guid :: HNil =>
cls :: guid :: None :: HNil
}, {
case cls :: guid :: None :: HNil =>
cls :: guid :: HNil
}
)
/**
* Codec for reading and formatting parent data from the stream.
*/
private val parent : Codec[Pattern] = (
("parentGuid" | PlanetSideGUID.codec) :: //16u
("objectClass" | uintL(0xb)) :: //11u
("guid" | PlanetSideGUID.codec) :: //16u
("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u
).xmap[Pattern](
{
case pguid :: cls :: guid :: slot :: HNil =>
cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil
}, {
case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil =>
pguid :: cls :: guid :: slot :: HNil
}
)
/**
* Take bit data and transform it into an object that expresses the important information of a game piece.
* This function is fail-safe because it catches errors involving bad parsing of the bitstream data.
* Generally, the `Exception` messages themselves are not useful here.
* The important parts are what the packet thought the object class should be and what it actually processed.
* @param objectClass the code for the type of object being constructed
* @param data the bitstream data
* @return the optional constructed object
*/
private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = {
var out : Option[ConstructorData] = None
try {
val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(data).toOption
if(outOpt.isDefined)
out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern]
}
catch {
case ex : Exception =>
//catch and release, any sort of parse error
}
out
def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage = {
ObjectCreateMessage(ObjectCreateBase.streamLen(None, data), objectClass, guid, None, Some(data))
}
/**
* Take the important information of a game piece and transform it into bit data.
* This function is fail-safe because it catches errors involving bad parsing of the object data.
* Generally, the `Exception` messages themselves are not useful here.
* @param objClass the code for the type of object being deconstructed
* @param obj the object data
* @return the bitstream data
*/
private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = {
var out = BitVector.empty
try {
val outOpt : Option[BitVector] = ObjectClass.selectDataCodec(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption
if(outOpt.isDefined)
out = outOpt.get
}
catch {
case ex : Exception =>
//catch and release, any sort of parse error
}
out
}
/**
* Calculate the stream length in number of bits by factoring in the whole message in two portions.
* This process automates for: object encoding.<br>
* <br>
* Ignoring the parent data, constant field lengths have already been factored into the results.
* That includes:
* the length of the stream length field (32u),
* the object's class (11u),
* the object's GUID (16u),
* and the bit to determine if there will be parent data.
* In total, these fields form a known fixed length of 60u.
* @param parentInfo if defined, the relationship between this object and another object (its parent);
* information about the parent adds either 24u or 32u
* @param data if defined, the data used to construct this type of object;
* the data length is indeterminate until it is walked-through;
* note: the type is `StreamBitSize` as opposed to `ConstructorData`
* @return the total length of the resulting data stream in bits
*/
private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : StreamBitSize) : Long = {
//knowable length
val base : Long = if(parentInfo.isDefined) {
if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u))
}
else {
60L
}
base + data.bitsize
}
implicit val codec : Codec[ObjectCreateMessage] = (
("streamLength" | uint32L) ::
(either(bool, parent, noParent).exmap[Pattern] (
{
case Left(a :: b :: Some(c) :: HNil) =>
Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c)
case Right(a :: b :: None :: HNil) =>
Attempt.successful(a :: b :: None :: HNil) //false, _, _, None
// failure cases
case Left(a :: b :: None :: HNil) =>
Attempt.failure(Err("missing parent structure")) //true, _, _, None
case Right(a :: b :: Some(c) :: HNil) =>
Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c)
}, {
case a :: b :: Some(c) :: HNil =>
Attempt.successful(Left(a :: b :: Some(c) :: HNil))
case a :: b :: None :: HNil =>
Attempt.successful(Right(a :: b :: None :: HNil))
}
) :+
("data" | bits)) //greed is good
).exmap[outPattern] (
implicit val codec : Codec[ObjectCreateMessage] = ObjectCreateBase.baseCodec.exmap[ObjectCreateMessage] (
{
case _ :: _ :: _ :: _ :: BitVector.empty :: HNil =>
Attempt.failure(Err("no data to decode"))
case len :: cls :: guid :: par :: data :: HNil =>
Attempt.successful(len :: cls :: guid :: par :: decodeData(cls, data) :: HNil)
val obj = ObjectCreateBase.decodeData(cls, data,
if(par.isDefined) {
ObjectClass.selectDataCodec
}
else {
ObjectClass.selectDataDroppedCodec
}
)
Attempt.successful(ObjectCreateMessage(len, cls, guid, par, obj))
},
{
case _ :: _ :: _ :: _ :: None :: HNil =>
case ObjectCreateMessage(_ , _ , _, _, None) =>
Attempt.failure(Err("no object to encode"))
case _ :: cls :: guid :: par :: Some(obj) :: HNil =>
Attempt.successful(streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil)
case ObjectCreateMessage(_, cls, guid, par, Some(obj)) =>
val len = ObjectCreateBase.streamLen(par, obj) //even if a stream length has been assigned, it can not be trusted during encoding
val bitvec = ObjectCreateBase.encodeData(cls, obj,
if(par.isDefined) {
ObjectClass.selectDataCodec
}
else {
ObjectClass.selectDataDroppedCodec
}
)
Attempt.successful(len :: cls :: guid :: par :: bitvec :: HNil)
}
).xmap[ObjectCreateMessage] (
{
case len :: cls :: guid :: par :: obj :: HNil =>
ObjectCreateMessage(len, cls, guid, par, obj)
},
{
case ObjectCreateMessage(len, cls, guid, par, obj) =>
len :: cls :: guid :: par :: obj :: HNil
}
).as[ObjectCreateMessage]
)
}

View file

@ -0,0 +1,89 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
/**
* An `Enumeration` for the forms of the event chat message produced by this packet.
*/
object DeploymentOutcome extends Enumeration(1) {
type Type = Value
val Failure = Value(2)
//3 produces a Success message, but 4 is common
val Success = Value(4)
val codec = PacketHelpers.createLongEnumerationCodec(this, uint32L)
}
/**
* Dispatched by the server to generate a message in the events chat when placing deployables.<br>
* <br>
* This packet does not actually modify anything in regards to deployables.
* The most common form of the generated message is:<br>
* `"You have placed x of a possible y thing s."`<br>
* ... where `x` is the current count of objects of this type that have been deployed;
* `y` is the (reported) maximum amount of objects of this type that can be deployed;
* and, `thing` is the token for objects of this type.
* If the `thing` is a valid string token, it will be replaced by language-appropriate descriptive text in the message.
* Otherwise, that text is placed directly into the message, with an obvious space between the text and the "s".
* "boomer," for example, is replaced by "Boomer Heavy Explosives" in the message for English language.
* "bullet_9mm_AP," however, is just "bullet_9mm_AP s."<br>
* <br>
* When the `action` is `Success`, the message in the chat will be shown as above.
* When the `action` is `Failure`, the message will be:<br>
* `"thing failed to deploy and was destroyed."`<br>
* ... where, again, `thing` is a valid string token.
* @param unk na;
* usually 0?
* @param desc descriptive text of what kind of object is being deployed;
* string token of the object, at best
* @param action the form the message will take
* @param count the current number of this type of object deployed
* @param max the maximum number of this type of object that can be deployed
*/
final case class ObjectDeployedMessage(unk : Int,
desc : String,
action : DeploymentOutcome.Value,
count : Long,
max : Long)
extends PlanetSideGamePacket {
type Packet = ObjectDeployedMessage
def opcode = GamePacketOpcode.ObjectDeployedMessage
def encode = ObjectDeployedMessage.encode(this)
}
object ObjectDeployedMessage extends Marshallable[ObjectDeployedMessage] {
/**
* Overloaded constructor for when the guid is not required.
* @param desc descriptive text of what kind of object is being deployed
* @param action na
* @param count the number of this type of object deployed
* @param max the maximum number of this type of object that can be deployed
* @return an `ObjectDeployedMessage` object
*/
def apply(desc : String, action : DeploymentOutcome.Value, count : Long, max : Long) : ObjectDeployedMessage =
new ObjectDeployedMessage(0, desc, action, count, max)
implicit val codec : Codec[ObjectDeployedMessage] = (
("unk" | uint16L) ::
("desc" | PacketHelpers.encodedString) ::
("action" | DeploymentOutcome.codec) ::
("count" | uint32L) ::
("max" | uint32L)
).xmap[ObjectDeployedMessage] (
{
case guid :: str :: unk :: cnt ::mx :: HNil =>
ObjectDeployedMessage(guid, str, unk, cnt, mx)
},
{
case ObjectDeployedMessage(guid, str, unk, cnt, mx) =>
//truncate string length to 100 characters; raise no warnings
val limitedStr : String = if(str.length() > 100) { str.substring(0,100) } else { str }
guid :: limitedStr :: unk :: cnt :: mx :: HNil
}
)
}

View file

@ -0,0 +1,21 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec
/** Packet send by client when clic on button after death
* https://streamable.com/4r16m
*/
final case class ReleaseAvatarRequestMessage()
extends PlanetSideGamePacket {
type Packet = ReleaseAvatarRequestMessage
def opcode = GamePacketOpcode.ReleaseAvatarRequestMessage
def encode = ReleaseAvatarRequestMessage.encode(this)
}
object ReleaseAvatarRequestMessage extends Marshallable[ReleaseAvatarRequestMessage] {
implicit val codec : Codec[ReleaseAvatarRequestMessage] = PacketHelpers.emptyCodec(ReleaseAvatarRequestMessage())
}

View file

@ -104,7 +104,7 @@ final case class SquadListing(index : Int = 255,
* `behavior behavior2`<br>
* `1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Update where initial entry removes a squad from the list<br>
* `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list and initialize new squad list<br>
* `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list (ransitions directly into 255-entry)<br>
* `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list (transitions directly into 255-entry)<br>
* `6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `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;

View file

@ -0,0 +1,37 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
/**
* Dispatched by the server to render a "damage cloud" around a target.<br>
* <br>
* Exploration:<br>
* This is not common but it happened while on Gemini Live.
* Why does it happen?
* @param unk1 na;
* usually 2;
* when 2, will generate a short dust cloud around the `target_guid`;
* if a player, will cause the "damage grunt animation" to occur, whether or not there is a dust cloud
* @param target_guid the target around which to generate the temporary damage effect
* @param unk2 na;
* usually 5L
*/
final case class TriggerEnvironmentalDamageMessage(unk1 : Int,
target_guid : PlanetSideGUID,
unk2 : Long)
extends PlanetSideGamePacket {
type Packet = TriggerEnvironmentalDamageMessage
def opcode = GamePacketOpcode.TriggerEnvironmentalDamageMessage
def encode = TriggerEnvironmentalDamageMessage.encode(this)
}
object TriggerEnvironmentalDamageMessage extends Marshallable[TriggerEnvironmentalDamageMessage] {
implicit val codec : Codec[TriggerEnvironmentalDamageMessage] = (
("unk1" | uint2L) ::
("target_guid" | PlanetSideGUID.codec) ::
("unk2" | uint32L)
).as[TriggerEnvironmentalDamageMessage]
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of an adaptive construction engine (ACE).
* This one-time-use item deploys a variety of utilities into the game environment.
* Has an advanced version internally called an `advanced_ace` and commonly called a Field Deployment Unit (FDU).
* @param unk1 na
* @param unk2 na
* @param unk3 na
*/
final case class ACEData(unk1 : Int,
unk2 : Int,
unk3 : Int = 0
) extends ConstructorData {
override def bitsize : Long = 34L
}
object ACEData extends Marshallable[ACEData] {
implicit val codec : Codec[ACEData] = (
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
("unk3" | uint4L) ::
uint2L
).exmap[ACEData] (
{
case unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil =>
Attempt.successful(ACEData(unk1, unk2, unk3))
case _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid ace data format"))
},
{
case ACEData(unk1, unk2, unk3) =>
Attempt.successful(unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,77 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* Data that is common to a number of items that are spawned by the adaptive construction engine, or its advanced version.
* @param pos where and how the object is oriented
* @param unk na
* @param player_guid the player who placed this object
*/
final case class ACEDeployableData(pos : PlacementData,
unk : Int,
player_guid : PlanetSideGUID
) extends StreamBitSize {
override def bitsize : Long = 23L + pos.bitsize
}
object ACEDeployableData extends Marshallable[ACEDeployableData] {
final val internalWeapon_bitsize : Long = 10
/**
* `Codec` for transforming reliable `WeaponData` from the internal structure of the turret when it is defined.
* Works for both `SmallTurretData` and `OneMannedFieldTurretData`.
*/
val internalWeaponCodec : Codec[InternalSlot] = (
uint8L :: //number of internal weapons (should be 1)?
uint2L ::
InternalSlot.codec
).exmap[InternalSlot] (
{
case 1 :: 0 :: InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)) :: HNil =>
Attempt.successful(InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)))
case 1 :: 0 :: InternalSlot(_, _, _, _) :: HNil =>
Attempt.failure(Err(s"turret internals must contain weapon data"))
case n :: 0 :: _ :: HNil =>
Attempt.failure(Err(s"turret internals can not have $n weapons"))
case _ =>
Attempt.failure(Err("invalid turret internals data format"))
},
{
case InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)) =>
Attempt.successful(1 :: 0 :: InternalSlot(a1, b1, c1, WeaponData(a2, b2, c2, d)) :: HNil)
case InternalSlot(_, _, _, _) =>
Attempt.failure(Err(s"turret internals must contain weapon data"))
case _ =>
Attempt.failure(Err("invalid turret internals data format"))
}
)
implicit val codec : Codec[ACEDeployableData] = (
("pos" | PlacementData.codec) ::
("unk1" | uint(7)) ::
("player_guid" | PlanetSideGUID.codec)
).exmap[ACEDeployableData] (
{
case pos :: unk :: player :: HNil =>
Attempt.successful(ACEDeployableData(pos, unk, player))
case _ =>
Attempt.failure(Err("invalid deployable data format"))
},
{
case ACEDeployableData(pos, unk, player) =>
Attempt.successful(pos :: unk :: player :: HNil)
}
)
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the aegis shield generator deployed using an advanced adaptive construction engine.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
*/
final case class AegisShieldGeneratorData(deploy : ACEDeployableData,
health : Int
) extends ConstructorData {
override def bitsize : Long = {
108 + deploy.bitsize //8u + 100u
}
}
object AegisShieldGeneratorData extends Marshallable[AegisShieldGeneratorData] {
implicit val codec : Codec[AegisShieldGeneratorData] = (
("deploy" | ACEDeployableData.codec) ::
("health" | uint8L) ::
uint32 :: uint32 :: uint32 :: uint4L //100 bits
).exmap[AegisShieldGeneratorData] (
{
case deploy :: health :: 0 :: 0 :: 0 :: 0 :: HNil =>
Attempt.successful(AegisShieldGeneratorData(deploy, health))
case _ =>
Attempt.failure(Err("invalid aegis data format"))
},
{
case AegisShieldGeneratorData(deploy, health) =>
Attempt.successful(deploy :: health :: 0L :: 0L :: 0L :: 0 :: HNil)
}
)
}

View file

@ -8,68 +8,46 @@ import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the ammunition portion of `ObjectCreateMessage` packet data.
* A representation of ammunition that can be created using `ObjectCreateMessage` packet data.
* This data will help construct a "box" of that type of ammunition when standalone.
* It can also be constructed directly inside a weapon as its magazine.<br>
* <br>
* The maximum amount of ammunition that can be stored in a single box is 65535 units.
* Regardless of the interface, however, the number will never be fully visible.
* Only the first three digits or the first four digits may be represented.
* @param magazine the number of rounds available
* @see WeaponData
* This ammunition object ompletely ignores thr capacity field, normal to detailed ammunition objects.
* Creating an object of this type directly and picking it up or observing it (in a weapon) reveals a single round.
* @param unk na;
* defaults to 0
* @see `DetailedAmmoBoxData`
*/
final case class AmmoBoxData(magazine : Int) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = 40L
final case class AmmoBoxData(unk : Int = 0) extends ConstructorData {
override def bitsize : Long = 24L
}
object AmmoBoxData extends Marshallable[AmmoBoxData] {
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot`.
* An abbreviated constructor for creating `AmmoBoxData` while masking use of `InternalSlot`.
* @param cls the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param ammo the `AmmoBoxData`
* @param ammo the ammunition object
* @return an `InternalSlot` object that encapsulates `AmmoBoxData`
*/
def apply(cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : InternalSlot =
new InternalSlot(cls, guid, parentSlot, ammo)
implicit val codec : Codec[AmmoBoxData] = (
uint8L ::
uint(15) ::
("magazine" | uint16L) ::
bool
).exmap[AmmoBoxData] (
uint4L ::
("unk" | uint4L) ::
uint(16)
).exmap[AmmoBoxData] (
{
case 0xC8 :: 0 :: mag :: false :: HNil =>
Attempt.successful(AmmoBoxData(mag))
case a :: b :: _ :: d :: HNil =>
case 0xC :: unk :: 0 :: HNil =>
Attempt.successful(AmmoBoxData(unk))
case _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid ammunition data format"))
},
{
case AmmoBoxData(mag) =>
Attempt.successful(0xC8 :: 0 :: mag :: false:: HNil)
}
)
/**
* Transform between AmmoBoxData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[AmmoBoxData])
case _ =>
Attempt.failure(Err("can not encode ammo box data"))
case AmmoBoxData(unk) =>
Attempt.successful(0xC :: unk :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,34 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the detonator utility that is created when putting down a Boomer with an ACE.
* @param unk na
*/
final case class BoomerTriggerData(unk : Int = 0x8) extends ConstructorData {
override def bitsize : Long = 34L
}
object BoomerTriggerData extends Marshallable[BoomerTriggerData] {
implicit val codec : Codec[BoomerTriggerData] = (
uint4L ::
uint4L ::
uint(26)
).exmap[BoomerTriggerData] (
{
case 0xC :: unk :: 0 :: HNil =>
Attempt.successful(BoomerTriggerData(unk))
case _ =>
Attempt.failure(Err("invalid command detonater format"))
},
{
case BoomerTriggerData(unk) =>
Attempt.successful(0xC :: unk :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,61 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.types.PlanetSideEmpire
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the capture flag portion of `ObjectCreateDetailedMessage` packet data.
* This creates what is known as a lattice logic unit, or LLU.
* It is originally spawned in the base object called the lattice link socket during certain base captures.<br>
* <br>
* Players can not directly interact with the capture flag.
* Whenever an applicable player is nearby, that client will rapidly fire off `ItemUseMessage` packets to the server.
* The capture flag will be picked-up by the player and stored in a special slot that is not part of their inventory.
* A special dropping keybind has been prepared to relinquish the capture flag back to the game world.
* @param faction the empire whose players may interact with this capture flag
* @param unk1 na
* @param unk2 na
* @param unk3 na
* @param unk4 na
*/
final case class CaptureFlagData(pos : PlacementData,
faction : PlanetSideEmpire.Value,
unk1 : Int,
unk2 : Int,
unk3 : Int,
unk4 : Int
) extends ConstructorData {
override def bitsize : Long = 88L + pos.bitsize
}
object CaptureFlagData extends Marshallable[CaptureFlagData] {
implicit val codec : Codec[CaptureFlagData] = (
("pos" | PlacementData.codec) ::
("faction" | PlanetSideEmpire.codec) ::
bool ::
uint4L ::
uint16L ::
("unk1" | uint8L) ::
uint8L ::
("unk2" | uint8L) ::
uint8L ::
("unk3" | uint16L) :: //probably a PlanetSideGUID
("unk4" | uint8L) ::
uint(9)
).exmap[CaptureFlagData] (
{
case pos :: fac :: false :: 4 :: 0 :: unk1 :: 0 :: unk2 :: 0 :: unk3 :: unk4 :: 0 :: HNil =>
Attempt.Successful(CaptureFlagData(pos, fac, unk1, unk2, unk3, unk4))
case _ =>
Attempt.failure(Err("invalid capture flag data"))
},
{
case CaptureFlagData(pos, fac, unk1, unk2, unk3, unk4) =>
Attempt.successful(pos :: fac :: false :: 4 :: 0 :: unk1 :: 0 :: unk2 :: 0 :: unk3 :: unk4 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,220 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.types.{CharacterGender, ExoSuitType, GrenadeState, PlanetSideEmpire}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* <br>
* This partition of the data stream contains information used to represent how the player's avatar is presented.
* This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet.<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;no voice`
* @param name the unique name of the avatar;
* minimum of two characters
* @param faction the empire to which the avatar belongs
* @param sex whether the avatar is `Male` or `Female`
* @param head the avatar's face and hair;
* by row and column on the character creation screen, the high nibble is the row and the low nibble is the column
* @param voice the avatar's voice selection
* @see `PlanetSideEmpire`
* @see `CharacaterGender`
*/
final case class BasicCharacterData(name : String,
faction : PlanetSideEmpire.Value,
sex : CharacterGender.Value,
head : Int,
voice : Int)
/**
* A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.<br>
* <br>
* This is a shared partition of the data used to represent how the player's avatar is presented.
* It is utilized by both `0x17 ObjectCreateMessage CharacterData` and `0x18 ObjectCreateDetailedMessage DetailedCharacterData`.
* This can be considered the data that goes into creating the player's model.<br>
* <br>
* Only a few changes would occur depending on which packet would deal with the data.
* One example is `facingYawUpper` which, when depicting avatars, can be set to represent non-trivial turning angles.
* When depicting other players, it is limited to a small range of angles in the direction of that model's forward-facing.
* Another example is the outfit information: not usually represented for avatars; but, always represented for other players.<br>
* <br>
* One way the player's model can be changed dramatically involves being depicted as "released."
* In this form, their body appears as a backpack (or pumpkin or pastry) that can be looted for the equipment carried while alive.
* Companion data will describe how the player is represented while he is "dead," usually a requirement for being "released."
* Without that requirement here, it is possible to depicte the player as a "living backpack."
* The said equipment is also defined elsewhere.
* Another dramatic change replaces the player's model with a ball of plasma that masks the player while riding zip lines.<br>
* <br>
* Exploration:<br>
* How do I crouch?
* @param pos the position of the character in the world environment (in three coordinates)
* @param basic_appearance the player's cardinal appearance settings
* @param voice2 na;
* affects the frequency by which the character's voice is heard (somehow);
* commonly 3 for best results
* @param black_ops whether or not this avatar is enrolled in Black OPs
* @param jammered the player has been caught in an EMP blast recently;
* creates a jammered sound effect that follows the player around and can be heard by others
* @param exosuit the type of exo-suit the avatar will be depicted in;
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
* @param outfit_name the name of the outfit to which this player belongs;
* if the option is selected, allies with see either "[`outfit_name`]" or "{No Outfit}" under the player's name
* @param outfit_logo the decal seen on the player's exo-suit (and beret and cap) associated with the player's outfit;
* if there is a variable color for that decal, the faction-appropriate one is selected
* @param facingPitch the angle with respect to the sky and the ground towards which the avatar is looking
* @param facingYawUpper the angle of the avatar's upper body with respect to its forward-facing direction
* @param lfs this player is looking for a squad;
* all allies will see the phrase "[Looking for Squad]" under the player's name
* @param is_cloaking avatar is cloaked by virtue of an Infiltration Suit
* @param grenade_state if the player has a grenade `Primed`;
* should be `GrenadeStateState.None` if nothing special
* @param charging_pose animation pose for both charging modules and BFR imprinting
* @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line
* @param ribbons the four merit commendation ribbon medals
* @see `CharacterData`
* @see `DetailedCharacterData`
* @see `PlacementData`
* @see `ExoSuitType`
* @see `GrenadeState`
* @see `RibbonBars`
* @see `http://wiki.planetsidesyndicate.com/index.php?title=Outfit_Logo` for a list of outfit decals
*/
final case class CharacterAppearanceData(pos : PlacementData,
basic_appearance : BasicCharacterData,
voice2 : Int,
black_ops : Boolean,
jammered : Boolean,
exosuit : ExoSuitType.Value,
outfit_name : String,
outfit_logo : Int,
backpack : Boolean,
facingPitch : Int,
facingYawUpper : Int,
lfs : Boolean,
grenade_state : GrenadeState.Value,
is_cloaking : Boolean,
charging_pose : Boolean,
on_zipline : Boolean,
ribbons : RibbonBars) extends StreamBitSize {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val placementSize : Long = pos.bitsize
val nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.init_move)
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding
val altModelSize = if(on_zipline || backpack) { 1L } else { 0L }
335L + placementSize + nameStringSize + outfitStringSize + altModelSize
}
}
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
/**
* Get the padding of the player's name.
* The padding will always be a number 0-7.
* @return the pad length in bits
*/
def namePadding(move : Option[_]) : Int = {
if(move.isDefined) {
2
}
else {
4
}
}
/**
* Get the padding of the outfit's name.
* The padding will always be a number 0-7.
* @return the pad length in bits
*/
def outfitNamePadding : Int = {
6
}
implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | PlacementData.codec) >>:~ { pos =>
("faction" | PlanetSideEmpire.codec) ::
("black_ops" | bool) ::
(("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models)
ignore(1) :: //unknown
("jammered" | bool) ::
bool :: //crashes client
uint(16) :: //unknown, but usually 0
("name" | PacketHelpers.encodedWideStringAligned( namePadding(pos.init_move) )) ::
("exosuit" | ExoSuitType.codec) ::
ignore(2) :: //unknown
("sex" | CharacterGender.codec) ::
("head" | uint8L) ::
("voice" | uint(3)) ::
("voice2" | uint2L) ::
ignore(78) :: //unknown
uint16L :: //usually either 0 or 65535
uint32L :: //for outfit_name (below) to be visible in-game, this value should be non-zero
("outfit_name" | PacketHelpers.encodedWideStringAligned( outfitNamePadding )) ::
("outfit_logo" | uint8L) ::
ignore(1) :: //unknown
("backpack" | bool) :: //requires alt_model flag (does NOT require health == 0)
bool :: //stream misalignment when set
("facingPitch" | uint8L) ::
("facingYawUpper" | uint8L) ::
ignore(1) :: //unknown
conditional(alt_model, bool) :: //alt_model flag adds a bit before lfs
ignore(1) :: //an alternate lfs?
("lfs" | bool) ::
("grenade_state" | GrenadeState.codec_2u) :: //note: bin10 and bin11 are neutral (bin00 is not defined)
("is_cloaking" | bool) ::
ignore(1) :: //unknown
bool :: //stream misalignment when set
("charging_pose" | bool) ::
ignore(1) :: //alternate charging pose?
("on_zipline" | bool) :: //requires alt_model flag
("ribbons" | RibbonBars.codec)
})
}).exmap[CharacterAppearanceData] (
{
case _ :: _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil |
_ :: _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: HNil =>
Attempt.Failure(Err("invalid character appearance data; can not encode alternate model without required bit set"))
case pos :: faction :: bops :: _ :: _ :: jamd :: false :: 0 :: name :: suit :: _ :: sex :: head :: v1 :: v2 :: _ :: _ :: _/*has_outfit_name*/ :: outfit :: logo :: _ :: bpack :: false :: facingPitch :: facingYawUpper :: _ :: _ :: _ :: lfs :: gstate :: cloaking :: _ :: false :: charging :: _ :: zipline :: ribbons :: HNil =>
Attempt.successful(
CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons)
)
case _ =>
Attempt.Failure(Err("invalid character appearance data; can not encode"))
},
{
case CharacterAppearanceData(_, BasicCharacterData(name, PlanetSideEmpire.NEUTRAL, _, _, _), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) =>
Attempt.failure(Err(s"character $name's faction can not declare as neutral"))
case CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) =>
val has_outfit_name : Long = outfit.length.toLong //todo this is a kludge
var alt_model : Boolean = false
var alt_model_extrabit : Option[Boolean] = None
if(zipline || bpack) {
alt_model = true
alt_model_extrabit = Some(false)
}
Attempt.successful(
pos :: faction :: bops :: alt_model :: () :: jamd :: false :: 0 :: name :: suit :: () :: sex :: head :: v1 :: v2 :: () :: 0 :: has_outfit_name :: outfit :: logo :: () :: bpack :: false :: facingPitch :: facingYawUpper :: () :: alt_model_extrabit :: () :: lfs :: gstate :: cloaking :: () :: false :: charging :: () :: zipline :: ribbons :: HNil
)
case _ =>
Attempt.Failure(Err("invalid character appearance data; can not decode"))
}
)
}

View file

@ -2,402 +2,194 @@
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* Values for the implant effects on a character model.
* The effects can not be activated simultaneously.
* In at least one case, attempting to activate multiple effects will cause the PlanetSide client to crash.<br>
* <br>
* This partition of the data stream contains information used to represent how the player's avatar is presented.
* This appearance can be considered the avatar's obvious points beyond experience levels.
* It does not include passive exo-suit upgrades, battle rank 24 cosmetics, special postures, or current equipment.
* Those will occur later back in the main data stream.<br>
* <br>
* 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.
* <br>
* Exo-suit:<br>
* `0 - Agile`<br>
* `1 - Refinforced`<br>
* `2 - Mechanized Assault`<br>
* `3 - Infiltration`<br>
* `4 - Standard`<br>
* <br>
* Sex:<br>
* `0 - invalid`<br>
* `1 - Male`<br>
* `2 - Female`<br>
* `3 - invalid`<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;no voice`
* @param pos the position of the character in the world environment (in three coordinates)
* @param objYaw the angle with respect to the horizon towards which the object's front is facing;
* every `0x1` is 2.813 degrees counter clockwise from North;
* every `0x10` is 45-degrees;
* it wraps at `0x0` == `0x80` == North
* (note: references the avatar as a game object?)
* @param faction the empire to which the avatar belongs;
* the value scale is different from `PlanetSideEmpire`
* @param bops whether or not this avatar is enrolled in Black OPs
* @param unk1 na;
* defaults to 4
* @param name the wide character name of the avatar, minimum of two characters
* @param exosuit the type of exosuit the avatar will be depicted in;
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
* @param sex whether the avatar is male or female
* @param face1 the avatar's face, as by column number on the character creation screen
* @param face2 the avatar's face, as by row number on the character creation screen
* @param voice the avatar's voice selection
* @param unk2 na
* @param unk3 na;
* can be missing from the stream under certain conditions;
* see next
* @param unk4 na;
* can be missing from the stream under certain conditions;
* see previous
* @param unk5 na;
* defaults to `0x8080`
* @param unk6 na;
* defaults to `0xFFFF`;
* may be `0x0`
* @param unk7 na;
* defaults to 2
* @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking;
* only supports downwards view angles;
* `0x0` is forwards-facing;
* `0x20` to `0xFF` is downwards-facing
* @param viewYaw the angle with respect to the horizon towards which the avatar is looking;
* every `0x1` is 2.813 degrees counter clockwise from North;
* every `0x10` is 45-degrees;
* it wraps at `0x0` == `0x80` == North
* @param unk8 na
* @param ribbons the four merit commendation ribbon medals
* `RegenEffects` is a reverse-flagged item - inactive when the corresponding bit is set.
* For that reason, every other effect is `n`+1, while `NoEffects` is 1 and `RegenEffects` is 0.
*/
final case class CharacterAppearanceData(pos : Vector3,
objYaw : Int,
faction : PlanetSideEmpire.Value,
bops : Boolean,
unk1 : Int,
name : String,
exosuit : Int,
sex : Int,
face1 : Int,
face2 : Int,
voice : Int,
unk2 : Int,
unk3 : Int,
unk4 : Int,
unk5 : Int,
unk6 : Int,
unk7 : Int,
viewPitch : Int,
viewYaw : Int,
unk8 : Int,
ribbons : RibbonBars) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = {
//TODO ongoing analysis, this value will be subject to change
430L + CharacterData.stringBitSize(name, 16) + CharacterAppearanceData.namePadding
}
}
object ImplantEffects extends Enumeration {
type Type = Value
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
/**
* Get the padding of the avatar's name.
* The padding will always be a number 0-7.
* @return the pad length in bits
*/
private def namePadding : Int = {
//TODO the parameters for this function are not correct
//TODO the proper padding length should reflect all variability in the substream prior to this point
4
}
val SurgeEffects = Value(9)
val PersonalShieldEffects = Value(5)
val DarklightEffects = Value(3)
val RegenEffects = Value(0)
val NoEffects = Value(1)
implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | Vector3.codec_pos) ::
ignore(16) ::
("objYaw" | uint8L) ::
ignore(1) ::
("faction" | PlanetSideEmpire.codec) ::
("bops" | bool) ::
("unk1" | uint4L) ::
ignore(16) ::
("name" | PacketHelpers.encodedWideStringAligned( namePadding )) ::
("exosuit" | uintL(3)) ::
ignore(2) ::
("sex" | uint2L) ::
("face1" | uint4L) ::
("face2" | uint4L) ::
("voice" | uintL(3)) ::
("unk2" | uint2L) ::
ignore(4) ::
("unk3" | uint8L) ::
("unk4" | uint8L) ::
("unk5" | uint16L) ::
ignore(42) ::
("unk6" | uint16L) ::
ignore(30) ::
("unk7" | uint4L) ::
ignore(24) ::
("viewPitch" | uint8L) ::
("viewYaw" | uint8L) ::
("unk8" | uint4L) ::
ignore(6) ::
("ribbons" | RibbonBars.codec)
).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
)
}
)
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}
/**
* A representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* Values for the four different color designs that impact a player's uniform.
* Exo-suits get minor graphical updates at the following battle rank levels: seven, fourteen, and twenty-five.
*/
object UniformStyle extends Enumeration {
type Type = Value
val Normal = Value(0)
val FirstUpgrade = Value(1)
val SecondUpgrade = Value(2)
val ThirdUpgrade = Value(4)
implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
/**
* The different cosmetics that a player can apply to their model's head.<br>
* <br>
* This object is huge, representing the quantity of densely-encoded data in its packet.
* Certain bits, when set or unset, introduce or remove other bits from the packet data as well.
* (As in: flipping a bit may create room or negate other bits from somewhere else in the data stream.
* Not accounting for this new pattern of bits will break decoding and encoding.)
* Due to the very real concern that bloating the constructor for this object with parameters could break the `apply` method,
* parameters will often be composed of nested case objects that contain a group of formal parameters.
* There are lists of byte-aligned `Strings` later-on in the packet data that will need access to these objects to calculate padding length.<br>
* The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade.
* @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit;
* all other cosmetics require `no_helmet` to be `true` before they can be seen
* @param beret player dons a beret
* @param sunglasses player dons sunglasses
* @param earpiece player dons an earpiece on the left
* @param brimmed_cap player dons a cap;
* the cap overrides the beret, if both are selected
*/
final case class Cosmetics(no_helmet : Boolean,
beret : Boolean,
sunglasses : Boolean,
earpiece : Boolean,
brimmed_cap : Boolean)
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
* This densely-packed information outlines most of the specifics of depicting some other character.<br>
* <br>
* The first subdivision of parameters concerns the avatar's basic aesthetics, mostly.
* (No other parts of the data divided up yet.)
* The final sections include two lists of accredited activity performed/completed by the player.
* The remainder of the data, following after that, can be read straight, up to and through the inventory.<br>
* The character created by this data is treated like an NPC from the perspective of the server.
* Someone else decides how that character is behaving and the server tells each client how to depict that behavior.
* For that reason, the character is mostly for presentation purposes, rather than really being fleshed-out.
* (As far as the client is concerned, nothing stops this character from being declared an "avatar."
* A player would find such a client-controlled character lacking many important details and have poor equipment.
* They would also be competing with some other player for input control, if they could control the character at all.)<br>
* <br>
* The base length of the stream is currently __1138__ bits, excluding `List`s and `String`s and inventory.
* Of that, __831__ bits are perfectly unknown.
* @param appearance data about the avatar's basic aesthetics
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
* range is 0-65535
* @param health for `x / y` of hitpoints, this is the avatar's `x` value;
* range is 0-65535
* @param armor for `x / y` of armor points, this is the avatar's `x` value;
* range is 0-65535;
* the avatar's `y` armor points is tied to their exo-suit type
* @param unk1 na;
* defaults to 1
* @param unk2 na;
* defaults to 7
* @param unk3 na;
* defaults to 7
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
* range is 0-65535
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* range is 0-65535
* @param unk4 na;
* defaults to 28
* @param unk5 na;
* defaults to 4
* @param unk6 na;
* defaults to 44
* @param unk7 na;
* defaults to 84
* @param unk8 na;
* defaults to 104
* @param unk9 na;
* defaults to 1900
* @param firstTimeEvents the list of first time events performed by this avatar;
* the size field is a 32-bit number;
* the first entry may be padded
* @param tutorials the list of tutorials completed by this avatar;
* the size field is a 32-bit number;
* the first entry may be padded
* @param inventory the avatar's inventory
* Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character.
* In general, it passes more simplified data about the character, the minimum that is necessary to explain status to some other player.
* For example, health and armor are percentages, and are depicted as bars over the player's head near the nameplate.
* The third is the inventory (composed of normal-type objects).
* Rather than equipment other players would never interact with, it only comprises the contents of the five holster slots.<br>
* <br>
* If this player is spawned as dead - with their `health` at 0% - he will start standing and then immediately fall into a lying pose.
* The death pose selected is randomized, can not be influenced, and is not be shared across clients.
* @param appearance the player's cardinal appearance settings
* @param health the amount of health the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state;
* when 0% (less than 3 of 255), the player will collapse into a death pose on the ground
* @param armor the amount of armor the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state
* @param uniform_upgrade the level of upgrade to apply to the player's base uniform
* @param command_rank the player's command rank as a number from 0 to 5;
* cosmetic armor associated with the command rank will be applied automatically
* @param implant_effects the effects of implants that can be seen on a player's character;
* though many implants can be used simultaneously, only one implant effect can be applied here
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
* they become available at battle rank 24, but here they require the third uniform upgrade (rank 25);
* these flags do not exist if they are not applicable
* @param inventory the avatar's inventory;
* typically, only the tools and weapons in the equipment holster slots
* @param drawn_slot the holster that is initially drawn;
* defaults to `DrawnSlot.None`
* @see `CharacterAppearanceData`
* @see `DetailedCharacterData`
* @see `InventoryData`
* @see `DrawnSlot`
*/
final case class CharacterData(appearance : CharacterAppearanceData,
healthMax : Int,
health : Int,
armor : Int,
unk1 : Int, //1
unk2 : Int, //7
unk3 : Int, //7
staminaMax : Int,
stamina : Int,
unk4 : Int, //28
unk5 : Int, //4
unk6 : Int, //44
unk7 : Int, //84
unk8 : Int, //104
unk9 : Int, //1900
firstTimeEvents : List[String],
tutorials : List[String],
inventory : InventoryData
) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
uniform_upgrade : UniformStyle.Value,
command_rank : Int,
implant_effects : Option[ImplantEffects.Value],
cosmetics : Option[Cosmetics],
inventory : Option[InventoryData],
drawn_slot : DrawnSlot.Value = DrawnSlot.None
) extends ConstructorData {
override def bitsize : Long = {
//TODO ongoing analysis, this value will be subject to change
//fte list
val fteLen = firstTimeEvents.size
var eventListSize : Long = 32L + CharacterData.ftePadding(fteLen)
for(str <- firstTimeEvents) {
eventListSize += CharacterData.stringBitSize(str)
}
//tutorial list
val tutLen = tutorials.size
var tutorialListSize : Long = 32L + CharacterData.tutPadding(fteLen, tutLen)
for(str <- tutorials) {
tutorialListSize += CharacterData.stringBitSize(str)
}
708L + appearance.bitsize + eventListSize + tutorialListSize + inventory.bitsize
//factor guard bool values into the base size, not its corresponding optional field
val appearanceSize : Long = appearance.bitsize
val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L }
val cosmeticsSize : Long = if(cosmetics.isDefined) { 5L } else { 0L }
val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L }
32L + appearanceSize + effectsSize + cosmeticsSize + inventorySize
}
}
object CharacterData extends Marshallable[CharacterData] {
/**
* Calculate the size of a string, including the length of the "string length" field that precedes it.
* Do not pass null-terminated strings.
* @param str a length-prefixed string
* @param width the width of the character encoding;
* defaults to the standard 8-bits
* @return the size in bits
* An overloaded constructor for `CharacterData` that allows for a not-optional inventory.
* @param appearance the player's cardinal appearance settings
* @param health the amount of health the player has, as a percentage of a filled bar
* @param armor the amount of armor the player has, as a percentage of a filled bar
* @param uniform the level of upgrade to apply to the player's base uniform
* @param cr the player's command rank as a number from 0 to 5
* @param implant_effects the effects of implants that can be seen on a player's character
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands
* @param inv the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @return a `CharacterData` object
*/
def stringBitSize(str : String, width : Int = 8) : Long = {
val strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L
lenSize + (strlen * width)
}
def apply(appearance : CharacterAppearanceData, health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics], inv : InventoryData, drawn_slot : DrawnSlot.Value) : CharacterData =
new CharacterData(appearance, health, armor, uniform, cr, implant_effects, cosmetics, Some(inv), drawn_slot)
/**
* Get the padding of the first entry in the first time events list.
* The padding will always be a number 0-7.
* @param len the length of the list
* @return the pad length in bits
* Check for the bit flags for the cosmetic items.
* These flags are only valid if the player has acquired their third uniform upgrade.
* @see `UniformStyle.ThirdUpgrade`
*/
private def ftePadding(len : Long) : Int = {
//TODO the parameters for this function are not correct
//TODO the proper padding length should reflect all variability in the stream prior to this point
if(len > 0) {
5
}
else
0
}
/**
* Get the padding of the first entry in the completed tutorials list.
* The padding will always be a number 0-7.<br>
* <br>
* The tutorials list follows the first time event list and that contains byte-aligned strings too.
* While there will be more to the padding, this other list is important.
* Any elements in that list causes the automatic byte-alignment of this list's first entry.
* @param len the length of the list
* @return the pad length in bits
*/
private def tutPadding(len : Long, len2 : Long) : Int = {
if(len > 0) //automatic alignment from previous List
0
else if(len2 > 0) //need to align for elements
5
else //both lists are empty
0
}
private val cosmeticsCodec : Codec[Cosmetics] = (
("no_helmet" | bool) ::
("beret" | bool) ::
("sunglasses" | bool) ::
("earpiece" | bool) ::
("brimmed_cap" | bool)
).as[Cosmetics]
implicit val codec : Codec[CharacterData] = (
("appearance" | CharacterAppearanceData.codec) ::
ignore(160) ::
("healthMax" | uint16L) ::
("health" | uint16L) ::
ignore(1) ::
("armor" | uint16L) ::
ignore(9) ::
("unk1" | uint8L) ::
ignore(8) ::
("unk2" | uint4L) ::
("unk3" | uintL(3)) ::
("staminaMax" | uint16L) ::
("stamina" | uint16L) ::
ignore(149) ::
("unk4" | uint16L) ::
("unk5" | uint8L) ::
("unk6" | uint8L) ::
("unk7" | uint8L) ::
("unk8" | uint8L) ::
("unk9" | uintL(12)) ::
ignore(19) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) ::
("inventory" | InventoryData.codec)
})
("app" | CharacterAppearanceData.codec) ::
("health" | uint8L) :: //dead state when health == 0
("armor" | uint8L) ::
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
ignore(3) :: //unknown
("command_rank" | uintL(3)) ::
bool :: //stream misalignment when != 1
optional(bool, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | cosmeticsCodec) ::
optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
})
).xmap[CharacterData] (
).exmap[CharacterData] (
{
case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: p :: q :: r :: s :: t :: u :: _ :: v :: HNil =>
//prepend the displaced first elements to their lists
val fteList : List[String] = if(q.isDefined) { q.get :: r } else r
val tutList : List[String] = if(t.isDefined) { t.get :: u } else u
CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v)
},
{
case CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p) =>
//shift the first elements off their lists
var fteListCopy = fteList
var firstEvent : Option[String] = None
if(fteList.nonEmpty) {
firstEvent = Some(fteList.head)
fteListCopy = fteList.drop(1)
case app :: health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: inv :: drawn_slot :: false :: HNil =>
var newHealth = health
if(app.backpack) {
newHealth = 0
}
var tutListCopy = tutList
var firstTutorial : Option[String] = None
if(tutList.nonEmpty) {
firstTutorial = Some(tutList.head)
tutListCopy = tutList.drop(1)
}
app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: HNil
}
).as[CharacterData]
Attempt.Successful(CharacterData(app, newHealth, armor, uniform, cr, implant_effects, cosmetics, inv, drawn_slot))
/**
* Transform between CharacterData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
case _ =>
Attempt.Failure(Err("invalid character data; can not encode"))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[CharacterData])
case CharacterData(app, health, armor, uniform, cr, implant_effects, cosmetics, inv, drawn_slot) =>
var newHealth = health
if(app.backpack) {
newHealth = 0
}
Attempt.Successful(app :: newHealth :: armor :: uniform :: () :: cr :: false :: implant_effects :: cosmetics :: inv :: drawn_slot :: false :: HNil)
case _ =>
Attempt.failure(Err("can not encode character data"))
Attempt.Failure(Err("invalid character data; can not decode"))
}
)
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the command uplink device.<br>
* I don't know much about the command uplink device so someone else has to provide this commentary.
*/
final case class CommandDetonaterData(unk1 : Int = 0,
unk2 : Int = 0) extends ConstructorData {
override def bitsize : Long = 34L
}
object CommandDetonaterData extends Marshallable[CommandDetonaterData] {
implicit val codec : Codec[CommandDetonaterData] = (
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(26)
).exmap[CommandDetonaterData] (
{
case unk1 :: unk2 :: 0 :: HNil =>
Attempt.successful(CommandDetonaterData(unk1, unk2))
case _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid command detonator data format"))
},
{
case CommandDetonaterData(unk1, unk2) =>
Attempt.successful(unk1 :: unk2 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of an object that can be interacted with when using a variety of terminals.
* This object is generally invisible.
* @param pos where and how the object is oriented
*/
final case class CommonTerminalData(pos : PlacementData) extends ConstructorData {
override def bitsize : Long = 24L + pos.bitsize
}
object CommonTerminalData extends Marshallable[CommonTerminalData] {
implicit val codec : Codec[CommonTerminalData] = (
("pos" | PlacementData.codec) ::
bool ::
bool ::
uint(22)
).exmap[CommonTerminalData] (
{
case pos :: false :: true :: 0 :: HNil =>
Attempt.successful(CommonTerminalData(pos))
case _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid terminal data format"))
},
{
case CommonTerminalData(pos) =>
Attempt.successful(pos :: false :: true :: 0 :: HNil)
}
)
}

View file

@ -3,62 +3,63 @@ package net.psforever.packet.game.objectcreate
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.codecs._
import scodec.codecs.{uint, _}
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data.
* A representation of a class of weapons that can be created using `ObjectCreateDetailedMessage` packet data.
* A "concurrent feed weapon" refers to a weapon system that can chamber multiple types of ammunition simultaneously.
* This data will help construct a "weapon" such as a Punisher.<br>
* <br>
* The data for the weapons nests information for the default (current) type and number of ammunition in its magazine.
* This ammunition data essentially is the weapon's magazines as numbered slots.
* @param unk na
* @param unk1 na
* @param unk2 na
* @param fire_mode the current mode of weapon's fire;
* zero-indexed
* @param ammo `List` data regarding the currently loaded ammunition types and quantities
* @see WeaponData
* @see AmmoBoxData
* @see `WeaponData`
* @see `AmmoBoxData`
*/
final case class ConcurrentFeedWeaponData(unk : Int,
final case class ConcurrentFeedWeaponData(unk1 : Int,
unk2 : Int,
fire_mode : Int,
ammo : List[InternalSlot]) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @see InternalSlot.bitsize
* @see AmmoBoxData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = {
var bitsize : Long = 0L
for(o <- ammo) {
bitsize += o.bitsize
}
61L + bitsize
44L + bitsize
}
}
object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] {
/**
* An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.<br>
* An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `DetailedAmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk na
* @param unk1 na
* @param unk2 na
* @param fire_mode data regarding the currently loaded ammunition type
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the constructor data for the ammunition
* @return a WeaponData object
* @return a DetailedWeaponData object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : ConcurrentFeedWeaponData =
new ConcurrentFeedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
def apply(unk1 : Int, unk2 : Int, fire_mode : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : ConcurrentFeedWeaponData =
new ConcurrentFeedWeaponData(unk1, unk2, fire_mode, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
implicit val codec : Codec[ConcurrentFeedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16 ::
uint2L ::
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
("fire_mode" | int(3)) ::
bool ::
bool ::
(uint8L >>:~ { size =>
uint2L ::
("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec)) ::
@ -66,41 +67,25 @@ object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] {
})
).exmap[ConcurrentFeedWeaponData] (
{
case code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil =>
case unk1 :: unk2 :: 0 :: fmode :: false :: true :: size :: 0 :: ammo :: false :: HNil =>
if(size != ammo.size)
Attempt.failure(Err("weapon encodes wrong number of ammunition"))
else if(size == 0)
Attempt.failure(Err("weapon needs to encode at least one type of ammunition"))
else
Attempt.successful(ConcurrentFeedWeaponData(code, ammo))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.successful(ConcurrentFeedWeaponData(unk1, unk2, fmode, ammo))
case _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case ConcurrentFeedWeaponData(code, ammo) =>
case ConcurrentFeedWeaponData(unk1, unk2, fmode, ammo) =>
val size = ammo.size
if(size == 0)
Attempt.failure(Err("weapon needs to encode at least one type of ammunition"))
else if(size >= 255)
Attempt.failure(Err("weapon has too much ammunition (255+ types!)"))
else
Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil)
}
).as[ConcurrentFeedWeaponData]
/**
* Transform between WeaponData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[ConcurrentFeedWeaponData])
case _ =>
Attempt.failure(Err("can not encode weapon data"))
Attempt.successful(unk1 :: unk2 :: 0 :: fmode :: false :: true :: size :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -1,6 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import scodec.{Attempt, Codec, Err}
/**
* The base type for the representation of any data used to produce objects from `ObjectCreateMessage` packet data.
* There is no reason to instantiate this class as-is.
@ -18,4 +20,26 @@ object ConstructorData {
* The casting will be performed through use of `exmap` in the child class.
*/
type genericPattern = Option[ConstructorData]
/**
* Transform a `Codec[T]` for object type `T` into `ConstructorData.genericPattern`.
* @param objCodec a `Codec` that satisfies the transformation `Codec[T] -> T`
* @param objType a `String` that explains what the object should be identified as in the `Err` message;
* defaults to "object"
* @tparam T a subclass of `ConstructorData` that indicates what type the object is
* @return `ConstructorData.genericPattern`
*/
def genericCodec[T <: ConstructorData](objCodec : Codec[T], objType : String = "object") : Codec[ConstructorData.genericPattern] =
objCodec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[T]) //why does this work? shouldn't type erasure be a problem?
case _ =>
Attempt.failure(Err(s"can not encode as $objType data"))
}
)
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of an adaptive construction engine (ACE).
* This one-time-use item deploys a variety of utilities into the game environment.
* Has an advanced version internally called an `advanced_ace` and commonly called a Field Deployment Unit (FDU).
* @param unk na
*/
final case class DetailedACEData(unk : Int) extends ConstructorData {
override def bitsize : Long = 51L
}
object DetailedACEData extends Marshallable[DetailedACEData] {
implicit val codec : Codec[DetailedACEData] = (
("unk" | uint4L) ::
uint4L ::
uintL(20) ::
uint4L ::
uint16L ::
uint(3)
).exmap[DetailedACEData] (
{
case code :: 8 :: 0 :: 2 :: 0 :: 4 :: HNil =>
Attempt.successful(DetailedACEData(code))
case _ =>
Attempt.failure(Err("invalid ace data format"))
},
{
case DetailedACEData(code) =>
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 4 :: HNil)
}
)
}

View file

@ -0,0 +1,58 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the ammunition portion of `ObjectCreateDetailedMessage` packet data.
* This data will help construct a "box" of that type of ammunition when standalone.
* It can also be constructed directly inside a weapon as its magazine.<br>
* <br>
* The maximum amount of ammunition that can be stored in a single box is 65535 units.
* Regardless of the interface, however, the number will never be fully visible.
* Only the first three digits or the first four digits may be represented.
* @param unk na
* @param magazine the number of rounds available
* @see DetailedWeaponData
*/
final case class DetailedAmmoBoxData(unk : Int,
magazine : Int
) extends ConstructorData {
override def bitsize : Long = 40L
}
object DetailedAmmoBoxData extends Marshallable[DetailedAmmoBoxData] {
/**
* An abbreviated constructor for creating `DetailedWeaponData` while masking use of `InternalSlot`.
* @param cls the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param ammo the `DetailedAmmoBoxData`
* @return an `InternalSlot` object that encapsulates `DetailedAmmoBoxData`
*/
def apply(cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : InternalSlot =
new InternalSlot(cls, guid, parentSlot, ammo)
implicit val codec : Codec[DetailedAmmoBoxData] = (
uint4L ::
("unk" | uint4L) ::
uint(15) ::
("magazine" | uint16L) ::
bool
).exmap[DetailedAmmoBoxData] (
{
case 0xC :: unk :: 0 :: mag :: false :: HNil =>
Attempt.successful(DetailedAmmoBoxData(unk, mag))
case _ =>
Attempt.failure(Err("invalid ammunition data format"))
},
{
case DetailedAmmoBoxData(unk, mag) =>
Attempt.successful(0xC :: unk :: 0 :: mag :: false:: HNil)
}
)
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the detonater utility that is created when putting down a Boomer with an ACE.
*/
final case class DetailedBoomerTriggerData() extends ConstructorData {
override def bitsize : Long = 51L
}
object DetailedBoomerTriggerData extends Marshallable[DetailedBoomerTriggerData] {
implicit val codec : Codec[DetailedBoomerTriggerData] = (
uint8L ::
uint(22) ::
bool :: //true
uint(17) ::
bool :: //true
uint2L
).exmap[DetailedBoomerTriggerData] (
{
case 0xC8 :: 0 :: true :: 0 :: true :: 0 :: HNil =>
Attempt.successful(DetailedBoomerTriggerData())
case _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid command detonater format"))
},
{
case DetailedBoomerTriggerData() =>
Attempt.successful(0xC8 :: 0 :: true :: 0 :: true :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,252 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.{Attempt, Codec}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.
* This densely-packed information outlines most of the specifics required to depict a character as an avatar.<br>
* <br>
* As an avatar, the character created by this data is expected to be controllable by the client that gets sent this data.
* It goes into depth about information related to the given character in-game career that is not revealed to other players.<br>
* <br>
* Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character as an avatar.
* In general, it passes more thorough data about the character that the client can display to the owner of the client.
* For example, health is a full number, rather than a percentage.
* Just as prominent is the list of first time events and the list of completed tutorials.
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
* The fourth is the inventory (composed of `Direct`-type objects).<br>
* <br>
* Exploration:<br>
* Lots of analysis needed for the remainder of the byte data.
* @param appearance data about the avatar's basic aesthetics
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
* range is 0-65535
* @param health for `x / y` of hitpoints, this is the avatar's `x` value;
* range is 0-65535
* @param armor for `x / y` of armor points, this is the avatar's `x` value;
* range is 0-65535;
* the avatar's `y` armor points is tied to their exo-suit type
* @param unk1 na;
* defaults to 1
* @param unk2 na;
* defaults to 7
* @param unk3 na;
* defaults to 7
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
* range is 0-65535
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* range is 0-65535
* @param unk4 na;
* defaults to 28
* @param unk5 na;
* defaults to 4
* @param unk6 na;
* defaults to 44
* @param unk7 na;
* defaults to 84
* @param unk8 na;
* defaults to 104
* @param unk9 na;
* defaults to 1900
* @param firstTimeEvents the list of first time events performed by this avatar;
* the size field is a 32-bit number;
* the first entry may be padded
* @param tutorials the list of tutorials completed by this avatar;
* the size field is a 32-bit number;
* the first entry may be padded
* @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @see `CharacterAppearanceData`
* @see `CharacterData`
* @see `InventoryData`
* @see `DrawnSlot`
*/
final case class DetailedCharacterData(appearance : CharacterAppearanceData,
healthMax : Int,
health : Int,
armor : Int,
unk1 : Int, //1
unk2 : Int, //7
unk3 : Int, //7
staminaMax : Int,
stamina : Int,
unk4 : Int, //28
unk5 : Int, //4
unk6 : Int, //44
unk7 : Int, //84
unk8 : Int, //104
unk9 : Int, //1900
firstTimeEvents : List[String],
tutorials : List[String],
inventory : Option[InventoryData],
drawn_slot : DrawnSlot.Value = DrawnSlot.None
) extends ConstructorData {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val appearanceSize = appearance.bitsize
val fteLen = firstTimeEvents.size //fte list
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen)
for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str)
}
val tutLen = tutorials.size //tutorial list
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen)
for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str)
}
var inventorySize : Long = 0L //inventory
if(inventory.isDefined) {
inventorySize = inventory.get.bitsize
}
713L + appearanceSize + eventListSize + tutorialListSize + inventorySize
}
}
object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
/**
* Overloaded constructor for `DetailedCharacterData` that skips all the unknowns by assigning defaulted values.
* It also allows for a not-optional inventory.
* @param appearance data about the avatar's basic aesthetics
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
* @param armor for `x / y` of armor points, this is the avatar's `x` value
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
* @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar
* @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object
*/
def apply(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
new DetailedCharacterData(appearance, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, 28, 4, 44, 84, 104, 1900, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
/**
* Overloaded constructor for `DetailedCharacterData` that allows for a not-optional inventory.
* @param appearance data about the avatar's basic aesthetics
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
* @param armor for `x / y` of armor points, this is the avatar's `x` value
* @param unk1 na
* @param unk2 na
* @param unk3 na
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
* @param unk4 na
* @param unk5 na
* @param unk6 na
* @param unk7 na
* @param unk8 na
* @param unk9 na
* @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar
* @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object
*/
def apply(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, unk1 : Int, unk2 : Int, unk3 : Int, staminaMax : Int, stamina : Int, unk4 : Int, unk5 : Int, unk6 : Int, unk7 : Int, unk8 : Int, unk9 : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
new DetailedCharacterData(appearance, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, unk7, unk8, unk9, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
/**
* Get the padding of the first entry in the first time events list.
* The padding will always be a number 0-7.
* @param len the length of the list
* @return the pad length in bits
*/
private def ftePadding(len : Long) : Int = {
//TODO the parameters for this function are not correct
//TODO the proper padding length should reflect all variability in the stream prior to this point
if(len > 0) {
5
}
else
0
}
/**
* Get the padding of the first entry in the completed tutorials list.
* The padding will always be a number 0-7.<br>
* <br>
* The tutorials list follows the first time event list and that contains byte-aligned strings too.
* While there will be more to the padding, this other list is important.
* Any elements in that list causes the automatic byte-alignment of this list's first entry.
* @param len the length of the list
* @return the pad length in bits
*/
private def tutPadding(len : Long, len2 : Long) : Int = {
if(len > 0) //automatic alignment from previous List
0
else if(len2 > 0) //need to align for elements
5
else //both lists are empty
0
}
implicit val codec : Codec[DetailedCharacterData] = (
("appearance" | CharacterAppearanceData.codec) ::
ignore(160) ::
("healthMax" | uint16L) ::
("health" | uint16L) ::
ignore(1) ::
("armor" | uint16L) ::
ignore(9) ::
("unk1" | uint8L) ::
ignore(8) ::
("unk2" | uint4L) ::
("unk3" | uintL(3)) ::
("staminaMax" | uint16L) ::
("stamina" | uint16L) ::
ignore(149) ::
("unk4" | uint16L) ::
("unk5" | uint8L) ::
("unk6" | uint8L) ::
("unk7" | uint8L) ::
("unk8" | uint8L) ::
("unk9" | uintL(12)) ::
ignore(19) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) ::
optional(bool, "inventory" | InventoryData.codec_detailed) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
})
})
).exmap[DetailedCharacterData] (
{
case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: _ :: q :: r :: _ :: t :: u :: _ :: v :: w :: false :: HNil =>
//prepend the displaced first elements to their lists
val fteList : List[String] = if(q.isDefined) { q.get :: r } else r
val tutList : List[String] = if(t.isDefined) { t.get :: u } else u
Attempt.successful(DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v, w))
},
{
case DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p, q) =>
//shift the first elements off their lists
var fteListCopy = fteList
var firstEvent : Option[String] = None
if(fteList.nonEmpty) {
firstEvent = Some(fteList.head)
fteListCopy = fteList.drop(1)
}
var tutListCopy = tutList
var firstTutorial : Option[String] = None
if(tutList.nonEmpty) {
firstTutorial = Some(tutList.head)
tutListCopy = tutList.drop(1)
}
Attempt.successful(app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: q :: false :: HNil)
}
)
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the command uplink device.<br>
* I don't know much about the command uplink device so someone else has to provide this commentary.
*/
final case class DetailedCommandDetonaterData(unk1 : Int = 8,
unk2 : Int = 0) extends ConstructorData {
override def bitsize : Long = 51L
}
object DetailedCommandDetonaterData extends Marshallable[DetailedCommandDetonaterData] {
implicit val codec : Codec[DetailedCommandDetonaterData] = (
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
uint4L ::
uint16 ::
uint(3)
).exmap[DetailedCommandDetonaterData] (
{
case unk1 :: unk2 :: 0 :: 2 :: 0 :: 4 :: HNil =>
Attempt.successful(DetailedCommandDetonaterData(unk1, unk2))
case _ =>
Attempt.failure(Err("invalid command detonator data format"))
},
{
case DetailedCommandDetonaterData(unk1, unk2) =>
Attempt.successful(unk1 :: unk2 :: 0 :: 2 :: 0 :: 4 :: HNil)
}
)
}

View file

@ -0,0 +1,86 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.game.PlanetSideGUID
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of a class of weapons that can be created using `ObjectCreateDetailedMessage` packet data.
* A "concurrent feed weapon" refers to a weapon system that can chamber multiple types of ammunition simultaneously.
* This data will help construct a "weapon" such as a Punisher.<br>
* <br>
* The data for the weapons nests information for the default (current) type of ammunition in its magazine.
* This ammunition data essentially is the weapon's magazines as numbered slots.
* @param unk1 na
* @param unk2 na
* @param ammo `List` data regarding the currently loaded ammunition types and quantities
* @see DetailedWeaponData
* @see DetailedAmmoBoxData
*/
final case class DetailedConcurrentFeedWeaponData(unk1 : Int,
unk2 : Int,
ammo : List[InternalSlot]) extends ConstructorData {
override def bitsize : Long = {
var bitsize : Long = 0L
for(o <- ammo) {
bitsize += o.bitsize
}
61L + bitsize
}
}
object DetailedConcurrentFeedWeaponData extends Marshallable[DetailedConcurrentFeedWeaponData] {
/**
* An abbreviated constructor for creating `DetailedConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `DetailedAmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk1 na
* @param unk2 na
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the constructor data for the ammunition
* @return a DetailedWeaponData object
*/
def apply(unk1 : Int, unk2 : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : DetailedConcurrentFeedWeaponData =
new DetailedConcurrentFeedWeaponData(unk1, unk2, InternalSlot(cls, guid, parentSlot, ammo) :: Nil)
implicit val codec : Codec[DetailedConcurrentFeedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16 ::
uint2L ::
(uint8L >>:~ { size =>
uint2L ::
("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec_detailed)) ::
bool
})
).exmap[DetailedConcurrentFeedWeaponData] (
{
case unk1 :: unk2 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil =>
if(size != ammo.size)
Attempt.failure(Err("weapon encodes wrong number of ammunition"))
else if(size == 0)
Attempt.failure(Err("weapon needs to encode at least one type of ammunition"))
else
Attempt.successful(DetailedConcurrentFeedWeaponData(unk1, unk2, ammo))
case _ =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case DetailedConcurrentFeedWeaponData(unk1, unk2, ammo) =>
val size = ammo.size
if(size == 0)
Attempt.failure(Err("weapon needs to encode at least one type of ammunition"))
else if(size >= 255)
Attempt.failure(Err("weapon has too much ammunition (255+ types!)"))
else
Attempt.successful(unk1 :: unk2 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the REK portion of `ObjectCreateDetailedMessage` packet data.
* This data will help construct the "tool" called a Remote Electronics Kit.<br>
* <br>
* Of note is the first portion of the data which resembles the `DetailedWeaponData` format.
* @param unk na
*/
final case class DetailedREKData(unk : Int) extends ConstructorData {
override def bitsize : Long = 67L
}
object DetailedREKData extends Marshallable[DetailedREKData] {
implicit val codec : Codec[DetailedREKData] = (
("unk" | uint4L) ::
uint4L ::
uintL(20) ::
uint4L ::
uint16L ::
uint4L ::
uintL(15)
).exmap[DetailedREKData] (
{
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(DetailedREKData(code))
case _ =>
Attempt.failure(Err("invalid rek data format"))
},
{
case DetailedREKData(code) =>
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,64 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of a class of weapons that can be created using `ObjectCreateDetailedMessage` packet data.
* This data will help construct a "loaded weapon" such as a Suppressor or a Gauss.<br>
* <br>
* The data for the weapons nests information for the default (current) type and number of ammunition in its magazine.
* This ammunition data essentially is the weapon's magazines as numbered slots.
* This format only handles one type of ammunition at a time.
* Any weapon that has two types of ammunition simultaneously loaded must be handled with another `Codec`.
* This functionality is unrelated to a weapon that switches ammunition type;
* a weapon with that behavior is handled perfectly fine using this `case class`.
* @param unk na
* @param ammo data regarding the currently loaded ammunition type and quantity
* @see DetailedAmmoBoxData
*/
final case class DetailedWeaponData(unk : Int,
ammo : InternalSlot) extends ConstructorData {
override def bitsize : Long = 61L + ammo.bitsize
}
object DetailedWeaponData extends Marshallable[DetailedWeaponData] {
/**
* An abbreviated constructor for creating `DetailedWeaponData` while masking use of `InternalSlot` for its `DetailedAmmoBoxData`.
* @param unk na
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the constructor data for the ammunition
* @return a DetailedWeaponData object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : DetailedAmmoBoxData) : DetailedWeaponData =
new DetailedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo))
implicit val codec : Codec[DetailedWeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16L ::
uint2 ::
uint8 :: //size = 1 type of ammunition loaded
uint2 ::
("ammo" | InternalSlot.codec_detailed) ::
bool
).exmap[DetailedWeaponData] (
{
case code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil =>
Attempt.successful(DetailedWeaponData(code, ammo))
case _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case DetailedWeaponData(code, ammo) =>
Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
/**
* Values for the equipment holster slot whose contained ("held") equipment can be drawn.
* The values for these Enums match the slot number by index for Infantry weapons.<br>
* <br>
* `None` is not a kludge.
* While any "not a holster" number can be used to indicate "no weapon drawn," seven is the value PlanetSide is looking for.
* Using five or six delays the first weapon draw while the client corrects its internal state.
*/
object DrawnSlot extends Enumeration {
type Type = Value
val Pistol1 = Value(0)
val Pistol2 = Value(1)
val Rifle1 = Value(2)
val Rifle2 = Value(3)
val Melee = Value(4)
val None = Value(7)
import net.psforever.packet.PacketHelpers
import scodec.codecs._
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* Provide information that positions a given object on the ground in the game world.
* @param pos where and how the object is oriented
* @param obj the object on the ground
* @tparam T a subclass of `ConstructorData` that indicates what type the object is
*/
final case class DroppedItemData[T <: ConstructorData](pos : PlacementData, obj : T) extends ConstructorData {
override def bitsize : Long = pos.bitsize + obj.bitsize
}
object DroppedItemData {
/**
* Transform `DroppedItemData[T]` for object type `T` into `ConstructorData.genericPattern`.<br>
* <br>
* This function eliminates the need to have a separate "DroppedFooData" class for every object "Foo."
* Two functions normally perform this transformation: an `implicit` `codec` used in a `genericCodec`.
* Since actual Generics are utilized, combining the processes eliminates defining to the type data multiple times.
* (If that is even possible here.)
* Knowledge of the object type is still necessary to recover the original object's data through casting.
* Not having to explicitly cast would have been the main upside of having specialized "DroppedFooData" classes.<br>
* <br>
* Use:<br>
* `DroppedItemCodec.genericCodec(T.codec)`
* @param objCodec a `Codec` that satisfies the transformation `Codec[T] -> T`
* @param objType a `String` that explains what the object should be identified as in the log;
* defaults to "object"
* @tparam T a subclass of `ConstructorData` that indicates what type the object is
* @return `ConstructorData.genericPattern`
* @see `ConstructorData.genericPattern` (function)
*/
def genericCodec[T <: ConstructorData](objCodec : Codec[T], objType : String = "object") : Codec[ConstructorData.genericPattern] = (
("pos" | PlacementData.codec) ::
("obj" | objCodec)
).xmap[DroppedItemData[T]] (
{
case pos :: obj :: HNil =>
DroppedItemData[T](pos, obj)
},
{
case DroppedItemData(pos, obj) =>
pos :: obj :: HNil
}
).exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[DroppedItemData[T]])
case _ =>
Attempt.failure(Err(s"can not encode dropped $objType data"))
}
)
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of an object that can be interacted with when using an implant terminal.
* This object is generally invisible.
*/
final case class ImplantInterfaceData() extends ConstructorData {
override def bitsize : Long = 24L
}
object ImplantInterfaceData extends Marshallable[ImplantInterfaceData] {
implicit val codec : Codec[ImplantInterfaceData] = (
bool ::
uint(23)
).exmap[ImplantInterfaceData] (
{
case true :: 0 :: HNil =>
Attempt.successful(ImplantInterfaceData())
case _ :: _ :: HNil =>
Attempt.failure(Err("invalid interface data format"))
},
{
case ImplantInterfaceData() =>
Attempt.successful(true :: 0 :: HNil)
}
)
}

View file

@ -1,46 +1,47 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.packet.PacketHelpers
import net.psforever.packet.game.PlanetSideGUID
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
/**
* An intermediate class for the primary fields of `ObjectCreateMessage` with an implicit parent-child relationship.<br>
* An intermediate class for the primary fields of `ObjectCreate*Message` with an implicit parent-child relationship.<br>
* <br>
* Any object that is contained in a "slot" of another object will use `InternalSlot` to hold the anchoring data.
* This prior object will clarify the identity of the "parent" object that owns the given `parentSlot`.<br>
* This prior object will clarify the identity of the "parent" object that owns the given `parentSlot`.
* As the name implies, this should never have to be used in the representation of a non-child object.<br>
* <br>
* Try to avoid exposing `InternalSlot` in the process of implementing code.
* Try to avoid exposing `InternalSlot` in the process of implementing object code.
* (Provide overrode constructors where applicable.)
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param obj the data used as representation of the object to be constructed
* @see ObjectClass.selectDataCodec
* @see `ObjectClass.selectDataCodec`
* @see `ObjectClass.selectDataDetailedCodec`
*/
final case class InternalSlot(objectClass : Int,
guid : PlanetSideGUID,
parentSlot : Int,
obj : ConstructorData) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = {
val base : Long = if(parentSlot > 127) 43L else 35L
base + obj.bitsize
}
}
object InternalSlot extends Marshallable[InternalSlot] {
implicit val codec : Codec[InternalSlot] = (
object InternalSlot {
/**
* Used for `0x18` `ObjectCreateDetailedMessage` packets
*/
val codec_detailed : Codec[InternalSlot] = (
("objectClass" | uintL(11)) >>:~ { obj_cls =>
("guid" | PlanetSideGUID.codec) ::
("parentSlot" | PacketHelpers.encodedStringSize) ::
("obj" | ObjectClass.selectDataCodec(obj_cls)) //it's fine for this call to fail
("obj" | ObjectClass.selectDataDetailedCodec(obj_cls)) //it's fine for this call to fail
}
).xmap[InternalSlot] (
{
@ -51,5 +52,25 @@ object InternalSlot extends Marshallable[InternalSlot] {
case InternalSlot(cls, guid, slot, obj) =>
cls :: guid :: slot :: Some(obj) :: HNil
}
).as[InternalSlot]
)
/**
* Used for `0x17` `ObjectCreateMessage` packets
*/
val codec : Codec[InternalSlot] = (
("objectClass" | uintL(11)) >>:~ { obj_cls =>
("guid" | PlanetSideGUID.codec) ::
("parentSlot" | PacketHelpers.encodedStringSize) ::
("obj" | ObjectClass.selectDataCodec(obj_cls)) //it's fine for this call to fail
}
).xmap[InternalSlot] (
{
case cls :: guid :: slot :: Some(obj) :: HNil =>
InternalSlot(cls, guid, slot, obj)
},
{
case InternalSlot(cls, guid, slot, obj) =>
cls :: guid :: slot :: Some(obj) :: HNil
}
)
}

View file

@ -1,52 +1,33 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.packet.PacketHelpers
import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.<br>
* A representation of the inventory portion of `ObjectCreate*Message` packet data for avatars.<br>
* <br>
* The inventory is a temperamental thing.
* Items placed into the inventory must follow their proper encoding schematics to the letter.
* The slot number refers to the position occupied by the item.
* In icon format, all-encompassing slots are absolute positions; and, grid-distributed icons use the upper-left corner.
* No values are allowed to be misplaced and no unexpected regions of data can be discovered.
* If there is even a minor failure, the whole of the inventory will fail to translate.<br>
* If there is even a minor failure, the remainder of the inventory will fail to translate.<br>
* <br>
* Under the official servers, when a new character was generated, the inventory encoded as `0x1C`.
* This inventory had no size field, no contents, and an indeterminate number of values.
* This format is no longer supported.
* Going forward, an empty inventory - approximately `0x10000` - should be used as substitute.<br>
* <br>
* Exploration:<br>
* 4u of ignored bits have been added to the end of the inventory to make up for missing stream length.
* They do not actually seem to be part of the inventory.
* Are these bits always at the end of the packet data and what is the significance?
* @param unk1 na;
* `true` to mark the start of the inventory data?
* is explicitly declaring the bit necessary when it always seems to be `true`?
* Inventories are usually prefaced with a `bin1` value not accounted for here.
* It can be treated as optional.
* @param contents the items in the inventory
* @param unk1 na
* @param unk2 na
* @param unk3 na
* @param contents the actual items in the inventory;
* holster slots are 0-4;
* an inaccessible slot is 5;
* internal capacity is 6-`n`, where `n` is defined by exosuit type and is mapped into a grid
*/
final case class InventoryData(unk1 : Boolean,
unk2 : Boolean,
unk3 : Boolean,
contents : List[InventoryItem]) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
final case class InventoryData(contents : List[InventoryItem] = List.empty,
unk1 : Boolean = false,
unk2 : Boolean = false) extends StreamBitSize {
override def bitsize : Long = {
//three booleans, the 4u extra, and the 8u length field
val base : Long = 15L
//length of all items in inventory
var invSize : Long = 0L
val base : Long = 10L //8u + 1u + 1u
var invSize : Long = 0L //length of all items in inventory
for(item <- contents) {
invSize += item.bitsize
}
@ -54,23 +35,49 @@ final case class InventoryData(unk1 : Boolean,
}
}
object InventoryData extends Marshallable[InventoryData] {
implicit val codec : Codec[InventoryData] = (
("unk1" | bool) ::
(("len" | uint8L) >>:~ { len =>
object InventoryData {
private def inventoryCodec(itemCodec : Codec[InventoryItem]) : Codec[InventoryData] = (
uint8L >>:~ { len =>
("unk1" | bool) ::
("unk2" | bool) ::
("unk3" | bool) ::
("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) ::
ignore(4)
})
).xmap[InventoryData] (
("contents" | PacketHelpers.listOfNSized(len, itemCodec))
}
).xmap[InventoryData] (
{
case u1 :: _ :: a :: b :: ctnt :: _ :: HNil =>
InventoryData(u1, a, b, ctnt)
case _ :: a :: b :: c :: HNil =>
InventoryData(c, a, b)
},
{
case InventoryData(u1, a, b, ctnt) =>
u1 :: ctnt.size :: a :: b :: ctnt :: () :: HNil
case InventoryData(c, a, b) =>
c.size :: a :: b :: c :: HNil
}
).as[InventoryData]
)
/**
* A `Codec` for `0x17` `ObjectCreateMessage` data.
*/
val codec : Codec[InventoryData] = inventoryCodec(InventoryItem.codec).hlist.xmap[InventoryData] (
{
case inventory :: HNil =>
inventory
},
{
case InventoryData(a, b, c) =>
InventoryData(a, b, c) :: HNil
}
)
/**
* A `Codec` for `0x18` `ObjectCreateDetailedMessage` data.
*/
val codec_detailed : Codec[InventoryData] = inventoryCodec(InventoryItem.codec_detailed).hlist.xmap[InventoryData] (
{
case inventory :: HNil =>
inventory
},
{
case InventoryData(a, b, c) =>
InventoryData(a, b, c) :: HNil
}
)
}

View file

@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.Codec
import scodec.codecs._
@ -16,16 +15,12 @@ import scodec.codecs._
* @param item the object in inventory
* @see InternalSlot
*/
final case class InventoryItem(item : InternalSlot) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
final case class InventoryItem(item : InternalSlot
) extends StreamBitSize {
override def bitsize : Long = item.bitsize
}
object InventoryItem extends Marshallable[InventoryItem] {
object InventoryItem {
/**
* An abbreviated constructor for creating an `InventoryItem` without interacting with `InternalSlot` directly.
* @param objClass the code for the type of object (ammunition) being constructed
@ -37,7 +32,13 @@ object InventoryItem extends Marshallable[InventoryItem] {
def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem =
InventoryItem(InternalSlot(objClass, guid, parentSlot, obj))
implicit val codec : Codec[InventoryItem] = (
"item" | InternalSlot.codec
).as[InventoryItem]
/**
* A `Codec` for `0x17` `ObjectCreateMessage` data.
*/
val codec : Codec[InventoryItem] = ("item" | InternalSlot.codec).as[InventoryItem]
/**
* A `Codec` for `0x18` `ObjectCreateDetailedMessage` data.
*/
val codec_detailed : Codec[InventoryItem] = ("item" | InternalSlot.codec_detailed).as[InventoryItem]
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation for a game object that can contain items.<br>
* <br>
* For whatever reason, these "lockers" are typically placed at the origin coordinates.
* @param inventory the items inside his locker
*/
final case class LockerContainerData(inventory : InventoryData) extends ConstructorData {
override def bitsize : Long = 105L + inventory.bitsize //81u + 2u + 21u + 1u
}
object LockerContainerData extends Marshallable[LockerContainerData] {
implicit val codec : Codec[LockerContainerData] = (
uint32 :: uint32 :: uint(17) :: //can substitute with PlacementData, if ever necessary
uint2L ::
uint(21) ::
bool ::
InventoryData.codec
).exmap[LockerContainerData] (
{
case 0 :: 0 :: 0 :: 3 :: 0 :: true :: inv :: HNil =>
Attempt.successful(LockerContainerData(inv))
case _ =>
Attempt.failure(Err("invalid locker container format"))
},
{
case LockerContainerData(inv) =>
Attempt.successful(0L :: 0L :: 0 :: 3 :: 0 :: true :: inv :: HNil)
}
)
}

View file

@ -0,0 +1,181 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.PacketHelpers
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, DecodeResult, Err}
import scodec.bits.BitVector
import scodec.codecs.{bool, either, uintL}
import shapeless.{::, HNil}
import scodec.codecs._
/**
* The parent information of a created object.<br>
* <br>
* In normal packet data order, there are two ways the parent object can be assigned.
* The first is an implicit association between a parent object and a child object that are both created at the same time.
* A player character object, for example, is initialized in the same breath as the objects in his inventory are initialized.
* A weapon object is constructed with an ammunition object already included within itself.
* The second is an explicit association between the child and the parent where the parent exists before the child is created.
* When a new inventory object is produced, it is usually assigned to some other existing object's inventory.
* That is the relationship to the role of "parent" that this object defines.
* As such, only its current unique identifier needs to be provided.
* If the parent can not be found, the child object is not created.<br>
* <br>
* A third form of parent object to child object association involves the impromptu assignment of an existing child to an existing parent.
* Since no objects are being created, that is unrelated to `ObjectCreateMessage`.
* Refer to `ObjectAttachMessage`, `MountVehicleMsg`, and `MountVehicleCargoMsg`.<br>
* <br>
* When associated, the child object is "attached" to the parent object at a specific location called a "slot."
* "Slots" are internal to the object and are (typically) invisible to the player.
* Any game object can possess any number of "slots" that serve specific purposes.
* Player objects have equipment holsters and grid inventory capacity.
* Weapon objects have magazine feed positions.
* Vehicle objects have seating for players and trunk inventory capacity.
* @param guid the GUID of the parent object
* @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent;
* encoded as the length field of a Pascal string
*/
final case class ObjectCreateMessageParent(guid : PlanetSideGUID,
slot : Int)
object ObjectCreateBase {
private type basePattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: BitVector :: HNil
private type parentPattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
/**
* Calculate the stream length in number of bits by factoring in the whole message in two portions.
* This process automates for: object encoding.<br>
* <br>
* Ignoring the parent data, constant field lengths have already been factored into the results.
* That includes:
* the length of the stream length field (32u),
* the object's class (11u),
* the object's GUID (16u),
* and the bit to determine if there will be parent data.
* In total, these fields form a known fixed length of 60u.
* @param parentInfo if defined, the relationship between this object and another object (its parent);
* information about the parent adds either 24u or 32u
* @param data if defined, the data used to construct this type of object;
* the data length is indeterminate until it is walked-through;
* note: the type is `StreamBitSize` as opposed to `ConstructorData`
* @return the total length of the resulting data stream in bits
*/
def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : StreamBitSize) : Long = {
//knowable length
val base : Long = if(parentInfo.isDefined) {
if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u))
}
else {
60L
}
base + data.bitsize
}
/**
* Take bit data and transform it into an object that expresses the important information of a game piece.
* This function is fail-safe because it catches errors involving bad parsing of the bitstream data.
* Generally, the `Exception` messages themselves are not useful here.
* The important parts are what the packet thought the object class should be and what it actually processed.
* @param objectClass the code for the type of object being constructed
* @param data the bitstream data
* @param getCodecFunc a lookup function that returns a `Codec` for this object class
* @return the optional constructed object
* @see `ObjectClass`
*/
def decodeData(objectClass : Int, data : BitVector, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : Option[ConstructorData] = {
var out : Option[ConstructorData] = None
try {
val outOpt : Option[DecodeResult[_]] = getCodecFunc(objectClass).decode(data).toOption
if(outOpt.isDefined)
out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern]
}
catch {
case _ : Exception =>
//catch and release, any sort of parse error
}
out
}
/**
* Take the important information of a game piece and transform it into bit data.
* This function is fail-safe because it catches errors involving bad parsing of the object data.
* Generally, the `Exception` messages themselves are not useful here.
* @param objClass the code for the type of object being deconstructed
* @param obj the object data
* @param getCodecFunc a lookup function that returns a `Codec` for this object class
* @return the bitstream data
* @see `ObjectClass`
*/
def encodeData(objClass : Int, obj : ConstructorData, getCodecFunc : (Int) => Codec[ConstructorData.genericPattern]) : BitVector = {
var out = BitVector.empty
try {
val outOpt : Option[BitVector] = getCodecFunc(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption
if(outOpt.isDefined)
out = outOpt.get
}
catch {
case _ : Exception =>
//catch and release, any sort of parse error
}
out
}
/**
* `Codec` for formatting around the lack of parent data in the stream.
*/
private val noParent : Codec[parentPattern] = (
("objectClass" | uintL(0xb)) :: //11u
("guid" | PlanetSideGUID.codec) //16u
).xmap[parentPattern](
{
case cls :: guid :: HNil =>
cls :: guid :: None :: HNil
}, {
case cls :: guid :: None :: HNil =>
cls :: guid :: HNil
}
)
/**
* `Codec` for reading and formatting parent data from the stream.
*/
private val parent : Codec[parentPattern] = (
("parentGuid" | PlanetSideGUID.codec) :: //16u
("objectClass" | uintL(0xb)) :: //11u
("guid" | PlanetSideGUID.codec) :: //16u
("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u
).xmap[parentPattern](
{
case pguid :: cls :: guid :: slot :: HNil =>
cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil
}, {
case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil =>
pguid :: cls :: guid :: slot :: HNil
}
)
/**
* `Codec` for handling the primary fields of both `ObjectCreateMessage` packets and `ObjectCreateDetailedMessage` packets.
*/
val baseCodec : Codec[basePattern] =
("streamLength" | uint32L) ::
(either(bool, parent, noParent).exmap[parentPattern] (
{
case Left(a :: b :: Some(c) :: HNil) =>
Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c)
case Right(a :: b :: None :: HNil) =>
Attempt.successful(a :: b :: None :: HNil) //false, _, _, None
// failure cases
case Left(_ :: _ :: None :: HNil) =>
Attempt.failure(Err("missing parent structure")) //true, _, _, None
case Right(_ :: _ :: Some(_) :: HNil) =>
Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c)
}, {
case a :: b :: Some(c) :: HNil =>
Attempt.successful(Left(a :: b :: Some(c) :: HNil))
case a :: b :: None :: HNil =>
Attempt.successful(Right(a :: b :: None :: HNil))
}
) :+
("data" | bits)) //greed is good
}

View file

@ -0,0 +1,165 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the player-mountable large field turrets deployed using an advanced adaptive construction engine.<br>
* <br>
* Field turrets are divided into the turret base, the mounted turret weapon, and the turret's ammunition.
* The ammunition is always the same regardless of which faction owns the turret.
* Turret bases and turret weapons are generally paired by the faction.<br>
* <br>
* If the turret has no `health`, it is rendered as destroyed.
* If the turret has no internal weapon, it is safest rendered as destroyed.
* Trying to fire a turret with no internal weapon will soft-lock the PlanetSide client.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param player_guid the player who owns this object
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mountable weapon
*/
final case class OneMannedFieldTurretData(deploy : ACEDeployableData,
player_guid : PlanetSideGUID, //might be able to re-package into field above
health : Int,
internals : Option[InternalSlot] = None
) extends ConstructorData {
override def bitsize : Long = {
val deploySize = deploy.bitsize
val internalSize = if(internals.isDefined) { ACEDeployableData.internalWeapon_bitsize + internals.get.bitsize } else { 0L }
38L + deploySize + internalSize //16u + 8u + 8u + 2u + 4u
}
}
object OneMannedFieldTurretData extends Marshallable[OneMannedFieldTurretData] {
/**
* Overloaded constructor that mandates information about the internal weapon of the field turret.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param player_guid the player who owns this object
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mountable weapon
* @return a `OneMannedFieldTurretData` object
*/
def apply(deploy : ACEDeployableData, player_guid : PlanetSideGUID, health : Int, internals : InternalSlot) : OneMannedFieldTurretData =
new OneMannedFieldTurretData(deploy, player_guid, health, Some(internals))
/**
* Prefabricated weapon data for a weaponless field turret mount (`portable_manned_turret`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
*
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def generic(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for the Terran Republic field turret, the Avenger (`portable_manned_turret_tr`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
*
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def avenger(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun_tr, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for the New Conglomerate field turret, the Osprey (`portable_manned_turret_vnc`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def osprey(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun_nc, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for the Vanu Soveriegnty field turret, the Orion (`portable_manned_turret_vs`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def orion(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.energy_gun_vs, wep_guid, 1,
WeaponData(wep_unk1, wep_unk2, ObjectClass.energy_gun_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
implicit val codec : Codec[OneMannedFieldTurretData] = (
("deploy" | ACEDeployableData.codec) ::
bool ::
("player_guid" | PlanetSideGUID.codec) ::
bool ::
("health" | uint8L) ::
uint2L ::
uint8L ::
bool ::
optional(bool, "internals" | ACEDeployableData.internalWeaponCodec)
).exmap[OneMannedFieldTurretData] (
{
case deploy :: false :: player :: false :: health :: 0 :: 0x1E :: false :: internals :: HNil =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(OneMannedFieldTurretData(deploy, player, newHealth, newInternals))
case _ =>
Attempt.failure(Err("invalid omft data format"))
},
{
case OneMannedFieldTurretData(deploy, player, health, internals) =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(deploy :: false :: player :: false :: newHealth :: 0 :: 0x1E :: false :: newInternals :: HNil)
case _ =>
Attempt.failure(Err("invalid omft data format"))
}
)
}

View file

@ -0,0 +1,74 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.types.Vector3
import scodec.codecs._
import scodec.Codec
/**
* A specific location and heading in game world coordinates and game world measurements.
* @param coord the xyz-coordinate location in the world
* @param roll the amount of roll that affects orientation
* @param pitch the amount of pitch that affects orientation
* @param yaw the amount of yaw that affects orientation
* @param init_move optional movement data that occurs upon placement
*/
final case class PlacementData(coord : Vector3,
roll : Int,
pitch : Int,
yaw : Int,
init_move : Option[Vector3] = None
) extends StreamBitSize {
override def bitsize : Long = {
val moveLength = if(init_move.isDefined) { 42 } else { 0 }
81L + moveLength
}
}
object PlacementData extends Marshallable[PlacementData] {
/**
* An abbreviated constructor for creating `PlacementData`, ignoring the `Vector3` for position data.
* @param x the x-coordinate location in the world
* @param y the y-coordinate location in the world
* @param z the z-coordinate location in the world
* @return a `PlacementData` object
*/
def apply(x : Float, y : Float, z : Float) : PlacementData =
new PlacementData(Vector3(x, y, z), 0, 0, 0)
/**
* An abbreviated constructor for creating `PlacementData`, ignoring the `Vector3` for position data, supplying other important fields.
* @param x the x-coordinate location in the world
* @param y the y-coordinate location in the world
* @param z the z-coordinate location in the world
* @param roll the amount of roll that affects orientation
* @param pitch the amount of pitch that affects orientation
* @param yaw the amount of yaw that affects orientation
* @return a `PlacementData` object
*/
def apply(x : Float, y : Float, z : Float, roll : Int, pitch : Int, yaw : Int) : PlacementData =
new PlacementData(Vector3(x, y, z), roll, pitch, yaw)
/**
* An abbreviated constructor for creating `PlacementData`, ignoring the `Vector3` for position data, supplying all other fields.
* @param x the x-coordinate location in the world
* @param y the y-coordinate location in the world
* @param z the z-coordinate location in the world
* @param roll the amount of roll that affects orientation
* @param pitch the amount of pitch that affects orientation
* @param yaw the amount of yaw that affects orientation
* @param init_move optional movement data that occurs upon placement
* @return a `PlacementData` object
*/
def apply(x : Float, y : Float, z : Float, roll : Int, pitch : Int, yaw : Int, init_move : Vector3) : PlacementData =
new PlacementData(Vector3(x, y, z), roll, pitch, yaw, Some(init_move))
implicit val codec : Codec[PlacementData] = (
("coord" | Vector3.codec_pos) ::
("roll" | uint8L) ::
("pitch" | uint8L) ::
("yaw" | uint8L) ::
optional(bool, "init_move" | Vector3.codec_vel)
).as[PlacementData]
}

View file

@ -7,56 +7,36 @@ import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of the REK portion of `ObjectCreateMessage` packet data.
* This data will help construct the "tool" called a Remote Electronics Kit.<br>
* <br>
* Of note is the first portion of the data which resembles the `WeaponData` format.
* @param unk na
* na
* @param unk1 na
* @param unk2 na;
* defaults to 0
* @see `DetailedREKData`
*/
final case class REKData(unk : Int) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = 67L
final case class REKData(unk1 : Int,
unk2 : Int,
unk3 : Int = 0
) extends ConstructorData {
override def bitsize : Long = 50L
}
object REKData extends Marshallable[REKData] {
implicit val codec : Codec[REKData] = (
("unk" | uint4L) ::
uint4L ::
uintL(20) ::
uint4L ::
uint16L ::
uint4L ::
uintL(15)
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(28) ::
("unk3" | uint4L) ::
uint(10)
).exmap[REKData] (
{
case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(REKData(code))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
case unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil =>
Attempt.successful(REKData(unk1, unk2, unk3))
case _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid rek data format"))
},
{
case REKData(code) =>
Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
}
).as[REKData]
/**
* Transform between REKData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[REKData])
case _ =>
Attempt.failure(Err("can not encode rek data"))
case REKData(unk1, unk2, unk3) =>
Attempt.successful(unk1 :: unk2 :: 0 :: unk3 :: 0 :: HNil)
}
)
}

View file

@ -17,19 +17,16 @@ import scodec.codecs._
* @param lower the lower configurable merit ribbon
* @param tos the top-most term of service merit ribbon
*/
final case class RibbonBars(upper : Long = 0xFFFFFFFFL,
middle : Long = 0xFFFFFFFFL,
lower : Long = 0xFFFFFFFFL,
tos : Long = 0xFFFFFFFFL) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
final case class RibbonBars(upper : Long = RibbonBars.noRibbon,
middle : Long = RibbonBars.noRibbon,
lower : Long = RibbonBars.noRibbon,
tos : Long = RibbonBars.noRibbon) extends StreamBitSize {
override def bitsize : Long = 128L
}
object RibbonBars extends Marshallable[RibbonBars] {
val noRibbon : Long = 0xFFFFFFFFL
implicit val codec : Codec[RibbonBars] = (
("upper" | uint32L) ::
("middle" | uint32L) ::

View file

@ -0,0 +1,34 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of simple objects that are spawned by the adaptive construction engine.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
*/
final case class SmallDeployableData(deploy : ACEDeployableData) extends ConstructorData {
override def bitsize : Long = deploy.bitsize + 1L
}
object SmallDeployableData extends Marshallable[SmallDeployableData] {
implicit val codec : Codec[SmallDeployableData] = (
("deploy" | ACEDeployableData.codec) ::
bool
).exmap[SmallDeployableData] (
{
case deploy :: false :: HNil =>
Attempt.successful(SmallDeployableData(deploy))
case _ =>
Attempt.failure(Err("invalid small deployable data format"))
},
{
case SmallDeployableData(deploy) =>
Attempt.successful(deploy :: false :: HNil)
}
)
}

View file

@ -0,0 +1,116 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the Spitfire-based small turrets deployed using an adaptive construction engine.<br>
* <br>
* The turret may contain substructure defining a weapon is a turret weapon contained within the turret itself.
* Furthermore, that turret-like weapon is loaded with turret-like ammunition.
* In other words, this outer turret can be considered a weapons platform for the inner turret weapon.<br>
* <br>
* If the turret has no `health`, it is rendered as destroyed.
* If the turret has no internal weapon, it is safest rendered as destroyed.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mounted weapon
*/
final case class SmallTurretData(deploy : ACEDeployableData,
health : Int,
internals : Option[InternalSlot] = None
) extends ConstructorData {
override def bitsize : Long = {
val deploySize = deploy.bitsize
val internalSize = if(internals.isDefined) { ACEDeployableData.internalWeapon_bitsize + internals.get.bitsize } else { 0L }
23L + deploySize + internalSize //1u + 8u + 7u + 4u + 2u + 1u
}
}
object SmallTurretData extends Marshallable[SmallTurretData] {
/**
* Overloaded constructor that mandates information about the internal weapon of the small turret.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
* @param internals data regarding the mounted weapon
* @return a `SmallTurretData` object
*/
def apply(deploy : ACEDeployableData, health : Int, internals : InternalSlot) : SmallTurretData =
new SmallTurretData(deploy, health, Some(internals))
/**
* Prefabricated weapon data for both Spitfires (`spitfire_turret`) and Shadow Turrets (`spitfire_cloaked`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def spitfire(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.spitfire_weapon, wep_guid, 0,
WeaponData(wep_unk1, wep_unk2, ObjectClass.spitfire_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
/**
* Prefabricated weapon data for Cerebus turrets (`spitfire_aa`).
* @param wep_guid the uid to assign to the weapon
* @param wep_unk1 na;
* used by `WeaponData`
* @param ammo_guid the uid to assign to the ammo
* @param wep_unk2 na;
* used by `WeaponData`
* @param ammo_unk na;
* used by `AmmoBoxData`
* @return an `InternalSlot` object
*/
def cerebus(wep_guid : PlanetSideGUID, wep_unk1 : Int, wep_unk2 : Int, ammo_guid : PlanetSideGUID, ammo_unk : Int) : InternalSlot =
InternalSlot(ObjectClass.spitfire_aa_weapon, wep_guid, 0,
WeaponData(wep_unk1, wep_unk2, ObjectClass.spitfire_aa_ammo, ammo_guid, 0,
AmmoBoxData(ammo_unk)
)
)
implicit val codec : Codec[SmallTurretData] = (
("deploy" | ACEDeployableData.codec) ::
bool ::
("health" | uint8L) ::
uintL(7) ::
uint4L ::
uint2L ::
optional(bool, "internals" | ACEDeployableData.internalWeaponCodec)
).exmap[SmallTurretData] (
{
case deploy :: false :: health :: 0 :: 0xF :: 0 :: internals :: HNil =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(SmallTurretData(deploy, newHealth, newInternals))
case _ =>
Attempt.failure(Err("invalid small turret data format"))
},
{
case SmallTurretData(deploy, health, internals) =>
var newHealth : Int = health
var newInternals : Option[InternalSlot] = internals
if(health == 0 || internals.isEmpty) {
newHealth = 0
newInternals = None
}
Attempt.successful(deploy :: false :: newHealth :: 0 :: 0xF :: 0 :: newInternals :: HNil)
}
)
}

View file

@ -2,17 +2,33 @@
package net.psforever.packet.game.objectcreate
/**
* Apply this trait to a class that needs to have its size in bits calculated.
* Apply this `trait` to a class that needs to have its size in bits calculated.
*/
trait StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* Performs a "sizeof()" analysis of the given object.<br>
* <br>
* The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields.
* For example, an `Int` is normally a 32u number;
* when parsed with a `uintL(7)`, it's length will be considered 7u.
* (Note: being permanently signed, an `scodec` 32u value must fit into a `Long` type.)
* @return the number of bits necessary to represent this object;
* For example, a traditional `Int` is normally a 32-bit number, often rendered as a `32u` number.
* When parsed with a `uintL(7)`, it's length will be considered 7 bits (`7u`).
* (Note: being permanently signed, an `scodec` value of `32u` or longer must fit into a `Long` type.)
* @return the number of bits necessary to measure an object of this class;
* defaults to `0L`
*/
def bitsize : Long = 0L
}
object StreamBitSize {
/**
* Calculate the bit size of a Pascal string.
* @param str a length-prefixed string
* @param width the width of the character encoding;
* defaults to 8 bits
* @return the size in bits
*/
def stringBitSize(str : String, width : Int = 8) : Long = {
val strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L
lenSize + (strlen * width)
}
}

View file

@ -0,0 +1,44 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of the tactical resonance area protection unit deployed using an advanced adaptive construction engine.
* Three metal beams, erect and tangled, block passage to enemies and their vehicles.
* @param deploy data common to objects spawned by the (advanced) adaptive construction engine
* @param health the amount of health the object has, as a percentage of a filled bar
*/
final case class TRAPData(deploy : ACEDeployableData,
health : Int
) extends ConstructorData {
override def bitsize : Long = {
23L + deploy.bitsize //8u + 7u + 4u + 3u + 1u
}
}
object TRAPData extends Marshallable[TRAPData] {
implicit val codec : Codec[TRAPData] = (
("deploy" | ACEDeployableData.codec) ::
bool ::
("health" | uint8L) ::
uint(7) ::
uint4L ::
uint(3)
).exmap[TRAPData] (
{
case deploy :: false :: health :: 0 :: 15 :: 0 :: HNil =>
Attempt.successful(TRAPData(deploy, health))
case _ =>
Attempt.failure(Err("invalid trap data format"))
},
{
case TRAPData(deploy, health) =>
Attempt.successful(deploy :: false :: health :: 0 :: 15 :: 0 :: HNil)
}
)
}

View file

@ -0,0 +1,86 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A representation of a projectile that the server must intentionally convey to players other than the shooter.
* @param pos where and how the projectile is oriented
* @param unk1 na
* @param unk2 na;
* data specific to the type of projectile(?)
*/
final case class TrackedProjectileData(pos : PlacementData,
unk1 : Int,
unk2 : Int
) extends ConstructorData {
override def bitsize : Long = 56L + pos.bitsize
}
object TrackedProjectileData extends Marshallable[TrackedProjectileData] {
final val oicw_projectile_data = 3355587
final val striker_missile_targetting_projectile_data = 6710918
final val hunter_seeker_missile_projectile_data = 10131913
final val starfire_projectile_data = 10131961
/**
* Overloaded constructor specifically for OICW projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def oicw(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, oicw_projectile_data)
/**
* Overloaded constructor specifically for Striker projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def striker(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, striker_missile_targetting_projectile_data)
/**
* Overloaded constructor specifically for Hunter Seeker (Phoenix) projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def hunter_seeker(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, hunter_seeker_missile_projectile_data)
/**
* Overloaded constructor specifically for Starfire projectiles.
* @param pos where and how the projectile is oriented
* @param unk na
* @return a `TrackedProjectileData` object
*/
def starfire(pos : PlacementData, unk : Int) : TrackedProjectileData =
new TrackedProjectileData(pos, unk, starfire_projectile_data)
implicit val codec : Codec[TrackedProjectileData] = (
("pos" | PlacementData.codec) ::
("unk1" | uint(3)) ::
uint4L ::
uint16L ::
("unk2" | uint24) ::
uint4L ::
uint(5)
).exmap[TrackedProjectileData] (
{
case pos :: unk1 :: 4 :: 0 :: unk2 :: 4 :: 0 :: HNil =>
Attempt.successful(TrackedProjectileData(pos, unk1, unk2))
case _ =>
Attempt.failure(Err("invalid projectile data format"))
},
{
case TrackedProjectileData(pos, unk1, unk2) =>
Attempt.successful(pos :: unk1 :: 4 :: 0 :: unk2 :: 4 :: 0 :: HNil)
}
)
}

View file

@ -3,87 +3,80 @@ package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable
import net.psforever.packet.game.PlanetSideGUID
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
/**
* A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data.
* This data will help construct a "loaded weapon" such as a Suppressor or a Gauss.<br>
* <br>
* The data for the weapons nests information for the default (current) type and number of ammunition in its magazine.
* This ammunition data essentially is the weapon's magazines as numbered slots.
* Having said that, this format only handles one type of ammunition at a time.
* Any weapon that has two types of ammunition simultaneously loaded, e.g., a Punisher, must be handled with another `Codec`.
* This functionality is unrelated to a weapon that switches ammunition type;
* a weapon with that behavior is handled perfectly fine using this `case class`.
* @param unk na
* @param ammo data regarding the currently loaded ammunition type and quantity
* @see AmmoBoxData
* Common uses include items deposited on the ground and items in another player's visible inventory (holsters).
* @param unk1 na;
* commonly 8
* @param unk2 na;
* commonly 12
* @param fire_mode the current mode of weapon's fire;
* zero-indexed
* @param ammo data regarding the currently loaded ammunition type
* @see `WeaponData`
* @see `AmmoBoxData`
*/
final case class WeaponData(unk : Int,
ammo : InternalSlot) extends ConstructorData {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @see AmmoBoxData.bitsize
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = 61L + ammo.bitsize
final case class WeaponData(unk1 : Int,
unk2 : Int,
fire_mode : Int,
ammo : InternalSlot
) extends ConstructorData {
override def bitsize : Long = 44L + ammo.bitsize
}
object WeaponData extends Marshallable[WeaponData] {
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.<br>
* <br>
* Exploration:<br>
* This class may need to be rewritten later to support objects spawned in the world environment.
* @param unk na
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
* @param unk1 na
* @param unk2 na
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the constructor data for the ammunition
* @return a WeaponData object
* @param ammo the ammunition object
* @return a `WeaponData` object
*/
def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData =
new WeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo))
def apply(unk1 : Int, unk2 : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData =
new WeaponData(unk1, unk2, 0, InternalSlot(cls, guid, parentSlot, ammo))
/**
* An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
* @param unk1 na
* @param unk2 na
* @param fire_mode data regarding the currently loaded ammunition type
* @param cls the code for the type of object (ammunition) being constructed
* @param guid the globally unique id assigned to the ammunition
* @param parentSlot the slot where the ammunition is to be installed in the weapon
* @param ammo the ammunition object
* @return a `WeaponData` object
*/
def apply(unk1 : Int, unk2 : Int, fire_mode : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData =
new WeaponData(unk1, unk2, fire_mode, InternalSlot(cls, guid, parentSlot, ammo))
implicit val codec : Codec[WeaponData] = (
("unk" | uint4L) ::
uint4L ::
uint24 ::
uint16L ::
uint2 ::
uint8 :: //size = 1 type of ammunition loaded
("unk1" | uint4L) ::
("unk2" | uint4L) ::
uint(20) ::
("fire_mode" | int(3)) ::
bool ::
bool ::
uint8L :: //size = 1 type of ammunition loaded
uint2 ::
("ammo" | InternalSlot.codec) ::
bool
).exmap[WeaponData] (
{
case code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil =>
Attempt.successful(WeaponData(code, ammo))
case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
case unk1 :: unk2 :: 0 :: fmode :: false :: true :: 1 :: 0 :: ammo :: false :: HNil =>
Attempt.successful(WeaponData(unk1, unk2, fmode, ammo))
case _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
Attempt.failure(Err("invalid weapon data format"))
},
{
case WeaponData(code, ammo) =>
Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil)
}
).as[WeaponData]
/**
* Transform between WeaponData and ConstructorData.
*/
val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] (
{
case x =>
Attempt.successful(Some(x.asInstanceOf[ConstructorData]))
},
{
case Some(x) =>
Attempt.successful(x.asInstanceOf[WeaponData])
case _ =>
Attempt.failure(Err("can not encode weapon data"))
case WeaponData(unk1, unk2, fmode, ammo) =>
Attempt.successful(unk1 :: unk2 :: 0 :: fmode :: false :: true :: 1 :: 0 :: ammo :: false :: HNil)
}
)
}

View file

@ -0,0 +1,16 @@
// Copyright (c) 2017 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs.uint2L
/**
* Values for two genders, Male and Female, starting at 1 = Male.
*/
object CharacterGender extends Enumeration(1) {
type Type = Value
val Male, Female = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
}

View file

@ -0,0 +1,19 @@
// Copyright (c) 2017 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
/**
* Values for the the different types of exo-suits that players can wear.
*/
object ExoSuitType extends Enumeration {
type Type = Value
val Agile,
Reinforced,
MAX,
Infiltration,
Standard= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
}

View file

@ -0,0 +1,21 @@
// Copyright (c) 2017 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
/**
* An `Enumeration` of the kinds of states applicable to the grenade animation.
*/
object GrenadeState extends Enumeration(1) {
type Type = Value
val Primed, //avatars and other depicted player characters
Thrown, //avatars only
None //non-actionable state of rest
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
val codec_2u = PacketHelpers.createEnumerationCodec(this, uint2L)
}

View file

@ -4,6 +4,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.ExoSuitType
import scodec.bits._
class ArmorChangedMessageTest extends Specification {
@ -13,7 +14,7 @@ class ArmorChangedMessageTest extends Specification {
PacketCoding.DecodePacket(string).require match {
case ArmorChangedMessage(player_guid, armor, subtype) =>
player_guid mustEqual PlanetSideGUID(273)
armor mustEqual 2
armor mustEqual ExoSuitType.MAX
subtype mustEqual 3
case _ =>
ko
@ -21,7 +22,7 @@ class ArmorChangedMessageTest extends Specification {
}
"encode" in {
val msg = ArmorChangedMessage(PlanetSideGUID(273), 2, 3)
val msg = ArmorChangedMessage(PlanetSideGUID(273), ExoSuitType.MAX, 3)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.Vector3
import scodec.bits._
class AvatarDeadStateMessageTest extends Specification {
val string = hex"ad3c1260801c12608009f99861fb0741e040000010"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case AvatarDeadStateMessage(unk1,unk2,unk3,pos,unk4,unk5) =>
unk1 mustEqual 1
unk2 mustEqual 300000
unk3 mustEqual 300000
pos mustEqual Vector3(6552.617f,4602.375f,60.90625f)
unk4 mustEqual 2
unk5 mustEqual true
case _ =>
ko
}
}
"encode" in {
val msg = AvatarDeadStateMessage(1, 300000, 300000, Vector3(6552.617f,4602.375f,60.90625f), 2, true)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -4,6 +4,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.GrenadeState
import scodec.bits._
class AvatarGrenadeStateMessageTest extends Specification {
@ -13,14 +14,14 @@ class AvatarGrenadeStateMessageTest extends Specification {
PacketCoding.DecodePacket(string).require match {
case AvatarGrenadeStateMessage(player_guid, state) =>
player_guid mustEqual PlanetSideGUID(4570)
state mustEqual GrenadeState.PRIMED
state mustEqual GrenadeState.Primed
case _ =>
ko
}
}
"encode" in {
val msg = AvatarGrenadeStateMessage(PlanetSideGUID(4570), GrenadeState.PRIMED)
val msg = AvatarGrenadeStateMessage(PlanetSideGUID(4570), GrenadeState.Primed)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string

View file

@ -0,0 +1,332 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits._
class BattleplanMessageTest extends Specification {
val string_start = hex"b3 3a197902 94 59006500740041006e006f0074006800650072004600610069006c0075007200650041006c007400 0000 01 e0"
val string_stop = hex"b3 3a197902 94 59006500740041006e006f0074006800650072004600610069006c0075007200650041006c007400 0000 01 f0"
val string_line = hex"b3 85647702 8c 4f0075007400730074006100620075006c006f0075007300 0a00 20 2aba2b4aae8bd2aba334aae8dd2aca3b4ab28fd2aca414ab29152aca474ab292d2ada4d4ab69452ada534ab695d2ada594ab696d2ada5d4ab697d2ada614ab698d2ada654ab699d2ada694ab69ad2aea6d4aba9bd2aea714aba9cd2aea754aba9dd2aea794aba9ed"
val string_style = hex"b3856477028c4f0075007400730074006100620075006c006f00750073000a00031d22aba2f4aae8cd"
val string_message = hex"b3 85647702 8c 4f0075007400730074006100620075006c006f0075007300 0a00 01 6aba2b5011c0480065006c006c006f00200041007500720061007800690073002100"
//0xb3856477028c4f0075007400730074006100620075006c006f00750073000a000130
"decode (start)" in {
PacketCoding.DecodePacket(string_start).require match {
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
char_id mustEqual 41490746
player_name mustEqual "YetAnotherFailureAlt"
zone_id mustEqual PlanetSideGUID(0)
diagrams.size mustEqual 1
//0
diagrams.head.action mustEqual DiagramActionCode.StartDrawing
diagrams.head.stroke.isDefined mustEqual false
case _ =>
ko
}
}
"decode (end)" in {
PacketCoding.DecodePacket(string_stop).require match {
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
char_id mustEqual 41490746
player_name mustEqual "YetAnotherFailureAlt"
zone_id mustEqual PlanetSideGUID(0)
diagrams.size mustEqual 1
//0
diagrams.head.action mustEqual DiagramActionCode.StopDrawing
diagrams.head.stroke.isDefined mustEqual false
case _ =>
ko
}
}
"decode (stop)" in {
PacketCoding.DecodePacket(string_line).require match {
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
char_id mustEqual 41378949
player_name mustEqual "Outstabulous"
zone_id mustEqual PlanetSideGUID(10)
diagrams.size mustEqual 32
//0
diagrams.head.action mustEqual DiagramActionCode.Vertex
diagrams.head.stroke.isDefined mustEqual true
diagrams.head.stroke.get.isInstanceOf[Vertex] mustEqual true
diagrams.head.stroke.get.asInstanceOf[Vertex].x mustEqual 7512.0f
diagrams.head.stroke.get.asInstanceOf[Vertex].y mustEqual 6312.0f
//1
diagrams(1).action mustEqual DiagramActionCode.Vertex
diagrams(1).stroke.get.asInstanceOf[Vertex].x mustEqual 7512.0f
diagrams(1).stroke.get.asInstanceOf[Vertex].y mustEqual 6328.0f
//2
diagrams(2).action mustEqual DiagramActionCode.Vertex
diagrams(2).stroke.get.asInstanceOf[Vertex].x mustEqual 7512.0f
diagrams(2).stroke.get.asInstanceOf[Vertex].y mustEqual 6344.0f
//3
diagrams(3).action mustEqual DiagramActionCode.Vertex
diagrams(3).stroke.get.asInstanceOf[Vertex].x mustEqual 7512.0f
diagrams(3).stroke.get.asInstanceOf[Vertex].y mustEqual 6360.0f
//4
diagrams(4).action mustEqual DiagramActionCode.Vertex
diagrams(4).stroke.get.asInstanceOf[Vertex].x mustEqual 7520.0f
diagrams(4).stroke.get.asInstanceOf[Vertex].y mustEqual 6376.0f
//5
diagrams(5).action mustEqual DiagramActionCode.Vertex
diagrams(5).stroke.get.asInstanceOf[Vertex].x mustEqual 7520.0f
diagrams(5).stroke.get.asInstanceOf[Vertex].y mustEqual 6392.0f
//6
diagrams(6).action mustEqual DiagramActionCode.Vertex
diagrams(6).stroke.get.asInstanceOf[Vertex].x mustEqual 7520.0f
diagrams(6).stroke.get.asInstanceOf[Vertex].y mustEqual 6400.0f
//7
diagrams(7).action mustEqual DiagramActionCode.Vertex
diagrams(7).stroke.get.asInstanceOf[Vertex].x mustEqual 7520.0f
diagrams(7).stroke.get.asInstanceOf[Vertex].y mustEqual 6416.0f
//8
diagrams(8).action mustEqual DiagramActionCode.Vertex
diagrams(8).stroke.get.asInstanceOf[Vertex].x mustEqual 7520.0f
diagrams(8).stroke.get.asInstanceOf[Vertex].y mustEqual 6424.0f
//9
diagrams(9).action mustEqual DiagramActionCode.Vertex
diagrams(9).stroke.get.asInstanceOf[Vertex].x mustEqual 7520.0f
diagrams(9).stroke.get.asInstanceOf[Vertex].y mustEqual 6440.0f
//10
diagrams(10).action mustEqual DiagramActionCode.Vertex
diagrams(10).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(10).stroke.get.asInstanceOf[Vertex].y mustEqual 6448.0f
//11
diagrams(11).action mustEqual DiagramActionCode.Vertex
diagrams(11).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(11).stroke.get.asInstanceOf[Vertex].y mustEqual 6464.0f
//12
diagrams(12).action mustEqual DiagramActionCode.Vertex
diagrams(12).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(12).stroke.get.asInstanceOf[Vertex].y mustEqual 6472.0f
//13
diagrams(13).action mustEqual DiagramActionCode.Vertex
diagrams(13).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(13).stroke.get.asInstanceOf[Vertex].y mustEqual 6488.0f
//14
diagrams(14).action mustEqual DiagramActionCode.Vertex
diagrams(14).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(14).stroke.get.asInstanceOf[Vertex].y mustEqual 6496.0f
//15
diagrams(15).action mustEqual DiagramActionCode.Vertex
diagrams(15).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(15).stroke.get.asInstanceOf[Vertex].y mustEqual 6504.0f
//16
diagrams(16).action mustEqual DiagramActionCode.Vertex
diagrams(16).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(16).stroke.get.asInstanceOf[Vertex].y mustEqual 6512.0f
//17
diagrams(17).action mustEqual DiagramActionCode.Vertex
diagrams(17).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(17).stroke.get.asInstanceOf[Vertex].y mustEqual 6520.0f
//18
diagrams(18).action mustEqual DiagramActionCode.Vertex
diagrams(18).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(18).stroke.get.asInstanceOf[Vertex].y mustEqual 6528.0f
//19
diagrams(19).action mustEqual DiagramActionCode.Vertex
diagrams(19).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(19).stroke.get.asInstanceOf[Vertex].y mustEqual 6536.0f
//20
diagrams(20).action mustEqual DiagramActionCode.Vertex
diagrams(20).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(20).stroke.get.asInstanceOf[Vertex].y mustEqual 6544.0f
//21
diagrams(21).action mustEqual DiagramActionCode.Vertex
diagrams(21).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(21).stroke.get.asInstanceOf[Vertex].y mustEqual 6552.0f
//22
diagrams(22).action mustEqual DiagramActionCode.Vertex
diagrams(22).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(22).stroke.get.asInstanceOf[Vertex].y mustEqual 6560.0f
//23
diagrams(23).action mustEqual DiagramActionCode.Vertex
diagrams(23).stroke.get.asInstanceOf[Vertex].x mustEqual 7528.0f
diagrams(23).stroke.get.asInstanceOf[Vertex].y mustEqual 6568.0f
//24
diagrams(24).action mustEqual DiagramActionCode.Vertex
diagrams(24).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(24).stroke.get.asInstanceOf[Vertex].y mustEqual 6576.0f
//25
diagrams(25).action mustEqual DiagramActionCode.Vertex
diagrams(25).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(25).stroke.get.asInstanceOf[Vertex].y mustEqual 6584.0f
//26
diagrams(26).action mustEqual DiagramActionCode.Vertex
diagrams(26).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(26).stroke.get.asInstanceOf[Vertex].y mustEqual 6592.0f
//27
diagrams(27).action mustEqual DiagramActionCode.Vertex
diagrams(27).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(27).stroke.get.asInstanceOf[Vertex].y mustEqual 6600.0f
//28
diagrams(28).action mustEqual DiagramActionCode.Vertex
diagrams(28).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(28).stroke.get.asInstanceOf[Vertex].y mustEqual 6608.0f
//29
diagrams(29).action mustEqual DiagramActionCode.Vertex
diagrams(29).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(29).stroke.get.asInstanceOf[Vertex].y mustEqual 6616.0f
//30
diagrams(30).action mustEqual DiagramActionCode.Vertex
diagrams(30).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(30).stroke.get.asInstanceOf[Vertex].y mustEqual 6624.0f
//31
diagrams(31).action mustEqual DiagramActionCode.Vertex
diagrams(31).stroke.get.asInstanceOf[Vertex].x mustEqual 7536.0f
diagrams(31).stroke.get.asInstanceOf[Vertex].y mustEqual 6632.0f
case _ =>
ko
}
}
"decode (style)" in {
PacketCoding.DecodePacket(string_style).require match {
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
char_id mustEqual 41378949
player_name mustEqual "Outstabulous"
zone_id mustEqual PlanetSideGUID(10)
diagrams.size mustEqual 3
//0
diagrams.head.action mustEqual DiagramActionCode.Style
diagrams.head.stroke.isDefined mustEqual true
diagrams.head.stroke.get.isInstanceOf[Style] mustEqual true
diagrams.head.stroke.get.asInstanceOf[Style].thickness mustEqual 3.0f
diagrams.head.stroke.get.asInstanceOf[Style].color mustEqual 2
//1
diagrams(1).action mustEqual DiagramActionCode.Vertex
diagrams(1).stroke.get.asInstanceOf[Vertex].x mustEqual 7512.0f
diagrams(1).stroke.get.asInstanceOf[Vertex].y mustEqual 6328.0f
//2
diagrams(2).action mustEqual DiagramActionCode.Vertex
diagrams(2).stroke.get.asInstanceOf[Vertex].x mustEqual 7512.0f
diagrams(2).stroke.get.asInstanceOf[Vertex].y mustEqual 6344.0f
case _ =>
ko
}
}
"decode (message)" in {
PacketCoding.DecodePacket(string_message).require match {
case BattleplanMessage(char_id, player_name, zone_id, diagrams) =>
char_id mustEqual 41378949
player_name mustEqual "Outstabulous"
zone_id mustEqual PlanetSideGUID(10)
diagrams.size mustEqual 1
//0
diagrams.head.action mustEqual DiagramActionCode.DrawString
diagrams.head.stroke.isDefined mustEqual true
diagrams.head.stroke.get.isInstanceOf[DrawString] mustEqual true
diagrams.head.stroke.get.asInstanceOf[DrawString].x mustEqual 7512.0f
diagrams.head.stroke.get.asInstanceOf[DrawString].y mustEqual 6312.0f
diagrams.head.stroke.get.asInstanceOf[DrawString].color mustEqual 2
diagrams.head.stroke.get.asInstanceOf[DrawString].channel mustEqual 0
diagrams.head.stroke.get.asInstanceOf[DrawString].message mustEqual "Hello Auraxis!"
case _ =>
ko
}
}
"encode (start)" in {
val msg = BattleplanMessage(
41490746,
"YetAnotherFailureAlt",
PlanetSideGUID(0),
BattleDiagramAction(DiagramActionCode.StartDrawing) ::
Nil
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_start
}
"encode (stop)" in {
val msg = BattleplanMessage(
41490746,
"YetAnotherFailureAlt",
PlanetSideGUID(0),
BattleDiagramAction(DiagramActionCode.StopDrawing) ::
Nil
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_stop
}
"encode (line)" in {
val msg = BattleplanMessage(
41378949,
"Outstabulous",
PlanetSideGUID(10),
BattleDiagramAction.vertex(7512.0f, 6312.0f) ::
BattleDiagramAction.vertex(7512.0f, 6328.0f) ::
BattleDiagramAction.vertex(7512.0f, 6344.0f) ::
BattleDiagramAction.vertex(7512.0f, 6360.0f) ::
BattleDiagramAction.vertex(7520.0f, 6376.0f) ::
BattleDiagramAction.vertex(7520.0f, 6392.0f) ::
BattleDiagramAction.vertex(7520.0f, 6400.0f) ::
BattleDiagramAction.vertex(7520.0f, 6416.0f) ::
BattleDiagramAction.vertex(7520.0f, 6424.0f) ::
BattleDiagramAction.vertex(7520.0f, 6440.0f) ::
BattleDiagramAction.vertex(7528.0f, 6448.0f) ::
BattleDiagramAction.vertex(7528.0f, 6464.0f) ::
BattleDiagramAction.vertex(7528.0f, 6472.0f) ::
BattleDiagramAction.vertex(7528.0f, 6488.0f) ::
BattleDiagramAction.vertex(7528.0f, 6496.0f) ::
BattleDiagramAction.vertex(7528.0f, 6504.0f) ::
BattleDiagramAction.vertex(7528.0f, 6512.0f) ::
BattleDiagramAction.vertex(7528.0f, 6520.0f) ::
BattleDiagramAction.vertex(7528.0f, 6528.0f) ::
BattleDiagramAction.vertex(7528.0f, 6536.0f) ::
BattleDiagramAction.vertex(7528.0f, 6544.0f) ::
BattleDiagramAction.vertex(7528.0f, 6552.0f) ::
BattleDiagramAction.vertex(7528.0f, 6560.0f) ::
BattleDiagramAction.vertex(7528.0f, 6568.0f) ::
BattleDiagramAction.vertex(7536.0f, 6576.0f) ::
BattleDiagramAction.vertex(7536.0f, 6584.0f) ::
BattleDiagramAction.vertex(7536.0f, 6592.0f) ::
BattleDiagramAction.vertex(7536.0f, 6600.0f) ::
BattleDiagramAction.vertex(7536.0f, 6608.0f) ::
BattleDiagramAction.vertex(7536.0f, 6616.0f) ::
BattleDiagramAction.vertex(7536.0f, 6624.0f) ::
BattleDiagramAction.vertex(7536.0f, 6632.0f) ::
Nil
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_line
}
"encode (style)" in {
val msg = BattleplanMessage(
41378949,
"Outstabulous",
PlanetSideGUID(10),
BattleDiagramAction.style(3.0f, 2) ::
BattleDiagramAction.vertex(7512.0f, 6328.0f) ::
BattleDiagramAction.vertex(7512.0f, 6344.0f) ::
Nil
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_style
}
"encode (message)" in {
val msg = BattleplanMessage(
41378949,
"Outstabulous",
PlanetSideGUID(10),
BattleDiagramAction.drawString(7512.0f, 6312.0f, 2, 0, "Hello Auraxis!") :: Nil
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_message
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits._
class BeginZoningMessageTest extends Specification {
val string = hex"43" //yes, just the opcode
"decode" in {
PacketCoding.DecodePacket(string).require match {
case BeginZoningMessage() =>
ok
case _ =>
ko
}
}
"encode" in {
val msg = BeginZoningMessage()
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,29 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits._
class ChildObjectStateMessageTest extends Specification {
val string = hex"1E 640B 06 47"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case ChildObjectStateMessage(object_guid, pitch, yaw) =>
object_guid mustEqual PlanetSideGUID(2916)
pitch mustEqual 6
yaw mustEqual 71
case _ =>
ko
}
}
"encode" in {
val msg = ChildObjectStateMessage(PlanetSideGUID(2916), 6, 71)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,37 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.Vector3
import scodec.bits._
class DeployObjectMessageTest extends Specification {
//fake data; see comments in packet; this test exists to maintain code coverage
val string = hex"5D 0000 00000000 00000 00000 0000 00 00 00 00000000"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case DeployObjectMessage(guid, unk1, pos, unk2, unk3, unk4, unk5) =>
guid mustEqual PlanetSideGUID(0)
unk1 mustEqual 0L
pos.x mustEqual 0f
pos.y mustEqual 0f
pos.z mustEqual 0f
unk2 mustEqual 0
unk3 mustEqual 0
unk4 mustEqual 0
unk5 mustEqual 0L
case _ =>
ko
}
}
"encode" in {
val msg = DeployObjectMessage(PlanetSideGUID(0), 0L, Vector3(0f, 0f, 0f), 0, 0, 0, 0L)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game.{PlanetSideGUID, DeploymentAction, DeployableIcon, DeployableInfo, DeployableObjectsInfoMessage}
import net.psforever.types.Vector3
import scodec.bits._
class DeployableObjectsInfoMessageTest extends Specification {
val string = hex"76 00 80 00 00 31 85 41 CF D3 7E B3 34 00 E6 30 48" //this was a TRAP @ Ogma, Forseral
"decode" in {
PacketCoding.DecodePacket(string).require match {
case DeployableObjectsInfoMessage(action, list) =>
action mustEqual DeploymentAction.Dismiss
list.size mustEqual 1
//0
list.head.object_guid mustEqual PlanetSideGUID(2659)
list.head.icon mustEqual DeployableIcon.TRAP
list.head.pos.x mustEqual 3572.4453f
list.head.pos.y mustEqual 3277.9766f
list.head.pos.z mustEqual 114.0f
list.head.player_guid mustEqual PlanetSideGUID(2502)
case _ =>
ko
}
}
"encode" in {
val msg = DeployableObjectsInfoMessage(
DeploymentAction.Dismiss,
DeployableInfo(PlanetSideGUID(2659), DeployableIcon.TRAP, Vector3(3572.4453f, 3277.9766f, 114.0f), PlanetSideGUID(2502))
)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.types.Vector3
import scodec.bits._
class LashMessageTest extends Specification {
val string = hex"4f644a82e2c297a738a1ed0b01b886c0"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case LashMessage(seq_time,player,victim,bullet,pos,unk1) =>
seq_time mustEqual 356
player mustEqual PlanetSideGUID(2858)
victim mustEqual PlanetSideGUID(2699)
bullet mustEqual PlanetSideGUID(40030)
pos mustEqual Vector3(5903.7656f,3456.5156f,111.53125f)
unk1 mustEqual 0
case _ =>
ko
}
}
"encode" in {
val msg = LashMessage(356, PlanetSideGUID(2858), PlanetSideGUID(2699), PlanetSideGUID(40030), Vector3(5903.7656f,3456.5156f,111.53125f), 0)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,30 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits._
class MailMessageTest extends Specification {
//we've never received this packet before so this whole test is faked
val string = hex"F1 86466174654A489250726 96F72697479204D61696C2054657374 8E48656C6C6F204175726178697321"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case MailMessage(sender, subject, msg) =>
sender mustEqual "FateJH"
subject mustEqual "Priority Mail Test"
msg mustEqual "Hello Auraxis!"
case _ =>
ko
}
}
"encode" in {
val msg = MailMessage("FateJH", "Priority Mail Test", "Hello Auraxis!")
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,423 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
import scodec.bits._
class ObjectCreateDetailedMessageTest extends Specification {
val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " //fake data?
val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //fake data
//val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16)
var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00"
val string_detonater = hex"18 87000000 6506 EA8 7420 80 8000000200008"
val string_ace = hex"18 87000000 1006 100 C70B 80 8800000200008"
val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000"
val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000"
val string_punisher = hex"18 27010000 2580 612 a706 82 080000020000c08 1c13a0d01900000780 13a4701a072000000800"
val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000"
val string_boomer_trigger = hex"18 87000000 6304CA8760B 80 C800000200008"
val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
"decode (2)" in {
//an invalid bit representation will fail to turn into an object
PacketCoding.DecodePacket(packet2).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 248
cls mustEqual ObjectClass.avatar
guid mustEqual PlanetSideGUID(2497)
parent mustEqual None
data.isDefined mustEqual false
case _ =>
ko
}
}
"decode (detonater)" in {
PacketCoding.DecodePacket(string_detonater).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 135
cls mustEqual ObjectClass.command_detonater
guid mustEqual PlanetSideGUID(8308)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(3530)
parent.get.slot mustEqual 0
data.isDefined mustEqual true
data.get.isInstanceOf[DetailedCommandDetonaterData] mustEqual true
case _ =>
ko
}
}
"decode (ace)" in {
PacketCoding.DecodePacket(string_ace).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 135
cls mustEqual ObjectClass.ace
guid mustEqual PlanetSideGUID(3015)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(3104)
parent.get.slot mustEqual 0
data.isDefined mustEqual true
data.get.isInstanceOf[DetailedACEData] mustEqual true
data.get.asInstanceOf[DetailedACEData].unk mustEqual 8
case _ =>
ko
}
}
"decode (9mm)" in {
PacketCoding.DecodePacket(string_9mm).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 124
cls mustEqual ObjectClass.bullet_9mm
guid mustEqual PlanetSideGUID(1280)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 33
data.isDefined mustEqual true
data.get.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
case _ =>
ko
}
}
"decode (gauss)" in {
PacketCoding.DecodePacket(string_gauss).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 220
cls mustEqual ObjectClass.gauss
guid mustEqual PlanetSideGUID(1465)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 2
data.isDefined mustEqual true
val obj_wep = data.get.asInstanceOf[DetailedWeaponData]
obj_wep.unk mustEqual 4
val obj_ammo = obj_wep.ammo
obj_ammo.objectClass mustEqual 28
obj_ammo.guid mustEqual PlanetSideGUID(1286)
obj_ammo.parentSlot mustEqual 0
obj_ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 30
case _ =>
ko
}
}
"decode (punisher)" in {
PacketCoding.DecodePacket(string_punisher).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 295
cls mustEqual ObjectClass.punisher
guid mustEqual PlanetSideGUID(1703)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 2
data.isDefined mustEqual true
val obj_wep = data.get.asInstanceOf[DetailedConcurrentFeedWeaponData]
obj_wep.unk1 mustEqual 0
obj_wep.unk2 mustEqual 8
val obj_ammo = obj_wep.ammo
obj_ammo.size mustEqual 2
obj_ammo.head.objectClass mustEqual ObjectClass.bullet_9mm
obj_ammo.head.guid mustEqual PlanetSideGUID(1693)
obj_ammo.head.parentSlot mustEqual 0
obj_ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 30
obj_ammo(1).objectClass mustEqual ObjectClass.jammer_cartridge
obj_ammo(1).guid mustEqual PlanetSideGUID(1564)
obj_ammo(1).parentSlot mustEqual 1
obj_ammo(1).obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1
case _ =>
ko
}
}
"decode (rek)" in {
PacketCoding.DecodePacket(string_rek).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 151
cls mustEqual ObjectClass.remote_electronics_kit
guid mustEqual PlanetSideGUID(1439)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 1
data.isDefined mustEqual true
data.get.asInstanceOf[DetailedREKData].unk mustEqual 4
case _ =>
ko
}
}
"decode (boomer trigger)" in {
PacketCoding.DecodePacket(string_boomer_trigger).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 135
cls mustEqual ObjectClass.boomer_trigger
guid mustEqual PlanetSideGUID(2934)
parent.isDefined mustEqual true
parent.get.guid mustEqual PlanetSideGUID(2502)
parent.get.slot mustEqual 0
data.isDefined mustEqual true
data.get.isInstanceOf[DetailedBoomerTriggerData] mustEqual true
case _ =>
ko
}
}
"decode (character)" in {
PacketCoding.DecodePacket(string_testchar).require match {
case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
len mustEqual 3159
cls mustEqual ObjectClass.avatar
guid mustEqual PlanetSideGUID(75)
parent.isDefined mustEqual false
data.isDefined mustEqual true
val char = data.get.asInstanceOf[DetailedCharacterData]
char.appearance.pos.coord.x mustEqual 3674.8438f
char.appearance.pos.coord.y mustEqual 2726.789f
char.appearance.pos.coord.z mustEqual 91.15625f
char.appearance.pos.roll mustEqual 0
char.appearance.pos.pitch mustEqual 0
char.appearance.pos.yaw mustEqual 19
char.appearance.basic_appearance.name mustEqual "IlllIIIlllIlIllIlllIllI"
char.appearance.basic_appearance.faction mustEqual PlanetSideEmpire.VS
char.appearance.basic_appearance.sex mustEqual CharacterGender.Female
char.appearance.basic_appearance.head mustEqual 41
char.appearance.basic_appearance.voice mustEqual 1 //female 1
char.appearance.voice2 mustEqual 3
char.appearance.black_ops mustEqual false
char.appearance.jammered mustEqual false
char.appearance.exosuit mustEqual ExoSuitType.Standard
char.appearance.outfit_name mustEqual ""
char.appearance.outfit_logo mustEqual 0
char.appearance.backpack mustEqual false
char.appearance.facingPitch mustEqual 127
char.appearance.facingYawUpper mustEqual 181
char.appearance.lfs mustEqual true
char.appearance.grenade_state mustEqual GrenadeState.None
char.appearance.is_cloaking mustEqual false
char.appearance.charging_pose mustEqual false
char.appearance.on_zipline mustEqual false
char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.tos mustEqual 0xFFFFFFFFL //none
char.healthMax mustEqual 100
char.health mustEqual 100
char.armor mustEqual 50 //standard exosuit value
char.unk1 mustEqual 1
char.unk2 mustEqual 7
char.unk3 mustEqual 7
char.staminaMax mustEqual 100
char.stamina mustEqual 100
char.unk4 mustEqual 28
char.unk5 mustEqual 4
char.unk6 mustEqual 44
char.unk7 mustEqual 84
char.unk8 mustEqual 104
char.unk9 mustEqual 1900
char.firstTimeEvents.size mustEqual 4
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
char.firstTimeEvents(2) mustEqual "used_beamer"
char.firstTimeEvents(3) mustEqual "map13"
char.tutorials.size mustEqual 0
char.inventory.isDefined mustEqual true
val inventory = char.inventory.get.contents
inventory.size mustEqual 10
//0
inventory.head.item.objectClass mustEqual ObjectClass.beamer
inventory.head.item.guid mustEqual PlanetSideGUID(76)
inventory.head.item.parentSlot mustEqual 0
var wep = inventory.head.item.obj.asInstanceOf[DetailedWeaponData]
wep.ammo.objectClass mustEqual ObjectClass.energy_cell
wep.ammo.guid mustEqual PlanetSideGUID(77)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 16
//1
inventory(1).item.objectClass mustEqual ObjectClass.suppressor
inventory(1).item.guid mustEqual PlanetSideGUID(78)
inventory(1).item.parentSlot mustEqual 2
wep = inventory(1).item.obj.asInstanceOf[DetailedWeaponData]
wep.ammo.objectClass mustEqual ObjectClass.bullet_9mm
wep.ammo.guid mustEqual PlanetSideGUID(79)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 25
//2
inventory(2).item.objectClass mustEqual ObjectClass.forceblade
inventory(2).item.guid mustEqual PlanetSideGUID(80)
inventory(2).item.parentSlot mustEqual 4
wep = inventory(2).item.obj.asInstanceOf[DetailedWeaponData]
wep.ammo.objectClass mustEqual ObjectClass.melee_ammo
wep.ammo.guid mustEqual PlanetSideGUID(81)
wep.ammo.parentSlot mustEqual 0
wep.ammo.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1
//3
inventory(3).item.objectClass mustEqual ObjectClass.locker_container
inventory(3).item.guid mustEqual PlanetSideGUID(82)
inventory(3).item.parentSlot mustEqual 5
inventory(3).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 1
//4
inventory(4).item.objectClass mustEqual ObjectClass.bullet_9mm
inventory(4).item.guid mustEqual PlanetSideGUID(83)
inventory(4).item.parentSlot mustEqual 6
inventory(4).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//5
inventory(5).item.objectClass mustEqual ObjectClass.bullet_9mm
inventory(5).item.guid mustEqual PlanetSideGUID(84)
inventory(5).item.parentSlot mustEqual 9
inventory(5).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//6
inventory(6).item.objectClass mustEqual ObjectClass.bullet_9mm
inventory(6).item.guid mustEqual PlanetSideGUID(85)
inventory(6).item.parentSlot mustEqual 12
inventory(6).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//7
inventory(7).item.objectClass mustEqual ObjectClass.bullet_9mm_AP
inventory(7).item.guid mustEqual PlanetSideGUID(86)
inventory(7).item.parentSlot mustEqual 33
inventory(7).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//8
inventory(8).item.objectClass mustEqual ObjectClass.energy_cell
inventory(8).item.guid mustEqual PlanetSideGUID(87)
inventory(8).item.parentSlot mustEqual 36
inventory(8).item.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 50
//9
inventory(9).item.objectClass mustEqual ObjectClass.remote_electronics_kit
inventory(9).item.guid mustEqual PlanetSideGUID(88)
inventory(9).item.parentSlot mustEqual 39
//the rek has data but none worth testing here
char.drawn_slot mustEqual DrawnSlot.Pistol1
case _ =>
ko
}
}
"encode (2)" in {
//the lack of an object will fail to turn into a bad bitstream
val msg = ObjectCreateDetailedMessage(0L, ObjectClass.avatar, PlanetSideGUID(2497), None, None)
PacketCoding.EncodePacket(msg).isFailure mustEqual true
}
"encode (detonater)" in {
val obj = DetailedCommandDetonaterData()
val msg = ObjectCreateDetailedMessage(ObjectClass.command_detonater, PlanetSideGUID(8308), ObjectCreateMessageParent(PlanetSideGUID(3530), 0), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_detonater
}
"encode (ace)" in {
val obj = DetailedACEData(8)
val msg = ObjectCreateDetailedMessage(ObjectClass.ace, PlanetSideGUID(3015), ObjectCreateMessageParent(PlanetSideGUID(3104), 0), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_ace
}
"encode (9mm)" in {
val obj = DetailedAmmoBoxData(8, 50)
val msg = ObjectCreateDetailedMessage(ObjectClass.bullet_9mm, PlanetSideGUID(1280), ObjectCreateMessageParent(PlanetSideGUID(75), 33), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_9mm
}
"encode (gauss)" in {
val obj = DetailedWeaponData(4, ObjectClass.bullet_9mm, PlanetSideGUID(1286), 0, DetailedAmmoBoxData(8, 30))
val msg = ObjectCreateDetailedMessage(ObjectClass.gauss, PlanetSideGUID(1465), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_gauss
}
"encode (punisher)" in {
val obj = DetailedConcurrentFeedWeaponData(0, 8, DetailedAmmoBoxData(ObjectClass.bullet_9mm, PlanetSideGUID(1693), 0, DetailedAmmoBoxData(8, 30)) :: DetailedAmmoBoxData(ObjectClass.jammer_cartridge, PlanetSideGUID(1564), 1, DetailedAmmoBoxData(8, 1)) :: Nil)
val msg = ObjectCreateDetailedMessage(ObjectClass.punisher, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj)
var pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_punisher
}
"encode (rek)" in {
val obj = DetailedREKData(4)
val msg = ObjectCreateDetailedMessage(ObjectClass.remote_electronics_kit, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_rek
}
"encode (boomer trigger)" in {
val obj = DetailedBoomerTriggerData()
val msg = ObjectCreateDetailedMessage(ObjectClass.boomer_trigger, PlanetSideGUID(2934), ObjectCreateMessageParent(PlanetSideGUID(2502), 0), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_boomer_trigger
}
"encode (character)" in {
val app = CharacterAppearanceData(
PlacementData(
Vector3(3674.8438f, 2726.789f, 91.15625f),
0, 0,
19
),
BasicCharacterData(
"IlllIIIlllIlIllIlllIllI",
PlanetSideEmpire.VS,
CharacterGender.Female,
41,
1
),
3,
false,
false,
ExoSuitType.Standard,
"",
0,
false,
127, 181,
true,
GrenadeState.None,
false,
false,
false,
RibbonBars()
)
val inv = InventoryItem(ObjectClass.beamer, PlanetSideGUID(76), 0, DetailedWeaponData(8, ObjectClass.energy_cell, PlanetSideGUID(77), 0, DetailedAmmoBoxData(8, 16))) ::
InventoryItem(ObjectClass.suppressor, PlanetSideGUID(78), 2, DetailedWeaponData(8, ObjectClass.bullet_9mm, PlanetSideGUID(79), 0, DetailedAmmoBoxData(8, 25))) ::
InventoryItem(ObjectClass.forceblade, PlanetSideGUID(80), 4, DetailedWeaponData(8, ObjectClass.melee_ammo, PlanetSideGUID(81), 0, DetailedAmmoBoxData(8, 1))) ::
InventoryItem(ObjectClass.locker_container, PlanetSideGUID(82), 5, DetailedAmmoBoxData(8, 1)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(83), 6, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(84), 9, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm, PlanetSideGUID(85), 12, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.bullet_9mm_AP, PlanetSideGUID(86), 33, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.energy_cell, PlanetSideGUID(87), 36, DetailedAmmoBoxData(8, 50)) ::
InventoryItem(ObjectClass.remote_electronics_kit, PlanetSideGUID(88), 39, DetailedREKData(8)) ::
Nil
val obj = DetailedCharacterData(
app,
100, 100,
50,
1, 7, 7,
100, 100,
28, 4, 44, 84, 104, 1900,
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
List.empty,
InventoryData(inv),
DrawnSlot.Pistol1
)
val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
val pkt_bitv = pkt.toBitVector
val ori_bitv = string_testchar.toBitVector
pkt_bitv.take(153) mustEqual ori_bitv.take(153) //skip 1
pkt_bitv.drop(154).take(422) mustEqual ori_bitv.drop(154).take(422) //skip 126
pkt_bitv.drop(702) mustEqual ori_bitv.drop(702)
//TODO work on DetailedCharacterData to make this pass as a single stream
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits._
class ObjectDeployedMessageTest extends Specification {
val string_boomer = hex"86 000086626F6F6D6572040000000100000019000000"
"decode" in {
PacketCoding.DecodePacket(string_boomer).require match {
case ObjectDeployedMessage(unk : Int, desc : String, act : DeploymentOutcome.Value, count : Long, max : Long) =>
unk mustEqual 0
desc mustEqual "boomer"
act mustEqual DeploymentOutcome.Success
count mustEqual 1
max mustEqual 25
case _ =>
ko
}
}
"encode" in {
val msg = ObjectDeployedMessage("boomer", DeploymentOutcome.Success, 1, 25)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_boomer
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits.HexStringSyntax
class ReleaseAvatarRequestMessageTest extends Specification {
val string = hex"ac"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case ReleaseAvatarRequestMessage() =>
ok
case _ =>
ko
}
}
"encode" in {
val msg = ReleaseAvatarRequestMessage()
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}

View file

@ -0,0 +1,29 @@
// Copyright (c) 2017 PSForever
package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
import scodec.bits._
class TriggerEnvironmentalDamageMessageTest extends Specification {
val string = hex"74 a7c44140000000"
"decode" in {
PacketCoding.DecodePacket(string).require match {
case TriggerEnvironmentalDamageMessage(unk1, guid, unk2) =>
unk1 mustEqual 2
guid mustEqual PlanetSideGUID(4511)
unk2 mustEqual 5L
case _ =>
ko
}
}
"encode" in {
val msg = TriggerEnvironmentalDamageMessage(2, PlanetSideGUID(4511), 5L)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
}