using System.Collections.Generic; using System.IO; using System.Linq; using log4net; using ACE.Database.Models.Shard; using ACE.Entity.Enum; using ACE.Server.Managers; using ACE.Server.WorldObjects; using ACE.Server.WorldObjects.Managers; namespace ACE.Server.Network.Structure { public class SquelchDB { private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// Account squelches /// /// This is defined by the network protocol, but appears to have always been empty in retail pcaps (possibly for security reasons?) /// /// When sending the SquelchDB to the player, the account squelches were sent in the Characters table below as SequelchInfo.Account=true /// /// Key: not sure if this was account name, or character name /// Index: not sure if this was account id, or character id /// public Dictionary Accounts; /// /// Character squelches /// /// Even though this is called Characters, it contains both the Character and Account squelches (denoted by SquelchInfo.Account) /// public Dictionary Characters; /// /// Global squelches /// public SquelchInfo Globals; /// /// Constructs a new SquelchDB for network sending /// public SquelchDB(List squelches, SquelchMask globals) { Accounts = new Dictionary(); Characters = new Dictionary(); Globals = new SquelchInfo(); foreach (var squelch in squelches) { var squelchPlayer = PlayerManager.FindByGuid(squelch.SquelchCharacterId); if (squelchPlayer == null && squelch.SquelchAccountId == 0) { log.Warn($"BuildSquelchDB(): couldn't find character 0x{squelch.SquelchCharacterId:X8}"); continue; } if (squelch.SquelchAccountId == 0) { // chracter squelch var squelchInfo = new SquelchInfo((SquelchMask)squelch.Type, squelchPlayer.Name, false); Characters.Add(squelch.SquelchCharacterId, squelchInfo); } else { // account squelch if (squelchPlayer == null) { var squelchedAccountPlayers = PlayerManager.GetAccountPlayers(squelch.SquelchAccountId); var mostRecentLoggedInCharacterForSquelchedAccount = squelchedAccountPlayers?.OrderByDescending(p => p.Value.GetProperty(ACE.Entity.Enum.Properties.PropertyFloat.LoginTimestamp) ?? 0).FirstOrDefault(); if (mostRecentLoggedInCharacterForSquelchedAccount == null) { log.Warn($"BuildSquelchDB(): couldn't find character 0x{squelch.SquelchCharacterId:X8} and account {squelch.SquelchAccountId} has no other characters"); continue; } squelchPlayer = mostRecentLoggedInCharacterForSquelchedAccount?.Value; } Accounts.Add(squelchPlayer.Account.AccountName, squelchPlayer.Guid.Full); } } // global squelch if (globals != SquelchMask.None) Globals.Filters.Add(globals); } /// /// Merges accounts + character for sending to clients /// public Dictionary CharactersPlus { get { var charactersPlus = new Dictionary(Characters); foreach (var account in Accounts) { var guid = account.Value; var accountPlayer = PlayerManager.FindByGuid(guid); if (accountPlayer == null) continue; if (!charactersPlus.TryGetValue(guid, out var existing)) charactersPlus.Add(guid, new SquelchInfo(SquelchMask.AllChannels, accountPlayer.Name, true)); else existing.Account = true; } return charactersPlus; } } /// /// Returns TRUE if the input player is filtered by this player /// public bool Contains(WorldObject source, ChatMessageType messageType = ChatMessageType.AllChannels) { // ensure this channel can be squelched if (messageType != ChatMessageType.AllChannels && !SquelchManager.IsLegalChannel(messageType)) return false; var squelchMask = messageType.ToMask(); if (source is Player player) { // check account squelches // the client forces account squelches to be AllMessages, // so for these, channel mask is not required if (Accounts.ContainsKey(player.Session.Account)) return true; // check character squelches Characters.TryGetValue(player.Guid.Full, out var squelchInfo); if (squelchInfo != null && squelchInfo.Filters[0].HasFlag(squelchMask)) return true; } // check global squelches if (Globals.Filters.Count > 0 && Globals.Filters[0].HasFlag(squelchMask)) return true; return false; } } public static class SquelchDBExtensions { public static void Write(this BinaryWriter writer, SquelchDB squelches) { //writer.Write(squelches.Accounts); writer.Write(new Dictionary()); // this part of the structure is always empty in retail pcaps, even with account squelches // perhaps for security reasons, always send account squelches in the characters section + account bool? writer.Write(squelches.CharactersPlus); writer.Write(squelches.Globals); } public static void Write(this BinaryWriter writer, Dictionary accountHash) { // unused in retail PackableHashTable.WriteHeader(writer, 0, 0); /*PHashTable.WriteHeader(writer, accountHash.Count); // verify foreach (var kvp in accountHash) { writer.WriteString16L(kvp.Key); writer.Write(kvp.Value); }*/ } // retail used either 32 or 128 here, but i could find no fully consistent pattern to discern them // seems to be 128 in client constructor? public static HashComparer HashComparer = new HashComparer(32); public static void Write(this BinaryWriter writer, Dictionary characterHash) { PackableHashTable.WriteHeader(writer, characterHash.Count, HashComparer.NumBuckets); var sorted = new SortedDictionary(characterHash, HashComparer); foreach (var kvp in sorted) { writer.Write(kvp.Key); writer.Write(kvp.Value); } } } }