using System; using MessagePack; using MessagePack.Formatters; namespace XIVChatCommon.Message { [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; } public TextChunk(string content) { this.Content = content; } public TextChunk(uint? fallbackColour, uint? foreground, uint? glow, bool italic, string content) { this.FallbackColour = fallbackColour; this.Foreground = foreground; this.Glow = glow; this.Italic = italic; this.Content = content; } } [MessagePackObject] public class IconChunk : Chunk { [Key(0)] public byte index; } public class ChatCode { private const ushort Clear7 = ~(~0 << 7); public ushort Raw { get; } public ChatType Type => (ChatType)(this.Raw & Clear7); public ChatSource Source => this.SourceFrom(11); public ChatSource Target => this.SourceFrom(7); private ChatSource SourceFrom(ushort shift) => (ChatSource)(1 << ((this.Raw >> shift) & 0xF)); public ChatCode(ushort raw) { this.Raw = raw; } public ChatType Parent() => this.Type switch { ChatType.Say => ChatType.Say, ChatType.GmSay => ChatType.Say, ChatType.Shout => ChatType.Shout, ChatType.GmShout => ChatType.Shout, ChatType.TellOutgoing => ChatType.TellOutgoing, ChatType.TellIncoming => ChatType.TellOutgoing, ChatType.GmTell => ChatType.TellOutgoing, ChatType.Party => ChatType.Party, ChatType.CrossParty => ChatType.Party, ChatType.GmParty => ChatType.Party, ChatType.Linkshell1 => ChatType.Linkshell1, ChatType.GmLinkshell1 => ChatType.Linkshell1, ChatType.Linkshell2 => ChatType.Linkshell2, ChatType.GmLinkshell2 => ChatType.Linkshell2, ChatType.Linkshell3 => ChatType.Linkshell3, ChatType.GmLinkshell3 => ChatType.Linkshell3, ChatType.Linkshell4 => ChatType.Linkshell4, ChatType.GmLinkshell4 => ChatType.Linkshell4, ChatType.Linkshell5 => ChatType.Linkshell5, ChatType.GmLinkshell5 => ChatType.Linkshell5, ChatType.Linkshell6 => ChatType.Linkshell6, ChatType.GmLinkshell6 => ChatType.Linkshell6, ChatType.Linkshell7 => ChatType.Linkshell7, ChatType.GmLinkshell7 => ChatType.Linkshell7, ChatType.Linkshell8 => ChatType.Linkshell8, ChatType.GmLinkshell8 => ChatType.Linkshell8, ChatType.FreeCompany => ChatType.FreeCompany, ChatType.GmFreeCompany => ChatType.FreeCompany, ChatType.NoviceNetwork => ChatType.NoviceNetwork, ChatType.GmNoviceNetwork => ChatType.NoviceNetwork, ChatType.CustomEmote => ChatType.CustomEmote, ChatType.StandardEmote => ChatType.StandardEmote, ChatType.Yell => ChatType.Yell, ChatType.GmYell => ChatType.Yell, ChatType.GainBuff => ChatType.GainBuff, ChatType.LoseBuff => ChatType.GainBuff, ChatType.GainDebuff => ChatType.GainDebuff, ChatType.LoseDebuff => ChatType.GainDebuff, ChatType.System => ChatType.System, ChatType.Alarm => ChatType.System, ChatType.RetainerSale => ChatType.System, ChatType.PeriodicRecruitmentNotification => ChatType.System, ChatType.Sign => ChatType.System, ChatType.Orchestrion => ChatType.System, ChatType.MessageBook => ChatType.System, ChatType.NpcDialogue => ChatType.NpcDialogue, ChatType.NpcAnnouncement => ChatType.NpcDialogue, ChatType.LootRoll => ChatType.LootRoll, ChatType.RandomNumber => ChatType.LootRoll, ChatType.FreeCompanyAnnouncement => ChatType.FreeCompanyAnnouncement, ChatType.FreeCompanyLoginLogout => ChatType.FreeCompanyAnnouncement, ChatType.PvpTeamAnnouncement => ChatType.PvpTeamAnnouncement, ChatType.PvpTeamLoginLogout => ChatType.PvpTeamAnnouncement, _ => this.Type, }; //public string ConfigKey() { // switch (this.Type) { // case ChatType.Say: // case ChatType.GmSay: // return "ColorSay"; // case ChatType.Shout: // case ChatType.GmShout: // return "ColorShout"; // case ChatType.TellOutgoing: // case ChatType.TellIncoming: // case ChatType.GmTell: // return "ColorTell"; // case ChatType.Party: // case ChatType.CrossParty: // case ChatType.GmParty: // return "ColorParty"; // case ChatType.Alliance: // return "ColorAlliance"; // case ChatType.Linkshell1: // case ChatType.GmLinkshell1: // return "ColorLS1"; // case ChatType.Linkshell2: // case ChatType.GmLinkshell2: // return "ColorLS2"; // case ChatType.Linkshell3: // case ChatType.GmLinkshell3: // return "ColorLS3"; // case ChatType.Linkshell4: // case ChatType.GmLinkshell4: // return "ColorLS4"; // case ChatType.Linkshell5: // case ChatType.GmLinkshell5: // return "ColorLS5"; // case ChatType.Linkshell6: // case ChatType.GmLinkshell6: // return "ColorLS6"; // case ChatType.Linkshell7: // case ChatType.GmLinkshell7: // return "ColorLS7"; // case ChatType.Linkshell8: // case ChatType.GmLinkshell8: // return "ColorLS8"; // case ChatType.FreeCompany: // case ChatType.GmFreeCompany: // return "ColorFCompany"; // case ChatType.NoviceNetwork: // case ChatType.GmNoviceNetwork: // return "ColorBeginner"; // case ChatType.CustomEmote: // return "ColorEmoteUser"; // case ChatType.StandardEmote: // return "ColorEmote"; // case ChatType.Yell: // case ChatType.GmYell: // return "ColorYell"; // case ChatType.PvpTeam: // return "ColorPvPGroup"; // case ChatType.CrossLinkshell1: // return "ColorCWLS"; // case ChatType.Damage: // return "ColorAttackSuccess"; // case ChatType.Miss: // return "ColorAttackFailure"; // case ChatType.Action: // return "ColorAction"; // case ChatType.Item: // return "ColorItem"; // case ChatType.Healing: // return "ColorCureGive"; // case ChatType.GainBuff: // case ChatType.GainDebuff: // return "ColorBuffGive"; // case ChatType.LoseBuff: // case ChatType.LoseDebuff: // return "ColorDebuffGive"; // case ChatType.Echo: // return "ColorEcho"; // case ChatType.System: // case ChatType.Alarm: // case ChatType.RetainerSale: // case ChatType.PeriodicRecruitmentNotification: // case ChatType.Sign: // case ChatType.Orchestrion: // case ChatType.MessageBook: // return "ColorSysMsg"; // case ChatType.BattleSystem: // return "ColorSysBattle"; // case ChatType.GatheringSystem: // return "ColorSysGathering"; // case ChatType.Error: // return "ColorSysError"; // case ChatType.NpcDialogue: // case ChatType.NpcAnnouncement: // return "ColorNpcSay"; // case ChatType.LootNotice: // return "ColorItemNotice"; // case ChatType.Progress: // return "ColorGrowup"; // case ChatType.LootRoll: // case ChatType.RandomNumber: // return "ColorLoot"; // case ChatType.Crafting: // return "ColorCraft"; // case ChatType.Gathering: // return "ColorGathering"; // case ChatType.FreeCompanyAnnouncement: // case ChatType.FreeCompanyLoginLogout: // return "ColorFCAnnounce"; // case ChatType.NoviceNetworkSystem: // return "ColorBeginnerAnnounce"; // case ChatType.PvpTeamAnnouncement: // case ChatType.PvpTeamLoginLogout: // return "ColorPvPGroupAnnounce"; // case ChatType.CrossLinkshell2: // return "ColorCWLS2"; // case ChatType.CrossLinkshell3: // return "ColorCWLS3"; // case ChatType.CrossLinkshell4: // return "ColorCWLS4"; // case ChatType.CrossLinkshell5: // return "ColorCWLS5"; // case ChatType.CrossLinkshell6: // return "ColorCWLS6"; // case ChatType.CrossLinkshell7: // return "ColorCWLS7"; // case ChatType.CrossLinkshell8: // return "ColorCWLS8"; // 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, } public static class ChatTypeExt { public static string? Name(this ChatType type) { return type switch { ChatType.Debug => "Debug", ChatType.Urgent => "Urgent", ChatType.Notice => "Notice", ChatType.Say => "Say", ChatType.Shout => "Shout", ChatType.TellOutgoing => "Tell (Outgoing)", ChatType.TellIncoming => "Tell (Incoming)", ChatType.Party => "Party", ChatType.Alliance => "Alliance", ChatType.Linkshell1 => "Linkshell [1]", ChatType.Linkshell2 => "Linkshell [2]", ChatType.Linkshell3 => "Linkshell [3]", ChatType.Linkshell4 => "Linkshell [4]", ChatType.Linkshell5 => "Linkshell [5]", ChatType.Linkshell6 => "Linkshell [6]", ChatType.Linkshell7 => "Linkshell [7]", ChatType.Linkshell8 => "Linkshell [8]", ChatType.FreeCompany => "Free Company", ChatType.NoviceNetwork => "Novice Network", ChatType.CustomEmote => "Custom Emotes", ChatType.StandardEmote => "Standard Emotes", ChatType.Yell => "Yell", ChatType.CrossParty => "Cross-world Party", ChatType.PvpTeam => "PvP Team", ChatType.CrossLinkshell1 => "Cross-world Linkshell [1]", ChatType.Damage => "Damage dealt", ChatType.Miss => "Failed attacks", ChatType.Action => "Actions used", ChatType.Item => "Items used", ChatType.Healing => "Healing", ChatType.GainBuff => "Beneficial effects granted", ChatType.GainDebuff => "Detrimental effects inflicted", ChatType.LoseBuff => "Beneficial effects lost", ChatType.LoseDebuff => "Detrimental effects cured", ChatType.Alarm => "Alarm Notifications", ChatType.Echo => "Echo", ChatType.System => "System Messages", ChatType.BattleSystem => "Battle System Messages", ChatType.GatheringSystem => "Gathering System Messages", ChatType.Error => "Error Messages", ChatType.NpcDialogue => "NPC Dialogue", ChatType.LootNotice => "Loot Notices", ChatType.Progress => "Progression Messages", ChatType.LootRoll => "Loot Messages", ChatType.Crafting => "Synthesis Messages", ChatType.Gathering => "Gathering Messages", ChatType.NpcAnnouncement => "NPC Dialogue (Announcements)", ChatType.FreeCompanyAnnouncement => "Free Company Announcements", ChatType.FreeCompanyLoginLogout => "Free Company Member Login Notifications", ChatType.RetainerSale => "Retainer Sale Notifications", ChatType.PeriodicRecruitmentNotification => "Periodic Recruitment Notifications", ChatType.Sign => "Sign Messages for PC Targets", ChatType.RandomNumber => "Random Number Messages", ChatType.NoviceNetworkSystem => "Novice Network Notifications", ChatType.Orchestrion => "Current Orchestrion Track Messages", ChatType.PvpTeamAnnouncement => "PvP Team Announcements", ChatType.PvpTeamLoginLogout => "PvP Team Member Login Notifications", ChatType.MessageBook => "Message Book Alert", ChatType.GmTell => "Tell (GM)", ChatType.GmSay => "Say (GM)", ChatType.GmShout => "Shout (GM)", ChatType.GmYell => "Yell (GM)", ChatType.GmParty => "Party (GM)", ChatType.GmFreeCompany => "Free Company (GM)", ChatType.GmLinkshell1 => "Linkshell [1] (GM)", ChatType.GmLinkshell2 => "Linkshell [2] (GM)", ChatType.GmLinkshell3 => "Linkshell [3] (GM)", ChatType.GmLinkshell4 => "Linkshell [4] (GM)", ChatType.GmLinkshell5 => "Linkshell [5] (GM)", ChatType.GmLinkshell6 => "Linkshell [6] (GM)", ChatType.GmLinkshell7 => "Linkshell [7] (GM)", ChatType.GmLinkshell8 => "Linkshell [8] (GM)", ChatType.GmNoviceNetwork => "Novice Network (GM)", ChatType.CrossLinkshell2 => "Cross-world Linkshell [2]", ChatType.CrossLinkshell3 => "Cross-world Linkshell [3]", ChatType.CrossLinkshell4 => "Cross-world Linkshell [4]", ChatType.CrossLinkshell5 => "Cross-world Linkshell [5]", ChatType.CrossLinkshell6 => "Cross-world Linkshell [6]", ChatType.CrossLinkshell7 => "Cross-world Linkshell [7]", ChatType.CrossLinkshell8 => "Cross-world Linkshell [8]", _ => type.ToString(), }; } } [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, } public enum InputChannel : uint { 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 static class InputChannelExt { public static uint LinkshellIndex(this InputChannel channel) => channel switch { InputChannel.Linkshell1 => 0, InputChannel.Linkshell2 => 1, InputChannel.Linkshell3 => 2, InputChannel.Linkshell4 => 3, InputChannel.Linkshell5 => 4, InputChannel.Linkshell6 => 5, InputChannel.Linkshell7 => 6, InputChannel.Linkshell8 => 7, InputChannel.CrossLinkshell1 => 0, InputChannel.CrossLinkshell2 => 1, InputChannel.CrossLinkshell3 => 2, InputChannel.CrossLinkshell4 => 3, InputChannel.CrossLinkshell5 => 4, InputChannel.CrossLinkshell6 => 5, InputChannel.CrossLinkshell7 => 6, InputChannel.CrossLinkshell8 => 7, _ => 0, }; } 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 bool HasStatus(PlayerStatus status) => (this.Status & ((ulong)1 << (int)status)) > 0; } public enum PlayerStatus { GameQa = 1, GameMaster1 = 2, GameMaster2 = 3, EventParticipant = 4, Disconnected = 5, WaitingForFriendListApproval = 6, WaitingForLinkshellApproval = 7, WaitingForFreeCompanyApproval = 8, NotFound = 9, Offline = 10, BattleMentor = 11, Busy = 12, Pvp = 13, PlayingTripleTriad = 14, ViewingCutscene = 15, UsingAChocoboPorter = 16, AwayFromKeyboard = 17, CameraMode = 18, LookingForRepairs = 19, LookingToRepair = 20, LookingToMeldMateria = 21, RolePlaying = 22, LookingForParty = 23, SwordForHire = 24, WaitingForDutyFinder = 25, RecruitingPartyMembers = 26, Mentor = 27, PveMentor = 28, TradeMentor = 29, PvpMentor = 30, Returner = 31, NewAdventurer = 32, AllianceLeader = 33, AlliancePartyLeader = 34, AlliancePartyMember = 35, PartyLeader = 36, PartyMember = 37, PartyLeaderCrossWorld = 38, PartyMemberCrossWorld = 39, AnotherWorld = 40, SharingDuty = 41, SimilarDuty = 42, InDuty = 43, TrialAdventurer = 44, FreeCompany = 45, GrandCompany = 46, Online = 47, } // ReSharper disable once IdentifierTypo public abstract class Encodable { protected abstract byte Code { get; } protected abstract byte[] PayloadEncode(); public byte[] Encode() { byte[] payload = this.PayloadEncode(); if (payload.Length == 0) { return new[] { this.Code, }; } byte[] bytes = new byte[1 + payload.Length]; bytes[0] = this.Code; Array.Copy(payload, 0, bytes, 1, payload.Length); return bytes; } } public class MillisecondsDateTimeFormatter : IMessagePackFormatter { private static readonly DateTime Epoch = new(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); } } }