[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:
97
MareSynchronos/Services/CommandManagerService.cs
Normal file
97
MareSynchronos/Services/CommandManagerService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
337
MareSynchronos/Services/DalamudUtilService.cs
Normal file
337
MareSynchronos/Services/DalamudUtilService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
6
MareSynchronos/Services/Mediator/IMediatorSubscriber.cs
Normal file
6
MareSynchronos/Services/Mediator/IMediatorSubscriber.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public interface IMediatorSubscriber
|
||||
{
|
||||
MareMediator Mediator { get; }
|
||||
}
|
||||
3
MareSynchronos/Services/Mediator/IMessage.cs
Normal file
3
MareSynchronos/Services/Mediator/IMessage.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MareSynchronos.Services.Mediator;
|
||||
|
||||
public interface IMessage { }
|
||||
131
MareSynchronos/Services/Mediator/MareMediator.cs
Normal file
131
MareSynchronos/Services/Mediator/MareMediator.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
23
MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs
Normal file
23
MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
57
MareSynchronos/Services/Mediator/Messages.cs
Normal file
57
MareSynchronos/Services/Mediator/Messages.cs
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
113
MareSynchronos/Services/NotificationService.cs
Normal file
113
MareSynchronos/Services/NotificationService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
197
MareSynchronos/Services/PerformanceCollectorService.cs
Normal file
197
MareSynchronos/Services/PerformanceCollectorService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
MareSynchronos/Services/ServerConfiguration/JwtCache.cs
Normal file
3
MareSynchronos/Services/ServerConfiguration/JwtCache.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MareSynchronos.Services.ServerConfiguration;
|
||||
|
||||
public record JwtCache(string ApiUrl, string PlayerName, uint WorldId, string SecretKey);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
66
MareSynchronos/Services/UiService.cs
Normal file
66
MareSynchronos/Services/UiService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user