[Draft] Update 0.8 (#46)

* move stuff out into file transfer manager

* obnoxious unsupported version text, adjustments to filetransfermanager

* add back file upload transfer progress

* restructure code

* cleanup some more stuff I guess

* downloadids by playername

* individual anim/sound bs

* fix migration stuff, finalize impl of individual sound/anim pause

* fixes with logging stuff

* move download manager to transient

* rework dl ui first iteration

* some refactoring and cleanup

* more code cleanup

* refactoring

* switch to hostbuilder

* some more rework I guess

* more refactoring

* clean up mediator calls and disposal

* fun code cleanup

* push error message when log level is set to anything but information in non-debug builds

* remove notificationservice

* move message to after login

* add download bars to gameworld

* fixes download progress bar

* set gpose ui min and max size

* remove unnecessary usings

* adjustments to reconnection logic

* add options to set visible/offline groups visibility

* add impl of uploading display, transfer list in settings ui

* attempt to fix issues with server selection

* add back download status to compact ui

* make dl bar fixed size based

* some fixes for upload/download handling

* adjust text from Syncing back to Uploading

---------

Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
Co-authored-by: Stanley Dimant <stanley.dimant@varian.com>
This commit is contained in:
rootdarkarchon
2023-03-14 19:48:35 +01:00
committed by GitHub
parent 0824ba434b
commit 0c87e84f25
109 changed files with 7323 additions and 6488 deletions

View File

@@ -0,0 +1,97 @@
using Dalamud.Game.Command;
using MareSynchronos.FileCache;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI;
using MareSynchronos.WebAPI;
namespace MareSynchronos.Services;
public sealed class CommandManagerService : IDisposable
{
private const string _commandName = "/mare";
private readonly ApiController _apiController;
private readonly CommandManager _commandManager;
private readonly MareMediator _mediator;
private readonly PerformanceCollectorService _performanceCollectorService;
private readonly PeriodicFileScanner _periodicFileScanner;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiService _uiService;
public CommandManagerService(CommandManager commandManager, PerformanceCollectorService performanceCollectorService,
UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner,
ApiController apiController, MareMediator mediator)
{
_commandManager = commandManager;
_performanceCollectorService = performanceCollectorService;
_uiService = uiService;
_serverConfigurationManager = serverConfigurationManager;
_periodicFileScanner = periodicFileScanner;
_apiController = apiController;
_mediator = mediator;
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
{
HelpMessage = "Opens the Mare Synchronos UI"
});
}
public void Dispose()
{
_commandManager.RemoveHandler(_commandName);
}
private void OnCommand(string command, string args)
{
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
if (splitArgs == null || splitArgs.Length == 0)
{
// Interpret this as toggling the UI
_uiService.ToggleUi();
return;
}
if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase))
{
if (_serverConfigurationManager.CurrentServer == null) return;
var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch
{
"on" => false,
"off" => true,
_ => !_serverConfigurationManager.CurrentServer.FullPause,
} : !_serverConfigurationManager.CurrentServer.FullPause;
if (fullPause != _serverConfigurationManager.CurrentServer.FullPause)
{
_serverConfigurationManager.CurrentServer.FullPause = fullPause;
_serverConfigurationManager.Save();
_ = _apiController.CreateConnections();
}
}
else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase))
{
_mediator.Publish(new UiToggleMessage(typeof(GposeUi)));
}
else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase))
{
_periodicFileScanner.InvokeScan(forced: true);
}
else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase))
{
if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], out var limitBySeconds))
{
_performanceCollectorService.PrintPerformanceStats(limitBySeconds);
}
else
{
_performanceCollectorService.PrintPerformanceStats();
}
}
else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase))
{
_mediator.PrintSubscriberInfo();
}
}
}

View File

@@ -0,0 +1,337 @@
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Numerics;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace MareSynchronos.Services;
public class DalamudUtilService : IHostedService
{
private readonly ClientState _clientState;
private readonly Condition _condition;
private readonly Framework _framework;
private readonly GameGui _gameGui;
private readonly ILogger<DalamudUtilService> _logger;
private readonly MareMediator _mediator;
private readonly ObjectTable _objectTable;
private readonly PerformanceCollectorService _performanceCollector;
private uint? _classJobId = 0;
private DateTime _delayedFrameworkUpdateCheck = DateTime.Now;
private bool _sentBetweenAreas = false;
public DalamudUtilService(ILogger<DalamudUtilService> logger, ClientState clientState, ObjectTable objectTable, Framework framework,
GameGui gameGui, Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollectorService performanceCollector)
{
_logger = logger;
_clientState = clientState;
_objectTable = objectTable;
_framework = framework;
_gameGui = gameGui;
_condition = condition;
_mediator = mediator;
_performanceCollector = performanceCollector;
WorldData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.World>(Dalamud.ClientLanguage.English)!
.Where(w => w.IsPublic && !w.Name.RawData.IsEmpty)
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
});
}
public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget;
public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex];
public bool IsInCutscene { get; private set; } = false;
public bool IsInGpose { get; private set; } = false;
public bool IsLoggedIn { get; private set; }
public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid();
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!;
public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--";
public string PlayerNameHashed => (PlayerName + _clientState.LocalPlayer!.HomeWorld.Id).GetHash256();
public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public uint WorldId => _clientState.LocalPlayer!.HomeWorld.Id;
public static bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
{
return obj != null && obj.IsValid();
}
public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference)
{
return _objectTable.CreateObjectReference(reference);
}
public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index)
{
var objTableObj = _objectTable[index];
if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null;
return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj;
}
public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null)
{
var mgr = CharacterManager.Instance();
playerPointer ??= PlayerPointer;
return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer);
}
public int? GetIndexFromObjectTableByName(string characterName)
{
for (int i = 0; i < _objectTable.Length; i++)
{
if (_objectTable[i] == null) continue;
if (_objectTable[i]!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (string.Equals(_objectTable[i]!.Name.ToString(), characterName, StringComparison.Ordinal)) return i;
}
return null;
}
public unsafe IntPtr GetMinion(IntPtr? playerPointer = null)
{
playerPointer ??= PlayerPointer;
return (IntPtr)((Character*)playerPointer)->CompanionObject;
}
public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null)
{
playerPointer ??= PlayerPointer;
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1);
}
public unsafe IntPtr GetPet(IntPtr? playerPointer = null)
{
var mgr = CharacterManager.Instance();
playerPointer ??= PlayerPointer;
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
}
public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName)
{
foreach (var item in _objectTable)
{
if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (PlayerCharacter)item;
}
return null;
}
public List<PlayerCharacter> GetPlayerCharacters()
{
return _objectTable.Where(obj =>
obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player &&
!string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Cast<PlayerCharacter>().ToList();
}
public unsafe bool IsGameObjectPresent(IntPtr key)
{
foreach (var obj in _objectTable)
{
if (obj.Address == key)
{
return true;
}
}
return false;
}
public async Task RunOnFrameworkThread(Action act)
{
_logger.LogTrace("Running Action on framework thread: {act}", act);
await _framework.RunOnFrameworkThread(act).ConfigureAwait(false);
}
public async Task<T> RunOnFrameworkThread<T>(Func<T> func)
{
_logger.LogTrace("Running Func on framework thread: {func}", func);
return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false);
}
public Task StartAsync(CancellationToken cancellationToken)
{
_framework.Update += FrameworkOnUpdate;
if (IsLoggedIn)
{
_classJobId = _clientState.LocalPlayer!.ClassJob.Id;
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("Stopping {type}", GetType());
_framework.Update -= FrameworkOnUpdate;
return Task.CompletedTask;
}
public Vector2 WorldToScreen(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
{
if (obj == null) return Vector2.Zero;
return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero;
}
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
{
if (!_clientState.IsLoggedIn || handler.Address == IntPtr.Zero) return;
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
const int tick = 250;
int curWaitTime = 0;
try
{
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while ((!ct?.IsCancellationRequested ?? true)
&& curWaitTime < timeOut
&& await handler.IsBeingDrawnRunOnFramework().ConfigureAwait(true)) // 0b100000000000 is "still rendering" or something
{
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
curWaitTime += tick;
await Task.Delay(tick).ConfigureAwait(true);
}
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
}
catch (NullReferenceException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
catch (AccessViolationException ex)
{
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
}
}
public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000)
{
Thread.Sleep(500);
var obj = (GameObject*)characterAddress;
const int tick = 250;
int curWaitTime = 0;
_logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X"));
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while (obj->RenderFlags != 0x00 && curWaitTime < timeOut)
{
_logger.LogTrace($"Waiting for gpose actor to finish drawing");
curWaitTime += tick;
Thread.Sleep(tick);
}
Thread.Sleep(tick * 2);
}
private void FrameworkOnUpdate(Framework framework)
{
_performanceCollector.LogPerformance(this, "FrameworkOnUpdate", FrameworkOnUpdateInternal);
}
private unsafe void FrameworkOnUpdateInternal()
{
if (GposeTarget != null && !IsInGpose)
{
_logger.LogDebug("Gpose start");
IsInGpose = true;
_mediator.Publish(new GposeStartMessage());
}
else if (GposeTarget == null && IsInGpose)
{
_logger.LogDebug("Gpose end");
IsInGpose = false;
_mediator.Publish(new GposeEndMessage());
}
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene)
{
_logger.LogDebug("Cutscene start");
IsInCutscene = true;
_mediator.Publish(new CutsceneStartMessage());
_mediator.Publish(new HaltScanMessage("Cutscene"));
}
else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene)
{
_logger.LogDebug("Cutscene end");
IsInCutscene = false;
_mediator.Publish(new CutsceneEndMessage());
_mediator.Publish(new ResumeScanMessage("Cutscene"));
}
if (IsInCutscene) { _mediator.Publish(new CutsceneFrameworkUpdateMessage()); return; }
if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51])
{
if (!_sentBetweenAreas)
{
_logger.LogDebug("Zone switch/Gpose start");
_sentBetweenAreas = true;
_mediator.Publish(new ZoneSwitchStartMessage());
_mediator.Publish(new HaltScanMessage("Zone switch"));
}
return;
}
if (_sentBetweenAreas)
{
_logger.LogDebug("Zone switch/Gpose end");
_sentBetweenAreas = false;
_mediator.Publish(new ZoneSwitchEndMessage());
_mediator.Publish(new ResumeScanMessage("Zone switch"));
}
_mediator.Publish(new FrameworkUpdateMessage());
if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return;
var localPlayer = _clientState.LocalPlayer;
if (localPlayer != null && !IsLoggedIn)
{
_logger.LogDebug("Logged in");
IsLoggedIn = true;
_mediator.Publish(new DalamudLoginMessage());
}
else if (localPlayer == null && IsLoggedIn)
{
_logger.LogDebug("Logged out");
IsLoggedIn = false;
_mediator.Publish(new DalamudLogoutMessage());
}
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid())
{
var newclassJobId = _clientState.LocalPlayer.ClassJob.Id;
if (_classJobId != newclassJobId)
{
_classJobId = newclassJobId;
_mediator.Publish(new ClassJobChangedMessage());
}
}
_mediator.Publish(new DelayedFrameworkUpdateMessage());
_delayedFrameworkUpdateCheck = DateTime.Now;
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, IDisposable
{
protected DisposableMediatorSubscriberBase(ILogger logger, MareMediator mediator) : base(logger, mediator)
{
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
Logger.LogTrace("Disposing {type} ({this})", GetType().Name, this);
UnsubscribeAll();
}
}

View File

@@ -0,0 +1,6 @@
namespace MareSynchronos.Services.Mediator;
public interface IMediatorSubscriber
{
MareMediator Mediator { get; }
}

View File

@@ -0,0 +1,3 @@
namespace MareSynchronos.Services.Mediator;
public interface IMessage { }

View File

@@ -0,0 +1,131 @@
using Microsoft.Extensions.Logging;
using System.Text;
namespace MareSynchronos.Services.Mediator;
public sealed class MareMediator : IDisposable
{
private readonly object _addRemoveLock = new();
private readonly Dictionary<object, DateTime> _lastErrorTime = new();
private readonly ILogger<MareMediator> _logger;
private readonly PerformanceCollectorService _performanceCollector;
private readonly Dictionary<Type, HashSet<SubscriberAction>> _subscriberDict = new();
public MareMediator(ILogger<MareMediator> logger, PerformanceCollectorService performanceCollector)
{
_logger = logger;
_performanceCollector = performanceCollector;
}
public void Dispose()
{
_logger.LogTrace("Disposing {type}", GetType());
_subscriberDict.Clear();
GC.SuppressFinalize(this);
}
public void PrintSubscriberInfo()
{
foreach (var kvp in _subscriberDict.SelectMany(c => c.Value.Select(v => v))
.DistinctBy(p => p.Subscriber).OrderBy(p => p.Subscriber.GetType().FullName, StringComparer.Ordinal).ToList())
{
_logger.LogInformation("Subscriber {type}: {sub}", kvp.Subscriber.GetType().Name, kvp.Subscriber.ToString());
StringBuilder sb = new();
sb.Append("=> ");
foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == kvp.Subscriber)).ToList())
{
sb.Append(item.Key.Name).Append(", ");
}
if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal))
_logger.LogInformation("{sb}", sb.ToString());
_logger.LogInformation("---");
}
}
public void Publish<T>(T message) where T : IMessage
{
if (_subscriberDict.TryGetValue(message.GetType(), out HashSet<SubscriberAction>? subscribers) && subscribers != null && subscribers.Any())
{
_performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}", () =>
{
foreach (SubscriberAction subscriber in subscribers?.Where(s => s.Subscriber != null).ToHashSet() ?? new HashSet<SubscriberAction>())
{
try
{
_performanceCollector.LogPerformance(this, $"Publish>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}", () => ((Action<T>)subscriber.Action).Invoke(message));
}
catch (Exception ex)
{
if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime) && lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow)
continue;
_logger.LogCritical(ex, "Error executing {type} for subscriber {subscriber}", message.GetType().Name, subscriber.Subscriber.GetType().Name);
_lastErrorTime[subscriber] = DateTime.UtcNow;
}
}
});
}
}
public void Subscribe<T>(IMediatorSubscriber subscriber, Action<T> action) where T : IMessage
{
lock (_addRemoveLock)
{
_subscriberDict.TryAdd(typeof(T), new HashSet<SubscriberAction>());
if (!_subscriberDict[typeof(T)].Add(new(subscriber, action)))
{
throw new InvalidOperationException("Already subscribed");
}
_logger.LogDebug("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name);
}
}
public void Unsubscribe<T>(IMediatorSubscriber subscriber) where T : IMessage
{
lock (_addRemoveLock)
{
if (_subscriberDict.ContainsKey(typeof(T)))
{
_subscriberDict[typeof(T)].RemoveWhere(p => p.Subscriber == subscriber);
}
}
}
internal void UnsubscribeAll(IMediatorSubscriber subscriber)
{
lock (_addRemoveLock)
{
foreach (KeyValuePair<Type, HashSet<SubscriberAction>> kvp in _subscriberDict)
{
int unSubbed = _subscriberDict[kvp.Key]?.RemoveWhere(p => p.Subscriber == subscriber) ?? 0;
if (unSubbed > 0)
{
_logger.LogDebug("{sub} unsubscribed from {msg}", subscriber.GetType().Name, kvp.Key.Name);
if (_subscriberDict[kvp.Key].Any())
{
_logger.LogTrace("Remaining Subscribers: {item}", string.Join(", ", _subscriberDict[kvp.Key].Select(k => k.Subscriber.GetType().Name)));
}
}
}
}
}
private sealed class SubscriberAction
{
public SubscriberAction(IMediatorSubscriber subscriber, object action)
{
Subscriber = subscriber;
Action = action;
}
public object Action { get; }
public IMediatorSubscriber Subscriber { get; }
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class MediatorSubscriberBase : IMediatorSubscriber
{
protected MediatorSubscriberBase(ILogger logger, MareMediator mediator)
{
Logger = logger;
Logger.LogTrace("Creating {type} ({this})", GetType().Name, this);
Mediator = mediator;
}
public MareMediator Mediator { get; }
protected ILogger Logger { get; }
protected void UnsubscribeAll()
{
Logger.LogTrace("Unsubscribing from all for {type} ({this})", GetType().Name, this);
Mediator.UnsubscribeAll(this);
}
}

View File

@@ -0,0 +1,57 @@
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.API.Dto;
using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.WebAPI.Files.Models;
namespace MareSynchronos.Services.Mediator;
#pragma warning disable MA0048 // File name must match type name
public record SwitchToIntroUiMessage : IMessage;
public record SwitchToMainUiMessage : IMessage;
public record OpenSettingsUiMessage : IMessage;
public record DalamudLoginMessage : IMessage;
public record DalamudLogoutMessage : IMessage;
public record FrameworkUpdateMessage : IMessage;
public record ClassJobChangedMessage : IMessage;
public record DelayedFrameworkUpdateMessage : IMessage;
public record ZoneSwitchStartMessage : IMessage;
public record ZoneSwitchEndMessage : IMessage;
public record CutsceneStartMessage : IMessage;
public record GposeStartMessage : IMessage;
public record GposeEndMessage : IMessage;
public record CutsceneEndMessage : IMessage;
public record CutsceneFrameworkUpdateMessage : IMessage;
public record ConnectedMessage(ConnectionDto Connection) : IMessage;
public record DisconnectedMessage : IMessage;
public record PenumbraModSettingChangedMessage : IMessage;
public record PenumbraInitializedMessage : IMessage;
public record PenumbraDisposedMessage : IMessage;
public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasRequested) : IMessage;
public record HeelsOffsetMessage : IMessage;
public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : IMessage;
public record CustomizePlusMessage : IMessage;
public record PalettePlusMessage : IMessage;
public record PlayerChangedMessage(API.Data.CharacterData Data) : IMessage;
public record CharacterChangedMessage(GameObjectHandler GameObjectHandler) : IMessage;
public record TransientResourceChangedMessage(IntPtr Address) : IMessage;
public record AddWatchedGameObjectHandler(GameObjectHandler Handler) : IMessage;
public record RemoveWatchedGameObjectHandler(GameObjectHandler Handler) : IMessage;
public record HaltScanMessage(string Source) : IMessage;
public record ResumeScanMessage(string Source) : IMessage;
public record NotificationMessage
(string Title, string Message, NotificationType Type, uint TimeShownOnScreen = 3000) : IMessage;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : IMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : IMessage;
public record CharacterDataCreatedMessage(API.Data.CharacterData CharacterData) : IMessage;
public record PenumbraStartRedrawMessage(IntPtr Address) : IMessage;
public record PenumbraEndRedrawMessage(IntPtr Address) : IMessage;
public record HubReconnectingMessage(Exception? Exception) : IMessage;
public record HubReconnectedMessage(string? Arg) : IMessage;
public record HubClosedMessage(Exception? Exception) : IMessage;
public record DownloadReadyMessage(Guid RequestId) : IMessage;
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : IMessage;
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : IMessage;
public record UiToggleMessage(Type UiType) : IMessage;
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : IMessage;
#pragma warning restore MA0048 // File name must match type name

View File

@@ -0,0 +1,45 @@
using Dalamud.Interface.Windowing;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.Mediator;
public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber, IDisposable
{
protected readonly ILogger _logger;
protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name) : base(name)
{
_logger = logger;
Mediator = mediator;
_logger.LogTrace("Creating {type}", GetType());
Mediator.Subscribe<UiToggleMessage>(this, (msg) =>
{
if (msg.UiType == GetType())
{
Toggle();
}
});
}
public MareMediator Mediator { get; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected virtual void Dispose(bool disposing)
{
_logger.LogTrace("Disposing {type}", GetType());
Mediator.UnsubscribeAll(this);
}
}

View File

@@ -0,0 +1,113 @@
using Dalamud.Game.Gui;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public class NotificationService : DisposableMediatorSubscriberBase
{
private readonly ChatGui _chatGui;
private readonly MareConfigService _configurationService;
private readonly UiBuilder _uiBuilder;
public NotificationService(ILogger<NotificationService> logger, MareMediator mediator, UiBuilder uiBuilder, ChatGui chatGui, MareConfigService configurationService) : base(logger, mediator)
{
_uiBuilder = uiBuilder;
_chatGui = chatGui;
_configurationService = configurationService;
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
}
private void PrintErrorChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private void ShowChat(NotificationMessage msg)
{
switch (msg.Type)
{
case NotificationType.Info:
case NotificationType.Success:
case NotificationType.None:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
}
}
private void ShowNotification(NotificationMessage msg)
{
Logger.LogInformation("{msg}", msg.ToString());
switch (msg.Type)
{
case NotificationType.Info:
case NotificationType.Success:
case NotificationType.None:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
}
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
_uiBuilder.AddNotification(msg.Message ?? string.Empty, "[Mare Synchronos] " + msg.Title, msg.Type, msg.TimeShownOnScreen);
}
}

View File

@@ -0,0 +1,197 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Text;
namespace MareSynchronos.Services;
public sealed class PerformanceCollectorService : IHostedService
{
private const string _counterSplit = "=>";
private readonly ILogger<PerformanceCollectorService> _logger;
private readonly MareConfigService _mareConfigService;
private readonly ConcurrentDictionary<string, RollingList<Tuple<TimeOnly, long>>> _performanceCounters = new(StringComparer.Ordinal);
private readonly CancellationTokenSource _periodicLogPruneTask = new();
public PerformanceCollectorService(ILogger<PerformanceCollectorService> logger, MareConfigService mareConfigService)
{
_logger = logger;
_mareConfigService = mareConfigService;
}
public T LogPerformance<T>(object sender, string counterName, Func<T> func)
{
if (!_mareConfigService.Current.LogPerformance) return func.Invoke();
counterName = sender.GetType().Name + _counterSplit + counterName;
if (!_performanceCounters.TryGetValue(counterName, out var list))
{
list = _performanceCounters[counterName] = new(10000);
}
Stopwatch st = Stopwatch.StartNew();
try
{
return func.Invoke();
}
finally
{
st.Stop();
list.Add(new(TimeOnly.FromDateTime(DateTime.Now), st.ElapsedTicks));
}
}
public void LogPerformance(object sender, string counterName, Action act)
{
if (!_mareConfigService.Current.LogPerformance) { act.Invoke(); return; }
counterName = sender.GetType().Name + _counterSplit + counterName;
if (!_performanceCounters.TryGetValue(counterName, out var list))
{
list = _performanceCounters[counterName] = new(10000);
}
Stopwatch st = Stopwatch.StartNew();
try
{
act.Invoke();
}
finally
{
st.Stop();
list.Add(new(TimeOnly.FromDateTime(DateTime.Now), st.ElapsedTicks));
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Run(PeriodicLogPrune, _periodicLogPruneTask.Token);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_periodicLogPruneTask.Cancel();
return Task.CompletedTask;
}
internal void PrintPerformanceStats(int limitBySeconds = 0)
{
if (!_mareConfigService.Current.LogPerformance)
{
_logger.LogWarning("Performance counters are disabled");
}
StringBuilder sb = new();
if (limitBySeconds > 0)
{
sb.AppendLine($"Performance Metrics over the past {limitBySeconds} seconds of each counter");
}
else
{
sb.AppendLine("Performance metrics over total lifetime of each counter");
}
var data = _performanceCounters.ToList();
var longestCounterName = data.OrderByDescending(d => d.Key.Length).First().Key.Length + 2;
sb.Append("-Last".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Max".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Average".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Last Update".PadRight(15, '-'));
sb.Append('|');
sb.Append("-Entries".PadRight(10, '-'));
sb.Append('|');
sb.Append("-Counter Name".PadRight(longestCounterName, '-'));
sb.AppendLine();
var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList();
var previousCaller = orderedData[0].Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
foreach (var entry in orderedData)
{
var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
if (!string.Equals(previousCaller, newCaller, StringComparison.Ordinal))
{
DrawSeparator(sb, longestCounterName);
}
var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : entry.Value.ToList();
if (pastEntries.Any())
{
sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault()?.Item2 ?? 0).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|');
sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
sb.Append('|');
sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
}
else
{
sb.Append(" -".PadRight(15));
sb.Append('|');
sb.Append(" -".PadRight(15));
sb.Append('|');
sb.Append(" -".PadRight(15));
}
sb.Append('|');
sb.Append((" " + (pastEntries.LastOrDefault()?.Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture) ?? "-")).PadRight(15, ' '));
sb.Append('|');
sb.Append((" " + pastEntries.Count).PadRight(10));
sb.Append('|');
sb.Append(' ').Append(entry.Key);
sb.AppendLine();
previousCaller = newCaller;
}
DrawSeparator(sb, longestCounterName);
_logger.LogInformation("{perf}", sb.ToString());
}
private static void DrawSeparator(StringBuilder sb, int longestCounterName)
{
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(15, '-'));
sb.Append('+');
sb.Append("".PadRight(10, '-'));
sb.Append('+');
sb.Append("".PadRight(longestCounterName, '-'));
sb.AppendLine();
}
private async Task PeriodicLogPrune()
{
while (!_periodicLogPruneTask.Token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTask.Token).ConfigureAwait(false);
foreach (var entries in _performanceCounters.ToList())
{
try
{
var last = entries.Value.ToList()[^1];
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now))
{
_performanceCounters.Remove(entries.Key, out _);
}
}
catch (Exception e)
{
_logger.LogDebug(e, "Error removing performance counter {counter}", entries.Key);
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
namespace MareSynchronos.Services.ServerConfiguration;
public record JwtCache(string ApiUrl, string PlayerName, uint WorldId, string SecretKey);

View File

@@ -0,0 +1,339 @@
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace MareSynchronos.Services.ServerConfiguration;
public class ServerConfigurationManager
{
private readonly ServerConfigService _configService;
private readonly DalamudUtilService _dalamudUtil;
private readonly ILogger<ServerConfigurationManager> _logger;
private readonly NotesConfigService _notesConfig;
private readonly ServerTagConfigService _serverTagConfig;
private readonly Dictionary<JwtCache, string> _tokenDictionary = new();
public ServerConfigurationManager(ILogger<ServerConfigurationManager> logger, ServerConfigService configService,
ServerTagConfigService serverTagConfig, NotesConfigService notesConfig, DalamudUtilService dalamudUtil)
{
_logger = logger;
_configService = configService;
_serverTagConfig = serverTagConfig;
_notesConfig = notesConfig;
_dalamudUtil = dalamudUtil;
}
public string CurrentApiUrl => string.IsNullOrEmpty(_configService.Current.CurrentServer) ? ApiController.MainServiceUri : _configService.Current.CurrentServer;
public ServerStorage? CurrentServer => _configService.Current.ServerStorage.TryGetValue(CurrentApiUrl, out ServerStorage? value) ? value : null;
public int GetCurrentServerIndex()
{
return Array.IndexOf(_configService.Current.ServerStorage.Keys.ToArray(), CurrentApiUrl);
}
public string? GetSecretKey(int serverIdx = -1)
{
ServerStorage? currentServer;
currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx);
if (currentServer == null)
{
currentServer = new();
Save();
}
var charaName = _dalamudUtil.PlayerName;
var worldId = _dalamudUtil.WorldId;
if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
{
currentServer.Authentications.Add(new Authentication()
{
CharacterName = charaName,
WorldId = worldId,
SecretKeyIdx = currentServer.SecretKeys.Last().Key,
});
Save();
}
var auth = currentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
if (auth == null) return null;
if (currentServer.SecretKeys.TryGetValue(auth.SecretKeyIdx, out var secretKey))
{
return secretKey.Key;
}
return null;
}
public string[] GetServerApiUrls()
{
return _configService.Current.ServerStorage.Keys.ToArray();
}
public ServerStorage GetServerByIndex(int idx)
{
try
{
return _configService.Current.ServerStorage.ElementAt(idx).Value;
}
catch
{
_configService.Current.CurrentServer = ApiController.MainServiceUri;
if (!_configService.Current.ServerStorage.ContainsKey(ApiController.MainServer))
{
_configService.Current.ServerStorage.Add(_configService.Current.CurrentServer, new ServerStorage() { ServerUri = ApiController.MainServiceUri, ServerName = ApiController.MainServer });
}
Save();
return CurrentServer!;
}
}
public string[] GetServerNames()
{
return _configService.Current.ServerStorage.Values.Select(v => v.ServerName).ToArray();
}
public string? GetToken()
{
var charaName = _dalamudUtil.PlayerName;
var worldId = _dalamudUtil.WorldId;
var secretKey = GetSecretKey();
if (secretKey == null) return null;
if (_tokenDictionary.TryGetValue(new JwtCache(CurrentApiUrl, charaName, worldId, secretKey), out var token))
{
return token;
}
return null;
}
public bool HasValidConfig()
{
return CurrentServer != null;
}
public void Save()
{
var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown";
_logger.LogDebug(caller + " Calling config save");
_configService.Save();
}
public void SaveToken(string token)
{
var charaName = _dalamudUtil.PlayerName;
var worldId = _dalamudUtil.WorldId;
var secretKey = GetSecretKey();
if (string.IsNullOrEmpty(secretKey)) throw new InvalidOperationException("No secret key set");
_tokenDictionary[new JwtCache(CurrentApiUrl, charaName, worldId, secretKey)] = token;
}
public void SelectServer(int idx)
{
_configService.Current.CurrentServer = GetServerByIndex(idx).ServerUri;
CurrentServer!.FullPause = false;
Save();
}
internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1, bool addLastSecretKey = false)
{
if (serverSelectionIndex == -1) serverSelectionIndex = GetCurrentServerIndex();
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Add(new Authentication()
{
CharacterName = _dalamudUtil.PlayerName,
WorldId = _dalamudUtil.WorldId,
SecretKeyIdx = addLastSecretKey ? server.SecretKeys.Last().Key : -1,
});
Save();
}
internal void AddEmptyCharacterToServer(int serverSelectionIndex)
{
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Add(new Authentication());
Save();
}
internal void AddOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Add(tag);
_serverTagConfig.Save();
}
internal void AddServer(ServerStorage serverStorage)
{
_configService.Current.ServerStorage[serverStorage.ServerUri] = serverStorage;
Save();
}
internal void AddTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Add(tag);
_serverTagConfig.Save();
}
internal void AddTagForUid(string uid, string tagName)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Add(tagName);
}
else
{
CurrentServerTagStorage().UidServerPairedUserTags[uid] = new() { tagName };
}
_serverTagConfig.Save();
}
internal bool ContainsOpenPairTag(string tag)
{
return CurrentServerTagStorage().OpenPairTags.Contains(tag);
}
internal bool ContainsTag(string uid, string tag)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Contains(tag, StringComparer.Ordinal);
}
return false;
}
internal void DeleteServer(ServerStorage selectedServer)
{
_configService.Current.ServerStorage.Remove(selectedServer.ServerUri);
Save();
}
internal string? GetNoteForGid(string gID)
{
if (CurrentNotesStorage().GidServerComments.TryGetValue(gID, out var note))
{
if (string.IsNullOrEmpty(note)) return null;
return note;
}
return null;
}
internal string? GetNoteForUid(string uid)
{
if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note))
{
if (string.IsNullOrEmpty(note)) return null;
return note;
}
return null;
}
internal HashSet<string> GetServerAvailablePairTags()
{
return CurrentServerTagStorage().ServerAvailablePairTags;
}
internal Dictionary<string, List<string>> GetUidServerPairedUserTags()
{
return CurrentServerTagStorage().UidServerPairedUserTags;
}
internal HashSet<string> GetUidsForTag(string tag)
{
return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
}
internal bool HasTags(string uid)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
return tags.Any();
}
return false;
}
internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item)
{
var server = GetServerByIndex(serverSelectionIndex);
server.Authentications.Remove(item);
Save();
}
internal void RemoveOpenPairTag(string tag)
{
CurrentServerTagStorage().OpenPairTags.Remove(tag);
_serverTagConfig.Save();
}
internal void RemoveTag(string tag)
{
CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag);
foreach (var uid in GetUidsForTag(tag))
{
RemoveTagForUid(uid, tag, save: false);
}
_serverTagConfig.Save();
}
internal void RemoveTagForUid(string uid, string tagName, bool save = true)
{
if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags))
{
tags.Remove(tagName);
if (save)
_serverTagConfig.Save();
}
}
internal void SaveNotes()
{
_notesConfig.Save();
}
internal void SetNoteForGid(string gid, string note, bool save = true)
{
CurrentNotesStorage().GidServerComments[gid] = note;
if (save)
_notesConfig.Save();
}
internal void SetNoteForUid(string uid, string note, bool save = true)
{
CurrentNotesStorage().UidServerComments[uid] = note;
if (save)
_notesConfig.Save();
}
private ServerNotesStorage CurrentNotesStorage()
{
TryCreateCurrentNotesStorage();
return _notesConfig.Current.ServerNotes[CurrentApiUrl];
}
private ServerTagStorage CurrentServerTagStorage()
{
TryCreateCurrentServerTagStorage();
return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl];
}
private void TryCreateCurrentNotesStorage()
{
if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl))
{
_notesConfig.Current.ServerNotes[CurrentApiUrl] = new();
}
}
private void TryCreateCurrentServerTagStorage()
{
if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl))
{
_serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new();
}
}
}

View File

@@ -0,0 +1,66 @@
using Dalamud.Plugin;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing;
using MareSynchronos.UI;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class UiService : IDisposable
{
private readonly DalamudPluginInterface _dalamudPluginInterface;
private readonly FileDialogManager _fileDialogManager;
private readonly ILogger<UiService> _logger;
private readonly MareConfigService _mareConfigService;
private readonly MareMediator _mareMediator;
private readonly WindowSystem _windowSystem;
public UiService(ILogger<UiService> logger, DalamudPluginInterface dalamudPluginInterface,
MareConfigService mareConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows,
FileDialogManager fileDialogManager, MareMediator mareMediator)
{
_logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name);
_dalamudPluginInterface = dalamudPluginInterface;
_mareConfigService = mareConfigService;
_windowSystem = windowSystem;
_fileDialogManager = fileDialogManager;
_mareMediator = mareMediator;
_dalamudPluginInterface.UiBuilder.DisableGposeUiHide = true;
_dalamudPluginInterface.UiBuilder.Draw += Draw;
_dalamudPluginInterface.UiBuilder.OpenConfigUi += ToggleUi;
foreach (var window in windows)
{
_windowSystem.AddWindow(window);
}
}
public void Dispose()
{
_logger.LogTrace("Disposing {type}", GetType().Name);
_windowSystem.RemoveAllWindows();
_dalamudPluginInterface.UiBuilder.Draw -= Draw;
_dalamudPluginInterface.UiBuilder.OpenConfigUi -= ToggleUi;
}
public void ToggleUi()
{
if (_mareConfigService.Current.HasValidSetup())
_mareMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
else
_mareMediator.Publish(new UiToggleMessage(typeof(IntroUi)));
}
private void Draw()
{
_windowSystem.Draw();
_fileDialogManager.Draw();
}
}