From da2b2701e8164399d51caafad3f60bc5f6d819c3 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Tue, 14 Jun 2022 21:53:41 +0200 Subject: [PATCH] actually start to bring structure into the project make it resilent against restarts/reloads remove all user interaction for resource gathering compute hashes on first time file resolving and on updates of said file on resolving --- MareSynchronos/Configuration.cs | 2 +- MareSynchronos/Factories/FileCacheFactory.cs | 21 +- .../Factories/FileReplacementFactory.cs | 20 +- MareSynchronos/FileCacheDB/FileCache.cs | 1 + .../FileCacheDB/FileCacheContext.cs | 2 + MareSynchronos/Hooks/DrawHooks.cs | 19 +- MareSynchronos/Managers/IpcManager.cs | 119 ++++++++ MareSynchronos/Models/CharacterCache.cs | 57 ++-- MareSynchronos/Models/FileReplacement.cs | 67 ++++- MareSynchronos/Plugin.cs | 279 +++++++----------- MareSynchronos/PluginUI.cs | 2 +- MareSynchronos/Utils/Crypto.cs | 22 ++ 12 files changed, 379 insertions(+), 232 deletions(-) create mode 100644 MareSynchronos/Managers/IpcManager.cs create mode 100644 MareSynchronos/Utils/Crypto.cs diff --git a/MareSynchronos/Configuration.cs b/MareSynchronos/Configuration.cs index a76485e..2e00b03 100644 --- a/MareSynchronos/Configuration.cs +++ b/MareSynchronos/Configuration.cs @@ -2,7 +2,7 @@ using Dalamud.Plugin; using System; -namespace SamplePlugin +namespace MareSynchronos { [Serializable] public class Configuration : IPluginConfiguration diff --git a/MareSynchronos/Factories/FileCacheFactory.cs b/MareSynchronos/Factories/FileCacheFactory.cs index f915f87..afba345 100644 --- a/MareSynchronos/Factories/FileCacheFactory.cs +++ b/MareSynchronos/Factories/FileCacheFactory.cs @@ -1,22 +1,15 @@ -using System; -using System.IO; +using System.IO; using MareSynchronos.FileCacheDB; -using System.Security.Cryptography; - +using MareSynchronos.Utils; namespace MareSynchronos.Factories { public class FileCacheFactory { - public FileCacheFactory() - { - - } - public FileCache Create(string file) { FileInfo fileInfo = new(file); - string sha1Hash = GetHash(fileInfo.FullName); + string sha1Hash = Crypto.GetFileHash(fileInfo.FullName); return new FileCache() { Filepath = fileInfo.FullName, @@ -28,14 +21,8 @@ namespace MareSynchronos.Factories public void UpdateFileCache(FileCache cache) { FileInfo fileInfo = new(cache.Filepath); - cache.Hash = GetHash(cache.Filepath); + cache.Hash = Crypto.GetFileHash(cache.Filepath); cache.LastModifiedDate = fileInfo.LastWriteTimeUtc.Ticks.ToString(); } - - private string GetHash(string filePath) - { - using SHA1CryptoServiceProvider cryptoProvider = new(); - return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", ""); - } } } diff --git a/MareSynchronos/Factories/FileReplacementFactory.cs b/MareSynchronos/Factories/FileReplacementFactory.cs index 9c4b33d..8218bd6 100644 --- a/MareSynchronos/Factories/FileReplacementFactory.cs +++ b/MareSynchronos/Factories/FileReplacementFactory.cs @@ -1,36 +1,34 @@ using Dalamud.Game.ClientState; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; -using MareSynchronos.FileCacheDB; +using MareSynchronos.Managers; using MareSynchronos.Models; namespace MareSynchronos.Factories { public class FileReplacementFactory { + private readonly IpcManager ipcManager; private readonly ClientState clientState; - private ICallGateSubscriber resolvePath; - private string penumbraDirectory; - public FileReplacementFactory(DalamudPluginInterface pluginInterface, ClientState clientState) + public FileReplacementFactory(IpcManager ipcManager, ClientState clientState) { - resolvePath = pluginInterface.GetIpcSubscriber("Penumbra.ResolveCharacterPath"); - penumbraDirectory = pluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc().ToLower() + '\\'; + this.ipcManager = ipcManager; this.clientState = clientState; } + public FileReplacement Create(string gamePath, bool resolve = true) { - var fileReplacement = new FileReplacement(gamePath, penumbraDirectory); + var fileReplacement = new FileReplacement(gamePath, ipcManager.PenumbraModDirectory()!); if (!resolve) return fileReplacement; - fileReplacement.SetResolvedPath(resolvePath.InvokeFunc(gamePath, clientState.LocalPlayer!.Name.ToString())); + string playerName = clientState.LocalPlayer!.Name.ToString(); + fileReplacement.SetResolvedPath(ipcManager.PenumbraResolvePath(gamePath, playerName)!); if (!fileReplacement.HasFileReplacement) { // try to resolve path with --filename instead? string[] tempGamePath = gamePath.Split('/'); tempGamePath[tempGamePath.Length - 1] = "--" + tempGamePath[tempGamePath.Length - 1]; string newTempGamePath = string.Join('/', tempGamePath); - var resolvedPath = resolvePath.InvokeFunc(newTempGamePath, clientState.LocalPlayer!.Name.ToString()); + var resolvedPath = ipcManager.PenumbraResolvePath(newTempGamePath, playerName)!; if (resolvedPath != newTempGamePath) { fileReplacement.SetResolvedPath(resolvedPath); diff --git a/MareSynchronos/FileCacheDB/FileCache.cs b/MareSynchronos/FileCacheDB/FileCache.cs index 0ce783e..ee513ac 100644 --- a/MareSynchronos/FileCacheDB/FileCache.cs +++ b/MareSynchronos/FileCacheDB/FileCache.cs @@ -10,5 +10,6 @@ namespace MareSynchronos.FileCacheDB public string Hash { get; set; } public string Filepath { get; set; } public string LastModifiedDate { get; set; } + public int Version { get; set; } } } diff --git a/MareSynchronos/FileCacheDB/FileCacheContext.cs b/MareSynchronos/FileCacheDB/FileCacheContext.cs index 7813da8..e5e62c6 100644 --- a/MareSynchronos/FileCacheDB/FileCacheContext.cs +++ b/MareSynchronos/FileCacheDB/FileCacheContext.cs @@ -37,6 +37,8 @@ namespace MareSynchronos.FileCacheDB entity.HasKey(e => new { e.Hash, e.Filepath }); entity.ToTable("FileCache"); + + entity.Property(c => c.Version).HasDefaultValue(0).IsRowVersion(); }); OnModelCreatingPartial(modelBuilder); diff --git a/MareSynchronos/Hooks/DrawHooks.cs b/MareSynchronos/Hooks/DrawHooks.cs index cf30c29..597fc33 100644 --- a/MareSynchronos/Hooks/DrawHooks.cs +++ b/MareSynchronos/Hooks/DrawHooks.cs @@ -40,6 +40,8 @@ namespace MareSynchronos.Hooks [Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")] public Hook? LoadMtrlTexHook; + public event EventHandler? PlayerLoadEvent; + public Hook? ResolveMdlPathHook; public Hook? ResolveMtrlPathHook; private readonly ClientState clientState; @@ -51,7 +53,6 @@ namespace MareSynchronos.Hooks private ConcurrentBag cachedResources = new(); private GameObject* lastGameObject = null; private ConcurrentBag loadedMaterials = new(); - private CharacterCache characterCache; public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui) { @@ -60,7 +61,6 @@ namespace MareSynchronos.Hooks this.objectTable = objectTable; this.factory = factory; this.gameGui = gameGui; - characterCache = new CharacterCache(); SignatureHelper.Initialise(this); } @@ -85,9 +85,9 @@ namespace MareSynchronos.Hooks resource.ImcData = string.Empty; } - var cache = new CharacterCache(); + PluginLog.Debug("Invaldated resource cache"); - PluginLog.Debug("Invaldated character cache"); + var cache = new CharacterCache(); var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); for (var idx = 0; idx < model->SlotCount; ++idx) @@ -100,6 +100,7 @@ namespace MareSynchronos.Hooks var mdlResource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString()); var cachedMdlResource = cachedResources.First(r => r.IsReplacedByThis(mdlResource)); + var imc = (ResourceHandle*)model->IMCArray[idx]; if (imc != null) { @@ -178,7 +179,6 @@ namespace MareSynchronos.Hooks private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) { - PluginLog.Debug("Character base detour"); var ret = CharacterBaseCreateHook!.Original(a, b, c, d); if (lastGameObject != null) { @@ -193,7 +193,7 @@ namespace MareSynchronos.Hooks if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx)) { var gameObj = GetGameObjectFromDrawObject(drawBase, idx); - if (gameObj == (GameObject*)clientState.LocalPlayer!.Address) + if (clientState.LocalPlayer != null && gameObj == (GameObject*)clientState.LocalPlayer!.Address) { PluginLog.Debug("Clearing resources"); cachedResources.Clear(); @@ -347,7 +347,8 @@ namespace MareSynchronos.Hooks private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle) { - LoadMtrlHelper(mtrlResourceHandle); + if (clientState.LocalPlayer != null) + LoadMtrlHelper(mtrlResourceHandle); var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle); return ret; } @@ -360,7 +361,7 @@ namespace MareSynchronos.Hooks private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path) { - if (path == IntPtr.Zero) + if (path == IntPtr.Zero || clientState.LocalPlayer == null) { return path; } @@ -393,6 +394,8 @@ namespace MareSynchronos.Hooks return path; } + PlayerLoadEvent?.Invoke((IntPtr)gameObject, new EventArgs()); + var resource = factory.Create(gamepath.ToString()); if (gamepath.ToString().EndsWith("mtrl")) diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs new file mode 100644 index 0000000..8c6a852 --- /dev/null +++ b/MareSynchronos/Managers/IpcManager.cs @@ -0,0 +1,119 @@ +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using System; + +namespace MareSynchronos.Managers +{ + public class IpcManager : IDisposable + { + private readonly DalamudPluginInterface pluginInterface; + private ICallGateSubscriber penumbraInit; + private readonly ICallGateSubscriber penumbraDispose; + private ICallGateSubscriber? penumbraResolvePath; + private ICallGateSubscriber? penumbraResolveModDir; + private ICallGateSubscriber? glamourerGetCharacterCustomization; + private ICallGateSubscriber? glamourerApplyCharacterCustomization; + private ICallGateSubscriber? penumbraRedraw; + + public bool Initialized { get; private set; } = false; + + public event EventHandler? IpcManagerInitialized; + + public IpcManager(DalamudPluginInterface pi) + { + pluginInterface = pi; + penumbraInit = pluginInterface.GetIpcSubscriber("Penumbra.Initialized"); + penumbraInit.Subscribe(Initialize); + penumbraDispose = pluginInterface.GetIpcSubscriber("Penumbra.Disposed"); + penumbraDispose.Subscribe(Uninitialize); + } + + private bool CheckPenumbraAPI() + { + try + { + var penumbraApiVersion = pluginInterface.GetIpcSubscriber("Penumbra.ApiVersion").InvokeFunc(); + return penumbraApiVersion >= 4; + } + catch + { + return false; + } + } + + private bool CheckGlamourerAPI() + { + try + { + var glamourerApiVersion = pluginInterface.GetIpcSubscriber("Glamourer.ApiVersion").InvokeFunc(); + return glamourerApiVersion >= 0; + } + catch + { + return false; + } + } + + public void Initialize() + { + if (Initialized) return; + if (!CheckPenumbraAPI()) throw new Exception("Penumbra API is outdated or not available"); + if (!CheckGlamourerAPI()) throw new Exception("Glamourer API is oudated or not available"); + penumbraResolvePath = pluginInterface.GetIpcSubscriber("Penumbra.ResolveCharacterPath"); + penumbraResolveModDir = pluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory"); + penumbraRedraw = pluginInterface.GetIpcSubscriber("Penumbra.RedrawObjectByName"); + glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber("Glamourer.GetCharacterCustomization"); + glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization"); + Initialized = true; + IpcManagerInitialized?.Invoke(this, new EventArgs()); + PluginLog.Debug("[IPC Manager] initialized"); + } + + private void Uninitialize() + { + penumbraResolvePath = null; + penumbraResolveModDir = null; + glamourerGetCharacterCustomization = null; + glamourerApplyCharacterCustomization = null; + Initialized = false; + PluginLog.Debug("IPC Manager disposed"); + } + + public string? PenumbraResolvePath(string path, string characterName) + { + if (!Initialized) return null; + return penumbraResolvePath!.InvokeFunc(path, characterName); + } + + public string? PenumbraModDirectory() + { + if (!Initialized) return null; + return penumbraResolveModDir!.InvokeFunc(); + } + + public string? GlamourerGetCharacterCustomization() + { + if (!Initialized) return null; + return glamourerGetCharacterCustomization!.InvokeFunc(); + } + + public void GlamourerApplyCharacterCustomization(string customization, string characterName) + { + if (!Initialized) return; + glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName); + } + + public void PenumbraRedraw(string actorName) + { + if (!Initialized) return; + penumbraRedraw!.InvokeAction(actorName, 0); + } + + public void Dispose() + { + Uninitialize(); + IpcManagerInitialized = null; + } + } +} diff --git a/MareSynchronos/Models/CharacterCache.cs b/MareSynchronos/Models/CharacterCache.cs index 441b2fc..71a1c81 100644 --- a/MareSynchronos/Models/CharacterCache.cs +++ b/MareSynchronos/Models/CharacterCache.cs @@ -11,41 +11,23 @@ namespace MareSynchronos.Models [JsonObject(MemberSerialization.OptIn)] public class CharacterCache { - public List FileReplacements { get; set; } = new List(); - [JsonProperty] public List AllReplacements => FileReplacements.Where(x => x.HasFileReplacement) .Concat(FileReplacements.SelectMany(f => f.Associated).Where(f => f.HasFileReplacement)) .Concat(FileReplacements.SelectMany(f => f.Associated).SelectMany(f => f.Associated).Where(f => f.HasFileReplacement)) + .Distinct() .ToList(); - public CharacterCache() - { + public List FileReplacements { get; set; } = new List(); - } + [JsonProperty] + public string GlamourerString { get; private set; } = string.Empty; - public void Invalidate(List? fileReplacements = null) - { - try - { - var fileReplacement = fileReplacements ?? FileReplacements.ToList(); - foreach (var item in fileReplacement) - { - item.IsInUse = false; - Invalidate(item.Associated); - if (FileReplacements.Contains(item)) - { - FileReplacements.Remove(item); - } - } - } - catch (Exception ex) - { - PluginLog.Debug(ex.Message); - } - } + public bool IsReady => FileReplacements.All(f => f.Computed); + [JsonProperty] + public uint JobId { get; set; } = 0; public void AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent) { try @@ -76,6 +58,31 @@ namespace MareSynchronos.Models } } + public void Invalidate(List? fileReplacements = null) + { + try + { + var fileReplacement = fileReplacements ?? FileReplacements.ToList(); + foreach (var item in fileReplacement) + { + item.IsInUse = false; + Invalidate(item.Associated); + if (FileReplacements.Contains(item)) + { + FileReplacements.Remove(item); + } + } + } + catch (Exception ex) + { + PluginLog.Debug(ex.Message); + } + } + + public void SetGlamourerData(string glamourerString) + { + GlamourerString = glamourerString; + } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index 9ca9ed1..c6cf015 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -6,6 +6,8 @@ using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using MareSynchronos.FileCacheDB; +using System.IO; +using MareSynchronos.Utils; namespace MareSynchronos.Models { @@ -24,6 +26,9 @@ namespace MareSynchronos.Models [JsonProperty] public string ImcData { get; set; } = string.Empty; public bool HasFileReplacement => GamePath != ResolvedPath; + + public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed); + private Task? computationTask = null; public FileReplacement(string gamePath, string penumbraDirectory) { GamePath = gamePath; @@ -50,15 +55,58 @@ namespace MareSynchronos.Models ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/'); if (!HasFileReplacement) return; - Task.Run(() => + computationTask = Task.Run(() => { - using FileCacheContext db = new FileCacheContext(); - var fileCache = db.FileCaches.SingleOrDefault(f => f.Filepath == path.ToLower()); + FileCache? fileCache; + using (FileCacheContext db = new()) + { + fileCache = db.FileCaches.SingleOrDefault(f => f.Filepath == path.ToLower()); + } + if (fileCache != null) - Hash = fileCache.Hash; + { + FileInfo fi = new(fileCache.Filepath); + if (fi.LastWriteTimeUtc.Ticks == long.Parse(fileCache.LastModifiedDate)) + { + Hash = fileCache.Hash; + } + else + { + Hash = ComputeHash(fi); + using var db = new FileCacheContext(); + var newTempCache = db.FileCaches.Single(f => f.Filepath == path.ToLower()); + newTempCache.Hash = Hash; + db.Update(newTempCache); + db.SaveChanges(); + } + } + else + { + Hash = ComputeHash(new FileInfo(path)); + } }); } + private string ComputeHash(FileInfo fi) + { + // compute hash if hash is not present + string hash = Crypto.GetFileHash(fi.FullName); + + using FileCacheContext db = new(); + var fileAddedDuringCompute = db.FileCaches.SingleOrDefault(f => f.Filepath == fi.FullName.ToLower()); + if (fileAddedDuringCompute != null) return fileAddedDuringCompute.Hash; + + db.Add(new FileCache() + { + Hash = hash, + Filepath = fi.FullName.ToLower(), + LastModifiedDate = fi.LastWriteTimeUtc.Ticks.ToString() + }); + db.SaveChanges(); + + return hash; + } + public bool IsReplacedByThis(string path) { return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower(); @@ -83,5 +131,16 @@ namespace MareSynchronos.Models } return builder.ToString(); } + + public override bool Equals(object? obj) + { + if (obj == null) return true; + if (obj.GetType() == typeof(FileReplacement)) + { + return Hash == ((FileReplacement)obj).Hash && GamePath == ((FileReplacement)obj).GamePath; + } + + return base.Equals(obj); + } } } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index bc40ebc..f98c1ae 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -18,7 +18,6 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; -using Glamourer.Customization; using System.Text; using Penumbra.GameData.Enums; using System; @@ -31,106 +30,133 @@ using System.Text.Unicode; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Reflection; +using MareSynchronos.Managers; +using FFXIVClientStructs.FFXIV.Client.Game.Object; -namespace SamplePlugin +namespace MareSynchronos { public sealed class Plugin : IDalamudPlugin { public string Name => "Mare Synchronos"; private const string commandName = "/mare"; + private readonly Framework framework; + private readonly ObjectTable objectTable; private readonly ClientState clientState; + private readonly GameGui gameGui; - private DalamudPluginInterface PluginInterface { get; init; } - private CommandManager CommandManager { get; init; } - private Configuration Configuration { get; init; } + private DalamudPluginInterface pluginInterface { get; init; } + private CommandManager commandManager { get; init; } + private Configuration configuration { get; init; } private PluginUI PluginUi { get; init; } - private FileCacheFactory FileCacheFactory { get; init; } private DrawHooks drawHooks; + private IpcManager ipcManager; - private CancellationTokenSource cts; - private IPlayerWatcher playerWatch; - - public Plugin( - [RequiredVersion("1.0")] DalamudPluginInterface pluginInterface, - [RequiredVersion("1.0")] CommandManager commandManager, - Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager, GameGui gameGui) + public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, + Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui) { - this.PluginInterface = pluginInterface; - this.CommandManager = commandManager; + this.pluginInterface = pluginInterface; + this.commandManager = commandManager; + this.framework = framework; + this.objectTable = objectTable; this.clientState = clientState; - this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - this.Configuration.Initialize(this.PluginInterface); + this.gameGui = gameGui; + configuration = this.pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + configuration.Initialize(this.pluginInterface); // you might normally want to embed resources and load them from the manifest stream - this.PluginUi = new PluginUI(this.Configuration); + this.PluginUi = new PluginUI(this.configuration); - this.CommandManager.AddHandler(commandName, new CommandInfo(OnCommand) + this.commandManager.AddHandler(commandName, new CommandInfo(OnCommand) { HelpMessage = "pass 'scan' to initialize or rescan files into the database" }); - FileCacheFactory = new FileCacheFactory(); + FileCacheContext db = new FileCacheContext(); + db.Dispose(); - this.PluginInterface.UiBuilder.Draw += DrawUI; - this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; + clientState.Login += ClientState_Login; + clientState.Logout += ClientState_Logout; - playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable); - drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(pluginInterface, clientState), gameGui); + if (clientState.IsLoggedIn) + { + ClientState_Login(null, null!); + } + + this.pluginInterface.UiBuilder.Draw += DrawUI; + this.pluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; + } + + private void IpcManager_IpcManagerInitialized(object? sender, EventArgs e) + { + PluginLog.Debug("IPC Manager initialized event"); + ipcManager.IpcManagerInitialized -= IpcManager_IpcManagerInitialized; + Task.Run(async () => + { + while (clientState.LocalPlayer == null) + { + await Task.Delay(500); + } + drawHooks.StartHooks(); + ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); + }); + } + + private void ClientState_Logout(object? sender, EventArgs e) + { + PluginLog.Debug("Client logout"); + drawHooks.PlayerLoadEvent -= DrawHooks_PlayerLoadEvent; + ipcManager.Dispose(); + drawHooks.Dispose(); + ipcManager = null!; + drawHooks = null!; + } + + private void ClientState_Login(object? sender, EventArgs e) + { + PluginLog.Debug("Client login"); + ipcManager = new IpcManager(pluginInterface); + drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui); + ipcManager.IpcManagerInitialized += IpcManager_IpcManagerInitialized; + ipcManager.Initialize(); + drawHooks.PlayerLoadEvent += DrawHooks_PlayerLoadEvent; + } + + private Task drawHookTask; + + private unsafe void DrawHooks_PlayerLoadEvent(object? sender, EventArgs e) + { + if (sender == null) return; + if (drawHookTask != null && !drawHookTask.IsCompleted) return; + + var obj = (GameObject*)(IntPtr)sender; + drawHookTask = Task.Run(() => + { + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something + { + Thread.Sleep(10); + } + + // we should recalculate cache here + // probably needs a different method + // at that point we will also have to send data to the api + _ = drawHooks.BuildCharacterCache(); + }); } public void Dispose() { - this.PluginUi.Dispose(); - this.CommandManager.RemoveHandler(commandName); - playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; - playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); - drawHooks.Dispose(); + this.PluginUi?.Dispose(); + this.commandManager.RemoveHandler(commandName); + drawHooks.PlayerLoadEvent -= DrawHooks_PlayerLoadEvent; + clientState.Login -= ClientState_Login; + clientState.Logout -= ClientState_Logout; + ipcManager?.Dispose(); + drawHooks?.Dispose(); } private void OnCommand(string command, string args) { - if (args == "stop") - { - cts?.Cancel(); - return; - } - - if(args == "playerdata") - { - PluginLog.Debug(PluginInterface.GetIpcSubscriber("Glamourer.GetCharacterCustomization").InvokeFunc()); - } - - if(args == "applyglam") - { - PluginInterface.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization") - .InvokeAction("Ah3/DwQBAR4IBHOABIOceTIIApkDAgADQmQBZJqepQZlAAEAAAAAAAAAAACcEwEAyxcBbrAXAUnKFwJIuBcBBkYAAQBIAAEANQABADUAAQACAAQAAQAAAIA/Eg==", "Ilya Zhelmo"); - } - - if (args == "scan") - { - cts = new CancellationTokenSource(); - - Task.Run(() => StartScan(), cts.Token); - } - - if (args == "watch") - { - playerWatch.AddPlayerToWatch("Ilya Zhelmo"); - playerWatch.PlayerChanged += PlayerWatch_PlayerChanged; - } - - if (args == "stopwatch") - { - playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; - playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); - } - - if (args == "hook") - { - drawHooks.StartHooks(); - } - if (args == "print") { var resources = drawHooks.PrintRequestedResources(); @@ -139,8 +165,17 @@ namespace SamplePlugin if (args == "printjson") { var cache = drawHooks.BuildCharacterCache(); - var json = JsonConvert.SerializeObject(cache, Formatting.Indented); - PluginLog.Debug(json); + cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!); + cache.JobId = clientState.LocalPlayer!.ClassJob.Id; + Task.Run(async () => + { + while (!cache.IsReady) + { + await Task.Delay(50); + } + var json = JsonConvert.SerializeObject(cache, Formatting.Indented); + PluginLog.Debug(json); + }); } if (args == "createtestmod") @@ -149,7 +184,7 @@ namespace SamplePlugin { var playerName = clientState.LocalPlayer!.Name.ToString(); var modName = $"Mare Synchronos Test Mod {playerName}"; - var modDirectory = PluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc(); + var modDirectory = ipcManager.PenumbraModDirectory()!; string modDirectoryPath = Path.Combine(modDirectory, modName); if (Directory.Exists(modDirectoryPath)) { @@ -216,100 +251,14 @@ namespace SamplePlugin private void PlayerWatch_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) { - var equipment = playerWatch.UpdatePlayerWithoutEvent(actor); - var customization = new CharacterCustomization(actor); + //var equipment = playerWatch.UpdatePlayerWithoutEvent(actor); + //var customization = new CharacterCustomization(actor); //DebugCustomization(customization); //PluginLog.Debug(customization.Gender.ToString()); - if (equipment != null) - { - PluginLog.Debug(equipment.ToString()); - } - } - - private void StartScan() - { - Stopwatch st = Stopwatch.StartNew(); - - string penumbraDir = PluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc(); - PluginLog.Debug("Getting files from " + penumbraDir); - ConcurrentDictionary charaFiles = new ConcurrentDictionary( - Directory.GetFiles(penumbraDir, "*.*", SearchOption.AllDirectories) - .Select(s => s.ToLowerInvariant()) - .Where(f => !f.EndsWith(".json")) - .Where(f => f.Contains(@"\chara\")) - .Select(p => new KeyValuePair(p, false))); - int count = 0; - using FileCacheContext db = new(); - var fileCaches = db.FileCaches.ToList(); - - var fileCachesToUpdate = new ConcurrentBag(); - var fileCachesToDelete = new ConcurrentBag(); - var fileCachesToAdd = new ConcurrentBag(); - - // scan files from database - Parallel.ForEach(fileCaches, new ParallelOptions() - { - CancellationToken = cts.Token, - MaxDegreeOfParallelism = 10 - }, - cache => - { - count = Interlocked.Increment(ref count); - PluginLog.Debug($"[{count}/{fileCaches.Count}] Checking: {cache.Filepath}"); - - if (!File.Exists(cache.Filepath)) - { - PluginLog.Debug("File was not found anymore: " + cache.Filepath); - fileCachesToDelete.Add(cache); - } - else - { - charaFiles[cache.Filepath] = true; - - FileInfo fileInfo = new(cache.Filepath); - if (fileInfo.LastWriteTimeUtc.Ticks != long.Parse(cache.LastModifiedDate)) - { - PluginLog.Debug("File was modified since last time: " + cache.Filepath + "; " + cache.LastModifiedDate + " / " + fileInfo.LastWriteTimeUtc.Ticks); - FileCacheFactory.UpdateFileCache(cache); - fileCachesToUpdate.Add(cache); - } - } - }); - - // scan new files - count = 0; - Parallel.ForEach(charaFiles.Where(c => c.Value == false), new ParallelOptions() - { - CancellationToken = cts.Token, - MaxDegreeOfParallelism = 10 - }, - file => - { - count = Interlocked.Increment(ref count); - PluginLog.Debug($"[{count}/{charaFiles.Count()}] Hashing: {file.Key}"); - - fileCachesToAdd.Add(FileCacheFactory.Create(file.Key)); - }); - - st.Stop(); - - if (cts.Token.IsCancellationRequested) return; - - PluginLog.Debug("Scanning complete, total elapsed time: " + st.Elapsed.ToString()); - - if (fileCachesToAdd.Any() || fileCachesToUpdate.Any() || fileCachesToDelete.Any()) - { - PluginLog.Debug("Writing files to database…"); - - db.FileCaches.AddRange(fileCachesToAdd); - db.FileCaches.UpdateRange(fileCachesToUpdate); - db.FileCaches.RemoveRange(fileCachesToDelete); - - db.SaveChanges(); - PluginLog.Debug("Database has been written."); - } - - cts = new CancellationTokenSource(); + //if (equipment != null) + //{ + // PluginLog.Debug(equipment.ToString()); + //} } private void DrawUI() diff --git a/MareSynchronos/PluginUI.cs b/MareSynchronos/PluginUI.cs index 1e3ca93..ef05770 100644 --- a/MareSynchronos/PluginUI.cs +++ b/MareSynchronos/PluginUI.cs @@ -2,7 +2,7 @@ using System; using System.Numerics; -namespace SamplePlugin +namespace MareSynchronos { // It is good to have this be disposable in general, in case you ever need it // to do any cleanup diff --git a/MareSynchronos/Utils/Crypto.cs b/MareSynchronos/Utils/Crypto.cs new file mode 100644 index 0000000..231ac4d --- /dev/null +++ b/MareSynchronos/Utils/Crypto.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace MareSynchronos.Utils +{ + public class Crypto + { + public static string GetFileHash(string filePath) + { + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", ""); + } + + public static string GetHash(string stringToHash) + { + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", ""); + } + } +}