diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs index 35fe22d..45aa675 100644 --- a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -56,4 +56,8 @@ public class MareConfig : IMareConfiguration public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; public bool DisableSyncshellChat { get; set; } = false; + public int ChatColor { get; set; } = 0; // 0 means "use plugin default" + public int ChatLogKind { get; set; } = 1; // XivChatType.Debug + public bool ExtraChatAPI { get; set; } = false; + public bool ExtraChatTags { get; set; } = false; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ShellConfig.cs b/MareSynchronos/MareConfiguration/Models/ShellConfig.cs index 05c8f37..54eb1e1 100644 --- a/MareSynchronos/MareConfiguration/Models/ShellConfig.cs +++ b/MareSynchronos/MareConfiguration/Models/ShellConfig.cs @@ -3,5 +3,8 @@ namespace MareSynchronos.MareConfiguration.Models; [Serializable] public class ShellConfig { + public bool Enabled { get; set; } = true; public int ShellNumber { get; set; } + public int Color { get; set; } = 0; // 0 means "default to the global setting" + public int LogKind { get; set; } = 0; // 0 means "default to the global setting" } \ No newline at end of file diff --git a/MareSynchronos/Services/ChatService.cs b/MareSynchronos/Services/ChatService.cs index 9d39fb3..b5660eb 100644 --- a/MareSynchronos/Services/ChatService.cs +++ b/MareSynchronos/Services/ChatService.cs @@ -5,9 +5,11 @@ using Dalamud.Plugin.Services; using MareSynchronos.API.Data; using MareSynchronos.Interop; using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; using MareSynchronos.PlayerData.Pairs; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; @@ -15,6 +17,9 @@ namespace MareSynchronos.Services; public class ChatService : DisposableMediatorSubscriberBase { + public const int DefaultColor = 710; + public const int CommandMaxNumber = 50; + private readonly ILogger _logger; private readonly IChatGui _chatGui; private readonly DalamudUtilService _dalamudUtil; @@ -45,6 +50,7 @@ public class ChatService : DisposableMediatorSubscriberBase protected override void Dispose(bool disposing) { + base.Dispose(disposing); if (_gameChatHooks.IsValueCreated) _gameChatHooks.Value!.Dispose(); } @@ -61,28 +67,81 @@ public class ChatService : DisposableMediatorSubscriberBase }); } + private ushort ResolveShellColor(int shellColor) + { + if (shellColor != 0) + return (ushort)shellColor; + var globalColor = _mareConfig.Current.ChatColor; + if (globalColor != 0) + return (ushort)globalColor; + return (ushort)DefaultColor; + } + + private XivChatType ResolveShellLogKind(int shellLogKind) + { + if (shellLogKind != 0) + return (XivChatType)shellLogKind; + return (XivChatType)_mareConfig.Current.ChatLogKind; + } + private void HandleGroupChat(GroupChatMsgMessage message) { if (_mareConfig.Current.DisableSyncshellChat) return; var chatMsg = message.ChatMsg; - var shellNumber = _serverConfigurationManager.GetShellNumberForGid(message.GroupInfo.GID); + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(message.GroupInfo.GID); + var shellNumber = shellConfig.ShellNumber; + + if (!shellConfig.Enabled) + return; + + ushort color = ResolveShellColor(shellConfig.Color); + var extraChatTags = _mareConfig.Current.ExtraChatTags; + var logKind = ResolveShellLogKind(shellConfig.LogKind); var msg = new SeStringBuilder(); - // TODO: Configure colors and appearance - msg.AddUiForeground(710); + if (extraChatTags) + { + msg.Add(ChatUtils.CreateExtraChatTagPayload(message.GroupInfo.GID)); + msg.Add(RawPayload.LinkTerminator); + } + if (color != 0) + msg.AddUiForeground((ushort)color); msg.AddText($"[SS{shellNumber}]<"); // TODO: Don't link to the local player because it lets you do invalid things - msg.Add(new PlayerPayload(chatMsg.SenderName, (uint)chatMsg.SenderHomeWorldId)); + msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId)); msg.AddText("> "); msg.Append(SeString.Parse(message.ChatMsg.PayloadContent)); - msg.AddUiForegroundOff(); + if (color != 0) + msg.AddUiForegroundOff(); _chatGui.Print(new XivChatEntry{ Message = msg.Build(), Name = chatMsg.SenderName, - Type = XivChatType.StandardEmote + Type = logKind + }); + } + + // Print an example message to the configured global chat channel + public void PrintChannelExample(string message, string gid = "") + { + int chatType = _mareConfig.Current.ChatLogKind; + + foreach (var group in _pairManager.Groups) + { + if (group.Key.GID == gid) + { + int shellChatType = _serverConfigurationManager.GetShellConfigForGid(gid).LogKind; + if (shellChatType != 0) + chatType = shellChatType; + } + } + + _chatGui.Print(new XivChatEntry{ + Message = message, + Name = "", + Type = (XivChatType)chatType }); } @@ -94,7 +153,8 @@ public class ChatService : DisposableMediatorSubscriberBase foreach (var group in _pairManager.Groups) { - if (_serverConfigurationManager.GetShellNumberForGid(group.Key.GID) == shellNumber) + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); + if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null) { @@ -113,7 +173,8 @@ public class ChatService : DisposableMediatorSubscriberBase foreach (var group in _pairManager.Groups) { - if (_serverConfigurationManager.GetShellNumberForGid(group.Key.GID) == shellNumber) + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); + if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID; // BUG: This doesn't always update the chat window e.g. when renaming a group @@ -136,7 +197,8 @@ public class ChatService : DisposableMediatorSubscriberBase foreach (var group in _pairManager.Groups) { - if (_serverConfigurationManager.GetShellNumberForGid(group.Key.GID) == shellNumber) + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); + if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { Task.Run(async () => { // TODO: Cache the name and home world instead of fetching it every time diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs index 7de2cec..3004330 100644 --- a/MareSynchronos/Services/CommandManagerService.cs +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -19,7 +19,6 @@ public sealed class CommandManagerService : IDisposable private const string _commandName2 = "/loporrit"; private const string _ssCommandPrefix = "/ss"; - private const int _ssCommandMaxNumber = 50; private readonly ApiController _apiController; private readonly ICommandManager _commandManager; @@ -52,7 +51,7 @@ public sealed class CommandManagerService : IDisposable }); // Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway - for (int i = 1; i <= _ssCommandMaxNumber; ++i) + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) { _commandManager.AddHandler($"{_ssCommandPrefix}{i}", new CommandInfo(OnChatCommand) { @@ -66,7 +65,7 @@ public sealed class CommandManagerService : IDisposable _commandManager.RemoveHandler(_commandName); _commandManager.RemoveHandler(_commandName2); - for (int i = 1; i <= _ssCommandMaxNumber; ++i) + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) _commandManager.RemoveHandler($"{_ssCommandPrefix}{i}"); } diff --git a/MareSynchronos/Services/DalamudUtilService.cs b/MareSynchronos/Services/DalamudUtilService.cs index 0039fc2..d6682b2 100644 --- a/MareSynchronos/Services/DalamudUtilService.cs +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -70,6 +70,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber .Where(w => w.Name.ByteLength > 0 && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper((char)w.Name.Data.Span[0]))) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); + UiColors = new(() => + { + return gameData.GetExcelSheet(Dalamud.Game.ClientLanguage.English)! + .Where(x => x.RowId != 0 && !(x.RowId >= 500 && (x.UIForeground & 0xFFFFFF00) == 0)) + .ToDictionary(x => (int)x.RowId); + }); mediator.Subscribe(this, async (msg) => { if (clientState.IsPvP) return; @@ -99,6 +105,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsInCombat { get; private set; } = false; public Lazy> WorldData { get; private set; } + public Lazy> UiColors { get; private set; } public MareMediator Mediator { get; } diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs index 7993581..64dba72 100644 --- a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -1,6 +1,7 @@ using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Microsoft.Extensions.Logging; using System.Diagnostics; @@ -246,16 +247,27 @@ public class ServerConfigurationManager return CurrentServerTagStorage().ServerAvailablePairTags; } - internal int GetShellNumberForGid(string gid) + internal ShellConfig GetShellConfigForGid(string gid) { if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config)) - { - return config.ShellNumber; - } + return config; - int newNumber = CurrentSyncshellStorage().GidShellConfig.Count > 0 ? CurrentSyncshellStorage().GidShellConfig.Select(x => x.Value.ShellNumber).Max() + 1 : 1; - SetShellNumberForGid(gid, newNumber, false); - return newNumber; + // Pick the next higher syncshell number that is available + int newShellNumber = CurrentSyncshellStorage().GidShellConfig.Count > 0 ? CurrentSyncshellStorage().GidShellConfig.Select(x => x.Value.ShellNumber).Max() + 1 : 1; + + var shellConfig = new ShellConfig{ + ShellNumber = newShellNumber + }; + + // Save config to avoid auto-generated numbers shuffling around + SaveShellConfigForGid(gid, shellConfig); + + return CurrentSyncshellStorage().GidShellConfig[gid]; + } + + internal int GetShellNumberForGid(string gid) + { + return GetShellConfigForGid(gid).ShellNumber; } internal Dictionary> GetUidServerPairedUserTags() @@ -359,23 +371,14 @@ public class ServerConfigurationManager _notesConfig.Save(); } - internal void SetShellNumberForGid(string gid, int number, bool save = true) + internal void SaveShellConfigForGid(string gid, ShellConfig config) { if (string.IsNullOrEmpty(gid)) return; - if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config)) - { - config.ShellNumber = number; - } - else - { - CurrentSyncshellStorage().GidShellConfig.Add(gid, new(){ - ShellNumber = number - }); - } + // This is somewhat pointless because ShellConfig is a ref type we returned to the caller anyway... + CurrentSyncshellStorage().GidShellConfig[gid] = config; - if (save) - _syncshellConfig.Save(); + _syncshellConfig.Save(); } private ServerNotesStorage CurrentNotesStorage() diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs index 3a8a33b..a75282c 100644 --- a/MareSynchronos/UI/Components/GroupPanel.cs +++ b/MareSynchronos/UI/Components/GroupPanel.cs @@ -239,7 +239,8 @@ internal sealed class GroupPanel if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal)) { - if (!_mareConfig.Current.DisableSyncshellChat) + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID); + if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled) { ImGui.TextUnformatted($"[{shellNumber}]"); UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber); diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 9d18a27..5cf59a1 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -1,4 +1,5 @@ -using Dalamud.Interface; +using Dalamud.Game.Text; +using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -38,11 +39,13 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly MareConfigService _configService; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly FileCompactor _fileCompactor; + private readonly DalamudUtilService _dalamudUtilService; private readonly FileUploadManager _fileTransferManager; private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileCacheManager _fileCacheManager; private readonly MareCharaFileManager _mareCharaFileManager; private readonly PairManager _pairManager; + private readonly ChatService _chatService; private readonly PerformanceCollectorService _performanceCollector; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly UiSharedService _uiShared; @@ -67,9 +70,10 @@ public class SettingsUi : WindowMediatorSubscriberBase public SettingsUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, - MareCharaFileManager mareCharaFileManager, PairManager pairManager, + MareCharaFileManager mareCharaFileManager, PairManager pairManager, ChatService chatService, ServerConfigurationManager serverConfigurationManager, MareMediator mediator, PerformanceCollectorService performanceCollector, + DalamudUtilService dalamudUtilService, FileUploadManager fileTransferManager, FileTransferOrchestrator fileTransferOrchestrator, FileCacheManager fileCacheManager, @@ -78,8 +82,10 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService = configService; _mareCharaFileManager = mareCharaFileManager; _pairManager = pairManager; + _chatService = chatService; _serverConfigurationManager = serverConfigurationManager; _performanceCollector = performanceCollector; + _dalamudUtilService = dalamudUtilService; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; _fileCacheManager = fileCacheManager; @@ -369,6 +375,233 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private static readonly List<(XivChatType, string)> _syncshellChatTypes = [ + (0, "(use global setting)"), + (XivChatType.Debug, "Debug"), + (XivChatType.Echo, "Echo"), + (XivChatType.StandardEmote, "Standard Emote"), + (XivChatType.CustomEmote, "Custom Emote"), + (XivChatType.SystemMessage, "System Message"), + (XivChatType.SystemError, "System Error"), + (XivChatType.GatheringSystemMessage, "Gathering Message"), + (XivChatType.ErrorMessage, "Error message"), + ]; + + private void DrawChatConfig() + { + _lastTab = "Chat"; + + UiSharedService.FontText("Chat Settings", _uiShared.UidFont); + + var disableSyncshellChat = _configService.Current.DisableSyncshellChat; + + if (ImGui.Checkbox("Disable chat globally", ref disableSyncshellChat)) + { + _configService.Current.DisableSyncshellChat = disableSyncshellChat; + _configService.Save(); + } + UiSharedService.DrawHelpText("Global setting to disable chat for all syncshells."); + + using var pushDisableGlobal = ImRaii.Disabled(disableSyncshellChat); + + var uiColors = _dalamudUtilService.UiColors.Value; + int globalChatColor = _configService.Current.ChatColor; + + if (globalChatColor != 0 && !uiColors.ContainsKey(globalChatColor)) + { + globalChatColor = 0; + _configService.Current.ChatColor = 0; + _configService.Save(); + } + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawColorCombo("Chat text color", Enumerable.Concat([0], uiColors.Keys), + i => i switch + { + 0 => (uiColors[ChatService.DefaultColor].UIForeground, "Plugin Default"), + _ => (uiColors[i].UIForeground, $"[{i}] Sample Text") + }, + i => { + _configService.Current.ChatColor = i; + _configService.Save(); + }, globalChatColor); + + int globalChatType = _configService.Current.ChatLogKind; + int globalChatTypeIdx = _syncshellChatTypes.FindIndex(x => globalChatType == (int)x.Item1); + + if (globalChatTypeIdx == -1) + globalChatTypeIdx = 0; + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("Chat channel", Enumerable.Range(1, _syncshellChatTypes.Count - 1), i => $"{_syncshellChatTypes[i].Item2}", + i => { + if (_configService.Current.ChatLogKind == (int)_syncshellChatTypes[i].Item1) + return; + _configService.Current.ChatLogKind = (int)_syncshellChatTypes[i].Item1; + _chatService.PrintChannelExample($"Selected channel: {_syncshellChatTypes[i].Item2}"); + _configService.Save(); + }, globalChatTypeIdx); + UiSharedService.DrawHelpText("FFXIV chat channel to output chat messages on."); + + ImGui.SetWindowFontScale(0.6f); + UiSharedService.FontText("\"Chat 2\" Plugin Integration", _uiShared.UidFont); + ImGui.SetWindowFontScale(1.0f); + + // TODO: ExtraChat API impersonation + /* + var extraChatAPI = _configService.Current.ExtraChatAPI; + if (ImGui.Checkbox("ExtraChat replacement mode", ref extraChatAPI)) + { + _configService.Current.ExtraChatAPI = extraChatAPI; + if (extraChatAPI) + _configService.Current.ExtraChatTags = true; + _configService.Save(); + } + ImGui.EndDisabled(); + UiSharedService.DrawHelpText("Enable ExtraChat APIs for full Chat 2 plugin integration.\n\nDo not enable this if ExtraChat is also installed and running."); + */ + + var extraChatTags = _configService.Current.ExtraChatTags; + if (ImGui.Checkbox("Tag messages as ExtraChat", ref extraChatTags)) + { + _configService.Current.ExtraChatTags = extraChatTags; + if (!extraChatTags) + _configService.Current.ExtraChatAPI = false; + _configService.Save(); + } + UiSharedService.DrawHelpText("If enabled, messages will be filtered under the category \"ExtraChat channels: All\".\n\nThis works even if ExtraChat is also installed and enabled."); + + ImGui.Separator(); + + UiSharedService.FontText("Syncshell Settings", _uiShared.UidFont); + + if (!ApiController.ServerAlive) + { + ImGui.Text("Connect to the server to configure individual syncshell settings."); + return; + } + + foreach (var group in _pairManager.Groups.OrderBy(k => k.Key.GID)) + { + var gid = group.Key.GID; + using var pushId = ImRaii.PushId(gid); + + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(gid); + var shellNumber = shellConfig.ShellNumber; + var shellEnabled = shellConfig.Enabled; + var shellName = _serverConfigurationManager.GetNoteForGid(gid) ?? group.Key.AliasOrGID; + + if (shellEnabled) + shellName = $"[{shellNumber}] {shellName}"; + + ImGui.SetWindowFontScale(0.6f); + UiSharedService.FontText(shellName, _uiShared.UidFont); + ImGui.SetWindowFontScale(1.0f); + + using var pushIndent = ImRaii.PushIndent(); + + if (ImGui.Checkbox($"Enable chat for this syncshell##{gid}", ref shellEnabled)) + { + // If there is an active group with the same syncshell number, pick a new one + int nextNumber = 1; + bool conflict = false; + foreach (var otherGroup in _pairManager.Groups) + { + if (gid == otherGroup.Key.GID) continue; + var otherShellConfig = _serverConfigurationManager.GetShellConfigForGid(otherGroup.Key.GID); + if (otherShellConfig.Enabled && otherShellConfig.ShellNumber == shellNumber) + conflict = true; + nextNumber = System.Math.Max(nextNumber, otherShellConfig.ShellNumber) + 1; + } + if (conflict) + shellConfig.ShellNumber = nextNumber; + shellConfig.Enabled = shellEnabled; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + } + + using var pushDisabled = ImRaii.Disabled(!shellEnabled); + + ImGui.SetNextItemWidth(50 * ImGuiHelpers.GlobalScale); + + var setSyncshellNumberFn = (int i) => { + // Find an active group with the same syncshell number as selected, and swap it + // This logic can leave duplicate IDs present in the config but its not critical + foreach (var otherGroup in _pairManager.Groups) + { + if (gid == otherGroup.Key.GID) continue; + var otherShellConfig = _serverConfigurationManager.GetShellConfigForGid(otherGroup.Key.GID); + if (otherShellConfig.Enabled && otherShellConfig.ShellNumber == i) + { + otherShellConfig.ShellNumber = shellNumber; + _serverConfigurationManager.SaveShellConfigForGid(otherGroup.Key.GID, otherShellConfig); + break; + } + } + shellConfig.ShellNumber = i; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + }; + + // _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change + if (ImGui.BeginCombo("Syncshell number##{gid}", $"{shellNumber}")) + { + // Same hard-coded number in CommandManagerService + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) + { + if (ImGui.Selectable($"{i}", i == shellNumber)) + { + // Find an active group with the same syncshell number as selected, and swap it + // This logic can leave duplicate IDs present in the config but its not critical + foreach (var otherGroup in _pairManager.Groups) + { + if (gid == otherGroup.Key.GID) continue; + var otherShellConfig = _serverConfigurationManager.GetShellConfigForGid(otherGroup.Key.GID); + if (otherShellConfig.Enabled && otherShellConfig.ShellNumber == i) + { + otherShellConfig.ShellNumber = shellNumber; + _serverConfigurationManager.SaveShellConfigForGid(otherGroup.Key.GID, otherShellConfig); + break; + } + } + shellConfig.ShellNumber = i; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + } + } + ImGui.EndCombo(); + } + + if (shellConfig.Color != 0 && !uiColors.ContainsKey(shellConfig.Color)) + { + shellConfig.Color = 0; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + } + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawColorCombo($"Chat text color##{gid}", Enumerable.Concat([0], uiColors.Keys), + i => i switch + { + 0 => (uiColors[globalChatColor > 0 ? globalChatColor : ChatService.DefaultColor].UIForeground, "(use global setting)"), + _ => (uiColors[i].UIForeground, $"[{i}] Sample Text") + }, + i => { + shellConfig.Color = i; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + }, shellConfig.Color); + + int shellChatTypeIdx = _syncshellChatTypes.FindIndex(x => shellConfig.LogKind == (int)x.Item1); + + if (shellChatTypeIdx == -1) + shellChatTypeIdx = 0; + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo($"Chat channel##{gid}", Enumerable.Range(0, _syncshellChatTypes.Count), i => $"{_syncshellChatTypes[i].Item2}", + i => { + shellConfig.LogKind = (int)_syncshellChatTypes[i].Item1; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + }, shellChatTypeIdx); + UiSharedService.DrawHelpText("Override the FFXIV chat channel used for this syncshell."); + } + } + private void DrawDebug() { _lastTab = "Debug"; @@ -651,18 +884,6 @@ public class SettingsUi : WindowMediatorSubscriberBase } UiSharedService.DrawHelpText("This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); - ImGui.Separator(); - UiSharedService.FontText("Chat", _uiShared.UidFont); - - var disableSyncshellChat = _configService.Current.DisableSyncshellChat; - - if (ImGui.Checkbox("Disable Syncshell Chat", ref disableSyncshellChat)) - { - _configService.Current.DisableSyncshellChat = disableSyncshellChat; - _configService.Save(); - } - UiSharedService.DrawHelpText("Disable sending/receiving all syncshell chat messages."); - ImGui.Separator(); UiSharedService.FontText("UI", _uiShared.UidFont); var showCharacterNames = _configService.Current.ShowCharacterNames; @@ -1230,6 +1451,12 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndDisabled(); // _registrationInProgress } + if (ImGui.BeginTabItem("Chat")) + { + DrawChatConfig(); + ImGui.EndTabItem(); + } + if (ImGui.BeginTabItem("Debug")) { DrawDebug(); diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index 0cc0eae..528094a 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -688,6 +688,51 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return (T)_selectedComboItems[comboName]; } + public T? DrawColorCombo(string comboName, IEnumerable comboItems, Func toEntry, + Action? onSelected = null, T? initialSelectedItem = default) + { + if (!comboItems.Any()) return default; + + if (!_selectedComboItems.TryGetValue(comboName, out var selectedItem) && selectedItem == null) + { + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + { + selectedItem = initialSelectedItem; + _selectedComboItems[comboName] = selectedItem!; + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + onSelected?.Invoke(initialSelectedItem); + } + else + { + selectedItem = comboItems.First(); + _selectedComboItems[comboName] = selectedItem!; + } + } + + var entry = toEntry((T)selectedItem!); + ImGui.PushStyleColor(ImGuiCol.Text, ColorHelpers.RgbaUintToVector4(ColorHelpers.SwapEndianness(entry.Color))); + if (ImGui.BeginCombo(comboName, entry.Name)) + { + foreach (var item in comboItems) + { + entry = toEntry(item); + ImGui.PushStyleColor(ImGuiCol.Text, ColorHelpers.RgbaUintToVector4(ColorHelpers.SwapEndianness(entry.Color))); + bool isSelected = EqualityComparer.Default.Equals(item, (T)selectedItem); + if (ImGui.Selectable(entry.Name, isSelected)) + { + _selectedComboItems[comboName] = item!; + onSelected?.Invoke(item!); + } + ImGui.PopStyleColor(); + } + + ImGui.EndCombo(); + } + ImGui.PopStyleColor(); + + return (T)_selectedComboItems[comboName]; + } + public void DrawFileScanState() { ImGui.TextUnformatted("File Scanner Status"); diff --git a/MareSynchronos/Utils/ChatUtils.cs b/MareSynchronos/Utils/ChatUtils.cs new file mode 100644 index 0000000..40a1b8e --- /dev/null +++ b/MareSynchronos/Utils/ChatUtils.cs @@ -0,0 +1,34 @@ +using Dalamud.Game.Text.SeStringHandling.Payloads; +using System.Security.Cryptography; +using System.Text; + +namespace MareSynchronos.Utils; + +public static class ChatUtils +{ + // Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/Util/PayloadUtil.cs + // This must store a Guid (16 bytes), as Chat 2 converts the data back to one + + public static RawPayload CreateExtraChatTagPayload(Guid guid) + { + var header = (byte[])[ + 0x02, // Payload.START_BYTE + 0x27, // SeStringChunkType.Interactable + 2 + 16, // remaining length: ExtraChat sends 19 here but I think its an error + 0x20 // Custom ExtraChat InfoType + ]; + + var footer = (byte)0x03; // Payload.END_BYTE + + return new RawPayload([..header, ..guid.ToByteArray(), footer]); + } + + // We have a unique identifier in the form of a GID, which can be consistently mapped to the same GUID + public static RawPayload CreateExtraChatTagPayload(string gid) + { + var gidBytes = UTF8Encoding.UTF8.GetBytes(gid); + var hashedBytes = MD5.HashData(gidBytes); + var guid = new Guid(hashedBytes); + return CreateExtraChatTagPayload(guid); + } +}