remove DrawHooks, use new penumbra IPC calls (otter pls)

This commit is contained in:
Stanley Dimant
2022-06-17 01:08:36 +02:00
parent f643b413f2
commit 176eb2a344
7 changed files with 230 additions and 560 deletions

View File

@@ -7,45 +7,20 @@ namespace MareSynchronos.Factories
public class FileReplacementFactory
{
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.clientState = clientState;
playerName = null!;
}
public FileReplacement Create(string gamePath, bool resolve = true)
public FileReplacement Create()
{
if (!ipcManager.CheckPenumbraAPI())
{
throw new System.Exception();
}
var fileReplacement = new FileReplacement(gamePath, 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;
return new FileReplacement(ipcManager.PenumbraModDirectory()!);
}
}
}

View File

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

View File

@@ -4,15 +4,21 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
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.Utils;
using MareSynchronos.WebAPI;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Interop.Structs;
using Penumbra.PlayerWatch;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -20,26 +26,37 @@ namespace MareSynchronos.Managers
{
public class CharacterManager : IDisposable
{
private readonly DrawHooks drawHooks;
private readonly ClientState clientState;
private readonly Framework framework;
private readonly ApiController apiController;
private readonly ObjectTable objectTable;
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.framework = framework;
this.apiController = apiController;
this.objectTable = objectTable;
this.ipcManager = ipcManager;
drawHooks.StartHooks();
this.factory = factory;
watcher = PlayerWatchFactory.Create(framework, clientState, objectTable);
clientState.TerritoryChanged += ClientState_TerritoryChanged;
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();
@@ -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)
{
localPlayers.Clear();
@@ -89,46 +218,31 @@ namespace MareSynchronos.Managers
public void Dispose()
{
framework.Update -= Framework_Update;
drawHooks.PlayerLoadEvent -= Drawhooks_PlayerLoadEvent;
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;
if (drawHookTask != null && !drawHookTask.IsCompleted) return;
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);
});
watcher.AddPlayerToWatch(clientState.LocalPlayer!.Name.ToString());
watcher.PlayerChanged += Watcher_PlayerChanged;
watcher.Enable();
}
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()
{
var cache = drawHooks.BuildCharacterCache();
var cache = BuildCharacterCache();
cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!);
cache.JobId = clientState.LocalPlayer!.ClassJob.Id;
await Task.Run(async () =>
@@ -137,6 +251,7 @@ namespace MareSynchronos.Managers
{
await Task.Delay(50);
}
var json = JsonConvert.SerializeObject(cache, Formatting.Indented);
cache.CacheHash = Crypto.GetHash(json);
@@ -145,12 +260,12 @@ namespace MareSynchronos.Managers
return cache;
}
public void DebugJson()
public async Task DebugJson()
{
var cache = CreateFullCharacterCache();
while (!cache.IsCompleted)
{
Task.Delay(50);
await Task.Delay(50);
}
PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented));

View File

@@ -15,10 +15,14 @@ namespace MareSynchronos.Managers
private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization;
private ICallGateSubscriber<int> penumbraApiVersion;
private ICallGateSubscriber<int> glamourerApiVersion;
private ICallGateSubscriber<string, string> penumbraObjectIsRedrawn;
private ICallGateSubscriber<string, int, object>? penumbraRedraw;
private ICallGateSubscriber<string, string, string[]>? penumbraReverseResolvePath;
public bool Initialized { get; private set; } = false;
public event EventHandler? PenumbraRedrawEvent;
public IpcManager(DalamudPluginInterface pi)
{
pluginInterface = pi;
@@ -29,9 +33,12 @@ namespace MareSynchronos.Managers
penumbraRedraw = pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization");
glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization");
penumbraReverseResolvePath = pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath");
penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion");
penumbraInit.Subscribe(() => penumbraRedraw!.InvokeAction("self", 0));
penumbraObjectIsRedrawn = pluginInterface.GetIpcSubscriber<string, string>("Penumbra.ObjectIsRedrawn");
penumbraObjectIsRedrawn.Subscribe(RedrawEvent);
penumbraInit.Subscribe(RedrawSelf);
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()
{
penumbraInit.Unsubscribe(RedrawSelf);
penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent);
penumbraResolvePath = null;
penumbraResolveModDir = null;
glamourerGetCharacterCustomization = null;
glamourerApplyCharacterCustomization = null;
penumbraReverseResolvePath = null;
Initialized = false;
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)
{
if (!CheckPenumbraAPI()) return null;

View File

@@ -13,10 +13,10 @@ namespace MareSynchronos.Models
{
[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))
.Distinct().OrderBy(f => f.GamePath)
FileReplacements.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)
.Distinct().OrderBy(f => f.GamePaths[0])
.ToList();
public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>();
@@ -31,11 +31,10 @@ namespace MareSynchronos.Models
[JsonProperty]
public uint JobId { get; set; } = 0;
public void AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent)
public void AddAssociatedResource(FileReplacement resource, FileReplacement? mdlParent, FileReplacement? mtrlParent)
{
try
{
if (resource == null) return;
if (mdlParent == null)
{
resource.IsInUse = true;
@@ -43,16 +42,16 @@ namespace MareSynchronos.Models
return;
}
FileReplacement replacement;
if (mtrlParent == null && (replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)!) != null)
var mdlReplacements = FileReplacements.Where(f => f == mdlParent && mtrlParent == null);
foreach (var mdlReplacement in mdlReplacements)
{
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)
@@ -89,7 +88,7 @@ namespace MareSynchronos.Models
public override string ToString()
{
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());
}

View File

@@ -17,21 +17,21 @@ namespace MareSynchronos.Models
private readonly string penumbraDirectory;
[JsonProperty]
public string GamePath { get; private set; }
public string ResolvedPath { get; private set; } = string.Empty;
public string[] GamePaths { get; set; } = Array.Empty<string>();
[JsonProperty]
public string ResolvedPath { get; set; } = string.Empty;
[JsonProperty]
public string Hash { get; set; } = string.Empty;
public bool IsInUse { get; set; } = false;
public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>();
[JsonProperty]
public string ImcData { get; set; } = string.Empty;
public bool HasFileReplacement => GamePath != ResolvedPath;
public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath;
public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed);
private Task? computationTask = null;
public FileReplacement(string gamePath, string penumbraDirectory)
public FileReplacement(string penumbraDirectory)
{
GamePath = gamePath;
this.penumbraDirectory = penumbraDirectory;
}
@@ -39,15 +39,7 @@ namespace MareSynchronos.Models
{
fileReplacement.IsInUse = true;
if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement)))
{
Associated.Add(fileReplacement);
}
}
public void SetGamePath(string path)
{
GamePath = path;
Associated.Add(fileReplacement);
}
public void SetResolvedPath(string path)
@@ -107,26 +99,16 @@ namespace MareSynchronos.Models
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()
{
StringBuilder builder = new();
builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}");
builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}");
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)
{
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ResolvedPath}");
builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}");
}
}
return builder.ToString();
@@ -137,7 +119,7 @@ namespace MareSynchronos.Models
if (obj == null) return true;
if (obj.GetType() == typeof(FileReplacement))
{
return Hash == ((FileReplacement)obj).Hash && GamePath == ((FileReplacement)obj).GamePath;
return Hash == ((FileReplacement)obj).Hash;
}
return base.Equals(obj);
@@ -148,8 +130,7 @@ namespace MareSynchronos.Models
int result = 13;
result *= 397;
result += Hash.GetHashCode();
result += GamePath.GetHashCode();
result += ImcData.GetHashCode();
result += ResolvedPath.GetHashCode();
return result;
}

View File

@@ -8,7 +8,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.Hooks;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState;
@@ -29,21 +28,19 @@ namespace MareSynchronos
private const string commandName = "/mare";
private readonly ClientState clientState;
private readonly Framework framework;
private readonly GameGui gameGui;
private readonly ObjectTable objectTable;
private readonly WindowSystem windowSystem;
private readonly ApiController apiController;
private CharacterManager? characterManager;
private IpcManager ipcManager;
public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager,
Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui)
Framework framework, ObjectTable objectTable, ClientState clientState)
{
this.PluginInterface = pluginInterface;
this.CommandManager = commandManager;
this.framework = framework;
this.objectTable = objectTable;
this.clientState = clientState;
this.gameGui = gameGui;
Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
Configuration.Initialize(this.PluginInterface);
@@ -94,8 +91,8 @@ namespace MareSynchronos
}
characterManager = new CharacterManager(
new DrawHooks(PluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui),
clientState, framework, apiController, objectTable, ipcManager);
clientState, framework, apiController, objectTable, ipcManager, new FileReplacementFactory(ipcManager));
characterManager.StartWatchingPlayer();
ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString());
});
@@ -140,11 +137,14 @@ namespace MareSynchronos
File.Copy(fileCache.Filepath, newFilePath);
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
{
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)
{
if (args == "print")
{
characterManager?.PrintRequestedResources();
}
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")
@@ -199,7 +206,7 @@ namespace MareSynchronos
Description = "Mare Synchronous Test Mod Export",
};
var resources = characterManager!.GetCharacterCache();
var resources = characterManager!.BuildCharacterCache();
var metaJson = JsonConvert.SerializeObject(meta);
File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson);