diff --git a/MareAPI b/MareAPI index 9dc1e90..57a7ab8 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 9dc1e901aa453e65bb432b2b288c1ff00021700a +Subproject commit 57a7ab82625ca350bce0442c60f457e5e5e172c3 diff --git a/MareSynchronos/Configuration.cs b/MareSynchronos/Configuration.cs index 017ef81..d5b51ad 100644 --- a/MareSynchronos/Configuration.cs +++ b/MareSynchronos/Configuration.cs @@ -7,195 +7,194 @@ using System.Linq; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -namespace MareSynchronos +namespace MareSynchronos; + +public static class ConfigurationExtensions { - public static class ConfigurationExtensions + public static bool HasValidSetup(this Configuration configuration) { - public static bool HasValidSetup(this Configuration configuration) - { - return configuration.AcceptedAgreement && configuration.InitialScanComplete - && !string.IsNullOrEmpty(configuration.CacheFolder) - && Directory.Exists(configuration.CacheFolder) - && configuration.ClientSecret.ContainsKey(configuration.ApiUri); - } - - public static Dictionary GetCurrentServerUidComments(this Configuration configuration) - { - return configuration.UidServerComments.ContainsKey(configuration.ApiUri) - ? configuration.UidServerComments[configuration.ApiUri] - : new Dictionary(); - } - - public static void SetCurrentServerUidComment(this Configuration configuration, string uid, string comment) - { - if (!configuration.UidServerComments.ContainsKey(configuration.ApiUri)) - { - configuration.UidServerComments[configuration.ApiUri] = new Dictionary(); - } - - configuration.UidServerComments[configuration.ApiUri][uid] = comment; - } + return configuration.AcceptedAgreement && configuration.InitialScanComplete + && !string.IsNullOrEmpty(configuration.CacheFolder) + && Directory.Exists(configuration.CacheFolder) + && configuration.ClientSecret.ContainsKey(configuration.ApiUri); } - [Serializable] - public class Configuration : IPluginConfiguration + public static Dictionary GetCurrentServerUidComments(this Configuration configuration) { - private string _apiUri = string.Empty; - private int _maxParallelScan = 10; - [NonSerialized] - private DalamudPluginInterface? _pluginInterface; + return configuration.UidServerComments.ContainsKey(configuration.ApiUri) + ? configuration.UidServerComments[configuration.ApiUri] + : new Dictionary(); + } - public bool DarkSoulsAgreement { get; set; } = false; - public bool AcceptedAgreement { get; set; } = false; - public string ApiUri + public static void SetCurrentServerUidComment(this Configuration configuration, string uid, string comment) + { + if (!configuration.UidServerComments.ContainsKey(configuration.ApiUri)) { - get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri; - set => _apiUri = value; + configuration.UidServerComments[configuration.ApiUri] = new Dictionary(); } - public string CacheFolder { get; set; } = string.Empty; - public Dictionary ClientSecret { get; set; } = new(); - public Dictionary CustomServerList { get; set; } = new(); - public int MaxLocalCacheInGiB { get; set; } = 20; - public bool ReverseUserSort { get; set; } = true; + configuration.UidServerComments[configuration.ApiUri][uid] = comment; + } +} - public int TimeSpanBetweenScansInSeconds { get; set; } = 30; - public bool FileScanPaused { get; set; } = false; +[Serializable] +public class Configuration : IPluginConfiguration +{ + private string _apiUri = string.Empty; + private int _maxParallelScan = 10; + [NonSerialized] + private DalamudPluginInterface? _pluginInterface; - public bool InitialScanComplete { get; set; } = false; + public bool DarkSoulsAgreement { get; set; } = false; + public bool AcceptedAgreement { get; set; } = false; + public string ApiUri + { + get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri; + set => _apiUri = value; + } - public bool FullPause { get; set; } = false; - public Dictionary> UidServerComments { get; set; } = new(); + public string CacheFolder { get; set; } = string.Empty; + public Dictionary ClientSecret { get; set; } = new(); + public Dictionary CustomServerList { get; set; } = new(); + public int MaxLocalCacheInGiB { get; set; } = 20; + public bool ReverseUserSort { get; set; } = true; - public Dictionary UidComments { get; set; } = new(); - public int Version { get; set; } = 5; + public int TimeSpanBetweenScansInSeconds { get; set; } = 30; + public bool FileScanPaused { get; set; } = false; - public bool ShowTransferWindow { get; set; } = true; + public bool InitialScanComplete { get; set; } = false; - // the below exist just to make saving less cumbersome - public void Initialize(DalamudPluginInterface pluginInterface) + public bool FullPause { get; set; } = false; + public Dictionary> UidServerComments { get; set; } = new(); + + public Dictionary UidComments { get; set; } = new(); + public int Version { get; set; } = 5; + + public bool ShowTransferWindow { get; set; } = true; + + // the below exist just to make saving less cumbersome + public void Initialize(DalamudPluginInterface pluginInterface) + { + _pluginInterface = pluginInterface; + + if (!Directory.Exists(CacheFolder)) { - _pluginInterface = pluginInterface; + InitialScanComplete = false; + } - if (!Directory.Exists(CacheFolder)) + Save(); + } + + public void Save() + { + _pluginInterface!.SavePluginConfig(this); + } + + public void Migrate() + { + if (Version == 0) + { + Logger.Debug("Migrating Configuration from V0 to V1"); + Version = 1; + ApiUri = ApiUri.Replace("https", "wss"); + foreach (var kvp in ClientSecret.ToList()) { - InitialScanComplete = false; + var newKey = kvp.Key.Replace("https", "wss"); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) + { + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); + } } - + UidServerComments.Add(ApiUri, UidComments.ToDictionary(k => k.Key, k => k.Value)); + UidComments.Clear(); Save(); } - public void Save() + if (Version == 1) { - _pluginInterface!.SavePluginConfig(this); + Logger.Debug("Migrating Configuration from V1 to V2"); + ApiUri = ApiUri.Replace("5001", "5000"); + foreach (var kvp in ClientSecret.ToList()) + { + var newKey = kvp.Key.Replace("5001", "5000"); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) + { + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); + } + } + + foreach (var kvp in UidServerComments.ToList()) + { + var newKey = kvp.Key.Replace("5001", "5000"); + UidServerComments.Remove(kvp.Key); + UidServerComments.Add(newKey, kvp.Value); + } + + Version = 2; + Save(); } - public void Migrate() + if (Version == 2) { - if (Version == 0) + Logger.Debug("Migrating Configuration from V2 to V3"); + ApiUri = "wss://v2202207178628194299.powersrv.de:6871"; + ClientSecret.Clear(); + UidServerComments.Clear(); + + Version = 3; + Save(); + } + + if (Version == 3) + { + Logger.Debug("Migrating Configuration from V3 to V4"); + + ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); + foreach (var kvp in ClientSecret.ToList()) { - Logger.Debug("Migrating Configuration from V0 to V1"); - Version = 1; - ApiUri = ApiUri.Replace("https", "wss"); - foreach (var kvp in ClientSecret.ToList()) + var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) { - var newKey = kvp.Key.Replace("https", "wss"); - ClientSecret.Remove(kvp.Key); - if (ClientSecret.ContainsKey(newKey)) - { - ClientSecret[newKey] = kvp.Value; - } - else - { - ClientSecret.Add(newKey, kvp.Value); - } + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); } - UidServerComments.Add(ApiUri, UidComments.ToDictionary(k => k.Key, k => k.Value)); - UidComments.Clear(); - Save(); } - if (Version == 1) + foreach (var kvp in UidServerComments.ToList()) { - Logger.Debug("Migrating Configuration from V1 to V2"); - ApiUri = ApiUri.Replace("5001", "5000"); - foreach (var kvp in ClientSecret.ToList()) - { - var newKey = kvp.Key.Replace("5001", "5000"); - ClientSecret.Remove(kvp.Key); - if (ClientSecret.ContainsKey(newKey)) - { - ClientSecret[newKey] = kvp.Value; - } - else - { - ClientSecret.Add(newKey, kvp.Value); - } - } - - foreach (var kvp in UidServerComments.ToList()) - { - var newKey = kvp.Key.Replace("5001", "5000"); - UidServerComments.Remove(kvp.Key); - UidServerComments.Add(newKey, kvp.Value); - } - - Version = 2; - Save(); + var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); + UidServerComments.Remove(kvp.Key); + UidServerComments.Add(newKey, kvp.Value); } - if (Version == 2) - { - Logger.Debug("Migrating Configuration from V2 to V3"); - ApiUri = "wss://v2202207178628194299.powersrv.de:6871"; - ClientSecret.Clear(); - UidServerComments.Clear(); + Version = 4; + Save(); + } - Version = 3; - Save(); - } + if (Version == 4) + { + Logger.Debug("Migrating Configuration from V4 to V5"); - if (Version == 3) - { - Logger.Debug("Migrating Configuration from V3 to V4"); + ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6872", "wss://maresynchronos.com"); + ClientSecret.Remove("wss://v2202207178628194299.powersrv.de:6872"); + UidServerComments.Remove("wss://v2202207178628194299.powersrv.de:6872"); - ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); - foreach (var kvp in ClientSecret.ToList()) - { - var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); - ClientSecret.Remove(kvp.Key); - if (ClientSecret.ContainsKey(newKey)) - { - ClientSecret[newKey] = kvp.Value; - } - else - { - ClientSecret.Add(newKey, kvp.Value); - } - } - - foreach (var kvp in UidServerComments.ToList()) - { - var newKey = kvp.Key.Replace("wss://v2202207178628194299.powersrv.de:6871", "wss://v2202207178628194299.powersrv.de:6872"); - UidServerComments.Remove(kvp.Key); - UidServerComments.Add(newKey, kvp.Value); - } - - Version = 4; - Save(); - } - - if (Version == 4) - { - Logger.Debug("Migrating Configuration from V4 to V5"); - - ApiUri = ApiUri.Replace("wss://v2202207178628194299.powersrv.de:6872", "wss://maresynchronos.com"); - ClientSecret.Remove("wss://v2202207178628194299.powersrv.de:6872"); - UidServerComments.Remove("wss://v2202207178628194299.powersrv.de:6872"); - - Version = 5; - Save(); - } + Version = 5; + Save(); } } } diff --git a/MareSynchronos/Interop/Weapon.cs b/MareSynchronos/Interop/Weapon.cs index 5670471..b02cbf2 100644 --- a/MareSynchronos/Interop/Weapon.cs +++ b/MareSynchronos/Interop/Weapon.cs @@ -4,35 +4,34 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Interop.Structs; -namespace MareSynchronos.Interop +namespace MareSynchronos.Interop; + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct Weapon { - [StructLayout(LayoutKind.Explicit)] - public unsafe struct Weapon - { - [FieldOffset(0x18)] public IntPtr Parent; - [FieldOffset(0x20)] public IntPtr NextSibling; - [FieldOffset(0x28)] public IntPtr PreviousSibling; - [FieldOffset(0xA8)] public WeaponDrawObject* WeaponRenderModel; - } - - [StructLayout(LayoutKind.Explicit)] - public unsafe struct WeaponDrawObject - { - [FieldOffset(0x00)] public RenderModel* RenderModel; - } - - [StructLayout(LayoutKind.Explicit)] - public unsafe struct HumanExt - { - [FieldOffset(0x0)] public Human Human; - [FieldOffset(0x9E8)] public ResourceHandle* Decal; - [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; - } - - [StructLayout(LayoutKind.Explicit)] - public unsafe struct CharaExt - { - [FieldOffset(0x0)] public Character Character; - [FieldOffset(0x650)] public Character* Mount; - } + [FieldOffset(0x18)] public IntPtr Parent; + [FieldOffset(0x20)] public IntPtr NextSibling; + [FieldOffset(0x28)] public IntPtr PreviousSibling; + [FieldOffset(0xA8)] public WeaponDrawObject* WeaponRenderModel; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct WeaponDrawObject +{ + [FieldOffset(0x00)] public RenderModel* RenderModel; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct HumanExt +{ + [FieldOffset(0x0)] public Human Human; + [FieldOffset(0x9E8)] public ResourceHandle* Decal; + [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; +} + +[StructLayout(LayoutKind.Explicit)] +public unsafe struct CharaExt +{ + [FieldOffset(0x0)] public Character Character; + [FieldOffset(0x650)] public Character* Mount; } diff --git a/MareSynchronos/Localization/Strings.cs b/MareSynchronos/Localization/Strings.cs index e55f8e8..4f1da9c 100644 --- a/MareSynchronos/Localization/Strings.cs +++ b/MareSynchronos/Localization/Strings.cs @@ -1,68 +1,67 @@ using CheapLoc; -namespace MareSynchronos.Localization +namespace MareSynchronos.Localization; + +public static class Strings { - public static class Strings + public class ToSStrings { - public class ToSStrings - { - public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language"); - public readonly string AgreementLabel = Loc.Localize("AgreementLabel", "Agreement of Usage of Service"); - public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY"); + public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language"); + public readonly string AgreementLabel = Loc.Localize("AgreementLabel", "Agreement of Usage of Service"); + public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY"); - public readonly string Paragraph1 = Loc.Localize("Paragraph1", - "All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " + - "The plugin will exclusively upload the necessary mod files and not the whole mod."); + public readonly string Paragraph1 = Loc.Localize("Paragraph1", + "All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " + + "The plugin will exclusively upload the necessary mod files and not the whole mod."); - public readonly string Paragraph2 = Loc.Localize("Paragraph2", - "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " + - "Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " + - "Files present on the service that already represent your active mod files will not be uploaded again."); + public readonly string Paragraph2 = Loc.Localize("Paragraph2", + "If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " + + "Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " + + "Files present on the service that already represent your active mod files will not be uploaded again."); - public readonly string Paragraph3 = Loc.Localize("Paragraph3", - "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " + - "Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " + - "Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod."); + public readonly string Paragraph3 = Loc.Localize("Paragraph3", + "The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " + + "Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " + + "Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod."); - public readonly string Paragraph4 = Loc.Localize("Paragraph4", - "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone."); + public readonly string Paragraph4 = Loc.Localize("Paragraph4", + "The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone."); - public readonly string Paragraph5 = Loc.Localize("Paragraph5", - "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " + - "After a period of not being used, the mod files will be automatically deleted. " + - "You will also be able to wipe all the files you have personally uploaded on request. " + - "The service holds no information about which mod files belong to which mod."); + public readonly string Paragraph5 = Loc.Localize("Paragraph5", + "Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " + + "After a period of not being used, the mod files will be automatically deleted. " + + "You will also be able to wipe all the files you have personally uploaded on request. " + + "The service holds no information about which mod files belong to which mod."); - public readonly string Paragraph6 = Loc.Localize("Paragraph6", - "This service is provided as-is. In case of abuse, contact darkarchon#4313 on Discord or join the Mare Synchronos Discord. " + - "To accept those conditions hold CTRL while clicking 'I agree'"); + public readonly string Paragraph6 = Loc.Localize("Paragraph6", + "This service is provided as-is. In case of abuse, contact darkarchon#4313 on Discord or join the Mare Synchronos Discord. " + + "To accept those conditions hold CTRL while clicking 'I agree'"); - public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); + public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree"); - public readonly string RemainingLabel = Loc.Localize("RemainingLabel", "remaining"); + public readonly string RemainingLabel = Loc.Localize("RemainingLabel", "remaining"); - public readonly string FailedLabel = Loc.Localize("FailedLabel", - "Congratulations. You have failed to read the agreements."); + public readonly string FailedLabel = Loc.Localize("FailedLabel", + "Congratulations. You have failed to read the agreements."); - public readonly string TimeoutLabel = Loc.Localize("TimeoutLabel", - "I'm going to give you 1 minute to read the agreements carefully again. If you fail once more you will have to solve an annoying puzzle."); + public readonly string TimeoutLabel = Loc.Localize("TimeoutLabel", + "I'm going to give you 1 minute to read the agreements carefully again. If you fail once more you will have to solve an annoying puzzle."); - public readonly string FailedAgainLabel = Loc.Localize("FailedAgainLabel", - "Congratulations. You have failed to read the agreements. Again."); + public readonly string FailedAgainLabel = Loc.Localize("FailedAgainLabel", + "Congratulations. You have failed to read the agreements. Again."); - public readonly string PuzzleLabel = Loc.Localize("PuzzleLabel", - "I did warn you. Here's your annoying puzzle:"); + public readonly string PuzzleLabel = Loc.Localize("PuzzleLabel", + "I did warn you. Here's your annoying puzzle:"); - public readonly string PuzzleDescLabel = Loc.Localize("PuzzleDescLabel", - "Enter the following 3 words from the agreement exactly as described without punctuation to make the \"I agree\" button visible again."); + public readonly string PuzzleDescLabel = Loc.Localize("PuzzleDescLabel", + "Enter the following 3 words from the agreement exactly as described without punctuation to make the \"I agree\" button visible again."); - public readonly string ParagraphLabel = Loc.Localize("ParagraphLabel", "Paragraph"); + public readonly string ParagraphLabel = Loc.Localize("ParagraphLabel", "Paragraph"); - public readonly string SentenceLabel = Loc.Localize("SentenceLabel", "Sentence"); + public readonly string SentenceLabel = Loc.Localize("SentenceLabel", "Sentence"); - public readonly string WordLabel = Loc.Localize("WordLabel", "Word"); - } - - public static ToSStrings ToS { get; set; } = new(); + public readonly string WordLabel = Loc.Localize("WordLabel", "Word"); } + + public static ToSStrings ToS { get; set; } = new(); } \ No newline at end of file diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 47186af..e7c5882 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -8,394 +8,393 @@ using MareSynchronos.WebAPI; using Action = System.Action; using System.Collections.Concurrent; -namespace MareSynchronos.Managers +namespace MareSynchronos.Managers; + +public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); +public delegate void HeelsOffsetChange(float change); +public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); +public class IpcManager : IDisposable { - public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); - public delegate void HeelsOffsetChange(float change); - public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); - public class IpcManager : IDisposable + private readonly ICallGateSubscriber _glamourerApiVersion; + private readonly ICallGateSubscriber? _glamourerApplyAll; + private readonly ICallGateSubscriber? _glamourerGetAllCustomization; + private readonly ICallGateSubscriber _glamourerRevertCustomization; + private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; + private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; + private readonly ICallGateSubscriber<(int, int)> _penumbraApiVersion; + private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; + private readonly ICallGateSubscriber _penumbraGetMetaManipulations; + private readonly ICallGateSubscriber _penumbraInit; + private readonly ICallGateSubscriber _penumbraDispose; + private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; + private readonly ICallGateSubscriber? _penumbraRedraw; + private readonly ICallGateSubscriber? _penumbraRedrawObject; + private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; + private readonly ICallGateSubscriber? _penumbraResolveModDir; + private readonly ICallGateSubscriber? _penumbraResolvePlayer; + private readonly ICallGateSubscriber? _reverseResolvePlayer; + private readonly ICallGateSubscriber, string, int, int> + _penumbraSetTemporaryMod; + private readonly ICallGateSubscriber _penumbraGameObjectResourcePathResolved; + + private readonly ICallGateSubscriber _heelsGetApiVersion; + private readonly ICallGateSubscriber _heelsGetOffset; + private readonly ICallGateSubscriber _heelsOffsetUpdate; + private readonly ICallGateSubscriber _heelsRegisterPlayer; + private readonly ICallGateSubscriber _heelsUnregisterPlayer; + + private readonly DalamudUtil _dalamudUtil; + private readonly ConcurrentQueue actionQueue = new(); + + public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) { - private readonly ICallGateSubscriber _glamourerApiVersion; - private readonly ICallGateSubscriber? _glamourerApplyAll; - private readonly ICallGateSubscriber? _glamourerGetAllCustomization; - private readonly ICallGateSubscriber _glamourerRevertCustomization; - private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; - private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; - private readonly ICallGateSubscriber<(int, int)> _penumbraApiVersion; - private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; - private readonly ICallGateSubscriber _penumbraGetMetaManipulations; - private readonly ICallGateSubscriber _penumbraInit; - private readonly ICallGateSubscriber _penumbraDispose; - private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; - private readonly ICallGateSubscriber? _penumbraRedraw; - private readonly ICallGateSubscriber? _penumbraRedrawObject; - private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; - private readonly ICallGateSubscriber? _penumbraResolveModDir; - private readonly ICallGateSubscriber? _penumbraResolvePlayer; - private readonly ICallGateSubscriber? _reverseResolvePlayer; - private readonly ICallGateSubscriber, string, int, int> - _penumbraSetTemporaryMod; - private readonly ICallGateSubscriber _penumbraGameObjectResourcePathResolved; + Logger.Verbose("Creating " + nameof(IpcManager)); - private readonly ICallGateSubscriber _heelsGetApiVersion; - private readonly ICallGateSubscriber _heelsGetOffset; - private readonly ICallGateSubscriber _heelsOffsetUpdate; - private readonly ICallGateSubscriber _heelsRegisterPlayer; - private readonly ICallGateSubscriber _heelsUnregisterPlayer; + _penumbraInit = pi.GetIpcSubscriber("Penumbra.Initialized"); + _penumbraDispose = pi.GetIpcSubscriber("Penumbra.Disposed"); + _penumbraResolvePlayer = pi.GetIpcSubscriber("Penumbra.ResolvePlayerPath"); + _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); + _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); + _penumbraRedrawObject = pi.GetIpcSubscriber("Penumbra.RedrawObject"); + _reverseResolvePlayer = pi.GetIpcSubscriber("Penumbra.ReverseResolvePlayerPath"); + _penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions"); + _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); + _penumbraGetMetaManipulations = + pi.GetIpcSubscriber("Penumbra.GetPlayerMetaManipulations"); + _penumbraSetTemporaryMod = pi.GetIpcSubscriber, string, int, + int>("Penumbra.AddTemporaryMod"); + _penumbraCreateTemporaryCollection = + pi.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); + _penumbraRemoveTemporaryCollection = + pi.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); + _penumbraGameObjectResourcePathResolved = pi.GetIpcSubscriber("Penumbra.GameObjectResourcePathResolved"); - private readonly DalamudUtil _dalamudUtil; - private readonly ConcurrentQueue actionQueue = new(); + _penumbraGameObjectResourcePathResolved.Subscribe(ResourceLoaded); + _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); + _penumbraInit.Subscribe(PenumbraInit); + _penumbraDispose.Subscribe(PenumbraDispose); - public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) - { - Logger.Verbose("Creating " + nameof(IpcManager)); + _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); + _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); + _glamourerApplyAll = pi.GetIpcSubscriber("Glamourer.ApplyAllToCharacter"); + _glamourerApplyOnlyCustomization = pi.GetIpcSubscriber("Glamourer.ApplyOnlyCustomizationToCharacter"); + _glamourerApplyOnlyEquipment = pi.GetIpcSubscriber("Glamourer.ApplyOnlyEquipmentToCharacter"); + _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacter"); - _penumbraInit = pi.GetIpcSubscriber("Penumbra.Initialized"); - _penumbraDispose = pi.GetIpcSubscriber("Penumbra.Disposed"); - _penumbraResolvePlayer = pi.GetIpcSubscriber("Penumbra.ResolvePlayerPath"); - _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); - _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); - _penumbraRedrawObject = pi.GetIpcSubscriber("Penumbra.RedrawObject"); - _reverseResolvePlayer = pi.GetIpcSubscriber("Penumbra.ReverseResolvePlayerPath"); - _penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions"); - _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); - _penumbraGetMetaManipulations = - pi.GetIpcSubscriber("Penumbra.GetPlayerMetaManipulations"); - _penumbraSetTemporaryMod = pi.GetIpcSubscriber, string, int, - int>("Penumbra.AddTemporaryMod"); - _penumbraCreateTemporaryCollection = - pi.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); - _penumbraRemoveTemporaryCollection = - pi.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); - _penumbraGameObjectResourcePathResolved = pi.GetIpcSubscriber("Penumbra.GameObjectResourcePathResolved"); + _heelsGetApiVersion = pi.GetIpcSubscriber("HeelsPlugin.ApiVersion"); + _heelsGetOffset = pi.GetIpcSubscriber("HeelsPlugin.GetOffset"); + _heelsRegisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.RegisterPlayer"); + _heelsUnregisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.UnregisterPlayer"); + _heelsOffsetUpdate = pi.GetIpcSubscriber("HeelsPlugin.OffsetChanged"); - _penumbraGameObjectResourcePathResolved.Subscribe(ResourceLoaded); - _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); - _penumbraInit.Subscribe(PenumbraInit); - _penumbraDispose.Subscribe(PenumbraDispose); + _heelsOffsetUpdate.Subscribe(HeelsOffsetChange); - _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); - _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); - _glamourerApplyAll = pi.GetIpcSubscriber("Glamourer.ApplyAllToCharacter"); - _glamourerApplyOnlyCustomization = pi.GetIpcSubscriber("Glamourer.ApplyOnlyCustomizationToCharacter"); - _glamourerApplyOnlyEquipment = pi.GetIpcSubscriber("Glamourer.ApplyOnlyEquipmentToCharacter"); - _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacter"); - - _heelsGetApiVersion = pi.GetIpcSubscriber("HeelsPlugin.ApiVersion"); - _heelsGetOffset = pi.GetIpcSubscriber("HeelsPlugin.GetOffset"); - _heelsRegisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.RegisterPlayer"); - _heelsUnregisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.UnregisterPlayer"); - _heelsOffsetUpdate = pi.GetIpcSubscriber("HeelsPlugin.OffsetChanged"); - - _heelsOffsetUpdate.Subscribe(HeelsOffsetChange); - - if (Initialized) - { - PenumbraInitialized?.Invoke(); - } - - _dalamudUtil = dalamudUtil; - _dalamudUtil.FrameworkUpdate += HandleActionQueue; - _dalamudUtil.ZoneSwitchEnd += ClearActionQueue; - } - - private void ClearActionQueue() - { - actionQueue.Clear(); - } - - private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) - { - if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) - { - PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); - //Logger.Debug($"Resolved {ptr:X}: {arg1} => {arg2}"); - } - } - - private void HandleActionQueue() - { - if (actionQueue.TryDequeue(out var action)) - { - if (action == null) return; - Logger.Debug("Execution action in queue: " + action.Method); - action(); - } - } - - public event VoidDelegate? PenumbraInitialized; - public event VoidDelegate? PenumbraDisposed; - public event PenumbraRedrawEvent? PenumbraRedrawEvent; - public event HeelsOffsetChange? HeelsOffsetChangeEvent; - public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; - - public bool Initialized => CheckPenumbraApi(); - public bool CheckGlamourerApi() - { - try - { - return _glamourerApiVersion.InvokeFunc() >= 0; - } - catch - { - return false; - } - } - - public bool CheckPenumbraApi() - { - try - { - return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 13 }; - } - catch - { - return false; - } - } - - public bool CheckHeelsApi() - { - try - { - return _heelsGetApiVersion.InvokeFunc() == "1.0.1"; - } - catch - { - return false; - } - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(IpcManager)); - - int totalSleepTime = 0; - while (actionQueue.Count > 0 && totalSleepTime < 2000) - { - Logger.Verbose("Waiting for actionqueue to clear..."); - HandleActionQueue(); - System.Threading.Thread.Sleep(16); - totalSleepTime += 16; - } - - if (totalSleepTime >= 2000) - { - Logger.Verbose("Action queue clear or not, disposing"); - } - - _dalamudUtil.FrameworkUpdate -= HandleActionQueue; - _dalamudUtil.ZoneSwitchEnd -= ClearActionQueue; - actionQueue.Clear(); - - _penumbraDispose.Unsubscribe(PenumbraDispose); - _penumbraInit.Unsubscribe(PenumbraInit); - _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); - _penumbraGameObjectResourcePathResolved.Unsubscribe(ResourceLoaded); - _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); - } - - public float GetHeelsOffset() - { - if (!CheckHeelsApi()) return 0.0f; - return _heelsGetOffset.InvokeFunc(); - } - - public void HeelsSetOffsetForPlayer(float offset, IntPtr character) - { - if (!CheckHeelsApi()) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) - { - Logger.Verbose("Applying Heels data to " + character.ToString("X")); - _heelsRegisterPlayer.InvokeAction(gameObj, offset); - } - }); - } - - public void HeelsRestoreOffsetForPlayer(IntPtr character) - { - if (!CheckHeelsApi()) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) - { - Logger.Verbose("Restoring Heels data to " + character.ToString("X")); - _heelsUnregisterPlayer.InvokeAction(gameObj); - } - }); - } - - public void GlamourerApplyAll(string? customization, IntPtr obj) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(obj); - if (gameObj is Character c) - { - Logger.Verbose("Glamourer applying for " + c.Address.ToString("X")); - _glamourerApplyAll!.InvokeAction(customization, c); - } - }); - } - - public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X")); - _glamourerApplyOnlyEquipment!.InvokeAction(customization, c); - } - }); - } - - public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) - { - if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X")); - _glamourerApplyOnlyCustomization!.InvokeAction(customization, c); - } - }); - } - - public string GlamourerGetCharacterCustomization(IntPtr character) - { - if (!CheckGlamourerApi()) return string.Empty; - try - { - var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj is Character c) - { - var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); - byte[] bytes = Convert.FromBase64String(glamourerString); - // ignore transparency - bytes[88] = 128; - bytes[89] = 63; - return Convert.ToBase64String(bytes); - } - return string.Empty; - } - catch - { - return string.Empty; - } - } - - public void GlamourerRevertCharacterCustomization(GameObject character) - { - if (!CheckGlamourerApi()) return; - actionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); - } - - public string PenumbraGetMetaManipulations() - { - if (!CheckPenumbraApi()) return string.Empty; - return _penumbraGetMetaManipulations.InvokeFunc(); - } - - public string? PenumbraModDirectory() - { - if (!CheckPenumbraApi()) return null; - return _penumbraResolveModDir!.InvokeFunc().ToLowerInvariant(); - } - - public void PenumbraRedraw(IntPtr obj) - { - if (!CheckPenumbraApi()) return; - actionQueue.Enqueue(() => - { - var gameObj = _dalamudUtil.CreateGameObject(obj); - if (gameObj != null) - { - Logger.Verbose("Redrawing " + gameObj); - _penumbraRedrawObject!.InvokeAction(gameObj, 0); - } - }); - } - - public void PenumbraRedraw(string actorName) - { - if (!CheckPenumbraApi()) return; - actionQueue.Enqueue(() => _penumbraRedraw!.InvokeAction(actorName, 0)); - } - - public void PenumbraRemoveTemporaryCollection(string characterName) - { - if (!CheckPenumbraApi()) return; - actionQueue.Enqueue(() => - { - Logger.Verbose("Removing temp collection for " + characterName); - _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); - }); - } - - public string PenumbraResolvePath(string path) - { - if (!CheckPenumbraApi()) return path; - var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); - return resolvedPath ?? path; - } - - public string[] PenumbraReverseResolvePlayer(string path) - { - if (!CheckPenumbraApi()) return new[] { path }; - var resolvedPaths = _reverseResolvePlayer!.InvokeFunc(path); - if (resolvedPaths.Length == 0) - { - resolvedPaths = new[] { path }; - } - return resolvedPaths; - } - - public void PenumbraSetTemporaryMods(string characterName, Dictionary modPaths, string manipulationData) - { - if (!CheckPenumbraApi()) return; - - actionQueue.Enqueue(() => - { - var ret = _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); - Logger.Verbose("Assigning temp mods for " + ret.Item2); - foreach (var mod in modPaths) - { - Logger.Verbose(mod.Key + " => " + mod.Value); - } - _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", ret.Item2, modPaths, manipulationData, 0); - }); - } - - private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) - { - PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); - } - - private void PenumbraInit() + if (Initialized) { PenumbraInitialized?.Invoke(); - _penumbraRedraw!.InvokeAction("self", 0); } - private void HeelsOffsetChange(float offset) - { - HeelsOffsetChangeEvent?.Invoke(offset); - } + _dalamudUtil = dalamudUtil; + _dalamudUtil.FrameworkUpdate += HandleActionQueue; + _dalamudUtil.ZoneSwitchEnd += ClearActionQueue; + } - private void PenumbraDispose() + private void ClearActionQueue() + { + actionQueue.Clear(); + } + + private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) + { + if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) { - PenumbraDisposed?.Invoke(); - actionQueue.Clear(); + PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); + //Logger.Debug($"Resolved {ptr:X}: {arg1} => {arg2}"); } } + + private void HandleActionQueue() + { + if (actionQueue.TryDequeue(out var action)) + { + if (action == null) return; + Logger.Debug("Execution action in queue: " + action.Method); + action(); + } + } + + public event VoidDelegate? PenumbraInitialized; + public event VoidDelegate? PenumbraDisposed; + public event PenumbraRedrawEvent? PenumbraRedrawEvent; + public event HeelsOffsetChange? HeelsOffsetChangeEvent; + public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; + + public bool Initialized => CheckPenumbraApi(); + public bool CheckGlamourerApi() + { + try + { + return _glamourerApiVersion.InvokeFunc() >= 0; + } + catch + { + return false; + } + } + + public bool CheckPenumbraApi() + { + try + { + return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 13 }; + } + catch + { + return false; + } + } + + public bool CheckHeelsApi() + { + try + { + return _heelsGetApiVersion.InvokeFunc() == "1.0.1"; + } + catch + { + return false; + } + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(IpcManager)); + + int totalSleepTime = 0; + while (actionQueue.Count > 0 && totalSleepTime < 2000) + { + Logger.Verbose("Waiting for actionqueue to clear..."); + HandleActionQueue(); + System.Threading.Thread.Sleep(16); + totalSleepTime += 16; + } + + if (totalSleepTime >= 2000) + { + Logger.Verbose("Action queue clear or not, disposing"); + } + + _dalamudUtil.FrameworkUpdate -= HandleActionQueue; + _dalamudUtil.ZoneSwitchEnd -= ClearActionQueue; + actionQueue.Clear(); + + _penumbraDispose.Unsubscribe(PenumbraDispose); + _penumbraInit.Unsubscribe(PenumbraInit); + _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); + _penumbraGameObjectResourcePathResolved.Unsubscribe(ResourceLoaded); + _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); + } + + public float GetHeelsOffset() + { + if (!CheckHeelsApi()) return 0.0f; + return _heelsGetOffset.InvokeFunc(); + } + + public void HeelsSetOffsetForPlayer(float offset, IntPtr character) + { + if (!CheckHeelsApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Applying Heels data to " + character.ToString("X")); + _heelsRegisterPlayer.InvokeAction(gameObj, offset); + } + }); + } + + public void HeelsRestoreOffsetForPlayer(IntPtr character) + { + if (!CheckHeelsApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Restoring Heels data to " + character.ToString("X")); + _heelsUnregisterPlayer.InvokeAction(gameObj); + } + }); + } + + public void GlamourerApplyAll(string? customization, IntPtr obj) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(obj); + if (gameObj is Character c) + { + Logger.Verbose("Glamourer applying for " + c.Address.ToString("X")); + _glamourerApplyAll!.InvokeAction(customization, c); + } + }); + } + + public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X")); + _glamourerApplyOnlyEquipment!.InvokeAction(customization, c); + } + }); + } + + public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) + { + if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X")); + _glamourerApplyOnlyCustomization!.InvokeAction(customization, c); + } + }); + } + + public string GlamourerGetCharacterCustomization(IntPtr character) + { + if (!CheckGlamourerApi()) return string.Empty; + try + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is Character c) + { + var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); + byte[] bytes = Convert.FromBase64String(glamourerString); + // ignore transparency + bytes[88] = 128; + bytes[89] = 63; + return Convert.ToBase64String(bytes); + } + return string.Empty; + } + catch + { + return string.Empty; + } + } + + public void GlamourerRevertCharacterCustomization(GameObject character) + { + if (!CheckGlamourerApi()) return; + actionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); + } + + public string PenumbraGetMetaManipulations() + { + if (!CheckPenumbraApi()) return string.Empty; + return _penumbraGetMetaManipulations.InvokeFunc(); + } + + public string? PenumbraModDirectory() + { + if (!CheckPenumbraApi()) return null; + return _penumbraResolveModDir!.InvokeFunc().ToLowerInvariant(); + } + + public void PenumbraRedraw(IntPtr obj) + { + if (!CheckPenumbraApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(obj); + if (gameObj != null) + { + Logger.Verbose("Redrawing " + gameObj); + _penumbraRedrawObject!.InvokeAction(gameObj, 0); + } + }); + } + + public void PenumbraRedraw(string actorName) + { + if (!CheckPenumbraApi()) return; + actionQueue.Enqueue(() => _penumbraRedraw!.InvokeAction(actorName, 0)); + } + + public void PenumbraRemoveTemporaryCollection(string characterName) + { + if (!CheckPenumbraApi()) return; + actionQueue.Enqueue(() => + { + Logger.Verbose("Removing temp collection for " + characterName); + _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); + }); + } + + public string PenumbraResolvePath(string path) + { + if (!CheckPenumbraApi()) return path; + var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); + return resolvedPath ?? path; + } + + public string[] PenumbraReverseResolvePlayer(string path) + { + if (!CheckPenumbraApi()) return new[] { path }; + var resolvedPaths = _reverseResolvePlayer!.InvokeFunc(path); + if (resolvedPaths.Length == 0) + { + resolvedPaths = new[] { path }; + } + return resolvedPaths; + } + + public void PenumbraSetTemporaryMods(string characterName, Dictionary modPaths, string manipulationData) + { + if (!CheckPenumbraApi()) return; + + actionQueue.Enqueue(() => + { + var ret = _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); + Logger.Verbose("Assigning temp mods for " + ret.Item2); + foreach (var mod in modPaths) + { + Logger.Verbose(mod.Key + " => " + mod.Value); + } + _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", ret.Item2, modPaths, manipulationData, 0); + }); + } + + private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) + { + PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); + } + + private void PenumbraInit() + { + PenumbraInitialized?.Invoke(); + _penumbraRedraw!.InvokeAction("self", 0); + } + + private void HeelsOffsetChange(float offset) + { + HeelsOffsetChangeEvent?.Invoke(offset); + } + + private void PenumbraDispose() + { + PenumbraDisposed?.Invoke(); + actionQueue.Clear(); + } } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index 95eed2f..913f35e 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -14,258 +14,257 @@ using MareSynchronos.FileCache; using Newtonsoft.Json; #endif -namespace MareSynchronos.Managers +namespace MareSynchronos.Managers; + +public delegate void PlayerHasChanged(CharacterCacheDto characterCache); + +public class PlayerManager : IDisposable { - public delegate void PlayerHasChanged(CharacterCacheDto characterCache); + private readonly ApiController _apiController; + private readonly CharacterDataFactory _characterDataFactory; + private readonly DalamudUtil _dalamudUtil; + private readonly TransientResourceManager _transientResourceManager; + private readonly PeriodicFileScanner _periodicFileScanner; + private readonly IpcManager _ipcManager; + public event PlayerHasChanged? PlayerHasChanged; + public CharacterCacheDto? LastCreatedCharacterData { get; private set; } + public CharacterData PermanentDataCache { get; private set; } = new(); + private readonly Dictionary> objectKindsToUpdate = new(); - public class PlayerManager : IDisposable + private CancellationTokenSource? _playerChangedCts = new(); + private CancellationTokenSource _transientUpdateCts = new(); + + private List playerRelatedObjects = new List(); + + public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, + CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil, TransientResourceManager transientResourceManager, + PeriodicFileScanner periodicFileScanner) { - private readonly ApiController _apiController; - private readonly CharacterDataFactory _characterDataFactory; - private readonly DalamudUtil _dalamudUtil; - private readonly TransientResourceManager _transientResourceManager; - private readonly PeriodicFileScanner _periodicFileScanner; - private readonly IpcManager _ipcManager; - public event PlayerHasChanged? PlayerHasChanged; - public CharacterCacheDto? LastCreatedCharacterData { get; private set; } - public CharacterData PermanentDataCache { get; private set; } = new(); - private readonly Dictionary> objectKindsToUpdate = new(); + Logger.Verbose("Creating " + nameof(PlayerManager)); - private CancellationTokenSource? _playerChangedCts = new(); - private CancellationTokenSource _transientUpdateCts = new(); + _apiController = apiController; + _ipcManager = ipcManager; + _characterDataFactory = characterDataFactory; + _dalamudUtil = dalamudUtil; + _transientResourceManager = transientResourceManager; + _periodicFileScanner = periodicFileScanner; + _apiController.Connected += ApiControllerOnConnected; + _apiController.Disconnected += ApiController_Disconnected; + _transientResourceManager.TransientResourceLoaded += HandleTransientResourceLoad; + _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; + _ipcManager.HeelsOffsetChangeEvent += HeelsOffsetChanged; + _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - private List playerRelatedObjects = new List(); - public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, - CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil, TransientResourceManager transientResourceManager, - PeriodicFileScanner periodicFileScanner) + Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); + if (_apiController.IsConnected) { - Logger.Verbose("Creating " + nameof(PlayerManager)); - - _apiController = apiController; - _ipcManager = ipcManager; - _characterDataFactory = characterDataFactory; - _dalamudUtil = dalamudUtil; - _transientResourceManager = transientResourceManager; - _periodicFileScanner = periodicFileScanner; - _apiController.Connected += ApiControllerOnConnected; - _apiController.Disconnected += ApiController_Disconnected; - _transientResourceManager.TransientResourceLoaded += HandleTransientResourceLoad; - _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; - _ipcManager.HeelsOffsetChangeEvent += HeelsOffsetChanged; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - - - Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); - if (_apiController.IsConnected) - { - ApiControllerOnConnected(); - } - - playerRelatedObjects = new List() - { - new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), - new PlayerRelatedObject(ObjectKind.MinionOrMount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), - new PlayerRelatedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), - new PlayerRelatedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), - }; + ApiControllerOnConnected(); } - private void DalamudUtilOnFrameworkUpdate() + playerRelatedObjects = new List() { - _transientResourceManager.PlayerRelatedPointers = playerRelatedObjects.Select(f => f.CurrentAddress).ToArray(); - } + new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), + new PlayerRelatedObject(ObjectKind.MinionOrMount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), + new PlayerRelatedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), + new PlayerRelatedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), + }; + } - public void HandleTransientResourceLoad(IntPtr gameObj) + private void DalamudUtilOnFrameworkUpdate() + { + _transientResourceManager.PlayerRelatedPointers = playerRelatedObjects.Select(f => f.CurrentAddress).ToArray(); + } + + public void HandleTransientResourceLoad(IntPtr gameObj) + { + foreach (var obj in playerRelatedObjects) { - foreach (var obj in playerRelatedObjects) + if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) { - if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) + _transientUpdateCts.Cancel(); + _transientUpdateCts = new CancellationTokenSource(); + var token = _transientUpdateCts.Token; + Task.Run(async () => { - _transientUpdateCts.Cancel(); - _transientUpdateCts = new CancellationTokenSource(); - var token = _transientUpdateCts.Token; - Task.Run(async () => - { - Logger.Debug("Delaying transient resource load update"); - await Task.Delay(750, token); - if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return; - Logger.Debug("Firing transient resource load update"); - obj.HasTransientsUpdate = true; - }, token); + Logger.Debug("Delaying transient resource load update"); + await Task.Delay(750, token); + if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return; + Logger.Debug("Firing transient resource load update"); + obj.HasTransientsUpdate = true; + }, token); - return; - } - } - } - - private void HeelsOffsetChanged(float change) - { - var player = playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); - if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) - { - Logger.Debug("Heels offset changed to " + change); - playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasTransientsUpdate = true; - } - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(PlayerManager)); - - _apiController.Connected -= ApiControllerOnConnected; - _apiController.Disconnected -= ApiController_Disconnected; - - _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; - - _transientResourceManager.TransientResourceLoaded -= HandleTransientResourceLoad; - - _playerChangedCts?.Cancel(); - _ipcManager.HeelsOffsetChangeEvent -= HeelsOffsetChanged; - } - - private unsafe void DalamudUtilOnDelayedFrameworkUpdate() - { - if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; - - playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject()); - if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && !c.IsProcessing)) - { - OnPlayerOrAttachedObjectsChanged(); - } - } - - private void ApiControllerOnConnected() - { - Logger.Debug("ApiController Connected"); - - _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; - } - - private void ApiController_Disconnected() - { - Logger.Debug(nameof(ApiController_Disconnected)); - - _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - } - - private async Task CreateFullCharacterCacheDto(CancellationToken token) - { - foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList()) - { - Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); - PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token); - if (!token.IsCancellationRequested) - { - unprocessedObject.HasUnprocessedUpdate = false; - unprocessedObject.IsProcessing = false; - unprocessedObject.HasTransientsUpdate = false; - } - token.ThrowIfCancellationRequested(); - } - - while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) - { - Logger.Verbose("Waiting until cache is ready"); - await Task.Delay(50, token); - } - - if (token.IsCancellationRequested) return null; - - Logger.Verbose("Cache creation complete"); - - var cache = PermanentDataCache.ToCharacterCacheDto(); - //Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented)); - return cache; - } - - private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) - { - Logger.Verbose("RedrawEvent for addr " + address); - - foreach (var item in playerRelatedObjects) - { - if (address == item.Address) - { - Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); - item.HasUnprocessedUpdate = true; - } - } - - if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && (!c.IsProcessing || (c.IsProcessing && c.DoNotSendUpdate)))) - { - OnPlayerOrAttachedObjectsChanged(); - } - } - - private void OnPlayerOrAttachedObjectsChanged() - { - var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList(); - foreach (var unprocessedObject in unprocessedObjects) - { - unprocessedObject.IsProcessing = true; - } - Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); - bool doNotSendUpdate = unprocessedObjects.All(c => c.DoNotSendUpdate); - unprocessedObjects.ForEach(p => p.DoNotSendUpdate = false); - _playerChangedCts?.Cancel(); - _playerChangedCts = new CancellationTokenSource(); - var token = _playerChangedCts.Token; - - // fix for redraw from anamnesis - while ((!_dalamudUtil.IsPlayerPresent || _dalamudUtil.PlayerName == "--") && !token.IsCancellationRequested) - { - Logger.Debug("Waiting Until Player is Present"); - Thread.Sleep(100); - } - - if (token.IsCancellationRequested) - { - Logger.Debug("Cancelled"); return; } - - if (!_ipcManager.Initialized) - { - Logger.Warn("Penumbra not active, doing nothing."); - return; - } - - Task.Run(async () => - { - _periodicFileScanner.HaltScan("Character creation"); - foreach (var item in unprocessedObjects) - { - _dalamudUtil.WaitWhileCharacterIsDrawing("self " + item.ObjectKind.ToString(), item.Address, 10000, token); - } - - CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); - _periodicFileScanner.ResumeScan("Character creation"); - if (cacheDto == null || token.IsCancellationRequested) return; - -#if DEBUG - //var json = JsonConvert.SerializeObject(cacheDto, Formatting.Indented); - //Logger.Verbose(json); -#endif - - if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) - { - Logger.Debug("Not sending data, already sent"); - return; - } - else - { - LastCreatedCharacterData = cacheDto; - } - - if (_apiController.IsConnected && !token.IsCancellationRequested && !doNotSendUpdate) - { - Logger.Verbose("Invoking PlayerHasChanged"); - PlayerHasChanged?.Invoke(cacheDto); - } - }, token); } } + + private void HeelsOffsetChanged(float change) + { + var player = playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); + if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) + { + Logger.Debug("Heels offset changed to " + change); + playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasTransientsUpdate = true; + } + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(PlayerManager)); + + _apiController.Connected -= ApiControllerOnConnected; + _apiController.Disconnected -= ApiController_Disconnected; + + _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; + _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; + _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; + + _transientResourceManager.TransientResourceLoaded -= HandleTransientResourceLoad; + + _playerChangedCts?.Cancel(); + _ipcManager.HeelsOffsetChangeEvent -= HeelsOffsetChanged; + } + + private unsafe void DalamudUtilOnDelayedFrameworkUpdate() + { + if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; + + playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject()); + if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && !c.IsProcessing)) + { + OnPlayerOrAttachedObjectsChanged(); + } + } + + private void ApiControllerOnConnected() + { + Logger.Debug("ApiController Connected"); + + _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; + } + + private void ApiController_Disconnected() + { + Logger.Debug(nameof(ApiController_Disconnected)); + + _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; + } + + private async Task CreateFullCharacterCacheDto(CancellationToken token) + { + foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList()) + { + Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); + PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token); + if (!token.IsCancellationRequested) + { + unprocessedObject.HasUnprocessedUpdate = false; + unprocessedObject.IsProcessing = false; + unprocessedObject.HasTransientsUpdate = false; + } + token.ThrowIfCancellationRequested(); + } + + while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) + { + Logger.Verbose("Waiting until cache is ready"); + await Task.Delay(50, token); + } + + if (token.IsCancellationRequested) return null; + + Logger.Verbose("Cache creation complete"); + + var cache = PermanentDataCache.ToCharacterCacheDto(); + //Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented)); + return cache; + } + + private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) + { + Logger.Verbose("RedrawEvent for addr " + address); + + foreach (var item in playerRelatedObjects) + { + if (address == item.Address) + { + Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); + item.HasUnprocessedUpdate = true; + } + } + + if (playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && (!c.IsProcessing || (c.IsProcessing && c.DoNotSendUpdate)))) + { + OnPlayerOrAttachedObjectsChanged(); + } + } + + private void OnPlayerOrAttachedObjectsChanged() + { + var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList(); + foreach (var unprocessedObject in unprocessedObjects) + { + unprocessedObject.IsProcessing = true; + } + Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); + bool doNotSendUpdate = unprocessedObjects.All(c => c.DoNotSendUpdate); + unprocessedObjects.ForEach(p => p.DoNotSendUpdate = false); + _playerChangedCts?.Cancel(); + _playerChangedCts = new CancellationTokenSource(); + var token = _playerChangedCts.Token; + + // fix for redraw from anamnesis + while ((!_dalamudUtil.IsPlayerPresent || _dalamudUtil.PlayerName == "--") && !token.IsCancellationRequested) + { + Logger.Debug("Waiting Until Player is Present"); + Thread.Sleep(100); + } + + if (token.IsCancellationRequested) + { + Logger.Debug("Cancelled"); + return; + } + + if (!_ipcManager.Initialized) + { + Logger.Warn("Penumbra not active, doing nothing."); + return; + } + + Task.Run(async () => + { + _periodicFileScanner.HaltScan("Character creation"); + foreach (var item in unprocessedObjects) + { + _dalamudUtil.WaitWhileCharacterIsDrawing("self " + item.ObjectKind.ToString(), item.Address, 10000, token); + } + + CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); + _periodicFileScanner.ResumeScan("Character creation"); + if (cacheDto == null || token.IsCancellationRequested) return; + +#if DEBUG + //var json = JsonConvert.SerializeObject(cacheDto, Formatting.Indented); + //Logger.Verbose(json); +#endif + + if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) + { + Logger.Debug("Not sending data, already sent"); + return; + } + else + { + LastCreatedCharacterData = cacheDto; + } + + if (_apiController.IsConnected && !token.IsCancellationRequested && !doNotSendUpdate) + { + Logger.Verbose("Invoking PlayerHasChanged"); + PlayerHasChanged?.Invoke(cacheDto); + } + }, token); + } } diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/Managers/TransientResourceManager.cs index 560355f..12668ab 100644 --- a/MareSynchronos/Managers/TransientResourceManager.cs +++ b/MareSynchronos/Managers/TransientResourceManager.cs @@ -8,198 +8,197 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MareSynchronos.Managers +namespace MareSynchronos.Managers; + +public delegate void TransientResourceLoadedEvent(IntPtr drawObject); + +public class TransientResourceManager : IDisposable { - public delegate void TransientResourceLoadedEvent(IntPtr drawObject); + private readonly IpcManager manager; + private readonly DalamudUtil dalamudUtil; - public class TransientResourceManager : IDisposable + public event TransientResourceLoadedEvent? TransientResourceLoaded; + public IntPtr[] PlayerRelatedPointers = Array.Empty(); + private readonly string[] FileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp" }; + + private ConcurrentDictionary> TransientResources { get; } = new(); + private ConcurrentDictionary> SemiTransientResources { get; } = new(); + public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) { - private readonly IpcManager manager; - private readonly DalamudUtil dalamudUtil; + manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; + this.manager = manager; + this.dalamudUtil = dalamudUtil; + dalamudUtil.FrameworkUpdate += DalamudUtil_FrameworkUpdate; + dalamudUtil.ClassJobChanged += DalamudUtil_ClassJobChanged; + } - public event TransientResourceLoadedEvent? TransientResourceLoaded; - public IntPtr[] PlayerRelatedPointers = Array.Empty(); - private readonly string[] FileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp" }; - - private ConcurrentDictionary> TransientResources { get; } = new(); - private ConcurrentDictionary> SemiTransientResources { get; } = new(); - public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) + private void DalamudUtil_ClassJobChanged() + { + if (SemiTransientResources.ContainsKey(ObjectKind.Pet)) { - manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; - this.manager = manager; - this.dalamudUtil = dalamudUtil; - dalamudUtil.FrameworkUpdate += DalamudUtil_FrameworkUpdate; - dalamudUtil.ClassJobChanged += DalamudUtil_ClassJobChanged; + SemiTransientResources[ObjectKind.Pet].Clear(); } + } - private void DalamudUtil_ClassJobChanged() + private void DalamudUtil_FrameworkUpdate() + { + foreach (var item in TransientResources.ToList()) { - if (SemiTransientResources.ContainsKey(ObjectKind.Pet)) + if (!dalamudUtil.IsGameObjectPresent(item.Key)) { - SemiTransientResources[ObjectKind.Pet].Clear(); - } - } - - private void DalamudUtil_FrameworkUpdate() - { - foreach (var item in TransientResources.ToList()) - { - if (!dalamudUtil.IsGameObjectPresent(item.Key)) - { - Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); - TransientResources.TryRemove(item.Key, out _); - } - } - } - - public void CleanSemiTransientResources(ObjectKind objectKind) - { - if (SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind].Clear(); - } - } - - public List GetTransientResources(IntPtr gameObject) - { - if (TransientResources.TryGetValue(gameObject, out var result)) - { - return result.ToList(); - } - - return new List(); - } - - public List GetSemiTransientResources(ObjectKind objectKind) - { - if (SemiTransientResources.TryGetValue(objectKind, out var result)) - { - return result.ToList(); - } - - return new List(); - } - - private void Manager_PenumbraResourceLoadEvent(IntPtr gameObject, string gamePath, string filePath) - { - if (!FileTypesToHandle.Any(type => gamePath.ToLowerInvariant().EndsWith(type))) - { - return; - } - if (!PlayerRelatedPointers.Contains(gameObject)) - { - return; - } - - if (!TransientResources.ContainsKey(gameObject)) - { - TransientResources[gameObject] = new(); - } - - if (filePath.StartsWith("|")) - { - filePath = filePath.Split("|")[2]; - } - - filePath = filePath.ToLowerInvariant().Replace("\\", "/"); - - var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/"); - - if (TransientResources[gameObject].Contains(replacedGamePath) || - SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath - && f.ResolvedPath.ToLowerInvariant() == filePath))) - { - Logger.Debug("Not adding " + replacedGamePath + ":" + filePath); - Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath - && f.ResolvedPath.ToLowerInvariant() == filePath)).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath)); - } - else - { - TransientResources[gameObject].Add(replacedGamePath); - Logger.Debug($"Adding {replacedGamePath} for {gameObject} ({filePath})"); - TransientResourceLoaded?.Invoke(gameObject); - } - } - - public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) - { - if (TransientResources.ContainsKey(gameObject)) - { - TransientResources[gameObject].RemoveWhere(f => fileReplacement.GamePaths.Any(g => g.ToLowerInvariant() == f.ToLowerInvariant())); - } - } - - public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func createFileReplacement) - { - if (!SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind] = new HashSet(); - } - - if (!TransientResources.TryGetValue(gameObject, out var resources)) - { - return; - } - - var transientResources = resources.ToList(); - Logger.Debug("Persisting " + transientResources.Count + " transient resources"); - foreach (var gamePath in transientResources) - { - var existingResource = SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == gamePath.ToLowerInvariant()); - if (existingResource) - { - Logger.Debug("Semi Transient resource replaced: " + gamePath); - SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == gamePath.ToLowerInvariant()); - } - - try - { - var fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), true); - if (!fileReplacement.HasFileReplacement) - fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), false); - if (fileReplacement.HasFileReplacement) - { - Logger.Debug("Persisting " + gamePath.ToLowerInvariant()); - if (SemiTransientResources[objectKind].Add(fileReplacement)) - { - Logger.Debug("Added " + fileReplacement); - } - else - { - Logger.Debug("Not added " + fileReplacement); - } - } - } - catch (Exception ex) - { - Logger.Warn("Issue during transient file persistence"); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace.ToString()); - } - } - - TransientResources[gameObject].Clear(); - } - - public void Dispose() - { - dalamudUtil.FrameworkUpdate -= DalamudUtil_FrameworkUpdate; - manager.PenumbraResourceLoadEvent -= Manager_PenumbraResourceLoadEvent; - dalamudUtil.ClassJobChanged -= DalamudUtil_ClassJobChanged; - TransientResources.Clear(); - } - - internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) - { - if (!SemiTransientResources.ContainsKey(objectKind)) - { - SemiTransientResources[objectKind] = new HashSet(); - } - - if (!SemiTransientResources[objectKind].Any(f => f.ResolvedPath.ToLowerInvariant() == item.ResolvedPath.ToLowerInvariant())) - { - SemiTransientResources[objectKind].Add(item); + Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); + TransientResources.TryRemove(item.Key, out _); } } } + + public void CleanSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind].Clear(); + } + } + + public List GetTransientResources(IntPtr gameObject) + { + if (TransientResources.TryGetValue(gameObject, out var result)) + { + return result.ToList(); + } + + return new List(); + } + + public List GetSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.TryGetValue(objectKind, out var result)) + { + return result.ToList(); + } + + return new List(); + } + + private void Manager_PenumbraResourceLoadEvent(IntPtr gameObject, string gamePath, string filePath) + { + if (!FileTypesToHandle.Any(type => gamePath.ToLowerInvariant().EndsWith(type))) + { + return; + } + if (!PlayerRelatedPointers.Contains(gameObject)) + { + return; + } + + if (!TransientResources.ContainsKey(gameObject)) + { + TransientResources[gameObject] = new(); + } + + if (filePath.StartsWith("|")) + { + filePath = filePath.Split("|")[2]; + } + + filePath = filePath.ToLowerInvariant().Replace("\\", "/"); + + var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/"); + + if (TransientResources[gameObject].Contains(replacedGamePath) || + SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath + && f.ResolvedPath.ToLowerInvariant() == filePath))) + { + Logger.Debug("Not adding " + replacedGamePath + ":" + filePath); + Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath + && f.ResolvedPath.ToLowerInvariant() == filePath)).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath)); + } + else + { + TransientResources[gameObject].Add(replacedGamePath); + Logger.Debug($"Adding {replacedGamePath} for {gameObject} ({filePath})"); + TransientResourceLoaded?.Invoke(gameObject); + } + } + + public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) + { + if (TransientResources.ContainsKey(gameObject)) + { + TransientResources[gameObject].RemoveWhere(f => fileReplacement.GamePaths.Any(g => g.ToLowerInvariant() == f.ToLowerInvariant())); + } + } + + public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func createFileReplacement) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(); + } + + if (!TransientResources.TryGetValue(gameObject, out var resources)) + { + return; + } + + var transientResources = resources.ToList(); + Logger.Debug("Persisting " + transientResources.Count + " transient resources"); + foreach (var gamePath in transientResources) + { + var existingResource = SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == gamePath.ToLowerInvariant()); + if (existingResource) + { + Logger.Debug("Semi Transient resource replaced: " + gamePath); + SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == gamePath.ToLowerInvariant()); + } + + try + { + var fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), true); + if (!fileReplacement.HasFileReplacement) + fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), false); + if (fileReplacement.HasFileReplacement) + { + Logger.Debug("Persisting " + gamePath.ToLowerInvariant()); + if (SemiTransientResources[objectKind].Add(fileReplacement)) + { + Logger.Debug("Added " + fileReplacement); + } + else + { + Logger.Debug("Not added " + fileReplacement); + } + } + } + catch (Exception ex) + { + Logger.Warn("Issue during transient file persistence"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace.ToString()); + } + } + + TransientResources[gameObject].Clear(); + } + + public void Dispose() + { + dalamudUtil.FrameworkUpdate -= DalamudUtil_FrameworkUpdate; + manager.PenumbraResourceLoadEvent -= Manager_PenumbraResourceLoadEvent; + dalamudUtil.ClassJobChanged -= DalamudUtil_ClassJobChanged; + TransientResources.Clear(); + } + + internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(); + } + + if (!SemiTransientResources[objectKind].Any(f => f.ResolvedPath.ToLowerInvariant() == item.ResolvedPath.ToLowerInvariant())) + { + SemiTransientResources[objectKind].Add(item); + } + } } diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index c0c349f..b0edfc4 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -6,83 +6,82 @@ using MareSynchronos.API; using MareSynchronos.Utils; using Lumina.Excel.GeneratedSheets; -namespace MareSynchronos.Models +namespace MareSynchronos.Models; + +[JsonObject(MemberSerialization.OptIn)] +public class CharacterData { - [JsonObject(MemberSerialization.OptIn)] - public class CharacterData + [JsonProperty] + public Dictionary> FileReplacements { get; set; } = new(); + + [JsonProperty] + public Dictionary GlamourerString { get; set; } = new(); + + public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); + + [JsonProperty] + public string ManipulationString { get; set; } = string.Empty; + + [JsonProperty] + public float HeelsOffset { get; set; } = 0f; + + public void AddFileReplacement(ObjectKind objectKind, FileReplacement fileReplacement) { - [JsonProperty] - public Dictionary> FileReplacements { get; set; } = new(); + if (!fileReplacement.HasFileReplacement) return; - [JsonProperty] - public Dictionary GlamourerString { get; set; } = new(); + if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); - public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); - - [JsonProperty] - public string ManipulationString { get; set; } = string.Empty; - - [JsonProperty] - public float HeelsOffset { get; set; } = 0f; - - public void AddFileReplacement(ObjectKind objectKind, FileReplacement fileReplacement) + var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); + if (existingReplacement != null) { - if (!fileReplacement.HasFileReplacement) return; - - if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); - - var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); - if (existingReplacement != null) - { - existingReplacement.GamePaths.AddRange(fileReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e))); - } - else - { - FileReplacements[objectKind].Add(fileReplacement); - } + existingReplacement.GamePaths.AddRange(fileReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e))); } - - public CharacterCacheDto ToCharacterCacheDto() + else { - var fileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap).GroupBy(f => f.Hash).Select(g => - { - return new FileReplacementDto() - { - GamePaths = g.SelectMany(f => f.GamePaths).Distinct().ToArray(), - Hash = g.First().Hash, - }; - }).ToList()); - - Logger.Debug("Adding fileSwaps"); - foreach (var item in FileReplacements) - { - Logger.Debug("Checking fileSwaps for " + item.Key); - var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); - Logger.Debug("Adding " + fileSwapsToAdd.Count() + " file swaps"); - foreach (var swap in fileSwapsToAdd) - { - Logger.Debug("Adding: " + swap.GamePaths.First() + ":" + swap.FileSwapPath); - } - fileReplacements[item.Key].AddRange(fileSwapsToAdd); - } - - return new CharacterCacheDto() - { - FileReplacements = fileReplacements, - GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value), - ManipulationData = ManipulationString, - HeelsOffset = HeelsOffset - }; - } - - public override string ToString() - { - StringBuilder stringBuilder = new(); - foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths[0])) - { - stringBuilder.AppendLine(fileReplacement.ToString()); - } - return stringBuilder.ToString(); + FileReplacements[objectKind].Add(fileReplacement); } } + + public CharacterCacheDto ToCharacterCacheDto() + { + var fileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap).GroupBy(f => f.Hash).Select(g => + { + return new FileReplacementDto() + { + GamePaths = g.SelectMany(f => f.GamePaths).Distinct().ToArray(), + Hash = g.First().Hash, + }; + }).ToList()); + + Logger.Debug("Adding fileSwaps"); + foreach (var item in FileReplacements) + { + Logger.Debug("Checking fileSwaps for " + item.Key); + var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); + Logger.Debug("Adding " + fileSwapsToAdd.Count() + " file swaps"); + foreach (var swap in fileSwapsToAdd) + { + Logger.Debug("Adding: " + swap.GamePaths.First() + ":" + swap.FileSwapPath); + } + fileReplacements[item.Key].AddRange(fileSwapsToAdd); + } + + return new CharacterCacheDto() + { + FileReplacements = fileReplacements, + GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value), + ManipulationData = ManipulationString, + HeelsOffset = HeelsOffset + }; + } + + public override string ToString() + { + StringBuilder stringBuilder = new(); + foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths[0])) + { + stringBuilder.AppendLine(fileReplacement.ToString()); + } + return stringBuilder.ToString(); + } } diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index d4b2406..a231afa 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -6,55 +6,54 @@ using MareSynchronos.API; using System.Text.RegularExpressions; using MareSynchronos.FileCache; -namespace MareSynchronos.Models +namespace MareSynchronos.Models; + +public class FileReplacement { - public class FileReplacement + private readonly FileCacheManager fileDbManager; + + public FileReplacement(FileCacheManager fileDbManager) { - private readonly FileCacheManager fileDbManager; + this.fileDbManager = fileDbManager; + } - public FileReplacement(FileCacheManager fileDbManager) + public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); + + public List GamePaths { get; set; } = new(); + + public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => p != ResolvedPath); + + public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && GamePaths.First() != ResolvedPath; + + public string Hash { get; set; } = string.Empty; + + public string ResolvedPath { get; set; } = string.Empty; + + public void SetResolvedPath(string path) + { + ResolvedPath = path.ToLowerInvariant().Replace('\\', '/'); + if (!HasFileReplacement || IsFileSwap) return; + + _ = Task.Run(() => { - this.fileDbManager = fileDbManager; - } + var cache = fileDbManager.GetFileCacheByPath(ResolvedPath); + Hash = cache.Hash; + }); + } - public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); - - public List GamePaths { get; set; } = new(); - - public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => p != ResolvedPath); - - public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && GamePaths.First() != ResolvedPath; - - public string Hash { get; set; } = string.Empty; - - public string ResolvedPath { get; set; } = string.Empty; - - public void SetResolvedPath(string path) + public FileReplacementDto ToFileReplacementDto() + { + return new FileReplacementDto { - ResolvedPath = path.ToLowerInvariant().Replace('\\', '/'); - if (!HasFileReplacement || IsFileSwap) return; - - _ = Task.Run(() => - { - var cache = fileDbManager.GetFileCacheByPath(ResolvedPath); - Hash = cache.Hash; - }); - } - - public FileReplacementDto ToFileReplacementDto() - { - return new FileReplacementDto - { - GamePaths = GamePaths.ToArray(), - Hash = Hash, - FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty - }; - } - public override string ToString() - { - StringBuilder builder = new(); - builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); - return builder.ToString(); - } + GamePaths = GamePaths.ToArray(), + Hash = Hash, + FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty + }; + } + public override string ToString() + { + StringBuilder builder = new(); + builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); + return builder.ToString(); } } diff --git a/MareSynchronos/Models/PlayerRelatedObject.cs b/MareSynchronos/Models/PlayerRelatedObject.cs index f91b221..d425891 100644 --- a/MareSynchronos/Models/PlayerRelatedObject.cs +++ b/MareSynchronos/Models/PlayerRelatedObject.cs @@ -5,132 +5,131 @@ using System.Runtime.InteropServices; using MareSynchronos.Utils; using Penumbra.GameData.ByteString; -namespace MareSynchronos.Models +namespace MareSynchronos.Models; + +public class PlayerRelatedObject { - public class PlayerRelatedObject + private readonly Func getAddress; + + public unsafe Character* Character => (Character*)Address; + + private string _name; + + public ObjectKind ObjectKind { get; } + public IntPtr Address { get; set; } + public IntPtr DrawObjectAddress { get; set; } + + public IntPtr CurrentAddress { - private readonly Func getAddress; - - public unsafe Character* Character => (Character*)Address; - - private string _name; - - public ObjectKind ObjectKind { get; } - public IntPtr Address { get; set; } - public IntPtr DrawObjectAddress { get; set; } - - public IntPtr CurrentAddress + get { - get + try { - try - { - return getAddress.Invoke(); - } - catch - { return IntPtr.Zero; } + return getAddress.Invoke(); } - } - - public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) - { - ObjectKind = objectKind; - Address = address; - DrawObjectAddress = drawObjectAddress; - this.getAddress = getAddress; - _name = string.Empty; - } - - public byte[] EquipSlotData { get; set; } = new byte[40]; - public byte[] CustomizeData { get; set; } = new byte[26]; - public byte? HatState { get; set; } - public byte? VisorWeaponState { get; set; } - - public bool HasTransientsUpdate { get; set; } = false; - public bool HasUnprocessedUpdate { get; set; } = false; - public bool DoNotSendUpdate { get; set; } = false; - public bool IsProcessing { get; set; } = false; - - public unsafe void CheckAndUpdateObject() - { - var curPtr = CurrentAddress; - if (curPtr != IntPtr.Zero) - { - var chara = (Character*)curPtr; - bool addr = Address == IntPtr.Zero || Address != curPtr; - bool equip = CompareAndUpdateByteData(chara->EquipSlotData, chara->CustomizeData); - bool drawObj = (IntPtr)chara->GameObject.DrawObject != DrawObjectAddress; - var name = new Utf8String(chara->GameObject.Name).ToString(); - bool nameChange = (name != _name); - if (addr || equip || drawObj || nameChange) - { - _name = name; - Logger.Verbose($"{ObjectKind} changed: {_name}, now: {curPtr:X}, {(IntPtr)chara->GameObject.DrawObject:X}"); - - Address = curPtr; - DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; - HasUnprocessedUpdate = true; - } - } - else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) - { - Address = IntPtr.Zero; - DrawObjectAddress = IntPtr.Zero; - Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + Address + ", " + DrawObjectAddress); - } - } - - private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) - { - bool hasChanges = false; - DoNotSendUpdate = false; - for (int i = 0; i < EquipSlotData.Length; i++) - { - var data = Marshal.ReadByte((IntPtr)equipSlotData, i); - if (EquipSlotData[i] != data) - { - EquipSlotData[i] = data; - hasChanges = true; - } - } - - for (int i = 0; i < CustomizeData.Length; i++) - { - var data = Marshal.ReadByte((IntPtr)customizeData, i); - if (CustomizeData[i] != data) - { - CustomizeData[i] = data; - hasChanges = true; - } - } - - var newHatState = Marshal.ReadByte((IntPtr)customizeData + 30, 0); - var newWeaponOrVisorState = Marshal.ReadByte((IntPtr)customizeData + 31, 0); - if (newHatState != HatState) - { - if (HatState != null && !hasChanges && !HasUnprocessedUpdate) - { - Logger.Debug("Not Sending Update, only Hat changed"); - DoNotSendUpdate = true; - } - HatState = newHatState; - hasChanges = true; - } - - newWeaponOrVisorState &= 0b1101; // ignore drawing weapon - - if (newWeaponOrVisorState != VisorWeaponState) - { - if (VisorWeaponState != null && !hasChanges && !HasUnprocessedUpdate) - { - Logger.Debug("Not Sending Update, only Visor/Weapon changed"); - DoNotSendUpdate = true; - } - VisorWeaponState = newWeaponOrVisorState; - hasChanges = true; - } - - return hasChanges; + catch + { return IntPtr.Zero; } } } + + public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + { + ObjectKind = objectKind; + Address = address; + DrawObjectAddress = drawObjectAddress; + this.getAddress = getAddress; + _name = string.Empty; + } + + public byte[] EquipSlotData { get; set; } = new byte[40]; + public byte[] CustomizeData { get; set; } = new byte[26]; + public byte? HatState { get; set; } + public byte? VisorWeaponState { get; set; } + + public bool HasTransientsUpdate { get; set; } = false; + public bool HasUnprocessedUpdate { get; set; } = false; + public bool DoNotSendUpdate { get; set; } = false; + public bool IsProcessing { get; set; } = false; + + public unsafe void CheckAndUpdateObject() + { + var curPtr = CurrentAddress; + if (curPtr != IntPtr.Zero) + { + var chara = (Character*)curPtr; + bool addr = Address == IntPtr.Zero || Address != curPtr; + bool equip = CompareAndUpdateByteData(chara->EquipSlotData, chara->CustomizeData); + bool drawObj = (IntPtr)chara->GameObject.DrawObject != DrawObjectAddress; + var name = new Utf8String(chara->GameObject.Name).ToString(); + bool nameChange = (name != _name); + if (addr || equip || drawObj || nameChange) + { + _name = name; + Logger.Verbose($"{ObjectKind} changed: {_name}, now: {curPtr:X}, {(IntPtr)chara->GameObject.DrawObject:X}"); + + Address = curPtr; + DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; + HasUnprocessedUpdate = true; + } + } + else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) + { + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + Address + ", " + DrawObjectAddress); + } + } + + private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) + { + bool hasChanges = false; + DoNotSendUpdate = false; + for (int i = 0; i < EquipSlotData.Length; i++) + { + var data = Marshal.ReadByte((IntPtr)equipSlotData, i); + if (EquipSlotData[i] != data) + { + EquipSlotData[i] = data; + hasChanges = true; + } + } + + for (int i = 0; i < CustomizeData.Length; i++) + { + var data = Marshal.ReadByte((IntPtr)customizeData, i); + if (CustomizeData[i] != data) + { + CustomizeData[i] = data; + hasChanges = true; + } + } + + var newHatState = Marshal.ReadByte((IntPtr)customizeData + 30, 0); + var newWeaponOrVisorState = Marshal.ReadByte((IntPtr)customizeData + 31, 0); + if (newHatState != HatState) + { + if (HatState != null && !hasChanges && !HasUnprocessedUpdate) + { + Logger.Debug("Not Sending Update, only Hat changed"); + DoNotSendUpdate = true; + } + HatState = newHatState; + hasChanges = true; + } + + newWeaponOrVisorState &= 0b1101; // ignore drawing weapon + + if (newWeaponOrVisorState != VisorWeaponState) + { + if (VisorWeaponState != null && !hasChanges && !HasUnprocessedUpdate) + { + Logger.Debug("Not Sending Update, only Visor/Weapon changed"); + DoNotSendUpdate = true; + } + VisorWeaponState = newWeaponOrVisorState; + hasChanges = true; + } + + return hasChanges; + } } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 74d5bf8..fbee90d 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -15,204 +15,203 @@ using MareSynchronos.Utils; using Dalamud.Game.ClientState.Conditions; using MareSynchronos.FileCache; -namespace MareSynchronos +namespace MareSynchronos; + +public sealed class Plugin : IDalamudPlugin { - public sealed class Plugin : IDalamudPlugin + private const string CommandName = "/mare"; + private readonly ApiController _apiController; + private readonly CommandManager _commandManager; + private readonly Configuration _configuration; + private readonly PeriodicFileScanner _periodicFileScanner; + private readonly IntroUi _introUi; + private readonly IpcManager _ipcManager; + private readonly DalamudPluginInterface _pluginInterface; + private readonly SettingsUi _settingsUi; + private readonly WindowSystem _windowSystem; + private PlayerManager? _playerManager; + private TransientResourceManager? _transientResourceManager; + private readonly DalamudUtil _dalamudUtil; + private OnlinePlayerManager? _characterCacheManager; + private readonly DownloadUi _downloadUi; + private readonly FileDialogManager _fileDialogManager; + private readonly FileCacheManager _fileDbManager; + private readonly CompactUi _compactUi; + private readonly UiShared _uiSharedComponent; + private readonly Dalamud.Localization _localization; + + + public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, + Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition) { - private const string CommandName = "/mare"; - private readonly ApiController _apiController; - private readonly CommandManager _commandManager; - private readonly Configuration _configuration; - private readonly PeriodicFileScanner _periodicFileScanner; - private readonly IntroUi _introUi; - private readonly IpcManager _ipcManager; - private readonly DalamudPluginInterface _pluginInterface; - private readonly SettingsUi _settingsUi; - private readonly WindowSystem _windowSystem; - private PlayerManager? _playerManager; - private TransientResourceManager? _transientResourceManager; - private readonly DalamudUtil _dalamudUtil; - private OnlinePlayerManager? _characterCacheManager; - private readonly DownloadUi _downloadUi; - private readonly FileDialogManager _fileDialogManager; - private readonly FileCacheManager _fileDbManager; - private readonly CompactUi _compactUi; - private readonly UiShared _uiSharedComponent; - private readonly Dalamud.Localization _localization; + Logger.Debug("Launching " + Name); + _pluginInterface = pluginInterface; + _commandManager = commandManager; + _configuration = _pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + _configuration.Initialize(_pluginInterface); + _configuration.Migrate(); + _localization = new Dalamud.Localization("MareSynchronos.Localization.", "", true); + _localization.SetupWithLangCode("en"); - public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, - Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition) + _windowSystem = new WindowSystem("MareSynchronos"); + + // those can be initialized outside of game login + _dalamudUtil = new DalamudUtil(clientState, objectTable, framework, condition); + + _ipcManager = new IpcManager(_pluginInterface, _dalamudUtil); + _fileDialogManager = new FileDialogManager(); + _fileDbManager = new FileCacheManager(_ipcManager, _configuration, _pluginInterface.ConfigDirectory.FullName); + _apiController = new ApiController(_configuration, _dalamudUtil, _fileDbManager); + _periodicFileScanner = new PeriodicFileScanner(_ipcManager, _configuration, _fileDbManager, _apiController, _dalamudUtil); + + _uiSharedComponent = + new UiShared(_ipcManager, _apiController, _periodicFileScanner, _fileDialogManager, _configuration, _dalamudUtil, _pluginInterface, _localization); + _settingsUi = new SettingsUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); + _compactUi = new CompactUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); + + _introUi = new IntroUi(_windowSystem, _uiSharedComponent, _configuration, _periodicFileScanner); + _settingsUi.SwitchToIntroUi += () => { - Logger.Debug("Launching " + Name); - _pluginInterface = pluginInterface; - _commandManager = commandManager; - _configuration = _pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - _configuration.Initialize(_pluginInterface); - _configuration.Migrate(); - - _localization = new Dalamud.Localization("MareSynchronos.Localization.", "", true); - _localization.SetupWithLangCode("en"); - - _windowSystem = new WindowSystem("MareSynchronos"); - - // those can be initialized outside of game login - _dalamudUtil = new DalamudUtil(clientState, objectTable, framework, condition); - - _ipcManager = new IpcManager(_pluginInterface, _dalamudUtil); - _fileDialogManager = new FileDialogManager(); - _fileDbManager = new FileCacheManager(_ipcManager, _configuration, _pluginInterface.ConfigDirectory.FullName); - _apiController = new ApiController(_configuration, _dalamudUtil, _fileDbManager); - _periodicFileScanner = new PeriodicFileScanner(_ipcManager, _configuration, _fileDbManager, _apiController, _dalamudUtil); - - _uiSharedComponent = - new UiShared(_ipcManager, _apiController, _periodicFileScanner, _fileDialogManager, _configuration, _dalamudUtil, _pluginInterface, _localization); - _settingsUi = new SettingsUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); - _compactUi = new CompactUi(_windowSystem, _uiSharedComponent, _configuration, _apiController); - - _introUi = new IntroUi(_windowSystem, _uiSharedComponent, _configuration, _periodicFileScanner); - _settingsUi.SwitchToIntroUi += () => - { - _introUi.IsOpen = true; - _settingsUi.IsOpen = false; - _compactUi.IsOpen = false; - }; - _introUi.SwitchToMainUi += () => - { - _introUi.IsOpen = false; - _compactUi.IsOpen = true; - _periodicFileScanner.StartScan(); - ReLaunchCharacterManager(); - }; - _compactUi.OpenSettingsUi += () => - { - _settingsUi.Toggle(); - }; - _downloadUi = new DownloadUi(_windowSystem, _configuration, _apiController, _uiSharedComponent); - - - _dalamudUtil.LogIn += DalamudUtilOnLogIn; - _dalamudUtil.LogOut += DalamudUtilOnLogOut; - - if (_dalamudUtil.IsLoggedIn) - { - DalamudUtilOnLogIn(); - } - } - - public string Name => "Mare Synchronos"; - public void Dispose() + _introUi.IsOpen = true; + _settingsUi.IsOpen = false; + _compactUi.IsOpen = false; + }; + _introUi.SwitchToMainUi += () => { - Logger.Verbose("Disposing " + Name); - _apiController?.Dispose(); - - _commandManager.RemoveHandler(CommandName); - _dalamudUtil.LogIn -= DalamudUtilOnLogIn; - _dalamudUtil.LogOut -= DalamudUtilOnLogOut; - - _uiSharedComponent.Dispose(); - _settingsUi?.Dispose(); - _introUi?.Dispose(); - _downloadUi?.Dispose(); - _compactUi?.Dispose(); - - _periodicFileScanner?.Dispose(); - _fileDbManager?.Dispose(); - _playerManager?.Dispose(); - _characterCacheManager?.Dispose(); - _ipcManager?.Dispose(); - _transientResourceManager?.Dispose(); - _dalamudUtil.Dispose(); - Logger.Debug("Shut down"); - } - - - private void DalamudUtilOnLogIn() - { - Logger.Debug("Client login"); - - _pluginInterface.UiBuilder.Draw += Draw; - _pluginInterface.UiBuilder.OpenConfigUi += OpenUi; - _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) - { - HelpMessage = "Opens the Mare Synchronos UI" - }); - - if (!_configuration.HasValidSetup()) - { - _introUi.IsOpen = true; - _configuration.FullPause = false; - _configuration.Save(); - return; - } - + _introUi.IsOpen = false; + _compactUi.IsOpen = true; _periodicFileScanner.StartScan(); ReLaunchCharacterManager(); - } - - private void DalamudUtilOnLogOut() + }; + _compactUi.OpenSettingsUi += () => { - Logger.Debug("Client logout"); - _characterCacheManager?.Dispose(); - _playerManager?.Dispose(); - _transientResourceManager?.Dispose(); - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenUi; - _commandManager.RemoveHandler(CommandName); - } + _settingsUi.Toggle(); + }; + _downloadUi = new DownloadUi(_windowSystem, _configuration, _apiController, _uiSharedComponent); - public void ReLaunchCharacterManager() + + _dalamudUtil.LogIn += DalamudUtilOnLogIn; + _dalamudUtil.LogOut += DalamudUtilOnLogOut; + + if (_dalamudUtil.IsLoggedIn) { - _characterCacheManager?.Dispose(); - _playerManager?.Dispose(); - _transientResourceManager?.Dispose(); - - Task.Run(WaitForPlayerAndLaunchCharacterManager); - } - - private async Task WaitForPlayerAndLaunchCharacterManager() - { - while (!_dalamudUtil.IsPlayerPresent) - { - await Task.Delay(100); - } - - try - { - _transientResourceManager = new TransientResourceManager(_ipcManager, _dalamudUtil); - var characterCacheFactory = - new CharacterDataFactory(_dalamudUtil, _ipcManager, _transientResourceManager, _fileDbManager); - _playerManager = new PlayerManager(_apiController, _ipcManager, - characterCacheFactory, _dalamudUtil, _transientResourceManager, _periodicFileScanner); - _characterCacheManager = new OnlinePlayerManager(_apiController, - _dalamudUtil, _ipcManager, _playerManager, _fileDbManager); - } - catch (Exception ex) - { - Logger.Debug(ex.Message); - } - } - - private void Draw() - { - _windowSystem.Draw(); - _fileDialogManager.Draw(); - } - - private void OnCommand(string command, string args) - { - if (string.IsNullOrEmpty(args)) - { - OpenUi(); - } - } - - private void OpenUi() - { - if (_configuration.HasValidSetup()) - _compactUi.Toggle(); - else - _introUi.Toggle(); + DalamudUtilOnLogIn(); } } + + public string Name => "Mare Synchronos"; + public void Dispose() + { + Logger.Verbose("Disposing " + Name); + _apiController?.Dispose(); + + _commandManager.RemoveHandler(CommandName); + _dalamudUtil.LogIn -= DalamudUtilOnLogIn; + _dalamudUtil.LogOut -= DalamudUtilOnLogOut; + + _uiSharedComponent.Dispose(); + _settingsUi?.Dispose(); + _introUi?.Dispose(); + _downloadUi?.Dispose(); + _compactUi?.Dispose(); + + _periodicFileScanner?.Dispose(); + _fileDbManager?.Dispose(); + _playerManager?.Dispose(); + _characterCacheManager?.Dispose(); + _ipcManager?.Dispose(); + _transientResourceManager?.Dispose(); + _dalamudUtil.Dispose(); + Logger.Debug("Shut down"); + } + + + private void DalamudUtilOnLogIn() + { + Logger.Debug("Client login"); + + _pluginInterface.UiBuilder.Draw += Draw; + _pluginInterface.UiBuilder.OpenConfigUi += OpenUi; + _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the Mare Synchronos UI" + }); + + if (!_configuration.HasValidSetup()) + { + _introUi.IsOpen = true; + _configuration.FullPause = false; + _configuration.Save(); + return; + } + + _periodicFileScanner.StartScan(); + ReLaunchCharacterManager(); + } + + private void DalamudUtilOnLogOut() + { + Logger.Debug("Client logout"); + _characterCacheManager?.Dispose(); + _playerManager?.Dispose(); + _transientResourceManager?.Dispose(); + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenUi; + _commandManager.RemoveHandler(CommandName); + } + + public void ReLaunchCharacterManager() + { + _characterCacheManager?.Dispose(); + _playerManager?.Dispose(); + _transientResourceManager?.Dispose(); + + Task.Run(WaitForPlayerAndLaunchCharacterManager); + } + + private async Task WaitForPlayerAndLaunchCharacterManager() + { + while (!_dalamudUtil.IsPlayerPresent) + { + await Task.Delay(100); + } + + try + { + _transientResourceManager = new TransientResourceManager(_ipcManager, _dalamudUtil); + var characterCacheFactory = + new CharacterDataFactory(_dalamudUtil, _ipcManager, _transientResourceManager, _fileDbManager); + _playerManager = new PlayerManager(_apiController, _ipcManager, + characterCacheFactory, _dalamudUtil, _transientResourceManager, _periodicFileScanner); + _characterCacheManager = new OnlinePlayerManager(_apiController, + _dalamudUtil, _ipcManager, _playerManager, _fileDbManager); + } + catch (Exception ex) + { + Logger.Debug(ex.Message); + } + } + + private void Draw() + { + _windowSystem.Draw(); + _fileDialogManager.Draw(); + } + + private void OnCommand(string command, string args) + { + if (string.IsNullOrEmpty(args)) + { + OpenUi(); + } + } + + private void OpenUi() + { + if (_configuration.HasValidSetup()) + _compactUi.Toggle(); + else + _introUi.Toggle(); + } } diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 6818fd2..cdc9758 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -14,550 +14,549 @@ using MareSynchronos.API; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +public class CompactUi : Window, IDisposable { - public class CompactUi : Window, IDisposable + private readonly ApiController _apiController; + private readonly Configuration _configuration; + private readonly Dictionary _showUidForEntry = new(); + private readonly UiShared _uiShared; + private readonly WindowSystem _windowSystem; + private string _characterOrCommentFilter = string.Empty; + + private string _editCharComment = string.Empty; + private string _editNickEntry = string.Empty; + private string _pairToAdd = string.Empty; + private readonly Stopwatch _timeout = new(); + private bool _buttonState; + + private float _transferPartHeight = 0; + + private float _windowContentWidth = 0; + + public CompactUi(WindowSystem windowSystem, + UiShared uiShared, Configuration configuration, ApiController apiController) : base("###MareSynchronosMainUI") { - private readonly ApiController _apiController; - private readonly Configuration _configuration; - private readonly Dictionary _showUidForEntry = new(); - private readonly UiShared _uiShared; - private readonly WindowSystem _windowSystem; - private string _characterOrCommentFilter = string.Empty; - - private string _editCharComment = string.Empty; - private string _editNickEntry = string.Empty; - private string _pairToAdd = string.Empty; - private readonly Stopwatch _timeout = new(); - private bool _buttonState; - - private float _transferPartHeight = 0; - - private float _windowContentWidth = 0; - - public CompactUi(WindowSystem windowSystem, - UiShared uiShared, Configuration configuration, ApiController apiController) : base("###MareSynchronosMainUI") - { #if DEBUG - string dateTime = "DEV VERSION"; - try - { - dateTime = VariousExtensions.GetLinkerTime(Assembly.GetCallingAssembly()).ToString("yyyyMMddHHmmss"); - } - catch (Exception ex) - { - Logger.Warn("Could not get assembly name"); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace); - } - this.WindowName = "Mare Synchronos " + dateTime + "###MareSynchronosMainUI"; - Toggle(); + string dateTime = "DEV VERSION"; + try + { + dateTime = VariousExtensions.GetLinkerTime(Assembly.GetCallingAssembly()).ToString("yyyyMMddHHmmss"); + } + catch (Exception ex) + { + Logger.Warn("Could not get assembly name"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace); + } + this.WindowName = "Mare Synchronos " + dateTime + "###MareSynchronosMainUI"; + Toggle(); #else - this.WindowName = "Mare Synchronos " + Assembly.GetExecutingAssembly().GetName().Version; + this.WindowName = "Mare Synchronos " + Assembly.GetExecutingAssembly().GetName().Version; #endif - Logger.Verbose("Creating " + nameof(CompactUi)); + Logger.Verbose("Creating " + nameof(CompactUi)); - _windowSystem = windowSystem; - _uiShared = uiShared; - _configuration = configuration; - _apiController = apiController; + _windowSystem = windowSystem; + _uiShared = uiShared; + _configuration = configuration; + _apiController = apiController; - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(300, 400), - MaximumSize = new Vector2(300, 2000), - }; - - windowSystem.AddWindow(this); - } - - public event SwitchUi? OpenSettingsUi; - public void Dispose() + SizeConstraints = new WindowSizeConstraints() { - Logger.Verbose("Disposing " + nameof(CompactUi)); - _windowSystem.RemoveWindow(this); - } + MinimumSize = new Vector2(300, 400), + MaximumSize = new Vector2(300, 2000), + }; - public override void Draw() + windowSystem.AddWindow(this); + } + + public event SwitchUi? OpenSettingsUi; + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(CompactUi)); + _windowSystem.RemoveWindow(this); + } + + public override void Draw() + { + _windowContentWidth = UiShared.GetWindowContentRegionWidth(); + UiShared.DrawWithID("header", DrawUIDHeader); + ImGui.Separator(); + UiShared.DrawWithID("serverstatus", DrawServerStatus); + + if (_apiController.ServerState is ServerState.Connected) { - _windowContentWidth = UiShared.GetWindowContentRegionWidth(); - UiShared.DrawWithID("header", DrawUIDHeader); ImGui.Separator(); - UiShared.DrawWithID("serverstatus", DrawServerStatus); + UiShared.DrawWithID("pairlist", DrawPairList); + ImGui.Separator(); + UiShared.DrawWithID("transfers", DrawTransfers); + _transferPartHeight = ImGui.GetCursorPosY() - _transferPartHeight; + } + } - if (_apiController.ServerState is ServerState.Connected) + public override void OnClose() + { + _editNickEntry = string.Empty; + _editCharComment = string.Empty; + base.OnClose(); + } + private void DrawAddPair() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + ImGui.InputTextWithHint("##otheruid", "Other players UID", ref _pairToAdd, 10); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + { + if (_apiController.PairedClients.All(w => w.OtherUID != _pairToAdd)) { - ImGui.Separator(); - UiShared.DrawWithID("pairlist", DrawPairList); - ImGui.Separator(); - UiShared.DrawWithID("transfers", DrawTransfers); - _transferPartHeight = ImGui.GetCursorPosY() - _transferPartHeight; + _ = _apiController.SendPairedClientAddition(_pairToAdd); + _pairToAdd = string.Empty; } } + UiShared.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); - public override void OnClose() + ImGuiHelpers.ScaledDummy(2); + } + + private void DrawFilter() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.ArrowUp); + var playButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Play); + if (!_configuration.ReverseUserSort) { - _editNickEntry = string.Empty; - _editCharComment = string.Empty; - base.OnClose(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown)) + { + _configuration.ReverseUserSort = true; + _configuration.Save(); + } + UiShared.AttachToolTip("Sort by newest additions first"); } - private void DrawAddPair() + else { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Plus); - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); - ImGui.InputTextWithHint("##otheruid", "Other players UID", ref _pairToAdd, 10); - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) + if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowUp)) { - if (_apiController.PairedClients.All(w => w.OtherUID != _pairToAdd)) - { - _ = _apiController.SendPairedClientAddition(_pairToAdd); - _pairToAdd = string.Empty; - } + _configuration.ReverseUserSort = false; + _configuration.Save(); } - UiShared.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); - - ImGuiHelpers.ScaledDummy(2); + UiShared.AttachToolTip("Sort by oldest additions first"); } + ImGui.SameLine(); + + var users = GetFilteredUsers().ToList(); + var userCount = users.Count; + + var spacing = userCount > 0 + ? playButtonSize.X + ImGui.GetStyle().ItemSpacing.X * 2 + : ImGui.GetStyle().ItemSpacing.X; + + ImGui.SetNextItemWidth(_windowContentWidth - buttonSize.X - spacing); + ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref _characterOrCommentFilter, 255); - private void DrawFilter() + if (userCount == 0) return; + ImGui.SameLine(); + + var pausedUsers = users.Where(u => u.IsPaused).ToList(); + var resumedUsers = users.Where(u => !u.IsPaused).ToList(); + + switch (_buttonState) { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.ArrowUp); - var playButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Play); - if (!_configuration.ReverseUserSort) - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowDown)) - { - _configuration.ReverseUserSort = true; - _configuration.Save(); - } - UiShared.AttachToolTip("Sort by newest additions first"); - } - else - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowUp)) - { - _configuration.ReverseUserSort = false; - _configuration.Save(); - } - UiShared.AttachToolTip("Sort by oldest additions first"); - } - ImGui.SameLine(); - - var users = GetFilteredUsers().ToList(); - var userCount = users.Count; - - var spacing = userCount > 0 - ? playButtonSize.X + ImGui.GetStyle().ItemSpacing.X * 2 - : ImGui.GetStyle().ItemSpacing.X; - - ImGui.SetNextItemWidth(_windowContentWidth - buttonSize.X - spacing); - ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref _characterOrCommentFilter, 255); - - if (userCount == 0) return; - ImGui.SameLine(); - - var pausedUsers = users.Where(u => u.IsPaused).ToList(); - var resumedUsers = users.Where(u => !u.IsPaused).ToList(); - - switch (_buttonState) - { - case true when !pausedUsers.Any(): - _buttonState = false; - break; - case false when !resumedUsers.Any(): - _buttonState = true; - break; - case true: - users = pausedUsers; - break; - case false: - users = resumedUsers; - break; - } - - var button = _buttonState ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - - if (!_timeout.IsRunning || _timeout.ElapsedMilliseconds > 15000) - { - _timeout.Reset(); - - if (ImGuiComponents.IconButton(button)) - { - if (UiShared.CtrlPressed()) - { - Logger.Debug(users.Count.ToString()); - foreach (var entry in users) - { - _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); - } - - _timeout.Start(); - _buttonState = !_buttonState; - } - } - UiShared.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); - } - else - { - var availableAt = (15000 - _timeout.ElapsedMilliseconds) / 1000; - ImGuiComponents.DisabledButton(button); - UiShared.AttachToolTip($"Next execution is available at {availableAt} seconds"); - } + case true when !pausedUsers.Any(): + _buttonState = false; + break; + case false when !resumedUsers.Any(): + _buttonState = true; + break; + case true: + users = pausedUsers; + break; + case false: + users = resumedUsers; + break; } - - private void DrawPairedClient(ClientPairDto entry) + + var button = _buttonState ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + + if (!_timeout.IsRunning || _timeout.ElapsedMilliseconds > 15000) { - var pauseIcon = entry.IsPaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - - var buttonSize = UiShared.GetIconButtonSize(pauseIcon); - var trashButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Trash); - var entryUID = string.IsNullOrEmpty(entry.VanityUID) ? entry.OtherUID : entry.VanityUID; - var textSize = ImGui.CalcTextSize(entryUID); - var originalY = ImGui.GetCursorPosY(); - var buttonSizes = buttonSize.Y + trashButtonSize.Y; - - var textPos = originalY + buttonSize.Y / 2 - textSize.Y / 2; - ImGui.SetCursorPosY(textPos); - if (!entry.IsSynced) - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed); - ImGui.PopFont(); - - UiShared.AttachToolTip(entryUID + " has not added you back"); - } - else if (entry.IsPaused || entry.IsPausedFromOthers) - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); - ImGui.PopFont(); - - UiShared.AttachToolTip("Pairing status with " + entryUID + " is paused"); - } - else - { - ImGui.PushFont(UiBuilder.IconFont); - UiShared.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); - ImGui.PopFont(); - - UiShared.AttachToolTip("You are paired with " + entryUID); - } - - var textIsUid = true; - _showUidForEntry.TryGetValue(entry.OtherUID, out var showUidInsteadOfName); - if (!showUidInsteadOfName && _configuration.GetCurrentServerUidComments().TryGetValue(entry.OtherUID, out var playerText)) - { - if (string.IsNullOrEmpty(playerText)) - { - playerText = entryUID; - } - else - { - textIsUid = false; - } - } - else - { - playerText = entryUID; - } - - ImGui.SameLine(); - if (_editNickEntry != entry.OtherUID) - { - ImGui.SetCursorPosY(textPos); - if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(playerText); - if (textIsUid) ImGui.PopFont(); - UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + - "Right click to change nick for " + entryUID); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - var prevState = textIsUid; - if (_showUidForEntry.ContainsKey(entry.OtherUID)) - { - prevState = _showUidForEntry[entry.OtherUID]; - } - - _showUidForEntry[entry.OtherUID] = !prevState; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _configuration.SetCurrentServerUidComment(_editNickEntry, _editCharComment); - _configuration.Save(); - _editCharComment = _configuration.GetCurrentServerUidComments().ContainsKey(entry.OtherUID) - ? _configuration.GetCurrentServerUidComments()[entry.OtherUID] - : string.Empty; - _editNickEntry = entry.OtherUID; - } - } - else - { - ImGui.SetCursorPosY(originalY); - - ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); - if (ImGui.InputTextWithHint("", "Nick/Notes", ref _editCharComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - _configuration.SetCurrentServerUidComment(entry.OtherUID, _editCharComment); - _configuration.Save(); - _editNickEntry = string.Empty; - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _editNickEntry = string.Empty; - } - UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); - } - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - ImGui.SetCursorPosY(originalY); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + _timeout.Reset(); + + if (ImGuiComponents.IconButton(button)) { if (UiShared.CtrlPressed()) { - _ = _apiController.SendPairedClientRemoval(entry.OtherUID); - _apiController.PairedClients.Remove(entry); + Logger.Debug(users.Count.ToString()); + foreach (var entry in users) + { + _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); + } + + _timeout.Start(); + _buttonState = !_buttonState; } } - UiShared.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); - - if (entry.IsSynced) - { - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X - ImGui.GetStyle().ItemSpacing.X - trashButtonSize.X); - ImGui.SetCursorPosY(originalY); - if (ImGuiComponents.IconButton(pauseIcon)) - { - _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); - } - UiShared.AttachToolTip(!entry.IsPaused - ? "Pause pairing with " + entryUID - : "Resume pairing with " + entryUID); - } + UiShared.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); } - - private void DrawPairList() + else { - UiShared.DrawWithID("addpair", DrawAddPair); - UiShared.DrawWithID("pairs", DrawPairs); - _transferPartHeight = ImGui.GetCursorPosY(); - UiShared.DrawWithID("filter", DrawFilter); - } - - private void DrawPairs() - { - var ySize = _transferPartHeight == 0 - ? 1 - : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _transferPartHeight - ImGui.GetCursorPosY(); - var users = GetFilteredUsers(); - - if (_configuration.ReverseUserSort) users = users.Reverse(); - - ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), false); - foreach (var entry in users.ToList()) - { - UiShared.DrawWithID(entry.OtherUID, () => DrawPairedClient(entry)); - } - ImGui.EndChild(); - } - - private IEnumerable GetFilteredUsers() - { - return _apiController.PairedClients.Where(p => - { - if (_characterOrCommentFilter.IsNullOrEmpty()) return true; - _configuration.GetCurrentServerUidComments().TryGetValue(p.OtherUID, out var comment); - var uid = p.VanityUID.IsNullOrEmpty() ? p.OtherUID : p.VanityUID; - return uid.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) || - (comment?.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) ?? false); - }); - } - - private void DrawServerStatus() - { - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Link); - var userCount = _apiController.OnlineUsers.ToString(); - var userSize = ImGui.CalcTextSize(userCount); - var textSize = ImGui.CalcTextSize("Users Online"); - - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2); - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("Users Online"); - } - else - { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server"); - } - - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - var color = UiShared.GetBoolColor(!_configuration.FullPause); - var connectedIcon = !_configuration.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; - - ImGui.PushStyleColor(ImGuiCol.Text, color); - if (ImGuiComponents.IconButton(connectedIcon)) - { - _configuration.FullPause = !_configuration.FullPause; - _configuration.Save(); - _ = _apiController.CreateConnections(); - } - ImGui.PopStyleColor(); - UiShared.AttachToolTip(!_configuration.FullPause ? "Disconnect from " + _apiController.ServerDictionary[_configuration.ApiUri] : "Connect to " + _apiController.ServerDictionary[_configuration.ApiUri]); - } - - private void DrawTransfers() - { - var currentUploads = _apiController.CurrentUploads.ToList(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Upload.ToIconString()); - ImGui.PopFont(); - ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); - - if (currentUploads.Any()) - { - var totalUploads = currentUploads.Count; - - var doneUploads = currentUploads.Count(c => c.IsTransferred); - var totalUploaded = currentUploads.Sum(c => c.Transferred); - var totalToUpload = currentUploads.Sum(c => c.Total); - - ImGui.Text($"{doneUploads}/{totalUploads}"); - var uploadText = $"({UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)})"; - var textSize = ImGui.CalcTextSize(uploadText); - ImGui.SameLine(_windowContentWidth - textSize.X); - ImGui.Text(uploadText); - } - else - { - ImGui.Text("No uploads in progress"); - } - - var currentDownloads = _apiController.CurrentDownloads.SelectMany(k => k.Value).ToList(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(FontAwesomeIcon.Download.ToIconString()); - ImGui.PopFont(); - ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); - - if (currentDownloads.Any()) - { - var totalDownloads = currentDownloads.Count(); - var doneDownloads = currentDownloads.Count(c => c.IsTransferred); - var totalDownloaded = currentDownloads.Sum(c => c.Transferred); - var totalToDownload = currentDownloads.Sum(c => c.Total); - - ImGui.Text($"{doneDownloads}/{totalDownloads}"); - var downloadText = - $"({UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)})"; - var textSize = ImGui.CalcTextSize(downloadText); - ImGui.SameLine(_windowContentWidth - textSize.X); - ImGui.Text(downloadText); - } - else - { - ImGui.Text("No downloads in progress"); - } - - ImGui.SameLine(); - } - - private void DrawUIDHeader() - { - var uidText = GetUidText(); - var buttonSizeX = 0f; - - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - var uidTextSize = ImGui.CalcTextSize(uidText); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - - var originalPos = ImGui.GetCursorPos(); - ImGui.SetWindowFontScale(1.5f); - var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Cog); - buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2; - ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); - ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) - { - OpenSettingsUi?.Invoke(); - } - UiShared.AttachToolTip("Open the Mare Synchronos Settings"); - - ImGui.SameLine(); //Important to draw the uidText consistently - ImGui.SetCursorPos(originalPos); - - if (_apiController.ServerState is ServerState.Connected) - { - buttonSizeX += UiShared.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; - ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) - { - ImGui.SetClipboardText(_apiController.UID); - } - UiShared.AttachToolTip("Copy your UID to clipboard"); - ImGui.SameLine(); - } - ImGui.SetWindowFontScale(1f); - - ImGui.SetCursorPosY(originalPos.Y + buttonSize.Y / 2 - uidTextSize.Y / 2 - ImGui.GetStyle().ItemSpacing.Y / 2); - ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 + buttonSizeX - uidTextSize.X / 2); - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextColored(GetUidColor(), uidText); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - - if (_apiController.ServerState is not ServerState.Connected) - { - UiShared.ColorTextWrapped(GetServerError(), GetUidColor()); - } - } - - - private string GetServerError() - { - return _apiController.ServerState switch - { - ServerState.Disconnected => "You are currently disconnected from the Mare Synchronos server.", - ServerState.Unauthorized => "Your account is not present on the server anymore or you are banned.", - ServerState.Offline => "Your selected Mare Synchronos server is currently offline.", - ServerState.VersionMisMatch => - "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", - ServerState.RateLimited => "You are rate limited for (re)connecting too often. Wait and try again later.", - ServerState.Connected => string.Empty, - _ => string.Empty - }; - } - - private Vector4 GetUidColor() - { - return _apiController.ServerState switch - { - ServerState.Connected => ImGuiColors.ParsedGreen, - ServerState.Disconnected => ImGuiColors.DalamudYellow, - ServerState.Unauthorized => ImGuiColors.DalamudRed, - ServerState.VersionMisMatch => ImGuiColors.DalamudRed, - ServerState.Offline => ImGuiColors.DalamudRed, - ServerState.RateLimited => ImGuiColors.DalamudYellow, - _ => ImGuiColors.DalamudRed - }; - } - - private string GetUidText() - { - return _apiController.ServerState switch - { - ServerState.Disconnected => "Disconnected", - ServerState.Unauthorized => "Unauthorized", - ServerState.VersionMisMatch => "Version mismatch", - ServerState.Offline => "Unavailable", - ServerState.RateLimited => "Rate Limited", - ServerState.Connected => _apiController.UID, - _ => string.Empty - }; + var availableAt = (15000 - _timeout.ElapsedMilliseconds) / 1000; + ImGuiComponents.DisabledButton(button); + UiShared.AttachToolTip($"Next execution is available at {availableAt} seconds"); } } + + private void DrawPairedClient(ClientPairDto entry) + { + var pauseIcon = entry.IsPaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + + var buttonSize = UiShared.GetIconButtonSize(pauseIcon); + var trashButtonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Trash); + var entryUID = string.IsNullOrEmpty(entry.VanityUID) ? entry.OtherUID : entry.VanityUID; + var textSize = ImGui.CalcTextSize(entryUID); + var originalY = ImGui.GetCursorPosY(); + var buttonSizes = buttonSize.Y + trashButtonSize.Y; + + var textPos = originalY + buttonSize.Y / 2 - textSize.Y / 2; + ImGui.SetCursorPosY(textPos); + if (!entry.IsSynced) + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), ImGuiColors.DalamudRed); + ImGui.PopFont(); + + UiShared.AttachToolTip(entryUID + " has not added you back"); + } + else if (entry.IsPaused || entry.IsPausedFromOthers) + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); + ImGui.PopFont(); + + UiShared.AttachToolTip("Pairing status with " + entryUID + " is paused"); + } + else + { + ImGui.PushFont(UiBuilder.IconFont); + UiShared.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); + ImGui.PopFont(); + + UiShared.AttachToolTip("You are paired with " + entryUID); + } + + var textIsUid = true; + _showUidForEntry.TryGetValue(entry.OtherUID, out var showUidInsteadOfName); + if (!showUidInsteadOfName && _configuration.GetCurrentServerUidComments().TryGetValue(entry.OtherUID, out var playerText)) + { + if (string.IsNullOrEmpty(playerText)) + { + playerText = entryUID; + } + else + { + textIsUid = false; + } + } + else + { + playerText = entryUID; + } + + ImGui.SameLine(); + if (_editNickEntry != entry.OtherUID) + { + ImGui.SetCursorPosY(textPos); + if (textIsUid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(playerText); + if (textIsUid) ImGui.PopFont(); + UiShared.AttachToolTip("Left click to switch between UID display and nick" + Environment.NewLine + + "Right click to change nick for " + entryUID); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsUid; + if (_showUidForEntry.ContainsKey(entry.OtherUID)) + { + prevState = _showUidForEntry[entry.OtherUID]; + } + + _showUidForEntry[entry.OtherUID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configuration.SetCurrentServerUidComment(_editNickEntry, _editCharComment); + _configuration.Save(); + _editCharComment = _configuration.GetCurrentServerUidComments().ContainsKey(entry.OtherUID) + ? _configuration.GetCurrentServerUidComments()[entry.OtherUID] + : string.Empty; + _editNickEntry = entry.OtherUID; + } + } + else + { + ImGui.SetCursorPosY(originalY); + + ImGui.SetNextItemWidth(UiShared.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); + if (ImGui.InputTextWithHint("", "Nick/Notes", ref _editCharComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _configuration.SetCurrentServerUidComment(entry.OtherUID, _editCharComment); + _configuration.Save(); + _editNickEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _editNickEntry = string.Empty; + } + UiShared.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SetCursorPosY(originalY); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) + { + if (UiShared.CtrlPressed()) + { + _ = _apiController.SendPairedClientRemoval(entry.OtherUID); + _apiController.PairedClients.Remove(entry); + } + } + UiShared.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); + + if (entry.IsSynced) + { + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X - ImGui.GetStyle().ItemSpacing.X - trashButtonSize.X); + ImGui.SetCursorPosY(originalY); + if (ImGuiComponents.IconButton(pauseIcon)) + { + _ = _apiController.SendPairedClientPauseChange(entry.OtherUID, !entry.IsPaused); + } + UiShared.AttachToolTip(!entry.IsPaused + ? "Pause pairing with " + entryUID + : "Resume pairing with " + entryUID); + } + } + + private void DrawPairList() + { + UiShared.DrawWithID("addpair", DrawAddPair); + UiShared.DrawWithID("pairs", DrawPairs); + _transferPartHeight = ImGui.GetCursorPosY(); + UiShared.DrawWithID("filter", DrawFilter); + } + + private void DrawPairs() + { + var ySize = _transferPartHeight == 0 + ? 1 + : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _transferPartHeight - ImGui.GetCursorPosY(); + var users = GetFilteredUsers(); + + if (_configuration.ReverseUserSort) users = users.Reverse(); + + ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), false); + foreach (var entry in users.ToList()) + { + UiShared.DrawWithID(entry.OtherUID, () => DrawPairedClient(entry)); + } + ImGui.EndChild(); + } + + private IEnumerable GetFilteredUsers() + { + return _apiController.PairedClients.Where(p => + { + if (_characterOrCommentFilter.IsNullOrEmpty()) return true; + _configuration.GetCurrentServerUidComments().TryGetValue(p.OtherUID, out var comment); + var uid = p.VanityUID.IsNullOrEmpty() ? p.OtherUID : p.VanityUID; + return uid.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) || + (comment?.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) ?? false); + }); + } + + private void DrawServerStatus() + { + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Link); + var userCount = _apiController.OnlineUsers.ToString(); + var userSize = ImGui.CalcTextSize(userCount); + var textSize = ImGui.CalcTextSize("Users Online"); + + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X) / 2 - (userSize.X + textSize.X) / 2); + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("Users Online"); + } + else + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server"); + } + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + var color = UiShared.GetBoolColor(!_configuration.FullPause); + var connectedIcon = !_configuration.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; + + ImGui.PushStyleColor(ImGuiCol.Text, color); + if (ImGuiComponents.IconButton(connectedIcon)) + { + _configuration.FullPause = !_configuration.FullPause; + _configuration.Save(); + _ = _apiController.CreateConnections(); + } + ImGui.PopStyleColor(); + UiShared.AttachToolTip(!_configuration.FullPause ? "Disconnect from " + _apiController.ServerDictionary[_configuration.ApiUri] : "Connect to " + _apiController.ServerDictionary[_configuration.ApiUri]); + } + + private void DrawTransfers() + { + var currentUploads = _apiController.CurrentUploads.ToList(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.Upload.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); + + if (currentUploads.Any()) + { + var totalUploads = currentUploads.Count; + + var doneUploads = currentUploads.Count(c => c.IsTransferred); + var totalUploaded = currentUploads.Sum(c => c.Transferred); + var totalToUpload = currentUploads.Sum(c => c.Total); + + ImGui.Text($"{doneUploads}/{totalUploads}"); + var uploadText = $"({UiShared.ByteToString(totalUploaded)}/{UiShared.ByteToString(totalToUpload)})"; + var textSize = ImGui.CalcTextSize(uploadText); + ImGui.SameLine(_windowContentWidth - textSize.X); + ImGui.Text(uploadText); + } + else + { + ImGui.Text("No uploads in progress"); + } + + var currentDownloads = _apiController.CurrentDownloads.SelectMany(k => k.Value).ToList(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.Download.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); + + if (currentDownloads.Any()) + { + var totalDownloads = currentDownloads.Count(); + var doneDownloads = currentDownloads.Count(c => c.IsTransferred); + var totalDownloaded = currentDownloads.Sum(c => c.Transferred); + var totalToDownload = currentDownloads.Sum(c => c.Total); + + ImGui.Text($"{doneDownloads}/{totalDownloads}"); + var downloadText = + $"({UiShared.ByteToString(totalDownloaded)}/{UiShared.ByteToString(totalToDownload)})"; + var textSize = ImGui.CalcTextSize(downloadText); + ImGui.SameLine(_windowContentWidth - textSize.X); + ImGui.Text(downloadText); + } + else + { + ImGui.Text("No downloads in progress"); + } + + ImGui.SameLine(); + } + + private void DrawUIDHeader() + { + var uidText = GetUidText(); + var buttonSizeX = 0f; + + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + var uidTextSize = ImGui.CalcTextSize(uidText); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + + var originalPos = ImGui.GetCursorPos(); + ImGui.SetWindowFontScale(1.5f); + var buttonSize = UiShared.GetIconButtonSize(FontAwesomeIcon.Cog); + buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2; + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiShared.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) + { + OpenSettingsUi?.Invoke(); + } + UiShared.AttachToolTip("Open the Mare Synchronos Settings"); + + ImGui.SameLine(); //Important to draw the uidText consistently + ImGui.SetCursorPos(originalPos); + + if (_apiController.ServerState is ServerState.Connected) + { + buttonSizeX += UiShared.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; + ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(_apiController.UID); + } + UiShared.AttachToolTip("Copy your UID to clipboard"); + ImGui.SameLine(); + } + ImGui.SetWindowFontScale(1f); + + ImGui.SetCursorPosY(originalPos.Y + buttonSize.Y / 2 - uidTextSize.Y / 2 - ImGui.GetStyle().ItemSpacing.Y / 2); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 + buttonSizeX - uidTextSize.X / 2); + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextColored(GetUidColor(), uidText); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + + if (_apiController.ServerState is not ServerState.Connected) + { + UiShared.ColorTextWrapped(GetServerError(), GetUidColor()); + } + } + + + private string GetServerError() + { + return _apiController.ServerState switch + { + ServerState.Disconnected => "You are currently disconnected from the Mare Synchronos server.", + ServerState.Unauthorized => "Your account is not present on the server anymore or you are banned.", + ServerState.Offline => "Your selected Mare Synchronos server is currently offline.", + ServerState.VersionMisMatch => + "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", + ServerState.RateLimited => "You are rate limited for (re)connecting too often. Wait and try again later.", + ServerState.Connected => string.Empty, + _ => string.Empty + }; + } + + private Vector4 GetUidColor() + { + return _apiController.ServerState switch + { + ServerState.Connected => ImGuiColors.ParsedGreen, + ServerState.Disconnected => ImGuiColors.DalamudYellow, + ServerState.Unauthorized => ImGuiColors.DalamudRed, + ServerState.VersionMisMatch => ImGuiColors.DalamudRed, + ServerState.Offline => ImGuiColors.DalamudRed, + ServerState.RateLimited => ImGuiColors.DalamudYellow, + _ => ImGuiColors.DalamudRed + }; + } + + private string GetUidText() + { + return _apiController.ServerState switch + { + ServerState.Disconnected => "Disconnected", + ServerState.Unauthorized => "Unauthorized", + ServerState.VersionMisMatch => "Version mismatch", + ServerState.Offline => "Unavailable", + ServerState.RateLimited => "Rate Limited", + ServerState.Connected => _apiController.UID, + _ => string.Empty + }; + } } diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index e15d85b..26239a3 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -12,287 +12,286 @@ using MareSynchronos.Localization; using Dalamud.Utility; using MareSynchronos.FileCache; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +internal class IntroUi : Window, IDisposable { - internal class IntroUi : Window, IDisposable + private readonly UiShared _uiShared; + private readonly Configuration _pluginConfiguration; + private readonly PeriodicFileScanner _fileCacheManager; + private readonly WindowSystem _windowSystem; + private bool _readFirstPage; + + public event SwitchUi? SwitchToMainUi; + + private string[] TosParagraphs; + + private Tuple _darkSoulsCaptcha1 = new(string.Empty, string.Empty); + private Tuple _darkSoulsCaptcha2 = new(string.Empty, string.Empty); + private Tuple _darkSoulsCaptcha3 = new(string.Empty, string.Empty); + private string _enteredDarkSoulsCaptcha1 = string.Empty; + private string _enteredDarkSoulsCaptcha2 = string.Empty; + private string _enteredDarkSoulsCaptcha3 = string.Empty; + + private bool _failedOnce = false; + private Task _timeoutTask; + private string _timeoutTime; + + private Dictionary _languages = new() { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; + private int _currentLanguage; + + private bool DarkSoulsCaptchaValid => _darkSoulsCaptcha1.Item2 == _enteredDarkSoulsCaptcha1.Trim() + && _darkSoulsCaptcha2.Item2 == _enteredDarkSoulsCaptcha2.Trim() + && _darkSoulsCaptcha3.Item2 == _enteredDarkSoulsCaptcha3.Trim(); + + + public void Dispose() { - private readonly UiShared _uiShared; - private readonly Configuration _pluginConfiguration; - private readonly PeriodicFileScanner _fileCacheManager; - private readonly WindowSystem _windowSystem; - private bool _readFirstPage; + Logger.Verbose("Disposing " + nameof(IntroUi)); - public event SwitchUi? SwitchToMainUi; + _windowSystem.RemoveWindow(this); + } - private string[] TosParagraphs; + public IntroUi(WindowSystem windowSystem, UiShared uiShared, Configuration pluginConfiguration, + PeriodicFileScanner fileCacheManager) : base("Mare Synchronos Setup") + { + Logger.Verbose("Creating " + nameof(IntroUi)); - private Tuple _darkSoulsCaptcha1 = new(string.Empty, string.Empty); - private Tuple _darkSoulsCaptcha2 = new(string.Empty, string.Empty); - private Tuple _darkSoulsCaptcha3 = new(string.Empty, string.Empty); - private string _enteredDarkSoulsCaptcha1 = string.Empty; - private string _enteredDarkSoulsCaptcha2 = string.Empty; - private string _enteredDarkSoulsCaptcha3 = string.Empty; + _uiShared = uiShared; + _pluginConfiguration = pluginConfiguration; + _fileCacheManager = fileCacheManager; + _windowSystem = windowSystem; - private bool _failedOnce = false; - private Task _timeoutTask; - private string _timeoutTime; - - private Dictionary _languages = new() { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; - private int _currentLanguage; - - private bool DarkSoulsCaptchaValid => _darkSoulsCaptcha1.Item2 == _enteredDarkSoulsCaptcha1.Trim() - && _darkSoulsCaptcha2.Item2 == _enteredDarkSoulsCaptcha2.Trim() - && _darkSoulsCaptcha3.Item2 == _enteredDarkSoulsCaptcha3.Trim(); - - - public void Dispose() + SizeConstraints = new WindowSizeConstraints() { - Logger.Verbose("Disposing " + nameof(IntroUi)); + MinimumSize = new Vector2(600, 400), + MaximumSize = new Vector2(600, 2000) + }; - _windowSystem.RemoveWindow(this); - } + GetToSLocalization(); - public IntroUi(WindowSystem windowSystem, UiShared uiShared, Configuration pluginConfiguration, - PeriodicFileScanner fileCacheManager) : base("Mare Synchronos Setup") + _windowSystem.AddWindow(this); + } + + public override void Draw() + { + if (!_pluginConfiguration.AcceptedAgreement && !_readFirstPage) { - Logger.Verbose("Creating " + nameof(IntroUi)); + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextUnformatted("Welcome to Mare Synchronos"); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + ImGui.Separator(); + UiShared.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " + + "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); + UiShared.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); - _uiShared = uiShared; - _pluginConfiguration = pluginConfiguration; - _fileCacheManager = fileCacheManager; - _windowSystem = windowSystem; - - SizeConstraints = new WindowSizeConstraints() + UiShared.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + + "might look broken because of this or others players mods might not apply on your end altogether. " + + "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); + if (!_uiShared.DrawOtherPluginState()) return; + ImGui.Separator(); + if (ImGui.Button("Next##toAgreement")) { - MinimumSize = new Vector2(600, 400), - MaximumSize = new Vector2(600, 2000) - }; - - GetToSLocalization(); - - _windowSystem.AddWindow(this); - } - - public override void Draw() - { - if (!_pluginConfiguration.AcceptedAgreement && !_readFirstPage) - { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("Welcome to Mare Synchronos"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - ImGui.Separator(); - UiShared.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other paired Mare Synchronos users. " + - "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); - UiShared.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); - - UiShared.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + - "might look broken because of this or others players mods might not apply on your end altogether. " + - "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); - if (!_uiShared.DrawOtherPluginState()) return; - ImGui.Separator(); - if (ImGui.Button("Next##toAgreement")) - { - _readFirstPage = true; - } + _readFirstPage = true; } - else if (!_pluginConfiguration.AcceptedAgreement && _readFirstPage) + } + else if (!_pluginConfiguration.AcceptedAgreement && _readFirstPage) + { + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + var textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); + ImGui.TextUnformatted(Strings.ToS.AgreementLabel); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + + ImGui.SameLine(); + var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); + ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2); + + ImGui.TextUnformatted(Strings.ToS.LanguageLabel); + ImGui.SameLine(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2); + ImGui.SetNextItemWidth(80); + if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count)) { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - var textSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); - ImGui.TextUnformatted(Strings.ToS.AgreementLabel); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); + GetToSLocalization(_currentLanguage); + } - ImGui.SameLine(); - var languageSize = ImGui.CalcTextSize(Strings.ToS.LanguageLabel); - ImGui.SetCursorPosX(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - languageSize.X - 80); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - languageSize.Y / 2); + ImGui.Separator(); + ImGui.SetWindowFontScale(1.5f); + string readThis = Strings.ToS.ReadLabel; + textSize = ImGui.CalcTextSize(readThis); + ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); + UiShared.ColorText(readThis, ImGuiColors.DalamudRed); + ImGui.SetWindowFontScale(1.0f); + ImGui.Separator(); - ImGui.TextUnformatted(Strings.ToS.LanguageLabel); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + textSize.Y / 2 - (languageSize.Y + ImGui.GetStyle().FramePadding.Y) / 2); - ImGui.SetNextItemWidth(80); - if (ImGui.Combo("", ref _currentLanguage, _languages.Keys.ToArray(), _languages.Count)) + + UiShared.TextWrapped(TosParagraphs[0]); + UiShared.TextWrapped(TosParagraphs[1]); + UiShared.TextWrapped(TosParagraphs[2]); + UiShared.TextWrapped(TosParagraphs[3]); + UiShared.TextWrapped(TosParagraphs[4]); + UiShared.TextWrapped(TosParagraphs[5]); + + ImGui.Separator(); + if ((!_pluginConfiguration.DarkSoulsAgreement || DarkSoulsCaptchaValid) && (_timeoutTask?.IsCompleted ?? true)) + { + if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup")) { - GetToSLocalization(_currentLanguage); - } + _enteredDarkSoulsCaptcha1 = string.Empty; + _enteredDarkSoulsCaptcha2 = string.Empty; + _enteredDarkSoulsCaptcha3 = string.Empty; - ImGui.Separator(); - ImGui.SetWindowFontScale(1.5f); - string readThis = Strings.ToS.ReadLabel; - textSize = ImGui.CalcTextSize(readThis); - ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); - UiShared.ColorText(readThis, ImGuiColors.DalamudRed); - ImGui.SetWindowFontScale(1.0f); - ImGui.Separator(); - - - UiShared.TextWrapped(TosParagraphs[0]); - UiShared.TextWrapped(TosParagraphs[1]); - UiShared.TextWrapped(TosParagraphs[2]); - UiShared.TextWrapped(TosParagraphs[3]); - UiShared.TextWrapped(TosParagraphs[4]); - UiShared.TextWrapped(TosParagraphs[5]); - - ImGui.Separator(); - if ((!_pluginConfiguration.DarkSoulsAgreement || DarkSoulsCaptchaValid) && (_timeoutTask?.IsCompleted ?? true)) - { - if (ImGui.Button(Strings.ToS.AgreeLabel + "##toSetup")) + if (UiShared.CtrlPressed()) { - _enteredDarkSoulsCaptcha1 = string.Empty; - _enteredDarkSoulsCaptcha2 = string.Empty; - _enteredDarkSoulsCaptcha3 = string.Empty; - - if (UiShared.CtrlPressed()) - { - _pluginConfiguration.AcceptedAgreement = true; - _pluginConfiguration.Save(); - } - else - { - if (!_failedOnce) - { - _failedOnce = true; - _timeoutTask = Task.Run(async () => - { - for (int i = 60; i > 0; i--) - { - _timeoutTime = $"{i}s " + Strings.ToS.RemainingLabel; - Logger.Debug(_timeoutTime); - await Task.Delay(TimeSpan.FromSeconds(1)); - } - }); - } - else - { - _pluginConfiguration.DarkSoulsAgreement = true; - _pluginConfiguration.Save(); - GenerateDarkSoulsAgreementCaptcha(); - } - } - } - } - else - { - if (_failedOnce && (!_timeoutTask?.IsCompleted ?? true)) - { - UiShared.ColorTextWrapped(Strings.ToS.FailedLabel, ImGuiColors.DalamudYellow); - UiShared.TextWrapped(Strings.ToS.TimeoutLabel); - UiShared.TextWrapped(_timeoutTime); + _pluginConfiguration.AcceptedAgreement = true; + _pluginConfiguration.Save(); } else { - UiShared.ColorTextWrapped(Strings.ToS.FailedAgainLabel, ImGuiColors.DalamudYellow); - UiShared.TextWrapped(Strings.ToS.PuzzleLabel); - UiShared.TextWrapped(Strings.ToS.PuzzleDescLabel); - ImGui.SetNextItemWidth(100); - ImGui.InputText(_darkSoulsCaptcha1.Item1, ref _enteredDarkSoulsCaptcha1, 255); - ImGui.SetNextItemWidth(100); - ImGui.InputText(_darkSoulsCaptcha2.Item1, ref _enteredDarkSoulsCaptcha2, 255); - ImGui.SetNextItemWidth(100); - ImGui.InputText(_darkSoulsCaptcha3.Item1, ref _enteredDarkSoulsCaptcha3, 255); + if (!_failedOnce) + { + _failedOnce = true; + _timeoutTask = Task.Run(async () => + { + for (int i = 60; i > 0; i--) + { + _timeoutTime = $"{i}s " + Strings.ToS.RemainingLabel; + Logger.Debug(_timeoutTime); + await Task.Delay(TimeSpan.FromSeconds(1)); + } + }); + } + else + { + _pluginConfiguration.DarkSoulsAgreement = true; + _pluginConfiguration.Save(); + GenerateDarkSoulsAgreementCaptcha(); + } } } } - else if (_pluginConfiguration.AcceptedAgreement - && (string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) - || _pluginConfiguration.InitialScanComplete == false - || !Directory.Exists(_pluginConfiguration.CacheFolder))) - { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("File Cache Setup"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - ImGui.Separator(); - - if (!_uiShared.HasValidPenumbraModPath) - { - UiShared.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); - } - else - { - UiShared.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " + - "Additionally, a local cache folder must be set where Mare Synchronos will download its local file cache to. " + - "Once the Cache Folder is set and the scan complete, this page will automatically forward to registration at a service."); - UiShared.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); - UiShared.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.db of Mare Synchronos in the Plugin Configurations folder of Dalamud. " + - "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); - _uiShared.DrawCacheDirectorySetting(); - } - - if (!_fileCacheManager.IsScanRunning && !string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_pluginConfiguration.CacheFolder)) - { - if (ImGui.Button("Start Scan##startScan")) - { - _fileCacheManager.InvokeScan(true); - } - } - else - { - _uiShared.DrawFileScanState(); - } - } - else if (!_uiShared.ApiController.ServerAlive) - { - if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); - ImGui.TextUnformatted("Service Registration"); - if (_uiShared.UidFontBuilt) ImGui.PopFont(); - ImGui.Separator(); - UiShared.TextWrapped("To be able to use Mare Synchronos you will have to register an account."); - UiShared.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise."); - UiShared.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #registration."); - - if (ImGui.Button("Join the Mare Synchronos Discord")) - { - Util.OpenLink("https://discord.gg/mpNdkrTRjW"); - } - - UiShared.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key."); - - ImGui.Separator(); - - UiShared.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below."); - - _uiShared.DrawServiceSelection(() => { }); - } else { - SwitchToMainUi?.Invoke(); - IsOpen = false; + if (_failedOnce && (!_timeoutTask?.IsCompleted ?? true)) + { + UiShared.ColorTextWrapped(Strings.ToS.FailedLabel, ImGuiColors.DalamudYellow); + UiShared.TextWrapped(Strings.ToS.TimeoutLabel); + UiShared.TextWrapped(_timeoutTime); + } + else + { + UiShared.ColorTextWrapped(Strings.ToS.FailedAgainLabel, ImGuiColors.DalamudYellow); + UiShared.TextWrapped(Strings.ToS.PuzzleLabel); + UiShared.TextWrapped(Strings.ToS.PuzzleDescLabel); + ImGui.SetNextItemWidth(100); + ImGui.InputText(_darkSoulsCaptcha1.Item1, ref _enteredDarkSoulsCaptcha1, 255); + ImGui.SetNextItemWidth(100); + ImGui.InputText(_darkSoulsCaptcha2.Item1, ref _enteredDarkSoulsCaptcha2, 255); + ImGui.SetNextItemWidth(100); + ImGui.InputText(_darkSoulsCaptcha3.Item1, ref _enteredDarkSoulsCaptcha3, 255); + } } } - - private string _secretKey = string.Empty; - - private void GetToSLocalization(int changeLanguageTo = -1) + else if (_pluginConfiguration.AcceptedAgreement + && (string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) + || _pluginConfiguration.InitialScanComplete == false + || !Directory.Exists(_pluginConfiguration.CacheFolder))) { - if (changeLanguageTo != -1) + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextUnformatted("File Cache Setup"); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + ImGui.Separator(); + + if (!_uiShared.HasValidPenumbraModPath) { - _uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value); + UiShared.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); } - - TosParagraphs = new[] { Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6 }; - - if (_pluginConfiguration.DarkSoulsAgreement) + else { - GenerateDarkSoulsAgreementCaptcha(); + UiShared.TextWrapped("To not unnecessary download files already present on your computer, Mare Synchronos will have to scan your Penumbra mod directory. " + + "Additionally, a local cache folder must be set where Mare Synchronos will download its local file cache to. " + + "Once the Cache Folder is set and the scan complete, this page will automatically forward to registration at a service."); + UiShared.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); + UiShared.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.db of Mare Synchronos in the Plugin Configurations folder of Dalamud. " + + "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); + _uiShared.DrawCacheDirectorySetting(); + } + + if (!_fileCacheManager.IsScanRunning && !string.IsNullOrEmpty(_pluginConfiguration.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_pluginConfiguration.CacheFolder)) + { + if (ImGui.Button("Start Scan##startScan")) + { + _fileCacheManager.InvokeScan(true); + } + } + else + { + _uiShared.DrawFileScanState(); } } - - private void GenerateDarkSoulsAgreementCaptcha() + else if (!_uiShared.ApiController.ServerAlive) { - _darkSoulsCaptcha1 = GetCaptchaTuple(); - _darkSoulsCaptcha2 = GetCaptchaTuple(); - _darkSoulsCaptcha3 = GetCaptchaTuple(); + if (_uiShared.UidFontBuilt) ImGui.PushFont(_uiShared.UidFont); + ImGui.TextUnformatted("Service Registration"); + if (_uiShared.UidFontBuilt) ImGui.PopFont(); + ImGui.Separator(); + UiShared.TextWrapped("To be able to use Mare Synchronos you will have to register an account."); + UiShared.TextWrapped("For the official Mare Synchronos Servers the account creation will be handled on the official Mare Synchronos Discord. Due to security risks for the server, there is no way to handle this senisibly otherwise."); + UiShared.TextWrapped("If you want to register at the main server \"" + WebAPI.ApiController.MainServer + "\" join the Discord and follow the instructions as described in #registration."); + + if (ImGui.Button("Join the Mare Synchronos Discord")) + { + Util.OpenLink("https://discord.gg/mpNdkrTRjW"); + } + + UiShared.TextWrapped("For all other non official services you will have to contact the appropriate service provider how to obtain a secret key."); + + ImGui.Separator(); + + UiShared.TextWrapped("Once you have received a secret key you can connect to the service using the tools provided below."); + + _uiShared.DrawServiceSelection(() => { }); } - - private Tuple GetCaptchaTuple() + else { - Random random = new Random(); - var paragraphIdx = random.Next(TosParagraphs.Length); - var splitParagraph = TosParagraphs[paragraphIdx].Split(".", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToArray(); - var sentenceIdx = random.Next(splitParagraph.Length); - var splitSentence = splitParagraph[sentenceIdx].Split(" ").Select(c => c.Trim()).Select(c => c.Replace(".", "").Replace(",", "").Replace("'", "")).ToArray(); - var wordIdx = random.Next(splitSentence.Length); - return new($"{Strings.ToS.ParagraphLabel} {paragraphIdx + 1}, {Strings.ToS.SentenceLabel} {sentenceIdx + 1}, {Strings.ToS.WordLabel} {wordIdx + 1}", splitSentence[wordIdx]); + SwitchToMainUi?.Invoke(); + IsOpen = false; } } + + private string _secretKey = string.Empty; + + private void GetToSLocalization(int changeLanguageTo = -1) + { + if (changeLanguageTo != -1) + { + _uiShared.LoadLocalization(_languages.ElementAt(changeLanguageTo).Value); + } + + TosParagraphs = new[] { Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6 }; + + if (_pluginConfiguration.DarkSoulsAgreement) + { + GenerateDarkSoulsAgreementCaptcha(); + } + } + + private void GenerateDarkSoulsAgreementCaptcha() + { + _darkSoulsCaptcha1 = GetCaptchaTuple(); + _darkSoulsCaptcha2 = GetCaptchaTuple(); + _darkSoulsCaptcha3 = GetCaptchaTuple(); + } + + private Tuple GetCaptchaTuple() + { + Random random = new Random(); + var paragraphIdx = random.Next(TosParagraphs.Length); + var splitParagraph = TosParagraphs[paragraphIdx].Split(".", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToArray(); + var sentenceIdx = random.Next(splitParagraph.Length); + var splitSentence = splitParagraph[sentenceIdx].Split(" ").Select(c => c.Trim()).Select(c => c.Replace(".", "").Replace(",", "").Replace("'", "")).ToArray(); + var wordIdx = random.Next(splitSentence.Length); + return new($"{Strings.ToS.ParagraphLabel} {paragraphIdx + 1}, {Strings.ToS.SentenceLabel} {sentenceIdx + 1}, {Strings.ToS.WordLabel} {wordIdx + 1}", splitSentence[wordIdx]); + } } diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 295f9cc..bfbf45d 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -14,600 +14,599 @@ using MareSynchronos.WebAPI.Utils; using System.Diagnostics; using Dalamud.Utility; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +public delegate void SwitchUi(); +public class SettingsUi : Window, IDisposable { - public delegate void SwitchUi(); - public class SettingsUi : Window, IDisposable + private readonly Configuration _configuration; + private readonly WindowSystem _windowSystem; + private readonly ApiController _apiController; + private readonly UiShared _uiShared; + public event SwitchUi? SwitchToIntroUi; + + public SettingsUi(WindowSystem windowSystem, + UiShared uiShared, Configuration configuration, ApiController apiController) : base("Mare Synchronos Settings") { - private readonly Configuration _configuration; - private readonly WindowSystem _windowSystem; - private readonly ApiController _apiController; - private readonly UiShared _uiShared; - public event SwitchUi? SwitchToIntroUi; + Logger.Verbose("Creating " + nameof(SettingsUi)); - public SettingsUi(WindowSystem windowSystem, - UiShared uiShared, Configuration configuration, ApiController apiController) : base("Mare Synchronos Settings") + SizeConstraints = new WindowSizeConstraints() { - Logger.Verbose("Creating " + nameof(SettingsUi)); + MinimumSize = new Vector2(800, 400), + MaximumSize = new Vector2(800, 2000), + }; - SizeConstraints = new WindowSizeConstraints() + _configuration = configuration; + _windowSystem = windowSystem; + _apiController = apiController; + _uiShared = uiShared; + windowSystem.AddWindow(this); + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(SettingsUi)); + + _windowSystem.RemoveWindow(this); + } + + public override void Draw() + { + var pluginState = _uiShared.DrawOtherPluginState(); + + DrawSettingsContent(); + } + + private void DrawSettingsContent() + { + _uiShared.PrintServerState(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("Community and Support:"); + ImGui.SameLine(); + if (ImGui.Button("Mare Synchronos Discord")) + { + Util.OpenLink("https://discord.gg/mpNdkrTRjW"); + } + ImGui.Separator(); + if (ImGui.BeginTabBar("mainTabBar")) + { + if (ImGui.BeginTabItem("Cache Settings")) { - MinimumSize = new Vector2(800, 400), - MaximumSize = new Vector2(800, 2000), - }; - - _configuration = configuration; - _windowSystem = windowSystem; - _apiController = apiController; - _uiShared = uiShared; - windowSystem.AddWindow(this); - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(SettingsUi)); - - _windowSystem.RemoveWindow(this); - } - - public override void Draw() - { - var pluginState = _uiShared.DrawOtherPluginState(); - - DrawSettingsContent(); - } - - private void DrawSettingsContent() - { - _uiShared.PrintServerState(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("Community and Support:"); - ImGui.SameLine(); - if (ImGui.Button("Mare Synchronos Discord")) - { - Util.OpenLink("https://discord.gg/mpNdkrTRjW"); + DrawFileCacheSettings(); + ImGui.EndTabItem(); } - ImGui.Separator(); - if (ImGui.BeginTabBar("mainTabBar")) + + if (_apiController.ServerState is ServerState.Connected) { - if (ImGui.BeginTabItem("Cache Settings")) + if (ImGui.BeginTabItem("Transfers")) { - DrawFileCacheSettings(); + DrawCurrentTransfers(); ImGui.EndTabItem(); } - - if (_apiController.ServerState is ServerState.Connected) - { - if (ImGui.BeginTabItem("Transfers")) - { - DrawCurrentTransfers(); - ImGui.EndTabItem(); - } - } - - if (ImGui.BeginTabItem("Blocked Transfers")) - { - DrawBlockedTransfers(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("User Administration")) - { - DrawUserAdministration(_apiController.IsConnected); - ImGui.EndTabItem(); - } - - if (_apiController.IsConnected && _apiController.IsModerator) - { - if (ImGui.BeginTabItem("Administration")) - { - DrawAdministration(); - ImGui.EndTabItem(); - } - } - - ImGui.EndTabBar(); } - } - private string _forbiddenFileHashEntry = string.Empty; - private string _forbiddenFileHashForbiddenBy = string.Empty; - private string _bannedUserHashEntry = string.Empty; - private string _bannedUserReasonEntry = string.Empty; - - private void DrawAdministration() - { - if (ImGui.TreeNode("Forbidden Files Changes")) + if (ImGui.BeginTabItem("Blocked Transfers")) { - if (ImGui.BeginTable("ForbiddenFilesTable", 3, ImGuiTableFlags.RowBg)) + DrawBlockedTransfers(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("User Administration")) + { + DrawUserAdministration(_apiController.IsConnected); + ImGui.EndTabItem(); + } + + if (_apiController.IsConnected && _apiController.IsModerator) + { + if (ImGui.BeginTabItem("Administration")) { - ImGui.TableSetupColumn("File Hash", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Forbidden By", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + DrawAdministration(); + ImGui.EndTabItem(); + } + } - ImGui.TableHeadersRow(); + ImGui.EndTabBar(); + } + } - foreach (var forbiddenFile in _apiController.AdminForbiddenFiles) + private string _forbiddenFileHashEntry = string.Empty; + private string _forbiddenFileHashForbiddenBy = string.Empty; + private string _bannedUserHashEntry = string.Empty; + private string _bannedUserReasonEntry = string.Empty; + + private void DrawAdministration() + { + if (ImGui.TreeNode("Forbidden Files Changes")) + { + if (ImGui.BeginTable("ForbiddenFilesTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("File Hash", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Forbidden By", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var forbiddenFile in _apiController.AdminForbiddenFiles) + { + ImGui.TableNextColumn(); + + ImGui.Text(forbiddenFile.Hash); + ImGui.TableNextColumn(); + string by = forbiddenFile.ForbiddenBy; + if (ImGui.InputText("##forbiddenBy" + forbiddenFile.Hash, ref by, 255)) { - ImGui.TableNextColumn(); - - ImGui.Text(forbiddenFile.Hash); - ImGui.TableNextColumn(); - string by = forbiddenFile.ForbiddenBy; - if (ImGui.InputText("##forbiddenBy" + forbiddenFile.Hash, ref by, 255)) - { - forbiddenFile.ForbiddenBy = by; - } - - ImGui.TableNextColumn(); - if (_apiController.IsAdmin) - { - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button( - FontAwesomeIcon.Upload.ToIconString() + "##updateFile" + forbiddenFile.Hash)) - { - _ = _apiController.AddOrUpdateForbiddenFileEntry(forbiddenFile); - } - - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteFile" + - forbiddenFile.Hash)) - { - _ = _apiController.DeleteForbiddenFileEntry(forbiddenFile); - } - - ImGui.PopFont(); - } - + forbiddenFile.ForbiddenBy = by; } + ImGui.TableNextColumn(); if (_apiController.IsAdmin) { - ImGui.TableNextColumn(); - ImGui.InputText("##addFileHash", ref _forbiddenFileHashEntry, 255); - ImGui.TableNextColumn(); - ImGui.InputText("##addForbiddenBy", ref _forbiddenFileHashForbiddenBy, 255); - ImGui.TableNextColumn(); ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) + if (ImGui.Button( + FontAwesomeIcon.Upload.ToIconString() + "##updateFile" + forbiddenFile.Hash)) { - _ = _apiController.AddOrUpdateForbiddenFileEntry(new ForbiddenFileDto() - { - ForbiddenBy = _forbiddenFileHashForbiddenBy, - Hash = _forbiddenFileHashEntry - }); + _ = _apiController.AddOrUpdateForbiddenFileEntry(forbiddenFile); } - ImGui.PopFont(); - ImGui.NextColumn(); - } - - ImGui.EndTable(); - } - - ImGui.TreePop(); - } - - if (ImGui.TreeNode("Banned Users")) - { - if (ImGui.BeginTable("BannedUsersTable", 3, ImGuiTableFlags.RowBg)) - { - ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 290); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); - - ImGui.TableHeadersRow(); - - foreach (var bannedUser in _apiController.AdminBannedUsers) - { - ImGui.TableNextColumn(); - ImGui.Text(bannedUser.CharacterHash); - - ImGui.TableNextColumn(); - string reason = bannedUser.Reason; - ImGuiInputTextFlags moderatorFlags = _apiController.IsModerator - ? ImGuiInputTextFlags.ReadOnly - : ImGuiInputTextFlags.None; - if (ImGui.InputText("##bannedReason" + bannedUser.CharacterHash, ref reason, 255, - moderatorFlags)) + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteFile" + + forbiddenFile.Hash)) { - bannedUser.Reason = reason; - } - - ImGui.TableNextColumn(); - ImGui.PushFont(UiBuilder.IconFont); - if (_apiController.IsAdmin) - { - if (ImGui.Button(FontAwesomeIcon.Upload.ToIconString() + "##updateUser" + - bannedUser.CharacterHash)) - { - _ = _apiController.AddOrUpdateBannedUserEntry(bannedUser); - } - - ImGui.SameLine(); - } - - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteUser" + - bannedUser.CharacterHash)) - { - _ = _apiController.DeleteBannedUserEntry(bannedUser); + _ = _apiController.DeleteForbiddenFileEntry(forbiddenFile); } ImGui.PopFont(); } - ImGui.TableNextColumn(); - ImGui.InputText("##addUserHash", ref _bannedUserHashEntry, 255); + } + if (_apiController.IsAdmin) + { ImGui.TableNextColumn(); - if (_apiController.IsAdmin) - { - ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255); - } - else - { - _bannedUserReasonEntry = "Banned by " + _uiShared.PlayerName; - ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255, - ImGuiInputTextFlags.ReadOnly); - } - + ImGui.InputText("##addFileHash", ref _forbiddenFileHashEntry, 255); + ImGui.TableNextColumn(); + ImGui.InputText("##addForbiddenBy", ref _forbiddenFileHashForbiddenBy, 255); ImGui.TableNextColumn(); ImGui.PushFont(UiBuilder.IconFont); if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) { - _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto() + _ = _apiController.AddOrUpdateForbiddenFileEntry(new ForbiddenFileDto() { - CharacterHash = _forbiddenFileHashForbiddenBy, - Reason = _forbiddenFileHashEntry + ForbiddenBy = _forbiddenFileHashForbiddenBy, + Hash = _forbiddenFileHashEntry }); } ImGui.PopFont(); - - ImGui.EndTable(); - } - - ImGui.TreePop(); - } - - if (ImGui.TreeNode("Online Users")) - { - if (ImGui.Button("Refresh Online Users")) - { - _ = _apiController.RefreshOnlineUsers(); - } - - if (ImGui.BeginTable("OnlineUsersTable", 3, ImGuiTableFlags.RowBg)) - { - ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 300); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); - - ImGui.TableHeadersRow(); - - foreach (var onlineUser in _apiController.AdminOnlineUsers) - { - ImGui.TableNextColumn(); - ImGui.PushFont(UiBuilder.IconFont); - string icon = onlineUser.IsModerator - ? FontAwesomeIcon.ChessKing.ToIconString() - : onlineUser.IsAdmin - ? FontAwesomeIcon.Crown.ToIconString() - : FontAwesomeIcon.User.ToIconString(); - ImGui.Text(icon); - ImGui.PopFont(); - ImGui.SameLine(); - - ImGui.Text(onlineUser.UID); - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyUID" + - onlineUser.CharacterNameHash)) - { - ImGui.SetClipboardText(onlineUser.UID); - } - - ImGui.PopFont(); - - ImGui.TableNextColumn(); - string charNameHash = onlineUser.CharacterNameHash; - ImGui.InputText("##onlineUserHash" + onlineUser.CharacterNameHash, ref charNameHash, 255, - ImGuiInputTextFlags.ReadOnly); - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyHash" + - onlineUser.CharacterNameHash)) - { - ImGui.SetClipboardText(onlineUser.UID); - } - - ImGui.PopFont(); - - ImGui.TableNextColumn(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.SkullCrossbones.ToIconString() + "##onlineUserBan" + - onlineUser.CharacterNameHash)) - { - _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto - { - CharacterHash = onlineUser.CharacterNameHash, - Reason = "Banned by " + _uiShared.PlayerName - }); - } - ImGui.SameLine(); - if (onlineUser.UID != _apiController.UID && _apiController.IsAdmin) - { - if (!onlineUser.IsModerator) - { - if (ImGui.Button(FontAwesomeIcon.ChessKing.ToIconString() + - "##onlineUserModerator" + - onlineUser.CharacterNameHash)) - { - _apiController.PromoteToModerator(onlineUser.UID); - } - } - else - { - if (ImGui.Button(FontAwesomeIcon.User.ToIconString() + - "##onlineUserNonModerator" + - onlineUser.CharacterNameHash)) - { - _apiController.DemoteFromModerator(onlineUser.UID); - } - } - } - - ImGui.PopFont(); - } - ImGui.EndTable(); - } - ImGui.TreePop(); - } - } - - private bool _deleteFilesPopupModalShown = false; - private bool _deleteAccountPopupModalShown = false; - - private void DrawUserAdministration(bool serverAlive) - { - if (serverAlive) - { - if (ImGui.Button("Delete all my files")) - { - _deleteFilesPopupModalShown = true; - ImGui.OpenPopup("Delete all your files?"); - } - - UiShared.DrawHelpText("Completely deletes all your uploaded files on the service."); - - if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, - ImGuiWindowFlags.AlwaysAutoResize)) - { - UiShared.TextWrapped( - "All your own uploaded files on the service will be deleted.\nThis operation cannot be undone."); - ImGui.Text("Are you sure you want to continue?"); - ImGui.Separator(); - if (ImGui.Button("Delete everything", new Vector2(150, 0))) - { - Task.Run(() => _apiController.DeleteAllMyFiles()); - ImGui.CloseCurrentPopup(); - _deleteFilesPopupModalShown = false; - } - - ImGui.SameLine(); - - if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) - { - ImGui.CloseCurrentPopup(); - _deleteFilesPopupModalShown = false; - } - - ImGui.EndPopup(); - } - - if (ImGui.Button("Delete account")) - { - _deleteAccountPopupModalShown = true; - ImGui.OpenPopup("Delete your account?"); - } - - UiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service."); - - if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, - ImGuiWindowFlags.AlwaysAutoResize)) - { - UiShared.TextWrapped( - "Your account and all associated files and data on the service will be deleted."); - UiShared.TextWrapped("Your UID will be removed from all pairing lists."); - ImGui.Text("Are you sure you want to continue?"); - ImGui.Separator(); - if (ImGui.Button("Delete account", new Vector2(150, 0))) - { - Task.Run(() => _apiController.DeleteAccount()); - ImGui.CloseCurrentPopup(); - _deleteAccountPopupModalShown = false; - SwitchToIntroUi?.Invoke(); - } - - ImGui.SameLine(); - - if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) - { - ImGui.CloseCurrentPopup(); - _deleteAccountPopupModalShown = false; - } - - ImGui.EndPopup(); - } - } - - if (!_configuration.FullPause) - { - UiShared.ColorTextWrapped("Note: to change servers you need to disconnect from your current Mare Synchronos server.", ImGuiColors.DalamudYellow); - } - - var marePaused = _configuration.FullPause; - - if (_configuration.HasValidSetup()) - { - if (ImGui.Checkbox("Disconnect Mare Synchronos", ref marePaused)) - { - _configuration.FullPause = marePaused; - _configuration.Save(); - Task.Run(_apiController.CreateConnections); - } - - UiShared.DrawHelpText("Completely pauses the sync and clears your current data (not uploaded files) on the service."); - } - else - { - UiShared.ColorText("You cannot reconnect without a valid account on the service.", ImGuiColors.DalamudYellow); - } - - if (marePaused) - { - _uiShared.DrawServiceSelection(() => { }); - } - } - - private void DrawBlockedTransfers() - { - UiShared.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + - "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + - "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Mare.", - ImGuiColors.DalamudGrey); - - if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) - { - ImGui.TableSetupColumn( - $"Hash/Filename"); - ImGui.TableSetupColumn($"Forbidden by"); - - ImGui.TableHeadersRow(); - - foreach (var item in _apiController.ForbiddenTransfers) - { - ImGui.TableNextColumn(); - if (item is UploadFileTransfer transfer) - { - ImGui.Text(transfer.LocalFile); - } - else - { - ImGui.Text(item.Hash); - } - ImGui.TableNextColumn(); - ImGui.Text(item.ForbiddenBy); - } - ImGui.EndTable(); - } - } - - private void DrawCurrentTransfers() - { - bool showTransferWindow = _configuration.ShowTransferWindow; - if (ImGui.Checkbox("Show separate Transfer window while transfers are active", ref showTransferWindow)) - { - _configuration.ShowTransferWindow = showTransferWindow; - _configuration.Save(); - } - - if (_configuration.ShowTransferWindow) - { - ImGui.Indent(); - bool editTransferWindowPosition = _uiShared.EditTrackerPosition; - if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) - { - _uiShared.EditTrackerPosition = editTransferWindowPosition; - } - ImGui.Unindent(); - } - - if (ImGui.BeginTable("TransfersTable", 2)) - { - ImGui.TableSetupColumn( - $"Uploads ({UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Total))})"); - ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Total))})"); - - ImGui.TableHeadersRow(); - - ImGui.TableNextColumn(); - if (ImGui.BeginTable("UploadsTable", 3)) - { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Uploaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in _apiController.CurrentUploads.ToArray()) - { - var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); - ImGui.TableNextRow(); - } - - ImGui.EndTable(); - } - - ImGui.TableNextColumn(); - if (ImGui.BeginTable("DownloadsTable", 3)) - { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Downloaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in _apiController.CurrentDownloads.SelectMany(k => k.Value).ToArray()) - { - var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.Text(transfer.Hash); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.Text(UiShared.ByteToString(transfer.Total)); - ImGui.PopStyleColor(); - ImGui.TableNextRow(); - } - - ImGui.EndTable(); + ImGui.NextColumn(); } ImGui.EndTable(); } + + ImGui.TreePop(); } - private void DrawFileCacheSettings() + if (ImGui.TreeNode("Banned Users")) { - _uiShared.DrawFileScanState(); - _uiShared.DrawTimeSpanBetweenScansSetting(); - _uiShared.DrawCacheDirectorySetting(); - ImGui.Text($"Local cache size: {UiShared.ByteToString(_uiShared.FileCacheSize)}"); - ImGui.SameLine(); - if (ImGui.Button("Clear local cache")) + if (ImGui.BeginTable("BannedUsersTable", 3, ImGuiTableFlags.RowBg)) { - Task.Run(() => + ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 290); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var bannedUser in _apiController.AdminBannedUsers) { - foreach (var file in Directory.GetFiles(_configuration.CacheFolder)) + ImGui.TableNextColumn(); + ImGui.Text(bannedUser.CharacterHash); + + ImGui.TableNextColumn(); + string reason = bannedUser.Reason; + ImGuiInputTextFlags moderatorFlags = _apiController.IsModerator + ? ImGuiInputTextFlags.ReadOnly + : ImGuiInputTextFlags.None; + if (ImGui.InputText("##bannedReason" + bannedUser.CharacterHash, ref reason, 255, + moderatorFlags)) { - File.Delete(file); + bannedUser.Reason = reason; } - _uiShared.RecalculateFileCacheSize(); - }); + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (_apiController.IsAdmin) + { + if (ImGui.Button(FontAwesomeIcon.Upload.ToIconString() + "##updateUser" + + bannedUser.CharacterHash)) + { + _ = _apiController.AddOrUpdateBannedUserEntry(bannedUser); + } + + ImGui.SameLine(); + } + + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteUser" + + bannedUser.CharacterHash)) + { + _ = _apiController.DeleteBannedUserEntry(bannedUser); + } + + ImGui.PopFont(); + } + + ImGui.TableNextColumn(); + ImGui.InputText("##addUserHash", ref _bannedUserHashEntry, 255); + + ImGui.TableNextColumn(); + if (_apiController.IsAdmin) + { + ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255); + } + else + { + _bannedUserReasonEntry = "Banned by " + _uiShared.PlayerName; + ImGui.InputText("##addUserReason", ref _bannedUserReasonEntry, 255, + ImGuiInputTextFlags.ReadOnly); + } + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addForbiddenFile")) + { + _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto() + { + CharacterHash = _forbiddenFileHashForbiddenBy, + Reason = _forbiddenFileHashEntry + }); + } + + ImGui.PopFont(); + + ImGui.EndTable(); } + + ImGui.TreePop(); } - public override void OnClose() + if (ImGui.TreeNode("Online Users")) { - _uiShared.EditTrackerPosition = false; - base.OnClose(); + if (ImGui.Button("Refresh Online Users")) + { + _ = _apiController.RefreshOnlineUsers(); + } + + if (ImGui.BeginTable("OnlineUsersTable", 3, ImGuiTableFlags.RowBg)) + { + ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 100); + ImGui.TableSetupColumn("Character Hash", ImGuiTableColumnFlags.None, 300); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 70); + + ImGui.TableHeadersRow(); + + foreach (var onlineUser in _apiController.AdminOnlineUsers) + { + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + string icon = onlineUser.IsModerator + ? FontAwesomeIcon.ChessKing.ToIconString() + : onlineUser.IsAdmin + ? FontAwesomeIcon.Crown.ToIconString() + : FontAwesomeIcon.User.ToIconString(); + ImGui.Text(icon); + ImGui.PopFont(); + ImGui.SameLine(); + + ImGui.Text(onlineUser.UID); + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyUID" + + onlineUser.CharacterNameHash)) + { + ImGui.SetClipboardText(onlineUser.UID); + } + + ImGui.PopFont(); + + ImGui.TableNextColumn(); + string charNameHash = onlineUser.CharacterNameHash; + ImGui.InputText("##onlineUserHash" + onlineUser.CharacterNameHash, ref charNameHash, 255, + ImGuiInputTextFlags.ReadOnly); + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Copy.ToIconString() + "##onlineUserCopyHash" + + onlineUser.CharacterNameHash)) + { + ImGui.SetClipboardText(onlineUser.UID); + } + + ImGui.PopFont(); + + ImGui.TableNextColumn(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.SkullCrossbones.ToIconString() + "##onlineUserBan" + + onlineUser.CharacterNameHash)) + { + _ = _apiController.AddOrUpdateBannedUserEntry(new BannedUserDto + { + CharacterHash = onlineUser.CharacterNameHash, + Reason = "Banned by " + _uiShared.PlayerName + }); + } + ImGui.SameLine(); + if (onlineUser.UID != _apiController.UID && _apiController.IsAdmin) + { + if (!onlineUser.IsModerator) + { + if (ImGui.Button(FontAwesomeIcon.ChessKing.ToIconString() + + "##onlineUserModerator" + + onlineUser.CharacterNameHash)) + { + _apiController.PromoteToModerator(onlineUser.UID); + } + } + else + { + if (ImGui.Button(FontAwesomeIcon.User.ToIconString() + + "##onlineUserNonModerator" + + onlineUser.CharacterNameHash)) + { + _apiController.DemoteFromModerator(onlineUser.UID); + } + } + } + + ImGui.PopFont(); + } + ImGui.EndTable(); + } + ImGui.TreePop(); } } + + private bool _deleteFilesPopupModalShown = false; + private bool _deleteAccountPopupModalShown = false; + + private void DrawUserAdministration(bool serverAlive) + { + if (serverAlive) + { + if (ImGui.Button("Delete all my files")) + { + _deleteFilesPopupModalShown = true; + ImGui.OpenPopup("Delete all your files?"); + } + + UiShared.DrawHelpText("Completely deletes all your uploaded files on the service."); + + if (ImGui.BeginPopupModal("Delete all your files?", ref _deleteFilesPopupModalShown, + ImGuiWindowFlags.AlwaysAutoResize)) + { + UiShared.TextWrapped( + "All your own uploaded files on the service will be deleted.\nThis operation cannot be undone."); + ImGui.Text("Are you sure you want to continue?"); + ImGui.Separator(); + if (ImGui.Button("Delete everything", new Vector2(150, 0))) + { + Task.Run(() => _apiController.DeleteAllMyFiles()); + ImGui.CloseCurrentPopup(); + _deleteFilesPopupModalShown = false; + } + + ImGui.SameLine(); + + if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) + { + ImGui.CloseCurrentPopup(); + _deleteFilesPopupModalShown = false; + } + + ImGui.EndPopup(); + } + + if (ImGui.Button("Delete account")) + { + _deleteAccountPopupModalShown = true; + ImGui.OpenPopup("Delete your account?"); + } + + UiShared.DrawHelpText("Completely deletes your account and all uploaded files to the service."); + + if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, + ImGuiWindowFlags.AlwaysAutoResize)) + { + UiShared.TextWrapped( + "Your account and all associated files and data on the service will be deleted."); + UiShared.TextWrapped("Your UID will be removed from all pairing lists."); + ImGui.Text("Are you sure you want to continue?"); + ImGui.Separator(); + if (ImGui.Button("Delete account", new Vector2(150, 0))) + { + Task.Run(() => _apiController.DeleteAccount()); + ImGui.CloseCurrentPopup(); + _deleteAccountPopupModalShown = false; + SwitchToIntroUi?.Invoke(); + } + + ImGui.SameLine(); + + if (ImGui.Button("Cancel##cancelDelete", new Vector2(150, 0))) + { + ImGui.CloseCurrentPopup(); + _deleteAccountPopupModalShown = false; + } + + ImGui.EndPopup(); + } + } + + if (!_configuration.FullPause) + { + UiShared.ColorTextWrapped("Note: to change servers you need to disconnect from your current Mare Synchronos server.", ImGuiColors.DalamudYellow); + } + + var marePaused = _configuration.FullPause; + + if (_configuration.HasValidSetup()) + { + if (ImGui.Checkbox("Disconnect Mare Synchronos", ref marePaused)) + { + _configuration.FullPause = marePaused; + _configuration.Save(); + Task.Run(_apiController.CreateConnections); + } + + UiShared.DrawHelpText("Completely pauses the sync and clears your current data (not uploaded files) on the service."); + } + else + { + UiShared.ColorText("You cannot reconnect without a valid account on the service.", ImGuiColors.DalamudYellow); + } + + if (marePaused) + { + _uiShared.DrawServiceSelection(() => { }); + } + } + + private void DrawBlockedTransfers() + { + UiShared.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + + "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + + "Ask your paired friend to send you the mod in question through other means, acquire the mod yourself or pester the mod creator to allow it to be sent over Mare.", + ImGuiColors.DalamudGrey); + + if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) + { + ImGui.TableSetupColumn( + $"Hash/Filename"); + ImGui.TableSetupColumn($"Forbidden by"); + + ImGui.TableHeadersRow(); + + foreach (var item in _apiController.ForbiddenTransfers) + { + ImGui.TableNextColumn(); + if (item is UploadFileTransfer transfer) + { + ImGui.Text(transfer.LocalFile); + } + else + { + ImGui.Text(item.Hash); + } + ImGui.TableNextColumn(); + ImGui.Text(item.ForbiddenBy); + } + ImGui.EndTable(); + } + } + + private void DrawCurrentTransfers() + { + bool showTransferWindow = _configuration.ShowTransferWindow; + if (ImGui.Checkbox("Show separate Transfer window while transfers are active", ref showTransferWindow)) + { + _configuration.ShowTransferWindow = showTransferWindow; + _configuration.Save(); + } + + if (_configuration.ShowTransferWindow) + { + ImGui.Indent(); + bool editTransferWindowPosition = _uiShared.EditTrackerPosition; + if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) + { + _uiShared.EditTrackerPosition = editTransferWindowPosition; + } + ImGui.Unindent(); + } + + if (ImGui.BeginTable("TransfersTable", 2)) + { + ImGui.TableSetupColumn( + $"Uploads ({UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentUploads.Sum(a => a.Total))})"); + ImGui.TableSetupColumn($"Downloads ({UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Transferred))} / {UiShared.ByteToString(_apiController.CurrentDownloads.SelectMany(k => k.Value).ToList().Sum(a => a.Total))})"); + + ImGui.TableHeadersRow(); + + ImGui.TableNextColumn(); + if (ImGui.BeginTable("UploadsTable", 3)) + { + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Uploaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in _apiController.CurrentUploads.ToArray()) + { + var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.Text(transfer.Hash); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Total)); + ImGui.PopStyleColor(); + ImGui.TableNextRow(); + } + + ImGui.EndTable(); + } + + ImGui.TableNextColumn(); + if (ImGui.BeginTable("DownloadsTable", 3)) + { + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Downloaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in _apiController.CurrentDownloads.SelectMany(k => k.Value).ToArray()) + { + var color = UiShared.UploadColor((transfer.Transferred, transfer.Total)); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.Text(transfer.Hash); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.Text(UiShared.ByteToString(transfer.Total)); + ImGui.PopStyleColor(); + ImGui.TableNextRow(); + } + + ImGui.EndTable(); + } + + ImGui.EndTable(); + } + } + + private void DrawFileCacheSettings() + { + _uiShared.DrawFileScanState(); + _uiShared.DrawTimeSpanBetweenScansSetting(); + _uiShared.DrawCacheDirectorySetting(); + ImGui.Text($"Local cache size: {UiShared.ByteToString(_uiShared.FileCacheSize)}"); + ImGui.SameLine(); + if (ImGui.Button("Clear local cache")) + { + Task.Run(() => + { + foreach (var file in Directory.GetFiles(_configuration.CacheFolder)) + { + File.Delete(file); + } + + _uiShared.RecalculateFileCacheSize(); + }); + } + } + + public override void OnClose() + { + _uiShared.EditTrackerPosition = false; + base.OnClose(); + } } diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index 3164223..7534b34 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -18,539 +18,538 @@ using MareSynchronos.Managers; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -namespace MareSynchronos.UI +namespace MareSynchronos.UI; + +public class UiShared : IDisposable { - public class UiShared : IDisposable + [DllImport("user32")] + public static extern short GetKeyState(int nVirtKey); + + private readonly IpcManager _ipcManager; + private readonly ApiController _apiController; + private readonly PeriodicFileScanner _cacheScanner; + private readonly FileDialogManager _fileDialogManager; + private readonly Configuration _pluginConfiguration; + private readonly DalamudUtil _dalamudUtil; + private readonly DalamudPluginInterface _pluginInterface; + private readonly Dalamud.Localization _localization; + public long FileCacheSize => _cacheScanner.FileCacheSize; + public string PlayerName => _dalamudUtil.PlayerName; + public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory() ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory()); + public bool EditTrackerPosition { get; set; } + public ImFontPtr UidFont { get; private set; } + public bool UidFontBuilt { get; private set; } + + public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; + + public ApiController ApiController => _apiController; + + public UiShared(IpcManager ipcManager, ApiController apiController, PeriodicFileScanner cacheScanner, FileDialogManager fileDialogManager, + Configuration pluginConfiguration, DalamudUtil dalamudUtil, DalamudPluginInterface pluginInterface, Dalamud.Localization localization) { - [DllImport("user32")] - public static extern short GetKeyState(int nVirtKey); + _ipcManager = ipcManager; + _apiController = apiController; + _cacheScanner = cacheScanner; + _fileDialogManager = fileDialogManager; + _pluginConfiguration = pluginConfiguration; + _dalamudUtil = dalamudUtil; + _pluginInterface = pluginInterface; + _localization = localization; + _isDirectoryWritable = IsDirectoryWritable(_pluginConfiguration.CacheFolder); - private readonly IpcManager _ipcManager; - private readonly ApiController _apiController; - private readonly PeriodicFileScanner _cacheScanner; - private readonly FileDialogManager _fileDialogManager; - private readonly Configuration _pluginConfiguration; - private readonly DalamudUtil _dalamudUtil; - private readonly DalamudPluginInterface _pluginInterface; - private readonly Dalamud.Localization _localization; - public long FileCacheSize => _cacheScanner.FileCacheSize; - public string PlayerName => _dalamudUtil.PlayerName; - public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory() ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory()); - public bool EditTrackerPosition { get; set; } - public ImFontPtr UidFont { get; private set; } - public bool UidFontBuilt { get; private set; } + _pluginInterface.UiBuilder.BuildFonts += BuildFont; + _pluginInterface.UiBuilder.RebuildFonts(); + } - public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; + public static float GetWindowContentRegionWidth() + { + return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + } - public ApiController ApiController => _apiController; + public static Vector2 GetIconButtonSize(FontAwesomeIcon icon) + { + ImGui.PushFont(UiBuilder.IconFont); + var buttonSize = ImGuiHelpers.GetButtonSize(icon.ToIconString()); + ImGui.PopFont(); + return buttonSize; + } - public UiShared(IpcManager ipcManager, ApiController apiController, PeriodicFileScanner cacheScanner, FileDialogManager fileDialogManager, - Configuration pluginConfiguration, DalamudUtil dalamudUtil, DalamudPluginInterface pluginInterface, Dalamud.Localization localization) - { - _ipcManager = ipcManager; - _apiController = apiController; - _cacheScanner = cacheScanner; - _fileDialogManager = fileDialogManager; - _pluginConfiguration = pluginConfiguration; - _dalamudUtil = dalamudUtil; - _pluginInterface = pluginInterface; - _localization = localization; - _isDirectoryWritable = IsDirectoryWritable(_pluginConfiguration.CacheFolder); + private void BuildFont() + { + var fontFile = Path.Combine(_pluginInterface.DalamudAssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + UidFontBuilt = false; - _pluginInterface.UiBuilder.BuildFonts += BuildFont; - _pluginInterface.UiBuilder.RebuildFonts(); - } - - public static float GetWindowContentRegionWidth() - { - return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; - } - - public static Vector2 GetIconButtonSize(FontAwesomeIcon icon) - { - ImGui.PushFont(UiBuilder.IconFont); - var buttonSize = ImGuiHelpers.GetButtonSize(icon.ToIconString()); - ImGui.PopFont(); - return buttonSize; - } - - private void BuildFont() - { - var fontFile = Path.Combine(_pluginInterface.DalamudAssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - UidFontBuilt = false; - - if (File.Exists(fontFile)) - { - try - { - UidFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontFile, 35); - UidFontBuilt = true; - } - catch (Exception ex) - { - Logger.Debug($"Font failed to load. {fontFile}"); - Logger.Debug(ex.ToString()); - } - } - else - { - Logger.Debug($"Font doesn't exist. {fontFile}"); - } - } - - public static void DrawWithID(string id, Action drawSubSection) - { - ImGui.PushID(id); - drawSubSection.Invoke(); - ImGui.PopID(); - } - - public static void AttachToolTip(string text) - { - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip(text); - } - } - - public bool DrawOtherPluginState() - { - var penumbraExists = _ipcManager.CheckPenumbraApi(); - var glamourerExists = _ipcManager.CheckGlamourerApi(); - var heelsExists = _ipcManager.CheckHeelsApi(); - - var penumbraColor = penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var glamourerColor = glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - var heelsColor = heelsExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - ImGui.Text("Penumbra:"); - ImGui.SameLine(); - ImGui.TextColored(penumbraColor, penumbraExists ? "Available" : "Unavailable"); - ImGui.SameLine(); - ImGui.Text("Glamourer:"); - ImGui.SameLine(); - ImGui.TextColored(glamourerColor, glamourerExists ? "Available" : "Unavailable"); - ImGui.Text("Optional Addons"); - ImGui.SameLine(); - ImGui.Text("Heels:"); - ImGui.SameLine(); - ImGui.TextColored(heelsColor, heelsExists ? "Available" : "Unavailable"); - - if (!penumbraExists || !glamourerExists) - { - ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos."); - return false; - } - - return true; - } - - public void DrawFileScanState() - { - ImGui.Text("File Scanner Status"); - ImGui.SameLine(); - if (_cacheScanner.IsScanRunning) - { - ImGui.Text("Scan is running"); - ImGui.Text("Current Progress:"); - ImGui.SameLine(); - ImGui.Text(_cacheScanner.TotalFiles == 1 - ? "Collecting files" - : $"Processing {_cacheScanner.CurrentFileProgress} / {_cacheScanner.TotalFiles} files"); - } - else if (_pluginConfiguration.FileScanPaused) - { - ImGui.Text("File scanner is paused"); - ImGui.SameLine(); - if (ImGui.Button("Force Rescan##forcedrescan")) - { - _cacheScanner.InvokeScan(true); - } - } - else if (_cacheScanner.haltScanLocks.Any(f => f.Value > 0)) - { - ImGui.Text("Halted (" + string.Join(", ", _cacheScanner.haltScanLocks.Where(f => f.Value > 0).Select(locker => locker.Key + ": " + locker.Value + " halt requests")) + ")"); - ImGui.SameLine(); - if (ImGui.Button("Reset halt requests##clearlocks")) - { - _cacheScanner.ResetLocks(); - } - } - else - { - ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); - } - } - - public void PrintServerState() - { - var serverName = _apiController.ServerDictionary.ContainsKey(_pluginConfiguration.ApiUri) - ? _apiController.ServerDictionary[_pluginConfiguration.ApiUri] - : _pluginConfiguration.ApiUri; - if (_apiController.ServerState is ServerState.Connected) - { - ImGui.TextUnformatted("Service " + serverName + ":"); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); - ImGui.SameLine(); - ImGui.TextUnformatted("("); - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString()); - ImGui.SameLine(); - ImGui.Text("Users Online"); - ImGui.SameLine(); - ImGui.Text(")"); - } - } - - public static void ColorText(string text, Vector4 color) - { - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(text); - ImGui.PopStyleColor(); - } - - public static void ColorTextWrapped(string text, Vector4 color) - { - ImGui.PushStyleColor(ImGuiCol.Text, color); - TextWrapped(text); - ImGui.PopStyleColor(); - } - - public static void TextWrapped(string text) - { - ImGui.PushTextWrapPos(0); - ImGui.TextUnformatted(text); - ImGui.PopTextWrapPos(); - } - - public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : - input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; - - public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; - - public static Vector4 UploadColor((long, long) data) => data.Item1 == 0 ? ImGuiColors.DalamudGrey : - data.Item1 == data.Item2 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudYellow; - - public void LoadLocalization(string languageCode) - { - _localization.SetupWithLangCode(languageCode); - Strings.ToS = new Strings.ToSStrings(); - } - - public static uint Color(byte r, byte g, byte b, byte a) - { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } - - public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) - { - drawList.AddText(textPos with { Y = textPos.Y - thickness }, - outlineColor, text); - drawList.AddText(textPos with { X = textPos.X - thickness }, - outlineColor, text); - drawList.AddText(textPos with { Y = textPos.Y + thickness }, - outlineColor, text); - drawList.AddText(textPos with { X = textPos.X + thickness }, - outlineColor, text); - drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), - outlineColor, text); - drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), - outlineColor, text); - - drawList.AddText(textPos, fontColor, text); - drawList.AddText(textPos, fontColor, text); - } - - public static string ByteToString(long bytes) - { - string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; - int i; - double dblSByte = bytes; - for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) - { - dblSByte = bytes / 1024.0; - } - - return $"{dblSByte:0.00} {suffix[i]}"; - } - - private int _serverSelectionIndex = 0; - private string _customServerName = ""; - private string _customServerUri = ""; - private bool _enterSecretKey = false; - private bool _cacheDirectoryHasOtherFilesThanCache = false; - private bool _cacheDirectoryIsValidPath = true; - - public void DrawServiceSelection(Action? callBackOnExit = null) - { - string[] comboEntries = _apiController.ServerDictionary.Values.ToArray(); - _serverSelectionIndex = Array.IndexOf(_apiController.ServerDictionary.Keys.ToArray(), _pluginConfiguration.ApiUri); - if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) - { - for (int i = 0; i < comboEntries.Length; i++) - { - bool isSelected = _serverSelectionIndex == i; - if (ImGui.Selectable(comboEntries[i], isSelected)) - { - _pluginConfiguration.ApiUri = _apiController.ServerDictionary.Single(k => k.Value == comboEntries[i]).Key; - _pluginConfiguration.Save(); - _ = _apiController.CreateConnections(); - } - - if (isSelected) - { - ImGui.SetItemDefaultFocus(); - } - } - - ImGui.EndCombo(); - } - - if (_serverSelectionIndex != 0) - { - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteService")) - { - _pluginConfiguration.CustomServerList.Remove(_pluginConfiguration.ApiUri); - _pluginConfiguration.ApiUri = _apiController.ServerDictionary.First().Key; - _pluginConfiguration.Save(); - } - ImGui.PopFont(); - } - - if (ImGui.TreeNode("Add Custom Service")) - { - ImGui.SetNextItemWidth(250); - ImGui.InputText("Custom Service Name", ref _customServerName, 255); - ImGui.SetNextItemWidth(250); - ImGui.InputText("Custom Service Address", ref _customServerUri, 255); - if (ImGui.Button("Add Custom Service")) - { - if (!string.IsNullOrEmpty(_customServerUri) - && !string.IsNullOrEmpty(_customServerName) - && !_pluginConfiguration.CustomServerList.ContainsValue(_customServerName) - && !_pluginConfiguration.CustomServerList.ContainsKey(_customServerUri)) - { - _pluginConfiguration.CustomServerList[_customServerUri] = _customServerName; - _customServerUri = string.Empty; - _customServerName = string.Empty; - _pluginConfiguration.Save(); - } - } - ImGui.TreePop(); - } - - PrintServerState(); - - if (!_apiController.ServerAlive && (_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) && !_pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri].IsNullOrEmpty())) - { - ColorTextWrapped("You already have an account on this server.", ImGuiColors.DalamudYellow); - ImGui.SameLine(); - if (ImGui.Button("Connect##connectToService")) - { - _pluginConfiguration.FullPause = false; - _pluginConfiguration.Save(); - Task.Run(_apiController.CreateConnections); - } - } - - string checkboxText = _pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) - ? "I want to switch accounts" - : "I have an account"; - ImGui.Checkbox(checkboxText, ref _enterSecretKey); - - if (_enterSecretKey) - { - ColorTextWrapped("This will overwrite your currently used secret key for the selected service. Make sure to have a backup for the current secret key if you want to switch back to this account.", ImGuiColors.DalamudYellow); - if (!_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri)) - { - ColorTextWrapped("IF YOU HAVE NEVER MADE AN ACCOUNT BEFORE DO NOT ENTER ANYTHING HERE", ImGuiColors.DalamudYellow); - } - - var text = "Enter Secret Key"; - var buttonText = "Save"; - var buttonWidth = _secretKey.Length != 64 ? 0 : ImGuiHelpers.GetButtonSize(buttonText).X + ImGui.GetStyle().ItemSpacing.X; - var textSize = ImGui.CalcTextSize(text); - ImGui.AlignTextToFramePadding(); - ImGui.Text(text); - ImGui.SameLine(); - ImGui.SetNextItemWidth(GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); - ImGui.InputText("", ref _secretKey, 64); - if (_secretKey.Length > 0 && _secretKey.Length != 64) - { - ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); - } - else if (_secretKey.Length == 64) - { - ImGui.SameLine(); - if (ImGui.Button(buttonText)) - { - _pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri] = _secretKey; - _pluginConfiguration.Save(); - _secretKey = string.Empty; - Task.Run(_apiController.CreateConnections); - _enterSecretKey = false; - callBackOnExit?.Invoke(); - } - } - } - } - - private string _secretKey = ""; - - public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) - { - var cursorPos = ImGui.GetCursorPos(); - UiShared.ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); - UiShared.ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); - UiShared.ColorTextWrapped(text, outlineColor); - ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); - UiShared.ColorTextWrapped(text, outlineColor); - - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - UiShared.ColorTextWrapped(text, textcolor); - ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); - UiShared.ColorTextWrapped(text, textcolor); - } - - public static void DrawHelpText(string helpText) - { - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.SetWindowFontScale(0.8f); - ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); - ImGui.SetWindowFontScale(1.0f); - ImGui.PopFont(); - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); - ImGui.TextUnformatted(helpText); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } - } - - public void DrawCacheDirectorySetting() - { - ColorTextWrapped("Note: The cache folder should be somewhere close to root (i.e. C:\\MareCache) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow); - var cacheDirectory = _pluginConfiguration.CacheFolder; - ImGui.InputText("Cache Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly); - - ImGui.SameLine(); - ImGui.PushFont(UiBuilder.IconFont); - string folderIcon = FontAwesomeIcon.Folder.ToIconString(); - if (ImGui.Button(folderIcon + "##chooseCacheFolder")) - { - _fileDialogManager.OpenFolderDialog("Pick Mare Synchronos Cache Folder", (success, path) => - { - if (!success) return; - - _isPenumbraDirectory = path.ToLowerInvariant() == _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(); - _isDirectoryWritable = IsDirectoryWritable(path); - _cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40); - _cacheDirectoryIsValidPath = Regex.IsMatch(path, @"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript); - - if (!string.IsNullOrEmpty(path) - && Directory.Exists(path) - && _isDirectoryWritable - && !_isPenumbraDirectory - && !_cacheDirectoryHasOtherFilesThanCache - && _cacheDirectoryIsValidPath) - { - _pluginConfiguration.CacheFolder = path; - _pluginConfiguration.Save(); - _cacheScanner.StartScan(); - } - }); - } - ImGui.PopFont(); - - if (_isPenumbraDirectory) - { - ColorTextWrapped("Do not point the cache path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed); - } - else if (!_isDirectoryWritable) - { - ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed); - } - else if (_cacheDirectoryHasOtherFilesThanCache) - { - ColorTextWrapped("Your selected directory has files inside that are not Mare related. Use an empty directory or a previous Mare cache directory only.", ImGuiColors.DalamudRed); - } - else if (!_cacheDirectoryIsValidPath) - { - ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " + - "Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed); - } - - int maxCacheSize = _pluginConfiguration.MaxLocalCacheInGiB; - if (ImGui.SliderInt("Maximum Cache Size in GB", ref maxCacheSize, 1, 50, "%d GB")) - { - _pluginConfiguration.MaxLocalCacheInGiB = maxCacheSize; - _pluginConfiguration.Save(); - } - DrawHelpText("The cache is automatically governed by Mare. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); - } - - private bool _isDirectoryWritable = false; - private bool _isPenumbraDirectory = false; - - public bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + if (File.Exists(fontFile)) { try { - using (FileStream fs = File.Create( - Path.Combine( - dirPath, - Path.GetRandomFileName() - ), - 1, - FileOptions.DeleteOnClose) - ) - { } - return true; + UidFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontFile, 35); + UidFontBuilt = true; } - catch + catch (Exception ex) { - if (throwIfFails) - throw; - else - return false; + Logger.Debug($"Font failed to load. {fontFile}"); + Logger.Debug(ex.ToString()); } } - - public void RecalculateFileCacheSize() + else { - _cacheScanner.InvokeScan(true); - } - - public void DrawTimeSpanBetweenScansSetting() - { - var timeSpan = _pluginConfiguration.TimeSpanBetweenScansInSeconds; - if (ImGui.SliderInt("Seconds between scans##timespan", ref timeSpan, 20, 60)) - { - _pluginConfiguration.TimeSpanBetweenScansInSeconds = timeSpan; - _pluginConfiguration.Save(); - } - DrawHelpText("This is the time in seconds between file scans. Increase it to reduce system load. A too high setting can cause issues when manually fumbling about in the cache or Penumbra mods folders."); - var isPaused = _pluginConfiguration.FileScanPaused; - if (ImGui.Checkbox("Pause periodic file scan##filescanpause", ref isPaused)) - { - _pluginConfiguration.FileScanPaused = isPaused; - _pluginConfiguration.Save(); - } - DrawHelpText("This allows you to stop the periodic scans of your Penumbra and Mare cache directories. Use this to move the Mare cache and Penumbra mod folders around. If you enable this permanently, run a Force rescan after adding mods to Penumbra."); - } - - public void Dispose() - { - _pluginInterface.UiBuilder.BuildFonts -= BuildFont; + Logger.Debug($"Font doesn't exist. {fontFile}"); } } + + public static void DrawWithID(string id, Action drawSubSection) + { + ImGui.PushID(id); + drawSubSection.Invoke(); + ImGui.PopID(); + } + + public static void AttachToolTip(string text) + { + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(text); + } + } + + public bool DrawOtherPluginState() + { + var penumbraExists = _ipcManager.CheckPenumbraApi(); + var glamourerExists = _ipcManager.CheckGlamourerApi(); + var heelsExists = _ipcManager.CheckHeelsApi(); + + var penumbraColor = penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var glamourerColor = glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var heelsColor = heelsExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + ImGui.Text("Penumbra:"); + ImGui.SameLine(); + ImGui.TextColored(penumbraColor, penumbraExists ? "Available" : "Unavailable"); + ImGui.SameLine(); + ImGui.Text("Glamourer:"); + ImGui.SameLine(); + ImGui.TextColored(glamourerColor, glamourerExists ? "Available" : "Unavailable"); + ImGui.Text("Optional Addons"); + ImGui.SameLine(); + ImGui.Text("Heels:"); + ImGui.SameLine(); + ImGui.TextColored(heelsColor, heelsExists ? "Available" : "Unavailable"); + + if (!penumbraExists || !glamourerExists) + { + ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos."); + return false; + } + + return true; + } + + public void DrawFileScanState() + { + ImGui.Text("File Scanner Status"); + ImGui.SameLine(); + if (_cacheScanner.IsScanRunning) + { + ImGui.Text("Scan is running"); + ImGui.Text("Current Progress:"); + ImGui.SameLine(); + ImGui.Text(_cacheScanner.TotalFiles == 1 + ? "Collecting files" + : $"Processing {_cacheScanner.CurrentFileProgress} / {_cacheScanner.TotalFiles} files"); + } + else if (_pluginConfiguration.FileScanPaused) + { + ImGui.Text("File scanner is paused"); + ImGui.SameLine(); + if (ImGui.Button("Force Rescan##forcedrescan")) + { + _cacheScanner.InvokeScan(true); + } + } + else if (_cacheScanner.haltScanLocks.Any(f => f.Value > 0)) + { + ImGui.Text("Halted (" + string.Join(", ", _cacheScanner.haltScanLocks.Where(f => f.Value > 0).Select(locker => locker.Key + ": " + locker.Value + " halt requests")) + ")"); + ImGui.SameLine(); + if (ImGui.Button("Reset halt requests##clearlocks")) + { + _cacheScanner.ResetLocks(); + } + } + else + { + ImGui.Text("Next scan in " + _cacheScanner.TimeUntilNextScan); + } + } + + public void PrintServerState() + { + var serverName = _apiController.ServerDictionary.ContainsKey(_pluginConfiguration.ApiUri) + ? _apiController.ServerDictionary[_pluginConfiguration.ApiUri] + : _pluginConfiguration.ApiUri; + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.TextUnformatted("Service " + serverName + ":"); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); + ImGui.SameLine(); + ImGui.TextUnformatted("("); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString()); + ImGui.SameLine(); + ImGui.Text("Users Online"); + ImGui.SameLine(); + ImGui.Text(")"); + } + } + + public static void ColorText(string text, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(text); + ImGui.PopStyleColor(); + } + + public static void ColorTextWrapped(string text, Vector4 color) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + TextWrapped(text); + ImGui.PopStyleColor(); + } + + public static void TextWrapped(string text) + { + ImGui.PushTextWrapPos(0); + ImGui.TextUnformatted(text); + ImGui.PopTextWrapPos(); + } + + public static Vector4 GetCpuLoadColor(double input) => input < 50 ? ImGuiColors.ParsedGreen : + input < 90 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudRed; + + public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + + public static Vector4 UploadColor((long, long) data) => data.Item1 == 0 ? ImGuiColors.DalamudGrey : + data.Item1 == data.Item2 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudYellow; + + public void LoadLocalization(string languageCode) + { + _localization.SetupWithLangCode(languageCode); + Strings.ToS = new Strings.ToSStrings(); + } + + public static uint Color(byte r, byte g, byte b, byte a) + { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } + + public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) + { + drawList.AddText(textPos with { Y = textPos.Y - thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X - thickness }, + outlineColor, text); + drawList.AddText(textPos with { Y = textPos.Y + thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X + thickness }, + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), + outlineColor, text); + + drawList.AddText(textPos, fontColor, text); + drawList.AddText(textPos, fontColor, text); + } + + public static string ByteToString(long bytes) + { + string[] suffix = { "B", "KiB", "MiB", "GiB", "TiB" }; + int i; + double dblSByte = bytes; + for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return $"{dblSByte:0.00} {suffix[i]}"; + } + + private int _serverSelectionIndex = 0; + private string _customServerName = ""; + private string _customServerUri = ""; + private bool _enterSecretKey = false; + private bool _cacheDirectoryHasOtherFilesThanCache = false; + private bool _cacheDirectoryIsValidPath = true; + + public void DrawServiceSelection(Action? callBackOnExit = null) + { + string[] comboEntries = _apiController.ServerDictionary.Values.ToArray(); + _serverSelectionIndex = Array.IndexOf(_apiController.ServerDictionary.Keys.ToArray(), _pluginConfiguration.ApiUri); + if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) + { + for (int i = 0; i < comboEntries.Length; i++) + { + bool isSelected = _serverSelectionIndex == i; + if (ImGui.Selectable(comboEntries[i], isSelected)) + { + _pluginConfiguration.ApiUri = _apiController.ServerDictionary.Single(k => k.Value == comboEntries[i]).Key; + _pluginConfiguration.Save(); + _ = _apiController.CreateConnections(); + } + + if (isSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + if (_serverSelectionIndex != 0) + { + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString() + "##deleteService")) + { + _pluginConfiguration.CustomServerList.Remove(_pluginConfiguration.ApiUri); + _pluginConfiguration.ApiUri = _apiController.ServerDictionary.First().Key; + _pluginConfiguration.Save(); + } + ImGui.PopFont(); + } + + if (ImGui.TreeNode("Add Custom Service")) + { + ImGui.SetNextItemWidth(250); + ImGui.InputText("Custom Service Name", ref _customServerName, 255); + ImGui.SetNextItemWidth(250); + ImGui.InputText("Custom Service Address", ref _customServerUri, 255); + if (ImGui.Button("Add Custom Service")) + { + if (!string.IsNullOrEmpty(_customServerUri) + && !string.IsNullOrEmpty(_customServerName) + && !_pluginConfiguration.CustomServerList.ContainsValue(_customServerName) + && !_pluginConfiguration.CustomServerList.ContainsKey(_customServerUri)) + { + _pluginConfiguration.CustomServerList[_customServerUri] = _customServerName; + _customServerUri = string.Empty; + _customServerName = string.Empty; + _pluginConfiguration.Save(); + } + } + ImGui.TreePop(); + } + + PrintServerState(); + + if (!_apiController.ServerAlive && (_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) && !_pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri].IsNullOrEmpty())) + { + ColorTextWrapped("You already have an account on this server.", ImGuiColors.DalamudYellow); + ImGui.SameLine(); + if (ImGui.Button("Connect##connectToService")) + { + _pluginConfiguration.FullPause = false; + _pluginConfiguration.Save(); + Task.Run(_apiController.CreateConnections); + } + } + + string checkboxText = _pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri) + ? "I want to switch accounts" + : "I have an account"; + ImGui.Checkbox(checkboxText, ref _enterSecretKey); + + if (_enterSecretKey) + { + ColorTextWrapped("This will overwrite your currently used secret key for the selected service. Make sure to have a backup for the current secret key if you want to switch back to this account.", ImGuiColors.DalamudYellow); + if (!_pluginConfiguration.ClientSecret.ContainsKey(_pluginConfiguration.ApiUri)) + { + ColorTextWrapped("IF YOU HAVE NEVER MADE AN ACCOUNT BEFORE DO NOT ENTER ANYTHING HERE", ImGuiColors.DalamudYellow); + } + + var text = "Enter Secret Key"; + var buttonText = "Save"; + var buttonWidth = _secretKey.Length != 64 ? 0 : ImGuiHelpers.GetButtonSize(buttonText).X + ImGui.GetStyle().ItemSpacing.X; + var textSize = ImGui.CalcTextSize(text); + ImGui.AlignTextToFramePadding(); + ImGui.Text(text); + ImGui.SameLine(); + ImGui.SetNextItemWidth(GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonWidth - textSize.X); + ImGui.InputText("", ref _secretKey, 64); + if (_secretKey.Length > 0 && _secretKey.Length != 64) + { + ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); + } + else if (_secretKey.Length == 64) + { + ImGui.SameLine(); + if (ImGui.Button(buttonText)) + { + _pluginConfiguration.ClientSecret[_pluginConfiguration.ApiUri] = _secretKey; + _pluginConfiguration.Save(); + _secretKey = string.Empty; + Task.Run(_apiController.CreateConnections); + _enterSecretKey = false; + callBackOnExit?.Invoke(); + } + } + } + } + + private string _secretKey = ""; + + public static void OutlineTextWrapped(string text, Vector4 textcolor, Vector4 outlineColor, float dist = 3) + { + var cursorPos = ImGui.GetCursorPos(); + UiShared.ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + dist)); + UiShared.ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y)); + UiShared.ColorTextWrapped(text, outlineColor); + ImGui.SetCursorPos(new(cursorPos.X + dist, cursorPos.Y + dist)); + UiShared.ColorTextWrapped(text, outlineColor); + + ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); + UiShared.ColorTextWrapped(text, textcolor); + ImGui.SetCursorPos(new(cursorPos.X + dist / 2, cursorPos.Y + dist / 2)); + UiShared.ColorTextWrapped(text, textcolor); + } + + public static void DrawHelpText(string helpText) + { + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.SetWindowFontScale(0.8f); + ImGui.TextDisabled(FontAwesomeIcon.Question.ToIconString()); + ImGui.SetWindowFontScale(1.0f); + ImGui.PopFont(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); + ImGui.TextUnformatted(helpText); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + public void DrawCacheDirectorySetting() + { + ColorTextWrapped("Note: The cache folder should be somewhere close to root (i.e. C:\\MareCache) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow); + var cacheDirectory = _pluginConfiguration.CacheFolder; + ImGui.InputText("Cache Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly); + + ImGui.SameLine(); + ImGui.PushFont(UiBuilder.IconFont); + string folderIcon = FontAwesomeIcon.Folder.ToIconString(); + if (ImGui.Button(folderIcon + "##chooseCacheFolder")) + { + _fileDialogManager.OpenFolderDialog("Pick Mare Synchronos Cache Folder", (success, path) => + { + if (!success) return; + + _isPenumbraDirectory = path.ToLowerInvariant() == _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(); + _isDirectoryWritable = IsDirectoryWritable(path); + _cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40); + _cacheDirectoryIsValidPath = Regex.IsMatch(path, @"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript); + + if (!string.IsNullOrEmpty(path) + && Directory.Exists(path) + && _isDirectoryWritable + && !_isPenumbraDirectory + && !_cacheDirectoryHasOtherFilesThanCache + && _cacheDirectoryIsValidPath) + { + _pluginConfiguration.CacheFolder = path; + _pluginConfiguration.Save(); + _cacheScanner.StartScan(); + } + }); + } + ImGui.PopFont(); + + if (_isPenumbraDirectory) + { + ColorTextWrapped("Do not point the cache path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed); + } + else if (!_isDirectoryWritable) + { + ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed); + } + else if (_cacheDirectoryHasOtherFilesThanCache) + { + ColorTextWrapped("Your selected directory has files inside that are not Mare related. Use an empty directory or a previous Mare cache directory only.", ImGuiColors.DalamudRed); + } + else if (!_cacheDirectoryIsValidPath) + { + ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " + + "Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed); + } + + int maxCacheSize = _pluginConfiguration.MaxLocalCacheInGiB; + if (ImGui.SliderInt("Maximum Cache Size in GB", ref maxCacheSize, 1, 50, "%d GB")) + { + _pluginConfiguration.MaxLocalCacheInGiB = maxCacheSize; + _pluginConfiguration.Save(); + } + DrawHelpText("The cache is automatically governed by Mare. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); + } + + private bool _isDirectoryWritable = false; + private bool _isPenumbraDirectory = false; + + public bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + { + try + { + using (FileStream fs = File.Create( + Path.Combine( + dirPath, + Path.GetRandomFileName() + ), + 1, + FileOptions.DeleteOnClose) + ) + { } + return true; + } + catch + { + if (throwIfFails) + throw; + else + return false; + } + } + + public void RecalculateFileCacheSize() + { + _cacheScanner.InvokeScan(true); + } + + public void DrawTimeSpanBetweenScansSetting() + { + var timeSpan = _pluginConfiguration.TimeSpanBetweenScansInSeconds; + if (ImGui.SliderInt("Seconds between scans##timespan", ref timeSpan, 20, 60)) + { + _pluginConfiguration.TimeSpanBetweenScansInSeconds = timeSpan; + _pluginConfiguration.Save(); + } + DrawHelpText("This is the time in seconds between file scans. Increase it to reduce system load. A too high setting can cause issues when manually fumbling about in the cache or Penumbra mods folders."); + var isPaused = _pluginConfiguration.FileScanPaused; + if (ImGui.Checkbox("Pause periodic file scan##filescanpause", ref isPaused)) + { + _pluginConfiguration.FileScanPaused = isPaused; + _pluginConfiguration.Save(); + } + DrawHelpText("This allows you to stop the periodic scans of your Penumbra and Mare cache directories. Use this to move the Mare cache and Penumbra mod folders around. If you enable this permanently, run a Force rescan after adding mods to Penumbra."); + } + + public void Dispose() + { + _pluginInterface.UiBuilder.BuildFonts -= BuildFont; + } } diff --git a/MareSynchronos/Utils/Crypto.cs b/MareSynchronos/Utils/Crypto.cs index 36dba7c..a4062c0 100644 --- a/MareSynchronos/Utils/Crypto.cs +++ b/MareSynchronos/Utils/Crypto.cs @@ -4,32 +4,31 @@ using System.Security.Cryptography; using System.Text; using Dalamud.Game.ClientState.Objects.SubKinds; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +public class Crypto { - public class Crypto + public static string GetFileHash(string filePath) { - public static string GetFileHash(string filePath) - { - using SHA1CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", ""); - } + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", ""); + } - public static string GetHash(string stringToHash) - { - using SHA1CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); - } + public static string GetHash(string stringToHash) + { + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); + } - public static string GetHash256(string stringToHash) - { - using SHA256CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); - } + public static string GetHash256(string stringToHash) + { + using SHA256CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); + } - public static string GetHash256(PlayerCharacter character) - { - using SHA256CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", ""); - } + public static string GetHash256(PlayerCharacter character) + { + using SHA256CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(character.Name + character.HomeWorld.Id.ToString()))).Replace("-", ""); } } diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index d2eba06..4a9b979 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -11,232 +11,231 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using FFXIVClientStructs.FFXIV.Client.Game.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +public delegate void PlayerChange(Dalamud.Game.ClientState.Objects.Types.Character actor); + +public delegate void LogIn(); +public delegate void LogOut(); +public delegate void ClassJobChanged(); + +public delegate void FrameworkUpdate(); +public delegate void VoidDelegate(); + +public class DalamudUtil : IDisposable { - public delegate void PlayerChange(Dalamud.Game.ClientState.Objects.Types.Character actor); + private readonly ClientState _clientState; + private readonly ObjectTable _objectTable; + private readonly Framework _framework; + private readonly Condition _condition; - public delegate void LogIn(); - public delegate void LogOut(); - public delegate void ClassJobChanged(); + public event LogIn? LogIn; + public event LogOut? LogOut; + public event FrameworkUpdate? FrameworkUpdate; + public event ClassJobChanged? ClassJobChanged; + private uint? classJobId = 0; + public event FrameworkUpdate? DelayedFrameworkUpdate; + public event VoidDelegate? ZoneSwitchStart; + public event VoidDelegate? ZoneSwitchEnd; + private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; + private bool _sentBetweenAreas = false; - public delegate void FrameworkUpdate(); - public delegate void VoidDelegate(); - - public class DalamudUtil : IDisposable + public unsafe bool IsGameObjectPresent(IntPtr key) { - private readonly ClientState _clientState; - private readonly ObjectTable _objectTable; - private readonly Framework _framework; - private readonly Condition _condition; - - public event LogIn? LogIn; - public event LogOut? LogOut; - public event FrameworkUpdate? FrameworkUpdate; - public event ClassJobChanged? ClassJobChanged; - private uint? classJobId = 0; - public event FrameworkUpdate? DelayedFrameworkUpdate; - public event VoidDelegate? ZoneSwitchStart; - public event VoidDelegate? ZoneSwitchEnd; - private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; - private bool _sentBetweenAreas = false; - - public unsafe bool IsGameObjectPresent(IntPtr key) + foreach (var obj in _objectTable) { - foreach (var obj in _objectTable) + if (obj.Address == key) { - if (obj.Address == key) - { - return true; - } - } - - return false; - } - - public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework, Condition condition) - { - _clientState = clientState; - _objectTable = objectTable; - _framework = framework; - _condition = condition; - _clientState.Login += ClientStateOnLogin; - _clientState.Logout += ClientStateOnLogout; - _framework.Update += FrameworkOnUpdate; - if (IsLoggedIn) - { - classJobId = _clientState.LocalPlayer!.ClassJob.Id; - ClientStateOnLogin(null, EventArgs.Empty); + return true; } } - private void FrameworkOnUpdate(Framework framework) - { - if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || IsInGpose) - { - if (!_sentBetweenAreas) - { - Logger.Debug("Zone switch/Gpose start"); - _sentBetweenAreas = true; - ZoneSwitchStart?.Invoke(); - } + return false; + } - return; - } - else if (_sentBetweenAreas) + public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework, Condition condition) + { + _clientState = clientState; + _objectTable = objectTable; + _framework = framework; + _condition = condition; + _clientState.Login += ClientStateOnLogin; + _clientState.Logout += ClientStateOnLogout; + _framework.Update += FrameworkOnUpdate; + if (IsLoggedIn) + { + classJobId = _clientState.LocalPlayer!.ClassJob.Id; + ClientStateOnLogin(null, EventArgs.Empty); + } + } + + private void FrameworkOnUpdate(Framework framework) + { + if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || IsInGpose) + { + if (!_sentBetweenAreas) { - Logger.Debug("Zone switch/Gpose end"); - _sentBetweenAreas = false; - ZoneSwitchEnd?.Invoke(); + Logger.Debug("Zone switch/Gpose start"); + _sentBetweenAreas = true; + ZoneSwitchStart?.Invoke(); } - foreach (FrameworkUpdate? frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + return; + } + else if (_sentBetweenAreas) + { + Logger.Debug("Zone switch/Gpose end"); + _sentBetweenAreas = false; + ZoneSwitchEnd?.Invoke(); + } + + foreach (FrameworkUpdate? frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + { + try { - try - { - frameworkInvocation?.Invoke(); - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - } + frameworkInvocation?.Invoke(); } - - if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; - if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) + catch (Exception ex) { - var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; - - if (classJobId != newclassJobId) - { - classJobId = newclassJobId; - ClassJobChanged?.Invoke(); - } + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); } + } - foreach (FrameworkUpdate? frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; + if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) + { + var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; + + if (classJobId != newclassJobId) { - try - { - frameworkInvocation?.Invoke(); - } - catch (Exception ex) - { - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - } + classJobId = newclassJobId; + ClassJobChanged?.Invoke(); } - _delayedFrameworkUpdateCheck = DateTime.Now; } - private void ClientStateOnLogout(object? sender, EventArgs e) + foreach (FrameworkUpdate? frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) { - LogOut?.Invoke(); - } - - private void ClientStateOnLogin(object? sender, EventArgs e) - { - LogIn?.Invoke(); - } - - public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) - { - return _objectTable.CreateObjectReference(reference); - } - - public bool IsLoggedIn => _clientState.IsLoggedIn; - - public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); - - public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) - { - return obj != null && obj.IsValid(); - } - - public unsafe IntPtr GetMinion() - { - return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)PlayerPointer)->CompanionObject; - } - - public unsafe IntPtr GetPet(IntPtr? playerPointer = null) - { - var mgr = CharacterManager.Instance(); - if (playerPointer == null) playerPointer = PlayerPointer; - return (IntPtr)mgr->LookupPetByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); - } - - public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) - { - var mgr = CharacterManager.Instance(); - if (playerPointer == null) playerPointer = PlayerPointer; - return (IntPtr)mgr->LookupBuddyByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); - } - - public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; - - public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; - - public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; - - public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id); - - public bool IsInGpose => _objectTable[201] != null; - - public List GetPlayerCharacters() - { - return _objectTable.Where(obj => - obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && - obj.Name.ToString() != PlayerName).Select(p => (PlayerCharacter)p).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) + try { - if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - if (item.Name.ToString() == characterName) return (PlayerCharacter)item; + frameworkInvocation?.Invoke(); } - - return null; - } - - public async Task RunOnFrameworkThread(Func func) - { - return await _framework.RunOnFrameworkThread(func); - } - - public unsafe void WaitWhileCharacterIsDrawing(string name, IntPtr characterAddress, int timeOut = 5000, CancellationToken? ct = null) - { - if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; - - var obj = (GameObject*)characterAddress; - const int tick = 250; - int curWaitTime = 0; - // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something + catch (Exception ex) { - Logger.Verbose($"Waiting for {name} to finish drawing"); - curWaitTime += tick; - Thread.Sleep(tick); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); } + } + _delayedFrameworkUpdateCheck = DateTime.Now; + } - if (ct?.IsCancellationRequested ?? false) return; - // wait quarter a second just in case + private void ClientStateOnLogout(object? sender, EventArgs e) + { + LogOut?.Invoke(); + } + + private void ClientStateOnLogin(object? sender, EventArgs e) + { + LogIn?.Invoke(); + } + + public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) + { + return _objectTable.CreateObjectReference(reference); + } + + public bool IsLoggedIn => _clientState.IsLoggedIn; + + public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); + + public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) + { + return obj != null && obj.IsValid(); + } + + public unsafe IntPtr GetMinion() + { + return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)PlayerPointer)->CompanionObject; + } + + public unsafe IntPtr GetPet(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + if (playerPointer == null) playerPointer = PlayerPointer; + return (IntPtr)mgr->LookupPetByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); + } + + public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + if (playerPointer == null) playerPointer = PlayerPointer; + return (IntPtr)mgr->LookupBuddyByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); + } + + public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; + + public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; + + public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; + + public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id); + + public bool IsInGpose => _objectTable[201] != null; + + public List GetPlayerCharacters() + { + return _objectTable.Where(obj => + obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && + obj.Name.ToString() != PlayerName).Select(p => (PlayerCharacter)p).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 (item.Name.ToString() == characterName) return (PlayerCharacter)item; + } + + return null; + } + + public async Task RunOnFrameworkThread(Func func) + { + return await _framework.RunOnFrameworkThread(func); + } + + public unsafe void WaitWhileCharacterIsDrawing(string name, IntPtr characterAddress, int timeOut = 5000, CancellationToken? ct = null) + { + if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; + + var obj = (GameObject*)characterAddress; + const int tick = 250; + int curWaitTime = 0; + // ReSharper disable once LoopVariableIsNeverChangedInsideLoop + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something + { + Logger.Verbose($"Waiting for {name} to finish drawing"); + curWaitTime += tick; Thread.Sleep(tick); } - public void Dispose() - { - _clientState.Login -= ClientStateOnLogin; - _clientState.Logout -= ClientStateOnLogout; - _framework.Update -= FrameworkOnUpdate; - } + if (ct?.IsCancellationRequested ?? false) return; + // wait quarter a second just in case + Thread.Sleep(tick); + } + + public void Dispose() + { + _clientState.Login -= ClientStateOnLogin; + _clientState.Logout -= ClientStateOnLogout; + _framework.Update -= FrameworkOnUpdate; } } diff --git a/MareSynchronos/Utils/Logger.cs b/MareSynchronos/Utils/Logger.cs index dcf0b61..4d90bac 100644 --- a/MareSynchronos/Utils/Logger.cs +++ b/MareSynchronos/Utils/Logger.cs @@ -5,108 +5,107 @@ using Dalamud.Logging; using Dalamud.Utility; using Microsoft.Extensions.Logging; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +[ProviderAlias("Dalamud")] +public class DalamudLoggingProvider : ILoggerProvider { - [ProviderAlias("Dalamud")] - public class DalamudLoggingProvider : ILoggerProvider + private readonly ConcurrentDictionary _loggers = + new(StringComparer.OrdinalIgnoreCase); + + public DalamudLoggingProvider() { - private readonly ConcurrentDictionary _loggers = - new(StringComparer.OrdinalIgnoreCase); - - public DalamudLoggingProvider() - { - } - - public ILogger CreateLogger(string categoryName) - { - return _loggers.GetOrAdd(categoryName, name => new Logger(categoryName)); - } - - public void Dispose() - { - _loggers.Clear(); - } } - internal class Logger : ILogger + public ILogger CreateLogger(string categoryName) { - private readonly string name; + return _loggers.GetOrAdd(categoryName, name => new Logger(categoryName)); + } - public static void Info(string info) - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - PluginLog.Information($"[{caller}] {info}"); - } - - public static void Debug(string debug, string stringToHighlight = "") - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - if (debug.Contains(stringToHighlight) && !stringToHighlight.IsNullOrEmpty()) - { - PluginLog.Warning($"[{caller}] {debug}"); - } - else - { - PluginLog.Debug($"[{caller}] {debug}"); - } - } - - public static void Warn(string warn) - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; - PluginLog.Warning($"[{caller}] {warn}"); - } - - public static void Verbose(string verbose) - { - var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; -#if DEBUG - PluginLog.Debug($"[{caller}] {verbose}"); -#else - PluginLog.Verbose($"[{caller}] {verbose}"); -#endif - } - - public Logger(string name) - { - this.name = name; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) return; - - switch (logLevel) - { - case LogLevel.Debug: - PluginLog.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Error: - case LogLevel.Critical: - PluginLog.Error($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Information: - PluginLog.Information($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Warning: - PluginLog.Warning($"[{name}] [{eventId}] {formatter(state, exception)}"); - break; - case LogLevel.Trace: - default: -#if DEBUG - PluginLog.Verbose($"[{name}] [{eventId}] {formatter(state, exception)}"); -#else - PluginLog.Verbose($"[{name}] {eventId} {state} {formatter(state, exception)}"); -#endif - break; - } - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public IDisposable BeginScope(TState state) => default!; + public void Dispose() + { + _loggers.Clear(); } } + +internal class Logger : ILogger +{ + private readonly string name; + + public static void Info(string info) + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + PluginLog.Information($"[{caller}] {info}"); + } + + public static void Debug(string debug, string stringToHighlight = "") + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + if (debug.Contains(stringToHighlight) && !stringToHighlight.IsNullOrEmpty()) + { + PluginLog.Warning($"[{caller}] {debug}"); + } + else + { + PluginLog.Debug($"[{caller}] {debug}"); + } + } + + public static void Warn(string warn) + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + PluginLog.Warning($"[{caller}] {warn}"); + } + + public static void Verbose(string verbose) + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; +#if DEBUG + PluginLog.Debug($"[{caller}] {verbose}"); +#else + PluginLog.Verbose($"[{caller}] {verbose}"); +#endif + } + + public Logger(string name) + { + this.name = name; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) return; + + switch (logLevel) + { + case LogLevel.Debug: + PluginLog.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Error: + case LogLevel.Critical: + PluginLog.Error($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Information: + PluginLog.Information($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Warning: + PluginLog.Warning($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Trace: + default: +#if DEBUG + PluginLog.Verbose($"[{name}] [{eventId}] {formatter(state, exception)}"); +#else + PluginLog.Verbose($"[{name}] {eventId} {state} {formatter(state, exception)}"); +#endif + break; + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) => default!; +} diff --git a/MareSynchronos/Utils/Various.cs b/MareSynchronos/Utils/Various.cs index fa96e17..98fb104 100644 --- a/MareSynchronos/Utils/Various.cs +++ b/MareSynchronos/Utils/Various.cs @@ -6,27 +6,26 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; -namespace MareSynchronos.Utils +namespace MareSynchronos.Utils; + +public static class VariousExtensions { - public static class VariousExtensions + public static DateTime GetLinkerTime(Assembly assembly) { - public static DateTime GetLinkerTime(Assembly assembly) + const string BuildVersionMetadataPrefix = "+build"; + + var attribute = assembly.GetCustomAttribute(); + if (attribute?.InformationalVersion != null) { - const string BuildVersionMetadataPrefix = "+build"; - - var attribute = assembly.GetCustomAttribute(); - if (attribute?.InformationalVersion != null) + var value = attribute.InformationalVersion; + var index = value.IndexOf(BuildVersionMetadataPrefix); + if (index > 0) { - var value = attribute.InformationalVersion; - var index = value.IndexOf(BuildVersionMetadataPrefix); - if (index > 0) - { - value = value[(index + BuildVersionMetadataPrefix.Length)..]; - return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture); - } + value = value[(index + BuildVersionMetadataPrefix.Length)..]; + return DateTime.ParseExact(value, "yyyy-MM-ddTHH:mm:ss:fffZ", CultureInfo.InvariantCulture); } - - return default; } + + return default; } } diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs index 48e2712..cd6b637 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -13,306 +13,305 @@ using MareSynchronos.Utils; using MareSynchronos.WebAPI.Utils; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + private readonly HashSet _verifiedUploadedHashes; + + private int _downloadId = 0; + public void CancelUpload() { - private readonly HashSet _verifiedUploadedHashes; - - private int _downloadId = 0; - public void CancelUpload() + if (_uploadCancellationTokenSource != null) { - if (_uploadCancellationTokenSource != null) - { - Logger.Debug("Cancelling upload"); - _uploadCancellationTokenSource?.Cancel(); - _mareHub!.SendAsync(Api.SendFileAbortUpload); - CurrentUploads.Clear(); - } - } - - public async Task DeleteAllMyFiles() - { - await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles); - } - - private async Task DownloadFile(int downloadId, string hash, Uri downloadUri, CancellationToken ct) - { - using WebClient wc = new(); - wc.Headers.Add("Authorization", SecretKey); - DownloadProgressChangedEventHandler progChanged = (s, e) => - { - try - { - CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred = e.BytesReceived; - } - catch (Exception ex) - { - Logger.Warn("Could not set download progress for " + hash); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - } - }; - wc.DownloadProgressChanged += progChanged; - - string fileName = Path.GetTempFileName(); - - ct.Register(wc.CancelAsync); - - try - { - await wc.DownloadFileTaskAsync(downloadUri, fileName); - } - catch { } - - CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred = CurrentDownloads[downloadId].Single(f => f.Hash == hash).Total; - - wc.DownloadProgressChanged -= progChanged; - return fileName; - } - - public int GetDownloadId() => _downloadId++; - - public async Task DownloadFiles(int currentDownloadId, List fileReplacementDto, CancellationToken ct) - { - DownloadStarted?.Invoke(); - try - { - await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct); - } - catch - { - CancelDownload(currentDownloadId); - } - finally - { - DownloadFinished?.Invoke(); - } - } - - private async Task DownloadFilesInternal(int currentDownloadId, List fileReplacementDto, CancellationToken ct) - { - Logger.Debug("Downloading files (Download ID " + currentDownloadId + ")"); - - List downloadFileInfoFromService = new List(); - downloadFileInfoFromService.AddRange(await _mareHub!.InvokeAsync>(Api.InvokeGetFilesSizes, fileReplacementDto.Select(f => f.Hash).ToList(), ct)); - - Logger.Debug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); - - CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) - .Where(d => d.CanBeTransferred).ToList(); - - foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) - { - if (ForbiddenTransfers.All(f => f.Hash != dto.Hash)) - { - ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); - } - } - - await Parallel.ForEachAsync(CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred), new ParallelOptions() - { - MaxDegreeOfParallelism = 5, - CancellationToken = ct - }, - async (file, token) => - { - var hash = file.Hash; - var tempFile = await DownloadFile(currentDownloadId, file.Hash, file.DownloadUri, token); - if (token.IsCancellationRequested) - { - File.Delete(tempFile); - Logger.Debug("Detected cancellation, removing " + currentDownloadId); - DownloadFinished?.Invoke(); - CancelDownload(currentDownloadId); - return; - } - - var tempFileData = await File.ReadAllBytesAsync(tempFile, token); - var extractedFile = LZ4Codec.Unwrap(tempFileData); - File.Delete(tempFile); - var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash); - await File.WriteAllBytesAsync(filePath, extractedFile, token); - var fi = new FileInfo(filePath); - Func RandomDayFunc() - { - DateTime start = new DateTime(1995, 1, 1); - Random gen = new Random(); - int range = (DateTime.Today - start).Days; - return () => start.AddDays(gen.Next(range)); - } - - fi.CreationTime = RandomDayFunc().Invoke(); - fi.LastAccessTime = RandomDayFunc().Invoke(); - fi.LastWriteTime = RandomDayFunc().Invoke(); - try - { - _ = _fileDbManager.CreateCacheEntry(filePath); - } - catch (Exception ex) - { - Logger.Warn("Issue adding file to the DB"); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace); - } - }); - - Logger.Debug("Download complete, removing " + currentDownloadId); - CancelDownload(currentDownloadId); - } - - public async Task PushCharacterData(CharacterCacheDto character, List visibleCharacterIds) - { - if (!IsConnected || SecretKey == "-") return; - Logger.Debug("Sending Character data to service " + ApiUri); - - CancelUpload(); - _uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = _uploadCancellationTokenSource.Token; - Logger.Verbose("New Token Created"); - - List unverifiedUploadHashes = new(); - foreach (var item in character.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct()).Distinct().ToList()) - { - if (!_verifiedUploadedHashes.Contains(item)) - { - unverifiedUploadHashes.Add(item); - } - } - - if (unverifiedUploadHashes.Any()) - { - Logger.Debug("Verifying " + unverifiedUploadHashes.Count + " files"); - var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, unverifiedUploadHashes, uploadToken); - - foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) - { - try - { - CurrentUploads.Add(new UploadFileTransfer(file) - { - Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length - }); - } - catch (Exception ex) - { - Logger.Warn("Tried to request file " + file.Hash + " but file was not present"); - Logger.Warn(ex.StackTrace!); - } - } - - foreach (var file in filesToUpload.Where(c => c.IsForbidden)) - { - if (ForbiddenTransfers.All(f => f.Hash != file.Hash)) - { - ForbiddenTransfers.Add(new UploadFileTransfer(file) - { - LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty - }); - } - } - - var totalSize = CurrentUploads.Sum(c => c.Total); - Logger.Debug("Compressing and uploading files"); - foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) - { - Logger.Debug("Compressing and uploading " + file); - var data = await GetCompressedFileData(file.Hash, uploadToken); - CurrentUploads.Single(e => e.Hash == data.Item1).Total = data.Item2.Length; - await UploadFile(data.Item2, file.Hash, uploadToken); - if (!uploadToken.IsCancellationRequested) continue; - Logger.Warn("Cancel in filesToUpload loop detected"); - CurrentUploads.Clear(); - break; - } - - if (CurrentUploads.Any()) - { - var compressedSize = CurrentUploads.Sum(c => c.Total); - Logger.Debug($"Compressed {totalSize} to {compressedSize} ({(compressedSize / (double)totalSize):P2})"); - } - - Logger.Debug("Upload tasks complete, waiting for server to confirm"); - var anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken); - Logger.Debug("Uploads open: " + anyUploadsOpen); - while (anyUploadsOpen && !uploadToken.IsCancellationRequested) - { - anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken); - await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); - Logger.Debug("Waiting for uploads to finish"); - } - - foreach (var item in unverifiedUploadHashes) - { - _verifiedUploadedHashes.Add(item); - } - - CurrentUploads.Clear(); - } - else - { - Logger.Debug("All files already verified"); - } - - if (!uploadToken.IsCancellationRequested) - { - Logger.Info("Pushing character data for " + character.GetHashCode() + " to " + string.Join(", ", visibleCharacterIds)); - StringBuilder sb = new StringBuilder(); - foreach (var item in character.FileReplacements) - { - sb.AppendLine($"FileReplacements for {item.Key}: {item.Value.Count}"); - } - foreach (var item in character.GlamourerData) - { - sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); - } - Logger.Debug("Chara data contained: " + Environment.NewLine + sb.ToString()); - await _mareHub!.InvokeAsync(Api.InvokeUserPushCharacterDataToVisibleClients, character, visibleCharacterIds, uploadToken); - } - else - { - Logger.Warn("=== Upload operation was cancelled ==="); - } - - Logger.Verbose("Upload complete for " + character.GetHashCode()); - _uploadCancellationTokenSource = null; - } - - private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) - { - var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath; - return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken), 0, - (int)new FileInfo(fileCache).Length)); - } - - private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) - { - if (uploadToken.IsCancellationRequested) return; - - async IAsyncEnumerable AsyncFileData([EnumeratorCancellation] CancellationToken token) - { - var chunkSize = 1024 * 512; // 512kb - using var ms = new MemoryStream(compressedFile); - var buffer = new byte[chunkSize]; - int bytesRead; - while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token)) > 0 && !token.IsCancellationRequested) - { - CurrentUploads.Single(f => f.Hash == fileHash).Transferred += bytesRead; - token.ThrowIfCancellationRequested(); - yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); - } - } - - await _mareHub!.SendAsync(Api.SendFileUploadFileStreamAsync, fileHash, AsyncFileData(uploadToken), uploadToken); - } - - public void CancelDownload(int downloadId) - { - while (CurrentDownloads.ContainsKey(downloadId)) - { - CurrentDownloads.TryRemove(downloadId, out _); - } + Logger.Debug("Cancelling upload"); + _uploadCancellationTokenSource?.Cancel(); + _mareHub!.SendAsync(Api.SendFileAbortUpload); + CurrentUploads.Clear(); } } + public async Task DeleteAllMyFiles() + { + await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles); + } + + private async Task DownloadFile(int downloadId, string hash, Uri downloadUri, CancellationToken ct) + { + using WebClient wc = new(); + wc.Headers.Add("Authorization", SecretKey); + DownloadProgressChangedEventHandler progChanged = (s, e) => + { + try + { + CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred = e.BytesReceived; + } + catch (Exception ex) + { + Logger.Warn("Could not set download progress for " + hash); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + } + }; + wc.DownloadProgressChanged += progChanged; + + string fileName = Path.GetTempFileName(); + + ct.Register(wc.CancelAsync); + + try + { + await wc.DownloadFileTaskAsync(downloadUri, fileName); + } + catch { } + + CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred = CurrentDownloads[downloadId].Single(f => f.Hash == hash).Total; + + wc.DownloadProgressChanged -= progChanged; + return fileName; + } + + public int GetDownloadId() => _downloadId++; + + public async Task DownloadFiles(int currentDownloadId, List fileReplacementDto, CancellationToken ct) + { + DownloadStarted?.Invoke(); + try + { + await DownloadFilesInternal(currentDownloadId, fileReplacementDto, ct); + } + catch + { + CancelDownload(currentDownloadId); + } + finally + { + DownloadFinished?.Invoke(); + } + } + + private async Task DownloadFilesInternal(int currentDownloadId, List fileReplacementDto, CancellationToken ct) + { + Logger.Debug("Downloading files (Download ID " + currentDownloadId + ")"); + + List downloadFileInfoFromService = new List(); + downloadFileInfoFromService.AddRange(await _mareHub!.InvokeAsync>(Api.InvokeGetFilesSizes, fileReplacementDto.Select(f => f.Hash).ToList(), ct)); + + Logger.Debug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); + + CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) + .Where(d => d.CanBeTransferred).ToList(); + + foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) + { + if (ForbiddenTransfers.All(f => f.Hash != dto.Hash)) + { + ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); + } + } + + await Parallel.ForEachAsync(CurrentDownloads[currentDownloadId].Where(f => f.CanBeTransferred), new ParallelOptions() + { + MaxDegreeOfParallelism = 5, + CancellationToken = ct + }, + async (file, token) => + { + var hash = file.Hash; + var tempFile = await DownloadFile(currentDownloadId, file.Hash, file.DownloadUri, token); + if (token.IsCancellationRequested) + { + File.Delete(tempFile); + Logger.Debug("Detected cancellation, removing " + currentDownloadId); + DownloadFinished?.Invoke(); + CancelDownload(currentDownloadId); + return; + } + + var tempFileData = await File.ReadAllBytesAsync(tempFile, token); + var extractedFile = LZ4Codec.Unwrap(tempFileData); + File.Delete(tempFile); + var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash); + await File.WriteAllBytesAsync(filePath, extractedFile, token); + var fi = new FileInfo(filePath); + Func RandomDayFunc() + { + DateTime start = new DateTime(1995, 1, 1); + Random gen = new Random(); + int range = (DateTime.Today - start).Days; + return () => start.AddDays(gen.Next(range)); + } + + fi.CreationTime = RandomDayFunc().Invoke(); + fi.LastAccessTime = RandomDayFunc().Invoke(); + fi.LastWriteTime = RandomDayFunc().Invoke(); + try + { + _ = _fileDbManager.CreateCacheEntry(filePath); + } + catch (Exception ex) + { + Logger.Warn("Issue adding file to the DB"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace); + } + }); + + Logger.Debug("Download complete, removing " + currentDownloadId); + CancelDownload(currentDownloadId); + } + + public async Task PushCharacterData(CharacterCacheDto character, List visibleCharacterIds) + { + if (!IsConnected || SecretKey == "-") return; + Logger.Debug("Sending Character data to service " + ApiUri); + + CancelUpload(); + _uploadCancellationTokenSource = new CancellationTokenSource(); + var uploadToken = _uploadCancellationTokenSource.Token; + Logger.Verbose("New Token Created"); + + List unverifiedUploadHashes = new(); + foreach (var item in character.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct()).Distinct().ToList()) + { + if (!_verifiedUploadedHashes.Contains(item)) + { + unverifiedUploadHashes.Add(item); + } + } + + if (unverifiedUploadHashes.Any()) + { + Logger.Debug("Verifying " + unverifiedUploadHashes.Count + " files"); + var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, unverifiedUploadHashes, uploadToken); + + foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) + { + try + { + CurrentUploads.Add(new UploadFileTransfer(file) + { + Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length + }); + } + catch (Exception ex) + { + Logger.Warn("Tried to request file " + file.Hash + " but file was not present"); + Logger.Warn(ex.StackTrace!); + } + } + + foreach (var file in filesToUpload.Where(c => c.IsForbidden)) + { + if (ForbiddenTransfers.All(f => f.Hash != file.Hash)) + { + ForbiddenTransfers.Add(new UploadFileTransfer(file) + { + LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty + }); + } + } + + var totalSize = CurrentUploads.Sum(c => c.Total); + Logger.Debug("Compressing and uploading files"); + foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) + { + Logger.Debug("Compressing and uploading " + file); + var data = await GetCompressedFileData(file.Hash, uploadToken); + CurrentUploads.Single(e => e.Hash == data.Item1).Total = data.Item2.Length; + await UploadFile(data.Item2, file.Hash, uploadToken); + if (!uploadToken.IsCancellationRequested) continue; + Logger.Warn("Cancel in filesToUpload loop detected"); + CurrentUploads.Clear(); + break; + } + + if (CurrentUploads.Any()) + { + var compressedSize = CurrentUploads.Sum(c => c.Total); + Logger.Debug($"Compressed {totalSize} to {compressedSize} ({(compressedSize / (double)totalSize):P2})"); + } + + Logger.Debug("Upload tasks complete, waiting for server to confirm"); + var anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken); + Logger.Debug("Uploads open: " + anyUploadsOpen); + while (anyUploadsOpen && !uploadToken.IsCancellationRequested) + { + anyUploadsOpen = await _mareHub!.InvokeAsync(Api.InvokeFileIsUploadFinished, uploadToken); + await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); + Logger.Debug("Waiting for uploads to finish"); + } + + foreach (var item in unverifiedUploadHashes) + { + _verifiedUploadedHashes.Add(item); + } + + CurrentUploads.Clear(); + } + else + { + Logger.Debug("All files already verified"); + } + + if (!uploadToken.IsCancellationRequested) + { + Logger.Info("Pushing character data for " + character.GetHashCode() + " to " + string.Join(", ", visibleCharacterIds)); + StringBuilder sb = new StringBuilder(); + foreach (var item in character.FileReplacements) + { + sb.AppendLine($"FileReplacements for {item.Key}: {item.Value.Count}"); + } + foreach (var item in character.GlamourerData) + { + sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); + } + Logger.Debug("Chara data contained: " + Environment.NewLine + sb.ToString()); + await _mareHub!.InvokeAsync(Api.InvokeUserPushCharacterDataToVisibleClients, character, visibleCharacterIds, uploadToken); + } + else + { + Logger.Warn("=== Upload operation was cancelled ==="); + } + + Logger.Verbose("Upload complete for " + character.GetHashCode()); + _uploadCancellationTokenSource = null; + } + + private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + var fileCache = _fileDbManager.GetFileCacheByHash(fileHash)!.ResolvedFilepath; + return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken), 0, + (int)new FileInfo(fileCache).Length)); + } + + private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) + { + if (uploadToken.IsCancellationRequested) return; + + async IAsyncEnumerable AsyncFileData([EnumeratorCancellation] CancellationToken token) + { + var chunkSize = 1024 * 512; // 512kb + using var ms = new MemoryStream(compressedFile); + var buffer = new byte[chunkSize]; + int bytesRead; + while ((bytesRead = await ms.ReadAsync(buffer, 0, chunkSize, token)) > 0 && !token.IsCancellationRequested) + { + CurrentUploads.Single(f => f.Hash == fileHash).Transferred += bytesRead; + token.ThrowIfCancellationRequested(); + yield return bytesRead == chunkSize ? buffer.ToArray() : buffer.Take(bytesRead).ToArray(); + } + } + + await _mareHub!.SendAsync(Api.SendFileUploadFileStreamAsync, fileHash, AsyncFileData(uploadToken), uploadToken); + } + + public void CancelDownload(int downloadId) + { + while (CurrentDownloads.ContainsKey(downloadId)) + { + CurrentDownloads.TryRemove(downloadId, out _); + } + } } + diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs index 967045f..1e0223a 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Users.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Users.cs @@ -1,44 +1,43 @@ using System.Collections.Generic; using System.Threading.Tasks; using MareSynchronos.API; -using MareSynchronos.Utils; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + public async Task DeleteAccount() { - public async Task DeleteAccount() - { - _pluginConfiguration.ClientSecret.Remove(ApiUri); - _pluginConfiguration.Save(); - await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles); - await _mareHub!.SendAsync(Api.SendUserDeleteAccount); - await CreateConnections(); - } - - public async Task> GetOnlineCharacters() - { - return await _mareHub!.InvokeAsync>(Api.InvokeUserGetOnlineCharacters); - } - - public async Task SendPairedClientAddition(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _mareHub!.SendAsync(Api.SendUserPairedClientAddition, uid); - } - - public async Task SendPairedClientPauseChange(string uid, bool paused) - { - if (!IsConnected || SecretKey == "-") return; - await _mareHub!.SendAsync(Api.SendUserPairedClientPauseChange, uid, paused); - } - - public async Task SendPairedClientRemoval(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _mareHub!.SendAsync(Api.SendUserPairedClientRemoval, uid); - } + _pluginConfiguration.ClientSecret.Remove(ApiUri); + _pluginConfiguration.Save(); + await _mareHub!.SendAsync(Api.SendFileDeleteAllFiles); + await _mareHub!.SendAsync(Api.SendUserDeleteAccount); + await CreateConnections(); } + public async Task> GetOnlineCharacters() + { + return await _mareHub!.InvokeAsync>(Api.InvokeUserGetOnlineCharacters); + } + + public async Task SendPairedClientAddition(string uid) + { + if (!IsConnected || SecretKey == "-") return; + await _mareHub!.SendAsync(Api.SendUserPairedClientAddition, uid); + } + + public async Task SendPairedClientPauseChange(string uid, bool paused) + { + if (!IsConnected || SecretKey == "-") return; + await _mareHub!.SendAsync(Api.SendUserPairedClientPauseChange, uid, paused); + } + + public async Task SendPairedClientRemoval(string uid) + { + if (!IsConnected || SecretKey == "-") return; + await _mareHub!.SendAsync(Api.SendUserPairedClientRemoval, uid); + } } + + diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs deleted file mode 100644 index 6088d99..0000000 --- a/MareSynchronos/WebAPI/ApiController.Connectivity.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MareSynchronos.API; -using MareSynchronos.FileCache; -using MareSynchronos.Utils; -using MareSynchronos.WebAPI.Utils; -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.AspNetCore.SignalR; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.Logging; - -namespace MareSynchronos.WebAPI -{ - public delegate void SimpleStringDelegate(string str); - public enum ServerState - { - Offline, - Disconnected, - Connected, - Unauthorized, - VersionMisMatch, - RateLimited - } - - public partial class ApiController : IDisposable - { - public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; - public const string MainServiceUri = "wss://maresynchronos.com"; - - public readonly int[] SupportedServerVersions = { Api.Version }; - - private readonly Configuration _pluginConfiguration; - private readonly DalamudUtil _dalamudUtil; - private readonly FileCacheManager _fileDbManager; - private CancellationTokenSource _connectionCancellationTokenSource; - - private HubConnection? _mareHub; - - private CancellationTokenSource? _uploadCancellationTokenSource = new(); - - private ConnectionDto? _connectionDto; - public SystemInfoDto SystemInfoDto { get; private set; } = new(); - public bool IsModerator => (_connectionDto?.IsAdmin ?? false) || (_connectionDto?.IsModerator ?? false); - - public bool IsAdmin => _connectionDto?.IsAdmin ?? false; - - public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil, FileCacheManager fileDbManager) - { - Logger.Verbose("Creating " + nameof(ApiController)); - - _pluginConfiguration = pluginConfiguration; - _dalamudUtil = dalamudUtil; - _fileDbManager = fileDbManager; - _connectionCancellationTokenSource = new CancellationTokenSource(); - _dalamudUtil.LogIn += DalamudUtilOnLogIn; - _dalamudUtil.LogOut += DalamudUtilOnLogOut; - ServerState = ServerState.Offline; - _verifiedUploadedHashes = new(); - - if (_dalamudUtil.IsLoggedIn) - { - DalamudUtilOnLogIn(); - } - } - - private void DalamudUtilOnLogOut() - { - Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); - ServerState = ServerState.Offline; - } - - private void DalamudUtilOnLogIn() - { - Task.Run(CreateConnections); - } - - - public event EventHandler? CharacterReceived; - - public event VoidDelegate? Connected; - - public event VoidDelegate? Disconnected; - - public event SimpleStringDelegate? PairedClientOffline; - - public event SimpleStringDelegate? PairedClientOnline; - - public event SimpleStringDelegate? PairedWithOther; - - public event SimpleStringDelegate? UnpairedFromOther; - public event VoidDelegate? DownloadStarted; - public event VoidDelegate? DownloadFinished; - - public ConcurrentDictionary> CurrentDownloads { get; } = new(); - - public List CurrentUploads { get; } = new(); - - public List ForbiddenTransfers { get; } = new(); - - public List AdminBannedUsers { get; private set; } = new(); - - public List AdminForbiddenFiles { get; private set; } = new(); - - public bool IsConnected => ServerState == ServerState.Connected; - - public bool IsDownloading => CurrentDownloads.Count > 0; - - public bool IsUploading => CurrentUploads.Count > 0; - - public List PairedClients { get; set; } = new(); - - public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) - ? _pluginConfiguration.ClientSecret[ApiUri] : string.Empty; - - public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; - - public Dictionary ServerDictionary => new Dictionary() - { { MainServiceUri, MainServer } } - .Concat(_pluginConfiguration.CustomServerList) - .ToDictionary(k => k.Key, k => k.Value); - - public string UID => _connectionDto?.UID ?? string.Empty; - private string ApiUri => _pluginConfiguration.ApiUri; - public int OnlineUsers => SystemInfoDto.OnlineUsers; - - private ServerState _serverState; - public ServerState ServerState - { - get => _serverState; - private set - { - Logger.Debug($"New ServerState: {value}, prev ServerState: {_serverState}"); - _serverState = value; - } - } - - public async Task CreateConnections() - { - Logger.Debug("CreateConnections called"); - - if (_pluginConfiguration.FullPause) - { - Logger.Info("Not recreating Connection, paused"); - ServerState = ServerState.Disconnected; - _connectionDto = null; - await StopConnection(_connectionCancellationTokenSource.Token); - return; - } - - await StopConnection(_connectionCancellationTokenSource.Token); - - Logger.Info("Recreating Connection"); - - _connectionCancellationTokenSource.Cancel(); - _connectionCancellationTokenSource = new CancellationTokenSource(); - var token = _connectionCancellationTokenSource.Token; - _verifiedUploadedHashes.Clear(); - while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) - { - if (string.IsNullOrEmpty(SecretKey)) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - continue; - } - - await StopConnection(token); - - try - { - Logger.Debug("Building connection"); - - while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested) - { - Logger.Debug("Player not loaded in yet, waiting"); - await Task.Delay(TimeSpan.FromSeconds(1), token); - } - - if (token.IsCancellationRequested) break; - - _mareHub = BuildHubConnection(Api.Path); - - await _mareHub.StartAsync(token); - - _mareHub.On(Api.OnUpdateSystemInfo, (dto) => SystemInfoDto = dto); - - _connectionDto = - await _mareHub.InvokeAsync(Api.InvokeHeartbeat, _dalamudUtil.PlayerNameHashed, token); - - ServerState = ServerState.Connected; - - if (_connectionDto.ServerVersion != Api.Version) - { - ServerState = ServerState.VersionMisMatch; - await StopConnection(token); - return; - } - - if (ServerState is ServerState.Connected) // user is authorized && server is legit - { - await InitializeData(token); - - _mareHub.Closed += MareHubOnClosed; - _mareHub.Reconnecting += MareHubOnReconnecting; - _mareHub.Reconnected += MareHubOnReconnected; - } - } - catch (HubException ex) - { - Logger.Warn(ex.GetType().ToString()); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - - ServerState = ServerState.RateLimited; - await StopConnection(token); - return; - } - catch (HttpRequestException ex) - { - Logger.Warn(ex.GetType().ToString()); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - - if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - ServerState = ServerState.Unauthorized; - await StopConnection(token); - return; - } - else - { - ServerState = ServerState.Offline; - Logger.Info("Failed to establish connection, retrying"); - await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); - } - } - catch (Exception ex) - { - Logger.Warn(ex.GetType().ToString()); - Logger.Warn(ex.Message); - Logger.Warn(ex.StackTrace ?? string.Empty); - Logger.Info("Failed to establish connection, retrying"); - await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); - } - } - } - - private Task MareHubOnReconnected(string? arg) - { - _ = Task.Run(CreateConnections); - return Task.CompletedTask; - } - - private async Task InitializeData(CancellationToken token) - { - if (_mareHub == null) return; - - Logger.Debug("Initializing data"); - _mareHub.On(Api.OnUserUpdateClientPairs, - UpdateLocalClientPairsCallback); - _mareHub.On(Api.OnUserReceiveCharacterData, - ReceiveCharacterDataCallback); - _mareHub.On(Api.OnUserRemoveOnlinePairedPlayer, - (s) => PairedClientOffline?.Invoke(s)); - _mareHub.On(Api.OnUserAddOnlinePairedPlayer, - (s) => PairedClientOnline?.Invoke(s)); - _mareHub.On(Api.OnAdminForcedReconnect, UserForcedReconnectCallback); - - PairedClients = - await _mareHub!.InvokeAsync>(Api.InvokeUserGetPairedClients, token); - - if (IsModerator) - { - AdminForbiddenFiles = - await _mareHub.InvokeAsync>(Api.InvokeAdminGetForbiddenFiles, - token); - AdminBannedUsers = - await _mareHub.InvokeAsync>(Api.InvokeAdminGetBannedUsers, - token); - _mareHub.On(Api.OnAdminUpdateOrAddBannedUser, - UpdateOrAddBannedUserCallback); - _mareHub.On(Api.OnAdminDeleteBannedUser, DeleteBannedUserCallback); - _mareHub.On(Api.OnAdminUpdateOrAddForbiddenFile, - UpdateOrAddForbiddenFileCallback); - _mareHub.On(Api.OnAdminDeleteForbiddenFile, - DeleteForbiddenFileCallback); - } - - Connected?.Invoke(); - } - - public void Dispose() - { - Logger.Verbose("Disposing " + nameof(ApiController)); - - _dalamudUtil.LogIn -= DalamudUtilOnLogIn; - _dalamudUtil.LogOut -= DalamudUtilOnLogOut; - - Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); - _connectionCancellationTokenSource?.Cancel(); - } - - private HubConnection BuildHubConnection(string hubName) - { - return new HubConnectionBuilder() - .WithUrl(ApiUri + hubName, options => - { - options.Headers.Add("Authorization", SecretKey); - options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; - }) - .WithAutomaticReconnect(new ForeverRetryPolicy()) - .ConfigureLogging(a => { - a.ClearProviders().AddProvider(new DalamudLoggingProvider()); - a.SetMinimumLevel(LogLevel.Warning); - }) - .Build(); - } - - private Task MareHubOnClosed(Exception? arg) - { - CurrentUploads.Clear(); - CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); - Disconnected?.Invoke(); - ServerState = ServerState.Offline; - Logger.Info("Connection closed"); - return Task.CompletedTask; - } - - private Task MareHubOnReconnecting(Exception? arg) - { - ServerState = ServerState.Disconnected; - Logger.Warn("Connection closed... Reconnecting"); - Logger.Warn(arg?.Message ?? string.Empty); - Logger.Warn(arg?.StackTrace ?? string.Empty); - Disconnected?.Invoke(); - ServerState = ServerState.Offline; - return Task.CompletedTask; - } - - private async Task StopConnection(CancellationToken token) - { - if (_mareHub is not null) - { - _uploadCancellationTokenSource?.Cancel(); - Logger.Info("Stopping existing connection"); - _mareHub.Closed -= MareHubOnClosed; - _mareHub.Reconnecting -= MareHubOnReconnecting; - _mareHub.Reconnected -= MareHubOnReconnected; - await _mareHub.StopAsync(token); - await _mareHub.DisposeAsync(); - CurrentUploads.Clear(); - CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); - Disconnected?.Invoke(); - _mareHub = null; - } - - if (ServerState != ServerState.Disconnected) - { - while (ServerState != ServerState.Offline) - { - await Task.Delay(16); - } - } - } - } -} diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs b/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs index 868c4e9..33720c4 100644 --- a/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs +++ b/MareSynchronos/WebAPI/ApiController.Functions.Admin.cs @@ -3,45 +3,44 @@ using System.Threading.Tasks; using MareSynchronos.API; using Microsoft.AspNetCore.SignalR.Client; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + public async Task AddOrUpdateForbiddenFileEntry(ForbiddenFileDto forbiddenFile) { - public async Task AddOrUpdateForbiddenFileEntry(ForbiddenFileDto forbiddenFile) - { - await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddForbiddenFile, forbiddenFile); - } + await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddForbiddenFile, forbiddenFile); + } - public async Task DeleteForbiddenFileEntry(ForbiddenFileDto forbiddenFile) - { - await _mareHub!.SendAsync(Api.SendAdminDeleteForbiddenFile, forbiddenFile); - } + public async Task DeleteForbiddenFileEntry(ForbiddenFileDto forbiddenFile) + { + await _mareHub!.SendAsync(Api.SendAdminDeleteForbiddenFile, forbiddenFile); + } - public async Task AddOrUpdateBannedUserEntry(BannedUserDto bannedUser) - { - await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddBannedUser, bannedUser); - } + public async Task AddOrUpdateBannedUserEntry(BannedUserDto bannedUser) + { + await _mareHub!.SendAsync(Api.SendAdminUpdateOrAddBannedUser, bannedUser); + } - public async Task DeleteBannedUserEntry(BannedUserDto bannedUser) - { - await _mareHub!.SendAsync(Api.SendAdminDeleteBannedUser, bannedUser); - } + public async Task DeleteBannedUserEntry(BannedUserDto bannedUser) + { + await _mareHub!.SendAsync(Api.SendAdminDeleteBannedUser, bannedUser); + } - public async Task RefreshOnlineUsers() - { - AdminOnlineUsers = await _mareHub!.InvokeAsync>(Api.InvokeAdminGetOnlineUsers); - } + public async Task RefreshOnlineUsers() + { + AdminOnlineUsers = await _mareHub!.InvokeAsync>(Api.InvokeAdminGetOnlineUsers); + } - public List AdminOnlineUsers { get; set; } = new List(); + public List AdminOnlineUsers { get; set; } = new List(); - public void PromoteToModerator(string onlineUserUID) - { - _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, true); - } + public void PromoteToModerator(string onlineUserUID) + { + _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, true); + } - public void DemoteFromModerator(string onlineUserUID) - { - _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, false); - } + public void DemoteFromModerator(string onlineUserUID) + { + _mareHub!.SendAsync(Api.SendAdminChangeModeratorStatus, onlineUserUID, false); } } diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs index e565e71..1109b35 100644 --- a/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs +++ b/MareSynchronos/WebAPI/ApiController.Functions.Callbacks.cs @@ -4,75 +4,84 @@ using MareSynchronos.API; using MareSynchronos.Utils; using MareSynchronos.WebAPI.Utils; -namespace MareSynchronos.WebAPI +namespace MareSynchronos.WebAPI; + +public partial class ApiController { - public partial class ApiController + private void UserForcedReconnectCallback() { - private void UserForcedReconnectCallback() + _ = CreateConnections(); + } + + private void UpdateLocalClientPairsCallback(ClientPairDto dto) + { + var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); + if (dto.IsRemoved) { - _ = CreateConnections(); + PairedClients.RemoveAll(p => p.OtherUID == dto.OtherUID); + return; + } + if (entry == null) + { + PairedClients.Add(dto); + return; } - private void UpdateLocalClientPairsCallback(ClientPairDto dto) - { - var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); - if (dto.IsRemoved) - { - PairedClients.RemoveAll(p => p.OtherUID == dto.OtherUID); - return; - } - if (entry == null) - { - PairedClients.Add(dto); - return; - } + entry.IsPaused = dto.IsPaused; + entry.IsPausedFromOthers = dto.IsPausedFromOthers; + entry.IsSynced = dto.IsSynced; + } - entry.IsPaused = dto.IsPaused; - entry.IsPausedFromOthers = dto.IsPausedFromOthers; - entry.IsSynced = dto.IsSynced; + private Task ReceiveCharacterDataCallback(CharacterCacheDto character, string characterHash) + { + Logger.Verbose("Received DTO for " + characterHash); + CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); + return Task.CompletedTask; + } + + private void UpdateOrAddBannedUserCallback(BannedUserDto obj) + { + var user = AdminBannedUsers.SingleOrDefault(b => b.CharacterHash == obj.CharacterHash); + if (user == null) + { + AdminBannedUsers.Add(obj); } - - private Task ReceiveCharacterDataCallback(CharacterCacheDto character, string characterHash) + else { - Logger.Verbose("Received DTO for " + characterHash); - CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); - return Task.CompletedTask; - } - - private void UpdateOrAddBannedUserCallback(BannedUserDto obj) - { - var user = AdminBannedUsers.SingleOrDefault(b => b.CharacterHash == obj.CharacterHash); - if (user == null) - { - AdminBannedUsers.Add(obj); - } - else - { - user.Reason = obj.Reason; - } - } - - private void DeleteBannedUserCallback(BannedUserDto obj) - { - AdminBannedUsers.RemoveAll(a => a.CharacterHash == obj.CharacterHash); - } - - private void UpdateOrAddForbiddenFileCallback(ForbiddenFileDto obj) - { - var user = AdminForbiddenFiles.SingleOrDefault(b => b.Hash == obj.Hash); - if (user == null) - { - AdminForbiddenFiles.Add(obj); - } - else - { - user.ForbiddenBy = obj.ForbiddenBy; - } - } - - private void DeleteForbiddenFileCallback(ForbiddenFileDto obj) - { - AdminForbiddenFiles.RemoveAll(f => f.Hash == obj.Hash); + user.Reason = obj.Reason; } } + + private void DeleteBannedUserCallback(BannedUserDto obj) + { + AdminBannedUsers.RemoveAll(a => a.CharacterHash == obj.CharacterHash); + } + + private void UpdateOrAddForbiddenFileCallback(ForbiddenFileDto obj) + { + var user = AdminForbiddenFiles.SingleOrDefault(b => b.Hash == obj.Hash); + if (user == null) + { + AdminForbiddenFiles.Add(obj); + } + else + { + user.ForbiddenBy = obj.ForbiddenBy; + } + } + + private void DeleteForbiddenFileCallback(ForbiddenFileDto obj) + { + AdminForbiddenFiles.RemoveAll(f => f.Hash == obj.Hash); + } + + private void GroupPairChangedCallback(GroupPairDto dto) + { + + } + + private void GroupChangedCallback(GroupDto dto) + { + + } } diff --git a/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs new file mode 100644 index 0000000..618c7bf --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.Functions.Groups.cs @@ -0,0 +1,64 @@ +using MareSynchronos.API; +using Microsoft.AspNetCore.SignalR.Client; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MareSynchronos.WebAPI; +public partial class ApiController +{ + public async Task CreateGroup() + { + return await _mareHub!.InvokeAsync(Api.InvokeGroupCreate); + } + + public async Task ChangeGroupPassword(string gid, string newpassword) + { + return await _mareHub!.InvokeAsync(Api.InvokeGroupChangePassword, gid, newpassword); + } + + public async Task> GetGroups() + { + return await _mareHub!.InvokeAsync>(Api.InvokeGroupGetGroups); + } + + public async Task> GetUsersInGroup(string gid) + { + return await _mareHub!.InvokeAsync>(Api.InvokeGroupGetUsersInGroup, gid); + } + + public async Task SendGroupJoin(string gid, string password) + { + if (!IsConnected || SecretKey == "-") return; + await _mareHub!.SendAsync(Api.SendGroupJoin, gid, password); + } + + public async Task SendGroupChangeInviteState(string gid, bool opened) + { + await _mareHub!.SendAsync(Api.SendGroupChangeInviteState, gid, opened); + } + + public async Task SendDeleteGroup(string gid) + { + await _mareHub!.SendAsync(Api.SendGroupDelete, gid); + } + + public async Task SendLeaveGroup(string gid) + { + await _mareHub!.SendAsync(Api.SendGroupLeave, gid); + } + + public async Task SendPauseGroup(string gid, bool isPaused) + { + await _mareHub!.SendAsync(Api.SendGroupPause, gid, isPaused); + } + + public async Task SendRemoveUserFromGroup(string gid, string uid) + { + await _mareHub!.SendAsync(Api.SendGroupRemoveUser, gid, uid); + } + + public async Task ChangeOwnerOfGroup(string gid, string uid) + { + await _mareHub!.SendAsync(Api.SendGroupChangeOwner, gid, uid); + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs new file mode 100644 index 0000000..f0e6481 --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using MareSynchronos.API; +using MareSynchronos.FileCache; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Utils; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.WebAPI; + +public delegate void SimpleStringDelegate(string str); + +public partial class ApiController : IDisposable +{ + public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; + public const string MainServiceUri = "wss://maresynchronos.com"; + + public readonly int[] SupportedServerVersions = { Api.Version }; + + private readonly Configuration _pluginConfiguration; + private readonly DalamudUtil _dalamudUtil; + private readonly FileCacheManager _fileDbManager; + private CancellationTokenSource _connectionCancellationTokenSource; + + private HubConnection? _mareHub; + + private CancellationTokenSource? _uploadCancellationTokenSource = new(); + + private ConnectionDto? _connectionDto; + public SystemInfoDto SystemInfoDto { get; private set; } = new(); + public bool IsModerator => (_connectionDto?.IsAdmin ?? false) || (_connectionDto?.IsModerator ?? false); + + public bool IsAdmin => _connectionDto?.IsAdmin ?? false; + + public ApiController(Configuration pluginConfiguration, DalamudUtil dalamudUtil, FileCacheManager fileDbManager) + { + Logger.Verbose("Creating " + nameof(ApiController)); + + _pluginConfiguration = pluginConfiguration; + _dalamudUtil = dalamudUtil; + _fileDbManager = fileDbManager; + _connectionCancellationTokenSource = new CancellationTokenSource(); + _dalamudUtil.LogIn += DalamudUtilOnLogIn; + _dalamudUtil.LogOut += DalamudUtilOnLogOut; + ServerState = ServerState.Offline; + _verifiedUploadedHashes = new(); + + if (_dalamudUtil.IsLoggedIn) + { + DalamudUtilOnLogIn(); + } + } + + private void DalamudUtilOnLogOut() + { + Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); + ServerState = ServerState.Offline; + } + + private void DalamudUtilOnLogIn() + { + Task.Run(CreateConnections); + } + + + public event EventHandler? CharacterReceived; + + public event VoidDelegate? Connected; + + public event VoidDelegate? Disconnected; + + public event SimpleStringDelegate? PairedClientOffline; + + public event SimpleStringDelegate? PairedClientOnline; + + public event SimpleStringDelegate? PairedWithOther; + + public event SimpleStringDelegate? UnpairedFromOther; + public event VoidDelegate? DownloadStarted; + public event VoidDelegate? DownloadFinished; + + public ConcurrentDictionary> CurrentDownloads { get; } = new(); + + public List CurrentUploads { get; } = new(); + + public List ForbiddenTransfers { get; } = new(); + + public List AdminBannedUsers { get; private set; } = new(); + + public List AdminForbiddenFiles { get; private set; } = new(); + + public bool IsConnected => ServerState == ServerState.Connected; + + public bool IsDownloading => CurrentDownloads.Count > 0; + + public bool IsUploading => CurrentUploads.Count > 0; + + public List PairedClients { get; set; } = new(); + public List GroupPairedClients { get; set; } = new(); + public List Groups { get; set; } = new(); + + public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) + ? _pluginConfiguration.ClientSecret[ApiUri] : string.Empty; + + public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; + + public Dictionary ServerDictionary => new Dictionary() + { { MainServiceUri, MainServer } } + .Concat(_pluginConfiguration.CustomServerList) + .ToDictionary(k => k.Key, k => k.Value); + + public string UID => _connectionDto?.UID ?? string.Empty; + private string ApiUri => _pluginConfiguration.ApiUri; + public int OnlineUsers => SystemInfoDto.OnlineUsers; + + private ServerState _serverState; + public ServerState ServerState + { + get => _serverState; + private set + { + Logger.Debug($"New ServerState: {value}, prev ServerState: {_serverState}"); + _serverState = value; + } + } + + public async Task CreateConnections() + { + Logger.Debug("CreateConnections called"); + + if (_pluginConfiguration.FullPause) + { + Logger.Info("Not recreating Connection, paused"); + ServerState = ServerState.Disconnected; + _connectionDto = null; + await StopConnection(_connectionCancellationTokenSource.Token); + return; + } + + await StopConnection(_connectionCancellationTokenSource.Token); + + Logger.Info("Recreating Connection"); + + _connectionCancellationTokenSource.Cancel(); + _connectionCancellationTokenSource = new CancellationTokenSource(); + var token = _connectionCancellationTokenSource.Token; + _verifiedUploadedHashes.Clear(); + while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) + { + if (string.IsNullOrEmpty(SecretKey)) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + continue; + } + + await StopConnection(token); + + try + { + Logger.Debug("Building connection"); + + while (!_dalamudUtil.IsPlayerPresent && !token.IsCancellationRequested) + { + Logger.Debug("Player not loaded in yet, waiting"); + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + + if (token.IsCancellationRequested) break; + + _mareHub = BuildHubConnection(Api.Path); + + await _mareHub.StartAsync(token); + + _mareHub.On(Api.OnUpdateSystemInfo, (dto) => SystemInfoDto = dto); + + _connectionDto = + await _mareHub.InvokeAsync(Api.InvokeHeartbeat, _dalamudUtil.PlayerNameHashed, token); + + ServerState = ServerState.Connected; + + if (_connectionDto.ServerVersion != Api.Version) + { + ServerState = ServerState.VersionMisMatch; + await StopConnection(token); + return; + } + + if (ServerState is ServerState.Connected) // user is authorized && server is legit + { + await InitializeData(token); + + _mareHub.Closed += MareHubOnClosed; + _mareHub.Reconnecting += MareHubOnReconnecting; + _mareHub.Reconnected += MareHubOnReconnected; + } + } + catch (HubException ex) + { + Logger.Warn(ex.GetType().ToString()); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + + ServerState = ServerState.RateLimited; + await StopConnection(token); + return; + } + catch (HttpRequestException ex) + { + Logger.Warn(ex.GetType().ToString()); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + + if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + ServerState = ServerState.Unauthorized; + await StopConnection(token); + return; + } + else + { + ServerState = ServerState.Offline; + Logger.Info("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); + } + } + catch (Exception ex) + { + Logger.Warn(ex.GetType().ToString()); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + Logger.Info("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token); + } + } + } + + private Task MareHubOnReconnected(string? arg) + { + _ = Task.Run(CreateConnections); + return Task.CompletedTask; + } + + private async Task InitializeData(CancellationToken token) + { + if (_mareHub == null) return; + + Logger.Debug("Initializing data"); + _mareHub.On(Api.OnUserUpdateClientPairs, + UpdateLocalClientPairsCallback); + _mareHub.On(Api.OnUserReceiveCharacterData, + ReceiveCharacterDataCallback); + _mareHub.On(Api.OnUserRemoveOnlinePairedPlayer, + (s) => PairedClientOffline?.Invoke(s)); + _mareHub.On(Api.OnUserAddOnlinePairedPlayer, + (s) => PairedClientOnline?.Invoke(s)); + _mareHub.On(Api.OnAdminForcedReconnect, UserForcedReconnectCallback); + _mareHub.On(Api.OnGroupChange, GroupChangedCallback); + _mareHub.On(Api.OnGroupUserChange, GroupPairChangedCallback); + + PairedClients = + await _mareHub!.InvokeAsync>(Api.InvokeUserGetPairedClients, token); + Groups = await GetGroups(); + foreach (var group in Groups) + { + GroupPairedClients.AddRange(await GetUsersInGroup(group.GID)); + } + + if (IsModerator) + { + AdminForbiddenFiles = + await _mareHub.InvokeAsync>(Api.InvokeAdminGetForbiddenFiles, + token); + AdminBannedUsers = + await _mareHub.InvokeAsync>(Api.InvokeAdminGetBannedUsers, + token); + _mareHub.On(Api.OnAdminUpdateOrAddBannedUser, + UpdateOrAddBannedUserCallback); + _mareHub.On(Api.OnAdminDeleteBannedUser, DeleteBannedUserCallback); + _mareHub.On(Api.OnAdminUpdateOrAddForbiddenFile, + UpdateOrAddForbiddenFileCallback); + _mareHub.On(Api.OnAdminDeleteForbiddenFile, + DeleteForbiddenFileCallback); + } + + Connected?.Invoke(); + } + + public void Dispose() + { + Logger.Verbose("Disposing " + nameof(ApiController)); + + _dalamudUtil.LogIn -= DalamudUtilOnLogIn; + _dalamudUtil.LogOut -= DalamudUtilOnLogOut; + + Task.Run(async () => await StopConnection(_connectionCancellationTokenSource.Token)); + _connectionCancellationTokenSource?.Cancel(); + } + + private HubConnection BuildHubConnection(string hubName) + { + return new HubConnectionBuilder() + .WithUrl(ApiUri + hubName, options => + { + options.Headers.Add("Authorization", SecretKey); + options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; + }) + .WithAutomaticReconnect(new ForeverRetryPolicy()) + .ConfigureLogging(a => + { + a.ClearProviders().AddProvider(new DalamudLoggingProvider()); + a.SetMinimumLevel(LogLevel.Warning); + }) + .Build(); + } + + private Task MareHubOnClosed(Exception? arg) + { + CurrentUploads.Clear(); + CurrentDownloads.Clear(); + _uploadCancellationTokenSource?.Cancel(); + Disconnected?.Invoke(); + ServerState = ServerState.Offline; + Logger.Info("Connection closed"); + return Task.CompletedTask; + } + + private Task MareHubOnReconnecting(Exception? arg) + { + ServerState = ServerState.Disconnected; + Logger.Warn("Connection closed... Reconnecting"); + Logger.Warn(arg?.Message ?? string.Empty); + Logger.Warn(arg?.StackTrace ?? string.Empty); + Disconnected?.Invoke(); + ServerState = ServerState.Offline; + return Task.CompletedTask; + } + + private async Task StopConnection(CancellationToken token) + { + if (_mareHub is not null) + { + _uploadCancellationTokenSource?.Cancel(); + Logger.Info("Stopping existing connection"); + _mareHub.Closed -= MareHubOnClosed; + _mareHub.Reconnecting -= MareHubOnReconnecting; + _mareHub.Reconnected -= MareHubOnReconnected; + await _mareHub.StopAsync(token); + await _mareHub.DisposeAsync(); + CurrentUploads.Clear(); + CurrentDownloads.Clear(); + _uploadCancellationTokenSource?.Cancel(); + Disconnected?.Invoke(); + _mareHub = null; + } + + if (ServerState != ServerState.Disconnected) + { + while (ServerState != ServerState.Offline) + { + await Task.Delay(16); + } + } + } +} diff --git a/MareSynchronos/WebAPI/ServerState.cs b/MareSynchronos/WebAPI/ServerState.cs new file mode 100644 index 0000000..d146f26 --- /dev/null +++ b/MareSynchronos/WebAPI/ServerState.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.WebAPI; + +public enum ServerState +{ + Offline, + Disconnected, + Connected, + Unauthorized, + VersionMisMatch, + RateLimited +}