added initial AvatarStatisticsMessage packet and tests; gave an Enumeration type to AvtarImplant action field

This commit is contained in:
FateJH 2018-03-08 10:26:36 -05:00
parent 2475b468b8
commit d965b5b3c8
8 changed files with 254 additions and 44 deletions

View file

@ -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,

View file

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

View file

@ -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.<br>
* 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.<br>
* <br>
* 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`:<br>
* `Add` - 0-9 depending on the `ImplantType`<br>
* `Remove` - any valid value; field is not significant to this action<br>
* `Initialization` - 0 to revoke slot; 1 to allocate implant slot<br>
* `Activation` - 0 to deactivate implant; 1 to activate implant<br>
* `UnlockMessage` - 0-3 as an unlocked implant slot; display a message<br>
* `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]

View file

@ -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)
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

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