Fix ChatService dispose + Add chat configuration

This commit is contained in:
Loporrit
2024-11-26 01:03:58 +00:00
parent a4f78e7835
commit dc9a7f4363
10 changed files with 432 additions and 47 deletions

View File

@@ -56,4 +56,8 @@ public class MareConfig : IMareConfiguration
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
public bool DisableSyncshellChat { get; set; } = false; 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;
} }

View File

@@ -3,5 +3,8 @@ namespace MareSynchronos.MareConfiguration.Models;
[Serializable] [Serializable]
public class ShellConfig public class ShellConfig
{ {
public bool Enabled { get; set; } = true;
public int ShellNumber { get; set; } 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"
} }

View File

@@ -5,9 +5,11 @@ using Dalamud.Plugin.Services;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.Interop; using MareSynchronos.Interop;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -15,6 +17,9 @@ namespace MareSynchronos.Services;
public class ChatService : DisposableMediatorSubscriberBase public class ChatService : DisposableMediatorSubscriberBase
{ {
public const int DefaultColor = 710;
public const int CommandMaxNumber = 50;
private readonly ILogger<ChatService> _logger; private readonly ILogger<ChatService> _logger;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
@@ -45,6 +50,7 @@ public class ChatService : DisposableMediatorSubscriberBase
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing);
if (_gameChatHooks.IsValueCreated) if (_gameChatHooks.IsValueCreated)
_gameChatHooks.Value!.Dispose(); _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) private void HandleGroupChat(GroupChatMsgMessage message)
{ {
if (_mareConfig.Current.DisableSyncshellChat) if (_mareConfig.Current.DisableSyncshellChat)
return; return;
var chatMsg = message.ChatMsg; 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(); var msg = new SeStringBuilder();
// TODO: Configure colors and appearance if (extraChatTags)
msg.AddUiForeground(710); {
msg.Add(ChatUtils.CreateExtraChatTagPayload(message.GroupInfo.GID));
msg.Add(RawPayload.LinkTerminator);
}
if (color != 0)
msg.AddUiForeground((ushort)color);
msg.AddText($"[SS{shellNumber}]<"); msg.AddText($"[SS{shellNumber}]<");
// TODO: Don't link to the local player because it lets you do invalid things // 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.AddText("> ");
msg.Append(SeString.Parse(message.ChatMsg.PayloadContent)); msg.Append(SeString.Parse(message.ChatMsg.PayloadContent));
msg.AddUiForegroundOff(); if (color != 0)
msg.AddUiForegroundOff();
_chatGui.Print(new XivChatEntry{ _chatGui.Print(new XivChatEntry{
Message = msg.Build(), Message = msg.Build(),
Name = chatMsg.SenderName, 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) 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) if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
{ {
@@ -113,7 +173,8 @@ public class ChatService : DisposableMediatorSubscriberBase
foreach (var group in _pairManager.Groups) 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; 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 // 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) 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 () => { Task.Run(async () => {
// TODO: Cache the name and home world instead of fetching it every time // TODO: Cache the name and home world instead of fetching it every time

View File

@@ -19,7 +19,6 @@ public sealed class CommandManagerService : IDisposable
private const string _commandName2 = "/loporrit"; private const string _commandName2 = "/loporrit";
private const string _ssCommandPrefix = "/ss"; private const string _ssCommandPrefix = "/ss";
private const int _ssCommandMaxNumber = 50;
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly ICommandManager _commandManager; 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 // 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) _commandManager.AddHandler($"{_ssCommandPrefix}{i}", new CommandInfo(OnChatCommand)
{ {
@@ -66,7 +65,7 @@ public sealed class CommandManagerService : IDisposable
_commandManager.RemoveHandler(_commandName); _commandManager.RemoveHandler(_commandName);
_commandManager.RemoveHandler(_commandName2); _commandManager.RemoveHandler(_commandName2);
for (int i = 1; i <= _ssCommandMaxNumber; ++i) for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}"); _commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
} }

View File

@@ -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]))) .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()); .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
}); });
UiColors = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.Sheets.UIColor>(Dalamud.Game.ClientLanguage.English)!
.Where(x => x.RowId != 0 && !(x.RowId >= 500 && (x.UIForeground & 0xFFFFFF00) == 0))
.ToDictionary(x => (int)x.RowId);
});
mediator.Subscribe<TargetPairMessage>(this, async (msg) => mediator.Subscribe<TargetPairMessage>(this, async (msg) =>
{ {
if (clientState.IsPvP) return; if (clientState.IsPvP) return;
@@ -99,6 +105,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsInCombat { get; private set; } = false; public bool IsInCombat { get; private set; } = false;
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; } public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
public MareMediator Mediator { get; } public MareMediator Mediator { get; }

View File

@@ -1,6 +1,7 @@
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics; using System.Diagnostics;
@@ -246,16 +247,27 @@ public class ServerConfigurationManager
return CurrentServerTagStorage().ServerAvailablePairTags; return CurrentServerTagStorage().ServerAvailablePairTags;
} }
internal int GetShellNumberForGid(string gid) internal ShellConfig GetShellConfigForGid(string gid)
{ {
if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config)) if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config))
{ return config;
return config.ShellNumber;
}
int newNumber = CurrentSyncshellStorage().GidShellConfig.Count > 0 ? CurrentSyncshellStorage().GidShellConfig.Select(x => x.Value.ShellNumber).Max() + 1 : 1; // Pick the next higher syncshell number that is available
SetShellNumberForGid(gid, newNumber, false); int newShellNumber = CurrentSyncshellStorage().GidShellConfig.Count > 0 ? CurrentSyncshellStorage().GidShellConfig.Select(x => x.Value.ShellNumber).Max() + 1 : 1;
return newNumber;
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<string, List<string>> GetUidServerPairedUserTags() internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
@@ -359,23 +371,14 @@ public class ServerConfigurationManager
_notesConfig.Save(); _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 (string.IsNullOrEmpty(gid)) return;
if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config)) // This is somewhat pointless because ShellConfig is a ref type we returned to the caller anyway...
{ CurrentSyncshellStorage().GidShellConfig[gid] = config;
config.ShellNumber = number;
}
else
{
CurrentSyncshellStorage().GidShellConfig.Add(gid, new(){
ShellNumber = number
});
}
if (save) _syncshellConfig.Save();
_syncshellConfig.Save();
} }
private ServerNotesStorage CurrentNotesStorage() private ServerNotesStorage CurrentNotesStorage()

View File

@@ -239,7 +239,8 @@ internal sealed class GroupPanel
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal)) 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}]"); ImGui.TextUnformatted($"[{shellNumber}]");
UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber); UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber);

View File

@@ -1,4 +1,5 @@
using Dalamud.Interface; using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
@@ -38,11 +39,13 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly MareConfigService _configService; private readonly MareConfigService _configService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new(); private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly FileCompactor _fileCompactor; private readonly FileCompactor _fileCompactor;
private readonly DalamudUtilService _dalamudUtilService;
private readonly FileUploadManager _fileTransferManager; private readonly FileUploadManager _fileTransferManager;
private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly MareCharaFileManager _mareCharaFileManager; private readonly MareCharaFileManager _mareCharaFileManager;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly ChatService _chatService;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
@@ -67,9 +70,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
public SettingsUi(ILogger<SettingsUi> logger, public SettingsUi(ILogger<SettingsUi> logger,
UiSharedService uiShared, MareConfigService configService, UiSharedService uiShared, MareConfigService configService,
MareCharaFileManager mareCharaFileManager, PairManager pairManager, MareCharaFileManager mareCharaFileManager, PairManager pairManager, ChatService chatService,
ServerConfigurationManager serverConfigurationManager, ServerConfigurationManager serverConfigurationManager,
MareMediator mediator, PerformanceCollectorService performanceCollector, MareMediator mediator, PerformanceCollectorService performanceCollector,
DalamudUtilService dalamudUtilService,
FileUploadManager fileTransferManager, FileUploadManager fileTransferManager,
FileTransferOrchestrator fileTransferOrchestrator, FileTransferOrchestrator fileTransferOrchestrator,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
@@ -78,8 +82,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService = configService; _configService = configService;
_mareCharaFileManager = mareCharaFileManager; _mareCharaFileManager = mareCharaFileManager;
_pairManager = pairManager; _pairManager = pairManager;
_chatService = chatService;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_performanceCollector = performanceCollector; _performanceCollector = performanceCollector;
_dalamudUtilService = dalamudUtilService;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_fileTransferOrchestrator = fileTransferOrchestrator; _fileTransferOrchestrator = fileTransferOrchestrator;
_fileCacheManager = fileCacheManager; _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() private void DrawDebug()
{ {
_lastTab = "Debug"; _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."); 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(); ImGui.Separator();
UiSharedService.FontText("UI", _uiShared.UidFont); UiSharedService.FontText("UI", _uiShared.UidFont);
var showCharacterNames = _configService.Current.ShowCharacterNames; var showCharacterNames = _configService.Current.ShowCharacterNames;
@@ -1230,6 +1451,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndDisabled(); // _registrationInProgress ImGui.EndDisabled(); // _registrationInProgress
} }
if (ImGui.BeginTabItem("Chat"))
{
DrawChatConfig();
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Debug")) if (ImGui.BeginTabItem("Debug"))
{ {
DrawDebug(); DrawDebug();

View File

@@ -688,6 +688,51 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return (T)_selectedComboItems[comboName]; return (T)_selectedComboItems[comboName];
} }
public T? DrawColorCombo<T>(string comboName, IEnumerable<T> comboItems, Func<T, (uint Color, string Name)> toEntry,
Action<T>? onSelected = null, T? initialSelectedItem = default)
{
if (!comboItems.Any()) return default;
if (!_selectedComboItems.TryGetValue(comboName, out var selectedItem) && selectedItem == null)
{
if (!EqualityComparer<T>.Default.Equals(initialSelectedItem, default))
{
selectedItem = initialSelectedItem;
_selectedComboItems[comboName] = selectedItem!;
if (!EqualityComparer<T>.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<T>.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() public void DrawFileScanState()
{ {
ImGui.TextUnformatted("File Scanner Status"); ImGui.TextUnformatted("File Scanner Status");

View File

@@ -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);
}
}