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