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
This commit is contained in:
Stanley Dimant
2022-06-14 21:53:41 +02:00
parent c0de781421
commit da2b2701e8
12 changed files with 379 additions and 232 deletions

View File

@@ -2,7 +2,7 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using System; using System;
namespace SamplePlugin namespace MareSynchronos
{ {
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration

View File

@@ -1,22 +1,15 @@
using System; using System.IO;
using System.IO;
using MareSynchronos.FileCacheDB; using MareSynchronos.FileCacheDB;
using System.Security.Cryptography; using MareSynchronos.Utils;
namespace MareSynchronos.Factories namespace MareSynchronos.Factories
{ {
public class FileCacheFactory public class FileCacheFactory
{ {
public FileCacheFactory()
{
}
public FileCache Create(string file) public FileCache Create(string file)
{ {
FileInfo fileInfo = new(file); FileInfo fileInfo = new(file);
string sha1Hash = GetHash(fileInfo.FullName); string sha1Hash = Crypto.GetFileHash(fileInfo.FullName);
return new FileCache() return new FileCache()
{ {
Filepath = fileInfo.FullName, Filepath = fileInfo.FullName,
@@ -28,14 +21,8 @@ namespace MareSynchronos.Factories
public void UpdateFileCache(FileCache cache) public void UpdateFileCache(FileCache cache)
{ {
FileInfo fileInfo = new(cache.Filepath); FileInfo fileInfo = new(cache.Filepath);
cache.Hash = GetHash(cache.Filepath); cache.Hash = Crypto.GetFileHash(cache.Filepath);
cache.LastModifiedDate = fileInfo.LastWriteTimeUtc.Ticks.ToString(); cache.LastModifiedDate = fileInfo.LastWriteTimeUtc.Ticks.ToString();
} }
private string GetHash(string filePath)
{
using SHA1CryptoServiceProvider cryptoProvider = new();
return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "");
}
} }
} }

View File

@@ -1,36 +1,34 @@
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Dalamud.Plugin; using MareSynchronos.Managers;
using Dalamud.Plugin.Ipc;
using MareSynchronos.FileCacheDB;
using MareSynchronos.Models; using MareSynchronos.Models;
namespace MareSynchronos.Factories namespace MareSynchronos.Factories
{ {
public class FileReplacementFactory public class FileReplacementFactory
{ {
private readonly IpcManager ipcManager;
private readonly ClientState clientState; private readonly ClientState clientState;
private ICallGateSubscriber<string, string, string> resolvePath;
private string penumbraDirectory;
public FileReplacementFactory(DalamudPluginInterface pluginInterface, ClientState clientState) public FileReplacementFactory(IpcManager ipcManager, ClientState clientState)
{ {
resolvePath = pluginInterface.GetIpcSubscriber<string, string, string>("Penumbra.ResolveCharacterPath"); this.ipcManager = ipcManager;
penumbraDirectory = pluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc().ToLower() + '\\';
this.clientState = clientState; this.clientState = clientState;
} }
public FileReplacement Create(string gamePath, bool resolve = true) 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; 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) if (!fileReplacement.HasFileReplacement)
{ {
// try to resolve path with --filename instead? // try to resolve path with --filename instead?
string[] tempGamePath = gamePath.Split('/'); string[] tempGamePath = gamePath.Split('/');
tempGamePath[tempGamePath.Length - 1] = "--" + tempGamePath[tempGamePath.Length - 1]; tempGamePath[tempGamePath.Length - 1] = "--" + tempGamePath[tempGamePath.Length - 1];
string newTempGamePath = string.Join('/', tempGamePath); string newTempGamePath = string.Join('/', tempGamePath);
var resolvedPath = resolvePath.InvokeFunc(newTempGamePath, clientState.LocalPlayer!.Name.ToString()); var resolvedPath = ipcManager.PenumbraResolvePath(newTempGamePath, playerName)!;
if (resolvedPath != newTempGamePath) if (resolvedPath != newTempGamePath)
{ {
fileReplacement.SetResolvedPath(resolvedPath); fileReplacement.SetResolvedPath(resolvedPath);

View File

@@ -10,5 +10,6 @@ namespace MareSynchronos.FileCacheDB
public string Hash { get; set; } public string Hash { get; set; }
public string Filepath { get; set; } public string Filepath { get; set; }
public string LastModifiedDate { get; set; } public string LastModifiedDate { get; set; }
public int Version { get; set; }
} }
} }

View File

@@ -37,6 +37,8 @@ namespace MareSynchronos.FileCacheDB
entity.HasKey(e => new { e.Hash, e.Filepath }); entity.HasKey(e => new { e.Hash, e.Filepath });
entity.ToTable("FileCache"); entity.ToTable("FileCache");
entity.Property(c => c.Version).HasDefaultValue(0).IsRowVersion();
}); });
OnModelCreatingPartial(modelBuilder); OnModelCreatingPartial(modelBuilder);

View File

@@ -40,6 +40,8 @@ namespace MareSynchronos.Hooks
[Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")] [Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")]
public Hook<LoadMtrlFilesDelegate>? LoadMtrlTexHook; public Hook<LoadMtrlFilesDelegate>? LoadMtrlTexHook;
public event EventHandler? PlayerLoadEvent;
public Hook<GeneralResolveDelegate>? ResolveMdlPathHook; public Hook<GeneralResolveDelegate>? ResolveMdlPathHook;
public Hook<MaterialResolveDetour>? ResolveMtrlPathHook; public Hook<MaterialResolveDetour>? ResolveMtrlPathHook;
private readonly ClientState clientState; private readonly ClientState clientState;
@@ -51,7 +53,6 @@ namespace MareSynchronos.Hooks
private ConcurrentBag<FileReplacement> cachedResources = new(); private ConcurrentBag<FileReplacement> cachedResources = new();
private GameObject* lastGameObject = null; private GameObject* lastGameObject = null;
private ConcurrentBag<FileReplacement> loadedMaterials = new(); private ConcurrentBag<FileReplacement> loadedMaterials = new();
private CharacterCache characterCache;
public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui) public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui)
{ {
@@ -60,7 +61,6 @@ namespace MareSynchronos.Hooks
this.objectTable = objectTable; this.objectTable = objectTable;
this.factory = factory; this.factory = factory;
this.gameGui = gameGui; this.gameGui = gameGui;
characterCache = new CharacterCache();
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
} }
@@ -85,9 +85,9 @@ namespace MareSynchronos.Hooks
resource.ImcData = string.Empty; 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(); var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject();
for (var idx = 0; idx < model->SlotCount; ++idx) 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 mdlResource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString());
var cachedMdlResource = cachedResources.First(r => r.IsReplacedByThis(mdlResource)); var cachedMdlResource = cachedResources.First(r => r.IsReplacedByThis(mdlResource));
var imc = (ResourceHandle*)model->IMCArray[idx]; var imc = (ResourceHandle*)model->IMCArray[idx];
if (imc != null) if (imc != null)
{ {
@@ -178,7 +179,6 @@ namespace MareSynchronos.Hooks
private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d)
{ {
PluginLog.Debug("Character base detour");
var ret = CharacterBaseCreateHook!.Original(a, b, c, d); var ret = CharacterBaseCreateHook!.Original(a, b, c, d);
if (lastGameObject != null) if (lastGameObject != null)
{ {
@@ -193,7 +193,7 @@ namespace MareSynchronos.Hooks
if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx)) if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx))
{ {
var gameObj = GetGameObjectFromDrawObject(drawBase, 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"); PluginLog.Debug("Clearing resources");
cachedResources.Clear(); cachedResources.Clear();
@@ -347,7 +347,8 @@ namespace MareSynchronos.Hooks
private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle) private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle)
{ {
LoadMtrlHelper(mtrlResourceHandle); if (clientState.LocalPlayer != null)
LoadMtrlHelper(mtrlResourceHandle);
var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle); var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle);
return ret; return ret;
} }
@@ -360,7 +361,7 @@ namespace MareSynchronos.Hooks
private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path) private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path)
{ {
if (path == IntPtr.Zero) if (path == IntPtr.Zero || clientState.LocalPlayer == null)
{ {
return path; return path;
} }
@@ -393,6 +394,8 @@ namespace MareSynchronos.Hooks
return path; return path;
} }
PlayerLoadEvent?.Invoke((IntPtr)gameObject, new EventArgs());
var resource = factory.Create(gamepath.ToString()); var resource = factory.Create(gamepath.ToString());
if (gamepath.ToString().EndsWith("mtrl")) if (gamepath.ToString().EndsWith("mtrl"))

View File

@@ -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<object> penumbraInit;
private readonly ICallGateSubscriber<object> penumbraDispose;
private ICallGateSubscriber<string, string, string>? penumbraResolvePath;
private ICallGateSubscriber<string>? penumbraResolveModDir;
private ICallGateSubscriber<string>? glamourerGetCharacterCustomization;
private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization;
private ICallGateSubscriber<string, int, object>? penumbraRedraw;
public bool Initialized { get; private set; } = false;
public event EventHandler? IpcManagerInitialized;
public IpcManager(DalamudPluginInterface pi)
{
pluginInterface = pi;
penumbraInit = pluginInterface.GetIpcSubscriber<object>("Penumbra.Initialized");
penumbraInit.Subscribe(Initialize);
penumbraDispose = pluginInterface.GetIpcSubscriber<object>("Penumbra.Disposed");
penumbraDispose.Subscribe(Uninitialize);
}
private bool CheckPenumbraAPI()
{
try
{
var penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion").InvokeFunc();
return penumbraApiVersion >= 4;
}
catch
{
return false;
}
}
private bool CheckGlamourerAPI()
{
try
{
var glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("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<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");
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;
}
}
}

View File

@@ -11,41 +11,23 @@ namespace MareSynchronos.Models
[JsonObject(MemberSerialization.OptIn)] [JsonObject(MemberSerialization.OptIn)]
public class CharacterCache public class CharacterCache
{ {
public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
[JsonProperty] [JsonProperty]
public List<FileReplacement> AllReplacements => public List<FileReplacement> AllReplacements =>
FileReplacements.Where(x => x.HasFileReplacement) FileReplacements.Where(x => x.HasFileReplacement)
.Concat(FileReplacements.SelectMany(f => f.Associated).Where(f => f.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)) .Concat(FileReplacements.SelectMany(f => f.Associated).SelectMany(f => f.Associated).Where(f => f.HasFileReplacement))
.Distinct()
.ToList(); .ToList();
public CharacterCache() public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
{
} [JsonProperty]
public string GlamourerString { get; private set; } = string.Empty;
public void Invalidate(List<FileReplacement>? fileReplacements = null) public bool IsReady => FileReplacements.All(f => f.Computed);
{
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);
}
}
[JsonProperty]
public uint JobId { get; set; } = 0;
public void AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent) public void AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent)
{ {
try try
@@ -76,6 +58,31 @@ namespace MareSynchronos.Models
} }
} }
public void Invalidate(List<FileReplacement>? 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() public override string ToString()
{ {
StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new StringBuilder();

View File

@@ -6,6 +6,8 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using MareSynchronos.FileCacheDB; using MareSynchronos.FileCacheDB;
using System.IO;
using MareSynchronos.Utils;
namespace MareSynchronos.Models namespace MareSynchronos.Models
{ {
@@ -24,6 +26,9 @@ namespace MareSynchronos.Models
[JsonProperty] [JsonProperty]
public string ImcData { get; set; } = string.Empty; public string ImcData { get; set; } = string.Empty;
public bool HasFileReplacement => GamePath != ResolvedPath; 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) public FileReplacement(string gamePath, string penumbraDirectory)
{ {
GamePath = gamePath; GamePath = gamePath;
@@ -50,15 +55,58 @@ namespace MareSynchronos.Models
ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/'); ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/');
if (!HasFileReplacement) return; if (!HasFileReplacement) return;
Task.Run(() => computationTask = Task.Run(() =>
{ {
using FileCacheContext db = new FileCacheContext(); FileCache? fileCache;
var fileCache = db.FileCaches.SingleOrDefault(f => f.Filepath == path.ToLower()); using (FileCacheContext db = new())
{
fileCache = db.FileCaches.SingleOrDefault(f => f.Filepath == path.ToLower());
}
if (fileCache != null) 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) public bool IsReplacedByThis(string path)
{ {
return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower(); return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower();
@@ -83,5 +131,16 @@ namespace MareSynchronos.Models
} }
return builder.ToString(); 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);
}
} }
} }

View File

@@ -18,7 +18,6 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Dalamud.Data; using Dalamud.Data;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Glamourer.Customization;
using System.Text; using System.Text;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using System; using System;
@@ -31,106 +30,133 @@ using System.Text.Unicode;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using System.Reflection; using System.Reflection;
using MareSynchronos.Managers;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
namespace SamplePlugin namespace MareSynchronos
{ {
public sealed class Plugin : IDalamudPlugin public sealed class Plugin : IDalamudPlugin
{ {
public string Name => "Mare Synchronos"; public string Name => "Mare Synchronos";
private const string commandName = "/mare"; private const string commandName = "/mare";
private readonly Framework framework;
private readonly ObjectTable objectTable;
private readonly ClientState clientState; private readonly ClientState clientState;
private readonly GameGui gameGui;
private DalamudPluginInterface PluginInterface { get; init; } private DalamudPluginInterface pluginInterface { get; init; }
private CommandManager CommandManager { get; init; } private CommandManager commandManager { get; init; }
private Configuration Configuration { get; init; } private Configuration configuration { get; init; }
private PluginUI PluginUi { get; init; } private PluginUI PluginUi { get; init; }
private FileCacheFactory FileCacheFactory { get; init; }
private DrawHooks drawHooks; private DrawHooks drawHooks;
private IpcManager ipcManager;
private CancellationTokenSource cts; public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager,
private IPlayerWatcher playerWatch; Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui)
public Plugin(
[RequiredVersion("1.0")] DalamudPluginInterface pluginInterface,
[RequiredVersion("1.0")] CommandManager commandManager,
Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager, GameGui gameGui)
{ {
this.PluginInterface = pluginInterface; this.pluginInterface = pluginInterface;
this.CommandManager = commandManager; this.commandManager = commandManager;
this.framework = framework;
this.objectTable = objectTable;
this.clientState = clientState; this.clientState = clientState;
this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); this.gameGui = gameGui;
this.Configuration.Initialize(this.PluginInterface); 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 // 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" 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; clientState.Login += ClientState_Login;
this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; clientState.Logout += ClientState_Logout;
playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable); if (clientState.IsLoggedIn)
drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(pluginInterface, clientState), gameGui); {
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() public void Dispose()
{ {
this.PluginUi.Dispose(); this.PluginUi?.Dispose();
this.CommandManager.RemoveHandler(commandName); this.commandManager.RemoveHandler(commandName);
playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; drawHooks.PlayerLoadEvent -= DrawHooks_PlayerLoadEvent;
playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); clientState.Login -= ClientState_Login;
drawHooks.Dispose(); clientState.Logout -= ClientState_Logout;
ipcManager?.Dispose();
drawHooks?.Dispose();
} }
private void OnCommand(string command, string args) private void OnCommand(string command, string args)
{ {
if (args == "stop")
{
cts?.Cancel();
return;
}
if(args == "playerdata")
{
PluginLog.Debug(PluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization").InvokeFunc());
}
if(args == "applyglam")
{
PluginInterface.GetIpcSubscriber<string, string, object>("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") if (args == "print")
{ {
var resources = drawHooks.PrintRequestedResources(); var resources = drawHooks.PrintRequestedResources();
@@ -139,8 +165,17 @@ namespace SamplePlugin
if (args == "printjson") if (args == "printjson")
{ {
var cache = drawHooks.BuildCharacterCache(); var cache = drawHooks.BuildCharacterCache();
var json = JsonConvert.SerializeObject(cache, Formatting.Indented); cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!);
PluginLog.Debug(json); 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") if (args == "createtestmod")
@@ -149,7 +184,7 @@ namespace SamplePlugin
{ {
var playerName = clientState.LocalPlayer!.Name.ToString(); var playerName = clientState.LocalPlayer!.Name.ToString();
var modName = $"Mare Synchronos Test Mod {playerName}"; var modName = $"Mare Synchronos Test Mod {playerName}";
var modDirectory = PluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc(); var modDirectory = ipcManager.PenumbraModDirectory()!;
string modDirectoryPath = Path.Combine(modDirectory, modName); string modDirectoryPath = Path.Combine(modDirectory, modName);
if (Directory.Exists(modDirectoryPath)) if (Directory.Exists(modDirectoryPath))
{ {
@@ -216,100 +251,14 @@ namespace SamplePlugin
private void PlayerWatch_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) private void PlayerWatch_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor)
{ {
var equipment = playerWatch.UpdatePlayerWithoutEvent(actor); //var equipment = playerWatch.UpdatePlayerWithoutEvent(actor);
var customization = new CharacterCustomization(actor); //var customization = new CharacterCustomization(actor);
//DebugCustomization(customization); //DebugCustomization(customization);
//PluginLog.Debug(customization.Gender.ToString()); //PluginLog.Debug(customization.Gender.ToString());
if (equipment != null) //if (equipment != null)
{ //{
PluginLog.Debug(equipment.ToString()); // PluginLog.Debug(equipment.ToString());
} //}
}
private void StartScan()
{
Stopwatch st = Stopwatch.StartNew();
string penumbraDir = PluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc();
PluginLog.Debug("Getting files from " + penumbraDir);
ConcurrentDictionary<string, bool> charaFiles = new ConcurrentDictionary<string, bool>(
Directory.GetFiles(penumbraDir, "*.*", SearchOption.AllDirectories)
.Select(s => s.ToLowerInvariant())
.Where(f => !f.EndsWith(".json"))
.Where(f => f.Contains(@"\chara\"))
.Select(p => new KeyValuePair<string, bool>(p, false)));
int count = 0;
using FileCacheContext db = new();
var fileCaches = db.FileCaches.ToList();
var fileCachesToUpdate = new ConcurrentBag<FileCache>();
var fileCachesToDelete = new ConcurrentBag<FileCache>();
var fileCachesToAdd = new ConcurrentBag<FileCache>();
// 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();
} }
private void DrawUI() private void DrawUI()

View File

@@ -2,7 +2,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
namespace SamplePlugin namespace MareSynchronos
{ {
// It is good to have this be disposable in general, in case you ever need it // It is good to have this be disposable in general, in case you ever need it
// to do any cleanup // to do any cleanup

View File

@@ -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("-", "");
}
}
}