diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index c34b247f..6e9984e5 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -534,7 +534,7 @@ object GamePacketOpcode extends Enumeration {
case 0xb3 => noDecoder(BattleplanMessage)
case 0xb4 => noDecoder(BattleExperienceMessage)
case 0xb5 => noDecoder(TargetingImplantRequest)
- case 0xb6 => noDecoder(ZonePopulationUpdateMessage)
+ case 0xb6 => game.ZonePopulationUpdateMessage.decode
case 0xb7 => noDecoder(DisconnectMessage)
// 0xb8
case 0xb8 => noDecoder(ExperienceAddedMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/ZonePopulationUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ZonePopulationUpdateMessage.scala
new file mode 100644
index 00000000..7be59000
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/ZonePopulationUpdateMessage.scala
@@ -0,0 +1,71 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * Report the raw numerical population for a zone (continent).
+ *
+ * Populations are displayed as percentages of the three main empires against each other.
+ * Populations specific to a zone will be displayed in the Incentives window for that zone.
+ * Populations in all zones will contribute to the Global Population window and the Incentives window for the server.
+ * The Black OPs population does not show up in the Incentives window for a zone but will be indirectly represented in the other two windows.
+ * This packet also shifts the flavor text for that zone.
+ *
+ * The size of zone's queue is the final upper population limit for that zone.
+ * Common values for the zone queue fields are 00 00 00 00 (locked) and 9E 01 00 00 (414 positions).
+ * When a continent can not accept any players at all, a lock icon will appear over its view pane in the Interstellar View.
+ * Setting the zone's queue to zero will also render this icon.
+ *
+ * The individual queue fields set the maximum empire occupancy for a zone that is represented in the zone Incentives text.
+ * Common values for the empire queue fields are 00 00 00 00 (locked population), 8A 00 00 00 (138 positions), and FA 01 00 00 (500 positions).
+ * Zone Incentives text, however, will never report more than a "100+" vacancy.
+ * The actual limits are probably set based on server load.
+ * The latter queue value is typical for VR area zones.
+ *
+ * The value of the zone queue trumps the sum of all individual empire queues.
+ * Regardless of individual queues, once total zone population matches the zone queue size, all populations will lock.
+ * For normal zones, if the individual queues are not set properly, whole empires can even be locked out of a zone for this reason.
+ * In the worst case, other empires are allowed enough individual queue vacancy that they can occupy all the available slots.
+ * Sanctuary zones possess strange queue values that are occasionally zero'd.
+ * They do not have a lock icon and may not limit populations the same way as normal zones.
+ *
+ * @param continent_guid identifies the zone (continent)
+ * @param zone_queue the maximum population of all three (four) empires that can join this zone
+ * @param tr_queue the maximum number of TR players that can join this zone
+ * @param tr_pop the current TR population in this zone
+ * @param nc_queue the maximum number of NC players that can join this zone
+ * @param nc_pop the current NC population in this zone
+ * @param vs_queue the maximum number of VS players that can join this zone
+ * @param vs_pop the VS population in this zone
+ * @param bo_queue the maximum number of Black OPs players that can join this zone
+ * @param bo_pop the current Black OPs population in this zone
+ */
+final case class ZonePopulationUpdateMessage(continent_guid : PlanetSideGUID,
+ zone_queue : Long,
+ tr_queue : Long,
+ tr_pop : Long,
+ nc_queue : Long,
+ nc_pop : Long,
+ vs_queue : Long,
+ vs_pop : Long,
+ bo_queue : Long,
+ bo_pop : Long)
+ extends PlanetSideGamePacket {
+ type Packet = ZonePopulationUpdateMessage
+ def opcode = GamePacketOpcode.ZonePopulationUpdateMessage
+ def encode = ZonePopulationUpdateMessage.encode(this)
+}
+
+object ZonePopulationUpdateMessage extends Marshallable[ZonePopulationUpdateMessage] {
+ implicit val codec : Codec[ZonePopulationUpdateMessage] = (
+ ("continent_guid" | PlanetSideGUID.codec) ::
+ ("zone_queue" | ulongL(32)) ::
+ ("tr_queue" | ulongL(32)) :: ("tr_pop" | ulongL(32)) ::
+ ("nc_queue" | ulongL(32)) :: ("nc_pop" | ulongL(32)) ::
+ ("vs_queue" | ulongL(32)) :: ("vs_pop" | ulongL(32)) ::
+ ("bo_queue" | ulongL(32)) :: ("bo_pop" | ulongL(32))
+ ).as[ZonePopulationUpdateMessage]
+}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 6ecdaf87..2f7e8a11 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -703,6 +703,35 @@ class GamePacketTest extends Specification {
}
}
+ "ZonePopulationUpdateMessage" should {
+ val string = hex"B6 0400 9E010000 8A000000 25000000 8A000000 25000000 8A000000 25000000 8A000000 25000000"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case ZonePopulationUpdateMessage(continent_guid, zone_queue, tr_queue, tr_pop, nc_queue, nc_pop, vs_queue, vs_pop, bo_queue, bo_pop) =>
+ continent_guid mustEqual PlanetSideGUID(4)
+ zone_queue mustEqual 414
+ tr_queue mustEqual 138
+ tr_pop mustEqual 37
+ nc_queue mustEqual 138
+ nc_pop mustEqual 37
+ vs_queue mustEqual 138
+ vs_pop mustEqual 37
+ bo_queue mustEqual 138
+ bo_pop mustEqual 37
+ case default =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = ZonePopulationUpdateMessage(PlanetSideGUID(4), 414, 138, 37, 138, 37, 138, 37, 138, 37)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+ }
+
"WeaponFireMessage" should {
val string = hex"34 44130029272F0B5DFD4D4EC5C00009BEF78172003FC0"
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index ac792f4b..ea99a03c 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -138,6 +138,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
// 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, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0)))
sendRawResponse(objectHex)
// These object_guids are specfic to VS Sanc