From 2dcd02d170c458bd95f8affccd3f9ac8939a9f61 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Fri, 24 Jun 2022 00:47:47 +0200 Subject: [PATCH] major refactoring, maybe some bugfixes, idk --- MareSynchronos/Configuration.cs | 34 +- ...acheFactory.cs => CharacterDataFactory.cs} | 28 +- MareSynchronos/Factories/FileCacheFactory.cs | 2 +- .../Factories/FileReplacementFactory.cs | 14 +- MareSynchronos/FileCacheDB/FileCache.cs | 5 +- .../FileCacheDB/FileCacheContext.cs | 1 - .../Managers/CharacterCacheManager.cs | 250 +++++++++ MareSynchronos/Managers/CharacterManager.cs | 433 +++------------ MareSynchronos/Managers/FileCacheManager.cs | 17 +- MareSynchronos/Managers/IpcManager.cs | 256 +++++---- MareSynchronos/Models/CachedPlayer.cs | 43 ++ .../{CharacterCache.cs => CharacterData.cs} | 41 +- MareSynchronos/Models/FileReplacement.cs | 123 +++-- MareSynchronos/PenumbraMod/DefaultMod.cs | 19 - MareSynchronos/PenumbraMod/Meta.cs | 21 - MareSynchronos/Plugin.cs | 53 +- MareSynchronos/UI/IntroUI.cs | 10 +- MareSynchronos/UI/PluginUI.cs | 10 +- MareSynchronos/UI/UIShared.cs | 9 +- MareSynchronos/Utils/DalamudUtil.cs | 59 ++ MareSynchronos/Utils/Logger.cs | 14 + MareSynchronos/WebAPI/ApiController.cs | 504 +++++++++--------- 22 files changed, 997 insertions(+), 949 deletions(-) rename MareSynchronos/Factories/{CharacterCacheFactory.cs => CharacterDataFactory.cs} (82%) create mode 100644 MareSynchronos/Managers/CharacterCacheManager.cs create mode 100644 MareSynchronos/Models/CachedPlayer.cs rename MareSynchronos/Models/{CharacterCache.cs => CharacterData.cs} (94%) delete mode 100644 MareSynchronos/PenumbraMod/DefaultMod.cs delete mode 100644 MareSynchronos/PenumbraMod/Meta.cs create mode 100644 MareSynchronos/Utils/DalamudUtil.cs create mode 100644 MareSynchronos/Utils/Logger.cs diff --git a/MareSynchronos/Configuration.cs b/MareSynchronos/Configuration.cs index a482782..d904142 100644 --- a/MareSynchronos/Configuration.cs +++ b/MareSynchronos/Configuration.cs @@ -11,22 +11,25 @@ namespace MareSynchronos [Serializable] public class Configuration : IPluginConfiguration { - public int Version { get; set; } = 0; - - public string CacheFolder { get; set; } = string.Empty; - public Dictionary ClientSecret { get; set; } = new(); - public Dictionary UidComments { get; set; } = new(); private string _apiUri = string.Empty; + private int _maxParallelScan = 10; + [NonSerialized] + private DalamudPluginInterface? _pluginInterface; + + public bool AcceptedAgreement { get; set; } = false; public string ApiUri { get => string.IsNullOrEmpty(_apiUri) ? ApiController.MainServiceUri : _apiUri; set => _apiUri = value; } - public bool UseCustomService { get; set; } = false; + public string CacheFolder { get; set; } = string.Empty; + public Dictionary ClientSecret { get; set; } = new(); + [JsonIgnore] + public bool HasValidSetup => AcceptedAgreement && InitialScanComplete && !string.IsNullOrEmpty(CacheFolder) && + Directory.Exists(CacheFolder) && ClientSecret.ContainsKey(ApiUri); + public bool InitialScanComplete { get; set; } = false; - public bool AcceptedAgreement { get; set; } = false; - private int _maxParallelScan = 10; public int MaxParallelScan { get => _maxParallelScan; @@ -41,23 +44,18 @@ namespace MareSynchronos } } - [JsonIgnore] - public bool HasValidSetup => AcceptedAgreement && InitialScanComplete && !string.IsNullOrEmpty(CacheFolder) && - Directory.Exists(CacheFolder) && ClientSecret.ContainsKey(ApiUri); - + public Dictionary UidComments { get; set; } = new(); + public bool UseCustomService { get; set; } = false; + public int Version { get; set; } = 0; // the below exist just to make saving less cumbersome - - [NonSerialized] - private DalamudPluginInterface? _pluginInterface; - public void Initialize(DalamudPluginInterface pluginInterface) { - this._pluginInterface = pluginInterface; + _pluginInterface = pluginInterface; } public void Save() { - this._pluginInterface!.SavePluginConfig(this); + _pluginInterface!.SavePluginConfig(this); } } } diff --git a/MareSynchronos/Factories/CharacterCacheFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs similarity index 82% rename from MareSynchronos/Factories/CharacterCacheFactory.cs rename to MareSynchronos/Factories/CharacterDataFactory.cs index 9e618a4..9a55bce 100644 --- a/MareSynchronos/Factories/CharacterCacheFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -1,29 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Diagnostics; using System.Threading; -using System.Threading.Tasks; using Dalamud.Game.ClientState; -using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using MareSynchronos.Managers; using MareSynchronos.Models; +using MareSynchronos.Utils; using Penumbra.GameData.ByteString; using Penumbra.Interop.Structs; namespace MareSynchronos.Factories { - public class CharacterCacheFactory + public class CharacterDataFactory { private readonly ClientState _clientState; private readonly IpcManager _ipcManager; private readonly FileReplacementFactory _factory; - public CharacterCacheFactory(ClientState clientState, IpcManager ipcManager, FileReplacementFactory factory) + public CharacterDataFactory(ClientState clientState, IpcManager ipcManager, FileReplacementFactory factory) { + Logger.Debug("Creating " + nameof(CharacterDataFactory)); + _clientState = clientState; _ipcManager = ipcManager; _factory = factory; @@ -34,13 +32,14 @@ namespace MareSynchronos.Factories return _clientState.LocalPlayer!.Name.ToString(); } - public unsafe CharacterCache BuildCharacterCache() + public unsafe CharacterData BuildCharacterData() { - var cache = new CharacterCache(); + Stopwatch st = Stopwatch.StartNew(); + var cache = new CharacterData(); while (_clientState.LocalPlayer == null) { - PluginLog.Debug("Character is null but it shouldn't be, waiting"); + Logger.Debug("Character is null but it shouldn't be, waiting"); Thread.Sleep(50); } var model = (CharacterBase*)((Character*)_clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); @@ -100,6 +99,13 @@ namespace MareSynchronos.Factories } } + cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization()!; + cache.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(_clientState.LocalPlayer!.Name.ToString()); + cache.JobId = _clientState.LocalPlayer!.ClassJob.Id; + + st.Stop(); + Logger.Debug("Building Character Data took " + st.Elapsed); + return cache; } } diff --git a/MareSynchronos/Factories/FileCacheFactory.cs b/MareSynchronos/Factories/FileCacheFactory.cs index afba345..f18457b 100644 --- a/MareSynchronos/Factories/FileCacheFactory.cs +++ b/MareSynchronos/Factories/FileCacheFactory.cs @@ -9,7 +9,7 @@ namespace MareSynchronos.Factories public FileCache Create(string file) { FileInfo fileInfo = new(file); - string sha1Hash = Crypto.GetFileHash(fileInfo.FullName); + var sha1Hash = Crypto.GetFileHash(fileInfo.FullName); return new FileCache() { Filepath = fileInfo.FullName, diff --git a/MareSynchronos/Factories/FileReplacementFactory.cs b/MareSynchronos/Factories/FileReplacementFactory.cs index aaab72c..9768f8f 100644 --- a/MareSynchronos/Factories/FileReplacementFactory.cs +++ b/MareSynchronos/Factories/FileReplacementFactory.cs @@ -1,26 +1,28 @@ -using Dalamud.Game.ClientState; -using MareSynchronos.Managers; +using MareSynchronos.Managers; using MareSynchronos.Models; +using MareSynchronos.Utils; namespace MareSynchronos.Factories { public class FileReplacementFactory { - private readonly IpcManager ipcManager; + private readonly IpcManager _ipcManager; public FileReplacementFactory(IpcManager ipcManager) { - this.ipcManager = ipcManager; + Logger.Debug("Creating " + nameof(FileReplacementFactory)); + + this._ipcManager = ipcManager; } public FileReplacement Create() { - if (!ipcManager.CheckPenumbraApi()) + if (!_ipcManager.CheckPenumbraApi()) { throw new System.Exception(); } - return new FileReplacement(ipcManager.PenumbraModDirectory()!); + return new FileReplacement(_ipcManager.PenumbraModDirectory()!); } } } diff --git a/MareSynchronos/FileCacheDB/FileCache.cs b/MareSynchronos/FileCacheDB/FileCache.cs index ee513ac..c25a3fd 100644 --- a/MareSynchronos/FileCacheDB/FileCache.cs +++ b/MareSynchronos/FileCacheDB/FileCache.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -#nullable disable +#nullable disable namespace MareSynchronos.FileCacheDB { diff --git a/MareSynchronos/FileCacheDB/FileCacheContext.cs b/MareSynchronos/FileCacheDB/FileCacheContext.cs index e5e62c6..98c07a5 100644 --- a/MareSynchronos/FileCacheDB/FileCacheContext.cs +++ b/MareSynchronos/FileCacheDB/FileCacheContext.cs @@ -1,7 +1,6 @@ using System; using System.IO; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; #nullable disable diff --git a/MareSynchronos/Managers/CharacterCacheManager.cs b/MareSynchronos/Managers/CharacterCacheManager.cs new file mode 100644 index 0000000..0113dd7 --- /dev/null +++ b/MareSynchronos/Managers/CharacterCacheManager.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Logging; +using MareSynchronos.API; +using MareSynchronos.FileCacheDB; +using MareSynchronos.Models; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronos.Managers; + +public class CharacterCacheManager : IDisposable +{ + private readonly ApiController _apiController; + private readonly ClientState _clientState; + private readonly DalamudUtil _dalamudUtil; + private readonly Framework _framework; + private readonly IpcManager _ipcManager; + private readonly ObjectTable _objectTable; + private readonly List _onlineCachedPlayers = new(); + private readonly List _localVisiblePlayers = new(); + private DateTime _lastPlayerObjectCheck = DateTime.Now; + + public CharacterCacheManager(ClientState clientState, Framework framework, ObjectTable objectTable, ApiController apiController, DalamudUtil dalamudUtil, IpcManager ipcManager) + { + Logger.Debug("Creating " + nameof(CharacterCacheManager)); + + _clientState = clientState; + _framework = framework; + _objectTable = objectTable; + _apiController = apiController; + _dalamudUtil = dalamudUtil; + _ipcManager = ipcManager; + } + + public void AddInitialPairs(List apiTaskResult) + { + _onlineCachedPlayers.AddRange(apiTaskResult.Select(a => new CachedPlayer(a))); + Logger.Debug("Online and paired users: " + string.Join(",", _onlineCachedPlayers)); + } + + public void Dispose() + { + Logger.Debug("Disposing " + nameof(CharacterCacheManager)); + + _apiController.CharacterReceived -= ApiControllerOnCharacterReceived; + _apiController.PairedClientOnline -= ApiControllerOnPairedClientOnline; + _apiController.PairedClientOffline -= ApiControllerOnPairedClientOffline; + _apiController.PairedWithOther -= ApiControllerOnPairedWithOther; + _apiController.UnpairedFromOther -= ApiControllerOnUnpairedFromOther; + _framework.Update -= FrameworkOnUpdate; + + foreach (var character in _onlineCachedPlayers.ToList()) + { + RestoreCharacter(character); + } + } + + public void Initialize() + { + _apiController.CharacterReceived += ApiControllerOnCharacterReceived; + _apiController.PairedClientOnline += ApiControllerOnPairedClientOnline; + _apiController.PairedClientOffline += ApiControllerOnPairedClientOffline; + _apiController.PairedWithOther += ApiControllerOnPairedWithOther; + _apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther; + _framework.Update += FrameworkOnUpdate; + } + public async Task UpdatePlayersFromService(Dictionary playerJobIds) + { + await _apiController.GetCharacterData(playerJobIds); + } + + private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) + { + Logger.Debug("Received hash for " + e.CharacterNameHash); + string otherPlayerName; + + var localPlayers = _dalamudUtil.GetLocalPlayers(); + if (localPlayers.ContainsKey(e.CharacterNameHash)) + { + _onlineCachedPlayers.Single(p => p.PlayerNameHash == e.CharacterNameHash).PlayerName = localPlayers[e.CharacterNameHash].Name.ToString(); + otherPlayerName = localPlayers[e.CharacterNameHash].Name.ToString(); + } + else + { + Logger.Debug("Found no local player for " + e.CharacterNameHash); + return; + } + + _onlineCachedPlayers.Single(p => p.PlayerNameHash == e.CharacterNameHash) + .CharacterCache[e.CharacterData.JobId] = e.CharacterData; + + List toDownloadReplacements; + using (var db = new FileCacheContext()) + { + Logger.Debug("Checking for files to download for player " + otherPlayerName); + Logger.Debug("Received total " + e.CharacterData.FileReplacements.Count + " file replacement data"); + Logger.Debug("Hash for data is " + e.CharacterData.Hash); + toDownloadReplacements = + e.CharacterData.FileReplacements.Where(f => !db.FileCaches.Any(c => c.Hash == f.Hash)) + .ToList(); + } + + Logger.Debug("Downloading missing files for player " + otherPlayerName); + // todo: make this cancellable + Task.Run(async () => + { + await _apiController.DownloadFiles(toDownloadReplacements); + + Logger.Debug("Assigned hash to visible player: " + otherPlayerName); + _ipcManager.PenumbraRemoveTemporaryCollection(otherPlayerName); + var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(otherPlayerName); + Dictionary moddedPaths = new(); + try + { + using var db = new FileCacheContext(); + foreach (var item in e.CharacterData.FileReplacements) + { + foreach (var gamePath in item.GamePaths) + { + var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash); + if (fileCache != null) + { + moddedPaths.Add(gamePath, fileCache.Filepath); + } + } + } + } + catch (Exception ex) + { + PluginLog.Error(ex, "Something went wrong during calculation replacements"); + } + + _dalamudUtil.WaitWhileCharacterIsDrawing(localPlayers[e.CharacterNameHash].Address); + + _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, e.CharacterData.ManipulationData); + _ipcManager.GlamourerApplyCharacterCustomization(e.CharacterData.GlamourerData, otherPlayerName); + }); + } + + private void ApiControllerOnPairedClientOffline(object? sender, EventArgs e) + { + Logger.Debug("Player offline: " + sender!); + _onlineCachedPlayers.RemoveAll(p => p.PlayerNameHash == ((string)sender!)); + } + + private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e) + { + Logger.Debug("Player online: " + sender!); + _onlineCachedPlayers.Add(new CachedPlayer((string)sender!)); + } + + private void ApiControllerOnPairedWithOther(object? sender, EventArgs e) + { + var characterHash = (string?)sender; + if (string.IsNullOrEmpty(characterHash)) return; + var players = _dalamudUtil.GetLocalPlayers(); + if (!players.ContainsKey(characterHash)) return; + Logger.Debug("Getting data for " + characterHash); + _ = _apiController.GetCharacterData(new Dictionary { { characterHash, (int)players[characterHash].ClassJob.Id } }); + } + + private void ApiControllerOnUnpairedFromOther(object? sender, EventArgs e) + { + var characterHash = (string?)sender; + if (string.IsNullOrEmpty(characterHash)) return; + RestoreCharacter(_onlineCachedPlayers.Single(p => p.PlayerNameHash == (string)sender!)); + } + + private void FrameworkOnUpdate(Framework framework) + { + try + { + if (_clientState.LocalPlayer == null) return; + + if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(2)) return; + + _localVisiblePlayers.Clear(); + foreach (var obj in _objectTable) + { + if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + var playerName = obj.Name.ToString(); + if (playerName == _dalamudUtil.PlayerName) continue; + var pObj = (PlayerCharacter)obj; + _localVisiblePlayers.Add(pObj.Name.ToString()); + if (_onlineCachedPlayers.Any(p => p.PlayerName == pObj.Name.ToString())) + { + _onlineCachedPlayers.Single(p => p.PlayerName == pObj.Name.ToString()).IsVisible = true; + continue; + } + + var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString()); + + if (_onlineCachedPlayers.All(p => p.PlayerNameHash != hashedName)) continue; + + var cachedPlayer = _onlineCachedPlayers.Single(p => p.PlayerNameHash == hashedName); + if (string.IsNullOrEmpty(cachedPlayer.PlayerName)) + { + cachedPlayer.PlayerName = pObj.Name.ToString(); + } + cachedPlayer.PlayerCharacter = pObj; + cachedPlayer.IsVisible = true; + } + + foreach (var item in _onlineCachedPlayers.Where(p => !string.IsNullOrEmpty(p.PlayerName) && !_localVisiblePlayers.Contains(p.PlayerName!))) + { + item.IsVisible = false; + } + + foreach (var item in _onlineCachedPlayers.Where(p => !string.IsNullOrEmpty(p.PlayerName) && !p.IsVisible && p.WasVisible)) + { + Logger.Debug("Player not visible anymore: " + item.PlayerName); + RestoreCharacter(item); + } + + var newVisiblePlayers = _onlineCachedPlayers.Where(p => p.IsVisible && !p.WasVisible).ToList(); + if (newVisiblePlayers.Any()) + { + Logger.Debug("Getting data for new players: " + string.Join(Environment.NewLine, newVisiblePlayers)); + Task.Run(async () => await UpdatePlayersFromService(newVisiblePlayers + .ToDictionary(k => k.PlayerNameHash, k => (int)k.PlayerCharacter!.ClassJob.Id))); + } + + _lastPlayerObjectCheck = DateTime.Now; + } + catch (Exception ex) + { + PluginLog.Error(ex, "error"); + } + } + private void RestoreCharacter(CachedPlayer character) + { + if (string.IsNullOrEmpty(character.PlayerName)) return; + + Logger.Debug("Restoring state for " + character.PlayerName); + _ipcManager.PenumbraRemoveTemporaryCollection(character.PlayerName); + _ipcManager.GlamourerRevertCharacterCustomization(character.PlayerName); + + character.Reset(); + } +} \ No newline at end of file diff --git a/MareSynchronos/Managers/CharacterManager.cs b/MareSynchronos/Managers/CharacterManager.cs index f746381..cc2a9ad 100644 --- a/MareSynchronos/Managers/CharacterManager.cs +++ b/MareSynchronos/Managers/CharacterManager.cs @@ -1,8 +1,5 @@ -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects; using Dalamud.Logging; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using MareSynchronos.Factories; using MareSynchronos.Models; using MareSynchronos.Utils; @@ -10,293 +7,91 @@ using MareSynchronos.WebAPI; using Newtonsoft.Json; using Penumbra.PlayerWatch; using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Dalamud.Game.ClientState.Objects.SubKinds; -using MareSynchronos.API; -using MareSynchronos.FileCacheDB; namespace MareSynchronos.Managers { - public class CachedPlayer - { - public string? PlayerName { get; set; } - public string? PlayerNameHash { get; set; } - public int JobId { get; set; } - public Dictionary? CharacterCache { get; set; } - public PlayerCharacter? PlayerCharacter { get; set; } - } - public class CharacterManager : IDisposable { private readonly ApiController _apiController; - readonly Dictionary _cachedLocalPlayers = new(); - private readonly Dictionary<(string, int), CharacterCacheDto> _characterCache = new(); - private readonly ClientState _clientState; - private readonly Framework _framework; + private readonly CharacterCacheManager _characterCacheManager; + private readonly CharacterDataFactory _characterDataFactory; + private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; private readonly ObjectTable _objectTable; - private readonly Configuration _pluginConfiguration; - private readonly CharacterCacheFactory _characterCacheFactory; private readonly IPlayerWatcher _watcher; - private DateTime _lastPlayerObjectCheck = DateTime.Now; private string _lastSentHash = string.Empty; - private Task? _playerChangedTask = null; + private Task? _playerChangedTask; - private List _onlineCachedPlayers = new(); - - private Dictionary _onlinePairedUsers = new(); - - public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, - Configuration pluginConfiguration, CharacterCacheFactory characterCacheFactory) + public CharacterManager(ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, + CharacterDataFactory characterDataFactory, CharacterCacheManager characterCacheManager, DalamudUtil dalamudUtil, IPlayerWatcher watcher) { - this._clientState = clientState; - this._framework = framework; - this._apiController = apiController; - this._objectTable = objectTable; - this._ipcManager = ipcManager; - _pluginConfiguration = pluginConfiguration; - _characterCacheFactory = characterCacheFactory; - _watcher = PlayerWatchFactory.Create(framework, clientState, objectTable); + Logger.Debug("Creating " + nameof(CharacterManager)); + + _apiController = apiController; + _objectTable = objectTable; + _ipcManager = ipcManager; + _characterDataFactory = characterDataFactory; + _characterCacheManager = characterCacheManager; + _dalamudUtil = dalamudUtil; + _watcher = watcher; } public void Dispose() { + Logger.Debug("Disposing " + nameof(CharacterManager)); + _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _framework.Update -= Framework_Update; - _clientState.TerritoryChanged -= ClientState_TerritoryChanged; _apiController.Connected -= ApiController_Connected; _apiController.Disconnected -= ApiController_Disconnected; - _apiController.CharacterReceived -= ApiControllerOnCharacterReceived; - _apiController.UnpairedFromOther -= ApiControllerOnUnpairedFromOther; - _apiController.PairedWithOther -= ApiControllerOnPairedWithOther; - _apiController.PairedClientOffline -= ApiControllerOnPairedClientOffline; - _watcher.Disable(); _watcher.PlayerChanged -= Watcher_PlayerChanged; - _watcher?.Dispose(); - - foreach (var character in _onlinePairedUsers) - { - RestoreCharacter(character); - } - } - - public async Task UpdatePlayersFromService(Dictionary currentLocalPlayers) - { - PluginLog.Debug("Updating local players from service"); - currentLocalPlayers = currentLocalPlayers.Where(k => _onlinePairedUsers.ContainsKey(k.Key)) - .ToDictionary(k => k.Key, k => k.Value); - await _apiController.GetCharacterData(currentLocalPlayers - .ToDictionary( - k => k.Key, - k => (int)k.Value.ClassJob.Id)); } internal void StartWatchingPlayer() { - _watcher.AddPlayerToWatch(GetPlayerName()); + _watcher.AddPlayerToWatch(_dalamudUtil.PlayerName); _watcher.PlayerChanged += Watcher_PlayerChanged; - _watcher.Enable(); _apiController.Connected += ApiController_Connected; _apiController.Disconnected += ApiController_Disconnected; - _apiController.CharacterReceived += ApiControllerOnCharacterReceived; - _apiController.UnpairedFromOther += ApiControllerOnUnpairedFromOther; - _apiController.PairedWithOther += ApiControllerOnPairedWithOther; - _apiController.PairedClientOffline += ApiControllerOnPairedClientOffline; - _apiController.PairedClientOnline += ApiControllerOnPairedClientOnline; - PluginLog.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); + Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); if (_apiController.IsConnected) { ApiController_Connected(null, EventArgs.Empty); } + + _ipcManager.PenumbraRedraw(_dalamudUtil.PlayerName); } private void ApiController_Connected(object? sender, EventArgs args) { - PluginLog.Debug(nameof(ApiController_Connected)); - PluginLog.Debug("MyHashedName:" + Crypto.GetHash256(GetPlayerName() + _clientState.LocalPlayer!.HomeWorld.Id)); + var apiTask = _apiController.SendCharacterName(_dalamudUtil.PlayerNameHashed); _lastSentHash = string.Empty; - var apiTask = _apiController.SendCharacterName(Crypto.GetHash256(GetPlayerName() + _clientState.LocalPlayer!.HomeWorld.Id)); + _characterCacheManager.Initialize(); Task.WaitAll(apiTask); - _onlinePairedUsers = apiTask.Result.ToDictionary(k => k, k => string.Empty); - var assignTask = AssignLocalPlayersData(); - Task.WaitAll(assignTask); - PluginLog.Debug("Online and paired users: " + string.Join(",", _onlinePairedUsers)); - - _framework.Update += Framework_Update; + _characterCacheManager.AddInitialPairs(apiTask.Result); + _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; - _clientState.TerritoryChanged += ClientState_TerritoryChanged; } private void ApiController_Disconnected(object? sender, EventArgs args) { - PluginLog.Debug(nameof(ApiController_Disconnected)); - _framework.Update -= Framework_Update; + _characterCacheManager.Dispose(); + + Logger.Debug(nameof(ApiController_Disconnected)); + _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _clientState.TerritoryChanged -= ClientState_TerritoryChanged; - foreach (var character in _onlinePairedUsers) - { - RestoreCharacter(character); - } - _onlinePairedUsers.Clear(); - - _lastSentHash = string.Empty; } - private void ApiControllerOnPairedWithOther(object? sender, EventArgs e) + private async Task CreateFullCharacterCache() { - var characterHash = (string?)sender; - if (string.IsNullOrEmpty(characterHash)) return; - var players = GetLocalPlayers(); - if (players.ContainsKey(characterHash)) - { - PluginLog.Debug("Removed pairing, restoring data for " + characterHash); - _ = _apiController.GetCharacterData(new Dictionary { { characterHash, (int)players[characterHash].ClassJob.Id } }); - } - } + var cache = _characterDataFactory.BuildCharacterData(); - private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) - { - PluginLog.Debug("Received hash for " + e.CharacterNameHash); - string otherPlayerName; - - var localPlayers = GetLocalPlayers(); - if (localPlayers.ContainsKey(e.CharacterNameHash)) - { - _onlinePairedUsers[e.CharacterNameHash] = localPlayers[e.CharacterNameHash].Name.ToString(); - otherPlayerName = _onlinePairedUsers[e.CharacterNameHash]; - } - else - { - PluginLog.Debug("Found no local player for " + e.CharacterNameHash); - return; - } - - _characterCache[(e.CharacterNameHash, e.CharacterData.JobId)] = e.CharacterData; - - List toDownloadReplacements; - using (var db = new FileCacheContext()) - { - PluginLog.Debug("Checking for files to download for player " + otherPlayerName); - PluginLog.Debug("Received total " + e.CharacterData.FileReplacements.Count + " file replacement data"); - PluginLog.Debug("Hash for data is " + e.CharacterData.Hash); - toDownloadReplacements = - e.CharacterData.FileReplacements.Where(f => !db.FileCaches.Any(c => c.Hash == f.Hash)) - .ToList(); - } - - PluginLog.Debug("Downloading missing files for player " + otherPlayerName); - // todo: make this cancellable - var downloadTask = _apiController.DownloadFiles(toDownloadReplacements, _pluginConfiguration.CacheFolder); - while (!downloadTask.IsCompleted) - { - Thread.Sleep(100); - } - - PluginLog.Debug("Assigned hash to visible player: " + otherPlayerName); - _ipcManager.PenumbraRemoveTemporaryCollection(otherPlayerName); - var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(otherPlayerName); - Dictionary moddedPaths = new(); - try - { - using var db = new FileCacheContext(); - foreach (var item in e.CharacterData.FileReplacements) - { - foreach (var gamePath in item.GamePaths) - { - var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash); - if (fileCache != null) - { - moddedPaths.Add(gamePath, fileCache.Filepath); - } - } - } - } - catch (Exception ex) - { - PluginLog.Error(ex, "Something went wrong during calculation replacements"); - } - - WaitWhileCharacterIsDrawing(localPlayers[e.CharacterNameHash].Address); - - _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, e.CharacterData.ManipulationData); - _ipcManager.GlamourerApplyCharacterCustomization(e.CharacterData.GlamourerData, otherPlayerName); - } - - private void ApiControllerOnUnpairedFromOther(object? sender, EventArgs e) - { - var characterHash = (string?)sender; - if (string.IsNullOrEmpty(characterHash)) return; - RestoreCharacter(new KeyValuePair(characterHash, _onlinePairedUsers[characterHash])); - } - - private void RestoreCharacter(KeyValuePair character) - { - if (string.IsNullOrEmpty(character.Value)) return; - - foreach (var entry in _characterCache.Where(c => c.Key.Item1 == character.Key)) - { - _characterCache.Remove(entry.Key); - } - - RestorePreviousCharacter(character.Value); - PluginLog.Debug("Removed from pairing, restoring state for " + character.Value); - _ipcManager.PenumbraRemoveTemporaryCollection(character.Value); - _ipcManager.GlamourerRevertCharacterCustomization(character.Value); - } - - private void ApiControllerOnPairedClientOffline(object? sender, EventArgs e) - { - PluginLog.Debug("Player offline: " + sender!); - _onlinePairedUsers.Remove((string)sender!); - } - - private void ApiControllerOnPairedClientOnline(object? sender, EventArgs e) - { - PluginLog.Debug("Player online: " + sender!); - _onlinePairedUsers.Add((string)sender!, string.Empty); - } - - private async Task AssignLocalPlayersData() - { - PluginLog.Debug("Temp assigning local players from cache"); - var currentLocalPlayers = GetLocalPlayers(); - foreach (var player in _characterCache) - { - if (currentLocalPlayers.ContainsKey(player.Key.Item1)) - { - await Task.Run(() => ApiControllerOnCharacterReceived(null, new CharacterReceivedEventArgs(player.Key.Item1, player.Value))); - } - } - - await UpdatePlayersFromService(currentLocalPlayers); - } - - private void ClientState_TerritoryChanged(object? sender, ushort e) - { - _ = Task.Run(async () => - { - while (_clientState.LocalPlayer == null) - { - await Task.Delay(250); - } - - await AssignLocalPlayersData(); - }); - } - - private async Task CreateFullCharacterCache() - { - var cache = _characterCacheFactory.BuildCharacterCache(); - cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization()!; - cache.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(_clientState.LocalPlayer!.Name.ToString()); - cache.JobId = _clientState.LocalPlayer!.ClassJob.Id; await Task.Run(async () => { while (!cache.IsReady) @@ -312,163 +107,75 @@ namespace MareSynchronos.Managers return cache; } - private unsafe void Framework_Update(Framework framework) - { - try - { - if (_clientState.LocalPlayer == null) return; - - if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(2)) return; - - List localPlayersList = new(); - Dictionary newPlayers = new(); - foreach (var obj in _objectTable) - { - if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - string playerName = obj.Name.ToString(); - if (playerName == GetPlayerName()) continue; - var pObj = (PlayerCharacter)obj; - var hashedName = Crypto.GetHash256(pObj.Name.ToString() + pObj.HomeWorld.Id.ToString()); - - if (!_onlinePairedUsers.ContainsKey(hashedName)) continue; - - _onlinePairedUsers[hashedName] = pObj.Name.ToString(); - localPlayersList.Add(hashedName); - if (!_cachedLocalPlayers.ContainsKey(hashedName)) newPlayers[hashedName] = pObj; - _cachedLocalPlayers[hashedName] = pObj.Name.ToString(); - } - - foreach (var item in _cachedLocalPlayers.ToList().Where(item => !localPlayersList.Contains(item.Key))) - { - foreach (var cachedPlayerNameJobId in _characterCache.Keys.ToList().Where(cachedPlayerNameJobId => cachedPlayerNameJobId.Item1 == item.Key)) - { - PluginLog.Debug("Player not visible anymore: " + cachedPlayerNameJobId.Item1); - RestorePreviousCharacter(_cachedLocalPlayers[cachedPlayerNameJobId.Item1]); - _characterCache.Remove(cachedPlayerNameJobId); - } - - _cachedLocalPlayers.Remove(item.Key); - } - - if (newPlayers.Any()) - { - PluginLog.Debug("Getting data for new players: " + string.Join(Environment.NewLine, newPlayers)); - _ = UpdatePlayersFromService(newPlayers); - } - - _lastPlayerObjectCheck = DateTime.Now; - } - catch (Exception ex) - { - PluginLog.Error(ex, "error"); - } - } - - private Dictionary GetLocalPlayers() - { - Dictionary allLocalPlayers = new(); - foreach (var obj in _objectTable) - { - if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; - string playerName = obj.Name.ToString(); - if (playerName == GetPlayerName()) continue; - var playerObject = (PlayerCharacter)obj; - allLocalPlayers[Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString())] = playerObject; - } - - return allLocalPlayers; - } - - private string GetPlayerName() - { - return _clientState.LocalPlayer!.Name.ToString(); - } - private void IpcManager_PenumbraRedrawEvent(object? objectTableIndex, EventArgs e) { var objTableObj = _objectTable[(int)objectTableIndex!]; - if (objTableObj!.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) - { - if (objTableObj.Name.ToString() == GetPlayerName()) - { - PluginLog.Debug("Penumbra Redraw Event"); - PlayerChanged(GetPlayerName()); - } - } + if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return; + if (objTableObj.Name.ToString() != _dalamudUtil.PlayerName) return; + Logger.Debug("Penumbra Redraw Event"); + PlayerChanged(_dalamudUtil.PlayerName); } - private unsafe void PlayerChanged(string name) + private void PlayerChanged(string name) { //if (sender == null) return; - PluginLog.Debug("Player changed: " + name); + Logger.Debug("Player changed: " + name); if (_playerChangedTask is { IsCompleted: false }) { PluginLog.Warning("PlayerChanged Task still running"); return; } - _playerChangedTask = Task.Run(() => + _playerChangedTask = Task.Run(async () => { - WaitWhileCharacterIsDrawing(_clientState.LocalPlayer!.Address); + Stopwatch st = Stopwatch.StartNew(); + _dalamudUtil.WaitWhileSelfIsDrawing(); - var characterCacheTask = CreateFullCharacterCache(); - Task.WaitAll(characterCacheTask); + var characterCacheTask = await CreateFullCharacterCache(); - var cacheDto = characterCacheTask.Result.ToCharacterCacheDto(); + var cacheDto = characterCacheTask.ToCharacterCacheDto(); + + st.Stop(); + Logger.Debug("Elapsed time PlayerChangedTask: " + st.Elapsed); if (cacheDto.Hash == _lastSentHash) { - PluginLog.Debug("Not sending data, already sent"); + Logger.Debug("Not sending data, already sent"); return; } - Task.WaitAll(_apiController.SendCharacterData(cacheDto, GetLocalPlayers().Select(d => d.Key).ToList())); + await _apiController.SendCharacterData(cacheDto, _dalamudUtil.GetLocalPlayers().Select(d => d.Key).ToList()); _lastSentHash = cacheDto.Hash; }); } - public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress) - { - var obj = (GameObject*)characterAddress; - - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something - { - //PluginLog.Debug("Waiting for character to finish drawing"); - Thread.Sleep(100); - } - - // wait half a second just in case - Thread.Sleep(500); - } - - private void RestorePreviousCharacter(string playerName) - { - PluginLog.Debug("Restoring state for " + playerName); - _ipcManager.PenumbraRemoveTemporaryCollection(playerName); - _ipcManager.GlamourerRevertCharacterCustomization(playerName); - } - private void Watcher_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) { - try + Logger.Debug("Watcher Player Changed"); + Task.Run(() => { - // fix for redraw from anamnesis - while (_clientState.LocalPlayer == null) + try { - Thread.Sleep(100); + // fix for redraw from anamnesis + while (!_dalamudUtil.IsPlayerPresent) + { + Logger.Debug("Waiting Until Player is Present"); + Thread.Sleep(100); + } + + if (actor.Name.ToString() == _dalamudUtil.PlayerName) + { + Logger.Debug("Watcher: PlayerChanged"); + PlayerChanged(actor.Name.ToString()); + } + else + { + Logger.Debug("PlayerChanged: " + actor.Name.ToString()); + } } - if (actor.Name.ToString() == _clientState.LocalPlayer!.Name.ToString()) + catch (Exception ex) { - PluginLog.Debug("Watcher: PlayerChanged"); - PlayerChanged(actor.Name.ToString()); + PluginLog.Error(ex, "Actor was null or broken " + actor); } - else - { - PluginLog.Debug("PlayerChanged: " + actor.Name.ToString()); - } - } - catch(Exception ex) - { - PluginLog.Error(ex, "Actor was null or broken " + actor); - } + }); } } } diff --git a/MareSynchronos/Managers/FileCacheManager.cs b/MareSynchronos/Managers/FileCacheManager.cs index 9952e94..bdf992e 100644 --- a/MareSynchronos/Managers/FileCacheManager.cs +++ b/MareSynchronos/Managers/FileCacheManager.cs @@ -4,12 +4,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Dalamud.Logging; using MareSynchronos.Factories; using MareSynchronos.FileCacheDB; +using MareSynchronos.Utils; namespace MareSynchronos.Managers { @@ -25,6 +25,8 @@ namespace MareSynchronos.Managers private Stopwatch? _timerStopWatch; public FileCacheManager(FileCacheFactory fileCacheFactory, IpcManager ipcManager, Configuration pluginConfiguration) { + Logger.Debug("Creating " + nameof(FileCacheManager)); + _fileCacheFactory = fileCacheFactory; _ipcManager = ipcManager; _pluginConfiguration = pluginConfiguration; @@ -47,7 +49,8 @@ namespace MareSynchronos.Managers public void Dispose() { - PluginLog.Debug("Disposing File Cache Manager"); + Logger.Debug("Disposing " + nameof(FileCacheManager)); + _scanScheduler?.Stop(); _scanCancellationTokenSource?.Cancel(); } @@ -62,7 +65,7 @@ namespace MareSynchronos.Managers { _scanCancellationTokenSource = new CancellationTokenSource(); var penumbraDir = _ipcManager.PenumbraModDirectory()!; - PluginLog.Debug("Getting files from " + penumbraDir); + Logger.Debug("Getting files from " + penumbraDir); var scannedFiles = new ConcurrentDictionary( Directory.EnumerateFiles(penumbraDir, "*.*", SearchOption.AllDirectories) .Select(s => s.ToLowerInvariant()) @@ -80,7 +83,7 @@ namespace MareSynchronos.Managers var fileCachesToDelete = new ConcurrentBag(); var fileCachesToAdd = new ConcurrentBag(); - PluginLog.Debug("Getting file list from Database"); + Logger.Debug("Getting file list from Database"); // scan files from database Parallel.ForEach(fileCaches, new ParallelOptions() { @@ -140,7 +143,7 @@ namespace MareSynchronos.Managers } } - PluginLog.Debug("Scan complete"); + Logger.Debug("Scan complete"); TotalFiles = 0; CurrentFileProgress = 0; @@ -160,7 +163,7 @@ namespace MareSynchronos.Managers private void StartScheduler() { - PluginLog.Debug("Scheduling next scan for in " + MinutesForScan + " minutes"); + Logger.Debug("Scheduling next scan for in " + MinutesForScan + " minutes"); _scanScheduler = new System.Timers.Timer(TimeSpan.FromMinutes(MinutesForScan).TotalMilliseconds) { AutoReset = false, @@ -176,7 +179,7 @@ namespace MareSynchronos.Managers return; } - PluginLog.Debug("Initiating periodic scan for mod changes"); + Logger.Debug("Initiating periodic scan for mod changes"); Task.Run(() => _scanTask = StartFileScan(_scanCancellationTokenSource!.Token)); _timerStopWatch = Stopwatch.StartNew(); }; diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 08b137a..ef4630f 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -2,75 +2,78 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using System; -using System.Buffers.Text; using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.IO.Compression; -using System.Reflection.Metadata; -using System.Text; -using Newtonsoft.Json; +using MareSynchronos.Utils; namespace MareSynchronos.Managers { public class IpcManager : IDisposable { - private readonly DalamudPluginInterface _pluginInterface; - private readonly ICallGateSubscriber _penumbraInit; - private readonly ICallGateSubscriber? _penumbraResolvePath; - private readonly ICallGateSubscriber? _penumbraResolveModDir; - private readonly ICallGateSubscriber? _glamourerGetCharacterCustomization; - private readonly ICallGateSubscriber? _glamourerApplyCharacterCustomization; - private readonly ICallGateSubscriber _penumbraApiVersion; private readonly ICallGateSubscriber _glamourerApiVersion; + private readonly ICallGateSubscriber? _glamourerApplyCharacterCustomization; + private readonly ICallGateSubscriber? _glamourerGetCharacterCustomization; + private readonly ICallGateSubscriber _glamourerRevertCustomization; + private readonly ICallGateSubscriber _penumbraApiVersion; + private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; + private readonly ICallGateSubscriber _penumbraGetMetaManipulations; + private readonly ICallGateSubscriber _penumbraInit; private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; private readonly ICallGateSubscriber? _penumbraRedraw; + private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; + private readonly ICallGateSubscriber? _penumbraResolveModDir; + private readonly ICallGateSubscriber? _penumbraResolvePath; private readonly ICallGateSubscriber? _penumbraReverseResolvePath; - private readonly ICallGateSubscriber _glamourerRevertCustomization; - private readonly ICallGateSubscriber _penumbraGetMetaManipulations; private readonly ICallGateSubscriber, string, int, int> _penumbraSetTemporaryMod; - private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; - private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; - - public bool Initialized { get; private set; } = false; - - public event EventHandler? PenumbraRedrawEvent; - public IpcManager(DalamudPluginInterface pi) { - _pluginInterface = pi; + Logger.Debug("Creating " + nameof(IpcManager)); - _penumbraInit = _pluginInterface.GetIpcSubscriber("Penumbra.Initialized"); - _penumbraResolvePath = _pluginInterface.GetIpcSubscriber("Penumbra.ResolveCharacterPath"); - _penumbraResolveModDir = _pluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory"); - _penumbraRedraw = _pluginInterface.GetIpcSubscriber("Penumbra.RedrawObjectByName"); - _glamourerGetCharacterCustomization = _pluginInterface.GetIpcSubscriber("Glamourer.GetCharacterCustomization"); - _glamourerApplyCharacterCustomization = _pluginInterface.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization"); - _penumbraReverseResolvePath = _pluginInterface.GetIpcSubscriber("Penumbra.ReverseResolvePath"); - _penumbraApiVersion = _pluginInterface.GetIpcSubscriber("Penumbra.ApiVersion"); - _glamourerApiVersion = _pluginInterface.GetIpcSubscriber("Glamourer.ApiVersion"); - _glamourerRevertCustomization = _pluginInterface.GetIpcSubscriber("Glamourer.RevertCharacterCustomization"); - _penumbraObjectIsRedrawn = _pluginInterface.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); + _penumbraInit = pi.GetIpcSubscriber("Penumbra.Initialized"); + _penumbraResolvePath = pi.GetIpcSubscriber("Penumbra.ResolveCharacterPath"); + _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); + _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); + _glamourerGetCharacterCustomization = pi.GetIpcSubscriber("Glamourer.GetCharacterCustomization"); + _glamourerApplyCharacterCustomization = pi.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization"); + _penumbraReverseResolvePath = pi.GetIpcSubscriber("Penumbra.ReverseResolvePath"); + _penumbraApiVersion = pi.GetIpcSubscriber("Penumbra.ApiVersion"); + _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); + _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacterCustomization"); + _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); _penumbraGetMetaManipulations = - _pluginInterface.GetIpcSubscriber("Penumbra.GetMetaManipulations"); + pi.GetIpcSubscriber("Penumbra.GetMetaManipulations"); _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); _penumbraInit.Subscribe(RedrawSelf); _penumbraSetTemporaryMod = - _pluginInterface + pi .GetIpcSubscriber, string, int, int>("Penumbra.AddTemporaryMod"); _penumbraCreateTemporaryCollection = - _pluginInterface.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); + pi.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); _penumbraRemoveTemporaryCollection = - _pluginInterface.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); + pi.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); Initialized = true; } + public event EventHandler? PenumbraRedrawEvent; + + public bool Initialized { get; private set; } = false; + public bool CheckGlamourerApi() + { + try + { + return _glamourerApiVersion.InvokeFunc() >= 0; + } + catch + { + return false; + } + } + public bool CheckPenumbraApi() { try @@ -82,17 +85,88 @@ namespace MareSynchronos.Managers return false; } } - - public bool CheckGlamourerApi() + public void Dispose() { - try - { - return _glamourerApiVersion.InvokeFunc() >= 0; - } - catch - { - return false; - } + Logger.Debug("Disposing " + nameof(IpcManager)); + + Uninitialize(); + } + + public void GlamourerApplyCharacterCustomization(string customization, string characterName) + { + if (!CheckGlamourerApi()) return; + Logger.Debug("GlamourerString: " + customization); + _glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName); + } + + public string? GlamourerGetCharacterCustomization() + { + if (!CheckGlamourerApi()) return null; + return _glamourerGetCharacterCustomization!.InvokeFunc(); + } + + public void GlamourerRevertCharacterCustomization(string characterName) + { + if (!CheckGlamourerApi()) return; + _glamourerRevertCustomization!.InvokeAction(characterName); + } + + public string PenumbraCreateTemporaryCollection(string characterName) + { + if (!CheckPenumbraApi()) return string.Empty; + Logger.Debug("Creating temp collection for " + characterName); + return _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true).Item2; + } + + public string PenumbraGetMetaManipulations(string characterName) + { + if (!CheckPenumbraApi()) return string.Empty; + return _penumbraGetMetaManipulations.InvokeFunc(characterName); + } + + public string? PenumbraModDirectory() + { + if (!CheckPenumbraApi()) return null; + return _penumbraResolveModDir!.InvokeFunc(); + } + + public void PenumbraRedraw(string actorName) + { + if (!CheckPenumbraApi()) return; + _penumbraRedraw!.InvokeAction(actorName, 0); + } + + public void PenumbraRemoveTemporaryCollection(string characterName) + { + if (!CheckPenumbraApi()) return; + Logger.Debug("Removing temp collection for " + characterName); + _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); + } + + public string? PenumbraResolvePath(string path, string characterName) + { + if (!CheckPenumbraApi()) return null; + var resolvedPath = _penumbraResolvePath!.InvokeFunc(path, characterName); + PluginLog.Verbose("Resolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPath)); + return resolvedPath; + } + + public string[] PenumbraReverseResolvePath(string path, string characterName) + { + if (!CheckPenumbraApi()) return new[] { path }; + var resolvedPaths = _penumbraReverseResolvePath!.InvokeFunc(path, characterName); + PluginLog.Verbose("ReverseResolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPaths)); + return resolvedPaths; + } + + public void PenumbraSetTemporaryMods(string collectionName, Dictionary modPaths, string manipulationData) + { + if (!CheckPenumbraApi()) return; + + Logger.Debug("Assigning temp mods for " + collectionName); + Logger.Debug("ManipulationString: " + manipulationData); + var ret = _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", collectionName, modPaths, manipulationData, 0); + Logger.Debug("Penumbra Ret: " + ret.ToString()); } private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) @@ -110,89 +184,7 @@ namespace MareSynchronos.Managers _penumbraInit.Unsubscribe(RedrawSelf); _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); Initialized = false; - PluginLog.Debug("IPC Manager disposed"); - } - - public string[] PenumbraReverseResolvePath(string path, string characterName) - { - if (!CheckPenumbraApi()) return new[] { path }; - var resolvedPaths = _penumbraReverseResolvePath!.InvokeFunc(path, characterName); - PluginLog.Verbose("ReverseResolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPaths)); - return resolvedPaths; - } - - public string? PenumbraResolvePath(string path, string characterName) - { - if (!CheckPenumbraApi()) return null; - var resolvedPath = _penumbraResolvePath!.InvokeFunc(path, characterName); - PluginLog.Verbose("Resolving " + path + Environment.NewLine + "=>" + string.Join(", ", resolvedPath)); - return resolvedPath; - } - - public string? PenumbraModDirectory() - { - if (!CheckPenumbraApi()) return null; - return _penumbraResolveModDir!.InvokeFunc(); - } - - public string? GlamourerGetCharacterCustomization() - { - if (!CheckGlamourerApi()) return null; - return _glamourerGetCharacterCustomization!.InvokeFunc(); - } - - public void GlamourerApplyCharacterCustomization(string customization, string characterName) - { - if (!CheckGlamourerApi()) return; - PluginLog.Debug("GlamourerString: " + customization); - _glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName); - } - - public void GlamourerRevertCharacterCustomization(string characterName) - { - if (!CheckGlamourerApi()) return; - _glamourerRevertCustomization!.InvokeAction(characterName); - } - - public void PenumbraRedraw(string actorName) - { - if (!CheckPenumbraApi()) return; - _penumbraRedraw!.InvokeAction(actorName, 0); - } - - public string PenumbraCreateTemporaryCollection(string characterName) - { - if (!CheckPenumbraApi()) return string.Empty; - PluginLog.Debug("Creating temp collection for " + characterName); - return _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true).Item2; - } - - public void PenumbraRemoveTemporaryCollection(string characterName) - { - if (!CheckPenumbraApi()) return; - PluginLog.Debug("Removing temp collection for " + characterName); - _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); - } - - public void PenumbraSetTemporaryMods(string collectionName, Dictionary modPaths, string manipulationData) - { - if (!CheckPenumbraApi()) return; - - PluginLog.Debug("Assigning temp mods for " + collectionName); - PluginLog.Debug("ManipulationString: " + manipulationData); - var ret = _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", collectionName, modPaths, manipulationData, 0); - PluginLog.Debug("Penumbra Ret: " + ret.ToString()); - } - - public string PenumbraGetMetaManipulations(string characterName) - { - if (!CheckPenumbraApi()) return string.Empty; - return _penumbraGetMetaManipulations.InvokeFunc(characterName); - } - - public void Dispose() - { - Uninitialize(); + Logger.Debug("IPC Manager disposed"); } } } diff --git a/MareSynchronos/Models/CachedPlayer.cs b/MareSynchronos/Models/CachedPlayer.cs new file mode 100644 index 0000000..12d60ac --- /dev/null +++ b/MareSynchronos/Models/CachedPlayer.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Dalamud.Game.ClientState.Objects.SubKinds; +using MareSynchronos.API; + +namespace MareSynchronos.Models; + +public class CachedPlayer +{ + private bool _isVisible = false; + + public CachedPlayer(string nameHash) + { + PlayerNameHash = nameHash; + } + + public Dictionary CharacterCache { get; set; } = new(); + public bool IsVisible + { + get => _isVisible; + set + { + WasVisible = _isVisible; + _isVisible = value; + } + } + + public int? JobId { get; set; } + public PlayerCharacter? PlayerCharacter { get; set; } + public string? PlayerName { get; set; } + public string PlayerNameHash { get; } + public bool WasVisible { get; private set; } + public void Reset() + { + PlayerName = string.Empty; + JobId = null; + PlayerCharacter = null; + } + + public override string ToString() + { + return PlayerNameHash + " : " + PlayerName + " : HasChar " + (PlayerCharacter != null); + } +} \ No newline at end of file diff --git a/MareSynchronos/Models/CharacterCache.cs b/MareSynchronos/Models/CharacterData.cs similarity index 94% rename from MareSynchronos/Models/CharacterCache.cs rename to MareSynchronos/Models/CharacterData.cs index 1dac9e5..de965a3 100644 --- a/MareSynchronos/Models/CharacterCache.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -1,29 +1,16 @@ -using Dalamud.Logging; -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading.Tasks; using MareSynchronos.API; +using MareSynchronos.Utils; namespace MareSynchronos.Models { [JsonObject(MemberSerialization.OptIn)] - public class CharacterCache + public class CharacterData { - public CharacterCacheDto ToCharacterCacheDto() - { - return new CharacterCacheDto() - { - FileReplacements = AllReplacements.Select(f => f.ToFileReplacementDto()).ToList(), - GlamourerData = GlamourerString, - Hash = CacheHash, - JobId = (int)JobId, - ManipulationData = ManipulationString - }; - } - [JsonProperty] public List AllReplacements => FileReplacements.Where(f => f.HasFileReplacement) @@ -32,6 +19,9 @@ namespace MareSynchronos.Models .Distinct().OrderBy(f => f.GamePaths[0]) .ToList(); + [JsonProperty] + public string CacheHash { get; set; } = string.Empty; + public List FileReplacements { get; set; } = new List(); [JsonProperty] @@ -40,12 +30,10 @@ namespace MareSynchronos.Models public bool IsReady => FileReplacements.All(f => f.Computed); [JsonProperty] - public string CacheHash { get; set; } = string.Empty; + public uint JobId { get; set; } = 0; public string ManipulationString { get; set; } = string.Empty; - [JsonProperty] - public uint JobId { get; set; } = 0; public void AddAssociatedResource(FileReplacement resource, FileReplacement? mdlParent, FileReplacement? mtrlParent) { try @@ -71,7 +59,7 @@ namespace MareSynchronos.Models } catch (Exception ex) { - PluginLog.Debug(ex.Message); + Logger.Debug(ex.Message); } } @@ -92,10 +80,21 @@ namespace MareSynchronos.Models } catch (Exception ex) { - PluginLog.Debug(ex.Message); + Logger.Debug(ex.Message); } } + public CharacterCacheDto ToCharacterCacheDto() + { + return new CharacterCacheDto() + { + FileReplacements = AllReplacements.Select(f => f.ToFileReplacementDto()).ToList(), + GlamourerData = GlamourerString, + Hash = CacheHash, + JobId = (int)JobId, + ManipulationData = ManipulationString + }; + } public override string ToString() { StringBuilder stringBuilder = new(); diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index fc84f13..03b1852 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -15,36 +15,35 @@ namespace MareSynchronos.Models [JsonObject(MemberSerialization.OptIn)] public class FileReplacement { - public FileReplacementDto ToFileReplacementDto() - { - return new FileReplacementDto - { - GamePaths = GamePaths, - Hash = Hash, - }; - } - private readonly string penumbraDirectory; - [JsonProperty] - public string[] GamePaths { get; set; } = Array.Empty(); - [JsonProperty] - public string ResolvedPath { get; set; } = string.Empty; - [JsonProperty] - public string Hash { get; set; } = string.Empty; - public bool IsInUse { get; set; } = false; - public List Associated { get; set; } = new List(); - [JsonProperty] - public string ImcData { get; set; } = string.Empty; - public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath; - - public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed); private Task? computationTask = null; + public FileReplacement(string penumbraDirectory) { this.penumbraDirectory = penumbraDirectory; } + public List Associated { get; set; } = new List(); + + public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed); + + [JsonProperty] + public string[] GamePaths { get; set; } = Array.Empty(); + + public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath; + + [JsonProperty] + public string Hash { get; set; } = string.Empty; + + [JsonProperty] + public string ImcData { get; set; } = string.Empty; + + public bool IsInUse { get; set; } = false; + + [JsonProperty] + public string ResolvedPath { get; set; } = string.Empty; + public void AddAssociated(FileReplacement fileReplacement) { fileReplacement.IsInUse = true; @@ -52,6 +51,27 @@ namespace MareSynchronos.Models Associated.Add(fileReplacement); } + public override bool Equals(object? obj) + { + if (obj == null) return true; + if (obj.GetType() == typeof(FileReplacement)) + { + return Hash == ((FileReplacement)obj).Hash; + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + int result = 13; + result *= 397; + result += Hash.GetHashCode(); + result += ResolvedPath.GetHashCode(); + + return result; + } + public void SetResolvedPath(string path) { ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/'); @@ -89,6 +109,29 @@ namespace MareSynchronos.Models }); } + public FileReplacementDto ToFileReplacementDto() + { + return new FileReplacementDto + { + GamePaths = GamePaths, + Hash = Hash, + }; + } + public override string ToString() + { + StringBuilder builder = new(); + builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); + foreach (var l1 in Associated) + { + builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {string.Join(",", l1.GamePaths)} => {l1.ResolvedPath}"); + foreach (var l2 in l1.Associated) + { + builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}"); + } + } + return builder.ToString(); + } + private string ComputeHash(FileInfo fi) { // compute hash if hash is not present @@ -115,41 +158,5 @@ namespace MareSynchronos.Models return hash; } - - public override string ToString() - { - StringBuilder builder = new(); - builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); - foreach (var l1 in Associated) - { - builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {string.Join(",", l1.GamePaths)} => {l1.ResolvedPath}"); - foreach (var l2 in l1.Associated) - { - builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}"); - } - } - return builder.ToString(); - } - - public override bool Equals(object? obj) - { - if (obj == null) return true; - if (obj.GetType() == typeof(FileReplacement)) - { - return Hash == ((FileReplacement)obj).Hash; - } - - return base.Equals(obj); - } - - public override int GetHashCode() - { - int result = 13; - result *= 397; - result += Hash.GetHashCode(); - result += ResolvedPath.GetHashCode(); - - return result; - } } } diff --git a/MareSynchronos/PenumbraMod/DefaultMod.cs b/MareSynchronos/PenumbraMod/DefaultMod.cs deleted file mode 100644 index 6cf02ee..0000000 --- a/MareSynchronos/PenumbraMod/DefaultMod.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MareSynchronos.PenumbraMod -{ - [JsonObject(MemberSerialization.OptOut)] - internal class DefaultMod - { - public string Name { get; set; } = "Default"; - public int Priority { get; set; } = 0; - public Dictionary Files { get; set; } = new(); - public Dictionary FileSwaps { get; set; } = new(); - public List Manipulations { get; set; } = new(); - } -} diff --git a/MareSynchronos/PenumbraMod/Meta.cs b/MareSynchronos/PenumbraMod/Meta.cs deleted file mode 100644 index 1f9ebe5..0000000 --- a/MareSynchronos/PenumbraMod/Meta.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace MareSynchronos.PenumbraMod -{ - [JsonObject(MemberSerialization.OptOut)] - internal class Meta - { - public int FileVersion { get; set; } = 1; - public string Name { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public string Version { get; set; } = "0"; - public string Website { get; set; } = string.Empty; - public long ImportDate { get; set; } = DateTime.Now.Ticks; - } -} diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 79c5806..6af00d4 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -1,25 +1,18 @@ using Dalamud.Game.Command; -using Dalamud.Logging; using Dalamud.Plugin; using MareSynchronos.FileCacheDB; using MareSynchronos.Factories; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState; using System; -using MareSynchronos.Models; -using MareSynchronos.PenumbraMod; -using Newtonsoft.Json; using MareSynchronos.Managers; -using LZ4; using MareSynchronos.WebAPI; using Dalamud.Interface.Windowing; using MareSynchronos.UI; +using MareSynchronos.Utils; +using Penumbra.PlayerWatch; namespace MareSynchronos { @@ -31,7 +24,6 @@ namespace MareSynchronos private readonly CommandManager _commandManager; private readonly Configuration _configuration; private readonly FileCacheManager _fileCacheManager; - private readonly Framework _framework; private readonly IntroUI _introUi; private readonly IpcManager _ipcManager; private readonly ObjectTable _objectTable; @@ -39,13 +31,16 @@ namespace MareSynchronos private readonly PluginUi _pluginUi; private readonly WindowSystem _windowSystem; private CharacterManager? _characterManager; + private readonly DalamudUtil _dalamudUtil; + private readonly CharacterCacheManager _characterCacheManager; + private readonly IPlayerWatcher _playerWatcher; public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, Framework framework, ObjectTable objectTable, ClientState clientState) { + Logger.Debug("Launching " + Name); _pluginInterface = pluginInterface; _commandManager = commandManager; - _framework = framework; _objectTable = objectTable; _clientState = clientState; _configuration = _pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); @@ -56,6 +51,11 @@ namespace MareSynchronos _apiController = new ApiController(_configuration); _ipcManager = new IpcManager(_pluginInterface); _fileCacheManager = new FileCacheManager(new FileCacheFactory(), _ipcManager, _configuration); + _dalamudUtil = new DalamudUtil(_clientState, _objectTable); + _characterCacheManager = new CharacterCacheManager(_clientState, framework, _objectTable, _apiController, + _dalamudUtil, _ipcManager); + _playerWatcher = PlayerWatchFactory.Create(framework, _clientState, _objectTable); + _playerWatcher.Enable(); var uiSharedComponent = new UIShared(_ipcManager, _apiController, _fileCacheManager, _configuration); @@ -83,6 +83,8 @@ namespace MareSynchronos public string Name => "Mare Synchronos"; public void Dispose() { + Logger.Debug("Disposing " + Name); + _commandManager.RemoveHandler(CommandName); _clientState.Login -= ClientState_Login; _clientState.Logout -= ClientState_Logout; @@ -93,13 +95,16 @@ namespace MareSynchronos _fileCacheManager?.Dispose(); _ipcManager?.Dispose(); _characterManager?.Dispose(); + _characterCacheManager.Dispose(); _apiController?.Dispose(); + _playerWatcher.Disable(); + _playerWatcher.Dispose(); } private void ClientState_Login(object? sender, EventArgs e) { - PluginLog.Debug("Client login"); + Logger.Debug("Client login"); _pluginInterface.UiBuilder.Draw += Draw; _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; @@ -119,7 +124,7 @@ namespace MareSynchronos private void ClientState_Logout(object? sender, EventArgs e) { - PluginLog.Debug("Client logout"); + Logger.Debug("Client logout"); _characterManager?.Dispose(); _pluginInterface.UiBuilder.Draw -= Draw; _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; @@ -132,17 +137,23 @@ namespace MareSynchronos Task.Run(async () => { - while (_clientState.LocalPlayer == null) + while (!_dalamudUtil.IsPlayerPresent) { - await Task.Delay(50); + await Task.Delay(100); } - var characterCacheFactory = - new CharacterCacheFactory(_clientState, _ipcManager, new FileReplacementFactory(_ipcManager)); - _characterManager = new CharacterManager( - _clientState, _framework, _apiController, _objectTable, _ipcManager, _configuration, characterCacheFactory); - _characterManager.StartWatchingPlayer(); - _ipcManager.PenumbraRedraw(_clientState.LocalPlayer!.Name.ToString()); + try + { + var characterCacheFactory = + new CharacterDataFactory(_clientState, _ipcManager, new FileReplacementFactory(_ipcManager)); + _characterManager = new CharacterManager(_apiController, _objectTable, _ipcManager, + characterCacheFactory, _characterCacheManager, _dalamudUtil, _playerWatcher); + _characterManager.StartWatchingPlayer(); + } + catch (Exception ex) + { + Logger.Debug(ex.Message); + } }); } diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 8edc5b7..6273316 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -1,15 +1,9 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using ImGuiNET; -using MareSynchronos.FileCacheDB; using MareSynchronos.Managers; -using MareSynchronos.WebAPI; +using MareSynchronos.Utils; namespace MareSynchronos.UI { @@ -25,6 +19,8 @@ namespace MareSynchronos.UI public void Dispose() { + Logger.Debug("Disposing " + nameof(IntroUI)); + _windowSystem.RemoveWindow(this); } diff --git a/MareSynchronos/UI/PluginUI.cs b/MareSynchronos/UI/PluginUI.cs index 8a90f88..5189215 100644 --- a/MareSynchronos/UI/PluginUI.cs +++ b/MareSynchronos/UI/PluginUI.cs @@ -5,6 +5,8 @@ using ImGuiNET; using MareSynchronos.WebAPI; using System; using System.Linq; +using MareSynchronos.Managers; +using MareSynchronos.Utils; namespace MareSynchronos.UI { @@ -24,15 +26,17 @@ namespace MareSynchronos.UI MaximumSize = new(800, 2000), }; - this._configuration = configuration; - this._windowSystem = windowSystem; - this._apiController = apiController; + _configuration = configuration; + _windowSystem = windowSystem; + _apiController = apiController; _uiShared = uiShared; windowSystem.AddWindow(this); } public void Dispose() { + Logger.Debug("Disposing " + nameof(PluginUi)); + _windowSystem.RemoveWindow(this); } diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index c1615d1..9587bbf 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.IO; using System.Numerics; -using System.Text; using System.Threading.Tasks; using Dalamud.Interface.Colors; using ImGuiNET; -using MareSynchronos.FileCacheDB; using MareSynchronos.Managers; using MareSynchronos.WebAPI; @@ -167,7 +162,7 @@ namespace MareSynchronos.UI if (!Directory.Exists(cacheDirectory)) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - UIShared.TextWrapped("The folder you selected does not exist. Please provide a valid path."); + TextWrapped("The folder you selected does not exist. Please provide a valid path."); ImGui.PopStyleColor(); } } diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs new file mode 100644 index 0000000..b43d237 --- /dev/null +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.SubKinds; +using FFXIVClientStructs.FFXIV.Client.Game.Object; + +namespace MareSynchronos.Utils +{ + public class DalamudUtil + { + private readonly ClientState _clientState; + private readonly ObjectTable _objectTable; + + public DalamudUtil(ClientState clientState, ObjectTable objectTable) + { + _clientState = clientState; + _objectTable = objectTable; + } + + public bool IsPlayerPresent => _clientState.LocalPlayer != null; + + public string PlayerName => _clientState.LocalPlayer!.Name.ToString(); + + public string PlayerNameHashed => Crypto.GetHash256(PlayerName + _clientState.LocalPlayer!.HomeWorld.Id); + + public Dictionary GetLocalPlayers() + { + Dictionary allLocalPlayers = new(); + foreach (var obj in _objectTable) + { + if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + string playerName = obj.Name.ToString(); + if (playerName == PlayerName) continue; + var playerObject = (PlayerCharacter)obj; + allLocalPlayers[Crypto.GetHash256(playerObject.Name.ToString() + playerObject.HomeWorld.Id.ToString())] = playerObject; + } + + return allLocalPlayers; + } + + public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress) + { + var obj = (GameObject*)characterAddress; + + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something + { + Logger.Debug("Waiting for character to finish drawing"); + Thread.Sleep(1000); + } + + // wait half a second just in case + Thread.Sleep(500); + } + + public void WaitWhileSelfIsDrawing() => WaitWhileCharacterIsDrawing(_clientState.LocalPlayer!.Address); + } +} diff --git a/MareSynchronos/Utils/Logger.cs b/MareSynchronos/Utils/Logger.cs new file mode 100644 index 0000000..084c811 --- /dev/null +++ b/MareSynchronos/Utils/Logger.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using Dalamud.Logging; + +namespace MareSynchronos.Utils +{ + internal class Logger + { + public static void Debug(string debug) + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + PluginLog.Debug($"[{caller}] {debug}"); + } + } +} diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs index 9cdd8a6..a2e394f 100644 --- a/MareSynchronos/WebAPI/ApiController.cs +++ b/MareSynchronos/WebAPI/ApiController.cs @@ -11,35 +11,57 @@ using System.Threading.Tasks; using LZ4; using MareSynchronos.API; using MareSynchronos.FileCacheDB; +using MareSynchronos.Utils; using Microsoft.AspNetCore.SignalR.Client; namespace MareSynchronos.WebAPI { - public class CharacterReceivedEventArgs : EventArgs - { - public CharacterReceivedEventArgs(string characterNameHash, CharacterCacheDto characterData) - { - CharacterData = characterData; - CharacterNameHash = characterNameHash; - } - - public CharacterCacheDto CharacterData { get; set; } - public string CharacterNameHash { get; set; } - } - public class ApiController : IDisposable { public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; - private readonly Configuration _pluginConfiguration; public const string MainServiceUri = "https://darkarchon.internet-box.ch:5001"; - public string UID { get; private set; } = string.Empty; - public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : "-"; - private string CacheFolder => _pluginConfiguration.CacheFolder; - public ConcurrentDictionary CurrentUploads { get; } = new(); + readonly CancellationTokenSource _cts; + private readonly Configuration _pluginConfiguration; + private HubConnection? _fileHub; + private HubConnection? _heartbeatHub; + private CancellationTokenSource? _uploadCancellationTokenSource; + private HubConnection? _userHub; + public ApiController(Configuration pluginConfiguration) + { + Logger.Debug("Creating " + nameof(ApiController)); + + _pluginConfiguration = pluginConfiguration; + _cts = new CancellationTokenSource(); + + _ = Heartbeat(); + } + + public event EventHandler? CharacterReceived; + + public event EventHandler? Connected; + + public event EventHandler? Disconnected; + + public event EventHandler? PairedClientOffline; + + public event EventHandler? PairedClientOnline; + + public event EventHandler? PairedWithOther; + + public event EventHandler? UnpairedFromOther; + public ConcurrentDictionary CurrentDownloads { get; } = new(); + public ConcurrentDictionary CurrentUploads { get; } = new(); + public bool IsConnected => !string.IsNullOrEmpty(UID); public bool IsDownloading { get; private set; } = false; public bool IsUploading { get; private set; } = false; + public List PairedClients { get; set; } = new(); + public string SecretKey => _pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? _pluginConfiguration.ClientSecret[ApiUri] : "-"; + public bool ServerAlive => + (_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected; + + public string UID { get; private set; } = string.Empty; public bool UseCustomService { get => _pluginConfiguration.UseCustomService; @@ -49,43 +71,91 @@ namespace MareSynchronos.WebAPI _pluginConfiguration.Save(); } } + private string ApiUri => UseCustomService ? _pluginConfiguration.ApiUri : MainServiceUri; - - public bool ServerAlive => - (_heartbeatHub?.State ?? HubConnectionState.Disconnected) == HubConnectionState.Connected; - public bool IsConnected => !string.IsNullOrEmpty(UID); - - public event EventHandler? Connected; - public event EventHandler? Disconnected; - public event EventHandler? CharacterReceived; - public event EventHandler? UnpairedFromOther; - public event EventHandler? PairedWithOther; - public event EventHandler? PairedClientOnline; - public event EventHandler? PairedClientOffline; - - public List PairedClients { get; set; } = new(); - - readonly CancellationTokenSource cts; - private HubConnection? _heartbeatHub; - private HubConnection? _fileHub; - private HubConnection? _userHub; - private CancellationTokenSource? uploadCancellationTokenSource; - - public ApiController(Configuration pluginConfiguration) + private string CacheFolder => _pluginConfiguration.CacheFolder; + public void CancelUpload() { - this._pluginConfiguration = pluginConfiguration; - cts = new CancellationTokenSource(); + if (_uploadCancellationTokenSource != null) + { + PluginLog.Warning("Cancelling upload"); + _uploadCancellationTokenSource?.Cancel(); + _fileHub!.InvokeAsync("AbortUpload"); + } + } - _ = Heartbeat(); + public void Dispose() + { + Logger.Debug("Disposing " + nameof(ApiController)); + + _cts?.Cancel(); + _ = DisposeHubConnections(); + } + + public async Task DownloadFile(string hash) + { + IsDownloading = true; + var reader = await _fileHub!.StreamAsChannelAsync("DownloadFile", hash); + List downloadedData = new(); + while (await reader.WaitToReadAsync()) + { + while (reader.TryRead(out var data)) + { + CurrentDownloads[hash] = (CurrentDownloads[hash].Item1 + data.Length, CurrentDownloads[hash].Item2); + downloadedData.AddRange(data); + //await Task.Delay(25); + } + } + + IsDownloading = false; + return downloadedData.ToArray(); + } + + public async Task DownloadFiles(List fileReplacementDto) + { + foreach (var file in fileReplacementDto) + { + var fileSize = await _fileHub!.InvokeAsync("GetFileSize", file.Hash); + CurrentDownloads[file.Hash] = (0, fileSize); + } + + foreach (var file in fileReplacementDto.Where(f => CurrentDownloads[f.Hash].Item2 > 0)) + { + var hash = file.Hash; + var data = await DownloadFile(hash); + var extractedFile = LZ4Codec.Unwrap(data); + var ext = file.GamePaths.First().Split(".", StringSplitOptions.None).Last(); + var filePath = Path.Combine(_pluginConfiguration.CacheFolder, file.Hash + "." + ext); + await File.WriteAllBytesAsync(filePath, extractedFile); + await using (var db = new FileCacheContext()) + { + db.Add(new FileCache + { + Filepath = filePath.ToLower(), + Hash = file.Hash, + LastModifiedDate = DateTime.Now.Ticks.ToString(), + }); + await db.SaveChangesAsync(); + } + Logger.Debug("File downloaded to " + filePath); + } + + CurrentDownloads.Clear(); + } + + public async Task GetCharacterData(Dictionary hashedCharacterNames) + { + await _userHub!.InvokeAsync("GetCharacterData", + hashedCharacterNames); } public async Task Heartbeat() { - while (!ServerAlive && !cts.Token.IsCancellationRequested) + while (!ServerAlive && !_cts.Token.IsCancellationRequested) { try { - PluginLog.Debug("Attempting to establish heartbeat connection to " + ApiUri); + Logger.Debug("Attempting to establish heartbeat connection to " + ApiUri); _heartbeatHub = new HubConnectionBuilder() .WithUrl(ApiUri + "/heartbeat", options => { @@ -105,9 +175,9 @@ namespace MareSynchronos.WebAPI #endif }).Build(); - await _heartbeatHub.StartAsync(cts.Token); + await _heartbeatHub.StartAsync(_cts.Token); UID = await _heartbeatHub!.InvokeAsync("Heartbeat"); - PluginLog.Debug("Heartbeat started: " + ApiUri); + Logger.Debug("Heartbeat started: " + ApiUri); try { await InitializeHubConnections(); @@ -121,7 +191,7 @@ namespace MareSynchronos.WebAPI _heartbeatHub.Closed += OnHeartbeatHubOnClosed; _heartbeatHub.Reconnected += OnHeartbeatHubOnReconnected; - PluginLog.Debug("Heartbeat established to: " + ApiUri); + Logger.Debug("Heartbeat established to: " + ApiUri); } catch (Exception ex) { @@ -130,46 +200,132 @@ namespace MareSynchronos.WebAPI } } - private async Task LoadInitialData() + public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash) { - var pairedClients = await _userHub!.InvokeAsync>("GetPairedClients"); - PairedClients = pairedClients.ToList(); + Logger.Debug("Received DTO for " + characterHash); + CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); + return Task.CompletedTask; + } + + public async Task Register() + { + if (!ServerAlive) return; + Logger.Debug("Registering at service " + ApiUri); + var response = await _userHub!.InvokeAsync("Register"); + _pluginConfiguration.ClientSecret[ApiUri] = response; + _pluginConfiguration.Save(); + RestartHeartbeat(); } public void RestartHeartbeat() { - PluginLog.Debug("Restarting heartbeat"); + Logger.Debug("Restarting heartbeat"); _heartbeatHub!.Closed -= OnHeartbeatHubOnClosed; _heartbeatHub!.Reconnected -= OnHeartbeatHubOnReconnected; Task.Run(async () => { - await _heartbeatHub.StopAsync(cts.Token); + await _heartbeatHub.StopAsync(_cts.Token); await _heartbeatHub.DisposeAsync(); _heartbeatHub = null!; _ = Heartbeat(); }); } - private async Task OnHeartbeatHubOnReconnected(string? s) + public async Task SendCharacterData(CharacterCacheDto character, List visibleCharacterIds) { - PluginLog.Debug("Reconnected: " + ApiUri); - UID = await _heartbeatHub!.InvokeAsync("Heartbeat"); + if (!IsConnected || SecretKey == "-") return; + Logger.Debug("Sending Character data to service " + ApiUri); + + CancelUpload(); + _uploadCancellationTokenSource = new CancellationTokenSource(); + var uploadToken = _uploadCancellationTokenSource.Token; + Logger.Debug("New Token Created"); + + var filesToUpload = await _fileHub!.InvokeAsync>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); + + IsUploading = true; + + Logger.Debug("Compressing files"); + Dictionary compressedFileData = new(); + foreach (var file in filesToUpload) + { + Logger.Debug(file); + var data = await GetCompressedFileData(file, uploadToken); + compressedFileData.Add(data.Item1, data.Item2); + CurrentUploads[data.Item1] = (0, data.Item2.Length); + } + Logger.Debug("Files compressed, uploading files"); + foreach (var data in compressedFileData) + { + await UploadFile(data.Value, data.Key, uploadToken); + if (uploadToken.IsCancellationRequested) + { + PluginLog.Warning("Cancel in filesToUpload loop detected"); + CurrentUploads.Clear(); + break; + } + } + Logger.Debug("Upload tasks complete, waiting for server to confirm"); + var anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); + Logger.Debug("Uploads open: " + anyUploadsOpen); + while (anyUploadsOpen && !uploadToken.IsCancellationRequested) + { + anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); + await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); + Logger.Debug("Waiting for uploads to finish"); + } + + CurrentUploads.Clear(); + IsUploading = false; + + if (!uploadToken.IsCancellationRequested) + { + Logger.Debug("=== Pushing character data ==="); + await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds, uploadToken); + } + else + { + PluginLog.Warning("=== Upload operation was cancelled ==="); + } + + Logger.Debug("== Upload complete for " + character.JobId); + _uploadCancellationTokenSource = null; } - private Task OnHeartbeatHubOnClosed(Exception? exception) + public async Task> SendCharacterName(string hashedName) { - PluginLog.Debug("Connection closed: " + ApiUri); - Disconnected?.Invoke(null, EventArgs.Empty); - RestartHeartbeat(); - return Task.CompletedTask; + return await _userHub!.InvokeAsync>("SendCharacterNameHash", hashedName); + } + + public async Task SendPairedClientAddition(string uid) + { + if (!IsConnected || SecretKey == "-") return; + await _userHub!.SendAsync("SendPairedClientAddition", uid); + } + + public async Task SendPairedClientPauseChange(string uid, bool paused) + { + if (!IsConnected || SecretKey == "-") return; + await _userHub!.SendAsync("SendPairedClientPauseChange", uid, paused); + } + + public async Task SendPairedClientRemoval(string uid) + { + if (!IsConnected || SecretKey == "-") return; + await _userHub!.SendAsync("SendPairedClientRemoval", uid); + } + + public async Task UpdateCurrentDownloadSize(string hash) + { + long fileSize = await _fileHub!.InvokeAsync("GetFileSize", hash); } private async Task DisposeHubConnections() { if (_fileHub != null) { - PluginLog.Debug("Disposing File Hub"); + Logger.Debug("Disposing File Hub"); CancelUpload(); await _fileHub!.StopAsync(); await _fileHub!.DisposeAsync(); @@ -177,17 +333,25 @@ namespace MareSynchronos.WebAPI if (_userHub != null) { - PluginLog.Debug("Disposing User Hub"); + Logger.Debug("Disposing User Hub"); await _userHub.StopAsync(); await _userHub.DisposeAsync(); } } + private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + await using var db = new FileCacheContext(); + var fileCache = db.FileCaches.First(f => f.Hash == fileHash); + return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath, uploadToken), 0, + (int)new FileInfo(fileCache.Filepath).Length)); + } + private async Task InitializeHubConnections() { await DisposeHubConnections(); - PluginLog.Debug("Creating User Hub"); + Logger.Debug("Creating User Hub"); _userHub = new HubConnectionBuilder() .WithUrl(ApiUri + "/user", options => { @@ -209,7 +373,7 @@ namespace MareSynchronos.WebAPI _userHub.On("RemoveOnlinePairedPlayer", (s) => PairedClientOffline?.Invoke(s, EventArgs.Empty)); _userHub.On("AddOnlinePairedPlayer", (s) => PairedClientOnline?.Invoke(s, EventArgs.Empty)); - PluginLog.Debug("Creating File Hub"); + Logger.Debug("Creating File Hub"); _fileHub = new HubConnectionBuilder() .WithUrl(ApiUri + "/files", options => { @@ -225,9 +389,27 @@ namespace MareSynchronos.WebAPI #endif }) .Build(); - await _fileHub.StartAsync(cts.Token); + await _fileHub.StartAsync(_cts.Token); } + private async Task LoadInitialData() + { + var pairedClients = await _userHub!.InvokeAsync>("GetPairedClients"); + PairedClients = pairedClients.ToList(); + } + private Task OnHeartbeatHubOnClosed(Exception? exception) + { + Logger.Debug("Connection closed: " + ApiUri); + Disconnected?.Invoke(null, EventArgs.Empty); + RestartHeartbeat(); + return Task.CompletedTask; + } + + private async Task OnHeartbeatHubOnReconnected(string? s) + { + Logger.Debug("Reconnected: " + ApiUri); + UID = await _heartbeatHub!.InvokeAsync("Heartbeat"); + } private void UpdateLocalClientPairs(ClientPairDto dto, string characterIdentifier) { var entry = PairedClients.SingleOrDefault(e => e.OtherUID == dto.OtherUID); @@ -258,15 +440,6 @@ namespace MareSynchronos.WebAPI UnpairedFromOther?.Invoke(characterIdentifier, EventArgs.Empty); } } - - private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) - { - await using var db = new FileCacheContext(); - var fileCache = db.FileCaches.First(f => f.Hash == fileHash); - return (fileHash, LZ4Codec.WrapHC(await File.ReadAllBytesAsync(fileCache.Filepath, uploadToken), 0, - (int)new FileInfo(fileCache.Filepath).Length)); - } - private async Task UploadFile(byte[] compressedFile, string fileHash, CancellationToken uploadToken) { if (uploadToken.IsCancellationRequested) return; @@ -290,184 +463,17 @@ namespace MareSynchronos.WebAPI channel.Writer.Complete(); } + } - public async Task Register() + public class CharacterReceivedEventArgs : EventArgs + { + public CharacterReceivedEventArgs(string characterNameHash, CharacterCacheDto characterData) { - if (!ServerAlive) return; - PluginLog.Debug("Registering at service " + ApiUri); - var response = await _userHub!.InvokeAsync("Register"); - _pluginConfiguration.ClientSecret[ApiUri] = response; - _pluginConfiguration.Save(); - RestartHeartbeat(); + CharacterData = characterData; + CharacterNameHash = characterNameHash; } - public void CancelUpload() - { - if (uploadCancellationTokenSource != null) - { - PluginLog.Warning("Cancelling upload"); - uploadCancellationTokenSource?.Cancel(); - _fileHub!.InvokeAsync("AbortUpload"); - } - } - - public async Task SendCharacterData(CharacterCacheDto character, List visibleCharacterIds) - { - if (!IsConnected || SecretKey == "-") return; - PluginLog.Debug("Sending Character data to service " + ApiUri); - - CancelUpload(); - uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = uploadCancellationTokenSource.Token; - PluginLog.Debug("New Token Created"); - - var filesToUpload = await _fileHub!.InvokeAsync>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); - - IsUploading = true; - - PluginLog.Debug("Compressing files"); - Dictionary compressedFileData = new(); - foreach (var file in filesToUpload) - { - PluginLog.Debug(file); - var data = await GetCompressedFileData(file, uploadToken); - compressedFileData.Add(data.Item1, data.Item2); - CurrentUploads[data.Item1] = (0, data.Item2.Length); - } - PluginLog.Debug("Files compressed, uploading files"); - foreach (var data in compressedFileData) - { - await UploadFile(data.Value, data.Key, uploadToken); - if (uploadToken.IsCancellationRequested) - { - PluginLog.Warning("Cancel in filesToUpload loop detected"); - CurrentUploads.Clear(); - break; - } - } - PluginLog.Debug("Upload tasks complete, waiting for server to confirm"); - var anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); - PluginLog.Debug("Uploads open: " + anyUploadsOpen); - while (anyUploadsOpen && !uploadToken.IsCancellationRequested) - { - anyUploadsOpen = await _fileHub!.InvokeAsync("IsUploadFinished", uploadToken); - await Task.Delay(TimeSpan.FromSeconds(0.5), uploadToken); - PluginLog.Debug("Waiting for uploads to finish"); - } - - CurrentUploads.Clear(); - IsUploading = false; - - if (!uploadToken.IsCancellationRequested) - { - PluginLog.Debug("=== Pushing character data ==="); - await _userHub!.InvokeAsync("PushCharacterData", character, visibleCharacterIds, uploadToken); - } - else - { - PluginLog.Warning("=== Upload operation was cancelled ==="); - } - - PluginLog.Debug("== Upload complete for " + character.JobId); - uploadCancellationTokenSource = null; - } - - public Task ReceiveCharacterData(CharacterCacheDto character, string characterHash) - { - PluginLog.Debug("Received DTO for " + characterHash); - CharacterReceived?.Invoke(null, new CharacterReceivedEventArgs(characterHash, character)); - return Task.CompletedTask; - } - - public async Task UpdateCurrentDownloadSize(string hash) - { - long fileSize = await _fileHub!.InvokeAsync("GetFileSize", hash); - } - - public async Task DownloadFiles(List fileReplacementDto, string cacheFolder) - { - foreach (var file in fileReplacementDto) - { - var fileSize = await _fileHub!.InvokeAsync("GetFileSize", file.Hash); - CurrentDownloads[file.Hash] = (0, fileSize); - } - - foreach (var file in fileReplacementDto.Where(f => CurrentDownloads[f.Hash].Item2 > 0)) - { - var hash = file.Hash; - var data = await DownloadFile(hash); - var extractedFile = LZ4.LZ4Codec.Unwrap(data); - var ext = file.GamePaths.First().Split(".", StringSplitOptions.None).Last(); - var filePath = Path.Combine(cacheFolder, file.Hash + "." + ext); - await File.WriteAllBytesAsync(filePath, extractedFile); - await using (var db = new FileCacheContext()) - { - db.Add(new FileCache - { - Filepath = filePath.ToLower(), - Hash = file.Hash, - LastModifiedDate = DateTime.Now.Ticks.ToString(), - }); - await db.SaveChangesAsync(); - } - PluginLog.Debug("File downloaded to " + filePath); - } - - CurrentDownloads.Clear(); - } - - public async Task DownloadFile(string hash) - { - IsDownloading = true; - var reader = await _fileHub!.StreamAsChannelAsync("DownloadFile", hash); - List downloadedData = new(); - while (await reader.WaitToReadAsync()) - { - while (reader.TryRead(out var data)) - { - CurrentDownloads[hash] = (CurrentDownloads[hash].Item1 + data.Length, CurrentDownloads[hash].Item2); - downloadedData.AddRange(data); - //await Task.Delay(25); - } - } - - IsDownloading = false; - return downloadedData.ToArray(); - } - - public async Task GetCharacterData(Dictionary hashedCharacterNames) - { - await _userHub!.InvokeAsync("GetCharacterData", - hashedCharacterNames); - } - - public async Task SendPairedClientPauseChange(string uid, bool paused) - { - if (!IsConnected || SecretKey == "-") return; - await _userHub!.SendAsync("SendPairedClientPauseChange", uid, paused); - } - - public async Task SendPairedClientAddition(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _userHub!.SendAsync("SendPairedClientAddition", uid); - } - - public async Task SendPairedClientRemoval(string uid) - { - if (!IsConnected || SecretKey == "-") return; - await _userHub!.SendAsync("SendPairedClientRemoval", uid); - } - - public void Dispose() - { - cts?.Cancel(); - _ = DisposeHubConnections(); - } - - public async Task> SendCharacterName(string hashedName) - { - return await _userHub!.InvokeAsync>("SendCharacterNameHash", hashedName); - } + public CharacterCacheDto CharacterData { get; set; } + public string CharacterNameHash { get; set; } } }