diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index d66750f7..931575c1 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -436,7 +436,7 @@ object GamePacketOpcode extends Enumeration {
case 0x60 => game.FavoritesMessage.decode
case 0x61 => game.ObjectDetectedMessage.decode
case 0x62 => game.SplashHitMessage.decode
- case 0x63 => noDecoder(SetChatFilterMessage)
+ case 0x63 => game.SetChatFilterMessage.decode
case 0x64 => noDecoder(AvatarSearchCriteriaMessage)
case 0x65 => noDecoder(AvatarSearchResponse)
case 0x66 => game.WeaponJammedMessage.decode
diff --git a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala
index 49c1f215..8eb97abe 100644
--- a/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala
+++ b/common/src/main/scala/net/psforever/packet/game/FriendsResponse.scala
@@ -6,6 +6,22 @@ import scodec.Codec
import scodec.codecs._
import shapeless.{::, HNil}
+object FriendAction extends Enumeration {
+ type Type = Value
+
+ val
+ InitializeFriendList,
+ AddFriend,
+ RemoveFriend,
+ UpdateFriend,
+ InitializeIgnoreList,
+ AddIgnoredPlayer,
+ RemoveIgnoredPlayer
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
+}
+
/**
* An entry in the list of players known to and tracked by this player.
* They're called "friends" even though they can be used for a list of ignored players as well.
@@ -37,7 +53,7 @@ final case class Friend(name : String,
* @param unk3 na; always `true`?
* @param friends a list of `Friend`s
*/
-final case class FriendsResponse(action : Int,
+final case class FriendsResponse(action : FriendAction.Value,
unk1 : Int,
unk2 : Boolean,
unk3 : Boolean,
@@ -66,7 +82,7 @@ object Friend extends Marshallable[Friend] {
object FriendsResponse extends Marshallable[FriendsResponse] {
implicit val codec : Codec[FriendsResponse] = (
- ("action" | uintL(3)) ::
+ ("action" | FriendAction.codec) ::
("unk1" | uint4L) ::
("unk2" | bool) ::
("unk3" | bool) ::
@@ -76,8 +92,8 @@ object FriendsResponse extends Marshallable[FriendsResponse] {
})
).xmap[FriendsResponse] (
{
- case act :: u1 :: u2 :: u3 :: num :: friend1 :: friends :: HNil =>
- val friendList : List[Friend] = if(friend1.isDefined) { friend1.get :: friends } else { friends }
+ case act :: u1 :: u2 :: u3 :: _ :: friend1 :: friends :: HNil =>
+ val friendList : List[Friend] = if(friend1.isDefined) { friend1.get +: friends } else { friends }
FriendsResponse(act, u1, u2, u3, friendList)
},
{
diff --git a/common/src/main/scala/net/psforever/packet/game/SetChatFilterMessage.scala b/common/src/main/scala/net/psforever/packet/game/SetChatFilterMessage.scala
new file mode 100644
index 00000000..c51f9f4b
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SetChatFilterMessage.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.{Attempt, Codec}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * An `Enumeration` of the valid chat channels.
+ */
+object ChatChannel extends Enumeration {
+ type Type = Value
+
+ val
+ Unknown,
+ Tells,
+ Local,
+ Squad,
+ Outfit,
+ Command,
+ Platoon,
+ Broadcast,
+ SquadLeader
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(7))
+}
+
+/**
+ * Which comm. channels are allowed to display in the main chat window.
+ * The server sends a `SetChatFilterMessage` and the client responds with the same during login.
+ *
+ * Nine channels exist.
+ * Their values can be modified by radio buttons found under the current chat window's "Options" pane.
+ * Each time the client updates the channel permissions, it sends this packet to the server nine times.
+ * The packet starts with the previous channel filter states and then updates each channel sequentially.
+ *
+ * The `send_channel` and the `channel_filter` values are in the following order:
+ * Unknown, Tells, Local, Squad, Outfit, Command, Platoon, Broadcast, Squad Leader
+ * The first channel is unlisted.
+ * @param send_channel automatically select the fully qualified channel to which the user sends messages
+ * @param origin where this packet was dispatched;
+ * `true`, from the server; `false`, from the client
+ * @param whitelist each channel permitted to post its messages;
+ * when evaluated from a packet, always in original order
+ */
+final case class SetChatFilterMessage(send_channel : ChatChannel.Value,
+ origin : Boolean,
+ whitelist : List[ChatChannel.Value])
+ extends PlanetSideGamePacket {
+ type Packet = SetChatFilterMessage
+ def opcode = GamePacketOpcode.SetChatFilterMessage
+ def encode = SetChatFilterMessage.encode(this)
+}
+
+object SetChatFilterMessage extends Marshallable[SetChatFilterMessage] {
+ /**
+ * Transform a `List` of `Boolean` values into a `List` of `ChatChannel` values.
+ * @param filters the boolean values representing ordered channel filters
+ * @return the names of the channels permitted
+ */
+ private def stateArrayToChannelFilters(filters : List[Boolean]) : List[ChatChannel.Value] = {
+ (0 until 9)
+ .filter(channel => { filters(channel) })
+ .map(channel => ChatChannel(channel))
+ .toList
+ }
+
+ /**
+ * Transform a `List` of `ChatChannel` values into a `List` of `Boolean` values.
+ * @param filters the names of the channels permitted
+ * @return the boolean values representing ordered channel filters
+ */
+ private def channelFiltersToStateArray(filters : List[ChatChannel.Value]) : List[Boolean] = {
+ import scala.collection.mutable.ListBuffer
+ val list = ListBuffer.fill(9)(false)
+ filters.foreach(channel => { list(channel.id) = true })
+ list.toList
+ }
+
+ implicit val codec : Codec[SetChatFilterMessage] = (
+ ("send_channel" | ChatChannel.codec) ::
+ ("origin" | bool) ::
+ ("whitelist" | PacketHelpers.listOfNSized(9, bool))
+ ).exmap[SetChatFilterMessage] (
+ {
+ case a :: b :: c :: HNil =>
+ Attempt.Successful(SetChatFilterMessage(a, b, stateArrayToChannelFilters(c)))
+ },
+ {
+ case SetChatFilterMessage(a, b, c) =>
+ Attempt.Successful(a :: b :: channelFiltersToStateArray(c) :: HNil)
+ }
+ )
+}
diff --git a/common/src/test/scala/game/DensityLevelUpdateMessageTest.scala b/common/src/test/scala/game/DensityLevelUpdateMessageTest.scala
index 2e7b6c69..fc0ea980 100644
--- a/common/src/test/scala/game/DensityLevelUpdateMessageTest.scala
+++ b/common/src/test/scala/game/DensityLevelUpdateMessageTest.scala
@@ -49,4 +49,4 @@ class DensityLevelUpdateMessageTest extends Specification {
val msg1 = DensityLevelUpdateMessage(1, 19999, List(0,0, 0,0, 0,-1, 0,0))
PacketCoding.EncodePacket(msg1).isSuccessful mustEqual false
}
-}
\ No newline at end of file
+}
diff --git a/common/src/test/scala/game/FriendsResponseTest.scala b/common/src/test/scala/game/FriendsResponseTest.scala
index 47a13c13..3b30d6b5 100644
--- a/common/src/test/scala/game/FriendsResponseTest.scala
+++ b/common/src/test/scala/game/FriendsResponseTest.scala
@@ -14,7 +14,7 @@ class FriendsResponseTest extends Specification {
"decode (one friend)" in {
PacketCoding.DecodePacket(stringOneFriend).require match {
case FriendsResponse(action, unk2, unk3, unk4, list) =>
- action mustEqual 3
+ action mustEqual FriendAction.UpdateFriend
unk2 mustEqual 0
unk3 mustEqual true
unk4 mustEqual true
@@ -29,7 +29,7 @@ class FriendsResponseTest extends Specification {
"decode (multiple friends)" in {
PacketCoding.DecodePacket(stringManyFriends).require match {
case FriendsResponse(action, unk2, unk3, unk4, list) =>
- action mustEqual 0
+ action mustEqual FriendAction.InitializeFriendList
unk2 mustEqual 0
unk3 mustEqual true
unk4 mustEqual true
@@ -52,7 +52,7 @@ class FriendsResponseTest extends Specification {
"decode (short)" in {
PacketCoding.DecodePacket(stringShort).require match {
case FriendsResponse(action, unk2, unk3, unk4, list) =>
- action mustEqual 4
+ action mustEqual FriendAction.InitializeIgnoreList
unk2 mustEqual 0
unk3 mustEqual true
unk4 mustEqual true
@@ -63,7 +63,7 @@ class FriendsResponseTest extends Specification {
}
"encode (one friend)" in {
- val msg = FriendsResponse(3, 0, true, true,
+ val msg = FriendsResponse(FriendAction.UpdateFriend, 0, true, true,
Friend("KurtHectic-G", false) ::
Nil)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -72,7 +72,7 @@ class FriendsResponseTest extends Specification {
}
"encode (multiple friends)" in {
- val msg = FriendsResponse(0, 0, true, true,
+ val msg = FriendsResponse(FriendAction.InitializeFriendList, 0, true, true,
Friend("Angello-W", false) ::
Friend("thephattphrogg", false) ::
Friend("Kimpossible12", false) ::
@@ -85,7 +85,7 @@ class FriendsResponseTest extends Specification {
}
"encode (short)" in {
- val msg = FriendsResponse(4, 0, true, true)
+ val msg = FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringShort
diff --git a/common/src/test/scala/game/SetChatFilterMessageTest.scala b/common/src/test/scala/game/SetChatFilterMessageTest.scala
new file mode 100644
index 00000000..7bf9f518
--- /dev/null
+++ b/common/src/test/scala/game/SetChatFilterMessageTest.scala
@@ -0,0 +1,75 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class SetChatFilterMessageTest extends Specification {
+ val string = hex"63 05FF80"
+ val string_custom = hex"63 05C180"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case SetChatFilterMessage(send, origin, filters) =>
+ send mustEqual ChatChannel.Local
+ origin mustEqual true
+ filters.length mustEqual 9
+ filters.head mustEqual ChatChannel.Unknown
+ filters(1) mustEqual ChatChannel.Tells
+ filters(2) mustEqual ChatChannel.Local
+ filters(3) mustEqual ChatChannel.Squad
+ filters(4) mustEqual ChatChannel.Outfit
+ filters(5) mustEqual ChatChannel.Command
+ filters(6) mustEqual ChatChannel.Platoon
+ filters(7) mustEqual ChatChannel.Broadcast
+ filters(8) mustEqual ChatChannel.SquadLeader
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (custom)" in {
+ PacketCoding.DecodePacket(string_custom).require match {
+ case SetChatFilterMessage(send, origin, filters) =>
+ send mustEqual ChatChannel.Local
+ origin mustEqual true
+ filters.length mustEqual 4
+ filters.head mustEqual ChatChannel.Unknown
+ filters(1) mustEqual ChatChannel.Tells
+ filters(2) mustEqual ChatChannel.Broadcast
+ filters(3) mustEqual ChatChannel.SquadLeader
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = SetChatFilterMessage(ChatChannel.Local, true, List(ChatChannel.Unknown, ChatChannel.Tells, ChatChannel.Local, ChatChannel.Squad, ChatChannel.Outfit, ChatChannel.Command, ChatChannel.Platoon, ChatChannel.Broadcast, ChatChannel.SquadLeader))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+
+ "encode (success; same channel listed multiple times)" in {
+ val msg = SetChatFilterMessage(ChatChannel.Local, true, List(ChatChannel.Unknown, ChatChannel.Unknown, ChatChannel.Tells, ChatChannel.Tells, ChatChannel.Local, ChatChannel.Squad, ChatChannel.Outfit, ChatChannel.Command, ChatChannel.Platoon, ChatChannel.Broadcast, ChatChannel.SquadLeader))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+
+ "encode (success; out of order)" in {
+ val msg = SetChatFilterMessage(ChatChannel.Local, true, List(ChatChannel.Squad, ChatChannel.Outfit, ChatChannel.SquadLeader, ChatChannel.Unknown, ChatChannel.Command, ChatChannel.Platoon, ChatChannel.Broadcast, ChatChannel.Tells, ChatChannel.Local))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+
+ "encode (success; custom)" in {
+ val msg = SetChatFilterMessage(ChatChannel.Local, true, List(ChatChannel.Unknown, ChatChannel.Tells, ChatChannel.Broadcast, ChatChannel.SquadLeader))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_custom
+ }
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index b5c1c3b8..286cd32b 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -999,11 +999,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(HotSpotUpdateMessage(continentNumber, 1, Nil)) //normally set in bulk; should be fine doing per continent
case InterstellarCluster.ClientInitializationComplete(tplayer)=>
- //custom
- sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary."
+ //PropertyOverrideMessage
+ sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 1))
+ sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
+ sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
+ sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
- //this will cause the client to send back a BeginZoningMessage packet (see below)
- sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L)) //VS Sanctuary
+ //LoadMapMessage will cause the client to send back a BeginZoningMessage packet (see below)
+ sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100,25,true,3770441820L))
log.info("Load the now-registered player")
//load the now-registered player
tplayer.Spawn
@@ -1017,13 +1020,23 @@ class WorldSessionActor extends Actor with MDCContextAware {
val guid = tplayer.GUID
LivePlayerList.Assign(continent.Number, sessionId, guid)
sendResponse(SetCurrentAvatarMessage(guid,0,0))
- sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))
- sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
-
+ 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(AvatarDeadStateMessage(DeadState.Nothing, 0,0, tplayer.Position, 0, true))
+ sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
+ //AvatarSearchCriteriaMessage
(1 to 73).foreach( i => {
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0))
})
+ //AvatarStatisticsMessage
+ //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage
+ //MapObjectStateBlockMessage and ObjectCreateMessage
+ //TacticsMessage
+
+ sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None)) //CC on
case Zone.ItemFromGround(tplayer, item) =>
val obj_guid = item.GUID
@@ -1236,12 +1249,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info("Reticulating splines ...")
//map-specific initializations
configZone(continent) //todo density
- //sendResponse(SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS)) //HART building C
- //sendResponse(SetEmpireMessage(PlanetSideGUID(29), PlanetSideEmpire.NC)) //South Villa Gun Tower
sendResponse(TimeOfDayMessage(1191182336))
- sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
-
- sendResponse(ZonePopulationUpdateMessage(6, 414, 138, 0, 138, 0, 138, 0, 138, 0))
+ //custom
+ sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary."
(1 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) })
//render Equipment that was dropped into zone before the player arrived
@@ -1399,6 +1409,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ SpawnRequestMessage(u1, u2, u3, u4, u5) =>
log.info(s"SpawnRequestMessage: $msg")
+ case msg @ SetChatFilterMessage(send_channel, origin, whitelist) =>
+ log.info("SetChatFilters: " + msg)
+
case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) =>
// TODO: Prevents log spam, but should be handled correctly
if (messagetype != ChatMessageType.CMT_TOGGLE_GM) {