diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 8151a256..1d1f4802 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -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
diff --git a/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala b/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala
new file mode 100644
index 00000000..cecd28ea
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala
@@ -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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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:
+ * RED-A-B-C will construct red lines segments A-B and B-C.
+ * RED-A-B-GREEN-C will only construct a red line segement A-B.
+ * RED-A-B-GREEN-C-D will construct a red line segement A-B and a green line segment C-D.
+ * (Default line color, if none is declared specifically, is gray.)
+ *
+ * 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.
+ *
+ * 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<E>
+ * @see java.util.LinkedList<E>
+ */
+ 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.
+ * 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)
+ }
+ )
+}
diff --git a/common/src/test/scala/game/BattleplanMessageTest.scala b/common/src/test/scala/game/BattleplanMessageTest.scala
new file mode 100644
index 00000000..0b015a70
--- /dev/null
+++ b/common/src/test/scala/game/BattleplanMessageTest.scala
@@ -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
+ }
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 091a5035..804149d7 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -190,7 +190,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.debug("Object: " + obj)
// LoadMapMessage 13714 in mossy .gcap
// XXX: hardcoded shit
- sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary
+ sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map10","z10",40100,25,true,3770441820L))) //VS Sanctuary
sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
sendResponse(PacketCoding.CreateGamePacket(0, objectHex))
@@ -413,6 +413,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info("PlanetsideAttributeMessage: "+msg)
sendResponse(PacketCoding.CreateGamePacket(0,PlanetsideAttributeMessage(avatar_guid, attribute_type, attribute_value)))
+ case msg @ BattleplanMessage(char_id, player_name, zonr_id, diagrams) =>
+ log.info("Battleplan: "+msg)
+
case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) =>
log.info("CreateShortcutMessage: "+msg)