[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:
@@ -7,6 +7,7 @@ namespace MareSynchronos.Utils;
|
||||
public static class Crypto
|
||||
{
|
||||
#pragma warning disable SYSLIB0021 // Type or member is obsolete
|
||||
|
||||
public static string GetFileHash(this string filePath)
|
||||
{
|
||||
using SHA1CryptoServiceProvider cryptoProvider = new();
|
||||
@@ -30,5 +31,6 @@ public static class Crypto
|
||||
using SHA256CryptoServiceProvider cryptoProvider = new();
|
||||
return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", "", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
#pragma warning restore SYSLIB0021 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using Dalamud.Logging;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
internal class DalamudLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
|
||||
public DalamudLogger(string name, MareConfigService mareConfigService)
|
||||
{
|
||||
this._name = name;
|
||||
_mareConfigService = mareConfigService;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel)) return;
|
||||
|
||||
if ((int)logLevel <= (int)LogLevel.Information)
|
||||
PluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
|
||||
else
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine($"[{_name}]{{{(int)logLevel}}} {state}: {exception?.Message}");
|
||||
sb.AppendLine(exception?.StackTrace);
|
||||
if (logLevel == LogLevel.Warning)
|
||||
PluginLog.Warning(sb.ToString());
|
||||
else if (logLevel == LogLevel.Error)
|
||||
PluginLog.Error(sb.ToString());
|
||||
else
|
||||
PluginLog.Fatal(sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return (int)_mareConfigService.Current.LogLevel <= (int)logLevel;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => default!;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
[ProviderAlias("Dalamud")]
|
||||
public class DalamudLoggingProvider : ILoggerProvider
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
|
||||
public DalamudLoggingProvider(MareConfigService mareConfigService)
|
||||
{
|
||||
_mareConfigService = mareConfigService;
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
string catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
|
||||
if (catName.Length > 15)
|
||||
{
|
||||
catName = string.Join("", catName.Take(6)) + "..." + string.Join("", catName.TakeLast(6));
|
||||
}
|
||||
else
|
||||
{
|
||||
catName = string.Join("", Enumerable.Range(0, 15 - catName.Length).Select(_ => " ")) + catName;
|
||||
}
|
||||
|
||||
return _loggers.GetOrAdd(catName, name => new DalamudLogger(catName, _mareConfigService));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_loggers.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
public static class DalamudLoggingProviderExtensions
|
||||
{
|
||||
public static ILoggingBuilder AddDalamudLogging(this ILoggingBuilder builder)
|
||||
{
|
||||
builder.ClearProviders();
|
||||
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using MareSynchronos.Mediator;
|
||||
using MareSynchronos.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
public class DalamudUtil : IDisposable
|
||||
{
|
||||
private readonly ILogger<DalamudUtil> _logger;
|
||||
private readonly ClientState _clientState;
|
||||
private readonly ObjectTable _objectTable;
|
||||
private readonly Framework _framework;
|
||||
private readonly Condition _condition;
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly PerformanceCollector _performanceCollector;
|
||||
private uint? _classJobId = 0;
|
||||
private DateTime _delayedFrameworkUpdateCheck = DateTime.Now;
|
||||
private bool _sentBetweenAreas = false;
|
||||
public bool IsInCutscene { get; private set; } = false;
|
||||
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||
public bool IsInGpose { get; private set; } = false;
|
||||
|
||||
public unsafe bool IsGameObjectPresent(IntPtr key)
|
||||
{
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.Address == key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public DalamudUtil(ILogger<DalamudUtil> logger, ClientState clientState, ObjectTable objectTable, Framework framework,
|
||||
Condition condition, Dalamud.Data.DataManager gameData, MareMediator mediator, PerformanceCollector performanceCollector)
|
||||
{
|
||||
_logger = logger;
|
||||
_clientState = clientState;
|
||||
_objectTable = objectTable;
|
||||
_framework = framework;
|
||||
_condition = condition;
|
||||
_mediator = mediator;
|
||||
_performanceCollector = performanceCollector;
|
||||
_framework.Update += FrameworkOnUpdate;
|
||||
if (IsLoggedIn)
|
||||
{
|
||||
_classJobId = _clientState.LocalPlayer!.ClassJob.Id;
|
||||
}
|
||||
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 Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference)
|
||||
{
|
||||
return _objectTable.CreateObjectReference(reference);
|
||||
}
|
||||
|
||||
public unsafe GameObject* GposeTarget => TargetSystem.Instance()->GPoseTarget;
|
||||
|
||||
public unsafe Dalamud.Game.ClientState.Objects.Types.GameObject? GposeTargetGameObject => GposeTarget == null ? null : _objectTable[GposeTarget->ObjectIndex];
|
||||
|
||||
public bool IsLoggedIn { get; private set; }
|
||||
|
||||
public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid();
|
||||
|
||||
public static bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj)
|
||||
{
|
||||
return obj != null && obj.IsValid();
|
||||
}
|
||||
|
||||
public unsafe IntPtr GetMinion(IntPtr? playerPointer = null)
|
||||
{
|
||||
playerPointer ??= PlayerPointer;
|
||||
return (IntPtr)((Character*)playerPointer)->CompanionObject;
|
||||
}
|
||||
|
||||
public unsafe IntPtr GetPet(IntPtr? playerPointer = null)
|
||||
{
|
||||
var mgr = CharacterManager.Instance();
|
||||
playerPointer ??= PlayerPointer;
|
||||
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
|
||||
}
|
||||
|
||||
public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null)
|
||||
{
|
||||
var mgr = CharacterManager.Instance();
|
||||
playerPointer ??= PlayerPointer;
|
||||
return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer);
|
||||
}
|
||||
|
||||
public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null)
|
||||
{
|
||||
playerPointer ??= PlayerPointer;
|
||||
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1);
|
||||
}
|
||||
|
||||
public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--";
|
||||
public uint WorldId => _clientState.LocalPlayer!.HomeWorld.Id;
|
||||
|
||||
public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
|
||||
|
||||
public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!;
|
||||
|
||||
public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id);
|
||||
|
||||
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 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 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 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 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 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");
|
||||
|
||||
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");
|
||||
curWaitTime += tick;
|
||||
await Task.Delay(tick).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
logger.LogTrace($"[{redrawId}] Finished drawing after {curWaitTime}ms");
|
||||
}
|
||||
catch (NullReferenceException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error accessing " + handler + ", object does not exist anymore?");
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error accessing " + handler + ", object does not exist anymore?");
|
||||
}
|
||||
}
|
||||
|
||||
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:" + 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);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogTrace($"Disposing {GetType()}");
|
||||
_framework.Update -= FrameworkOnUpdate;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using MareSynchronos.Models;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
public class FileReplacementComparer : IEqualityComparer<FileReplacement>
|
||||
{
|
||||
public static FileReplacementComparer Instance => _instance;
|
||||
private static FileReplacementComparer _instance = new();
|
||||
private FileReplacementComparer() { }
|
||||
public bool Equals(FileReplacement? x, FileReplacement? y)
|
||||
{
|
||||
if (x == null || y == null) return false;
|
||||
return x.ResolvedPath.Equals(y.ResolvedPath) && CompareLists(x.GamePaths, y.GamePaths);
|
||||
}
|
||||
|
||||
public int GetHashCode(FileReplacement obj)
|
||||
{
|
||||
return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths));
|
||||
}
|
||||
|
||||
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (T element in source)
|
||||
{
|
||||
hash = unchecked(hash +
|
||||
EqualityComparer<T>.Default.GetHashCode(element));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private bool CompareLists(HashSet<string> list1, HashSet<string> list2)
|
||||
{
|
||||
if (list1.Count != list2.Count)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < list1.Count; i++)
|
||||
{
|
||||
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using MareSynchronos.API.Data;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData>
|
||||
{
|
||||
public static FileReplacementDataComparer Instance => _instance;
|
||||
private static FileReplacementDataComparer _instance = new();
|
||||
private FileReplacementDataComparer() { }
|
||||
public bool Equals(FileReplacementData? x, FileReplacementData? y)
|
||||
{
|
||||
if (x == null || y == null) return false;
|
||||
return x.Hash.Equals(y.Hash) && CompareLists(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int GetHashCode(FileReplacementData obj)
|
||||
{
|
||||
return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath));
|
||||
}
|
||||
|
||||
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (T element in source)
|
||||
{
|
||||
hash = unchecked(hash +
|
||||
EqualityComparer<T>.Default.GetHashCode(element));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private bool CompareLists(HashSet<string> list1, HashSet<string> list2)
|
||||
{
|
||||
if (list1.Count != list2.Count)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < list1.Count; i++)
|
||||
{
|
||||
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
public class PerformanceCollector : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, RollingList<Tuple<TimeOnly, long>>> _performanceCounters = new(StringComparer.Ordinal);
|
||||
private readonly ILogger<PerformanceCollector> _logger;
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private const string _counterSplit = "=>";
|
||||
private readonly CancellationTokenSource _periodicLogPruneTask = new();
|
||||
|
||||
public PerformanceCollector(ILogger<PerformanceCollector> logger, MareConfigService mareConfigService)
|
||||
{
|
||||
_logger = logger;
|
||||
_mareConfigService = mareConfigService;
|
||||
_ = Task.Run(PeriodicLogPrune, _periodicLogPruneTask.Token);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogTrace("Disposing {this}", GetType());
|
||||
_periodicLogPruneTask.Cancel();
|
||||
}
|
||||
|
||||
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().Last();
|
||||
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now))
|
||||
{
|
||||
_performanceCounters.Remove(entries.Key, out _);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogDebug("Error removing performance counter {counter}", entries.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
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();
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
st.Stop();
|
||||
list.Add(new(TimeOnly.FromDateTime(DateTime.Now), st.ElapsedTicks));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.First().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();
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ namespace MareSynchronos.Utils;
|
||||
|
||||
public class RollingList<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly LinkedList<T> _list = new();
|
||||
private readonly object _addLock = new();
|
||||
private readonly LinkedList<T> _list = new();
|
||||
|
||||
public RollingList(int maximumCount)
|
||||
{
|
||||
@@ -15,8 +15,19 @@ public class RollingList<T> : IEnumerable<T>
|
||||
MaximumCount = maximumCount;
|
||||
}
|
||||
|
||||
public int MaximumCount { get; }
|
||||
public int Count => _list.Count;
|
||||
public int MaximumCount { get; }
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
return _list.Skip(index).First();
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(T value)
|
||||
{
|
||||
@@ -30,17 +41,7 @@ public class RollingList<T> : IEnumerable<T>
|
||||
}
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
return _list.Skip(index).First();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MareSynchronos.Utils;
|
||||
|
||||
public static class VariousExtensions
|
||||
{
|
||||
public static DateTime GetLinkerTime(Assembly assembly)
|
||||
{
|
||||
const string BuildVersionMetadataPrefix = "+build";
|
||||
|
||||
var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
||||
if (attribute?.InformationalVersion != null)
|
||||
{
|
||||
var value = attribute.InformationalVersion;
|
||||
var index = value.IndexOf(BuildVersionMetadataPrefix, StringComparison.Ordinal);
|
||||
if (index > 0)
|
||||
{
|
||||
value = value[(index + BuildVersionMetadataPrefix.Length)..];
|
||||
return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static T DeepClone<T>(this T obj)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj))!;
|
||||
return JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))!;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user