using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; 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; 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; private readonly MareConfigService _mareConfig; private readonly ApiController _apiController; private readonly PairManager _pairManager; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly Lazy _gameChatHooks; public ChatService(ILogger logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController, PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator) { _logger = logger; _dalamudUtil = dalamudUtil; _chatGui = chatGui; _mareConfig = mareConfig; _apiController = apiController; _pairManager = pairManager; _serverConfigurationManager = serverConfigurationManager; Mediator.Subscribe(this, HandleUserChat); Mediator.Subscribe(this, HandleGroupChat); _gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger(), gameInteropProvider, SendChatShell)); // Initialize chat hooks in advance _ = Task.Run(() => { try { _ = _gameChatHooks.Value; } catch (Exception ex) { _logger.LogError(ex, "Failed to initialize chat hooks"); } }); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (_gameChatHooks.IsValueCreated) _gameChatHooks.Value!.Dispose(); } private void HandleUserChat(UserChatMsgMessage message) { var chatMsg = message.ChatMsg; var prefix = new SeStringBuilder(); prefix.AddText("[SnowChat] "); _chatGui.Print(new XivChatEntry{ MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent], Name = chatMsg.SenderName, Type = XivChatType.TellIncoming }); } 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 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(); if (extraChatTags) { msg.Add(ChatUtils.CreateExtraChatTagPayload(message.GroupInfo.GID)); msg.Add(RawPayload.LinkTerminator); } if (color != 0) msg.AddUiForeground((ushort)color); msg.AddText($"[SS{shellNumber}]<"); if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal)) { // Don't link to your own character msg.AddText(chatMsg.SenderName); } else { msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId)); } msg.AddText("> "); msg.Append(SeString.Parse(message.ChatMsg.PayloadContent)); if (color != 0) msg.AddUiForegroundOff(); _chatGui.Print(new XivChatEntry{ Message = msg.Build(), Name = chatMsg.SenderName, 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.Equals(gid, StringComparison.Ordinal)) { int shellChatType = _serverConfigurationManager.GetShellConfigForGid(gid).LogKind; if (shellChatType != 0) chatType = shellChatType; } } _chatGui.Print(new XivChatEntry{ Message = message, Name = "", Type = (XivChatType)chatType }); } // Called to update the active chat shell name if its renamed public void MaybeUpdateShellName(int shellNumber) { if (_mareConfig.Current.DisableSyncshellChat) return; foreach (var group in _pairManager.Groups) { var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null) { // Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal)) SwitchChatShell(shellNumber); } } } } public void SwitchChatShell(int shellNumber) { if (_mareConfig.Current.DisableSyncshellChat) return; foreach (var group in _pairManager.Groups) { 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 _gameChatHooks.Value.ChatChannelOverride = new() { ChannelName = $"SS [{shellNumber}]: {name}", ChatMessageHandler = chatBytes => SendChatShell(shellNumber, chatBytes) }; return; } } _chatGui.PrintError($"[SnowcloakSync] Syncshell number #{shellNumber} not found"); } public void SendChatShell(int shellNumber, byte[] chatBytes) { if (_mareConfig.Current.DisableSyncshellChat) return; foreach (var group in _pairManager.Groups) { var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) { _ = Task.Run(async () => { // Should cache the name and home world instead of fetching it every time var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => { return new ChatMessage() { SenderName = _dalamudUtil.GetPlayerName(), SenderHomeWorldId = _dalamudUtil.GetHomeWorldId(), PayloadContent = chatBytes }; }).ConfigureAwait(false); await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false); }).ConfigureAwait(false); return; } } _chatGui.PrintError($"[SnowcloakSync] Syncshell number #{shellNumber} not found"); } }