using MessagePack; using MessagePack.Formatters; using System; using System.Collections.Generic; namespace XIVChatCommon { [MessagePackObject] public class ServerMessage : IEncodable { [MessagePackFormatter(typeof(MillisecondsDateTimeFormatter))] [Key(0)] public DateTime Timestamp { get; set; } [Key(1)] public ChatType Channel { get; set; } [Key(2)] public byte[] Sender { get; set; } [Key(3)] public byte[] Content { get; set; } [Key(4)] public List Chunks { get; set; } [IgnoreMember] public string ContentText => XivString.GetText(this.Content); [IgnoreMember] public string SenderText => XivString.GetText(this.Sender); [IgnoreMember] protected override byte Code => (byte)ServerOperation.Message; public static ServerMessage Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } [Union(1, typeof(TextChunk))] [Union(2, typeof(IconChunk))] [MessagePackObject] public abstract class Chunk { } [MessagePackObject] public class TextChunk : Chunk { [Key(0)] public uint? FallbackColour { get; set; } [Key(1)] public uint? Foreground { get; set; } [Key(2)] public uint? Glow { get; set; } [Key(3)] public bool Italic { get; set; } [Key(4)] public string Content { get; set; } } [MessagePackObject] public class IconChunk : Chunk { [Key(0)] public byte Index; } public class NameFormatting { public string Before { get; private set; } = string.Empty; public string After { get; private set; } = string.Empty; public bool IsPresent { get; private set; } = true; public static NameFormatting Empty() { return new NameFormatting { IsPresent = false, }; } public static NameFormatting Of(string before, string after) { return new NameFormatting { Before = before, After = after, }; } public static NameFormatting Basic() { return new NameFormatting { Before = "", After = ": ", }; } } public class ChatCode { private const ushort CLEAR_7 = ~(~0 << 7); private readonly ushort code; public ChatType Type => (ChatType)(this.code & CLEAR_7); public ChatSource Source => this.SourceFrom(11); public ChatSource Target => this.SourceFrom(7); private ChatSource SourceFrom(ushort shift) => (ChatSource)(1 << ((this.code >> shift) & 0xF)); public ChatCode(ushort code) { this.code = code; } public NameFormatting NameFormat() { switch (this.Type) { case ChatType.Say: case ChatType.Shout: case ChatType.Yell: case ChatType.NpcAnnouncement: case ChatType.NpcDialogue: return NameFormatting.Of("", ": "); case ChatType.TellOutgoing: return NameFormatting.Of(">> ", ": "); case ChatType.TellIncoming: return NameFormatting.Of("", " >> "); case ChatType.GmTell: return NameFormatting.Of("[GM]", " >> "); case ChatType.GmSay: case ChatType.GmShout: case ChatType.GmYell: return NameFormatting.Of("[GM]", ": "); case ChatType.GmParty: return NameFormatting.Of("([GM]", ") "); case ChatType.GmFreeCompany: return NameFormatting.Of("[FC]<[GM]", "> "); case ChatType.GmLinkshell1: return NameFormatting.Of("[1]<[GM]", "> "); case ChatType.GmLinkshell2: return NameFormatting.Of("[2]<[GM]", "> "); case ChatType.GmLinkshell3: return NameFormatting.Of("[3]<[GM]", "> "); case ChatType.GmLinkshell4: return NameFormatting.Of("[4]<[GM]", "> "); case ChatType.GmLinkshell5: return NameFormatting.Of("[5]<[GM]", "> "); case ChatType.GmLinkshell6: return NameFormatting.Of("[6]<[GM]", "> "); case ChatType.GmLinkshell7: return NameFormatting.Of("[7]<[GM]", "> "); case ChatType.GmLinkshell8: return NameFormatting.Of("[8]<[GM]", "> "); case ChatType.GmNoviceNetwork: return NameFormatting.Of("[NOVICE][GM]", ": "); case ChatType.Party: case ChatType.CrossParty: return NameFormatting.Of("(", ") "); case ChatType.Alliance: return NameFormatting.Of("((", ")) "); case ChatType.PvpTeam: return NameFormatting.Of("[PVP]<", "> "); case ChatType.FreeCompany: return NameFormatting.Of("[FC]<", "> "); case ChatType.Linkshell1: return NameFormatting.Of("[1]<", "> "); case ChatType.Linkshell2: return NameFormatting.Of("[2]<", "> "); case ChatType.Linkshell3: return NameFormatting.Of("[3]<", "> "); case ChatType.Linkshell4: return NameFormatting.Of("[4]<", "> "); case ChatType.Linkshell5: return NameFormatting.Of("[5]<", "> "); case ChatType.Linkshell6: return NameFormatting.Of("[6]<", "> "); case ChatType.Linkshell7: return NameFormatting.Of("[7]<", "> "); case ChatType.Linkshell8: return NameFormatting.Of("[8]<", "> "); case ChatType.StandardEmote: return NameFormatting.Empty(); case ChatType.CustomEmote: return NameFormatting.Of("", ""); case ChatType.CrossLinkshell1: return NameFormatting.Of("[CWLS1]<", "> "); case ChatType.CrossLinkshell2: return NameFormatting.Of("[CWLS2]<", "> "); case ChatType.CrossLinkshell3: return NameFormatting.Of("[CWLS3]<", "> "); case ChatType.CrossLinkshell4: return NameFormatting.Of("[CWLS4]<", "> "); case ChatType.CrossLinkshell5: return NameFormatting.Of("[CWLS5]<", "> "); case ChatType.CrossLinkshell6: return NameFormatting.Of("[CWLS6]<", "> "); case ChatType.CrossLinkshell7: return NameFormatting.Of("[CWLS7]<", "> "); case ChatType.CrossLinkshell8: return NameFormatting.Of("[CWLS8]<", "> "); case ChatType.NoviceNetwork: return NameFormatting.Of("[NOVICE]", ": "); default: return null; } } public bool IsBattle() { switch (this.Type) { case ChatType.Damage: case ChatType.Miss: case ChatType.Action: case ChatType.Item: case ChatType.Healing: case ChatType.GainBuff: case ChatType.LoseBuff: case ChatType.GainDebuff: case ChatType.LoseDebuff: case ChatType.BattleSystem: return true; default: return false; } } public uint? DefaultColour() { switch (this.Type) { case ChatType.Debug: return Rgba(204, 204, 204); case ChatType.Urgent: return Rgba(255, 127, 127); case ChatType.Notice: return Rgba(179, 140, 255); case ChatType.Say: return Rgba(247, 247, 247); case ChatType.Shout: return Rgba(255, 166, 102); case ChatType.TellIncoming: case ChatType.TellOutgoing: case ChatType.GmTell: return Rgba(255, 184, 222); case ChatType.Party: case ChatType.CrossParty: return Rgba(102, 229, 255); case ChatType.Alliance: return Rgba(255, 127, 0); case ChatType.NoviceNetwork: case ChatType.NoviceNetworkSystem: return Rgba(212, 255, 125); case ChatType.Linkshell1: case ChatType.Linkshell2: case ChatType.Linkshell3: case ChatType.Linkshell4: case ChatType.Linkshell5: case ChatType.Linkshell6: case ChatType.Linkshell7: case ChatType.Linkshell8: case ChatType.CrossLinkshell1: case ChatType.CrossLinkshell2: case ChatType.CrossLinkshell3: case ChatType.CrossLinkshell4: case ChatType.CrossLinkshell5: case ChatType.CrossLinkshell6: case ChatType.CrossLinkshell7: case ChatType.CrossLinkshell8: return Rgba(212, 255, 125); case ChatType.StandardEmote: return Rgba(186, 255, 240); case ChatType.CustomEmote: return Rgba(186, 255, 240); case ChatType.Yell: return Rgba(255, 255, 0); case ChatType.Echo: return Rgba(204, 204, 204); case ChatType.System: case ChatType.GatheringSystem: case ChatType.PeriodicRecruitmentNotification: case ChatType.Orchestrion: case ChatType.Alarm: case ChatType.RetainerSale: case ChatType.Sign: case ChatType.MessageBook: return Rgba(204, 204, 204); case ChatType.NpcAnnouncement: case ChatType.NpcDialogue: return Rgba(171, 214, 71); case ChatType.Error: return Rgba(255, 74, 74); case ChatType.FreeCompany: case ChatType.FreeCompanyAnnouncement: case ChatType.FreeCompanyLoginLogout: return Rgba(171, 219, 229); case ChatType.PvpTeam: return Rgba(171, 219, 229); case ChatType.PvpTeamAnnouncement: case ChatType.PvpTeamLoginLogout: return Rgba(171, 219, 229); case ChatType.Action: case ChatType.Item: case ChatType.LootNotice: return Rgba(255, 255, 176); case ChatType.Progress: return Rgba(255, 222, 115); case ChatType.LootRoll: case ChatType.RandomNumber: return Rgba(199, 191, 158); case ChatType.Crafting: case ChatType.Gathering: return Rgba(222, 191, 247); case ChatType.Damage: return Rgba(255, 125, 125); case ChatType.Miss: return Rgba(204, 204, 204); case ChatType.Healing: return Rgba(212, 255, 125); case ChatType.GainBuff: case ChatType.LoseBuff: return Rgba(148, 191, 255); case ChatType.GainDebuff: case ChatType.LoseDebuff: return Rgba(255, 138, 196); case ChatType.BattleSystem: return Rgba(204, 204, 204); default: return null; } } private static uint Rgba(byte red, byte green, byte blue, byte alpha = 0xFF) => alpha | (uint)(red << 24) | (uint)(green << 16) | (uint)(blue << 8); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")] public enum ChatType : ushort { Debug = 1, Urgent = 2, Notice = 3, Say = 10, Shout = 11, TellOutgoing = 12, TellIncoming = 13, Party = 14, Alliance = 15, Linkshell1 = 16, Linkshell2 = 17, Linkshell3 = 18, Linkshell4 = 19, Linkshell5 = 20, Linkshell6 = 21, Linkshell7 = 22, Linkshell8 = 23, FreeCompany = 24, NoviceNetwork = 27, CustomEmote = 28, StandardEmote = 29, Yell = 30, // 31 - also party? CrossParty = 32, PvpTeam = 36, CrossLinkshell1 = 37, Damage = 41, Miss = 42, Action = 43, Item = 44, Healing = 45, GainBuff = 46, GainDebuff = 47, LoseBuff = 48, LoseDebuff = 49, Alarm = 55, Echo = 56, System = 57, BattleSystem = 58, GatheringSystem = 59, Error = 60, NpcDialogue = 61, LootNotice = 62, Progress = 64, LootRoll = 65, Crafting = 66, Gathering = 67, NpcAnnouncement = 68, FreeCompanyAnnouncement = 69, FreeCompanyLoginLogout = 70, RetainerSale = 71, PeriodicRecruitmentNotification = 72, Sign = 73, RandomNumber = 74, NoviceNetworkSystem = 75, Orchestrion = 76, PvpTeamAnnouncement = 77, PvpTeamLoginLogout = 78, MessageBook = 79, GmTell = 80, GmSay = 81, GmShout = 82, GmYell = 83, GmParty = 84, GmFreeCompany = 85, GmLinkshell1 = 86, GmLinkshell2 = 87, GmLinkshell3 = 88, GmLinkshell4 = 89, GmLinkshell5 = 90, GmLinkshell6 = 91, GmLinkshell7 = 92, GmLinkshell8 = 93, GmNoviceNetwork = 94, CrossLinkshell2 = 101, CrossLinkshell3 = 102, CrossLinkshell4 = 103, CrossLinkshell5 = 104, CrossLinkshell6 = 105, CrossLinkshell7 = 106, CrossLinkshell8 = 107, } [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1028:Enum Storage should be Int32")] public enum ChatSource : ushort { Self = 2, PartyMember = 4, AllianceMember = 8, Other = 16, EngagedEnemy = 32, UnengagedEnemy = 64, FriendlyNpc = 128, SelfPet = 256, PartyPet = 512, AlliancePet = 1024, OtherPet = 2048, } [MessagePackObject] public class ClientMessage : IEncodable { [Key(0)] public string Content { get; set; } [IgnoreMember] protected override byte Code => (byte)ClientOperation.Message; public static ClientMessage Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } public enum ServerOperation : byte { /// /// Sent in response to a client ping. Has no payload. /// Pong = 1, /// /// A message was sent in game and is being relayed to the client. /// Message = 2, /// /// The server is shutting down. Clients should send no response and close their sockets. Has no payload. /// Shutdown = 3, PlayerData = 4, Availability = 5, Channel = 6, Backlog = 7, PlayerList = 8, LinkshellList = 9, } [MessagePackObject] public class PlayerData : IEncodable { [Key(0)] public readonly string homeWorld; [Key(1)] public readonly string currentWorld; [Key(2)] public readonly string location; [Key(3)] public readonly string name; public PlayerData(string homeWorld, string currentWorld, string location, string name) { this.homeWorld = homeWorld; this.currentWorld = currentWorld; this.location = location; this.name = name; } [IgnoreMember] protected override byte Code => (byte)ServerOperation.PlayerData; public static PlayerData Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } public class EmptyPlayerData : IEncodable { public static EmptyPlayerData Instance { get; } = new EmptyPlayerData(); [IgnoreMember] protected override byte Code => (byte)ServerOperation.PlayerData; protected override byte[] PayloadEncode() { return new byte[0]; } } [MessagePackObject] public class Availability : IEncodable { [Key(0)] public readonly bool available; public Availability(bool available) { this.available = available; } [IgnoreMember] protected override byte Code => (byte)ServerOperation.Availability; public static Availability Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } [MessagePackObject] public class ServerChannel : IEncodable { [Key(0)] public readonly byte channel; [Key(1)] public readonly string name; [IgnoreMember] public InputChannel InputChannel => (InputChannel)this.channel; protected override byte Code => (byte)ServerOperation.Channel; public ServerChannel(InputChannel channel, string name) : this((byte)channel, name) { } public ServerChannel(byte channel, string name) { this.channel = channel; this.name = name; } public static ServerChannel Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } public enum InputChannel : byte { Tell = 0, Say = 1, Party = 2, Alliance = 3, Yell = 4, Shout = 5, FreeCompany = 6, PvpTeam = 7, NoviceNetwork = 8, CrossLinkshell1 = 9, CrossLinkshell2 = 10, CrossLinkshell3 = 11, CrossLinkshell4 = 12, CrossLinkshell5 = 13, CrossLinkshell6 = 14, CrossLinkshell7 = 15, CrossLinkshell8 = 16, // 17 - unused? // 18 - unused? Linkshell1 = 19, Linkshell2 = 20, Linkshell3 = 21, Linkshell4 = 22, Linkshell5 = 23, Linkshell6 = 24, Linkshell7 = 25, Linkshell8 = 26, } public class Pong : IEncodable { public static Pong Instance { get; } = new Pong(); [IgnoreMember] protected override byte Code => (byte)ServerOperation.Pong; protected override byte[] PayloadEncode() { return new byte[0]; } } public class Ping : IEncodable { public static Ping Instance { get; } = new Ping(); [IgnoreMember] protected override byte Code => (byte)ClientOperation.Ping; protected override byte[] PayloadEncode() { return new byte[0]; } } public class ServerShutdown : IEncodable { public static ServerShutdown Instance { get; } = new ServerShutdown(); [IgnoreMember] protected override byte Code => (byte)ServerOperation.Shutdown; protected override byte[] PayloadEncode() { return new byte[0]; } } public class ClientShutdown : IEncodable { public static ClientShutdown Instance { get; } = new ClientShutdown(); [IgnoreMember] protected override byte Code => (byte)ClientOperation.Shutdown; protected override byte[] PayloadEncode() { return new byte[0]; } } [MessagePackObject] public class ServerBacklog : IEncodable { [Key(0)] public readonly ServerMessage[] messages; protected override byte Code => (byte)ServerOperation.Backlog; public ServerBacklog(ServerMessage[] messages) { this.messages = messages; } public static ServerBacklog Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } [MessagePackObject] public class ClientBacklog : IEncodable { [Key(0)] public ushort Amount { get; set; } protected override byte Code => (byte)ClientOperation.Backlog; public static ClientBacklog Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } [MessagePackObject] public class ClientCatchUp : IEncodable { [MessagePackFormatter(typeof(MillisecondsDateTimeFormatter))] [Key(0)] public DateTime After { get; set; } protected override byte Code => (byte)ClientOperation.CatchUp; public static ClientCatchUp Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } [MessagePackObject] public class ServerPlayerList : IEncodable { [Key(0)] public PlayerListType Type { get; set; } [Key(1)] public Player[] Players { get; set; } protected override byte Code => (byte)ServerOperation.PlayerList; public static ServerPlayerList Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } [MessagePackObject] public class ClientPlayerList : IEncodable { [Key(0)] public PlayerListType Type { get; set; } protected override byte Code => (byte)ClientOperation.PlayerList; public static ClientPlayerList Decode(byte[] bytes) { return MessagePackSerializer.Deserialize(bytes); } protected override byte[] PayloadEncode() { return MessagePackSerializer.Serialize(this); } } public enum PlayerListType : byte { Party = 1, Friend = 2, Linkshell = 3, CrossLinkshell = 4, } [MessagePackObject] public class Player { [Key(0)] public string Name { get; set; } [Key(1)] public string FreeCompany { get; set; } [Key(2)] public ulong Status { get; set; } [Key(3)] public ushort CurrentWorld { get; set; } [Key(4)] public string CurrentWorldName { get; set; } [Key(5)] public ushort HomeWorld { get; set; } [Key(6)] public string HomeWorldName { get; set; } [Key(7)] public ushort Territory { get; set; } [Key(8)] public string TerritoryName { get; set; } [Key(9)] public byte Job { get; set; } [Key(10)] public string JobName { get; set; } [Key(11)] public byte GrandCompany { get; set; } [Key(12)] public string GrandCompanyName { get; set; } [Key(13)] public byte Languages { get; set; } [Key(14)] public byte MainLanguage { get; set; } } public abstract class IEncodable { protected abstract byte Code { get; } protected abstract byte[] PayloadEncode(); public byte[] Encode() { byte[] payload = this.PayloadEncode(); if (payload.Length == 0) { return new byte[] { this.Code }; } byte[] bytes = new byte[1 + payload.Length]; bytes[0] = this.Code; Array.Copy(payload, 0, bytes, 1, payload.Length); return bytes; } } public enum ClientOperation : byte { /// /// The client is sending data to the server to keep the socket alive. Has no payload. /// Ping = 1, /// /// The client has a message to be sent in the game and is relaying it to the server. /// Message = 2, /// /// The client is shutting down. Clients should send this and close their socket for a clean shutdown. /// Shutdown = 3, Backlog = 4, CatchUp = 5, PlayerList = 6, LinkshellList = 7, } public class MillisecondsDateTimeFormatter : IMessagePackFormatter { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); public DateTime Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { var millis = reader.ReadInt64(); return Epoch.AddMilliseconds(millis); } public void Serialize(ref MessagePackWriter writer, DateTime value, MessagePackSerializerOptions options) { var millis = (long)(value.ToUniversalTime() - Epoch).TotalMilliseconds; writer.WriteInt64(millis); } } }