remove DrawHooks, use new penumbra IPC calls (otter pls)
This commit is contained in:
@@ -7,45 +7,20 @@ namespace MareSynchronos.Factories
|
|||||||
public class FileReplacementFactory
|
public class FileReplacementFactory
|
||||||
{
|
{
|
||||||
private readonly IpcManager ipcManager;
|
private readonly IpcManager ipcManager;
|
||||||
private readonly ClientState clientState;
|
|
||||||
private string playerName;
|
|
||||||
|
|
||||||
public FileReplacementFactory(IpcManager ipcManager, ClientState clientState)
|
public FileReplacementFactory(IpcManager ipcManager)
|
||||||
{
|
{
|
||||||
this.ipcManager = ipcManager;
|
this.ipcManager = ipcManager;
|
||||||
this.clientState = clientState;
|
|
||||||
playerName = null!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileReplacement Create(string gamePath, bool resolve = true)
|
public FileReplacement Create()
|
||||||
{
|
{
|
||||||
if (!ipcManager.CheckPenumbraAPI())
|
if (!ipcManager.CheckPenumbraAPI())
|
||||||
{
|
{
|
||||||
throw new System.Exception();
|
throw new System.Exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileReplacement = new FileReplacement(gamePath, ipcManager.PenumbraModDirectory()!);
|
return new FileReplacement(ipcManager.PenumbraModDirectory()!);
|
||||||
if (!resolve) return fileReplacement;
|
|
||||||
|
|
||||||
if (clientState.LocalPlayer != null)
|
|
||||||
{
|
|
||||||
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[^1] = "--" + tempGamePath[^1];
|
|
||||||
string newTempGamePath = string.Join('/', tempGamePath);
|
|
||||||
var resolvedPath = ipcManager.PenumbraResolvePath(newTempGamePath, playerName)!;
|
|
||||||
if (resolvedPath != newTempGamePath)
|
|
||||||
{
|
|
||||||
fileReplacement.SetResolvedPath(resolvedPath);
|
|
||||||
fileReplacement.SetGamePath(newTempGamePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fileReplacement;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,433 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState;
|
|
||||||
using Dalamud.Game.ClientState.Objects;
|
|
||||||
using Dalamud.Game.Gui;
|
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Logging;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using MareSynchronos.Factories;
|
|
||||||
using MareSynchronos.Managers;
|
|
||||||
using MareSynchronos.Models;
|
|
||||||
using Penumbra.GameData.ByteString;
|
|
||||||
using Penumbra.Interop.Structs;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MareSynchronos.Hooks
|
|
||||||
{
|
|
||||||
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)]
|
|
||||||
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;
|
|
||||||
|
|
||||||
public event EventHandler? PlayerLoadEvent;
|
|
||||||
|
|
||||||
public Hook<GeneralResolveDelegate>? ResolveMdlPathHook;
|
|
||||||
public Hook<MaterialResolveDetour>? ResolveMtrlPathHook;
|
|
||||||
private readonly ClientState clientState;
|
|
||||||
private readonly Dictionary<IntPtr, ushort> DrawObjectToObject = new();
|
|
||||||
private readonly FileReplacementFactory factory;
|
|
||||||
private readonly GameGui gameGui;
|
|
||||||
private readonly ObjectTable objectTable;
|
|
||||||
private readonly DalamudPluginInterface pluginInterface;
|
|
||||||
private ConcurrentDictionary<string, FileReplacement> cachedResources = new();
|
|
||||||
private GameObject* lastGameObject = null;
|
|
||||||
private ConcurrentBag<FileReplacement> loadedMaterials = new();
|
|
||||||
|
|
||||||
public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui)
|
|
||||||
{
|
|
||||||
this.pluginInterface = pluginInterface;
|
|
||||||
this.clientState = clientState;
|
|
||||||
this.objectTable = objectTable;
|
|
||||||
this.factory = factory;
|
|
||||||
this.gameGui = gameGui;
|
|
||||||
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.Value.IsInUse = false;
|
|
||||||
resource.Value.ImcData = string.Empty;
|
|
||||||
resource.Value.Associated.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Verbose("Invaldated resource cache");
|
|
||||||
|
|
||||||
var cache = new CharacterCache();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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.Value.IsReplacedByThis(mdlResource)).Value;
|
|
||||||
|
|
||||||
var imc = (ResourceHandle*)model->IMCArray[idx];
|
|
||||||
if (imc != null)
|
|
||||||
{
|
|
||||||
byte[] imcData = new byte[imc->Data->DataLength / sizeof(long)];
|
|
||||||
Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength / sizeof(long));
|
|
||||||
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.Value.IsReplacedByThis(mtrlFileResource)).Value;
|
|
||||||
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.Value.IsReplacedByThis(texResource)).Value;
|
|
||||||
cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PluginLog.Error(ex, ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PrintRequestedResources()
|
|
||||||
{
|
|
||||||
var cache = BuildCharacterCache();
|
|
||||||
|
|
||||||
PluginLog.Verbose("--- CURRENTLY LOADED FILES ---");
|
|
||||||
|
|
||||||
PluginLog.Verbose(cache.ToString());
|
|
||||||
|
|
||||||
PluginLog.Verbose("--- LOOSE FILES ---");
|
|
||||||
|
|
||||||
foreach (var resource in cachedResources.Where(r => !r.Value.IsInUse).OrderBy(a => a.Value.GamePath))
|
|
||||||
{
|
|
||||||
PluginLog.Verbose(resource.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartHooks()
|
|
||||||
{
|
|
||||||
cachedResources.Clear();
|
|
||||||
SetupHumanHooks();
|
|
||||||
EnableHumanHooks();
|
|
||||||
PluginLog.Verbose("Hooks enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopHooks()
|
|
||||||
{
|
|
||||||
DisableHumanHooks();
|
|
||||||
DisposeHumanHooks();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddRequestedResource(FileReplacement replacement)
|
|
||||||
{
|
|
||||||
if (!cachedResources.Any(a => a.Value.IsReplacedByThis(replacement)) || cachedResources.Any(c => c.Key == replacement.GamePath))
|
|
||||||
{
|
|
||||||
cachedResources[replacement.GamePath] = replacement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d)
|
|
||||||
{
|
|
||||||
var ret = CharacterBaseCreateHook!.Original(a, b, c, d);
|
|
||||||
if (lastGameObject != null)
|
|
||||||
{
|
|
||||||
DrawObjectToObject[ret] = (lastGameObject->ObjectIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CharacterBaseDestructorDetour(IntPtr drawBase)
|
|
||||||
{
|
|
||||||
if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx))
|
|
||||||
{
|
|
||||||
var gameObj = GetGameObjectFromDrawObject(drawBase, idx);
|
|
||||||
if (clientState.LocalPlayer != null && gameObj == (GameObject*)clientState.LocalPlayer!.Address)
|
|
||||||
{
|
|
||||||
//PluginLog.Verbose("Clearing resources");
|
|
||||||
//cachedResources.Clear();
|
|
||||||
DrawObjectToObject.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CharacterBaseDestructorHook!.Original.Invoke(drawBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisableHumanHooks()
|
|
||||||
{
|
|
||||||
ResolveMdlPathHook?.Disable();
|
|
||||||
ResolveMdlPathHook?.Disable();
|
|
||||||
ResolveMtrlPathHook?.Disable();
|
|
||||||
EnableDrawHook?.Disable();
|
|
||||||
LoadMtrlTexHook?.Disable();
|
|
||||||
CharacterBaseCreateHook?.Disable();
|
|
||||||
CharacterBaseDestructorHook?.Disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisposeHumanHooks()
|
|
||||||
{
|
|
||||||
ResolveMdlPathHook?.Dispose();
|
|
||||||
ResolveMtrlPathHook?.Dispose();
|
|
||||||
EnableDrawHook?.Dispose();
|
|
||||||
LoadMtrlTexHook?.Dispose();
|
|
||||||
CharacterBaseCreateHook?.Dispose();
|
|
||||||
CharacterBaseDestructorHook?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d)
|
|
||||||
{
|
|
||||||
var oldObject = lastGameObject;
|
|
||||||
lastGameObject = (GameObject*)gameObject;
|
|
||||||
EnableDrawHook!.Original.Invoke(gameObject, b, c, d);
|
|
||||||
lastGameObject = oldObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableHumanHooks()
|
|
||||||
{
|
|
||||||
if (ResolveMdlPathHook?.IsEnabled ?? false) return;
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (mtrlResourceHandle == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var mtrl = (MtrlResource*)mtrlResourceHandle;
|
|
||||||
var mtrlPath = Utf8String.FromSpanUnsafe(mtrl->Handle.FileNameSpan(), true, null, true);
|
|
||||||
PluginLog.Verbose("Attempting to resolve: " + mtrlPath.ToString());
|
|
||||||
var mtrlResource = factory.Create(mtrlPath.ToString());
|
|
||||||
var existingMat = loadedMaterials.FirstOrDefault(m => m.IsReplacedByThis(mtrlResource));
|
|
||||||
if (existingMat != null)
|
|
||||||
{
|
|
||||||
PluginLog.Verbose("Resolving material: " + existingMat.GamePath);
|
|
||||||
for (int i = 0; i < mtrl->NumTex; i++)
|
|
||||||
{
|
|
||||||
var texPath = new Utf8String(mtrl->TexString(i));
|
|
||||||
PluginLog.Verbose("Resolving tex: " + texPath.ToString());
|
|
||||||
|
|
||||||
AddRequestedResource(factory.Create(texPath.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedMaterials = new(loadedMaterials.Except(new[] { existingMat }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
PluginLog.Error(ex, "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (path == IntPtr.Zero || clientState.LocalPlayer == null)
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gamepath = new Utf8String((byte*)path);
|
|
||||||
|
|
||||||
var playerName = GetPlayerName();
|
|
||||||
var gameDrawObject = (DrawObject*)drawObject;
|
|
||||||
GameObject* gameObject = lastGameObject;
|
|
||||||
|
|
||||||
if (DrawObjectToObject.TryGetValue(drawObject, out ushort idx))
|
|
||||||
{
|
|
||||||
gameObject = GetGameObjectFromDrawObject(drawObject, DrawObjectToObject[drawObject]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Verbose("Resolving resource: " + gamepath.ToString());
|
|
||||||
PlayerLoadEvent?.Invoke((IntPtr)gameObject, new EventArgs());
|
|
||||||
|
|
||||||
var resource = factory.Create(gamepath.ToString());
|
|
||||||
|
|
||||||
if (gamepath.ToString().EndsWith("mtrl"))
|
|
||||||
{
|
|
||||||
loadedMaterials.Add(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddRequestedResource(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupHumanHooks()
|
|
||||||
{
|
|
||||||
if (ResolveMdlPathHook != null) return;
|
|
||||||
|
|
||||||
ResolveMdlPathHook = new Hook<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour);
|
|
||||||
ResolveMtrlPathHook = new Hook<MaterialResolveDetour>(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,21 @@ using Dalamud.Game.ClientState.Objects;
|
|||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
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 MareSynchronos.Hooks;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
|
using MareSynchronos.Factories;
|
||||||
using MareSynchronos.Models;
|
using MareSynchronos.Models;
|
||||||
using MareSynchronos.Utils;
|
using MareSynchronos.Utils;
|
||||||
using MareSynchronos.WebAPI;
|
using MareSynchronos.WebAPI;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
using Penumbra.PlayerWatch;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -20,26 +26,37 @@ namespace MareSynchronos.Managers
|
|||||||
{
|
{
|
||||||
public class CharacterManager : IDisposable
|
public class CharacterManager : IDisposable
|
||||||
{
|
{
|
||||||
private readonly DrawHooks drawHooks;
|
|
||||||
private readonly ClientState clientState;
|
private readonly ClientState clientState;
|
||||||
private readonly Framework framework;
|
private readonly Framework framework;
|
||||||
private readonly ApiController apiController;
|
private readonly ApiController apiController;
|
||||||
private readonly ObjectTable objectTable;
|
private readonly ObjectTable objectTable;
|
||||||
private readonly IpcManager ipcManager;
|
private readonly IpcManager ipcManager;
|
||||||
private Task? drawHookTask = null;
|
private readonly FileReplacementFactory factory;
|
||||||
|
private readonly IPlayerWatcher watcher;
|
||||||
|
private Task? playerChangedTask = null;
|
||||||
|
|
||||||
public CharacterManager(DrawHooks drawhooks, ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager)
|
public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, FileReplacementFactory factory)
|
||||||
{
|
{
|
||||||
this.drawHooks = drawhooks;
|
|
||||||
this.clientState = clientState;
|
this.clientState = clientState;
|
||||||
this.framework = framework;
|
this.framework = framework;
|
||||||
this.apiController = apiController;
|
this.apiController = apiController;
|
||||||
this.objectTable = objectTable;
|
this.objectTable = objectTable;
|
||||||
this.ipcManager = ipcManager;
|
this.ipcManager = ipcManager;
|
||||||
drawHooks.StartHooks();
|
this.factory = factory;
|
||||||
|
watcher = PlayerWatchFactory.Create(framework, clientState, objectTable);
|
||||||
clientState.TerritoryChanged += ClientState_TerritoryChanged;
|
clientState.TerritoryChanged += ClientState_TerritoryChanged;
|
||||||
framework.Update += Framework_Update;
|
framework.Update += Framework_Update;
|
||||||
drawhooks.PlayerLoadEvent += Drawhooks_PlayerLoadEvent;
|
ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IpcManager_PenumbraRedrawEvent(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var actorName = ((string)sender!);
|
||||||
|
PluginLog.Debug("Penumbra redraw " + actorName);
|
||||||
|
if (actorName == GetPlayerName())
|
||||||
|
{
|
||||||
|
PlayerChanged(actorName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<string, string> localPlayers = new();
|
Dictionary<string, string> localPlayers = new();
|
||||||
@@ -81,6 +98,118 @@ namespace MareSynchronos.Managers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetPlayerName()
|
||||||
|
{
|
||||||
|
return clientState.LocalPlayer!.Name.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Watcher_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor)
|
||||||
|
{
|
||||||
|
if (actor.Name.ToString() == clientState.LocalPlayer!.Name.ToString())
|
||||||
|
{
|
||||||
|
PlayerChanged(actor.Name.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PluginLog.Debug("PlayerChanged: " + actor.Name.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void PlayerChanged(string name)
|
||||||
|
{
|
||||||
|
//if (sender == null) return;
|
||||||
|
PluginLog.Debug("Player changed: " + name);
|
||||||
|
if (playerChangedTask is { IsCompleted: false }) return;
|
||||||
|
|
||||||
|
playerChangedTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var obj = (GameObject*)clientState.LocalPlayer!.Address;
|
||||||
|
|
||||||
|
PluginLog.Debug("Waiting for charater to be drawn");
|
||||||
|
while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something
|
||||||
|
{
|
||||||
|
PluginLog.Debug("Waiting for character to finish drawing");
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
PluginLog.Debug("Character finished drawing");
|
||||||
|
|
||||||
|
// wait half a second just in case
|
||||||
|
Thread.Sleep(500);
|
||||||
|
|
||||||
|
var cache = CreateFullCharacterCache();
|
||||||
|
while (!cache.IsCompleted)
|
||||||
|
{
|
||||||
|
Thread.Sleep(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = apiController.SendCharacterData(cache.Result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe CharacterCache BuildCharacterCache()
|
||||||
|
{
|
||||||
|
var cache = new CharacterCache();
|
||||||
|
|
||||||
|
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 mdlPath = new Utf8String(mdl->ResourceHandle->FileName()).ToString();
|
||||||
|
|
||||||
|
FileReplacement cachedMdlResource = factory.Create();
|
||||||
|
cachedMdlResource.GamePaths = ipcManager.PenumbraReverseResolvePath(mdlPath, GetPlayerName());
|
||||||
|
cachedMdlResource.SetResolvedPath(mdlPath);
|
||||||
|
PluginLog.Verbose("Resolving for model " + mdlPath);
|
||||||
|
|
||||||
|
cache.AddAssociatedResource(cachedMdlResource, null!, null!);
|
||||||
|
|
||||||
|
var imc = (ResourceHandle*)model->IMCArray[idx];
|
||||||
|
if (imc != null)
|
||||||
|
{
|
||||||
|
byte[] imcData = new byte[imc->Data->DataLength / sizeof(long)];
|
||||||
|
Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength / sizeof(long));
|
||||||
|
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();
|
||||||
|
var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2];
|
||||||
|
PluginLog.Verbose("Resolving for material " + mtrlPath);
|
||||||
|
var cachedMtrlResource = factory.Create();
|
||||||
|
cachedMtrlResource.GamePaths = ipcManager.PenumbraReverseResolvePath(mtrlPath, GetPlayerName());
|
||||||
|
cachedMtrlResource.SetResolvedPath(mtrlPath);
|
||||||
|
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)).ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(texPath.ToString())) continue;
|
||||||
|
PluginLog.Verbose("Resolving for texture " + texPath);
|
||||||
|
|
||||||
|
var cachedTexResource = factory.Create();
|
||||||
|
cachedTexResource.GamePaths = new[] { texPath };
|
||||||
|
cachedTexResource.SetResolvedPath(ipcManager.PenumbraResolvePath(texPath, GetPlayerName())!);
|
||||||
|
cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
private void ClientState_TerritoryChanged(object? sender, ushort e)
|
private void ClientState_TerritoryChanged(object? sender, ushort e)
|
||||||
{
|
{
|
||||||
localPlayers.Clear();
|
localPlayers.Clear();
|
||||||
@@ -89,46 +218,31 @@ namespace MareSynchronos.Managers
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
framework.Update -= Framework_Update;
|
framework.Update -= Framework_Update;
|
||||||
drawHooks.PlayerLoadEvent -= Drawhooks_PlayerLoadEvent;
|
|
||||||
clientState.TerritoryChanged -= ClientState_TerritoryChanged;
|
clientState.TerritoryChanged -= ClientState_TerritoryChanged;
|
||||||
drawHooks?.Dispose();
|
watcher.PlayerChanged -= Watcher_PlayerChanged;
|
||||||
|
watcher?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void Drawhooks_PlayerLoadEvent(object? sender, EventArgs e)
|
internal void StartWatchingPlayer()
|
||||||
{
|
{
|
||||||
if (sender == null) return;
|
watcher.AddPlayerToWatch(clientState.LocalPlayer!.Name.ToString());
|
||||||
if (drawHookTask != null && !drawHookTask.IsCompleted) return;
|
watcher.PlayerChanged += Watcher_PlayerChanged;
|
||||||
|
watcher.Enable();
|
||||||
var obj = (GameObject*)(IntPtr)sender;
|
|
||||||
drawHookTask = Task.Run(() =>
|
|
||||||
{
|
|
||||||
PluginLog.Debug("Waiting for charater to be drawn");
|
|
||||||
while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something
|
|
||||||
{
|
|
||||||
Thread.Sleep(10);
|
|
||||||
}
|
|
||||||
PluginLog.Debug("Character finished drawing");
|
|
||||||
|
|
||||||
// wait one more second just in case
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
|
|
||||||
var cache = CreateFullCharacterCache();
|
|
||||||
while (!cache.IsCompleted)
|
|
||||||
{
|
|
||||||
Task.Delay(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = apiController.SendCharacterData(cache.Result);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CharacterCache GetCharacterCache() => drawHooks.BuildCharacterCache();
|
public void StopWatchPlayer(string name)
|
||||||
|
{
|
||||||
|
watcher.RemovePlayerFromWatch(name);
|
||||||
|
}
|
||||||
|
|
||||||
public void PrintRequestedResources() => drawHooks.PrintRequestedResources();
|
public void WatchPlayer(string name)
|
||||||
|
{
|
||||||
|
watcher.AddPlayerToWatch(name);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<CharacterCache> CreateFullCharacterCache()
|
private async Task<CharacterCache> CreateFullCharacterCache()
|
||||||
{
|
{
|
||||||
var cache = drawHooks.BuildCharacterCache();
|
var cache = BuildCharacterCache();
|
||||||
cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!);
|
cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!);
|
||||||
cache.JobId = clientState.LocalPlayer!.ClassJob.Id;
|
cache.JobId = clientState.LocalPlayer!.ClassJob.Id;
|
||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
@@ -137,6 +251,7 @@ namespace MareSynchronos.Managers
|
|||||||
{
|
{
|
||||||
await Task.Delay(50);
|
await Task.Delay(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
var json = JsonConvert.SerializeObject(cache, Formatting.Indented);
|
var json = JsonConvert.SerializeObject(cache, Formatting.Indented);
|
||||||
|
|
||||||
cache.CacheHash = Crypto.GetHash(json);
|
cache.CacheHash = Crypto.GetHash(json);
|
||||||
@@ -145,12 +260,12 @@ namespace MareSynchronos.Managers
|
|||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DebugJson()
|
public async Task DebugJson()
|
||||||
{
|
{
|
||||||
var cache = CreateFullCharacterCache();
|
var cache = CreateFullCharacterCache();
|
||||||
while (!cache.IsCompleted)
|
while (!cache.IsCompleted)
|
||||||
{
|
{
|
||||||
Task.Delay(50);
|
await Task.Delay(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented));
|
PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented));
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ namespace MareSynchronos.Managers
|
|||||||
private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization;
|
private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization;
|
||||||
private ICallGateSubscriber<int> penumbraApiVersion;
|
private ICallGateSubscriber<int> penumbraApiVersion;
|
||||||
private ICallGateSubscriber<int> glamourerApiVersion;
|
private ICallGateSubscriber<int> glamourerApiVersion;
|
||||||
|
private ICallGateSubscriber<string, string> penumbraObjectIsRedrawn;
|
||||||
private ICallGateSubscriber<string, int, object>? penumbraRedraw;
|
private ICallGateSubscriber<string, int, object>? penumbraRedraw;
|
||||||
|
private ICallGateSubscriber<string, string, string[]>? penumbraReverseResolvePath;
|
||||||
|
|
||||||
public bool Initialized { get; private set; } = false;
|
public bool Initialized { get; private set; } = false;
|
||||||
|
|
||||||
|
public event EventHandler? PenumbraRedrawEvent;
|
||||||
|
|
||||||
public IpcManager(DalamudPluginInterface pi)
|
public IpcManager(DalamudPluginInterface pi)
|
||||||
{
|
{
|
||||||
pluginInterface = pi;
|
pluginInterface = pi;
|
||||||
@@ -29,9 +33,12 @@ namespace MareSynchronos.Managers
|
|||||||
penumbraRedraw = pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
|
penumbraRedraw = pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
|
||||||
glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization");
|
glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization");
|
||||||
glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization");
|
glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization");
|
||||||
|
penumbraReverseResolvePath = pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath");
|
||||||
penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
|
penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
|
||||||
glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion");
|
glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion");
|
||||||
penumbraInit.Subscribe(() => penumbraRedraw!.InvokeAction("self", 0));
|
penumbraObjectIsRedrawn = pluginInterface.GetIpcSubscriber<string, string>("Penumbra.ObjectIsRedrawn");
|
||||||
|
penumbraObjectIsRedrawn.Subscribe(RedrawEvent);
|
||||||
|
penumbraInit.Subscribe(RedrawSelf);
|
||||||
|
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
}
|
}
|
||||||
@@ -60,16 +67,35 @@ namespace MareSynchronos.Managers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RedrawEvent(string actorName)
|
||||||
|
{
|
||||||
|
PenumbraRedrawEvent?.Invoke(actorName, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RedrawSelf()
|
||||||
|
{
|
||||||
|
penumbraRedraw!.InvokeAction("self", 0);
|
||||||
|
}
|
||||||
|
|
||||||
private void Uninitialize()
|
private void Uninitialize()
|
||||||
{
|
{
|
||||||
|
penumbraInit.Unsubscribe(RedrawSelf);
|
||||||
|
penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent);
|
||||||
penumbraResolvePath = null;
|
penumbraResolvePath = null;
|
||||||
penumbraResolveModDir = null;
|
penumbraResolveModDir = null;
|
||||||
glamourerGetCharacterCustomization = null;
|
glamourerGetCharacterCustomization = null;
|
||||||
glamourerApplyCharacterCustomization = null;
|
glamourerApplyCharacterCustomization = null;
|
||||||
|
penumbraReverseResolvePath = null;
|
||||||
Initialized = false;
|
Initialized = false;
|
||||||
PluginLog.Debug("IPC Manager disposed");
|
PluginLog.Debug("IPC Manager disposed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string[] PenumbraReverseResolvePath(string path, string characterName)
|
||||||
|
{
|
||||||
|
if (!CheckPenumbraAPI()) return new[] { path };
|
||||||
|
return penumbraReverseResolvePath!.InvokeFunc(path, characterName);
|
||||||
|
}
|
||||||
|
|
||||||
public string? PenumbraResolvePath(string path, string characterName)
|
public string? PenumbraResolvePath(string path, string characterName)
|
||||||
{
|
{
|
||||||
if (!CheckPenumbraAPI()) return null;
|
if (!CheckPenumbraAPI()) return null;
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ namespace MareSynchronos.Models
|
|||||||
{
|
{
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public List<FileReplacement> AllReplacements =>
|
public List<FileReplacement> AllReplacements =>
|
||||||
FileReplacements.Where(x => x.HasFileReplacement)
|
FileReplacements.Where(f => f.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().OrderBy(f => f.GamePath)
|
.Distinct().OrderBy(f => f.GamePaths[0])
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
|
public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
|
||||||
@@ -31,11 +31,10 @@ namespace MareSynchronos.Models
|
|||||||
|
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public uint JobId { get; set; } = 0;
|
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
|
||||||
{
|
{
|
||||||
if (resource == null) return;
|
|
||||||
if (mdlParent == null)
|
if (mdlParent == null)
|
||||||
{
|
{
|
||||||
resource.IsInUse = true;
|
resource.IsInUse = true;
|
||||||
@@ -43,16 +42,16 @@ namespace MareSynchronos.Models
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileReplacement replacement;
|
var mdlReplacements = FileReplacements.Where(f => f == mdlParent && mtrlParent == null);
|
||||||
|
foreach (var mdlReplacement in mdlReplacements)
|
||||||
if (mtrlParent == null && (replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)!) != null)
|
|
||||||
{
|
{
|
||||||
replacement.AddAssociated(resource);
|
mdlReplacement.AddAssociated(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)?.Associated.SingleOrDefault(f => f == mtrlParent)!) != null)
|
var mtrlReplacements = FileReplacements.Where(f => f == mdlParent).SelectMany(a => a.Associated).Where(f => f == mtrlParent);
|
||||||
|
foreach (var mtrlReplacement in mtrlReplacements)
|
||||||
{
|
{
|
||||||
replacement.AddAssociated(resource);
|
mtrlReplacement.AddAssociated(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -89,7 +88,7 @@ namespace MareSynchronos.Models
|
|||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
StringBuilder stringBuilder = new();
|
StringBuilder stringBuilder = new();
|
||||||
foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePath))
|
foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePaths[0]))
|
||||||
{
|
{
|
||||||
stringBuilder.AppendLine(fileReplacement.ToString());
|
stringBuilder.AppendLine(fileReplacement.ToString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ namespace MareSynchronos.Models
|
|||||||
private readonly string penumbraDirectory;
|
private readonly string penumbraDirectory;
|
||||||
|
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public string GamePath { get; private set; }
|
public string[] GamePaths { get; set; } = Array.Empty<string>();
|
||||||
public string ResolvedPath { get; private set; } = string.Empty;
|
[JsonProperty]
|
||||||
|
public string ResolvedPath { get; set; } = string.Empty;
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public string Hash { get; set; } = string.Empty;
|
public string Hash { get; set; } = string.Empty;
|
||||||
public bool IsInUse { get; set; } = false;
|
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]
|
[JsonProperty]
|
||||||
public string ImcData { get; set; } = string.Empty;
|
public string ImcData { get; set; } = string.Empty;
|
||||||
public bool HasFileReplacement => GamePath != ResolvedPath;
|
public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath;
|
||||||
|
|
||||||
public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed);
|
public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed);
|
||||||
private Task? computationTask = null;
|
private Task? computationTask = null;
|
||||||
public FileReplacement(string gamePath, string penumbraDirectory)
|
public FileReplacement(string penumbraDirectory)
|
||||||
{
|
{
|
||||||
GamePath = gamePath;
|
|
||||||
this.penumbraDirectory = penumbraDirectory;
|
this.penumbraDirectory = penumbraDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,15 +39,7 @@ namespace MareSynchronos.Models
|
|||||||
{
|
{
|
||||||
fileReplacement.IsInUse = true;
|
fileReplacement.IsInUse = true;
|
||||||
|
|
||||||
if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement)))
|
Associated.Add(fileReplacement);
|
||||||
{
|
|
||||||
Associated.Add(fileReplacement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetGamePath(string path)
|
|
||||||
{
|
|
||||||
GamePath = path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetResolvedPath(string path)
|
public void SetResolvedPath(string path)
|
||||||
@@ -107,26 +99,16 @@ namespace MareSynchronos.Models
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReplacedByThis(string path)
|
|
||||||
{
|
|
||||||
return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsReplacedByThis(FileReplacement replacement)
|
|
||||||
{
|
|
||||||
return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ResolvedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
StringBuilder builder = new();
|
StringBuilder builder = new();
|
||||||
builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}");
|
builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}");
|
||||||
foreach (var l1 in Associated)
|
foreach (var l1 in Associated)
|
||||||
{
|
{
|
||||||
builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ResolvedPath}");
|
builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {string.Join(",", l1.GamePaths)} => {l1.ResolvedPath}");
|
||||||
foreach (var l2 in l1.Associated)
|
foreach (var l2 in l1.Associated)
|
||||||
{
|
{
|
||||||
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ResolvedPath}");
|
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
@@ -137,7 +119,7 @@ namespace MareSynchronos.Models
|
|||||||
if (obj == null) return true;
|
if (obj == null) return true;
|
||||||
if (obj.GetType() == typeof(FileReplacement))
|
if (obj.GetType() == typeof(FileReplacement))
|
||||||
{
|
{
|
||||||
return Hash == ((FileReplacement)obj).Hash && GamePath == ((FileReplacement)obj).GamePath;
|
return Hash == ((FileReplacement)obj).Hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.Equals(obj);
|
return base.Equals(obj);
|
||||||
@@ -148,8 +130,7 @@ namespace MareSynchronos.Models
|
|||||||
int result = 13;
|
int result = 13;
|
||||||
result *= 397;
|
result *= 397;
|
||||||
result += Hash.GetHashCode();
|
result += Hash.GetHashCode();
|
||||||
result += GamePath.GetHashCode();
|
result += ResolvedPath.GetHashCode();
|
||||||
result += ImcData.GetHashCode();
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MareSynchronos.Hooks;
|
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Game.ClientState.Objects;
|
using Dalamud.Game.ClientState.Objects;
|
||||||
using Dalamud.Game.ClientState;
|
using Dalamud.Game.ClientState;
|
||||||
@@ -29,21 +28,19 @@ namespace MareSynchronos
|
|||||||
private const string commandName = "/mare";
|
private const string commandName = "/mare";
|
||||||
private readonly ClientState clientState;
|
private readonly ClientState clientState;
|
||||||
private readonly Framework framework;
|
private readonly Framework framework;
|
||||||
private readonly GameGui gameGui;
|
|
||||||
private readonly ObjectTable objectTable;
|
private readonly ObjectTable objectTable;
|
||||||
private readonly WindowSystem windowSystem;
|
private readonly WindowSystem windowSystem;
|
||||||
private readonly ApiController apiController;
|
private readonly ApiController apiController;
|
||||||
private CharacterManager? characterManager;
|
private CharacterManager? characterManager;
|
||||||
private IpcManager ipcManager;
|
private IpcManager ipcManager;
|
||||||
public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager,
|
public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager,
|
||||||
Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui)
|
Framework framework, ObjectTable objectTable, ClientState clientState)
|
||||||
{
|
{
|
||||||
this.PluginInterface = pluginInterface;
|
this.PluginInterface = pluginInterface;
|
||||||
this.CommandManager = commandManager;
|
this.CommandManager = commandManager;
|
||||||
this.framework = framework;
|
this.framework = framework;
|
||||||
this.objectTable = objectTable;
|
this.objectTable = objectTable;
|
||||||
this.clientState = clientState;
|
this.clientState = clientState;
|
||||||
this.gameGui = gameGui;
|
|
||||||
Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
|
Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
Configuration.Initialize(this.PluginInterface);
|
Configuration.Initialize(this.PluginInterface);
|
||||||
|
|
||||||
@@ -94,8 +91,8 @@ namespace MareSynchronos
|
|||||||
}
|
}
|
||||||
|
|
||||||
characterManager = new CharacterManager(
|
characterManager = new CharacterManager(
|
||||||
new DrawHooks(PluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui),
|
clientState, framework, apiController, objectTable, ipcManager, new FileReplacementFactory(ipcManager));
|
||||||
clientState, framework, apiController, objectTable, ipcManager);
|
characterManager.StartWatchingPlayer();
|
||||||
ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString());
|
ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,11 +137,14 @@ namespace MareSynchronos
|
|||||||
File.Copy(fileCache.Filepath, newFilePath);
|
File.Copy(fileCache.Filepath, newFilePath);
|
||||||
if (resourceDict != null)
|
if (resourceDict != null)
|
||||||
{
|
{
|
||||||
resourceDict[replacement.GamePath] = $"files\\{fileCache.Hash.ToLower() + ext}";
|
foreach(var path in replacement.GamePaths)
|
||||||
|
{
|
||||||
|
resourceDict[path] = $"files\\{fileCache.Hash.ToLower() + ext}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," });
|
//File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,14 +167,21 @@ namespace MareSynchronos
|
|||||||
|
|
||||||
private void OnCommand(string command, string args)
|
private void OnCommand(string command, string args)
|
||||||
{
|
{
|
||||||
if (args == "print")
|
|
||||||
{
|
|
||||||
characterManager?.PrintRequestedResources();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args == "printjson")
|
if (args == "printjson")
|
||||||
{
|
{
|
||||||
characterManager?.DebugJson();
|
_ = characterManager?.DebugJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.StartsWith("watch"))
|
||||||
|
{
|
||||||
|
var playerName = args.Replace("watch", "").Trim();
|
||||||
|
characterManager!.WatchPlayer(playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.StartsWith("stop"))
|
||||||
|
{
|
||||||
|
var playerName = args.Replace("watch", "").Trim();
|
||||||
|
characterManager!.StopWatchPlayer(playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args == "createtestmod")
|
if (args == "createtestmod")
|
||||||
@@ -199,7 +206,7 @@ namespace MareSynchronos
|
|||||||
Description = "Mare Synchronous Test Mod Export",
|
Description = "Mare Synchronous Test Mod Export",
|
||||||
};
|
};
|
||||||
|
|
||||||
var resources = characterManager!.GetCharacterCache();
|
var resources = characterManager!.BuildCharacterCache();
|
||||||
var metaJson = JsonConvert.SerializeObject(meta);
|
var metaJson = JsonConvert.SerializeObject(meta);
|
||||||
File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson);
|
File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user