I should actually be working
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
using Dalamud.Game.ClientState;
|
using Dalamud.Game.ClientState;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.FileCacheDB;
|
||||||
|
using MareSynchronos.Models;
|
||||||
|
|
||||||
namespace MareSynchronos.Models
|
namespace MareSynchronos.Factories
|
||||||
{
|
{
|
||||||
public class FileReplacementFactory
|
public class FileReplacementFactory
|
||||||
{
|
{
|
||||||
@@ -16,20 +18,22 @@ namespace MareSynchronos.Models
|
|||||||
penumbraDirectory = pluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc().ToLower() + '\\';
|
penumbraDirectory = pluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc().ToLower() + '\\';
|
||||||
this.clientState = clientState;
|
this.clientState = clientState;
|
||||||
}
|
}
|
||||||
public FileReplacement Create(string gamePath)
|
public FileReplacement Create(string gamePath, bool resolve = true)
|
||||||
{
|
{
|
||||||
var fileReplacement = new FileReplacement(gamePath, penumbraDirectory);
|
var fileReplacement = new FileReplacement(gamePath, penumbraDirectory);
|
||||||
fileReplacement.SetReplacedPath(resolvePath.InvokeFunc(gamePath, clientState.LocalPlayer!.Name.ToString()));
|
if (!resolve) return fileReplacement;
|
||||||
|
|
||||||
|
fileReplacement.SetResolvedPath(resolvePath.InvokeFunc(gamePath, clientState.LocalPlayer!.Name.ToString()));
|
||||||
if (!fileReplacement.HasFileReplacement)
|
if (!fileReplacement.HasFileReplacement)
|
||||||
{
|
{
|
||||||
// try to resolve path with -- 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 = resolvePath.InvokeFunc(newTempGamePath, clientState.LocalPlayer!.Name.ToString());
|
||||||
if (resolvedPath != newTempGamePath)
|
if (resolvedPath != newTempGamePath)
|
||||||
{
|
{
|
||||||
fileReplacement.SetReplacedPath(resolvedPath);
|
fileReplacement.SetResolvedPath(resolvedPath);
|
||||||
fileReplacement.SetGamePath(newTempGamePath);
|
fileReplacement.SetGamePath(newTempGamePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
using Dalamud.Game.ClientState;
|
using Dalamud.Game.ClientState;
|
||||||
using Dalamud.Game.ClientState.Objects;
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
using Dalamud.Game.Gui;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using MareSynchronos.Factories;
|
||||||
using MareSynchronos.Models;
|
using MareSynchronos.Models;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
@@ -17,76 +20,143 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MareSynchronos.Hooks
|
namespace MareSynchronos.Hooks
|
||||||
{
|
{
|
||||||
public unsafe class DrawHooks : IDisposable
|
public unsafe class DrawHooks : IDisposable
|
||||||
{
|
{
|
||||||
|
public const int ResolveMdlIdx = 73;
|
||||||
|
public const int ResolveMtrlIdx = 82;
|
||||||
|
|
||||||
|
[Signature("E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour")]
|
||||||
|
public Hook<CharacterBaseCreateDelegate>? CharacterBaseCreateHook;
|
||||||
|
[Signature("E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30",
|
||||||
|
DetourName = "CharacterBaseDestructorDetour")]
|
||||||
|
public Hook<CharacterBaseDestructorDelegate>? CharacterBaseDestructorHook;
|
||||||
[Signature("48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress)]
|
[Signature("48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress)]
|
||||||
public IntPtr* DrawObjectHumanVTable;
|
public IntPtr* DrawObjectHumanVTable;
|
||||||
|
[Signature("E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0")]
|
||||||
|
public Hook<EnableDrawDelegate>? EnableDrawHook;
|
||||||
|
[Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")]
|
||||||
|
public Hook<LoadMtrlFilesDelegate>? LoadMtrlTexHook;
|
||||||
|
|
||||||
// [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )]
|
|
||||||
// public IntPtr* DrawObjectVTable;
|
|
||||||
//
|
|
||||||
// [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )]
|
|
||||||
// public IntPtr* DrawObjectDemihumanVTable;
|
|
||||||
//
|
|
||||||
// [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )]
|
|
||||||
// public IntPtr* DrawObjectMonsterVTable;
|
|
||||||
//
|
|
||||||
// public const int ResolveRootIdx = 71;
|
|
||||||
|
|
||||||
public const int ResolveSklbIdx = 72;
|
|
||||||
public const int ResolveMdlIdx = 73;
|
|
||||||
public const int ResolveSkpIdx = 74;
|
|
||||||
public const int ResolvePhybIdx = 75;
|
|
||||||
public const int ResolvePapIdx = 76;
|
|
||||||
public const int ResolveTmbIdx = 77;
|
|
||||||
public const int ResolveMPapIdx = 79;
|
|
||||||
public const int ResolveImcIdx = 81;
|
|
||||||
public const int ResolveMtrlIdx = 82;
|
|
||||||
public const int ResolveDecalIdx = 83;
|
|
||||||
public const int ResolveVfxIdx = 84;
|
|
||||||
public const int ResolveEidIdx = 85;
|
|
||||||
private readonly DalamudPluginInterface pluginInterface;
|
|
||||||
private readonly ClientState clientState;
|
|
||||||
private readonly ObjectTable objectTable;
|
|
||||||
private readonly FileReplacementFactory factory;
|
|
||||||
|
|
||||||
public delegate IntPtr GeneralResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4);
|
|
||||||
public delegate IntPtr MPapResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5);
|
|
||||||
public delegate IntPtr MaterialResolveDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5);
|
|
||||||
public delegate IntPtr EidResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3);
|
|
||||||
|
|
||||||
public delegate void OnModelLoadCompleteDelegate(IntPtr drawObject);
|
|
||||||
|
|
||||||
public Hook<GeneralResolveDelegate>? ResolveDecalPathHook;
|
|
||||||
public Hook<EidResolveDelegate>? ResolveEidPathHook;
|
|
||||||
public Hook<GeneralResolveDelegate>? ResolveImcPathHook;
|
|
||||||
public Hook<MPapResolveDelegate>? ResolveMPapPathHook;
|
|
||||||
public Hook<GeneralResolveDelegate>? ResolveMdlPathHook;
|
public Hook<GeneralResolveDelegate>? ResolveMdlPathHook;
|
||||||
public Hook<MaterialResolveDetour>? ResolveMtrlPathHook;
|
public Hook<MaterialResolveDetour>? ResolveMtrlPathHook;
|
||||||
public Hook<MaterialResolveDetour>? ResolvePapPathHook;
|
private readonly ClientState clientState;
|
||||||
public Hook<GeneralResolveDelegate>? ResolvePhybPathHook;
|
private readonly Dictionary<IntPtr, ushort> DrawObjectToObject = new();
|
||||||
public Hook<GeneralResolveDelegate>? ResolveSklbPathHook;
|
private readonly FileReplacementFactory factory;
|
||||||
public Hook<GeneralResolveDelegate>? ResolveSkpPathHook;
|
private readonly GameGui gameGui;
|
||||||
public Hook<EidResolveDelegate>? ResolveTmbPathHook;
|
private readonly ObjectTable objectTable;
|
||||||
public Hook<MaterialResolveDetour>? ResolveVfxPathHook;
|
private readonly DalamudPluginInterface pluginInterface;
|
||||||
|
private ConcurrentBag<FileReplacement> cachedResources = new();
|
||||||
|
private GameObject* lastGameObject = null;
|
||||||
|
private ConcurrentBag<FileReplacement> loadedMaterials = new();
|
||||||
|
private CharacterCache characterCache;
|
||||||
|
|
||||||
public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory)
|
public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui)
|
||||||
{
|
{
|
||||||
this.pluginInterface = pluginInterface;
|
this.pluginInterface = pluginInterface;
|
||||||
this.clientState = clientState;
|
this.clientState = clientState;
|
||||||
this.objectTable = objectTable;
|
this.objectTable = objectTable;
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
|
this.gameGui = gameGui;
|
||||||
|
characterCache = new CharacterCache();
|
||||||
SignatureHelper.Initialise(this);
|
SignatureHelper.Initialise(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d);
|
||||||
|
public delegate void CharacterBaseDestructorDelegate(IntPtr drawBase);
|
||||||
|
public delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d);
|
||||||
|
public delegate IntPtr GeneralResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4);
|
||||||
|
public delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle);
|
||||||
|
public delegate IntPtr MaterialResolveDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5);
|
||||||
|
public delegate void OnModelLoadCompleteDelegate(IntPtr drawObject);
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisableHumanHooks();
|
||||||
|
DisposeHumanHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharacterCache BuildCharacterCache()
|
||||||
|
{
|
||||||
|
foreach (var resource in cachedResources)
|
||||||
|
{
|
||||||
|
resource.IsInUse = false;
|
||||||
|
resource.ImcData = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache = new CharacterCache();
|
||||||
|
|
||||||
|
PluginLog.Debug("Invaldated character cache");
|
||||||
|
|
||||||
|
var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject();
|
||||||
|
for (var idx = 0; idx < model->SlotCount; ++idx)
|
||||||
|
{
|
||||||
|
var mdl = (RenderModel*)model->ModelArray[idx];
|
||||||
|
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
byte[] imcData = new byte[imc->Data->DataLength];
|
||||||
|
Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength);
|
||||||
|
string imcDataStr = BitConverter.ToString(imcData).Replace("-", "");
|
||||||
|
cachedMdlResource.ImcData = imcDataStr;
|
||||||
|
}
|
||||||
|
cache.AddAssociatedResource(cachedMdlResource, null!, null!);
|
||||||
|
|
||||||
|
for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
|
||||||
|
{
|
||||||
|
var mtrl = (Material*)mdl->Materials[mtrlIdx];
|
||||||
|
if (mtrl == null) continue;
|
||||||
|
|
||||||
|
var mtrlFileResource = factory.Create(new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]);
|
||||||
|
var cachedMtrlResource = cachedResources.First(r => r.IsReplacedByThis(mtrlFileResource));
|
||||||
|
cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!);
|
||||||
|
|
||||||
|
var mtrlResource = (MtrlResource*)mtrl->ResourceHandle;
|
||||||
|
for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++)
|
||||||
|
{
|
||||||
|
var texPath = new Utf8String(mtrlResource->TexString(resIdx));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(texPath.ToString())) continue;
|
||||||
|
|
||||||
|
var texResource = factory.Create(texPath.ToString());
|
||||||
|
var cachedTexResource = cachedResources.First(r => r.IsReplacedByThis(texResource));
|
||||||
|
cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileReplacement> PrintRequestedResources()
|
||||||
|
{
|
||||||
|
var cache = BuildCharacterCache();
|
||||||
|
|
||||||
|
PluginLog.Debug("--- CURRENTLY LOADED FILES ---");
|
||||||
|
|
||||||
|
PluginLog.Debug(cache.ToString());
|
||||||
|
|
||||||
|
PluginLog.Debug("--- LOOSE FILES ---");
|
||||||
|
|
||||||
|
foreach (var resource in cachedResources.Where(r => !r.IsInUse).OrderBy(a => a.GamePath))
|
||||||
|
{
|
||||||
|
PluginLog.Debug(resource.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.FileReplacements;
|
||||||
|
}
|
||||||
|
|
||||||
public void StartHooks()
|
public void StartHooks()
|
||||||
{
|
{
|
||||||
allRequestedResources.Clear();
|
cachedResources.Clear();
|
||||||
SetupHumanHooks();
|
SetupHumanHooks();
|
||||||
EnableHumanHooks();
|
EnableHumanHooks();
|
||||||
PluginLog.Debug("Hooks enabled");
|
PluginLog.Debug("Hooks enabled");
|
||||||
@@ -98,153 +168,151 @@ namespace MareSynchronos.Hooks
|
|||||||
DisposeHumanHooks();
|
DisposeHumanHooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupHumanHooks()
|
private void AddRequestedResource(FileReplacement replacement)
|
||||||
{
|
{
|
||||||
if (ResolveDecalPathHook != null) return;
|
if (!cachedResources.Any(a => a.IsReplacedByThis(replacement)))
|
||||||
|
{
|
||||||
ResolveDecalPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveDecalIdx], ResolveDecalDetour);
|
cachedResources.Add(replacement);
|
||||||
ResolveEidPathHook = new Hook<EidResolveDelegate>(DrawObjectHumanVTable[ResolveEidIdx], ResolveEidDetour);
|
}
|
||||||
ResolveImcPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveImcIdx], ResolveImcDetour);
|
|
||||||
ResolveMPapPathHook = new Hook<MPapResolveDelegate>(DrawObjectHumanVTable[ResolveMPapIdx], ResolveMPapDetour);
|
|
||||||
ResolveMdlPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour);
|
|
||||||
ResolveMtrlPathHook = new Hook<MaterialResolveDetour>(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour);
|
|
||||||
ResolvePapPathHook = new Hook<MaterialResolveDetour>(DrawObjectHumanVTable[ResolvePapIdx], ResolvePapDetour);
|
|
||||||
ResolvePhybPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolvePhybIdx], ResolvePhybDetour);
|
|
||||||
ResolveSklbPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveSklbIdx], ResolveSklbDetour);
|
|
||||||
ResolveSkpPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveSkpIdx], ResolveSkpDetour);
|
|
||||||
ResolveTmbPathHook = new Hook<EidResolveDelegate>(DrawObjectHumanVTable[ResolveTmbIdx], ResolveTmbDetour);
|
|
||||||
ResolveVfxPathHook = new Hook<MaterialResolveDetour>(DrawObjectHumanVTable[ResolveVfxIdx], ResolveVfxDetour);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnableHumanHooks()
|
private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d)
|
||||||
{
|
{
|
||||||
if (ResolveDecalPathHook?.IsEnabled ?? false) return;
|
PluginLog.Debug("Character base detour");
|
||||||
|
var ret = CharacterBaseCreateHook!.Original(a, b, c, d);
|
||||||
|
if (lastGameObject != null)
|
||||||
|
{
|
||||||
|
DrawObjectToObject[ret] = (lastGameObject->ObjectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
ResolveDecalPathHook?.Enable();
|
return ret;
|
||||||
//ResolveEidPathHook?.Enable();
|
}
|
||||||
//ResolveImcPathHook?.Enable();
|
|
||||||
//ResolveMPapPathHook?.Enable();
|
private void CharacterBaseDestructorDetour(IntPtr drawBase)
|
||||||
ResolveMdlPathHook?.Enable();
|
{
|
||||||
ResolveMtrlPathHook?.Enable();
|
if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx))
|
||||||
//ResolvePapPathHook?.Enable();
|
{
|
||||||
//ResolvePhybPathHook?.Enable();
|
var gameObj = GetGameObjectFromDrawObject(drawBase, idx);
|
||||||
//ResolveSklbPathHook?.Enable();
|
if (gameObj == (GameObject*)clientState.LocalPlayer!.Address)
|
||||||
//ResolveSkpPathHook?.Enable();
|
{
|
||||||
//ResolveTmbPathHook?.Enable();
|
PluginLog.Debug("Clearing resources");
|
||||||
//ResolveVfxPathHook?.Enable();
|
cachedResources.Clear();
|
||||||
EnableDrawHook?.Enable();
|
DrawObjectToObject.Clear();
|
||||||
LoadMtrlTexHook?.Enable();
|
}
|
||||||
|
}
|
||||||
|
CharacterBaseDestructorHook!.Original.Invoke(drawBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisableHumanHooks()
|
private void DisableHumanHooks()
|
||||||
{
|
{
|
||||||
ResolveDecalPathHook?.Disable();
|
ResolveMdlPathHook?.Disable();
|
||||||
//ResolveEidPathHook?.Disable();
|
|
||||||
//ResolveImcPathHook?.Disable();
|
|
||||||
//ResolveMPapPathHook?.Disable();
|
|
||||||
ResolveMdlPathHook?.Disable();
|
ResolveMdlPathHook?.Disable();
|
||||||
ResolveMtrlPathHook?.Disable();
|
ResolveMtrlPathHook?.Disable();
|
||||||
//ResolvePapPathHook?.Disable();
|
|
||||||
//ResolvePhybPathHook?.Disable();
|
|
||||||
//ResolveSklbPathHook?.Disable();
|
|
||||||
//ResolveSkpPathHook?.Disable();
|
|
||||||
//ResolveTmbPathHook?.Disable();
|
|
||||||
//ResolveVfxPathHook?.Disable();
|
|
||||||
EnableDrawHook?.Disable();
|
EnableDrawHook?.Disable();
|
||||||
LoadMtrlTexHook?.Disable();
|
LoadMtrlTexHook?.Disable();
|
||||||
|
CharacterBaseCreateHook?.Disable();
|
||||||
|
CharacterBaseDestructorHook?.Disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeHumanHooks()
|
private void DisposeHumanHooks()
|
||||||
{
|
{
|
||||||
ResolveDecalPathHook?.Dispose();
|
|
||||||
//ResolveEidPathHook?.Dispose();
|
|
||||||
//ResolveImcPathHook?.Dispose();
|
|
||||||
//ResolveMPapPathHook?.Dispose();
|
|
||||||
ResolveMdlPathHook?.Dispose();
|
ResolveMdlPathHook?.Dispose();
|
||||||
ResolveMtrlPathHook?.Dispose();
|
ResolveMtrlPathHook?.Dispose();
|
||||||
//ResolvePapPathHook?.Dispose();
|
|
||||||
//ResolvePhybPathHook?.Dispose();
|
|
||||||
//ResolveSklbPathHook?.Dispose();
|
|
||||||
//ResolveSkpPathHook?.Dispose();
|
|
||||||
//ResolveTmbPathHook?.Dispose();
|
|
||||||
//ResolveVfxPathHook?.Dispose();
|
|
||||||
EnableDrawHook?.Dispose();
|
EnableDrawHook?.Dispose();
|
||||||
LoadMtrlTexHook?.Dispose();
|
LoadMtrlTexHook?.Dispose();
|
||||||
|
CharacterBaseCreateHook?.Dispose();
|
||||||
|
CharacterBaseDestructorHook?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Humans
|
|
||||||
private IntPtr ResolveDecalDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveDecalPathHook!.Original(drawObject, path, unk3, unk4));
|
|
||||||
|
|
||||||
private IntPtr ResolveEidDetour(IntPtr drawObject, IntPtr path, IntPtr unk3)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveEidPathHook!.Original(drawObject, path, unk3));
|
|
||||||
|
|
||||||
private IntPtr ResolveImcDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveImcPathHook!.Original(drawObject, path, unk3, unk4));
|
|
||||||
|
|
||||||
private IntPtr ResolveMPapDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveMPapPathHook!.Original(drawObject, path, unk3, unk4, unk5));
|
|
||||||
|
|
||||||
private IntPtr ResolveMdlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType)
|
|
||||||
{
|
|
||||||
return ResolvePathDetour(drawObject, ResolveMdlPathHook!.Original(drawObject, path, unk3, modelType));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr ResolveMtrlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveMtrlPathHook!.Original(drawObject, path, unk3, unk4, unk5));
|
|
||||||
|
|
||||||
private IntPtr ResolvePapDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5)
|
|
||||||
{
|
|
||||||
return ResolvePathDetour(drawObject, ResolvePapPathHook!.Original(drawObject, path, unk3, unk4, unk5));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr ResolvePhybDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4)
|
|
||||||
{
|
|
||||||
return ResolvePathDetour(drawObject, ResolvePhybPathHook!.Original(drawObject, path, unk3, unk4));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr ResolveSklbDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4)
|
|
||||||
{
|
|
||||||
return ResolvePathDetour(drawObject, ResolveSklbPathHook!.Original(drawObject, path, unk3, unk4));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr ResolveSkpDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4)
|
|
||||||
{
|
|
||||||
return ResolvePathDetour(drawObject, ResolveSkpPathHook!.Original(drawObject, path, unk3, unk4));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr ResolveTmbDetour(IntPtr drawObject, IntPtr path, IntPtr unk3)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveTmbPathHook!.Original(drawObject, path, unk3));
|
|
||||||
|
|
||||||
private IntPtr ResolveVfxDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5)
|
|
||||||
=> ResolvePathDetour(drawObject, ResolveVfxPathHook!.Original(drawObject, path, unk3, unk4, unk5));
|
|
||||||
|
|
||||||
public delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d);
|
|
||||||
|
|
||||||
[Signature("E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0")]
|
|
||||||
public Hook<EnableDrawDelegate>? EnableDrawHook;
|
|
||||||
|
|
||||||
public GameObject* LastGameObject { get; private set; }
|
|
||||||
|
|
||||||
private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d)
|
private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d)
|
||||||
{
|
{
|
||||||
//PluginLog.Debug("Draw start");
|
var oldObject = lastGameObject;
|
||||||
var oldObject = LastGameObject;
|
lastGameObject = (GameObject*)gameObject;
|
||||||
LastGameObject = (GameObject*)gameObject;
|
|
||||||
EnableDrawHook!.Original.Invoke(gameObject, b, c, d);
|
EnableDrawHook!.Original.Invoke(gameObject, b, c, d);
|
||||||
LastGameObject = oldObject;
|
lastGameObject = oldObject;
|
||||||
//PluginLog.Debug("Draw end");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle);
|
private void EnableHumanHooks()
|
||||||
[Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")]
|
|
||||||
public Hook<LoadMtrlFilesDelegate>? LoadMtrlTexHook;
|
|
||||||
|
|
||||||
private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle)
|
|
||||||
{
|
{
|
||||||
LoadMtrlHelper(mtrlResourceHandle);
|
if (ResolveMdlPathHook?.IsEnabled ?? false) return;
|
||||||
var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle);
|
|
||||||
return ret;
|
ResolveMdlPathHook?.Enable();
|
||||||
|
ResolveMtrlPathHook?.Enable();
|
||||||
|
EnableDrawHook?.Enable();
|
||||||
|
LoadMtrlTexHook?.Enable();
|
||||||
|
CharacterBaseCreateHook?.Enable();
|
||||||
|
CharacterBaseDestructorHook?.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetCardName()
|
||||||
|
{
|
||||||
|
var uiModule = (UIModule*)gameGui.GetUIModule();
|
||||||
|
var agentModule = uiModule->GetAgentModule();
|
||||||
|
var agent = (byte*)agentModule->GetAgentByInternalID(393);
|
||||||
|
if (agent == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = *(byte**)(agent + 0x28);
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var block = data + 0x7A;
|
||||||
|
return new Utf8String(block).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameObject* GetGameObjectFromDrawObject(IntPtr drawObject, int gameObjectIdx)
|
||||||
|
{
|
||||||
|
var tmp = objectTable[gameObjectIdx];
|
||||||
|
GameObject* gameObject;
|
||||||
|
if (tmp != null)
|
||||||
|
{
|
||||||
|
gameObject = (GameObject*)tmp.Address;
|
||||||
|
if (gameObject->DrawObject == (DrawObject*)drawObject)
|
||||||
|
{
|
||||||
|
return gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawObjectToObject.Remove(drawObject);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetGlamourName()
|
||||||
|
{
|
||||||
|
var addon = gameGui.GetAddonByName("MiragePrismMiragePlate", 1);
|
||||||
|
return addon == IntPtr.Zero ? null : GetPlayerName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetInspectName()
|
||||||
|
{
|
||||||
|
var addon = gameGui.GetAddonByName("CharacterInspect", 1);
|
||||||
|
if (addon == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ui = (AtkUnitBase*)addon;
|
||||||
|
if (ui->UldManager.NodeListCount < 60)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = (AtkTextNode*)ui->UldManager.NodeList[59];
|
||||||
|
if (text == null || !text->AtkResNode.IsVisible)
|
||||||
|
{
|
||||||
|
text = (AtkTextNode*)ui->UldManager.NodeList[60];
|
||||||
|
}
|
||||||
|
|
||||||
|
return text != null ? text->NodeText.ToString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPlayerName()
|
||||||
|
{
|
||||||
|
return clientState.LocalPlayer!.Name.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadMtrlHelper(IntPtr mtrlResourceHandle)
|
private void LoadMtrlHelper(IntPtr mtrlResourceHandle)
|
||||||
@@ -268,7 +336,7 @@ namespace MareSynchronos.Hooks
|
|||||||
AddRequestedResource(factory.Create(texPath.ToString()));
|
AddRequestedResource(factory.Create(texPath.ToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedMaterials.Remove(existingMat);
|
loadedMaterials = new(loadedMaterials.Except(new[] { existingMat }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -277,6 +345,19 @@ namespace MareSynchronos.Hooks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle)
|
||||||
|
{
|
||||||
|
LoadMtrlHelper(mtrlResourceHandle);
|
||||||
|
var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr ResolveMdlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType)
|
||||||
|
=> ResolvePathDetour(drawObject, ResolveMdlPathHook!.Original(drawObject, path, unk3, modelType));
|
||||||
|
|
||||||
|
private IntPtr ResolveMtrlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5)
|
||||||
|
=> ResolvePathDetour(drawObject, ResolveMtrlPathHook!.Original(drawObject, path, unk3, unk4, unk5));
|
||||||
|
|
||||||
private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path)
|
private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path)
|
||||||
{
|
{
|
||||||
if (path == IntPtr.Zero)
|
if (path == IntPtr.Zero)
|
||||||
@@ -286,23 +367,34 @@ namespace MareSynchronos.Hooks
|
|||||||
|
|
||||||
var gamepath = new Utf8String((byte*)path);
|
var gamepath = new Utf8String((byte*)path);
|
||||||
|
|
||||||
var playerName = clientState.LocalPlayer.Name.ToString();
|
var playerName = GetPlayerName();
|
||||||
var gameDrawObject = (DrawObject*)drawObject;
|
var gameDrawObject = (DrawObject*)drawObject;
|
||||||
var playerDrawObject = ((Character*)clientState.LocalPlayer.Address)->GameObject.GetDrawObject();
|
GameObject* gameObject = lastGameObject;
|
||||||
|
|
||||||
if (LastGameObject != null && (LastGameObject->DrawObject == null || LastGameObject->DrawObject == gameDrawObject))
|
if (DrawObjectToObject.TryGetValue(drawObject, out ushort idx))
|
||||||
{
|
{
|
||||||
var owner = new Utf8String(LastGameObject->Name).ToString();
|
gameObject = GetGameObjectFromDrawObject(drawObject, DrawObjectToObject[drawObject]);
|
||||||
if (owner != playerName)
|
}
|
||||||
|
|
||||||
|
if (gameObject != null && (gameObject->DrawObject == null || gameObject->DrawObject == gameDrawObject))
|
||||||
|
{
|
||||||
|
// 240, 241, 242 and 243 might need Penumbra config readout
|
||||||
|
var actualName = gameObject->ObjectIndex switch
|
||||||
|
{
|
||||||
|
240 => GetPlayerName(), // character window
|
||||||
|
241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor.
|
||||||
|
242 => GetPlayerName(), // try-on
|
||||||
|
243 => GetPlayerName(), // dye preview
|
||||||
|
_ => null,
|
||||||
|
} ?? new Utf8String(gameObject->Name).ToString();
|
||||||
|
|
||||||
|
if (actualName != playerName)
|
||||||
{
|
{
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddRequestedResource(factory.Create(gamepath.ToString()));
|
|
||||||
}
|
|
||||||
else if (playerDrawObject == gameDrawObject)
|
|
||||||
{
|
|
||||||
var resource = factory.Create(gamepath.ToString());
|
var resource = factory.Create(gamepath.ToString());
|
||||||
|
|
||||||
if (gamepath.ToString().EndsWith("mtrl"))
|
if (gamepath.ToString().EndsWith("mtrl"))
|
||||||
{
|
{
|
||||||
loadedMaterials.Add(resource);
|
loadedMaterials.Add(resource);
|
||||||
@@ -314,140 +406,12 @@ namespace MareSynchronos.Hooks
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FileReplacement> loadedMaterials = new();
|
private void SetupHumanHooks()
|
||||||
ConcurrentBag<FileReplacement> allRequestedResources = new();
|
|
||||||
|
|
||||||
public List<FileReplacement> PrintRequestedResources()
|
|
||||||
{
|
{
|
||||||
foreach (var resource in allRequestedResources)
|
if (ResolveMdlPathHook != null) return;
|
||||||
{
|
|
||||||
PluginLog.Debug(resource.ToString());
|
|
||||||
}
|
|
||||||
//PluginLog.Debug("---");
|
|
||||||
|
|
||||||
var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject();
|
ResolveMdlPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour);
|
||||||
|
ResolveMtrlPathHook = new Hook<MaterialResolveDetour>(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour);
|
||||||
List<FileReplacement> fluctuatingResources = new();
|
|
||||||
|
|
||||||
for (var i = 0; i < model->SlotCount; ++i)
|
|
||||||
{
|
|
||||||
var mdl = (RenderModel*)model->ModelArray[i];
|
|
||||||
|
|
||||||
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString());
|
|
||||||
|
|
||||||
PluginLog.Debug("Checking model: " + resource);
|
|
||||||
|
|
||||||
var mdlResourceRepl = allRequestedResources.FirstOrDefault(r => r.IsReplacedByThis(resource));
|
|
||||||
|
|
||||||
if (mdlResourceRepl != null)
|
|
||||||
{
|
|
||||||
//PluginLog.Debug("Fluctuating resource detected: " + mdlResourceRepl);
|
|
||||||
//allRequestedResources.Remove(mdlResourceRepl);
|
|
||||||
fluctuatingResources.Add(mdlResourceRepl);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//var resolvedPath = ResolvePath(mdlFile);
|
|
||||||
//if (resolvedPath != mdlFile)
|
|
||||||
//{
|
|
||||||
//fluctuatingResources[mdlFile] = resolvedPath;
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
|
|
||||||
{
|
|
||||||
var mtrl = (Material*)mdl->Materials[mtrlIdx];
|
|
||||||
|
|
||||||
if (mtrl == null) continue;
|
|
||||||
|
|
||||||
var mtrlresource = factory.Create(new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]);
|
|
||||||
|
|
||||||
var mtrlResourceRepl = allRequestedResources.FirstOrDefault(r => r.IsReplacedByThis(mtrlresource));
|
|
||||||
if (mtrlResourceRepl != null)
|
|
||||||
{
|
|
||||||
mdlResourceRepl.AddAssociated(mtrlResourceRepl);
|
|
||||||
//PluginLog.Debug("Fluctuating resource detected: " + mtrlResourceRepl);
|
|
||||||
//allRequestedResources.Remove(mtrlResourceRepl);
|
|
||||||
//fluctuatingResources.Add(mtrlResourceRepl);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//var resolvedPath = ResolvePath(mtrlPath);
|
|
||||||
//if (resolvedPath != mtrlPath)
|
|
||||||
//{
|
|
||||||
// fluctuatingResources[mtrlPath] = resolvedPath;
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mtrlResource = (MtrlResource*)mtrl->ResourceHandle;
|
|
||||||
|
|
||||||
for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++)
|
|
||||||
{
|
|
||||||
var path = new Utf8String(mtrlResource->TexString(resIdx));
|
|
||||||
var gamePath = Utf8GamePath.FromString(path.ToString(), out var p, true) ? p : Utf8GamePath.Empty;
|
|
||||||
|
|
||||||
var texResource = factory.Create(path.ToString());
|
|
||||||
|
|
||||||
var texResourceRepl = allRequestedResources.FirstOrDefault(r => r.IsReplacedByThis(texResource));
|
|
||||||
if (texResourceRepl != null)
|
|
||||||
{
|
|
||||||
//PluginLog.Debug("Fluctuating resource detected: " + texResourceRepl);
|
|
||||||
//allRequestedResources.Remove(texResourceRepl);
|
|
||||||
//fluctuatingResources.Add(texResourceRepl);
|
|
||||||
mtrlResourceRepl.AddAssociated(texResourceRepl);
|
|
||||||
//fluctuatingResources[existingResource.Key] = existingResource.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//var resolvedPath = ResolvePath(path.ToString());
|
|
||||||
//if (resolvedPath != path.ToString())
|
|
||||||
//{
|
|
||||||
// fluctuatingResources[path.ToString()] = resolvedPath;
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Debug("---");
|
|
||||||
|
|
||||||
foreach (var resource in fluctuatingResources.OrderBy(a => a.GamePath))
|
|
||||||
{
|
|
||||||
PluginLog.Debug(Environment.NewLine + resource.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Debug("---");
|
|
||||||
|
|
||||||
/*foreach (var resource in allRequestedResources.Where(r => r.HasFileReplacement && r.Associated.Count == 0).OrderBy(a => a.GamePath))
|
|
||||||
{
|
|
||||||
PluginLog.Debug(resource.ToString());
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return fluctuatingResources;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddRequestedResource(FileReplacement replacement)
|
|
||||||
{
|
|
||||||
if (allRequestedResources.Any(a => a.IsReplacedByThis(replacement)))
|
|
||||||
{
|
|
||||||
PluginLog.Debug("Already added: " + replacement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Debug("Adding: " + replacement.GamePath);
|
|
||||||
|
|
||||||
allRequestedResources.Add(replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DisableHumanHooks();
|
|
||||||
DisposeHumanHooks();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.17" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.17" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="2.0.0-preview1-final" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="2.0.0-preview1-final" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -71,12 +72,6 @@
|
|||||||
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
|
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
|
||||||
<Private>false</Private>
|
<Private>false</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Penumbra.GameData">
|
|
||||||
<HintPath>..\..\..\..\..\AppData\Roaming\XIVLauncher\installedPlugins\Penumbra\0.5.0.5\Penumbra.GameData.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Penumbra.PlayerWatch">
|
|
||||||
<HintPath>..\..\..\..\..\AppData\Roaming\XIVLauncher\installedPlugins\Penumbra\0.5.0.5\Penumbra.PlayerWatch.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
89
MareSynchronos/Models/CharacterCache.cs
Normal file
89
MareSynchronos/Models/CharacterCache.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Dalamud.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Models
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
|
public class CharacterCache
|
||||||
|
{
|
||||||
|
public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public List<FileReplacement> 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))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public CharacterCache()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (resource == null) return;
|
||||||
|
if (mdlParent == null)
|
||||||
|
{
|
||||||
|
resource.IsInUse = true;
|
||||||
|
FileReplacements.Add(resource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileReplacement replacement;
|
||||||
|
|
||||||
|
if (mtrlParent == null && (replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)!) != null)
|
||||||
|
{
|
||||||
|
replacement.AddAssociated(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)?.Associated.SingleOrDefault(f => f == mtrlParent)!) != null)
|
||||||
|
{
|
||||||
|
replacement.AddAssociated(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PluginLog.Debug(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePath))
|
||||||
|
{
|
||||||
|
stringBuilder.AppendLine(fileReplacement.ToString());
|
||||||
|
}
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
using System;
|
using Dalamud.Logging;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using MareSynchronos.FileCacheDB;
|
||||||
|
|
||||||
namespace MareSynchronos.Models
|
namespace MareSynchronos.Models
|
||||||
{
|
{
|
||||||
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
public class FileReplacement
|
public class FileReplacement
|
||||||
{
|
{
|
||||||
private readonly string penumbraDirectory;
|
private readonly string penumbraDirectory;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
public string GamePath { get; private set; }
|
public string GamePath { get; private set; }
|
||||||
public string ReplacedPath { get; private set; } = string.Empty;
|
public string ResolvedPath { get; private 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>();
|
public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>();
|
||||||
|
[JsonProperty]
|
||||||
public bool HasFileReplacement => GamePath != ReplacedPath;
|
public string ImcData { get; set; } = string.Empty;
|
||||||
|
public bool HasFileReplacement => GamePath != ResolvedPath;
|
||||||
public FileReplacement(string gamePath, string penumbraDirectory)
|
public FileReplacement(string gamePath, string penumbraDirectory)
|
||||||
{
|
{
|
||||||
GamePath = gamePath;
|
GamePath = gamePath;
|
||||||
@@ -24,6 +32,8 @@ namespace MareSynchronos.Models
|
|||||||
|
|
||||||
public void AddAssociated(FileReplacement fileReplacement)
|
public void AddAssociated(FileReplacement fileReplacement)
|
||||||
{
|
{
|
||||||
|
fileReplacement.IsInUse = true;
|
||||||
|
|
||||||
if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement)))
|
if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement)))
|
||||||
{
|
{
|
||||||
Associated.Add(fileReplacement);
|
Associated.Add(fileReplacement);
|
||||||
@@ -35,31 +45,40 @@ namespace MareSynchronos.Models
|
|||||||
GamePath = path;
|
GamePath = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetReplacedPath(string path)
|
public void SetResolvedPath(string path)
|
||||||
{
|
{
|
||||||
ReplacedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/');
|
ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/');
|
||||||
|
if (!HasFileReplacement) return;
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
using FileCacheContext db = new FileCacheContext();
|
||||||
|
var fileCache = db.FileCaches.SingleOrDefault(f => f.Filepath == path.ToLower());
|
||||||
|
if (fileCache != null)
|
||||||
|
Hash = fileCache.Hash;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReplacedByThis(string path)
|
public bool IsReplacedByThis(string path)
|
||||||
{
|
{
|
||||||
return GamePath.ToLower() == path.ToLower() || ReplacedPath.ToLower() == path.ToLower();
|
return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReplacedByThis(FileReplacement replacement)
|
public bool IsReplacedByThis(FileReplacement replacement)
|
||||||
{
|
{
|
||||||
return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ReplacedPath);
|
return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ResolvedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ReplacedPath}");
|
builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}");
|
||||||
foreach (var l1 in Associated)
|
foreach (var l1 in Associated)
|
||||||
{
|
{
|
||||||
builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ReplacedPath}");
|
builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ResolvedPath}");
|
||||||
foreach (var l2 in l1.Associated)
|
foreach (var l2 in l1.Associated)
|
||||||
{
|
{
|
||||||
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ReplacedPath}");
|
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ResolvedPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
|
|||||||
19
MareSynchronos/PenumbraMod/DefaultMod.cs
Normal file
19
MareSynchronos/PenumbraMod/DefaultMod.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
MareSynchronos/PenumbraMod/Meta.cs
Normal file
21
MareSynchronos/PenumbraMod/Meta.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,14 @@ using System.Text;
|
|||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using System;
|
using System;
|
||||||
using MareSynchronos.Models;
|
using MareSynchronos.Models;
|
||||||
|
using Dalamud.Game.Gui;
|
||||||
|
using MareSynchronos.PenumbraMod;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Unicode;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace SamplePlugin
|
namespace SamplePlugin
|
||||||
{
|
{
|
||||||
@@ -30,7 +38,8 @@ namespace SamplePlugin
|
|||||||
{
|
{
|
||||||
public string Name => "Mare Synchronos";
|
public string Name => "Mare Synchronos";
|
||||||
|
|
||||||
private const string commandName = "/pscan";
|
private const string commandName = "/mare";
|
||||||
|
private readonly ClientState clientState;
|
||||||
|
|
||||||
private DalamudPluginInterface PluginInterface { get; init; }
|
private DalamudPluginInterface PluginInterface { get; init; }
|
||||||
private CommandManager CommandManager { get; init; }
|
private CommandManager CommandManager { get; init; }
|
||||||
@@ -45,11 +54,11 @@ namespace SamplePlugin
|
|||||||
public Plugin(
|
public Plugin(
|
||||||
[RequiredVersion("1.0")] DalamudPluginInterface pluginInterface,
|
[RequiredVersion("1.0")] DalamudPluginInterface pluginInterface,
|
||||||
[RequiredVersion("1.0")] CommandManager commandManager,
|
[RequiredVersion("1.0")] CommandManager commandManager,
|
||||||
Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager)
|
Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager, GameGui gameGui)
|
||||||
{
|
{
|
||||||
this.PluginInterface = pluginInterface;
|
this.PluginInterface = pluginInterface;
|
||||||
this.CommandManager = commandManager;
|
this.CommandManager = commandManager;
|
||||||
|
this.clientState = clientState;
|
||||||
this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
|
this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
this.Configuration.Initialize(this.PluginInterface);
|
this.Configuration.Initialize(this.PluginInterface);
|
||||||
|
|
||||||
@@ -67,7 +76,7 @@ namespace SamplePlugin
|
|||||||
this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI;
|
this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI;
|
||||||
|
|
||||||
playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable);
|
playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable);
|
||||||
drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new MareSynchronos.Models.FileReplacementFactory(pluginInterface, clientState));
|
drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(pluginInterface, clientState), gameGui);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -87,6 +96,17 @@ namespace SamplePlugin
|
|||||||
return;
|
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")
|
if (args == "scan")
|
||||||
{
|
{
|
||||||
cts = new CancellationTokenSource();
|
cts = new CancellationTokenSource();
|
||||||
@@ -116,45 +136,81 @@ namespace SamplePlugin
|
|||||||
var resources = drawHooks.PrintRequestedResources();
|
var resources = drawHooks.PrintRequestedResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args == "copy")
|
if (args == "printjson")
|
||||||
|
{
|
||||||
|
var cache = drawHooks.BuildCharacterCache();
|
||||||
|
var json = JsonConvert.SerializeObject(cache, Formatting.Indented);
|
||||||
|
PluginLog.Debug(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args == "createtestmod")
|
||||||
{
|
{
|
||||||
var resources = drawHooks.PrintRequestedResources();
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
PluginLog.Debug("Copying files");
|
var playerName = clientState.LocalPlayer!.Name.ToString();
|
||||||
foreach (var file in Directory.GetFiles(@"G:\Penumbra\TestMod\files"))
|
var modName = $"Mare Synchronos Test Mod {playerName}";
|
||||||
|
var modDirectory = PluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc();
|
||||||
|
string modDirectoryPath = Path.Combine(modDirectory, modName);
|
||||||
|
if (Directory.Exists(modDirectoryPath))
|
||||||
{
|
{
|
||||||
File.Delete(file);
|
Directory.Delete(modDirectoryPath, true);
|
||||||
}
|
}
|
||||||
File.Delete(@"G:\Penumbra\testmod\filelist.txt");
|
|
||||||
using FileCacheContext db = new FileCacheContext();
|
Directory.CreateDirectory(modDirectoryPath);
|
||||||
|
Directory.CreateDirectory(Path.Combine(modDirectoryPath, "files"));
|
||||||
|
Meta meta = new Meta()
|
||||||
|
{
|
||||||
|
Name = modName,
|
||||||
|
Author = playerName,
|
||||||
|
Description = "Mare Synchronous Test Mod Export",
|
||||||
|
};
|
||||||
|
|
||||||
|
var resources = drawHooks.PrintRequestedResources();
|
||||||
|
var metaJson = JsonConvert.SerializeObject(meta);
|
||||||
|
File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson);
|
||||||
|
|
||||||
|
DefaultMod defaultMod = new DefaultMod();
|
||||||
|
|
||||||
|
using var db = new FileCacheContext();
|
||||||
foreach (var resource in resources)
|
foreach (var resource in resources)
|
||||||
{
|
{
|
||||||
CopyRecursive(resource, db);
|
CopyRecursive(resource, modDirectoryPath, db, defaultMod.Files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultModJson = JsonConvert.SerializeObject(defaultMod);
|
||||||
|
File.WriteAllText(Path.Combine(modDirectoryPath, "default_mod.json"), defaultModJson);
|
||||||
|
|
||||||
|
PluginLog.Debug("Mod created to " + modDirectoryPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CopyRecursive(FileReplacement replacement, FileCacheContext db)
|
private void CopyRecursive(FileReplacement replacement, string targetDirectory, FileCacheContext db, Dictionary<string, string>? resourceDict = null)
|
||||||
{
|
{
|
||||||
if (replacement.HasFileReplacement)
|
if (replacement.HasFileReplacement)
|
||||||
{
|
{
|
||||||
PluginLog.Debug("Copying file \"" + replacement.ReplacedPath + "\"");
|
PluginLog.Debug("Copying file \"" + replacement.ResolvedPath + "\"");
|
||||||
|
|
||||||
var fileCache = db.FileCaches.Single(f => f.Filepath.Contains(replacement.ReplacedPath.Replace('/', '\\')));
|
var fileCache = db.FileCaches.Single(f => f.Filepath.Contains(replacement.ResolvedPath.Replace('/', '\\')));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ext = new FileInfo(fileCache.Filepath).Extension;
|
var ext = new FileInfo(fileCache.Filepath).Extension;
|
||||||
File.Copy(fileCache.Filepath, Path.Combine(@"G:\Penumbra\TestMod\files", fileCache.Hash.ToLower() + ext));
|
File.Copy(fileCache.Filepath, Path.Combine(targetDirectory, "files", fileCache.Hash.ToLower() + ext));
|
||||||
File.AppendAllLines(Path.Combine(@"G:\Penumbra\TestMod", "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," });
|
if (resourceDict != null)
|
||||||
|
{
|
||||||
|
resourceDict[replacement.GamePath] = $"files\\{fileCache.Hash.ToLower() + ext}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var associated in replacement.Associated)
|
foreach (var associated in replacement.Associated)
|
||||||
{
|
{
|
||||||
CopyRecursive(associated, db);
|
CopyRecursive(associated, targetDirectory, db, resourceDict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +230,7 @@ namespace SamplePlugin
|
|||||||
{
|
{
|
||||||
Stopwatch st = Stopwatch.StartNew();
|
Stopwatch st = Stopwatch.StartNew();
|
||||||
|
|
||||||
string penumbraDir = Configuration.PenumbraFolder;
|
string penumbraDir = PluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory").InvokeFunc();
|
||||||
PluginLog.Debug("Getting files from " + penumbraDir);
|
PluginLog.Debug("Getting files from " + penumbraDir);
|
||||||
ConcurrentDictionary<string, bool> charaFiles = new ConcurrentDictionary<string, bool>(
|
ConcurrentDictionary<string, bool> charaFiles = new ConcurrentDictionary<string, bool>(
|
||||||
Directory.GetFiles(penumbraDir, "*.*", SearchOption.AllDirectories)
|
Directory.GetFiles(penumbraDir, "*.*", SearchOption.AllDirectories)
|
||||||
|
|||||||
Reference in New Issue
Block a user