major refactoring, maybe some bugfixes, idk
This commit is contained in:
		| @@ -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<string, string> ClientSecret { get; set; } = new(); | ||||
|         public Dictionary<string, string> 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<string, string> 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<string, string> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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()!); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,4 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| #nullable disable | ||||
| #nullable disable | ||||
|  | ||||
| namespace MareSynchronos.FileCacheDB | ||||
| { | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| using System; | ||||
| using System.IO; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
|   | ||||
							
								
								
									
										250
									
								
								MareSynchronos/Managers/CharacterCacheManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								MareSynchronos/Managers/CharacterCacheManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<CachedPlayer> _onlineCachedPlayers = new(); | ||||
|     private readonly List<string> _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<string> 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<string, int> 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<FileReplacementDto> 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<string, string> 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<string, int> { { 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(); | ||||
|     } | ||||
| } | ||||
| @@ -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<int, CharacterCacheDto>? CharacterCache { get; set; } | ||||
|         public PlayerCharacter? PlayerCharacter { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class CharacterManager : IDisposable | ||||
|     { | ||||
|         private readonly ApiController _apiController; | ||||
|         readonly Dictionary<string, string> _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<CachedPlayer> _onlineCachedPlayers = new(); | ||||
|  | ||||
|         private Dictionary<string, string> _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<string, PlayerCharacter> 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)); | ||||
|             _characterCacheManager.AddInitialPairs(apiTask.Result); | ||||
|              | ||||
|             _framework.Update += Framework_Update; | ||||
|             _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<CharacterData> 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<string, int> { { 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<FileReplacementDto> 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<string, string> 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<string, string>(characterHash, _onlinePairedUsers[characterHash])); | ||||
|         } | ||||
|  | ||||
|         private void RestoreCharacter(KeyValuePair<string, string> 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<CharacterCache> 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<string> localPlayersList = new(); | ||||
|                 Dictionary<string, PlayerCharacter> 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<string, PlayerCharacter> GetLocalPlayers() | ||||
|         { | ||||
|             Dictionary<string, PlayerCharacter> 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); | ||||
|             } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<string, bool>( | ||||
|                 Directory.EnumerateFiles(penumbraDir, "*.*", SearchOption.AllDirectories) | ||||
|                                 .Select(s => s.ToLowerInvariant()) | ||||
| @@ -80,7 +83,7 @@ namespace MareSynchronos.Managers | ||||
|             var fileCachesToDelete = new ConcurrentBag<FileCache>(); | ||||
|             var fileCachesToAdd = new ConcurrentBag<FileCache>(); | ||||
|  | ||||
|             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(); | ||||
|             }; | ||||
|   | ||||
| @@ -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<object> _penumbraInit; | ||||
|         private readonly ICallGateSubscriber<string, string, string>? _penumbraResolvePath; | ||||
|         private readonly ICallGateSubscriber<string>? _penumbraResolveModDir; | ||||
|         private readonly ICallGateSubscriber<string>? _glamourerGetCharacterCustomization; | ||||
|         private readonly ICallGateSubscriber<string, string, object>? _glamourerApplyCharacterCustomization; | ||||
|         private readonly ICallGateSubscriber<int> _penumbraApiVersion; | ||||
|         private readonly ICallGateSubscriber<int> _glamourerApiVersion; | ||||
|         private readonly ICallGateSubscriber<string, string, object>? _glamourerApplyCharacterCustomization; | ||||
|         private readonly ICallGateSubscriber<string>? _glamourerGetCharacterCustomization; | ||||
|         private readonly ICallGateSubscriber<string, object> _glamourerRevertCustomization; | ||||
|         private readonly ICallGateSubscriber<int> _penumbraApiVersion; | ||||
|         private readonly ICallGateSubscriber<string, string, bool, (int, string)> _penumbraCreateTemporaryCollection; | ||||
|         private readonly ICallGateSubscriber<string, string> _penumbraGetMetaManipulations; | ||||
|         private readonly ICallGateSubscriber<object> _penumbraInit; | ||||
|         private readonly ICallGateSubscriber<IntPtr, int, object?> _penumbraObjectIsRedrawn; | ||||
|         private readonly ICallGateSubscriber<string, int, object>? _penumbraRedraw; | ||||
|         private readonly ICallGateSubscriber<string, int> _penumbraRemoveTemporaryCollection; | ||||
|         private readonly ICallGateSubscriber<string>? _penumbraResolveModDir; | ||||
|         private readonly ICallGateSubscriber<string, string, string>? _penumbraResolvePath; | ||||
|         private readonly ICallGateSubscriber<string, string, string[]>? _penumbraReverseResolvePath; | ||||
|         private readonly ICallGateSubscriber<string, object> _glamourerRevertCustomization; | ||||
|         private readonly ICallGateSubscriber<string, string> _penumbraGetMetaManipulations; | ||||
|         private readonly ICallGateSubscriber<string, string, Dictionary<string, string>, string, int, int> | ||||
|             _penumbraSetTemporaryMod; | ||||
|         private readonly ICallGateSubscriber<string, string, bool, (int, string)> _penumbraCreateTemporaryCollection; | ||||
|         private readonly ICallGateSubscriber<string, int> _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<object>("Penumbra.Initialized"); | ||||
|             _penumbraResolvePath = _pluginInterface.GetIpcSubscriber<string, string, string>("Penumbra.ResolveCharacterPath"); | ||||
|             _penumbraResolveModDir = _pluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory"); | ||||
|             _penumbraRedraw = _pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName"); | ||||
|             _glamourerGetCharacterCustomization = _pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization"); | ||||
|             _glamourerApplyCharacterCustomization = _pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization"); | ||||
|             _penumbraReverseResolvePath = _pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath"); | ||||
|             _penumbraApiVersion = _pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion"); | ||||
|             _glamourerApiVersion = _pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion"); | ||||
|             _glamourerRevertCustomization = _pluginInterface.GetIpcSubscriber<string, object>("Glamourer.RevertCharacterCustomization"); | ||||
|             _penumbraObjectIsRedrawn = _pluginInterface.GetIpcSubscriber<IntPtr, int, object?>("Penumbra.GameObjectRedrawn"); | ||||
|             _penumbraInit = pi.GetIpcSubscriber<object>("Penumbra.Initialized"); | ||||
|             _penumbraResolvePath = pi.GetIpcSubscriber<string, string, string>("Penumbra.ResolveCharacterPath"); | ||||
|             _penumbraResolveModDir = pi.GetIpcSubscriber<string>("Penumbra.GetModDirectory"); | ||||
|             _penumbraRedraw = pi.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName"); | ||||
|             _glamourerGetCharacterCustomization = pi.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization"); | ||||
|             _glamourerApplyCharacterCustomization = pi.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization"); | ||||
|             _penumbraReverseResolvePath = pi.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath"); | ||||
|             _penumbraApiVersion = pi.GetIpcSubscriber<int>("Penumbra.ApiVersion"); | ||||
|             _glamourerApiVersion = pi.GetIpcSubscriber<int>("Glamourer.ApiVersion"); | ||||
|             _glamourerRevertCustomization = pi.GetIpcSubscriber<string, object>("Glamourer.RevertCharacterCustomization"); | ||||
|             _penumbraObjectIsRedrawn = pi.GetIpcSubscriber<IntPtr, int, object?>("Penumbra.GameObjectRedrawn"); | ||||
|             _penumbraGetMetaManipulations = | ||||
|                 _pluginInterface.GetIpcSubscriber<string, string>("Penumbra.GetMetaManipulations"); | ||||
|                 pi.GetIpcSubscriber<string, string>("Penumbra.GetMetaManipulations"); | ||||
|  | ||||
|             _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); | ||||
|             _penumbraInit.Subscribe(RedrawSelf); | ||||
|  | ||||
|             _penumbraSetTemporaryMod = | ||||
|                 _pluginInterface | ||||
|                 pi | ||||
|                     .GetIpcSubscriber<string, string, Dictionary<string, string>, string, int, | ||||
|                         int>("Penumbra.AddTemporaryMod"); | ||||
|  | ||||
|             _penumbraCreateTemporaryCollection = | ||||
|                 _pluginInterface.GetIpcSubscriber<string, string, bool, (int, string)>("Penumbra.CreateTemporaryCollection"); | ||||
|                 pi.GetIpcSubscriber<string, string, bool, (int, string)>("Penumbra.CreateTemporaryCollection"); | ||||
|             _penumbraRemoveTemporaryCollection = | ||||
|                 _pluginInterface.GetIpcSubscriber<string, int>("Penumbra.RemoveTemporaryCollection"); | ||||
|                 pi.GetIpcSubscriber<string, int>("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<string, string> 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<string, string> 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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										43
									
								
								MareSynchronos/Models/CachedPlayer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								MareSynchronos/Models/CachedPlayer.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<int, CharacterCacheDto> 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<FileReplacement> 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<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>(); | ||||
| 
 | ||||
|         [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(); | ||||
| @@ -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<string>(); | ||||
|         [JsonProperty] | ||||
|         public string ResolvedPath { get; set; } = string.Empty; | ||||
|         [JsonProperty] | ||||
|         public string Hash { get; set; } = string.Empty; | ||||
|         public bool IsInUse { get; set; } = false; | ||||
|         public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>(); | ||||
|         [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<FileReplacement> Associated { get; set; } = new List<FileReplacement>(); | ||||
|  | ||||
|         public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed); | ||||
|  | ||||
|         [JsonProperty] | ||||
|         public string[] GamePaths { get; set; } = Array.Empty<string>(); | ||||
|  | ||||
|         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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<string, string> Files { get; set; } = new(); | ||||
|         public Dictionary<string, string> FileSwaps { get; set; } = new(); | ||||
|         public List<string> Manipulations { get; set; } = new(); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|             } | ||||
|         } | ||||
|   | ||||
							
								
								
									
										59
									
								
								MareSynchronos/Utils/DalamudUtil.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								MareSynchronos/Utils/DalamudUtil.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<string, PlayerCharacter> GetLocalPlayers() | ||||
|         { | ||||
|             Dictionary<string, PlayerCharacter> 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); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										14
									
								
								MareSynchronos/Utils/Logger.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								MareSynchronos/Utils/Logger.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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}"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<string, (long, long)> 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<CharacterReceivedEventArgs>? 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<string, (long, long)> CurrentDownloads { get; } = new(); | ||||
|         public ConcurrentDictionary<string, (long, long)> 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<ClientPairDto> 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<CharacterReceivedEventArgs>? CharacterReceived; | ||||
|         public event EventHandler? UnpairedFromOther; | ||||
|         public event EventHandler? PairedWithOther; | ||||
|         public event EventHandler? PairedClientOnline; | ||||
|         public event EventHandler? PairedClientOffline; | ||||
|  | ||||
|         public List<ClientPairDto> 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<byte[]> DownloadFile(string hash) | ||||
|         { | ||||
|             IsDownloading = true; | ||||
|             var reader = await _fileHub!.StreamAsChannelAsync<byte[]>("DownloadFile", hash); | ||||
|             List<byte> 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> fileReplacementDto) | ||||
|         { | ||||
|             foreach (var file in fileReplacementDto) | ||||
|             { | ||||
|                 var fileSize = await _fileHub!.InvokeAsync<long>("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<string, int> 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<string>("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<List<ClientPairDto>>("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<string>("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<string> visibleCharacterIds) | ||||
|         { | ||||
|             PluginLog.Debug("Reconnected: " + ApiUri); | ||||
|             UID = await _heartbeatHub!.InvokeAsync<string>("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<List<string>>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); | ||||
|  | ||||
|             IsUploading = true; | ||||
|  | ||||
|             Logger.Debug("Compressing files"); | ||||
|             Dictionary<string, byte[]> 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<bool>("IsUploadFinished", uploadToken); | ||||
|             Logger.Debug("Uploads open: " + anyUploadsOpen); | ||||
|             while (anyUploadsOpen && !uploadToken.IsCancellationRequested) | ||||
|             { | ||||
|                 anyUploadsOpen = await _fileHub!.InvokeAsync<bool>("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<List<string>> SendCharacterName(string hashedName) | ||||
|         { | ||||
|             PluginLog.Debug("Connection closed: " + ApiUri); | ||||
|             Disconnected?.Invoke(null, EventArgs.Empty); | ||||
|             RestartHeartbeat(); | ||||
|             return Task.CompletedTask; | ||||
|             return await _userHub!.InvokeAsync<List<string>>("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<long>("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<string>("RemoveOnlinePairedPlayer", (s) => PairedClientOffline?.Invoke(s, EventArgs.Empty)); | ||||
|             _userHub.On<string>("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<List<ClientPairDto>>("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<string>("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<string>("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<string> 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<List<string>>("SendFiles", character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); | ||||
|  | ||||
|             IsUploading = true; | ||||
|  | ||||
|             PluginLog.Debug("Compressing files"); | ||||
|             Dictionary<string, byte[]> 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<bool>("IsUploadFinished", uploadToken); | ||||
|             PluginLog.Debug("Uploads open: " + anyUploadsOpen); | ||||
|             while (anyUploadsOpen && !uploadToken.IsCancellationRequested) | ||||
|             { | ||||
|                 anyUploadsOpen = await _fileHub!.InvokeAsync<bool>("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<long>("GetFileSize", hash); | ||||
|         } | ||||
|  | ||||
|         public async Task DownloadFiles(List<FileReplacementDto> fileReplacementDto, string cacheFolder) | ||||
|         { | ||||
|             foreach (var file in fileReplacementDto) | ||||
|             { | ||||
|                 var fileSize = await _fileHub!.InvokeAsync<long>("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<byte[]> DownloadFile(string hash) | ||||
|         { | ||||
|             IsDownloading = true; | ||||
|             var reader = await _fileHub!.StreamAsChannelAsync<byte[]>("DownloadFile", hash); | ||||
|             List<byte> 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<string, int> 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<List<string>> SendCharacterName(string hashedName) | ||||
|         { | ||||
|             return await _userHub!.InvokeAsync<List<string>>("SendCharacterNameHash", hashedName); | ||||
|         } | ||||
|         public CharacterCacheDto CharacterData { get; set; } | ||||
|         public string CharacterNameHash { get; set; } | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stanley Dimant
					Stanley Dimant