diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala
index 1549d76f3..51c62e4ea 100644
--- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala
@@ -16,7 +16,7 @@ import net.psforever.packet.game.objectcreate.ObjectClass
*/
class ImplantTerminalInterfaceDefinition extends TerminalDefinition(ObjectClass.implant_terminal_interface) {
Packet = new ImplantTerminalInterfaceConverter
- Name = "implante_terminal_interface"
+ Name = "implant_terminal_interface"
private val implants : Map[String, ImplantDefinition] = Map (
"advanced_regen" -> GlobalDefinitions.advanced_regen,
diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index b6ab5d938..1c1185470 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -468,7 +468,7 @@ object GamePacketOpcode extends Enumeration {
case 0x7c => game.DismountBuildingMsg.decode
case 0x7d => noDecoder(UnknownMessage125)
case 0x7e => noDecoder(UnknownMessage126)
- case 0x7f => noDecoder(AvatarStatisticsMessage)
+ case 0x7f => game.AvatarStatisticsMessage.decode
// OPCODES 0x80-8f
case 0x80 => noDecoder(GenericObjectAction2Message)
diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala
index 8369a6425..4505578f9 100644
--- a/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala
@@ -1,40 +1,48 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game
-import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
-import net.psforever.types.ImplantType
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.Codec
import scodec.codecs._
/**
- * Change the state of the implant.
+ * An `Enumeration` for all the actions that can be applied to implants and implant slots.
+ */
+object ImplantAction extends Enumeration {
+ type Type = Value
+
+ val
+ Add,
+ Remove,
+ Initialization,
+ Activation,
+ UnlockMessage,
+ OutOfStamina
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(3))
+}
+
+/**
+ * Change the state of the implant.
+ * Spawn messages for certain implant-related events.
*
* The implant Second Wind is technically an invalid `ImplantType` for this packet.
* This owes to the unique activation trigger for that implant - a near-death experience of ~0HP.
+ * @see `ImplantType`
* @param player_guid the player
- * @param action
- * 0 : add implant
- * with status = 0 to 9 (from ImplantType)
- * 1 : remove implant
- * seems work with any value in status
- * 2 : init implant
- * status : 0 to "uninit"
- * status : 1 to init
- * 3 : activate implant
- * status : 0 to desactivate
- * status : 1 to activate
- * 4 : number of implant slots unlocked
- * status : 0 = no implant slot
- * status : 1 = first implant slot + "implant message"
- * status : 2 or 3 = unlock second & third slots
- * 5 : out of stamina message
- * status : 0 to stop the lock
- * status : 1 to active the lock
+ * @param action how to affect the implant or the slot
* @param implantSlot : from 0 to 2
- * @param status : see action
+ * @param status : a value that depends on context from `ImplantAction`:
+ * `Add` - 0-9 depending on the `ImplantType`
+ * `Remove` - any valid value; field is not significant to this action
+ * `Initialization` - 0 to revoke slot; 1 to allocate implant slot
+ * `Activation` - 0 to deactivate implant; 1 to activate implant
+ * `UnlockMessage` - 0-3 as an unlocked implant slot; display a message
+ * `OutOfStamina` - lock implant; 0 to lock; 1 to unlock; display a message
*/
final case class AvatarImplantMessage(player_guid : PlanetSideGUID,
- action : Int,
+ action : ImplantAction.Value,
implantSlot : Int,
status : Int)
extends PlanetSideGamePacket {
@@ -46,7 +54,7 @@ final case class AvatarImplantMessage(player_guid : PlanetSideGUID,
object AvatarImplantMessage extends Marshallable[AvatarImplantMessage] {
implicit val codec : Codec[AvatarImplantMessage] = (
("player_guid" | PlanetSideGUID.codec) ::
- ("action" | uintL(3)) ::
+ ("action" | ImplantAction.codec) ::
("implantSlot" | uint2L) ::
("status" | uint4L)
).as[AvatarImplantMessage]
diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarStatisticsMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarStatisticsMessage.scala
new file mode 100644
index 000000000..9b510f629
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/AvatarStatisticsMessage.scala
@@ -0,0 +1,105 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+import scala.annotation.switch
+
+/**
+ * na
+ * @param unk1 na
+ * @param unk2 na
+ * @param unk3 na
+ */
+final case class Statistics(unk1 : Option[Int],
+ unk2 : Option[Int],
+ unk3 : List[Long])
+
+/**
+ * na
+ * @param unk na
+ * @param stats na
+ */
+final case class AvatarStatisticsMessage(unk : Int,
+ stats : Statistics)
+ extends PlanetSideGamePacket {
+ type Packet = AvatarStatisticsMessage
+ def opcode = GamePacketOpcode.AvatarStatisticsMessage
+ def encode = AvatarStatisticsMessage.encode(this)
+}
+
+object AvatarStatisticsMessage extends Marshallable[AvatarStatisticsMessage] {
+ /**
+ * na
+ */
+ private val longCodec : Codec[Statistics] = ulong(32).hlist.exmap (
+ {
+ case n :: HNil =>
+ Attempt.Successful(Statistics(None,None, List(n)))
+ },
+ {
+ case Statistics(_, _, Nil) =>
+ Attempt.Failure(Err("missing value (32-bit)"))
+
+ case Statistics(_, _, n) =>
+ Attempt.Successful(n.head :: HNil)
+ }
+ )
+
+ /**
+ * na
+ */
+ private val complexCodec : Codec[Statistics] = (
+ uint(5) ::
+ uintL(11) ::
+ PacketHelpers.listOfNSized(8, uint32L)
+ ).exmap[Statistics] (
+ {
+ case a :: b :: c :: HNil =>
+ Attempt.Successful(Statistics(Some(a), Some(b), c))
+ },
+ {
+ case Statistics(None, _, _) =>
+ Attempt.Failure(Err("missing value (5-bit)"))
+
+ case Statistics(_, None, _) =>
+ Attempt.Failure(Err("missing value (11-bit)"))
+
+ case Statistics(a, b, c) =>
+ if(c.length != 8) {
+ Attempt.Failure(Err("list must have 8 entries"))
+ }
+ else {
+ Attempt.Successful(a.get :: b.get :: c :: HNil)
+ }
+ }
+ )
+
+ /**
+ * na
+ * @param n na
+ * @return na
+ */
+ private def selectCodec(n : Int) : Codec[Statistics] = (n : @switch) match {
+ case 2 | 3 =>
+ longCodec
+ case _ =>
+ complexCodec
+ }
+
+ implicit val codec : Codec[AvatarStatisticsMessage] = (
+ ("unk" | uint(3)) >>:~ { unk =>
+ ("stats" | selectCodec(unk)).hlist
+ }).as[AvatarStatisticsMessage]
+}
+
+object Statistics {
+ def apply(unk : Long) : Statistics =
+ Statistics(None, None, List(unk))
+
+ def apply(unk1 : Int, unk2 : Int, unk3 : List[Long]) : Statistics =
+ Statistics(Some(unk1), Some(unk2), unk3)
+}
diff --git a/common/src/test/scala/game/AvatarImplantMessageTest.scala b/common/src/test/scala/game/AvatarImplantMessageTest.scala
index c52d50db0..78b269603 100644
--- a/common/src/test/scala/game/AvatarImplantMessageTest.scala
+++ b/common/src/test/scala/game/AvatarImplantMessageTest.scala
@@ -4,7 +4,6 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
-import net.psforever.types.ImplantType
import scodec.bits._
class AvatarImplantMessageTest extends Specification {
@@ -14,7 +13,7 @@ class AvatarImplantMessageTest extends Specification {
PacketCoding.DecodePacket(string).require match {
case AvatarImplantMessage(player_guid, unk1, unk2, implant) =>
player_guid mustEqual PlanetSideGUID(3171)
- unk1 mustEqual 3
+ unk1 mustEqual ImplantAction.Activation
unk2 mustEqual 1
implant mustEqual 1
case _ =>
@@ -23,7 +22,7 @@ class AvatarImplantMessageTest extends Specification {
}
"encode" in {
- val msg = AvatarImplantMessage(PlanetSideGUID(3171), 3, 1, 1)
+ val msg = AvatarImplantMessage(PlanetSideGUID(3171), ImplantAction.Activation, 1, 1)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
diff --git a/common/src/test/scala/game/AvatarStatisticsMessageTest.scala b/common/src/test/scala/game/AvatarStatisticsMessageTest.scala
new file mode 100644
index 000000000..bca185fb5
--- /dev/null
+++ b/common/src/test/scala/game/AvatarStatisticsMessageTest.scala
@@ -0,0 +1,79 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class AvatarStatisticsMessageTest extends Specification {
+ val string_long = hex"7F 4 00000000 0"
+ val string_complex = hex"7F 01 3C 40 20 00 00 00 C0 00 00 00 00 00 00 00 20 00 00 00 20 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00"
+
+ "decode (long)" in {
+ PacketCoding.DecodePacket(string_long).require match {
+ case AvatarStatisticsMessage(unk, stats) =>
+ unk mustEqual 2
+ stats.unk1 mustEqual None
+ stats.unk2 mustEqual None
+ stats.unk3.length mustEqual 1
+ stats.unk3.head mustEqual 0
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (complex)" in {
+ PacketCoding.DecodePacket(string_complex).require match {
+ case AvatarStatisticsMessage(unk, stats) =>
+ unk mustEqual 0
+ stats.unk1 mustEqual Some(1)
+ stats.unk2 mustEqual Some(572)
+ stats.unk3.length mustEqual 8
+ stats.unk3.head mustEqual 1
+ stats.unk3(1) mustEqual 6
+ stats.unk3(2) mustEqual 0
+ stats.unk3(3) mustEqual 1
+ stats.unk3(4) mustEqual 1
+ stats.unk3(5) mustEqual 2
+ stats.unk3(6) mustEqual 0
+ stats.unk3(7) mustEqual 0
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (long)" in {
+ val msg = AvatarStatisticsMessage(2, Statistics(0L))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_long
+ }
+
+ "encode (complex)" in {
+ val msg = AvatarStatisticsMessage(0, Statistics(1, 572, List[Long](1,6,0,1,1,2,0,0)))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_complex
+ }
+
+ "encode (failure; long; missing value)" in {
+ val msg = AvatarStatisticsMessage(0, Statistics(None, None,List(0L)))
+ PacketCoding.EncodePacket(msg).isFailure mustEqual true
+ }
+
+ "encode (failure; complex; missing value (5-bit))" in {
+ val msg = AvatarStatisticsMessage(0, Statistics(None, Some(572), List[Long](1,6,0,1,1,2,0,0)))
+ PacketCoding.EncodePacket(msg).isFailure mustEqual true
+ }
+
+ "encode (failure; complex; missing value (11-bit))" in {
+ val msg = AvatarStatisticsMessage(0, Statistics(Some(1), None, List[Long](1,6,0,1,1,2,0,0)))
+ PacketCoding.EncodePacket(msg).isFailure mustEqual true
+ }
+
+ "encode (failure; complex; wrong number of list entries)" in {
+ val msg = AvatarStatisticsMessage(0, Statistics(Some(1), None, List[Long](1,6,0,1)))
+ PacketCoding.EncodePacket(msg).isFailure mustEqual true
+ }
+}
diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala
index e764c0bc7..ab19d2dbb 100644
--- a/pslogin/src/main/scala/Maps.scala
+++ b/pslogin/src/main/scala/Maps.scala
@@ -56,10 +56,11 @@ object Maps {
LocalObject(ServerObjectBuilder(373, Door.Constructor))
LocalObject(ServerObjectBuilder(520, ImplantTerminalMech.Constructor)) //Hart B
- LocalObject(ServerObjectBuilder(1081, Terminal.Constructor(implant_terminal_interface))) //tube 520
LocalObject(ServerObjectBuilder(853, Terminal.Constructor(order_terminal)))
LocalObject(ServerObjectBuilder(855, Terminal.Constructor(order_terminal)))
LocalObject(ServerObjectBuilder(860, Terminal.Constructor(order_terminal)))
+ LocalObject(ServerObjectBuilder(1081, Terminal.Constructor(implant_terminal_interface))) //tube 520
+ TerminalToInterface(520, 1081)
LocalBuilding(2, FoundationBuilder(Building.Structure)) //HART building C
LocalObject(ServerObjectBuilder(186, Terminal.Constructor(cert_terminal)))
@@ -128,7 +129,14 @@ object Maps {
ObjectToBuilding(843, 2)
ObjectToBuilding(844, 2)
ObjectToBuilding(845, 2)
- TerminalToInterface(520, 1081)
+ ObjectToBuilding(1082, 2)
+ ObjectToBuilding(1083, 2)
+ ObjectToBuilding(1084, 2)
+ ObjectToBuilding(1085, 2)
+ ObjectToBuilding(1086, 2)
+ ObjectToBuilding(1087, 2)
+ ObjectToBuilding(1088, 2)
+ ObjectToBuilding(1089, 2)
TerminalToInterface(522, 1082)
TerminalToInterface(523, 1083)
TerminalToInterface(524, 1084)
@@ -178,8 +186,6 @@ object Maps {
ObjectToBuilding(706, 77)
TerminalToSpawnPad(1063, 706)
- //ObjectToBuilding(1081, ?)
- //ObjectToBuilding(520, ?)
ObjectToBuilding(853, 2) //TODO check building_id
ObjectToBuilding(855, 2) //TODO check building_id
ObjectToBuilding(860, 2) //TODO check building_id
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index ac418032b..cbd3698d0 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -801,7 +801,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"$message - put in slot $slot")
- sendResponse(AvatarImplantMessage(tplayer.GUID, 0, slot, implant_type.id))
+ sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Add, slot, implant_type.id))
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, true))
}
else {
@@ -836,7 +836,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) {
val slot = slotNumber.get
log.info(s"$tplayer is selling $implant_type - take from slot $slot")
- sendResponse(AvatarImplantMessage(tplayer.GUID, 1, slot, 0))
+ sendResponse(AvatarImplantMessage(tplayer.GUID, ImplantAction.Remove, slot, 0))
sendResponse(ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, true))
}
else {
@@ -982,10 +982,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
case Zone.ClientInitialization(zone) =>
val continentNumber = zone.Number
val poplist = LivePlayerList.ZonePopulation(continentNumber, _ => true)
+ val popBO = 0 //TODO black ops test (partition)
val popTR = poplist.count(_.Faction == PlanetSideEmpire.TR)
val popNC = poplist.count(_.Faction == PlanetSideEmpire.NC)
val popVS = poplist.count(_.Faction == PlanetSideEmpire.VS)
- val popBO = poplist.size - popTR - popNC - popVS
zone.Buildings.foreach({ case(id, building) => initBuilding(continentNumber, id, building) })
sendResponse(ZonePopulationUpdateMessage(continentNumber, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
@@ -1020,18 +1020,30 @@ class WorldSessionActor extends Actor with MDCContextAware {
val guid = tplayer.GUID
LivePlayerList.Assign(continent.Number, sessionId, guid)
sendResponse(SetCurrentAvatarMessage(guid,0,0))
+
+ (0 until DetailedCharacterData.numberOfImplantSlots(tplayer.BEP)).foreach(slot => {
+ sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1)) //init implant slot
+ sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 0)) //deactivate implant
+ //TODO: if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
+ })
+
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
+ //TODO: if Medkit does not have shortcut, add to a free slot or write over slot 64
sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))
sendResponse(ChangeShortcutBankMessage(guid, 0))
//FavoritesMessage
- sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on"
+ sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this
sendResponse(AvatarDeadStateMessage(DeadState.Nothing, 0,0, tplayer.Position, 0, true))
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
sendResponse(AvatarSearchCriteriaMessage(guid, List(0,0,0,0,0,0)))
- (1 to 73).foreach( i => {
+ (1 to 73).foreach(i => {
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0))
})
- //AvatarStatisticsMessage
+ (0 to 30).foreach(i => { //TODO 30 for a new character only?
+ sendResponse(AvatarStatisticsMessage(2, Statistics(0L)))
+ })
+ //AvatarAwardMessage
+ //DisplayAwardMessage
//SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage
//MapObjectStateBlockMessage and ObjectCreateMessage
//TacticsMessage
@@ -2023,11 +2035,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info("ItemTransaction: " + msg)
continent.GUID(terminal_guid) match {
case Some(term : Terminal) =>
- if(player.Faction == term.Faction) {
- term.Actor ! Terminal.Request(player, msg)
- }
- case Some(obj : PlanetSideGameObject) => ;
- case None => ;
+ log.info(s"ItemTransaction: ${term.Definition.Name} found")
+ term.Actor ! Terminal.Request(player, msg)
+ case Some(obj : PlanetSideGameObject) =>
+ log.error(s"ItemTransaction: $obj is not a terminal")
+ case _ =>
+ log.error(s"ItemTransaction: $terminal_guid does not exist")
}
case msg @ FavoritesRequest(player_guid, unk, action, line, label) =>