diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ca3df2c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +indent_style = space +tab_width = 4 +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/ChatTwo/ChatTwo.csproj b/ChatTwo/ChatTwo.csproj index 275f83e..a1b0450 100755 --- a/ChatTwo/ChatTwo.csproj +++ b/ChatTwo/ChatTwo.csproj @@ -1,7 +1,7 @@ - 1.20.0 + 1.20.3 net8.0-windows enable enable diff --git a/ChatTwo/Chunk.cs b/ChatTwo/Chunk.cs index d88c263..245b26c 100755 --- a/ChatTwo/Chunk.cs +++ b/ChatTwo/Chunk.cs @@ -12,14 +12,14 @@ internal abstract class Chunk { internal Payload? Link { get; set; } protected Chunk(ChunkSource source, Payload? link) { - this.Source = source; - this.Link = link; + Source = source; + Link = link; } - internal SeString? GetSeString() => this.Source switch { + internal SeString? GetSeString() => Source switch { ChunkSource.None => null, - ChunkSource.Sender => this.Message?.SenderSource, - ChunkSource.Content => this.Message?.ContentSource, + ChunkSource.Sender => Message?.SenderSource, + ChunkSource.Content => Message?.ContentSource, _ => null, }; @@ -52,20 +52,33 @@ internal class TextChunk : Chunk { internal string Content { get; set; } internal TextChunk(ChunkSource source, Payload? link, string content) : base(source, link) { - this.Content = content; + Content = content; } #pragma warning disable CS8618 public TextChunk() : base(ChunkSource.None, null) { } #pragma warning restore CS8618 + + /// + /// Creates a new TextChunk with identical styling to this one. + /// + public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content) + { + return new TextChunk(source, link, content) { + FallbackColour = FallbackColour, + Foreground = Foreground, + Glow = Glow, + Italic = Italic, + }; + } } internal class IconChunk : Chunk { internal BitmapFontIcon Icon { get; set; } public IconChunk(ChunkSource source, Payload? link, BitmapFontIcon icon) : base(source, link) { - this.Icon = icon; + Icon = icon; } public IconChunk() : base(ChunkSource.None, null) { diff --git a/ChatTwo/Code/ChatCode.cs b/ChatTwo/Code/ChatCode.cs index 2a7a411..c1ecffd 100755 --- a/ChatTwo/Code/ChatCode.cs +++ b/ChatTwo/Code/ChatCode.cs @@ -10,24 +10,24 @@ internal class ChatCode { internal ChatType Type { get; } internal ChatSource Source { get; } internal ChatSource Target { get; } - private ChatSource SourceFrom(ushort shift) => (ChatSource) (1 << ((this.Raw >> shift) & 0xF)); + private ChatSource SourceFrom(ushort shift) => (ChatSource) (1 << ((Raw >> shift) & 0xF)); internal ChatCode(ushort raw) { - this.Raw = raw; - this.Type = (ChatType) (this.Raw & Clear7); - this.Source = this.SourceFrom(11); - this.Target = this.SourceFrom(7); + Raw = raw; + Type = (ChatType) (Raw & Clear7); + Source = SourceFrom(11); + Target = SourceFrom(7); } [BsonCtor] public ChatCode(ushort raw, ChatType type, ChatSource source, ChatSource target) { - this.Raw = raw; - this.Type = type; - this.Source = source; - this.Target = target; + Raw = raw; + Type = type; + Source = source; + Target = target; } - internal ChatType Parent() => this.Type switch { + internal ChatType Parent() => Type switch { ChatType.Say => ChatType.Say, ChatType.GmSay => ChatType.Say, ChatType.Shout => ChatType.Shout, @@ -81,11 +81,11 @@ internal class ChatCode { ChatType.FreeCompanyLoginLogout => ChatType.FreeCompanyAnnouncement, ChatType.PvpTeamAnnouncement => ChatType.PvpTeamAnnouncement, ChatType.PvpTeamLoginLogout => ChatType.PvpTeamAnnouncement, - _ => this.Type, + _ => Type, }; internal bool IsBattle() { - switch (this.Type) { + switch (Type) { case ChatType.Damage: case ChatType.Miss: case ChatType.Action: diff --git a/ChatTwo/Commands.cs b/ChatTwo/Commands.cs index 7b84a3b..bda10e5 100755 --- a/ChatTwo/Commands.cs +++ b/ChatTwo/Commands.cs @@ -7,18 +7,18 @@ internal sealed class Commands : IDisposable { private Dictionary Registered { get; } = new(); internal Commands(Plugin plugin) { - this.Plugin = plugin; + Plugin = plugin; } public void Dispose() { - foreach (var name in this.Registered.Keys) { + foreach (var name in Registered.Keys) { Plugin.CommandManager.RemoveHandler(name); } } internal void Initialise() { - foreach (var wrapper in this.Registered.Values) { - Plugin.CommandManager.AddHandler(wrapper.Name, new CommandInfo(this.Invoke) { + foreach (var wrapper in Registered.Values) { + Plugin.CommandManager.AddHandler(wrapper.Name, new CommandInfo(Invoke) { HelpMessage = wrapper.Description ?? string.Empty, ShowInHelp = wrapper.ShowInHelp, }); @@ -26,7 +26,7 @@ internal sealed class Commands : IDisposable { } internal CommandWrapper Register(string name, string? description = null, bool? showInHelp = null) { - if (this.Registered.TryGetValue(name, out var wrapper)) { + if (Registered.TryGetValue(name, out var wrapper)) { if (description != null) { wrapper.Description = description; } @@ -38,12 +38,12 @@ internal sealed class Commands : IDisposable { return wrapper; } - this.Registered[name] = new CommandWrapper(name, description, showInHelp ?? true); - return this.Registered[name]; + Registered[name] = new CommandWrapper(name, description, showInHelp ?? true); + return Registered[name]; } private void Invoke(string command, string arguments) { - if (!this.Registered.TryGetValue(command, out var wrapper)) { + if (!Registered.TryGetValue(command, out var wrapper)) { Plugin.Log.Warning($"Missing registration for command {command}"); return; } @@ -64,12 +64,12 @@ internal sealed class CommandWrapper { internal event Action? Execute; internal CommandWrapper(string name, string? description, bool showInHelp) { - this.Name = name; - this.Description = description; - this.ShowInHelp = showInHelp; + Name = name; + Description = description; + ShowInHelp = showInHelp; } internal void Invoke(string command, string arguments) { - this.Execute?.Invoke(command, arguments); + Execute?.Invoke(command, arguments); } } diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index b36ae12..4cd9a41 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -17,6 +17,7 @@ internal class Configuration : IPluginConfiguration { public bool HideDuringCutscenes = true; public bool HideWhenNotLoggedIn = true; public bool HideWhenUiHidden = true; + public bool HideInLoadingScreens; public bool NativeItemTooltips = true; public bool PrettierTimestamps = true; public bool MoreCompactPretty; @@ -50,6 +51,9 @@ internal class Configuration : IPluginConfiguration { public Dictionary ChatColours = new(); public List Tabs = new(); + public bool OverrideStyle = false; + public string ChosenStyle = ""; + public uint DatabaseMigration = LatestDbVersion; internal void UpdateFrom(Configuration other) { @@ -57,6 +61,7 @@ internal class Configuration : IPluginConfiguration { HideDuringCutscenes = other.HideDuringCutscenes; HideWhenNotLoggedIn = other.HideWhenNotLoggedIn; HideWhenUiHidden = other.HideWhenUiHidden; + HideInLoadingScreens = other.HideInLoadingScreens; NativeItemTooltips = other.NativeItemTooltips; PrettierTimestamps = other.PrettierTimestamps; MoreCompactPretty = other.MoreCompactPretty; @@ -88,6 +93,8 @@ internal class Configuration : IPluginConfiguration { ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); Tabs = other.Tabs.Select(t => t.Clone()).ToList(); DatabaseMigration = other.DatabaseMigration; + OverrideStyle = other.OverrideStyle; + ChosenStyle = other.ChosenStyle; } public void Migrate() { diff --git a/ChatTwo/GameFunctions/Context.cs b/ChatTwo/GameFunctions/Context.cs index b51ca87..a2c4237 100755 --- a/ChatTwo/GameFunctions/Context.cs +++ b/ChatTwo/GameFunctions/Context.cs @@ -39,21 +39,21 @@ internal sealed unsafe class Context { private Plugin Plugin { get; } internal Context(Plugin plugin) { - this.Plugin = plugin; + Plugin = plugin; Plugin.GameInteropProvider.InitializeFromAttributes(this); } internal void InviteToNoviceNetwork(string name, ushort world) { - if (this._inviteToNoviceNetwork == null) { + if (_inviteToNoviceNetwork == null) { return; } // 6.3: 221EFD - var a1 = this.Plugin.Functions.GetInfoProxyByIndex(0x14); + var a1 = Plugin.Functions.GetInfoProxyByIndex(0x14); fixed (byte* namePtr = name.ToTerminatedBytes()) { // can specify content id if we have it, but there's no need - this._inviteToNoviceNetwork(a1, 0, world, namePtr); + _inviteToNoviceNetwork(a1, 0, world, namePtr); } } @@ -66,48 +66,48 @@ internal sealed unsafe class Context { // 0x10006: search recipes using this material internal void TryOn(uint itemId, byte stainId) { - if (this._tryOn == null) { + if (_tryOn == null) { return; } - this._tryOn(0xFF, itemId, stainId, 0, 0); + _tryOn(0xFF, itemId, stainId, 0, 0); } internal void LinkItem(uint itemId) { - if (this._linkItem == null) { + if (_linkItem == null) { return; } var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ChatLog); - this._linkItem(agent, itemId); + _linkItem(agent, itemId); } internal void OpenItemComparison(uint itemId) { - if (this._itemComparison == null) { + if (_itemComparison == null) { return; } var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.ItemCompare); - this._itemComparison(agent, 0x4D, itemId, 0); + _itemComparison(agent, 0x4D, itemId, 0); } internal void SearchForRecipesUsingItem(uint itemId) { - if (this._searchForRecipesUsingItem == null || this._searchForRecipesUsingItemVfunc is not { } offset) { + if (_searchForRecipesUsingItem == null || _searchForRecipesUsingItemVfunc is not { } offset) { return; } var uiModule = Framework.Instance()->GetUiModule(); var vf = (delegate* unmanaged) uiModule->vfunc[offset / 8]; var a1 = vf(uiModule); - this._searchForRecipesUsingItem(a1, itemId); + _searchForRecipesUsingItem(a1, itemId); } internal void SearchForItem(uint itemId) { - if (this._searchForItem == null) { + if (_searchForItem == null) { return; } var itemFinder = Framework.Instance()->GetUiModule()->GetItemFinderModule(); - this._searchForItem(itemFinder, itemId, 1); + _searchForItem(itemFinder, itemId, 1); } } diff --git a/ChatTwo/GameFunctions/GameFunctions.cs b/ChatTwo/GameFunctions/GameFunctions.cs index 8a604fe..bb71eaa 100755 --- a/ChatTwo/GameFunctions/GameFunctions.cs +++ b/ChatTwo/GameFunctions/GameFunctions.cs @@ -66,26 +66,26 @@ internal unsafe class GameFunctions : IDisposable { internal Context Context { get; } internal GameFunctions(Plugin plugin) { - this.Plugin = plugin; - this.Party = new Party(this.Plugin); - this.Chat = new Chat(this.Plugin); - this.Context = new Context(this.Plugin); + Plugin = plugin; + Party = new Party(Plugin); + Chat = new Chat(Plugin); + Context = new Context(Plugin); Plugin.GameInteropProvider.InitializeFromAttributes(this); - this.ResolveTextCommandPlaceholderHook?.Enable(); + ResolveTextCommandPlaceholderHook?.Enable(); } public void Dispose() { - this.Chat.Dispose(); + Chat.Dispose(); - this.ResolveTextCommandPlaceholderHook?.Dispose(); + ResolveTextCommandPlaceholderHook?.Dispose(); - Marshal.FreeHGlobal(this._placeholderNamePtr); + Marshal.FreeHGlobal(_placeholderNamePtr); } private IntPtr GetInfoModule() { - if (this._infoModuleVfunc is not { } vfunc) { + if (_infoModuleVfunc is not { } vfunc) { return IntPtr.Zero; } @@ -95,25 +95,25 @@ internal unsafe class GameFunctions : IDisposable { } internal IntPtr GetInfoProxyByIndex(uint idx) { - var infoModule = this.GetInfoModule(); - return infoModule == IntPtr.Zero ? IntPtr.Zero : this._getInfoProxyByIndex(infoModule, idx); + var infoModule = GetInfoModule(); + return infoModule == IntPtr.Zero ? IntPtr.Zero : _getInfoProxyByIndex(infoModule, idx); } internal uint? GetCurrentChatLogEntryIndex() { - if (this._currentChatEntryOffset == null) { + if (_currentChatEntryOffset == null) { return null; } var log = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureLogModule(); - return *(uint*) (log + this._currentChatEntryOffset.Value); + return *(uint*) (log + _currentChatEntryOffset.Value); } internal void SendFriendRequest(string name, ushort world) { - this.ListCommand(name, world, "friendlist"); + ListCommand(name, world, "friendlist"); } internal void AddToBlacklist(string name, ushort world) { - this.ListCommand(name, world, "blist"); + ListCommand(name, world, "blist"); } private void ListCommand(string name, ushort world, string commandName) { @@ -123,8 +123,8 @@ internal unsafe class GameFunctions : IDisposable { } var worldName = row.Name.RawString; - this._replacementName = $"{name}@{worldName}"; - this.Plugin.Common.Functions.Chat.SendMessage($"/{commandName} add {this._placeholder}"); + _replacementName = $"{name}@{worldName}"; + Plugin.Common.Functions.Chat.SendMessage($"/{commandName} add {_placeholder}"); } internal static void SetAddonInteractable(string name, bool interactable) { @@ -236,41 +236,41 @@ internal unsafe class GameFunctions : IDisposable { } internal bool IsMentor() { - if (this._isMentor == null || this._isMentorA1 == null || this._isMentorA1.Value == IntPtr.Zero) { + if (_isMentor == null || _isMentorA1 == null || _isMentorA1.Value == IntPtr.Zero) { return false; } - return this._isMentor(this._isMentorA1.Value) > 0; + return _isMentor(_isMentorA1.Value) > 0; } internal void OpenPartyFinder(uint id) { - if (this._openPartyFinder == null) { + if (_openPartyFinder == null) { return; } var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.LookingForGroup); if (agent != null) { - this._openPartyFinder(agent, id); + _openPartyFinder(agent, id); } } internal void OpenAchievement(uint id) { - if (this._openAchievement == null) { + if (_openAchievement == null) { return; } var agent = Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Achievement); if (agent != null) { - this._openAchievement(agent, id); + _openAchievement(agent, id); } } internal bool IsInInstance() { - if (this._inInstance == null) { + if (_inInstance == null) { return false; } - return this._inInstance() != 0; + return _inInstance() != 0; } internal bool TryOpenAdventurerPlate(ulong playerId) @@ -304,21 +304,21 @@ internal unsafe class GameFunctions : IDisposable { private string? _replacementName; private IntPtr ResolveTextCommandPlaceholderDetour(IntPtr a1, byte* placeholderText, byte a3, byte a4) { - if (this._replacementName == null) { + if (_replacementName == null) { goto Original; } var placeholder = MemoryHelper.ReadStringNullTerminated((IntPtr) placeholderText); - if (placeholder != this._placeholder) { + if (placeholder != _placeholder) { goto Original; } - MemoryHelper.WriteString(this._placeholderNamePtr, this._replacementName); - this._replacementName = null; + MemoryHelper.WriteString(_placeholderNamePtr, _replacementName); + _replacementName = null; - return this._placeholderNamePtr; + return _placeholderNamePtr; Original: - return this.ResolveTextCommandPlaceholderHook!.Original(a1, placeholderText, a3, a4); + return ResolveTextCommandPlaceholderHook!.Original(a1, placeholderText, a3, a4); } } diff --git a/ChatTwo/GameFunctions/Party.cs b/ChatTwo/GameFunctions/Party.cs index 3e582f8..55e9afd 100755 --- a/ChatTwo/GameFunctions/Party.cs +++ b/ChatTwo/GameFunctions/Party.cs @@ -25,57 +25,57 @@ internal sealed unsafe class Party { private Plugin Plugin { get; } internal Party(Plugin plugin) { - this.Plugin = plugin; + Plugin = plugin; Plugin.GameInteropProvider.InitializeFromAttributes(this); } internal void InviteSameWorld(string name, ushort world, ulong contentId) { - if (this._inviteToParty == null) { + if (_inviteToParty == null) { return; } // 6.11: 214A55 - var a1 = this.Plugin.Functions.GetInfoProxyByIndex(2); + var a1 = Plugin.Functions.GetInfoProxyByIndex(2); fixed (byte* namePtr = name.ToTerminatedBytes()) { // this only works if target is on the same world - this._inviteToParty(a1, contentId, namePtr, world); + _inviteToParty(a1, contentId, namePtr, world); } } internal void InviteOtherWorld(ulong contentId) { - if (this._inviteToPartyContentId == null) { + if (_inviteToPartyContentId == null) { return; } // 6.11: 214A55 - var a1 = this.Plugin.Functions.GetInfoProxyByIndex(2); + var a1 = Plugin.Functions.GetInfoProxyByIndex(2); if (contentId != 0) { // third param is world, but it requires a specific world // if they're not on that world, it will fail // pass 0 and it will work on any world EXCEPT for the world the // current player is on - this._inviteToPartyContentId(a1, contentId, 0); + _inviteToPartyContentId(a1, contentId, 0); } } internal void InviteInInstance(ulong contentId) { - if (this._inviteToPartyInInstance == null) { + if (_inviteToPartyInInstance == null) { return; } // 6.11: 214A55 - var a1 = this.Plugin.Functions.GetInfoProxyByIndex(2); + var a1 = Plugin.Functions.GetInfoProxyByIndex(2); if (contentId != 0) { // third param is world, but it requires a specific world // if they're not on that world, it will fail // pass 0 and it will work on any world EXCEPT for the world the // current player is on - this._inviteToPartyInInstance(a1, contentId); + _inviteToPartyInInstance(a1, contentId); } } internal void Kick(string name, ulong contentId) { - if (this._kick == null) { + if (_kick == null) { return; } @@ -85,12 +85,12 @@ internal sealed unsafe class Party { } fixed (byte* namePtr = name.ToTerminatedBytes()) { - this._kick(agent, namePtr, 0, contentId); + _kick(agent, namePtr, 0, contentId); } } internal void Promote(string name, ulong contentId) { - if (this._promote == null) { + if (_promote == null) { return; } @@ -100,7 +100,7 @@ internal sealed unsafe class Party { } fixed (byte* namePtr = name.ToTerminatedBytes()) { - this._promote(agent, namePtr, 0, contentId); + _promote(agent, namePtr, 0, contentId); } } } diff --git a/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs b/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs index f4d1c87..3d82528 100755 --- a/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs +++ b/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs @@ -9,9 +9,9 @@ internal class ChannelSwitchInfo { internal string? Text { get; } internal ChannelSwitchInfo(InputChannel? channel, bool permanent = false, RotateMode rotate = RotateMode.None, string? text = null) { - this.Channel = channel; - this.Permanent = permanent; - this.Rotate = rotate; - this.Text = text; + Channel = channel; + Permanent = permanent; + Rotate = rotate; + Text = text; } } diff --git a/ChatTwo/GameFunctions/Types/ChatActivatedArgs.cs b/ChatTwo/GameFunctions/Types/ChatActivatedArgs.cs index da753da..ada612a 100755 --- a/ChatTwo/GameFunctions/Types/ChatActivatedArgs.cs +++ b/ChatTwo/GameFunctions/Types/ChatActivatedArgs.cs @@ -8,6 +8,6 @@ internal sealed class ChatActivatedArgs { internal TellTarget? TellTarget { get; init; } internal ChatActivatedArgs(ChannelSwitchInfo channelSwitchInfo) { - this.ChannelSwitchInfo = channelSwitchInfo; + ChannelSwitchInfo = channelSwitchInfo; } } diff --git a/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs b/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs index 3804724..dc9d2f8 100755 --- a/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs +++ b/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs @@ -6,8 +6,8 @@ internal sealed class TellHistoryInfo { internal ulong ContentId { get; } internal TellHistoryInfo(string name, uint world, ulong contentId) { - this.Name = name; - this.World = world; - this.ContentId = contentId; + Name = name; + World = world; + ContentId = contentId; } } diff --git a/ChatTwo/GameFunctions/Types/TellTarget.cs b/ChatTwo/GameFunctions/Types/TellTarget.cs index 47b6324..a8705d1 100755 --- a/ChatTwo/GameFunctions/Types/TellTarget.cs +++ b/ChatTwo/GameFunctions/Types/TellTarget.cs @@ -7,9 +7,9 @@ internal sealed class TellTarget { internal TellReason Reason { get; } internal TellTarget(string name, ushort world, ulong contentId, TellReason reason) { - this.Name = name; - this.World = world; - this.ContentId = contentId; - this.Reason = reason; + Name = name; + World = world; + ContentId = contentId; + Reason = reason; } } diff --git a/ChatTwo/Ipc/ExtraChat.cs b/ChatTwo/Ipc/ExtraChat.cs index bfc607d..113a025 100644 --- a/ChatTwo/Ipc/ExtraChat.cs +++ b/ChatTwo/Ipc/ExtraChat.cs @@ -19,47 +19,47 @@ internal sealed class ExtraChat : IDisposable { internal (string, uint)? ChannelOverride { get; set; } private Dictionary ChannelCommandColoursInternal { get; set; } = new(); - internal IReadOnlyDictionary ChannelCommandColours => this.ChannelCommandColoursInternal; + internal IReadOnlyDictionary ChannelCommandColours => ChannelCommandColoursInternal; private Dictionary ChannelNamesInternal { get; set; } = new(); - internal IReadOnlyDictionary ChannelNames => this.ChannelNamesInternal; + internal IReadOnlyDictionary ChannelNames => ChannelNamesInternal; internal ExtraChat(Plugin plugin) { - this.Plugin = plugin; + Plugin = plugin; - this.OverrideChannelGate = Plugin.Interface.GetIpcSubscriber("ExtraChat.OverrideChannelColour"); - this.ChannelCommandColoursGate = Plugin.Interface.GetIpcSubscriber, Dictionary>("ExtraChat.ChannelCommandColours"); - this.ChannelNamesGate = Plugin.Interface.GetIpcSubscriber, Dictionary>("ExtraChat.ChannelNames"); + OverrideChannelGate = Plugin.Interface.GetIpcSubscriber("ExtraChat.OverrideChannelColour"); + ChannelCommandColoursGate = Plugin.Interface.GetIpcSubscriber, Dictionary>("ExtraChat.ChannelCommandColours"); + ChannelNamesGate = Plugin.Interface.GetIpcSubscriber, Dictionary>("ExtraChat.ChannelNames"); - this.OverrideChannelGate.Subscribe(this.OnOverrideChannel); - this.ChannelCommandColoursGate.Subscribe(this.OnChannelCommandColours); - this.ChannelNamesGate.Subscribe(this.OnChannelNames); + OverrideChannelGate.Subscribe(OnOverrideChannel); + ChannelCommandColoursGate.Subscribe(OnChannelCommandColours); + ChannelNamesGate.Subscribe(OnChannelNames); try { - this.ChannelCommandColoursInternal = this.ChannelCommandColoursGate.InvokeFunc(null!); - this.ChannelNamesInternal = this.ChannelNamesGate.InvokeFunc(null!); + ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!); + ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!); } catch (Exception) { // no-op } } public void Dispose() { - this.OverrideChannelGate.Unsubscribe(this.OnOverrideChannel); + OverrideChannelGate.Unsubscribe(OnOverrideChannel); } private void OnOverrideChannel(OverrideInfo info) { if (info.Channel == null) { - this.ChannelOverride = null; + ChannelOverride = null; return; } - this.ChannelOverride = (info.Channel, info.Rgba); + ChannelOverride = (info.Channel, info.Rgba); } private void OnChannelCommandColours(Dictionary obj) { - this.ChannelCommandColoursInternal = obj; + ChannelCommandColoursInternal = obj; } private void OnChannelNames(Dictionary obj) { - this.ChannelNamesInternal = obj; + ChannelNamesInternal = obj; } } diff --git a/ChatTwo/IpcManager.cs b/ChatTwo/IpcManager.cs index 0148d53..801b996 100755 --- a/ChatTwo/IpcManager.cs +++ b/ChatTwo/IpcManager.cs @@ -15,38 +15,38 @@ internal sealed class IpcManager : IDisposable { internal List Registered { get; } = new(); public IpcManager(DalamudPluginInterface pluginInterface) { - this.Interface = pluginInterface; + Interface = pluginInterface; - this.RegisterGate = this.Interface.GetIpcProvider("ChatTwo.Register"); - this.RegisterGate.RegisterFunc(this.Register); + RegisterGate = Interface.GetIpcProvider("ChatTwo.Register"); + RegisterGate.RegisterFunc(Register); - this.AvailableGate = this.Interface.GetIpcProvider("ChatTwo.Available"); + AvailableGate = Interface.GetIpcProvider("ChatTwo.Available"); - this.UnregisterGate = this.Interface.GetIpcProvider("ChatTwo.Unregister"); - this.UnregisterGate.RegisterAction(this.Unregister); + UnregisterGate = Interface.GetIpcProvider("ChatTwo.Unregister"); + UnregisterGate.RegisterAction(Unregister); - this.InvokeGate = this.Interface.GetIpcProvider("ChatTwo.Invoke"); + InvokeGate = Interface.GetIpcProvider("ChatTwo.Invoke"); - this.AvailableGate.SendMessage(); + AvailableGate.SendMessage(); } internal void Invoke(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content) { - this.InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content); + InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content); } private string Register() { var id = Guid.NewGuid().ToString(); - this.Registered.Add(id); + Registered.Add(id); return id; } private void Unregister(string id) { - this.Registered.Remove(id); + Registered.Remove(id); } public void Dispose() { - this.UnregisterGate.UnregisterFunc(); - this.RegisterGate.UnregisterFunc(); - this.Registered.Clear(); + UnregisterGate.UnregisterFunc(); + RegisterGate.UnregisterFunc(); + Registered.Clear(); } } diff --git a/ChatTwo/Message.cs b/ChatTwo/Message.cs index bcba4ce..4f5803e 100755 --- a/ChatTwo/Message.cs +++ b/ChatTwo/Message.cs @@ -1,7 +1,9 @@ using ChatTwo.Code; +using ChatTwo.Util; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using LiteDB; +using System.Text.RegularExpressions; namespace ChatTwo; @@ -10,15 +12,15 @@ internal class SortCode { internal ChatSource Source { get; set; } internal SortCode(ChatType type, ChatSource source) { - this.Type = type; - this.Source = source; + Type = type; + Source = source; } public SortCode() { } private bool Equals(SortCode other) { - return this.Type == other.Type && this.Source == other.Source; + return Type == other.Type && Source == other.Source; } public override bool Equals(object? obj) { @@ -30,12 +32,12 @@ internal class SortCode { return true; } - return obj.GetType() == this.GetType() && this.Equals((SortCode) obj); + return obj.GetType() == GetType() && Equals((SortCode) obj); } public override int GetHashCode() { unchecked { - return ((int) this.Type * 397) ^ (int) this.Source; + return ((int) Type * 397) ^ (int) Source; } } } @@ -66,16 +68,16 @@ internal class Message { internal int Hash { get; } internal Message(ulong receiver, ChatCode code, List sender, List content, SeString senderSource, SeString contentSource) { - this.Receiver = receiver; - this.Date = DateTime.UtcNow; - this.Code = code; - this.Sender = sender; - this.Content = content; - this.SenderSource = senderSource; - this.ContentSource = contentSource; - this.SortCode = new SortCode(this.Code.Type, this.Code.Source); - this.ExtraChatChannel = this.ExtractExtraChatChannel(); - this.Hash = this.GenerateHash(); + Receiver = receiver; + Date = DateTime.UtcNow; + Code = code; + Sender = sender; + Content = ReplaceContentURLs(content); + SenderSource = senderSource; + ContentSource = contentSource; + SortCode = new SortCode(Code.Type, Code.Source); + ExtraChatChannel = ExtractExtraChatChannel(); + Hash = GenerateHash(); foreach (var chunk in sender.Concat(content)) { chunk.Message = this; @@ -83,52 +85,56 @@ internal class Message { } internal Message(ObjectId id, ulong receiver, ulong contentId, DateTime date, BsonDocument code, BsonArray sender, BsonArray content, BsonValue senderSource, BsonValue contentSource, BsonDocument sortCode) { - this.Id = id; - this.Receiver = receiver; - this.ContentId = contentId; - this.Date = date; - this.Code = BsonMapper.Global.ToObject(code); - this.Sender = BsonMapper.Global.Deserialize>(sender); - this.Content = BsonMapper.Global.Deserialize>(content); - this.SenderSource = BsonMapper.Global.Deserialize(senderSource); - this.ContentSource = BsonMapper.Global.Deserialize(contentSource); - this.SortCode = BsonMapper.Global.ToObject(sortCode); - this.ExtraChatChannel = this.ExtractExtraChatChannel(); - this.Hash = this.GenerateHash(); + Id = id; + Receiver = receiver; + ContentId = contentId; + Date = date; + Code = BsonMapper.Global.ToObject(code); + Sender = BsonMapper.Global.Deserialize>(sender); + // Don't call ReplaceContentURLs here since we're loading the message + // from the database and it should already have parsed URL data. + Content = BsonMapper.Global.Deserialize>(content); + SenderSource = BsonMapper.Global.Deserialize(senderSource); + ContentSource = BsonMapper.Global.Deserialize(contentSource); + SortCode = BsonMapper.Global.ToObject(sortCode); + ExtraChatChannel = ExtractExtraChatChannel(); + Hash = GenerateHash(); - foreach (var chunk in this.Sender.Concat(this.Content)) { + foreach (var chunk in Sender.Concat(Content)) { chunk.Message = this; } } internal Message(ObjectId id, ulong receiver, ulong contentId, DateTime date, BsonDocument code, BsonArray sender, BsonArray content, BsonValue senderSource, BsonValue contentSource, BsonDocument sortCode, BsonValue extraChatChannel) { - this.Id = id; - this.Receiver = receiver; - this.ContentId = contentId; - this.Date = date; - this.Code = BsonMapper.Global.ToObject(code); - this.Sender = BsonMapper.Global.Deserialize>(sender); - this.Content = BsonMapper.Global.Deserialize>(content); - this.SenderSource = BsonMapper.Global.Deserialize(senderSource); - this.ContentSource = BsonMapper.Global.Deserialize(contentSource); - this.SortCode = BsonMapper.Global.ToObject(sortCode); - this.ExtraChatChannel = BsonMapper.Global.Deserialize(extraChatChannel); - this.Hash = this.GenerateHash(); + Id = id; + Receiver = receiver; + ContentId = contentId; + Date = date; + Code = BsonMapper.Global.ToObject(code); + Sender = BsonMapper.Global.Deserialize>(sender); + // Don't call ReplaceContentURLs here since we're loading the message + // from the database and it should already have parsed URL data. + Content = BsonMapper.Global.Deserialize>(content); + SenderSource = BsonMapper.Global.Deserialize(senderSource); + ContentSource = BsonMapper.Global.Deserialize(contentSource); + SortCode = BsonMapper.Global.ToObject(sortCode); + ExtraChatChannel = BsonMapper.Global.Deserialize(extraChatChannel); + Hash = GenerateHash(); - foreach (var chunk in this.Sender.Concat(this.Content)) { + foreach (var chunk in Sender.Concat(Content)) { chunk.Message = this; } } private int GenerateHash() { - return this.SortCode.GetHashCode() - ^ this.ExtraChatChannel.GetHashCode() - ^ string.Join("", this.Sender.Select(c => c.StringValue())).GetHashCode() - ^ string.Join("", this.Content.Select(c => c.StringValue())).GetHashCode(); + return SortCode.GetHashCode() + ^ ExtraChatChannel.GetHashCode() + ^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode() + ^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode(); } private Guid ExtractExtraChatChannel() { - if (this.ContentSource.Payloads.Count > 0 && this.ContentSource.Payloads[0] is RawPayload raw) { + if (ContentSource.Payloads.Count > 0 && ContentSource.Payloads[0] is RawPayload raw) { // this does an encode and clone every time it's accessed, so cache var data = raw.Data; if (data[1] == 0x27 && data[2] == 18 && data[3] == 0x20) { @@ -138,4 +144,83 @@ internal class Message { return Guid.Empty; } + + /// + /// URLRegex returns a regex object that matches URLs like: + /// - https://example.com + /// - http://example.com + /// - www.example.com + /// - https://sub.example.com + /// - example.com + /// - sub.example.com + /// + /// It matches URLs with www. or https:// prefix, and also matches URLs + /// without a prefix on specific TLDs. + /// + private static Regex URLRegex = new( + @"((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]+)?", + RegexOptions.Compiled | RegexOptions.IgnoreCase + ); + + /// + /// Finds all URL strings in all TextChunks, splits the parent TextChunk + /// apart and inserts a new TextChunk with a URIPayload. + /// + private List ReplaceContentURLs(List content) + { + var newChunks = new List(); + void AddChunkWithMessage(Chunk chunk) { + chunk.Message = this; + newChunks.Add(chunk); + } + + foreach (var chunk in content) + { + // Use as is if it's not a text chunk or it already has a payload. + if (chunk is not TextChunk text || chunk.Link != null) + { + // No need to call AddChunkWithMessage here since the chunk + // already has the Message field set. + newChunks.Add(chunk); + continue; + } + + // Find all URLs with the regex and insert a new TextChunk with a + // URIPayload. + var matches = URLRegex.Matches(text.Content); + var remainderIndex = 0; + foreach (Match match in matches.Cast()) + { + // Add the text before the URL. + if (match.Index > remainderIndex) + { + AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, text.Content[remainderIndex..match.Index])); + } + + // Update the remainder index. + remainderIndex = match.Index + match.Length; + + // Create a new TextChunk with a URIPayload for the URL text. + try + { + var link = URIPayload.ResolveURI(match.Value); + AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, match.Value)); + } + catch (UriFormatException) + { + Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{match.Value}'"); + // If the URL is invalid, set the remainder index to the + // beginning of the match so it'll get included in the next + // regular text chunk. + remainderIndex = match.Index; + } + } + + // Add the text after the last URL. + if (remainderIndex < text.Content.Length) + AddChunkWithMessage(text.NewWithStyle(chunk.Source, null, text.Content[remainderIndex..])); + } + + return newChunks; + } } diff --git a/ChatTwo/PayloadHandler.cs b/ChatTwo/PayloadHandler.cs index 5170d5a..0722a03 100755 --- a/ChatTwo/PayloadHandler.cs +++ b/ChatTwo/PayloadHandler.cs @@ -86,6 +86,11 @@ public sealed class PayloadHandler { drawn = true; break; } + case URIPayload uri: { + DrawUriPopup(uri); + drawn = true; + break; + } } ContextFooter(drawn, chunk); @@ -100,11 +105,10 @@ public sealed class PayloadHandler { if (registered.Count == 0) { return; } + ImGui.Separator(); var contentId = chunk.Message?.ContentId ?? 0; - var sender = chunk.Message?.Sender - .Select(chunk => chunk.Link) - .FirstOrDefault(chunk => chunk is PlayerPayload) as PlayerPayload; + var sender = chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload) as PlayerPayload; if (ImGui.BeginMenu(Language.Context_Integrations)) { var cursor = ImGui.GetCursorPos(); @@ -127,13 +131,18 @@ public sealed class PayloadHandler { } } - private void ContextFooter(bool separator, Chunk chunk) { - if (separator) { + private void ContextFooter(bool didCustomContext, Chunk chunk) { + if (didCustomContext) { ImGui.Separator(); - } - if (!ImGui.BeginMenu(Plugin.PluginName)) { - return; + // Only place these menu items in a submenu if we've already drawn + // custom context menu items based on the payload. + // + // It makes it much more convenient in the majority of cases to + // copy the message content without having to open a submenu. + if (!ImGui.BeginMenu(Plugin.PluginName)) { + return; + } } ImGui.Checkbox(Language.Context_ScreenshotMode, ref LogWindow.ScreenshotMode); @@ -143,21 +152,16 @@ public sealed class PayloadHandler { } if (chunk.Message is { } message) { - if (ImGui.BeginMenu(Language.Context_Copy)) { - var text = message.Sender - .Concat(message.Content) - .Where(chunk => chunk is TextChunk) - .Cast() - .Select(text => text.Content) - .Aggregate(string.Concat); - ImGui.InputTextMultiline( - "##chat2-copy", - ref text, - (uint) text.Length, - new Vector2(350, 100) * ImGuiHelpers.GlobalScale, - ImGuiInputTextFlags.ReadOnly - ); - ImGui.EndMenu(); + if (ImGui.Selectable(Language.Context_Copy)) { + ImGui.SetClipboardText(StringifyMessage(message, true)); + WrapperUtil.AddNotification(Language.Context_CopySuccess, NotificationType.Info); + } + + // Only show a separate "Copy content" option if the message has + // Sender chunks so it doesn't show for system messages. + if (message.Sender.Count > 0 && ImGui.Selectable(Language.Context_CopyContent)) { + ImGui.SetClipboardText(StringifyMessage(message, false)); + WrapperUtil.AddNotification(Language.Context_CopyContentSuccess, NotificationType.Info); } var col = ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]; @@ -172,7 +176,20 @@ public sealed class PayloadHandler { } } - ImGui.EndMenu(); + if (didCustomContext) ImGui.EndMenu(); + } + + internal static string StringifyMessage(Message message, bool withSender = false) + { + if (message == null) + return string.Empty; + + var chunks = withSender ? message.Sender.Concat(message.Content) : message.Content; + return chunks + .Where(chunk => chunk is TextChunk) + .Cast() + .Select(text => text.Content) + .Aggregate(string.Concat); } internal void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { @@ -215,6 +232,11 @@ public sealed class PayloadHandler { DoHover(() => HoverItem(item), hoverSize); break; } + case URIPayload uri: + { + DoHover(() => HoverURI(uri), hoverSize); + break; + } } } @@ -334,6 +356,11 @@ public sealed class PayloadHandler { } } + private void HoverURI(URIPayload uri) { + ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority)); + ImGuiUtil.WarningText(Language.Context_URLWarning); + } + private void LeftClickPayload(Chunk chunk, Payload? payload) { switch (payload) { case MapLinkPayload map: { @@ -372,6 +399,10 @@ public sealed class PayloadHandler { break; } + case URIPayload uri: { + TryOpenURI(uri.Uri); + break; + } } } @@ -625,4 +656,38 @@ public sealed class PayloadHandler { return null; } + + private void DrawUriPopup(URIPayload uri) + { + ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority)); + ImGuiUtil.WarningText(Language.Context_URLWarning, false); + ImGui.Separator(); + + if (ImGui.Selectable(Language.Context_OpenInBrowser)) + { + TryOpenURI(uri.Uri); + } + + if (ImGui.Selectable(Language.Context_CopyLink)) + { + ImGui.SetClipboardText(uri.Uri.ToString()); + WrapperUtil.AddNotification(Language.Context_CopyLinkNotification, NotificationType.Info); + } + } + + private void TryOpenURI(Uri uri) + { + new Thread(() => { + try + { + Plugin.Log.Debug($"Opening URI {uri} in default browser"); + Dalamud.Utility.Util.OpenLink(uri.ToString()); + } + catch (Exception ex) + { + Plugin.Log.Error($"Error opening URI: {ex}"); + WrapperUtil.AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error); + } + }).Start(); + } } diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 45c1092..b9b4c8d 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -1,9 +1,11 @@ using System.Diagnostics; using System.Globalization; +using System.Reflection; using ChatTwo.Ipc; using ChatTwo.Resources; using ChatTwo.Ui; using ChatTwo.Util; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.Windowing; using Dalamud.IoC; @@ -15,7 +17,8 @@ using XivCommon; namespace ChatTwo; // ReSharper disable once ClassNeverInstantiated.Global -public sealed class Plugin : IDalamudPlugin { +public sealed class Plugin : IDalamudPlugin +{ internal const string PluginName = "Chat 2"; [PluginService] internal static IPluginLog Log { get; private set; } = null!; @@ -37,11 +40,14 @@ public sealed class Plugin : IDalamudPlugin { [PluginService] internal static INotificationManager Notification { get; private set; } = null!; [PluginService] internal static IAddonLifecycle AddonLifecycle { get; private set; } = null!; - public readonly WindowSystem WindowSystem = new(PluginName); + public const string Authors = "Infi, Anna"; + public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "Unknown"; + public readonly WindowSystem WindowSystem = new(PluginName); public SettingsWindow SettingsWindow { get; } public ChatLogWindow ChatLogWindow { get; } public CommandHelpWindow CommandHelpWindow { get; } + public SeStringDebugger SeStringDebugger { get; } internal Configuration Config { get; } internal Commands Commands { get; } @@ -58,7 +64,8 @@ public sealed class Plugin : IDalamudPlugin { internal DateTime GameStarted { get; } #pragma warning disable CS8618 - public Plugin() { + public Plugin() + { GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime(); Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); @@ -81,10 +88,12 @@ public sealed class Plugin : IDalamudPlugin { ChatLogWindow = new ChatLogWindow(this); SettingsWindow = new SettingsWindow(this); CommandHelpWindow = new CommandHelpWindow(ChatLogWindow); + SeStringDebugger = new SeStringDebugger(this); WindowSystem.AddWindow(ChatLogWindow); WindowSystem.AddWindow(SettingsWindow); WindowSystem.AddWindow(CommandHelpWindow); + WindowSystem.AddWindow(SeStringDebugger); FontManager.BuildFonts(); Interface.UiBuilder.DisableCutsceneUiHide = true; @@ -105,7 +114,8 @@ public sealed class Plugin : IDalamudPlugin { } #pragma warning restore CS8618 - public void Dispose() { + public void Dispose() + { Interface.LanguageChanged -= LanguageChanged; Interface.UiBuilder.Draw -= Draw; Framework.Update -= FrameworkUpdate; @@ -114,6 +124,7 @@ public sealed class Plugin : IDalamudPlugin { WindowSystem.RemoveAllWindows(); ChatLogWindow.Dispose(); SettingsWindow.Dispose(); + SeStringDebugger.Dispose(); ExtraChat.Dispose(); Ipc.Dispose(); @@ -126,6 +137,10 @@ public sealed class Plugin : IDalamudPlugin { private void Draw() { + + if (Config.HideInLoadingScreens && Condition[ConditionFlag.BetweenAreas]) + return; + Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden; ChatLogWindow.DefaultText = ImGui.GetStyle().Colors[(int) ImGuiCol.Text]; @@ -135,11 +150,13 @@ public sealed class Plugin : IDalamudPlugin { } } - internal void SaveConfig() { + internal void SaveConfig() + { Interface.SavePluginConfig(Config); } - internal void LanguageChanged(string langCode) { + internal void LanguageChanged(string langCode) + { var info = Config.LanguageOverride is LanguageOverride.None ? new CultureInfo(langCode) : new CultureInfo(Config.LanguageOverride.Code()); @@ -147,15 +164,17 @@ public sealed class Plugin : IDalamudPlugin { Language.Culture = info; } - private static readonly string[] ChatAddonNames = { + private static readonly string[] ChatAddonNames = + [ "ChatLog", "ChatLogPanel_0", "ChatLogPanel_1", "ChatLogPanel_2", - "ChatLogPanel_3", - }; + "ChatLogPanel_3" + ]; - private void FrameworkUpdate(IFramework framework) { + private void FrameworkUpdate(IFramework framework) + { if (DeferredSaveFrames >= 0 && DeferredSaveFrames-- == 0) { SaveConfig(); } diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index bce47a3..01c8b87 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -1103,6 +1103,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Copy content. + /// + internal static string Context_CopyContent { + get { + return ResourceManager.GetString("Context_CopyContent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copied message content to clipboard. + /// + internal static string Context_CopyContentSuccess { + get { + return ResourceManager.GetString("Context_CopyContentSuccess", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy Item Name. /// @@ -1112,6 +1130,33 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Copy link to clipboard. + /// + internal static string Context_CopyLink { + get { + return ResourceManager.GetString("Context_CopyLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copied link to clipboard. + /// + internal static string Context_CopyLinkNotification { + get { + return ResourceManager.GetString("Context_CopyLinkNotification", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copied message to clipboard. + /// + internal static string Context_CopySuccess { + get { + return ResourceManager.GetString("Context_CopySuccess", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hide chat. /// @@ -1193,6 +1238,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Open link in browser. + /// + internal static string Context_OpenInBrowser { + get { + return ResourceManager.GetString("Context_OpenInBrowser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to open the link in the browser, please report this issue. + /// + internal static string Context_OpenInBrowserError { + get { + return ResourceManager.GetString("Context_OpenInBrowserError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Promote. /// @@ -1274,6 +1337,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to URL at {0}. + /// + internal static string Context_URLDomain { + get { + return ResourceManager.GetString("Context_URLDomain", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only open URLs from websites you trust. + /// + internal static string Context_URLWarning { + get { + return ResourceManager.GetString("Context_URLWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Chinese (full). /// @@ -1418,6 +1499,15 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Authors: . + /// + internal static string Options_About_Authors { + get { + return ResourceManager.GetString("Options_About_Authors", resourceCulture); + } + } + /// /// Looks up a localized string similar to Click the button to the left to see what's being worked on and what's next.. /// @@ -1428,7 +1518,7 @@ namespace ChatTwo.Resources { } /// - /// Looks up a localized string similar to Click the button to the left to help translate {0}.. + /// Looks up a localized string similar to Help to translate: . /// internal static string Options_About_CrowdIn { get { @@ -1436,6 +1526,33 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Discord: . + /// + internal static string Options_About_Discord { + get { + return ResourceManager.GetString("Options_About_Discord", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get help in the discord thread: . + /// + internal static string Options_About_Discord_Thread { + get { + return ResourceManager.GetString("Options_About_Discord_Thread", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get help through github issues: . + /// + internal static string Options_About_Github_Issues { + get { + return ResourceManager.GetString("Options_About_Github_Issues", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0} is a project to completely recreate the in-game chat and make it even better.. /// @@ -1463,6 +1580,15 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Version: . + /// + internal static string Options_About_Version { + get { + return ResourceManager.GetString("Options_About_Version", resourceCulture); + } + } + /// /// Looks up a localized string similar to Allow moving chat. /// @@ -1716,7 +1842,7 @@ namespace ChatTwo.Resources { } /// - /// Looks up a localized string similar to Hide chat during cutscenes. + /// Looks up a localized string similar to Hide during cutscenes. /// internal static string Options_HideDuringCutscenes_Name { get { @@ -1724,6 +1850,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Hide {0} during loading screens.. + /// + internal static string Options_HideInLoadingScreens_Description { + get { + return ResourceManager.GetString("Options_HideInLoadingScreens_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hide during loading screens. + /// + internal static string Options_HideInLoadingScreens_Name { + get { + return ResourceManager.GetString("Options_HideInLoadingScreens_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hide timestamps when previous messages have the same timestamp.. /// @@ -1904,6 +2048,33 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Override Style. + /// + internal static string Options_OverrideStyle_Name { + get { + return ResourceManager.GetString("Options_OverrideStyle_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Override your selected dalamud style with a different one. + /// + internal static string Options_OverrideStyle_Name_Desc { + get { + return ResourceManager.GetString("Options_OverrideStyle_Name_Desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Styles. + /// + internal static string Options_OverrideStyleDropdown_Name { + get { + return ResourceManager.GetString("Options_OverrideStyleDropdown_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display messages in a more modern style.. /// diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index 8609d85..da1ea88 100644 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -128,7 +128,7 @@ Hide the in-game chat window when the plugin is active. - Hide chat during cutscenes + Hide during cutscenes Hide {0} during cutscenes. @@ -377,7 +377,7 @@ Click the button to the left to see what's being worked on and what's next. - Click the button to the left to help translate {0}. + Help to translate: Translators @@ -502,6 +502,12 @@ If this is enabled, the Auto Translate list will be sorted alphabetically. + + Override Style + + + Styles + Ctrl + {0} @@ -859,16 +865,16 @@ Do not close FFXIV, unload Dalamud, or turn off your computer during this time. - + View Adventurer Plate - + Unable to open adventurer plate at this moment - + Tooltip offset - + Use this option if you experience cut-off tooltips. @@ -895,4 +901,55 @@ ExtraChat Linkshell [8] + + Copy content + + + Copied message content to clipboard + + + Copied message to clipboard + + + Copy link to clipboard + + + Copied link to clipboard + + + Open link in browser + + + Failed to open the link in the browser, please report this issue + + + URL at {0} + + + Only open URLs from websites you trust + + + Authors: + + + Discord: + + + Version: + + + Get help through github issues: + + + Get help in the discord thread: + + + Override your selected dalamud style with a different one + + + Hide during loading screens + + + Hide {0} during loading screens. + diff --git a/ChatTwo/Store.cs b/ChatTwo/Store.cs index dda6102..60a6209 100755 --- a/ChatTwo/Store.cs +++ b/ChatTwo/Store.cs @@ -86,6 +86,11 @@ internal class Store : IDisposable { ["Type"] = new("PartyFinder"), ["Id"] = new(partyFinder.Id), }); + case URIPayload uri: + return new BsonDocument(new Dictionary { + ["Type"] = new("URI"), + ["Uri"] = new(uri.Uri.ToString()), + }); } return payload?.Encode(); @@ -99,6 +104,7 @@ internal class Store : IDisposable { return bson["Type"].AsString switch { "Achievement" => new AchievementPayload((uint) bson["Id"].AsInt64), "PartyFinder" => new PartyFinderPayload((uint) bson["Id"].AsInt64), + "URI" => new URIPayload(new Uri(bson["Uri"].AsString)), _ => null, }; } @@ -257,6 +263,7 @@ internal class Store : IDisposable { } } + public (SeString? Sender, SeString? Message) LastMessage = (null, null); private void ChatMessage(XivChatType type, uint senderId, SeString sender, SeString message) { var chatCode = new ChatCode((ushort) type); @@ -265,6 +272,7 @@ internal class Store : IDisposable { formatting = FormatFor(chatCode.Type); } + LastMessage = (sender, message); var senderChunks = new List(); if (formatting is { IsPresent: true }) { senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.Before) { diff --git a/ChatTwo/TextureCache.cs b/ChatTwo/TextureCache.cs index 6c0caad..06ad9a4 100755 --- a/ChatTwo/TextureCache.cs +++ b/ChatTwo/TextureCache.cs @@ -11,17 +11,17 @@ internal class TextureCache : IDisposable { private readonly Dictionary<(uint, bool), IDalamudTextureWrap> _statusIcons = new(); private readonly Dictionary<(uint, bool), IDalamudTextureWrap> _eventItemIcons = new(); - internal IReadOnlyDictionary<(uint, bool), IDalamudTextureWrap> ItemIcons => this._itemIcons; - internal IReadOnlyDictionary<(uint, bool), IDalamudTextureWrap> StatusIcons => this._statusIcons; - internal IReadOnlyDictionary<(uint, bool), IDalamudTextureWrap> EventItemIcons => this._eventItemIcons; + internal IReadOnlyDictionary<(uint, bool), IDalamudTextureWrap> ItemIcons => _itemIcons; + internal IReadOnlyDictionary<(uint, bool), IDalamudTextureWrap> StatusIcons => _statusIcons; + internal IReadOnlyDictionary<(uint, bool), IDalamudTextureWrap> EventItemIcons => _eventItemIcons; internal TextureCache(ITextureProvider textureProvider) { - this.TextureProvider = textureProvider; + TextureProvider = textureProvider; } public void Dispose() { - var allIcons = this.ItemIcons.Values - .Concat(this.StatusIcons.Values); + var allIcons = ItemIcons.Values + .Concat(StatusIcons.Values); foreach (var tex in allIcons) { tex.Dispose(); @@ -34,40 +34,40 @@ internal class TextureCache : IDisposable { } var tex = hq - ? this.TextureProvider.GetIcon(icon, ITextureProvider.IconFlags.ItemHighQuality) - : this.TextureProvider.GetIcon(icon); + ? TextureProvider.GetIcon(icon, ITextureProvider.IconFlags.ItemHighQuality) + : TextureProvider.GetIcon(icon); if (tex != null) { dict[(icon, hq)] = tex; } } internal void AddItem(Item item, bool hq) { - this.AddIcon(this._itemIcons, item.Icon, hq); + AddIcon(_itemIcons, item.Icon, hq); } internal void AddStatus(Status status) { - this.AddIcon(this._statusIcons, status.Icon); + AddIcon(_statusIcons, status.Icon); } internal void AddEventItem(EventItem item) { - this.AddIcon(this._eventItemIcons, item.Icon); + AddIcon(_eventItemIcons, item.Icon); } internal IDalamudTextureWrap? GetItem(Item item, bool hq = false) { - this.AddItem(item, hq); - this.ItemIcons.TryGetValue((item.Icon, hq), out var icon); + AddItem(item, hq); + ItemIcons.TryGetValue((item.Icon, hq), out var icon); return icon; } internal IDalamudTextureWrap? GetStatus(Status status) { - this.AddStatus(status); - this.StatusIcons.TryGetValue((status.Icon, false), out var icon); + AddStatus(status); + StatusIcons.TryGetValue((status.Icon, false), out var icon); return icon; } internal IDalamudTextureWrap? GetEventItem(EventItem item) { - this.AddEventItem(item); - this.EventItemIcons.TryGetValue((item.Icon, false), out var icon); + AddEventItem(item); + EventItemIcons.TryGetValue((item.Icon, false), out var icon); return icon; } } diff --git a/ChatTwo/Ui/AutoCompleteInfo.cs b/ChatTwo/Ui/AutoCompleteInfo.cs index 756c425..e0d7222 100755 --- a/ChatTwo/Ui/AutoCompleteInfo.cs +++ b/ChatTwo/Ui/AutoCompleteInfo.cs @@ -6,8 +6,8 @@ internal class AutoCompleteInfo { internal int EndPos { get; } internal AutoCompleteInfo(string toComplete, int startPos, int endPos) { - this.ToComplete = toComplete; - this.StartPos = startPos; - this.EndPos = endPos; + ToComplete = toComplete; + StartPos = startPos; + EndPos = endPos; } } diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index b665d01..7692a63 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -13,6 +13,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.Internal; +using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Memory; @@ -75,12 +76,8 @@ public sealed class ChatLogWindow : Window, IUiComponent { Plugin = plugin; Salt = new Random().Next().ToString(); + Size = new Vector2(500, 250); SizeCondition = ImGuiCond.FirstUseEver; - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(500, 250), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) - }; PayloadHandler = new PayloadHandler(this); HandlerLender = new Lender(() => new PayloadHandler(this)); @@ -100,6 +97,24 @@ public sealed class ChatLogWindow : Window, IUiComponent { Plugin.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip); } + public override void PreDraw() + { + if (Plugin.Config.OverrideStyle) + { + var styles = StyleModel.GetConfiguredStyles(); + styles?.First(style => style.Name.Equals(Plugin.Config.ChosenStyle)).Push(); + } + } + + public override void PostDraw() + { + if (Plugin.Config.OverrideStyle) + { + var styles = StyleModel.GetConfiguredStyles(); + styles?.First(style => style.Name.Equals(Plugin.Config.ChosenStyle)).Pop(); + } + } + public void Dispose() { Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip); Plugin.ClientState.Logout -= Logout; @@ -1292,10 +1307,8 @@ public sealed class ChatLogWindow : Window, IUiComponent { || cmd.Alias.RawString == command || cmd.ShortCommand.RawString == command || cmd.ShortAlias.RawString == command); - if (cmd != null) { + if (cmd != null) Plugin.CommandHelpWindow.UpdateContent(cmd); - Plugin.CommandHelpWindow.IsOpen = true; - } } if (data->EventFlag != ImGuiInputTextFlags.CallbackHistory) { @@ -1367,18 +1380,20 @@ public sealed class ChatLogWindow : Window, IUiComponent { private void DrawChunk(Chunk chunk, bool wrap = true, PayloadHandler? handler = null, float lineWidth = 0f) { if (chunk is IconChunk icon && _fontIcon != null) { - var bounds = IconUtil.GetBounds((byte) icon.Icon); - if (bounds != null) { - var texSize = new Vector2(_fontIcon.Width, _fontIcon.Height); + var bounds = IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out var entry); + if (!bounds) + return; - var sizeRatio = Plugin.Config.FontSize / bounds.Value.W; - var size = new Vector2(bounds.Value.Z, bounds.Value.W) * sizeRatio * ImGuiHelpers.GlobalScale; + var texSize = new Vector2(_fontIcon.Width, _fontIcon.Height); - var uv0 = new Vector2(bounds.Value.X, bounds.Value.Y - 2) / texSize; - var uv1 = new Vector2(bounds.Value.X + bounds.Value.Z, bounds.Value.Y - 2 + bounds.Value.W) / texSize; - ImGui.Image(_fontIcon.ImGuiHandle, size, uv0, uv1); - ImGuiUtil.PostPayload(chunk, handler); - } + var sizeRatio = Plugin.Config.FontSize / entry.Height; + var size = new Vector2(entry.Width, entry.Height) * sizeRatio * ImGuiHelpers.GlobalScale; + + var uv0 = new Vector2(entry.Left, entry.Top + 170) * 2 / texSize; + var uv1 = new Vector2(entry.Left + entry.Width, entry.Top + entry.Height + 170) * 2 / texSize; + + ImGui.Image(_fontIcon.ImGuiHandle, size, uv0, uv1); + ImGuiUtil.PostPayload(chunk, handler); return; } diff --git a/ChatTwo/Ui/CommandHelpWindow.cs b/ChatTwo/Ui/CommandHelpWindow.cs index ab9ac55..23590a3 100644 --- a/ChatTwo/Ui/CommandHelpWindow.cs +++ b/ChatTwo/Ui/CommandHelpWindow.cs @@ -20,6 +20,7 @@ public class CommandHelpWindow : Window { ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize; } + // Sets IsOpen to true if it should be drawn public void UpdateContent(TextCommand command) { Command = command; @@ -36,6 +37,7 @@ public class CommandHelpWindow : Window { break; case CommandHelpSide.None: default: + IsOpen = false; return; } @@ -45,6 +47,8 @@ public class CommandHelpWindow : Window { MinimumSize = new Vector2(width, 0), MaximumSize = LogWindow.LastWindowSize with { X = width } }; + + IsOpen = true; } public override void Draw() diff --git a/ChatTwo/Ui/Fonts.cs b/ChatTwo/Ui/Fonts.cs index ceec9e1..2468152 100755 --- a/ChatTwo/Ui/Fonts.cs +++ b/ChatTwo/Ui/Fonts.cs @@ -169,7 +169,7 @@ internal sealed class FaceData { internal byte[] Data { get; } internal FaceData(byte[] data) { - this.Data = data; + Data = data; } } @@ -178,8 +178,8 @@ internal sealed class FontData { internal FaceData? Italic { get; } internal FontData(FaceData regular, FaceData? italic) { - this.Regular = regular; - this.Italic = italic; + Regular = regular; + Italic = italic; } } @@ -189,8 +189,8 @@ internal sealed class Font { internal string ResourcePathItalic { get; } internal Font(string name, string resourcePath, string resourcePathItalic) { - this.Name = name; - this.ResourcePath = resourcePath; - this.ResourcePathItalic = resourcePathItalic; + Name = name; + ResourcePath = resourcePath; + ResourcePathItalic = resourcePathItalic; } } diff --git a/ChatTwo/Ui/Popout.cs b/ChatTwo/Ui/Popout.cs index 5aa0636..96c0dfa 100644 --- a/ChatTwo/Ui/Popout.cs +++ b/ChatTwo/Ui/Popout.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Dalamud.Interface.Style; using Dalamud.Interface.Windowing; using ImGuiNET; @@ -16,16 +17,17 @@ internal class Popout : Window Tab = tab; Idx = idx; + Size = new Vector2(350, 350); SizeCondition = ImGuiCond.FirstUseEver; - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(350, 350), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) - }; } public override void PreDraw() { + if (ChatLogWindow.Plugin.Config.OverrideStyle) + { + var styles = StyleModel.GetConfiguredStyles(); + styles?.First(style => style.Name.Equals(ChatLogWindow.Plugin.Config.ChosenStyle)).Push(); + } Flags = ImGuiWindowFlags.None; if (!ChatLogWindow.Plugin.Config.ShowPopOutTitleBar) Flags |= ImGuiWindowFlags.NoTitleBar; @@ -53,7 +55,13 @@ internal class Popout : Window public override void PostDraw() { + ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); + if (ChatLogWindow.Plugin.Config.OverrideStyle) + { + var styles = StyleModel.GetConfiguredStyles(); + styles?.First(style => style.Name.Equals(ChatLogWindow.Plugin.Config.ChosenStyle)).Pop(); + } } public override void OnClose() @@ -64,4 +72,4 @@ internal class Popout : Window Tab.PopOut = false; ChatLogWindow.Plugin.SaveConfig(); } -} \ No newline at end of file +} diff --git a/ChatTwo/Ui/SeStringDebugger.cs b/ChatTwo/Ui/SeStringDebugger.cs new file mode 100644 index 0000000..74664e7 --- /dev/null +++ b/ChatTwo/Ui/SeStringDebugger.cs @@ -0,0 +1,323 @@ +using System.Numerics; +using System.Text; +using ChatTwo.Util; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload; + +namespace ChatTwo.Ui; + +public class SeStringDebugger : Window +{ + private readonly Plugin Plugin; + + public SeStringDebugger(Plugin plugin) : base($"SeString Debugger###chat2-sestringdebugger") + { + Plugin = plugin; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(475, 600), + MaximumSize = new Vector2(float.MaxValue, float.MaxValue) + }; + + #if DEBUG + Plugin.Commands.Register("/chat2Debugger").Execute += Toggle; + #endif + } + + public void Dispose() + { + #if DEBUG + Plugin.Commands.Register("/chat2Debugger").Execute -= Toggle; + #endif + } + + private void Toggle(string _, string __) => Toggle(); + + public override void Draw() + { + ImGui.TextUnformatted("SeString Content"); + ImGui.Spacing(); + + if (Plugin.Store.LastMessage.Message == null) + { + ImGui.TextUnformatted("Nothing to show"); + return; + } + + // TODO: Make SeString freely selectable through chat + foreach (var payload in Plugin.Store.LastMessage.Message.Payloads) + { + switch (payload) + { + case UIForegroundPayload color: + { + RenderMetadataDictionary("Link ForegroundColor", new Dictionary + { + { "Enabled?", color.IsEnabled.ToString() }, + { "ColorKey", color.IsEnabled ? color.ColorKey.ToString() : "Color Ended" }, + }); + break; + } + case MapLinkPayload map: + { + RenderMetadataDictionary("Link MapLinkPayload", new Dictionary + { + { "Map.RowId", map.Map?.RowId.ToString() }, + { "Map.PlaceName", map.Map?.PlaceName.Value?.Name.ToString() }, + { "Map.PlaceNameRegion", map.Map?.PlaceNameRegion.Value?.Name.ToString() }, + { "Map.PlaceNameSub", map.Map?.PlaceNameSub.Value?.Name.ToString() }, + { "TerritoryType.RowId", map.TerritoryType?.RowId.ToString() }, + { "RawX", map.RawX.ToString() }, + { "RawY", map.RawY.ToString() }, + { "XCoord", map.XCoord.ToString() }, + { "YCoord", map.YCoord.ToString() }, + { "CoordinateString", map.CoordinateString }, + { "DataString", map.DataString }, + }); + break; + } + case QuestPayload quest: + { + RenderMetadataDictionary("Link QuestPayload", new Dictionary + { + { "Quest.RowId", quest.Quest?.RowId.ToString() }, + { "Quest.Name", quest.Quest?.Name.ToString() }, + }); + break; + } + case DalamudLinkPayload link: + { + RenderMetadataDictionary("Link DalamudLinkPayload", new Dictionary + { + { "CommandId", link.CommandId.ToString() }, + { "Plugin", link.Plugin }, + }); + break; + } + case DalamudPartyFinderPayload pf: + { + RenderMetadataDictionary("Link PartyFinderPayload", new Dictionary + { + { "ListingId", pf.ListingId.ToString() }, + { "LinkType", EnumName(pf.LinkType) }, + }); + break; + } + case PlayerPayload player: + { + RenderMetadataDictionary("Link PlayerPayload", new Dictionary + { + { "Real", player.DisplayedName }, + { "PlayerName", player.PlayerName }, + { "World.Name", player.World.Name }, + }); + break; + } + case ItemPayload item: + { + RenderMetadataDictionary("Link ItemPayload", new Dictionary + { + { "ItemId", item.ItemId.ToString() }, + { "RawItemId", item.RawItemId.ToString() }, + { "Kind", EnumName(item.Kind) }, + { "IsHQ", item.IsHQ.ToString() }, + { "Item.Name", item.Item?.Name.ToString() }, + }); + break; + } + case AutoTranslatePayload at: + { + RenderMetadataDictionary("Link AutoTranslatePayload", new Dictionary + { + { "Text", at.Text }, + }); + break; + } + case IconPayload icon: + { + var found = IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out var entry); + RenderMetadataDictionary("Link IconPayload", new Dictionary + { + { "Found", found.ToString() }, + { "Icon ID", ((uint) icon.Icon).ToString() }, + }); + break; + } + case RawPayload raw: + { + var colorPayload = ColorPayload.From(raw.Data); + if (colorPayload != null) + { + var push = colorPayload.Enabled && colorPayload.Color != 0; + // if (push) ImGui.PushStyleColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(colorPayload.U)); + RenderMetadataDictionary("Link ColorPayload", new Dictionary + { + { "Unshifted", colorPayload.UnshiftedColor.ToString("X8") }, + { "Color", colorPayload.Color.ToString("X8") }, + { "Enabled?", colorPayload.Enabled.ToString() }, + }); + // if (push) ImGui.PopStyleColor(); + } + else + { + RenderMetadataDictionary("Link RawPayload", new Dictionary + { + { "Data", string.Join(" ", raw.Data.Select(b => b.ToString("X2"))) }, + { "Type", EnumName(raw.Type) }, + }); + } + break; + } + case StatusPayload status: + { + RenderMetadataDictionary("Link StatusPayload", new Dictionary + { + { "Status.RowId", status.Status.RowId.ToString() }, + { "Status.Name", status.Status.Name }, + { "Status.Icon", status.Status.Icon.ToString() } + }); + break; + } + + case Util.PartyFinderPayload pf: + { + RenderMetadataDictionary("Link PartyFinderPayload", new Dictionary + { + { "Id", pf.Id.ToString() } + }); + break; + } + case AchievementPayload achievement: + { + RenderMetadataDictionary("Link AchievementPayload", new Dictionary + { + { "Id", achievement.Id.ToString() } + }); + break; + } + default: + var payloadData = payload.Encode(); + + var initialByte = payloadData.First(); + if (initialByte != 0x02) + { + RenderMetadataDictionary("Text Payload", new Dictionary + { + { "Content", Encoding.UTF8.GetString(payloadData) }, + }); + } + else + { + var unknown = new RawPayload(payloadData); + RenderMetadataDictionary("Link Unknown", new Dictionary + { + { "Unknown", string.Join(" ", unknown.Data.Select(b => b.ToString("X2"))) }, + }); + } + break; + } + } + } + + private static string? EnumName(T? value) where T : Enum + { + if (value == null) + { + return null; + } + var rawValue = Convert.ChangeType(value, value.GetTypeCode()); + return (Enum.GetName(value.GetType(), value) ?? "Unknown") + $" ({rawValue})"; + } + + private static void RenderMetadataDictionary(string name, Dictionary metadata) + { + var style = ImGui.GetStyle(); + + ImGui.Text($"{name}:"); + ImGui.Indent(style.IndentSpacing); + if (!ImGui.BeginTable($"##chat3-{name}", 2, 0)) + { + ImGui.EndTable(); + ImGui.Unindent(style.IndentSpacing); + return; + } + ImGui.TableSetupColumn($"##chat3-{name}-key", 0, 0.4f); + ImGui.TableSetupColumn($"##chat3-{name}-value"); + for (var i = 0; i < metadata.Count; i++) + { + var (key, value) = metadata.ElementAt(i); + ImGui.PushID(i); + ImGui.TableNextColumn(); + ImGui.Text(key); + ImGui.TableNextColumn(); + ImGuiTextVisibleWhitespace(value); + ImGui.PopID(); + } + ImGui.EndTable(); + ImGui.Unindent(style.IndentSpacing); + ImGui.NewLine(); + } + + // ImGuiTextVisibleWhitespace replaces leading and trailing whitespace with + // visible characters. The extra characters are rendered with a muted font. + private static void ImGuiTextVisibleWhitespace(string? original, bool wrap = true) + { + if (string.IsNullOrEmpty(original)) + { + var str = original == null ? "(null)" : "(empty)"; + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 1, 1, 0.5f)); + ImGui.TextUnformatted(str); + ImGui.PopStyleColor(); + return; + } + + var text = original; + var start = 0; + var end = text.Length; + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + + void WriteText(string text) + { + if (wrap) + { + ImGui.TextWrapped(text); + } + else + { + ImGui.TextUnformatted(text); + } + } + + while (start < end && char.IsWhiteSpace(text[start])) + { + start++; + } + if (start > 0) + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 1, 1, 0.5f)); + WriteText(new string('_', start)); + ImGui.PopStyleColor(); + ImGui.SameLine(); + } + + while (end > start && char.IsWhiteSpace(text[end - 1])) + { + end--; + } + + WriteText(text[start..end]); + if (end < text.Length) + { + ImGui.SameLine(); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 1, 1, 0.5f)); + WriteText(new string('_', text.Length - end)); + ImGui.PopStyleColor(); + } + + ImGui.PopStyleVar(); + } +} diff --git a/ChatTwo/Ui/SettingsTabs/About.cs b/ChatTwo/Ui/SettingsTabs/About.cs index 4c212e2..d7b0927 100755 --- a/ChatTwo/Ui/SettingsTabs/About.cs +++ b/ChatTwo/Ui/SettingsTabs/About.cs @@ -2,6 +2,8 @@ using System.Numerics; using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using ImGuiNET; namespace ChatTwo.Ui.SettingsTabs; @@ -31,23 +33,40 @@ internal sealed class About : ISettingsTab { ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName)); + ImGuiHelpers.ScaledDummy(10.0f); + + ImGui.TextUnformatted(Language.Options_About_Authors); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGold, Plugin.Authors); + + ImGui.TextUnformatted(Language.Options_About_Discord); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGold, "@infi"); + + ImGui.TextUnformatted(Language.Options_About_Version); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedOrange, Plugin.Version); + + ImGuiHelpers.ScaledDummy(10.0f); + + ImGui.TextUnformatted(Language.Options_About_Discord_Thread); + ImGui.SameLine(); + if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "discordThread")) + Dalamud.Utility.Util.OpenLink("https://canary.discord.com/channels/581875019861328007/1224865018789761126"); + ImGui.Spacing(); - if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "clickup")) - { - Dalamud.Utility.Util.OpenLink("https://sharing.clickup.com/b/h/6-122378074-2/1047d21a39a4140"); - } - + ImGui.TextUnformatted(Language.Options_About_Github_Issues); ImGui.SameLine(); - ImGui.TextUnformatted(Language.Options_About_ClickUp); + if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues")) + Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo/issues"); + ImGuiHelpers.ScaledDummy(10.0f); + + ImGui.TextUnformatted(Language.Options_About_CrowdIn); + ImGui.SameLine(); if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "crowdin")) - { Dalamud.Utility.Util.OpenLink("https://crowdin.com/project/chattwo"); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(string.Format(Language.Options_About_CrowdIn, Plugin.PluginName)); ImGui.Spacing(); @@ -68,7 +87,6 @@ internal sealed class About : ISettingsTab { ImGui.EndChild(); } - ImGuiUtil.HelpText($"{Plugin.PluginName} v{GetType().Assembly.GetName().Version?.ToString(3)}"); ImGui.PopTextWrapPos(); } } diff --git a/ChatTwo/Ui/SettingsTabs/ChatColours.cs b/ChatTwo/Ui/SettingsTabs/ChatColours.cs index b49b21f..0571895 100755 --- a/ChatTwo/Ui/SettingsTabs/ChatColours.cs +++ b/ChatTwo/Ui/SettingsTabs/ChatColours.cs @@ -13,8 +13,8 @@ internal sealed class ChatColours : ISettingsTab { public string Name => Language.Options_ChatColours_Tab + "###tabs-chat-colours"; internal ChatColours(Configuration mutable, Plugin plugin) { - this.Mutable = mutable; - this.Plugin = plugin; + Mutable = mutable; + Plugin = plugin; #if DEBUG var sortable = ChatTypeExt.SortOrder @@ -36,23 +36,23 @@ internal sealed class ChatColours : ISettingsTab { foreach (var (_, types) in ChatTypeExt.SortOrder) { foreach (var type in types) { if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset)) { - this.Mutable.ChatColours.Remove(type); + Mutable.ChatColours.Remove(type); } ImGui.SameLine(); if (ImGuiUtil.IconButton(FontAwesomeIcon.LongArrowAltDown, $"{type}", Language.Options_ChatColours_Import)) { - var gameColour = this.Plugin.Functions.Chat.GetChannelColour(type); - this.Mutable.ChatColours[type] = gameColour ?? type.DefaultColour() ?? 0; + var gameColour = Plugin.Functions.Chat.GetChannelColour(type); + Mutable.ChatColours[type] = gameColour ?? type.DefaultColour() ?? 0; } ImGui.SameLine(); - var vec = this.Mutable.ChatColours.TryGetValue(type, out var colour) + var vec = Mutable.ChatColours.TryGetValue(type, out var colour) ? ColourUtil.RgbaToVector3(colour) : ColourUtil.RgbaToVector3(type.DefaultColour() ?? 0); if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs)) { - this.Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec); + Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec); } } } diff --git a/ChatTwo/Ui/SettingsTabs/Database.cs b/ChatTwo/Ui/SettingsTabs/Database.cs index 97aa67b..f0814f5 100755 --- a/ChatTwo/Ui/SettingsTabs/Database.cs +++ b/ChatTwo/Ui/SettingsTabs/Database.cs @@ -11,36 +11,36 @@ internal sealed class Database : ISettingsTab { public string Name => Language.Options_Database_Tab + "###tabs-database"; internal Database(Configuration mutable, Store store) { - this.Store = store; - this.Mutable = mutable; + Store = store; + Mutable = mutable; } private bool _showAdvanced; public void Draw(bool changed) { if (changed) { - this._showAdvanced = ImGui.GetIO().KeyShift; + _showAdvanced = ImGui.GetIO().KeyShift; } - ImGuiUtil.OptionCheckbox(ref this.Mutable.DatabaseBattleMessages, Language.Options_DatabaseBattleMessages_Name, Language.Options_DatabaseBattleMessages_Description); + ImGuiUtil.OptionCheckbox(ref Mutable.DatabaseBattleMessages, Language.Options_DatabaseBattleMessages_Name, Language.Options_DatabaseBattleMessages_Description); ImGui.Spacing(); - if (ImGuiUtil.OptionCheckbox(ref this.Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description)) { - if (this.Mutable.LoadPreviousSession) { - this.Mutable.FilterIncludePreviousSessions = true; + if (ImGuiUtil.OptionCheckbox(ref Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description)) { + if (Mutable.LoadPreviousSession) { + Mutable.FilterIncludePreviousSessions = true; } } ImGui.Spacing(); - if (ImGuiUtil.OptionCheckbox(ref this.Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description)) { - if (!this.Mutable.FilterIncludePreviousSessions) { - this.Mutable.LoadPreviousSession = false; + if (ImGuiUtil.OptionCheckbox(ref Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description)) { + if (!Mutable.FilterIncludePreviousSessions) { + Mutable.LoadPreviousSession = false; } } ImGuiUtil.OptionCheckbox( - ref this.Mutable.SharedMode, + ref Mutable.SharedMode, Language.Options_SharedMode_Name, string.Format(Language.Options_SharedMode_Description, Plugin.PluginName) ); @@ -48,16 +48,16 @@ internal sealed class Database : ISettingsTab { ImGui.Spacing(); - if (this._showAdvanced && ImGui.TreeNodeEx(Language.Options_Database_Advanced)) { + if (_showAdvanced && ImGui.TreeNodeEx(Language.Options_Database_Advanced)) { ImGui.PushTextWrapPos(); ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning); if (ImGui.Button("Checkpoint")) { - this.Store.Database.Checkpoint(); + Store.Database.Checkpoint(); } if (ImGui.Button("Rebuild")) { - this.Store.Database.Rebuild(); + Store.Database.Rebuild(); } ImGui.PopTextWrapPos(); diff --git a/ChatTwo/Ui/SettingsTabs/Display.cs b/ChatTwo/Ui/SettingsTabs/Display.cs index 4f54571..c127086 100755 --- a/ChatTwo/Ui/SettingsTabs/Display.cs +++ b/ChatTwo/Ui/SettingsTabs/Display.cs @@ -1,5 +1,6 @@ using ChatTwo.Resources; using ChatTwo.Util; +using Dalamud.Interface.Style; using ImGuiNET; namespace ChatTwo.Ui.SettingsTabs; @@ -40,6 +41,12 @@ internal sealed class Display : ISettingsTab { ); ImGui.Spacing(); + ImGuiUtil.OptionCheckbox( + ref Mutable.HideInLoadingScreens, + Language.Options_HideInLoadingScreens_Name, + string.Format(Language.Options_HideInLoadingScreens_Description, Plugin.PluginName)); + ImGui.Spacing(); + ImGuiUtil.OptionCheckbox( ref Mutable.NativeItemTooltips, Language.Options_NativeItemTooltips_Name, @@ -89,6 +96,24 @@ internal sealed class Display : ISettingsTab { ImGuiUtil.OptionCheckbox(ref Mutable.ShowPopOutTitleBar, Language.Options_ShowPopOutTitleBar_Name); ImGui.Spacing(); + ImGuiUtil.OptionCheckbox(ref Mutable.OverrideStyle, Language.Options_OverrideStyle_Name, Language.Options_OverrideStyle_Name_Desc); + ImGui.Spacing(); + + if (Mutable.OverrideStyle) + { + var currentStyle = Mutable.ChosenStyle.Equals("") ? StyleModel.GetConfiguredStyle().Name : Mutable.ChosenStyle; + if (ImGui.BeginCombo(Language.Options_OverrideStyleDropdown_Name, currentStyle)) { + foreach (var style in StyleModel.GetConfiguredStyles()) { + if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name)) { + Mutable.ChosenStyle = style.Name; + } + } + + ImGui.EndCombo(); + } + } + ImGui.Spacing(); + ImGui.PopTextWrapPos(); } } diff --git a/ChatTwo/Ui/SettingsTabs/Fonts.cs b/ChatTwo/Ui/SettingsTabs/Fonts.cs index db14ee8..95474f4 100755 --- a/ChatTwo/Ui/SettingsTabs/Fonts.cs +++ b/ChatTwo/Ui/SettingsTabs/Fonts.cs @@ -12,45 +12,45 @@ public class Fonts : ISettingsTab { private List JpFonts { get; set; } = new(); internal Fonts(Configuration mutable) { - this.Mutable = mutable; - this.UpdateFonts(); + Mutable = mutable; + UpdateFonts(); } private void UpdateFonts() { - this.GlobalFonts = Ui.Fonts.GetFonts(); - this.JpFonts = Ui.Fonts.GetJpFonts(); + GlobalFonts = Ui.Fonts.GetFonts(); + JpFonts = Ui.Fonts.GetJpFonts(); } public void Draw(bool changed) { if (changed) { - this.UpdateFonts(); + UpdateFonts(); } ImGui.PushTextWrapPos(); - ImGui.Checkbox(Language.Options_FontsEnabled, ref this.Mutable.FontsEnabled); + ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled); ImGui.Spacing(); - if (this.Mutable.FontsEnabled) { - if (ImGuiUtil.BeginComboVertical(Language.Options_Font_Name, this.Mutable.GlobalFont)) { + if (Mutable.FontsEnabled) { + if (ImGuiUtil.BeginComboVertical(Language.Options_Font_Name, Mutable.GlobalFont)) { foreach (var font in Ui.Fonts.GlobalFonts) { - if (ImGui.Selectable(font.Name, this.Mutable.GlobalFont == font.Name)) { - this.Mutable.GlobalFont = font.Name; + if (ImGui.Selectable(font.Name, Mutable.GlobalFont == font.Name)) { + Mutable.GlobalFont = font.Name; } - if (ImGui.IsWindowAppearing() && this.Mutable.GlobalFont == font.Name) { + if (ImGui.IsWindowAppearing() && Mutable.GlobalFont == font.Name) { ImGui.SetScrollHereY(0.5f); } } ImGui.Separator(); - foreach (var name in this.GlobalFonts) { - if (ImGui.Selectable(name, this.Mutable.GlobalFont == name)) { - this.Mutable.GlobalFont = name; + foreach (var name in GlobalFonts) { + if (ImGui.Selectable(name, Mutable.GlobalFont == name)) { + Mutable.GlobalFont = name; } - if (ImGui.IsWindowAppearing() && this.Mutable.GlobalFont == name) { + if (ImGui.IsWindowAppearing() && Mutable.GlobalFont == name) { ImGui.SetScrollHereY(0.5f); } } @@ -62,25 +62,25 @@ public class Fonts : ISettingsTab { ImGuiUtil.WarningText(Language.Options_Font_Warning); ImGui.Spacing(); - if (ImGuiUtil.BeginComboVertical(Language.Options_JapaneseFont_Name, this.Mutable.JapaneseFont)) { + if (ImGuiUtil.BeginComboVertical(Language.Options_JapaneseFont_Name, Mutable.JapaneseFont)) { foreach (var (name, _) in Ui.Fonts.JapaneseFonts) { - if (ImGui.Selectable(name, this.Mutable.JapaneseFont == name)) { - this.Mutable.JapaneseFont = name; + if (ImGui.Selectable(name, Mutable.JapaneseFont == name)) { + Mutable.JapaneseFont = name; } - if (ImGui.IsWindowAppearing() && this.Mutable.JapaneseFont == name) { + if (ImGui.IsWindowAppearing() && Mutable.JapaneseFont == name) { ImGui.SetScrollHereY(0.5f); } } ImGui.Separator(); - foreach (var family in this.JpFonts) { - if (ImGui.Selectable(family, this.Mutable.JapaneseFont == family)) { - this.Mutable.JapaneseFont = family; + foreach (var family in JpFonts) { + if (ImGui.Selectable(family, Mutable.JapaneseFont == family)) { + Mutable.JapaneseFont = family; } - if (ImGui.IsWindowAppearing() && this.Mutable.JapaneseFont == family) { + if (ImGui.IsWindowAppearing() && Mutable.JapaneseFont == family) { ImGui.SetScrollHereY(0.5f); } } @@ -94,12 +94,12 @@ public class Fonts : ISettingsTab { if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name)) { ImGuiUtil.HelpText(string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName)); - var range = (int) this.Mutable.ExtraGlyphRanges; + var range = (int) Mutable.ExtraGlyphRanges; foreach (var extra in Enum.GetValues()) { ImGui.CheckboxFlags(extra.Name(), ref range, (int) extra); } - this.Mutable.ExtraGlyphRanges = (ExtraGlyphRanges) range; + Mutable.ExtraGlyphRanges = (ExtraGlyphRanges) range; } ImGui.Spacing(); @@ -108,9 +108,9 @@ public class Fonts : ISettingsTab { const float speed = .0125f; const float min = 8f; const float max = 36f; - ImGuiUtil.DragFloatVertical(Language.Options_FontSize_Name, ref this.Mutable.FontSize, speed, min, max, $"{this.Mutable.FontSize:N1}"); - ImGuiUtil.DragFloatVertical(Language.Options_JapaneseFontSize_Name, ref this.Mutable.JapaneseFontSize, speed, min, max, $"{this.Mutable.JapaneseFontSize:N1}"); - ImGuiUtil.DragFloatVertical(Language.Options_SymbolsFontSize_Name, ref this.Mutable.SymbolsFontSize, speed, min, max, $"{this.Mutable.SymbolsFontSize:N1}"); + ImGuiUtil.DragFloatVertical(Language.Options_FontSize_Name, ref Mutable.FontSize, speed, min, max, $"{Mutable.FontSize:N1}"); + ImGuiUtil.DragFloatVertical(Language.Options_JapaneseFontSize_Name, ref Mutable.JapaneseFontSize, speed, min, max, $"{Mutable.JapaneseFontSize:N1}"); + ImGuiUtil.DragFloatVertical(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSize, speed, min, max, $"{Mutable.SymbolsFontSize:N1}"); ImGuiUtil.HelpText(Language.Options_SymbolsFontSize_Description); ImGui.PopTextWrapPos(); diff --git a/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs b/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs index 5b9dceb..e03a795 100755 --- a/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs +++ b/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs @@ -10,14 +10,14 @@ internal sealed class Miscellaneous : ISettingsTab { public string Name => Language.Options_Miscellaneous_Tab + "###tabs-miscellaneous"; public Miscellaneous(Configuration mutable) { - this.Mutable = mutable; + Mutable = mutable; } public void Draw(bool changed) { - if (ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, this.Mutable.LanguageOverride.Name())) { + if (ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, Mutable.LanguageOverride.Name())) { foreach (var language in Enum.GetValues()) { if (ImGui.Selectable(language.Name())) { - this.Mutable.LanguageOverride = language; + Mutable.LanguageOverride = language; } } @@ -27,10 +27,10 @@ internal sealed class Miscellaneous : ISettingsTab { ImGuiUtil.HelpText(string.Format(Language.Options_Language_Description, Plugin.PluginName)); ImGui.Spacing(); - if (ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, this.Mutable.CommandHelpSide.Name())) { + if (ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, Mutable.CommandHelpSide.Name())) { foreach (var side in Enum.GetValues()) { - if (ImGui.Selectable(side.Name(), this.Mutable.CommandHelpSide == side)) { - this.Mutable.CommandHelpSide = side; + if (ImGui.Selectable(side.Name(), Mutable.CommandHelpSide == side)) { + Mutable.CommandHelpSide = side; } } @@ -40,10 +40,10 @@ internal sealed class Miscellaneous : ISettingsTab { ImGuiUtil.HelpText(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName)); ImGui.Spacing(); - if (ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, this.Mutable.KeybindMode.Name())) { + if (ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name())) { foreach (var mode in Enum.GetValues()) { - if (ImGui.Selectable(mode.Name(), this.Mutable.KeybindMode == mode)) { - this.Mutable.KeybindMode = mode; + if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode)) { + Mutable.KeybindMode = mode; } if (ImGui.IsItemHovered()) { @@ -59,7 +59,7 @@ internal sealed class Miscellaneous : ISettingsTab { ImGuiUtil.HelpText(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName)); ImGui.Spacing(); - ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref this.Mutable.SortAutoTranslate); + ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate); ImGuiUtil.HelpText(Language.Options_SortAutoTranslate_Description); ImGui.Spacing(); } diff --git a/ChatTwo/Ui/SettingsTabs/Tabs.cs b/ChatTwo/Ui/SettingsTabs/Tabs.cs index 711cc54..6c238a7 100755 --- a/ChatTwo/Ui/SettingsTabs/Tabs.cs +++ b/ChatTwo/Ui/SettingsTabs/Tabs.cs @@ -15,8 +15,8 @@ internal sealed class Tabs : ISettingsTab { private int _toOpen = -2; internal Tabs(Plugin plugin, Configuration mutable) { - this.Plugin = plugin; - this.Mutable = mutable; + Plugin = plugin; + Mutable = mutable; } public void Draw(bool changed) { @@ -28,29 +28,29 @@ internal sealed class Tabs : ISettingsTab { if (ImGui.BeginPopup(addTabPopup)) { if (ImGui.Selectable(Language.Options_Tabs_NewTab)) { - this.Mutable.Tabs.Add(new Tab()); + Mutable.Tabs.Add(new Tab()); } ImGui.Separator(); if (ImGui.Selectable(string.Format(Language.Options_Tabs_Preset, Language.Tabs_Presets_General))) { - this.Mutable.Tabs.Add(TabsUtil.VanillaGeneral); + Mutable.Tabs.Add(TabsUtil.VanillaGeneral); } if (ImGui.Selectable(string.Format(Language.Options_Tabs_Preset, Language.Tabs_Presets_Event))) { - this.Mutable.Tabs.Add(TabsUtil.VanillaEvent); + Mutable.Tabs.Add(TabsUtil.VanillaEvent); } ImGui.EndPopup(); } var toRemove = -1; - var doOpens = this._toOpen > -2; - for (var i = 0; i < this.Mutable.Tabs.Count; i++) { - var tab = this.Mutable.Tabs[i]; + var doOpens = _toOpen > -2; + for (var i = 0; i < Mutable.Tabs.Count; i++) { + var tab = Mutable.Tabs[i]; if (doOpens) { - ImGui.SetNextItemOpen(i == this._toOpen); + ImGui.SetNextItemOpen(i == _toOpen); } if (ImGui.TreeNodeEx($"{tab.Name}###tab-{i}")) { @@ -58,21 +58,21 @@ internal sealed class Tabs : ISettingsTab { if (ImGuiUtil.IconButton(FontAwesomeIcon.TrashAlt, tooltip: Language.Options_Tabs_Delete)) { toRemove = i; - this._toOpen = -1; + _toOpen = -1; } ImGui.SameLine(); if (ImGuiUtil.IconButton(FontAwesomeIcon.ArrowUp, tooltip: Language.Options_Tabs_MoveUp) && i > 0) { - (this.Mutable.Tabs[i - 1], this.Mutable.Tabs[i]) = (this.Mutable.Tabs[i], this.Mutable.Tabs[i - 1]); - this._toOpen = i - 1; + (Mutable.Tabs[i - 1], Mutable.Tabs[i]) = (Mutable.Tabs[i], Mutable.Tabs[i - 1]); + _toOpen = i - 1; } ImGui.SameLine(); - if (ImGuiUtil.IconButton(FontAwesomeIcon.ArrowDown, tooltip: Language.Options_Tabs_MoveDown) && i < this.Mutable.Tabs.Count - 1) { - (this.Mutable.Tabs[i + 1], this.Mutable.Tabs[i]) = (this.Mutable.Tabs[i], this.Mutable.Tabs[i + 1]); - this._toOpen = i + 1; + if (ImGuiUtil.IconButton(FontAwesomeIcon.ArrowDown, tooltip: Language.Options_Tabs_MoveDown) && i < Mutable.Tabs.Count - 1) { + (Mutable.Tabs[i + 1], Mutable.Tabs[i]) = (Mutable.Tabs[i], Mutable.Tabs[i + 1]); + _toOpen = i + 1; } ImGui.InputText(Language.Options_Tabs_Name, ref tab.Name, 512, ImGuiInputTextFlags.EnterReturnsTrue); @@ -160,7 +160,7 @@ internal sealed class Tabs : ISettingsTab { ImGui.TreePop(); } - if (this.Plugin.ExtraChat.ChannelNames.Count > 0 && ImGui.TreeNodeEx(Language.Options_Tabs_ExtraChatChannels)) { + if (Plugin.ExtraChat.ChannelNames.Count > 0 && ImGui.TreeNodeEx(Language.Options_Tabs_ExtraChatChannels)) { ImGui.Checkbox(Language.Options_Tabs_ExtraChatAll, ref tab.ExtraChatAll); ImGui.Separator(); @@ -169,7 +169,7 @@ internal sealed class Tabs : ISettingsTab { ImGui.BeginDisabled(); } - foreach (var (id, name) in this.Plugin.ExtraChat.ChannelNames) { + foreach (var (id, name) in Plugin.ExtraChat.ChannelNames) { var enabled = tab.ExtraChatChannels.Contains(id); if (!ImGui.Checkbox($"{name}##ec-{id}", ref enabled)) { continue; @@ -196,11 +196,11 @@ internal sealed class Tabs : ISettingsTab { } if (toRemove > -1) { - this.Mutable.Tabs.RemoveAt(toRemove); + Mutable.Tabs.RemoveAt(toRemove); } if (doOpens) { - this._toOpen = -2; + _toOpen = -2; } } } diff --git a/ChatTwo/Util/AutoTranslate.cs b/ChatTwo/Util/AutoTranslate.cs index 7c85e26..556521e 100644 --- a/ChatTwo/Util/AutoTranslate.cs +++ b/ChatTwo/Util/AutoTranslate.cs @@ -278,7 +278,7 @@ internal class SingleRow : ISelectorPart { public uint Row { get; } public SingleRow(uint row) { - this.Row = row; + Row = row; } } @@ -287,8 +287,8 @@ internal class IndexRange : ISelectorPart { public uint End { get; } public IndexRange(uint start, uint end) { - this.Start = start; - this.End = end; + Start = start; + End = end; } } @@ -299,7 +299,7 @@ internal class ColumnSpecifier : ISelectorPart { public uint Column { get; } public ColumnSpecifier(uint column) { - this.Column = column; + Column = column; } } @@ -310,9 +310,9 @@ internal class AutoTranslateEntry { internal SeString SeString { get; } public AutoTranslateEntry(uint group, uint row, string str, SeString seStr) { - this.Group = group; - this.Row = row; - this.String = str; - this.SeString = seStr; + Group = group; + Row = row; + String = str; + SeString = seStr; } } diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs index 45368da..3874c77 100755 --- a/ChatTwo/Util/ChunkUtil.cs +++ b/ChatTwo/Util/ChunkUtil.cs @@ -1,6 +1,7 @@ using ChatTwo.Code; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using System.Text; namespace ChatTwo.Util; @@ -68,15 +69,19 @@ internal static class ChunkUtil { break; case PayloadType.Unknown: var rawPayload = (RawPayload) payload; - if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x13) + var colorPayload = ColorPayload.From(rawPayload.Data); + if (colorPayload != null) { - if (foreground.Count > 0) { - foreground.Pop(); - } - else if (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6) + if (colorPayload.Enabled) { - var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]); - foreground.Push(ColourUtil.ComponentsToRgba(r, g, b)); + if (colorPayload.Color > 0) + foreground.Push(colorPayload.Color); + else if (foreground.Count > 0) // Push the previous color as we don't want invisible text + foreground.Push(foreground.Peek()); + } + else if (foreground.Count > 0) + { + foreground.Pop(); } } else if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x14) @@ -100,6 +105,10 @@ internal static class ChunkUtil { var reader = new BinaryReader(new MemoryStream(rawPayload.Data[4..])); var id = GetInteger(reader); link = new AchievementPayload(id); + } else if (rawPayload.Data.Length > 5 && rawPayload.Data[1] == 0x27 && rawPayload.Data[3] == 0x07) { + // uri payload + var uri = new Uri(Encoding.UTF8.GetString(rawPayload.Data[4..])); + link = new URIPayload(uri); } else if (Equals(rawPayload, RawPayload.LinkTerminator)) { link = null; } diff --git a/ChatTwo/Util/ColourUtil.cs b/ChatTwo/Util/ColourUtil.cs index 0887e4c..145afe6 100755 --- a/ChatTwo/Util/ColourUtil.cs +++ b/ChatTwo/Util/ColourUtil.cs @@ -12,8 +12,8 @@ internal static class ColourUtil { } internal static uint RgbaToAbgr(uint rgba) { - var (r, g, b, a) = RgbaToComponents(rgba); - return (uint) ((a << 24) | (b << 16) | (g << 8) | r); + var tmp = ((rgba << 8) & 0xFF00FF00) | ((rgba >> 8) & 0xFF00FF); + return (tmp << 16) | (tmp >> 16); } internal static Vector3 RgbaToVector3(uint rgba) { @@ -38,6 +38,13 @@ internal static class ColourUtil { )); } + public static unsafe uint ArgbToRgba(uint x) + { + var buf = (byte*)&x; + (buf[1], buf[2], buf[3], buf[0]) = (buf[0], buf[1], buf[2], buf[3]); + return x; + } + internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF) => alpha | (uint) (red << 24) | (uint) (green << 16) diff --git a/ChatTwo/Util/ExtraPayload.cs b/ChatTwo/Util/ExtraPayload.cs new file mode 100644 index 0000000..9e736c9 --- /dev/null +++ b/ChatTwo/Util/ExtraPayload.cs @@ -0,0 +1,109 @@ +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.Text; + +namespace ChatTwo.Util; + +public class ColorPayload +{ + private const byte START_BYTE = 2; + + public bool Enabled; + public uint Color; + public uint UnshiftedColor; + + public static ColorPayload? From(byte[] data) + { + using var stream = new MemoryStream(data); + if (stream.ReadByte() != START_BYTE || stream.ReadByte() != 0x13) + return null; + + stream.ReadByte(); // skip the length byte; + + var typeByte = stream.ReadByte(); + var payload = new ColorPayload(); + if (typeByte == 0xEC) + { + payload.Enabled = false; + return payload; + } + + if (typeByte == 0xE9) + { + var param = stream.ReadByte(); + var ok = TryGetGNumDefault((uint) (param - 2), out var value); + if (!ok) + { + Plugin.Log.Error($"Unable to GetGNum for param {param - 2}"); + return null; + } + + payload.Enabled = true; + payload.UnshiftedColor = value; + payload.Color = ColourUtil.ArgbToRgba(value); + + return payload; + } + + if (typeByte is >= 0xF0 and <= 0xFE) + { + // From: https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Text/Expressions/IntegerExpression.cs#L119-L128 + uint ShiftAndThrowIfZero(int v, int shift) + { + return v switch + { + -1 => throw new ArgumentException("Encountered premature end of input (unexpected EOF).", nameof(v)), + 0 => throw new ArgumentException("Encountered premature end of input (unexpected null character).", nameof(v)), + _ => (uint)v << shift + }; + } + + typeByte += 1; + var value = 0u; + if ((typeByte & 8) != 0) + value |= ShiftAndThrowIfZero(stream.ReadByte(), 24); + else + value |= 0xff000000u; + + if( (typeByte & 4) != 0 ) value |= ShiftAndThrowIfZero( stream.ReadByte(), 16 ); + if( (typeByte & 2) != 0 ) value |= ShiftAndThrowIfZero( stream.ReadByte(), 8 ); + if( (typeByte & 1) != 0 ) value |= ShiftAndThrowIfZero( stream.ReadByte(), 0 ); + + payload.Enabled = true; + payload.Color = ColourUtil.ArgbToRgba(value); + + return payload; + } + + return null; + } + + private static unsafe bool TryGetGNumDefault(uint parameterIndex, out uint value) + { + value = 0u; + + var rtm = RaptureTextModule.Instance(); + if (rtm is null) + return false; + + if (!ThreadSafety.IsMainThread) + { + Plugin.Log.Error("Global parameters may only be used from the main thread."); + return false; + } + + ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters; + if (parameterIndex >= gp.MySize) + return false; + + var p = rtm->TextModule.MacroDecoder.GlobalParameters.Get(parameterIndex); + switch (p.Type) + { + case TextParameterType.Integer: + value = (uint)p.IntValue; + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/ChatTwo/Util/IconUtil.cs b/ChatTwo/Util/IconUtil.cs index d4e68de..11cd426 100755 --- a/ChatTwo/Util/IconUtil.cs +++ b/ChatTwo/Util/IconUtil.cs @@ -1,95 +1,148 @@ -using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace ChatTwo.Util; -internal static class IconUtil { - internal static Vector4? GetBounds(byte id) => id switch { - 1 => new Vector4(0, 342, 40, 40), - 2 => new Vector4(40, 342, 40, 40), - 3 => new Vector4(80, 342, 40, 40), - 4 => new Vector4(120, 342, 40, 40), - 5 => new Vector4(160, 342, 40, 40), - 6 => new Vector4(0, 382, 40, 40), - 7 => new Vector4(40, 382, 40, 40), - 8 => new Vector4(80, 382, 40, 40), - 9 => new Vector4(120, 382, 40, 40), - 10 => new Vector4(160, 382, 40, 40), - 11 => new Vector4(0, 422, 40, 40), - 12 => new Vector4(40, 422, 40, 40), - 13 => new Vector4(80, 422, 40, 40), - 14 => new Vector4(120, 422, 40, 40), - 15 => new Vector4(160, 422, 40, 40), - 16 => new Vector4(120, 542, 40, 40), - 17 => new Vector4(160, 542, 40, 40), - 18 => new Vector4(0, 462, 108, 40), - 19 => new Vector4(108, 462, 108, 40), - 20 => new Vector4(120, 502, 40, 40), - 21 => new Vector4(0, 502, 56, 40), - 22 => new Vector4(56, 502, 64, 40), - 23 => new Vector4(160, 502, 40, 40), - 24 => new Vector4(0, 542, 56, 40), - 25 => new Vector4(56, 542, 64, 40), - 51 => new Vector4(248, 342, 40, 40), - 52 => new Vector4(288, 342, 40, 40), - 53 => new Vector4(328, 342, 40, 40), - 54 => new Vector4(200, 342, 24, 40), - 55 => new Vector4(224, 342, 24, 40), - 56 => new Vector4(200, 382, 40, 40), - 57 => new Vector4(240, 382, 40, 40), - 58 => new Vector4(280, 382, 40, 40), - 59 => new Vector4(200, 422, 40, 40), - 60 => new Vector4(240, 422, 40, 40), - 61 => new Vector4(280, 422, 40, 40), - 62 => new Vector4(320, 382, 40, 40), - 63 => new Vector4(320, 422, 40, 40), - 64 => new Vector4(368, 342, 40, 40), - 65 => new Vector4(408, 342, 40, 40), - 66 => new Vector4(448, 342, 40, 40), - 67 => new Vector4(360, 382, 40, 40), - 68 => new Vector4(400, 382, 40, 40), - 70 => new Vector4(360, 422, 40, 40), - 71 => new Vector4(400, 422, 40, 40), - 72 => new Vector4(440, 422, 40, 40), - 73 => new Vector4(440, 382, 40, 40), - 74 => new Vector4(216, 462, 40, 40), - 75 => new Vector4(256, 462, 40, 40), - 76 => new Vector4(296, 462, 40, 40), - 77 => new Vector4(336, 462, 40, 40), - 78 => new Vector4(376, 462, 40, 40), - 79 => new Vector4(416, 462, 40, 40), - 80 => new Vector4(456, 462, 40, 40), - 81 => new Vector4(200, 502, 40, 40), - 82 => new Vector4(240, 502, 40, 40), - 83 => new Vector4(280, 502, 40, 40), - 84 => new Vector4(320, 502, 40, 40), - 85 => new Vector4(360, 502, 40, 40), - 86 => new Vector4(400, 502, 40, 40), - 87 => new Vector4(440, 502, 40, 40), - 88 => new Vector4(200, 542, 40, 40), - 89 => new Vector4(240, 542, 40, 40), - 90 => new Vector4(280, 542, 40, 40), - 91 => new Vector4(320, 542, 40, 40), - 92 => new Vector4(360, 542, 40, 40), - 93 => new Vector4(400, 542, 40, 40), - 94 => new Vector4(440, 542, 40, 40), - 95 => new Vector4(0, 582, 40, 40), - 96 => new Vector4(40, 582, 40, 40), - 97 => new Vector4(80, 582, 40, 40), - 98 => new Vector4(120, 582, 40, 40), - 99 => new Vector4(160, 582, 40, 40), - 100 => new Vector4(200, 582, 40, 40), - 101 => new Vector4(240, 582, 40, 40), - 102 => new Vector4(280, 582, 40, 40), - 103 => new Vector4(320, 582, 40, 40), - 104 => new Vector4(360, 582, 40, 40), - 105 => new Vector4(400, 582, 40, 40), - 106 => new Vector4(440, 582, 40, 40), - 107 => new Vector4(0, 622, 40, 40), - 108 => new Vector4(40, 622, 40, 40), - 109 => new Vector4(80, 622, 40, 40), - 110 => new Vector4(120, 622, 40, 40), - 111 => new Vector4(160, 622, 40, 40), - 112 => new Vector4(200, 622, 40, 40), - _ => null, - }; +// From Kizer: https://github.com/Soreepeong/Dalamud/blob/feature/log-wordwrap/Dalamud/Interface/Spannables/Internal/GfdFileView.cs +public readonly unsafe ref struct GfdFileView +{ + private readonly ReadOnlySpan Span; + private readonly bool DirectLookup; + + /// Initializes a new instance of the struct. + /// The data. + public GfdFileView(ReadOnlySpan span) + { + Span = span; + if (span.Length < sizeof(GfdHeader)) + throw new InvalidDataException($"Not enough space for a {nameof(GfdHeader)}"); + if (span.Length < sizeof(GfdHeader) + (Header.Count * sizeof(GfdEntry))) + throw new InvalidDataException($"Not enough space for all the {nameof(GfdEntry)}"); + + var entries = Entries; + DirectLookup = true; + for (var i = 0; i < entries.Length && DirectLookup; i++) + DirectLookup &= i + 1 == entries[i].Id; + } + + /// Gets the header. + private ref readonly GfdHeader Header => ref MemoryMarshal.AsRef(Span); + + /// Gets the entries. + private ReadOnlySpan Entries => MemoryMarshal.Cast(Span[sizeof(GfdHeader)..]); + + /// Attempts to get an entry. + /// The icon ID. + /// The entry. + /// Whether to follow redirects. + /// true if found. + public bool TryGetEntry(uint iconId, out GfdEntry entry, bool followRedirect = true) + { + if (iconId == 0) + { + entry = default; + return false; + } + + var entries = Entries; + if (DirectLookup) + { + if (iconId <= entries.Length) + { + entry = entries[(int)(iconId - 1)]; + return !entry.IsEmpty; + } + + entry = default; + return false; + } + + var lo = 0; + var hi = entries.Length; + while (lo <= hi) + { + var i = lo + ((hi - lo) >> 1); + if (entries[i].Id == iconId) + { + if (followRedirect && entries[i].Redirect != 0) + { + iconId = entries[i].Redirect; + lo = 0; + hi = entries.Length; + continue; + } + + entry = entries[i]; + return !entry.IsEmpty; + } + + if (entries[i].Id < iconId) + lo = i + 1; + else + hi = i - 1; + } + + entry = default; + return false; + } + + /// Header of a .gfd file. + [StructLayout(LayoutKind.Sequential)] + public struct GfdHeader + { + /// Signature: "gftd0100". + public fixed byte Signature[8]; + + /// Number of entries. + public int Count; + + /// Unused/unknown. + public fixed byte Padding[4]; + } + + /// An entry of a .gfd file. + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct GfdEntry + { + /// ID of the entry. + public ushort Id; + + /// The left offset of the entry. + public ushort Left; + + /// The top offset of the entry. + public ushort Top; + + /// The width of the entry. + public ushort Width; + + /// The height of the entry. + public ushort Height; + + /// Unknown/unused. + public ushort Unk0A; + + /// The redirected entry, maybe. + public ushort Redirect; + + /// Unknown/unused. + public ushort Unk0E; + + /// Gets a value indicating whether this entry is effectively empty. + public bool IsEmpty => Width == 0 || Height == 0; + } +} + + + +internal static class IconUtil { + private static byte[]? GfdFile; + public static unsafe GfdFileView GfdFileView + { + get + { + GfdFile ??= Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data; + return new GfdFileView(new ReadOnlySpan(Unsafe.AsPointer(ref GfdFile[0]), GfdFile.Length)); + } + } } diff --git a/ChatTwo/Util/ImGuiUtil.cs b/ChatTwo/Util/ImGuiUtil.cs index 0075556..31c4efc 100755 --- a/ChatTwo/Util/ImGuiUtil.cs +++ b/ChatTwo/Util/ImGuiUtil.cs @@ -197,16 +197,16 @@ internal static class ImGuiUtil { } } - internal static void WarningText(string text) { + internal static void WarningText(string text, bool wrap = true) { var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent(); var dalamudOrange = style.BuiltInColors?.DalamudOrange; if (dalamudOrange != null) { ImGui.PushStyleColor(ImGuiCol.Text, dalamudOrange.Value); } - ImGui.PushTextWrapPos(); + if (wrap) ImGui.PushTextWrapPos(); ImGui.TextUnformatted(text); - ImGui.PopTextWrapPos(); + if (wrap) ImGui.PopTextWrapPos(); if (dalamudOrange != null) { ImGui.PopStyleColor(); diff --git a/ChatTwo/Util/Lender.cs b/ChatTwo/Util/Lender.cs index a833683..456f9f4 100755 --- a/ChatTwo/Util/Lender.cs +++ b/ChatTwo/Util/Lender.cs @@ -6,18 +6,18 @@ internal class Lender { private int _counter; internal Lender(Func ctor) { - this._ctor = ctor; + _ctor = ctor; } internal void ResetCounter() { - this._counter = 0; + _counter = 0; } internal T Borrow() { - if (this._items.Count <= this._counter) { - this._items.Add(this._ctor()); + if (_items.Count <= _counter) { + _items.Add(_ctor()); } - return this._items[this._counter++]; + return _items[_counter++]; } } diff --git a/ChatTwo/Util/Payloads.cs b/ChatTwo/Util/Payloads.cs index a1d0a58..92311d2 100755 --- a/ChatTwo/Util/Payloads.cs +++ b/ChatTwo/Util/Payloads.cs @@ -8,7 +8,7 @@ internal class PartyFinderPayload : Payload { internal uint Id { get; } internal PartyFinderPayload(uint id) { - this.Id = id; + Id = id; } protected override byte[] EncodeImpl() { @@ -26,7 +26,7 @@ internal class AchievementPayload : Payload { internal uint Id { get; } internal AchievementPayload(uint id) { - this.Id = id; + Id = id; } protected override byte[] EncodeImpl() { @@ -37,3 +37,51 @@ internal class AchievementPayload : Payload { throw new NotImplementedException(); } } + + +internal class URIPayload(Uri uri) : Payload +{ + public override PayloadType Type => (PayloadType) 0x52; + + public Uri Uri { get; init; } = uri; + + private static readonly string[] ExpectedSchemes = ["http", "https"]; + private static readonly string DefaultScheme = "https"; + + /// + /// Create a URIPayload from a raw URI string. If the URI does not have a + /// scheme, it will default to https://. + /// + /// + /// If the URI is invalid, or if the scheme is not supported. + /// + public static URIPayload ResolveURI(string rawURI) + { + ArgumentNullException.ThrowIfNull(rawURI); + + // Check for expected scheme ://, if not add https:// + foreach (var scheme in ExpectedSchemes) + { + if (rawURI.StartsWith($"{scheme}://")) + { + return new URIPayload(new Uri(rawURI)); + } + } + if (rawURI.Contains("://")) + { + throw new UriFormatException($"Unsupported scheme in URL: {rawURI}"); + } + + return new URIPayload(new Uri($"{DefaultScheme}://{rawURI}")); + } + + protected override void DecodeImpl(BinaryReader reader, long endOfStream) + { + throw new NotImplementedException(); + } + + protected override byte[] EncodeImpl() + { + throw new NotImplementedException(); + } +}