diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 5ba940bbc..dcde70310 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -461,7 +461,7 @@ object GamePacketOpcode extends Enumeration {
case 0x76 => game.DeployableObjectsInfoMessage.decode
case 0x77 => noDecoder(SquadState)
// 0x78
- case 0x78 => noDecoder(OxygenStateMessage)
+ case 0x78 => game.OxygenStateMessage.decode
case 0x79 => noDecoder(TradeMessage)
case 0x7a => noDecoder(UnknownMessage122)
case 0x7b => noDecoder(DamageFeedbackMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala
new file mode 100644
index 000000000..54da0ab08
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala
@@ -0,0 +1,111 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.newcodecs.newcodecs
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * Alert the condition of a vehicle the player is using when going too far underwater.
+ * The player must be mounted in/on this vehicle at start time for this countdown to display.
+ * @param vehicle_guid the player's mounted vehicle
+ * @param progress the remaining countdown;
+ * for vehicle waterlog condition, the progress per second rate is very high
+ * @param active show a new countdown if `true` (resets any active countdown);
+ * clear any active countdowns if `false`;
+ * defaults to `true`
+ */
+final case class WaterloggedVehicleState(vehicle_guid : PlanetSideGUID,
+ progress : Float,
+ active : Boolean = true)
+
+/**
+ * Dispatched by the server to cause the player to slowly drown.
+ * If the player is mounted in a vehicle at the time, alert the player that the vehicle may be disabled.
+ *
+ * When a player walks too far underwater, a borderless red progress bar with a countdown from 100 (98) is displayed across the screen.
+ * The countdown proceeds to zero at a fixed rate and is timed with the depleting progress bar.
+ * When it reaches zero, the player will be killed.
+ * If the player is in a vehicle after a certain depth, a blue bar and countdown pair will superimpose the red indicators.
+ * It depletes much more rapidly than the red indicators.
+ * When it reaches zero, the vehicle will become disabled.
+ * All players in the vehicle's seats will be kicked and they will not be allowed back in.
+ *
+ * Normally, the countdowns should be set to begin at 100 (100.0).
+ * This is the earliest the drowning GUI will appear for either blue or red indicators.
+ * Passing greater intervals - up to 204.8 - will start the countdown silently but the GUI will be hidden until 100.0.
+ * (The progress indicators will actually appear to start counting from 98.)
+ * Managing the secondary vehicle countdown independent of the primary player countdown requires updating with the correct levels.
+ * The countdown can be cancelled by instructing it to be `active = false`.
+ *
+ * Except for updating the indicators, all other functionality of "drowning" is automated by the server.
+ * @param player_guid the player
+ * @param progress the remaining countdown;
+ * for character oxygen, the progress per second rate is about 1
+ * @param active show a new countdown if `true` (resets any active countdown);
+ * clear any active countdowns if `false`
+ * @param vehicle_state optional state of the vehicle the player is driving
+ */
+final case class OxygenStateMessage(player_guid : PlanetSideGUID,
+ progress : Float,
+ active : Boolean,
+ vehicle_state : Option[WaterloggedVehicleState] = None)
+ extends PlanetSideGamePacket {
+ type Packet = OxygenStateMessage
+ def opcode = GamePacketOpcode.OxygenStateMessage
+ def encode = OxygenStateMessage.encode(this)
+}
+
+object OxygenStateMessage extends Marshallable[OxygenStateMessage] {
+ /**
+ * Overloaded constructor that removes the optional state of the `WaterloggedVehicleState` parameter.
+ * @param player_guid the player
+ * @param progress the remaining countdown
+ * @param active show or clear the countdown
+ * @param vehicle_state state of the vehicle the player is driving
+ * @return
+ */
+ def apply(player_guid : PlanetSideGUID, progress : Float, active : Boolean, vehicle_state : WaterloggedVehicleState) : OxygenStateMessage =
+ OxygenStateMessage(player_guid, progress, active, Some(vehicle_state))
+
+ /**
+ * A simple pattern that expands the datatypes of the packet's basic `Codec`.
+ */
+ private type basePattern = PlanetSideGUID :: Float :: Boolean :: HNil
+
+ /**
+ * A `Codec` for the repeated processing of three values.
+ * This `Codec` is the basis for the packet's data.
+ */
+ private val base_codec : Codec[basePattern] =
+ PlanetSideGUID.codec ::
+ newcodecs.q_float(0.0f, 204.8f, 11) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode
+ bool
+
+ implicit val codec : Codec[OxygenStateMessage] = (
+ base_codec.exmap[basePattern] (
+ {
+ case guid :: time :: active :: HNil =>
+ Attempt.successful(guid :: time :: active :: HNil)
+ },
+ {
+ case guid :: time :: active :: HNil =>
+ Attempt.successful(guid :: time :: active :: HNil)
+ }
+ ) :+
+ optional(bool,
+ "vehicle_state" | base_codec.exmap[WaterloggedVehicleState] (
+ {
+ case guid :: time :: active :: HNil =>
+ Attempt.successful(WaterloggedVehicleState(guid, time, active))
+ },
+ {
+ case WaterloggedVehicleState(guid, time, active) =>
+ Attempt.successful(guid :: time :: active :: HNil)
+ }
+ ).as[WaterloggedVehicleState]
+ )
+ ).as[OxygenStateMessage]
+}
diff --git a/common/src/test/scala/game/OxygenStateMessageTest.scala b/common/src/test/scala/game/OxygenStateMessageTest.scala
new file mode 100644
index 000000000..034be18ef
--- /dev/null
+++ b/common/src/test/scala/game/OxygenStateMessageTest.scala
@@ -0,0 +1,53 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class OxygenStateMessageTest extends Specification {
+ val string_self = hex"78 4b00f430"
+ val string_vehicle = hex"78 4b00f4385037a180"
+
+ "decode (self)" in {
+ PacketCoding.DecodePacket(string_self).require match {
+ case OxygenStateMessage(guid, progress, active, veh_state) =>
+ guid mustEqual PlanetSideGUID(75)
+ progress mustEqual 50.0
+ active mustEqual true
+ veh_state.isDefined mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (vehicle)" in {
+ PacketCoding.DecodePacket(string_vehicle).require match {
+ case OxygenStateMessage(guid, progress, active, veh_state) =>
+ guid mustEqual PlanetSideGUID(75)
+ progress mustEqual 50.0f
+ active mustEqual true
+ veh_state.isDefined mustEqual true
+ veh_state.get.vehicle_guid mustEqual PlanetSideGUID(1546)
+ veh_state.get.progress mustEqual 50.0f
+ veh_state.get.active mustEqual true
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (self)" in {
+ val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_self
+ }
+
+ "encode (vehicle)" in {
+ val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true, WaterloggedVehicleState(PlanetSideGUID(1546), 50.0f, true))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_vehicle
+ }
+}