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) =>