Basic syncshell chat impl with game hooks

This commit is contained in:
Loporrit
2024-11-24 22:21:41 +00:00
parent 7075c43a49
commit c2723fd005
25 changed files with 610 additions and 13 deletions

View File

@@ -0,0 +1,205 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using FFXIVClientStructs.FFXIV.Component.Shell;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Interop;
public class ChatChannelOverride
{
public string ChannelName = string.Empty;
public Action<byte[]>? ChatMessageHandler;
}
public unsafe sealed class GameChatHooks : IDisposable
{
// Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/GameFunctions.cs
private readonly ILogger<GameChatHooks> _logger;
#region signatures
// I do not know what kind of black magic this function performs
[Signature("E8 ?? ?? ?? ?? 0F B7 7F 08 48 8B CE")]
private readonly delegate* unmanaged<PronounModule*, Utf8String*, byte, Utf8String*> ProcessStringStep2;
// Component::Shell::ShellCommandModule::ExecuteCommandInner
private delegate void SendMessageDelegate(ShellCommandModule* module, Utf8String* message, UIModule* uiModule);
[Signature(
"E8 ?? ?? ?? ?? FE 86 ?? ?? ?? ?? C7 86",
DetourName = nameof(SendMessageDetour)
)]
private Hook<SendMessageDelegate>? SendMessageHook { get; init; }
// Client::UI::Shell::RaptureShellModule::SetChatChannel
private delegate void SetChatChannelDelegate(RaptureShellModule* module, uint channel);
[Signature(
"E8 ?? ?? ?? ?? 33 C0 EB 1D",
DetourName = nameof(SetChatChannelDetour)
)]
private Hook<SetChatChannelDelegate>? SetChatChannelHook { get; init; }
// Component::Shell::ShellCommandModule::ExecuteCommandInner
private delegate byte* ChangeChannelNameDelegate(AgentChatLog* agent);
[Signature(
"E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6",
DetourName = nameof(ChangeChannelNameDetour)
)]
private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; init; }
// Client::UI::Agent::AgentChatLog_???
private delegate byte ShouldDoNameLookupDelegate(AgentChatLog* agent);
[Signature(
"48 89 5C 24 ?? 57 48 83 EC 20 48 8B D9 40 32 FF 48 8B 49 10",
DetourName = nameof(ShouldDoNameLookupDetour)
)]
private Hook<ShouldDoNameLookupDelegate>? ShouldDoNameLookupHook { get; init; }
#endregion
private ChatChannelOverride? _chatChannelOverride;
private bool _shouldForceNameLookup = false;
public ChatChannelOverride? ChatChannelOverride
{
get => _chatChannelOverride;
set {
_chatChannelOverride = value;
this._shouldForceNameLookup = true;
}
}
public GameChatHooks(ILogger<GameChatHooks> logger, IGameInteropProvider gameInteropProvider)
{
_logger = logger;
logger.LogInformation("Initializing GameChatHooks");
gameInteropProvider.InitializeFromAttributes(this);
this.SendMessageHook?.Enable();
this.SetChatChannelHook?.Enable();
this.ChangeChannelNameHook?.Enable();
this.ShouldDoNameLookupHook?.Enable();
}
public void Dispose()
{
this.SendMessageHook?.Dispose();
this.SetChatChannelHook?.Dispose();
this.ChangeChannelNameHook?.Dispose();
this.ShouldDoNameLookupHook?.Dispose();
}
private void SendMessageDetour(ShellCommandModule* thisPtr, Utf8String* message, UIModule* uiModule)
{
try
{
var messageLength = message->Length;
var messageSpan = message->AsSpan();
bool isCommand = false;
// Check if chat input begins with a command (or auto-translated command)
if (messageLength == 0 || messageSpan[0] == (byte)'/' || !messageSpan.ContainsAnyExcept((byte)' '))
{
isCommand = true;
}
else if (messageSpan[0] == (byte)0x02) /* Payload.START_BYTE */
{
var payload = Payload.Decode(new BinaryReader(new UnmanagedMemoryStream(message->StringPtr, message->BufSize))) as AutoTranslatePayload;
// Auto-translate text begins with /
if (payload != null && payload.Text.Length > 2 && payload.Text[2] == '/')
isCommand = true;
}
// If not a command, or no override is set, then call the original chat handler
if (isCommand || this._chatChannelOverride == null)
{
SendMessageHook!.OriginalDisposeSafe(thisPtr, message, uiModule);
return;
}
// Otherwise, the text is to be sent to the emulated chat channel handler
// The chat input string is rendered in to a payload for display first
var pronounModule = UIModule.Instance()->GetPronounModule();
var chatString1 = pronounModule->ProcessString(message, true);
var chatString2 = this.ProcessStringStep2(pronounModule, chatString1, 1);
var chatBytes = MemoryHelper.ReadRaw((nint)chatString2->StringPtr, chatString2->Length);
if (chatBytes.Length > 0)
this._chatChannelOverride.ChatMessageHandler?.Invoke(chatBytes);
}
catch (Exception e)
{
_logger.LogError(e, "Exception thrown during SendMessageDetour");
}
}
private void SetChatChannelDetour(RaptureShellModule* module, uint channel)
{
try
{
if (this._chatChannelOverride != null)
{
this._chatChannelOverride = null;
this._shouldForceNameLookup = true;
}
}
catch (Exception e)
{
_logger.LogError(e, "Exception thrown during SetChatChannelDetour");
}
SetChatChannelHook!.OriginalDisposeSafe(module, channel);
}
private byte* ChangeChannelNameDetour(AgentChatLog* agent)
{
var originalResult = ChangeChannelNameHook!.OriginalDisposeSafe(agent);
try
{
// Replace the chat channel name on the UI if active
if (this._chatChannelOverride != null)
{
agent->ChannelLabel.SetString(this._chatChannelOverride.ChannelName);
}
}
catch (Exception e)
{
_logger.LogError(e, "Exception thrown during ChangeChannelNameDetour");
}
return originalResult;
}
private byte ShouldDoNameLookupDetour(AgentChatLog* agent)
{
var originalResult = ShouldDoNameLookupHook!.OriginalDisposeSafe(agent);
try
{
// Force the chat channel name to update when required
if (this._shouldForceNameLookup)
{
_shouldForceNameLookup = false;
return 1;
}
}
catch (Exception e)
{
_logger.LogError(e, "Exception thrown during ShouldDoNameLookupDetour");
}
return originalResult;
}
}

View File

@@ -54,4 +54,6 @@ public class MareConfig : IMareConfiguration
public bool UseCompactor { get; set; } = false;
public int Version { get; set; } = 1;
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
public bool DisableSyncshellChat { get; set; } = false;
}

View File

@@ -2,6 +2,7 @@
namespace MareSynchronos.MareConfiguration.Configurations;
[Serializable]
public class ServerTagConfig : IMareConfiguration
{
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);

View File

@@ -0,0 +1,10 @@
using MareSynchronos.MareConfiguration.Models;
namespace MareSynchronos.MareConfiguration.Configurations;
[Serializable]
public class SyncshellConfig : IMareConfiguration
{
public Dictionary<string, ServerShellStorage> ServerShellStorage { get; set; } = new(StringComparer.Ordinal);
public int Version { get; set; } = 0;
}

View File

@@ -2,6 +2,7 @@
namespace MareSynchronos.MareConfiguration.Configurations;
[Serializable]
public class UidNotesConfig : IMareConfiguration
{
public Dictionary<string, ServerNotesStorage> ServerNotes { get; set; } = new(StringComparer.Ordinal);

View File

@@ -1,5 +1,6 @@
namespace MareSynchronos.MareConfiguration.Models;
[Serializable]
public class ServerNotesStorage
{
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);

View File

@@ -0,0 +1,7 @@
namespace MareSynchronos.MareConfiguration.Models;
[Serializable]
public class ServerShellStorage
{
public Dictionary<string, ShellConfig> GidShellConfig { get; set; } = new(StringComparer.Ordinal);
}

View File

@@ -0,0 +1,7 @@
namespace MareSynchronos.MareConfiguration.Models;
[Serializable]
public class ShellConfig
{
public int ShellNumber { get; set; }
}

View File

@@ -0,0 +1,14 @@
using MareSynchronos.MareConfiguration.Configurations;
namespace MareSynchronos.MareConfiguration;
public class SyncshellConfigService : ConfigurationServiceBase<SyncshellConfig>
{
public const string ConfigName = "syncshells.json";
public SyncshellConfigService(string configDir) : base(configDir)
{
}
protected override string ConfigurationName => ConfigName;
}

View File

@@ -147,6 +147,7 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
#if !DEBUG
if (_mareConfigService.Current.LogLevel != LogLevel.Information)

View File

@@ -1,4 +1,5 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;

View File

@@ -46,7 +46,7 @@ public sealed class Plugin : IDalamudPlugin
public Plugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IDataManager gameData,
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
IGameGui gameGui, IDtrBar dtrBar, IToastGui toastGui, IPluginLog pluginLog, ITargetManager targetManager, IGameLifecycle addonLifecycle,
INotificationManager notificationManager, ITextureProvider textureProvider, IContextMenu contextMenu)
INotificationManager notificationManager, ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider)
{
Plugin.Self = this;
_hostBuilderRunTask = new HostBuilder()
@@ -103,6 +103,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new SyncshellConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ConfigurationMigrator(s.GetRequiredService<ILogger<ConfigurationMigrator>>(), pluginInterface, s.GetRequiredService<NotesConfigService>()));
collection.AddSingleton<HubFactory>();
@@ -133,13 +134,17 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<UiFactory>(),
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareMediator>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<MareMediator>(), s.GetRequiredService<MareConfigService>()));
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<ChatService>(),
s.GetRequiredService<ApiController>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<MareConfigService>()));
collection.AddScoped((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<MareMediator>(), notificationManager, chatGui, s.GetRequiredService<MareConfigService>()));
collection.AddScoped((s) => new UiSharedService(s.GetRequiredService<ILogger<UiSharedService>>(), s.GetRequiredService<IpcManager>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareConfigService>(), s.GetRequiredService<DalamudUtilService>(),
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<MareMediator>()));
collection.AddScoped((s) => new ChatService(s.GetRequiredService<ILogger<ChatService>>(), s.GetRequiredService<DalamudUtilService>(),
s.GetRequiredService<MareMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<PairManager>(),
s.GetRequiredService<ILogger<GameChatHooks>>(), gameInteropProvider, chatGui,
s.GetRequiredService<MareConfigService>(), s.GetRequiredService<ServerConfigurationManager>()));
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());

View File

@@ -0,0 +1,23 @@
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class ChatIntegrationService : MediatorSubscriberBase, IDisposable
{
public bool Activated { get; private set; } = false;
public ChatIntegrationService(ILogger<ChatIntegrationService> logger, MareMediator mediator) : base(logger, mediator)
{
}
public void Activate()
{
if (Activated)
return;
}
public void Dispose()
{
}
}

View File

@@ -0,0 +1,157 @@
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.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public class ChatService : DisposableMediatorSubscriberBase
{
private readonly ILogger<ChatService> _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> _gameChatHooks;
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
PairManager pairManager, ILogger<GameChatHooks> logger2, 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<UserChatMsgMessage>(this, HandleUserChat);
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
_gameChatHooks = new(() => new GameChatHooks(logger2, gameInteropProvider));
}
protected override void Dispose(bool disposing)
{
if (_gameChatHooks.IsValueCreated)
_gameChatHooks.Value!.Dispose();
}
private void HandleUserChat(UserChatMsgMessage message)
{
var chatMsg = message.ChatMsg;
var prefix = new SeStringBuilder();
prefix.AddText("[BnnuyChat] ");
_chatGui.Print(new XivChatEntry{
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
Name = chatMsg.SenderName,
Type = XivChatType.TellIncoming
});
}
private void HandleGroupChat(GroupChatMsgMessage message)
{
if (_mareConfig.Current.DisableSyncshellChat)
return;
var chatMsg = message.ChatMsg;
var shellNumber = _serverConfigurationManager.GetShellNumberForGid(message.GroupInfo.GID);
var prefix = new SeStringBuilder();
// TODO: Configure colors and appearance
prefix.AddUiForeground(710);
prefix.AddText($"[SS{shellNumber}]<");
// TODO: Don't link to the local player because it lets you do invalid things
prefix.Add(new PlayerPayload(chatMsg.SenderName, (uint)chatMsg.SenderHomeWorldId));
prefix.AddText("> ");
_chatGui.Print(new XivChatEntry{
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
Name = chatMsg.SenderName,
Type = XivChatType.Debug
});
}
// 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)
{
if (_serverConfigurationManager.GetShellNumberForGid(group.Key.GID) == 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}]"))
SwitchChatShell(shellNumber);
}
}
}
}
public void SwitchChatShell(int shellNumber)
{
if (_mareConfig.Current.DisableSyncshellChat)
return;
foreach (var group in _pairManager.Groups)
{
if (_serverConfigurationManager.GetShellNumberForGid(group.Key.GID) == 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($"[LoporritSync] Syncshell number #{shellNumber} not found");
}
public void SendChatShell(int shellNumber, byte[] chatBytes)
{
if (_mareConfig.Current.DisableSyncshellChat)
return;
foreach (var group in _pairManager.Groups)
{
if (_serverConfigurationManager.GetShellNumberForGid(group.Key.GID) == shellNumber)
{
Task.Run(async () => {
// TODO: 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
};
});
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg);
});
return;
}
}
_chatGui.PrintError($"[LoporritSync] Syncshell number #{shellNumber} not found");
}
}

View File

@@ -1,4 +1,5 @@
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
using MareSynchronos.FileCache;
@@ -8,6 +9,7 @@ using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI;
using System.Globalization;
using System.Text;
namespace MareSynchronos.Services;
@@ -16,22 +18,27 @@ public sealed class CommandManagerService : IDisposable
private const string _commandName = "/sync";
private const string _commandName2 = "/loporrit";
private const string _ssCommandPrefix = "/ss";
private const int _ssCommandMaxNumber = 50;
private readonly ApiController _apiController;
private readonly ICommandManager _commandManager;
private readonly MareMediator _mediator;
private readonly MareConfigService _mareConfigService;
private readonly PerformanceCollectorService _performanceCollectorService;
private readonly PeriodicFileScanner _periodicFileScanner;
private readonly ChatService _chatService;
private readonly ServerConfigurationManager _serverConfigurationManager;
public CommandManagerService(ICommandManager commandManager, PerformanceCollectorService performanceCollectorService,
ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner,
ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, ChatService chatService,
ApiController apiController, MareMediator mediator, MareConfigService mareConfigService)
{
_commandManager = commandManager;
_performanceCollectorService = performanceCollectorService;
_serverConfigurationManager = serverConfigurationManager;
_periodicFileScanner = periodicFileScanner;
_chatService = chatService;
_apiController = apiController;
_mediator = mediator;
_mareConfigService = mareConfigService;
@@ -43,12 +50,24 @@ public sealed class CommandManagerService : IDisposable
{
HelpMessage = "Opens the Loporrit UI"
});
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
for (int i = 1; i <= _ssCommandMaxNumber; ++i)
{
_commandManager.AddHandler($"{_ssCommandPrefix}{i}", new CommandInfo(OnChatCommand)
{
ShowInHelp = false
});
}
}
public void Dispose()
{
_commandManager.RemoveHandler(_commandName);
_commandManager.RemoveHandler(_commandName2);
for (int i = 1; i <= _ssCommandMaxNumber; ++i)
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
}
private void OnCommand(string command, string args)
@@ -116,4 +135,23 @@ public sealed class CommandManagerService : IDisposable
_mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
}
private void OnChatCommand(string command, string args)
{
if (_mareConfigService.Current.DisableSyncshellChat)
return;
int shellNumber = int.Parse(command[_ssCommandPrefix.Length..]);
if (args.Length == 0)
{
_chatService.SwitchChatShell(shellNumber);
}
else
{
// FIXME: Chat content seems to already be stripped of any special characters here?
byte[] chatBytes = Encoding.UTF8.GetBytes(args);
_chatService.SendChatShell(shellNumber, chatBytes);
}
}
}

View File

@@ -79,5 +79,7 @@ public record TargetPairMessage(Pair Pair) : MessageBase;
public record CombatStartMessage : MessageBase;
public record CombatEndMessage : MessageBase;
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
#pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name

View File

@@ -15,14 +15,16 @@ public class ServerConfigurationManager
private readonly MareMediator _mareMediator;
private readonly NotesConfigService _notesConfig;
private readonly ServerTagConfigService _serverTagConfig;
private readonly SyncshellConfigService _syncshellConfig;
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil,
MareMediator mareMediator)
ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig,
DalamudUtilService dalamudUtil, MareMediator mareMediator)
{
_logger = logger;
_configService = configService;
_serverTagConfig = serverTagConfig;
_syncshellConfig = syncshellConfig;
_notesConfig = notesConfig;
_dalamudUtil = dalamudUtil;
_mareMediator = mareMediator;
@@ -244,6 +246,18 @@ public class ServerConfigurationManager
return CurrentServerTagStorage().ServerAvailablePairTags;
}
internal int GetShellNumberForGid(string gid)
{
if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config))
{
return config.ShellNumber;
}
int newNumber = CurrentSyncshellStorage().GidShellConfig.Count > 0 ? CurrentSyncshellStorage().GidShellConfig.Select(x => x.Value.ShellNumber).Max() + 1 : 1;
SetShellNumberForGid(gid, newNumber, false);
return newNumber;
}
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
{
return CurrentServerTagStorage().UidServerPairedUserTags;
@@ -345,6 +359,25 @@ public class ServerConfigurationManager
_notesConfig.Save();
}
internal void SetShellNumberForGid(string gid, int number, bool save = true)
{
if (string.IsNullOrEmpty(gid)) return;
if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config))
{
config.ShellNumber = number;
}
else
{
CurrentSyncshellStorage().GidShellConfig.Add(gid, new(){
ShellNumber = number
});
}
if (save)
_syncshellConfig.Save();
}
private ServerNotesStorage CurrentNotesStorage()
{
TryCreateCurrentNotesStorage();
@@ -357,6 +390,12 @@ public class ServerConfigurationManager
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private ServerShellStorage CurrentSyncshellStorage()
{
TryCreateCurrentSyncshellStorage();
return _syncshellConfig.Current.ServerShellStorage[CurrentApiUrl];
}
private void EnsureMainExists()
{
bool lopExists = false;
@@ -406,4 +445,12 @@ public class ServerConfigurationManager
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
private void TryCreateCurrentSyncshellStorage()
{
if (!_syncshellConfig.Current.ServerShellStorage.ContainsKey(CurrentApiUrl))
{
_syncshellConfig.Current.ServerShellStorage[CurrentApiUrl] = new();
}
}
}

View File

@@ -15,6 +15,7 @@ using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components;
@@ -38,6 +39,7 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly GroupPanel _groupPanel;
private readonly PairGroupsUi _pairGroupsUi;
private readonly PairManager _pairManager;
private readonly ChatService _chatService;
private readonly SelectGroupForPairUi _selectGroupForPairUi;
private readonly SelectPairForGroupUi _selectPairsForGroupUi;
private readonly ServerConfigurationManager _serverManager;
@@ -56,19 +58,20 @@ public class CompactUi : WindowMediatorSubscriberBase
private bool _showSyncShells;
private bool _wasOpen;
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager,
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler) : base(logger, mediator, "###LoporritSyncMainUI")
{
_uiShared = uiShared;
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
_chatService = chatService;
_serverManager = serverManager;
_fileTransferManager = fileTransferManager;
_uidDisplayHandler = uidDisplayHandler;
var tagHandler = new TagHandler(_serverManager);
_groupPanel = new(this, uiShared, _pairManager, uidDisplayHandler, _serverManager);
_groupPanel = new(this, uiShared, _pairManager, _chatService, uidDisplayHandler, _configService, _serverManager);
_selectGroupForPairUi = new(tagHandler, uidDisplayHandler);
_selectPairsForGroupUi = new(tagHandler, uidDisplayHandler);
_pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi);

View File

@@ -17,6 +17,9 @@ using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using MareSynchronos.Services;
using MareSynchronos.MareConfiguration;
namespace MareSynchronos.UI;
@@ -25,6 +28,8 @@ internal sealed class GroupPanel
private readonly Dictionary<string, bool> _expandedGroupState = new(StringComparer.Ordinal);
private readonly CompactUi _mainUi;
private readonly PairManager _pairManager;
private readonly ChatService _chatService;
private readonly MareConfigService _mareConfig;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal);
private readonly UidDisplayHandler _uidDisplayHandler;
@@ -50,12 +55,15 @@ internal sealed class GroupPanel
private string _syncShellPassword = string.Empty;
private string _syncShellToJoin = string.Empty;
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, UidDisplayHandler uidDisplayHandler, ServerConfigurationManager serverConfigurationManager)
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
{
_mainUi = mainUi;
_uiShared = uiShared;
_pairManager = pairManager;
_chatService = chatServivce;
_uidDisplayHandler = uidDisplayHandler;
_mareConfig = mareConfig;
_serverConfigurationManager = serverConfigurationManager;
}
@@ -185,6 +193,8 @@ internal sealed class GroupPanel
private void DrawSyncshell(GroupFullInfoDto groupDto, List<Pair> pairsInGroup)
{
int shellNumber = _serverConfigurationManager.GetShellNumberForGid(groupDto.GID);
var name = groupDto.Group.Alias ?? groupDto.GID;
if (!_expandedGroupState.TryGetValue(groupDto.GID, out bool isExpanded))
{
@@ -230,7 +240,13 @@ internal sealed class GroupPanel
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
{
if (!_mareConfig.Current.DisableSyncshellChat)
{
ImGui.TextUnformatted($"[{shellNumber}]");
UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber);
}
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
ImGui.SameLine();
ImGui.TextUnformatted(groupName);
if (textIsGid) ImGui.PopFont();
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
@@ -252,6 +268,7 @@ internal sealed class GroupPanel
_serverConfigurationManager.SetNoteForGid(_editGroupEntry, _editGroupComment);
_editGroupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID) ?? string.Empty;
_editGroupEntry = groupDto.GID;
_chatService.MaybeUpdateShellName(shellNumber);
}
}
else
@@ -262,6 +279,7 @@ internal sealed class GroupPanel
{
_serverConfigurationManager.SetNoteForGid(groupDto.GID, _editGroupComment);
_editGroupEntry = string.Empty;
_chatService.MaybeUpdateShellName(shellNumber);
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))

View File

@@ -651,6 +651,18 @@ 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;
@@ -664,7 +676,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
var enableDtrEntry = _configService.Current.EnableDtrEntry;
var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip;
var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip;
var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible;
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
{

View File

@@ -33,6 +33,12 @@ public partial class ApiController
await _mareHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
}
public async Task UserChatSendMsg(UserDto user, ChatMessage message)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(UserChatSendMsg), user, message).ConfigureAwait(false);
}
public async Task UserDelete()
{
CheckConnection();

View File

@@ -1,6 +1,7 @@
using Dalamud.Interface.ImGuiNotification;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.Chat;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Services.Mediator;
@@ -25,6 +26,13 @@ public partial class ApiController
return Task.CompletedTask;
}
public Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto)
{
Logger.LogDebug("Client_GroupChatMsg: {msg}", groupChatMsgDto.Message);
Mediator.Publish(new GroupChatMsgMessage(groupChatMsgDto.Group, groupChatMsgDto.Message));
return Task.CompletedTask;
}
public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto dto)
{
Logger.LogTrace("Client_GroupPairChangePermissions: {dto}", dto);
@@ -120,6 +128,13 @@ public partial class ApiController
return Task.CompletedTask;
}
public Task Client_UserChatMsg(UserChatMsgDto chatMsgDto)
{
Logger.LogDebug("Client_UserChatMsg: {msg}", chatMsgDto.Message);
Mediator.Publish(new UserChatMsgMessage(chatMsgDto.Message));
return Task.CompletedTask;
}
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
{
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
@@ -188,6 +203,12 @@ public partial class ApiController
_mareHub!.On(nameof(Client_GroupChangePermissions), act);
}
public void OnGroupChatMsg(Action<GroupChatMsgDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_GroupChatMsg), act);
}
public void OnGroupPairChangePermissions(Action<GroupPairUserPermissionDto> act)
{
if (_initialized) return;
@@ -248,6 +269,12 @@ public partial class ApiController
_mareHub!.On(nameof(Client_UserAddClientPair), act);
}
public void OnUserChatMsg(Action<UserChatMsgDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserChatMsg), act);
}
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
{
if (_initialized) return;

View File

@@ -1,4 +1,5 @@
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.WebAPI.SignalR.Utils;
using Microsoft.AspNetCore.SignalR.Client;
@@ -36,6 +37,12 @@ public partial class ApiController
return await _mareHub!.InvokeAsync<bool>(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false);
}
public async Task GroupChatSendMsg(GroupDto group, ChatMessage message)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(GroupChatSendMsg), group, message).ConfigureAwait(false);
}
public async Task GroupClear(GroupDto group)
{
CheckConnection();

View File

@@ -323,6 +323,9 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IM
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
OnGroupPairChangePermissions((dto) => _ = Client_GroupPairChangePermissions(dto));
OnUserChatMsg((dto) => _ = Client_UserChatMsg(dto));
OnGroupChatMsg((dto) => _ = Client_GroupChatMsg(dto));
_healthCheckTokenSource?.Cancel();
_healthCheckTokenSource?.Dispose();
_healthCheckTokenSource = new CancellationTokenSource();