diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index b697b677..24331184 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -544,7 +544,7 @@ object GamePacketOpcode extends Enumeration {
case 0xbc => noDecoder(SnoopMsg)
case 0xbd => game.PlayerStateMessageUpstream.decode
case 0xbe => game.PlayerStateShiftMessage.decode
- case 0xbf => noDecoder(ZipLineMessage)
+ case 0xbf => game.ZipLineMessage.decode
// OPCODES 0xc0-cf
case 0xc0 => noDecoder(CaptureFlagUpdateMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/ZipLineMessage.scala b/common/src/main/scala/net/psforever/packet/game/ZipLineMessage.scala
new file mode 100644
index 00000000..8afc4cbe
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/ZipLineMessage.scala
@@ -0,0 +1,74 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game
+
+import net.psforever.newcodecs.newcodecs
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * Dispatched by the client when the player is interacting with a zip line.
+ * Dispatched by the server to instruct the client to use the zip line.
+ * Cavern teleportation rings also count as "zip lines" as far as the game is concerned, in that they use this packet.
+ *
+ * Action:
+ * `0 - Attach to a node`
+ * `1 - Arrived at destination`
+ * `2 - Forcibly detach from zip line in mid-transit`
+ * @param player_guid the player
+ * @param origin_side whether this corresponds with the "entry" or the "exit" of the zip line, as per the direction of the light pulse visuals
+ * @param action how the player interacts with the zip line
+ * @param guid a number that is consistent to a terminus
+ * @param x the x-coordinate of the point where the player is interacting with the zip line
+ * @param y the y-coordinate of the point where the player is interacting with the zip line
+ * @param z the z-coordinate of the point where the player is interacting with the zip line
+ */
+final case class ZipLineMessage(player_guid : PlanetSideGUID,
+ origin_side : Boolean,
+ action : Int,
+ guid : Long,
+ x : Float,
+ y : Float,
+ z : Float)
+ extends PlanetSideGamePacket {
+ type Packet = ZipLineMessage
+ def opcode = GamePacketOpcode.ZipLineMessage
+ def encode = ZipLineMessage.encode(this)
+}
+
+object ZipLineMessage extends Marshallable[ZipLineMessage] {
+ type threeFloatsPattern = Float :: Float :: Float :: HNil
+
+ /**
+ * A `Codec` for when three `Float` values are to be read or written.
+ */
+ val threeLongValues : Codec[threeFloatsPattern] = (
+ ("x" | floatL) ::
+ ("y" | floatL) ::
+ ("z" | floatL)
+ ).as[threeFloatsPattern]
+
+ /**
+ * A `Codec` for when there are no extra `Float` values present.
+ */
+ val noLongValues : Codec[threeFloatsPattern] = ignore(0).xmap[threeFloatsPattern] (
+ {
+ case () =>
+ 0f :: 0f :: 0f :: HNil
+ },
+ {
+ case _ =>
+ ()
+ }
+ )
+
+ implicit val codec : Codec[ZipLineMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) >>:~ { player =>
+ ("origin_side" | bool) ::
+ ("action" | uint2) ::
+ ("id" | uint32L) ::
+ newcodecs.binary_choice(player.guid > 0, threeLongValues, noLongValues) // !(player.guid == 0)
+ }
+ ).as[ZipLineMessage]
+}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 9bcf21bb..53c6f0f9 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -1139,6 +1139,32 @@ class GamePacketTest extends Specification {
}
}
+ "ZipLineMessage" should {
+ val string = hex"BF 4B00 19 80000010 5bb4089c 52116881 cf76e840"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case ZipLineMessage(player_guid, origin_side, action, uid, x, y, z) =>
+ player_guid mustEqual PlanetSideGUID(75)
+ origin_side mustEqual false
+ action mustEqual 0
+ uid mustEqual 204
+ x mustEqual 1286.9221f
+ y mustEqual 1116.5276f
+ z mustEqual 91.74034f
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = ZipLineMessage(PlanetSideGUID(75), false, 0, 204, 1286.9221f, 1116.5276f, 91.74034f)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+ }
+
"PlayerStateShiftMessage" should {
val string_short = hex"BE 68"
val string_pos = hex"BE 95 A0 89 13 91 B8 B0 BF F0"
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 1f5792a8..4ee8e057 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -286,6 +286,19 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ AvatarJumpMessage(state) =>
//log.info("AvatarJump: " + msg)
+ case msg @ ZipLineMessage(player_guid,origin_side,action,id,x,y,z) =>
+ log.info("ZipLineMessage: " + msg)
+ if(action == 0) {
+ //doing this lets you use the zip line, but you can't get off
+ //sendResponse(PacketCoding.CreateGamePacket(0,ZipLineMessage(player_guid, origin_side, action, id, x,y,z)))
+ }
+ else if(action == 1) {
+ //disembark from zipline at destination?
+ }
+ else if(action == 2) {
+ //get off by force
+ }
+
case msg @ RequestDestroyMessage(object_guid) =>
log.info("RequestDestroy: " + msg)
// TODO: Make sure this is the correct response in all cases