diff --git a/MareAPI b/MareAPI index af8516d..a649b36 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit af8516d44a99d3a46e2a0de36c848732937f9c6a +Subproject commit a649b364955ce431f41c53c016823763febde708 diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index 9ad813a..6d5a2a3 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -7,6 +7,7 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using MareSynchronos.API; using MareSynchronos.Interop; using MareSynchronos.Managers; using MareSynchronos.Models; @@ -16,6 +17,7 @@ using Penumbra.Interop.Structs; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; namespace MareSynchronos.Factories; + public class CharacterDataFactory { private readonly DalamudUtil _dalamudUtil; @@ -29,23 +31,31 @@ public class CharacterDataFactory _ipcManager = ipcManager; } - public CharacterData? BuildCharacterData() + public CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer) { if (!_ipcManager.Initialized) { throw new ArgumentException("Penumbra is not connected"); } + if (playerPointer == IntPtr.Zero) + { + Logger.Verbose("Pointer was zero for " + objectKind); + previousData.FileReplacements.Remove(objectKind); + previousData.GlamourerString.Remove(objectKind); + return previousData; + } + try { - return CreateCharacterData(); + return CreateCharacterData(previousData, objectKind, playerPointer); } catch (Exception e) { - Logger.Warn("Failed to create character data"); + Logger.Warn("Failed to create " + objectKind + " data"); Logger.Warn(e.Message); Logger.Warn(e.StackTrace ?? string.Empty); - return null; + return previousData; } } @@ -54,16 +64,15 @@ public class CharacterDataFactory return (string.Join("", Enumerable.Repeat("\t", inheritanceLevel)), string.Join("", Enumerable.Repeat("\t", inheritanceLevel + 2))); } - private void DebugPrint(FileReplacement fileReplacement, string objectKind, string resourceType, int inheritanceLevel) + private void DebugPrint(FileReplacement fileReplacement, ObjectKind objectKind, string resourceType, int inheritanceLevel) { var indentation = GetIndentationForInheritanceLevel(inheritanceLevel); - objectKind += string.IsNullOrEmpty(objectKind) ? "" : " "; Logger.Verbose(indentation.Item1 + objectKind + resourceType + " [" + string.Join(", ", fileReplacement.GamePaths) + "]"); Logger.Verbose(indentation.Item2 + "=> " + fileReplacement.ResolvedPath); } - private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, CharacterData cache, int inheritanceLevel = 0, string objectKind = "") + private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) { if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) { @@ -85,18 +94,18 @@ public class CharacterDataFactory FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath); DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel); - cache.AddFileReplacement(mdlFileReplacement); + cache.AddFileReplacement(objectKind, mdlFileReplacement); for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) { var mtrl = (Material*)mdl->Materials[mtrlIdx]; if (mtrl == null) continue; - AddReplacementsFromMaterial(mtrl, cache, inheritanceLevel + 1, objectKind); + AddReplacementsFromMaterial(mtrl, objectKind, cache, inheritanceLevel + 1); } } - private unsafe void AddReplacementsFromMaterial(Material* mtrl, CharacterData cache, int inheritanceLevel = 0, string objectKind = "") + private unsafe void AddReplacementsFromMaterial(Material* mtrl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) { string fileName; try @@ -116,7 +125,7 @@ public class CharacterDataFactory var mtrlFileReplacement = CreateFileReplacement(mtrlPath); DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel); - cache.AddFileReplacement(mtrlFileReplacement); + cache.AddFileReplacement(objectKind, mtrlFileReplacement); var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) @@ -125,11 +134,11 @@ public class CharacterDataFactory if (string.IsNullOrEmpty(texPath)) continue; - AddReplacementsFromTexture(texPath, cache, inheritanceLevel + 1, objectKind); + AddReplacementsFromTexture(texPath, objectKind, cache, inheritanceLevel + 1); } } - private void AddReplacementsFromTexture(string texPath, CharacterData cache, int inheritanceLevel = 0, string objectKind = "", bool doNotReverseResolve = true) + private void AddReplacementsFromTexture(string texPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = true) { if (texPath.IsNullOrEmpty()) return; @@ -138,7 +147,7 @@ public class CharacterDataFactory var texFileReplacement = CreateFileReplacement(texPath, doNotReverseResolve); DebugPrint(texFileReplacement, objectKind, "Texture", inheritanceLevel); - cache.AddFileReplacement(texFileReplacement); + cache.AddFileReplacement(objectKind, texFileReplacement); if (texPath.Contains("/--")) return; @@ -147,25 +156,33 @@ public class CharacterDataFactory DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel); - cache.AddFileReplacement(texDx11Replacement); + cache.AddFileReplacement(objectKind, texDx11Replacement); } - private unsafe CharacterData CreateCharacterData() + private unsafe CharacterData CreateCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer) { Stopwatch st = Stopwatch.StartNew(); - while (!_dalamudUtil.IsPlayerPresent) + var chara = _dalamudUtil.CreateGameObject(charaPointer)!; + while (!_dalamudUtil.IsObjectPresent(chara)) { Logger.Verbose("Character is null but it shouldn't be, waiting"); Thread.Sleep(50); } - _dalamudUtil.WaitWhileCharacterIsDrawing(_dalamudUtil.PlayerPointer); - var cache = new CharacterData - { - GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(_dalamudUtil.PlayerCharacter), - ManipulationString = _ipcManager.PenumbraGetMetaManipulations() - }; + _dalamudUtil.WaitWhileCharacterIsDrawing(charaPointer); - var human = (Human*)((Character*)_dalamudUtil.PlayerPointer)->GameObject.GetDrawObject(); + if (previousData.FileReplacements.ContainsKey(objectKind)) + { + previousData.FileReplacements[objectKind].Clear(); + } + + previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); + + if (objectKind is not ObjectKind.Mount) + { + previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(chara); + } + + var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) { var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; @@ -174,63 +191,50 @@ public class CharacterDataFactory continue; } - AddReplacementsFromRenderModel(mdl, cache, 0, "Character"); + AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); } - var weaponObject = (Weapon*)((Object*)human)->ChildObject; - - if ((IntPtr)weaponObject != IntPtr.Zero) + if (objectKind == ObjectKind.Player) { - var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; + var weaponObject = (Weapon*)((Object*)human)->ChildObject; - AddReplacementsFromRenderModel(mainHandWeapon, cache, 0, "Weapon"); - - if (weaponObject->NextSibling != (IntPtr)weaponObject) + if ((IntPtr)weaponObject != IntPtr.Zero) { - var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; - AddReplacementsFromRenderModel(offHandWeapon, cache, 1, "OffHand Weapon"); - } - } + AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0); - AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), cache, 0, "Decal", false); - AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), cache, 0, "Legacy Decal", false); - AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, cache); - - var minion = ((Character*)_dalamudUtil.PlayerPointer)->CompanionObject; - if (minion != null) - { - var minionDrawObj = ((CharacterBase*)minion->Character.GameObject.GetDrawObject()); - for (var mdlIdx = 0; mdlIdx < minionDrawObj->SlotCount; mdlIdx++) - { - var mdl = (RenderModel*)minionDrawObj->ModelArray[mdlIdx]; - if (mdl == null || mdl->ResourceHandle == null) + if (weaponObject->NextSibling != (IntPtr)weaponObject) { - continue; - } + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; - AddReplacementsFromRenderModel(mdl, cache, 0, "Companion"); + AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1); + } } + + AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, objectKind, previousData); + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), objectKind, previousData, 0, false); + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), objectKind, previousData, 0, false); } st.Stop(); - Logger.Verbose("Building Character Data took " + st.Elapsed); + Logger.Verbose("Building " + objectKind + " Data took " + st.Elapsed); - return cache; + return previousData; } - private void AddReplacementSkeleton(ushort raceSexId, CharacterData cache) + private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache) { string raceSexIdString = raceSexId.ToString("0000"); string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; - Logger.Verbose("Adding File Replacement for Skeleton " + skeletonPath); + //Logger.Verbose("Adding File Replacement for Skeleton " + skeletonPath); var replacement = CreateFileReplacement(skeletonPath, true); - cache.AddFileReplacement(replacement); + cache.AddFileReplacement(objectKind, replacement); - DebugPrint(replacement, "Skeleton", "SKLB", 0); + DebugPrint(replacement, objectKind, "SKLB", 0); } private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false) diff --git a/MareSynchronos/Interop/Weapon.cs b/MareSynchronos/Interop/Weapon.cs index 0588559..5670471 100644 --- a/MareSynchronos/Interop/Weapon.cs +++ b/MareSynchronos/Interop/Weapon.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Interop.Structs; @@ -27,4 +28,11 @@ namespace MareSynchronos.Interop [FieldOffset(0x9E8)] public ResourceHandle* Decal; [FieldOffset(0x9F0)] public ResourceHandle* LegacyBodyDecal; } + + [StructLayout(LayoutKind.Explicit)] + public unsafe struct CharaExt + { + [FieldOffset(0x0)] public Character Character; + [FieldOffset(0x650)] public Character* Mount; + } } diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index dbb164b..ccbdf3a 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -8,9 +8,9 @@ using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.Game.Character; using MareSynchronos.API; using MareSynchronos.FileCacheDB; +using MareSynchronos.Interop; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -using MareSynchronos.WebAPI.Utils; using Penumbra.GameData.ByteString; using Penumbra.GameData.Structs; @@ -42,24 +42,23 @@ public class CachedPlayer } private bool _isDisposed = false; - private CancellationTokenSource? _downloadCancellationTokenSource; + private CancellationTokenSource? _downloadCancellationTokenSource = new(); private string _lastGlamourerData = string.Empty; private string _originalGlamourerData = string.Empty; - public PlayerCharacter? PlayerCharacter { get; set; } + public Dalamud.Game.ClientState.Objects.Types.Character? PlayerCharacter { get; set; } public string? PlayerName { get; private set; } public string PlayerNameHash { get; } - private string _lastAppliedEquipmentHash = string.Empty; public bool RequestedPenumbraRedraw { get; set; } public bool WasVisible { get; private set; } - private readonly Dictionary _cache = new(); + private CharacterCacheDto _cachedData = new(); private CharacterEquipment? _currentCharacterEquipment; @@ -68,43 +67,37 @@ public class CachedPlayer Logger.Debug("Received data for " + this); Logger.Debug("Checking for files to download for player " + PlayerName); - Logger.Debug("Hash for data is " + characterData.Hash); - if (!_cache.ContainsKey(characterData.Hash)) - { - Logger.Debug("Received total " + characterData.FileReplacements.Count + " file replacement data"); - _cache[characterData.Hash] = characterData; - } - else - { - Logger.Debug("Had valid local cache for " + PlayerName); - } - _lastAppliedEquipmentHash = characterData.Hash; + Logger.Debug("Hash for data is " + characterData.GetHashCode()); - DownloadAndApplyCharacter(); + if (characterData.GetHashCode() == _cachedData.GetHashCode()) return; + + List charaDataToUpdate = new List(); + foreach (var objectKind in Enum.GetValues()) + { + bool doesntContainKey = !_cachedData.FileReplacements.ContainsKey(objectKind) + || (_cachedData.FileReplacements.ContainsKey(objectKind) && !characterData.FileReplacements.ContainsKey(objectKind)); + if (doesntContainKey) + { + charaDataToUpdate.Add(objectKind); + continue; + } + + bool listsAreEqual = Enumerable.SequenceEqual(_cachedData.FileReplacements[objectKind], characterData.FileReplacements[objectKind]); + bool glamourerDataDifferent = _cachedData.GlamourerData[objectKind] != characterData.GlamourerData[objectKind]; + if (!listsAreEqual || glamourerDataDifferent) + { + Logger.Debug("Updating " + objectKind); + + charaDataToUpdate.Add(objectKind); + } + } + + _cachedData = characterData; + + DownloadAndApplyCharacter(charaDataToUpdate); } - private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) - { - if (string.IsNullOrEmpty(PlayerName) || e.CharacterNameHash != PlayerNameHash) return; - Logger.Debug("Received data for " + this); - - Logger.Debug("Checking for files to download for player " + PlayerName); - Logger.Debug("Hash for data is " + e.CharacterData.Hash); - if (!_cache.ContainsKey(e.CharacterData.Hash)) - { - Logger.Debug("Received total " + e.CharacterData.FileReplacements.Count + " file replacement data"); - _cache[e.CharacterData.Hash] = e.CharacterData; - } - else - { - Logger.Debug("Had valid local cache for " + PlayerName); - } - _lastAppliedEquipmentHash = e.CharacterData.Hash; - - DownloadAndApplyCharacter(); - } - - private void DownloadAndApplyCharacter() + private void DownloadAndApplyCharacter(List objectKind) { _downloadCancellationTokenSource?.Cancel(); _downloadCancellationTokenSource = new CancellationTokenSource(); @@ -116,9 +109,9 @@ public class CachedPlayer Dictionary moddedPaths; int attempts = 0; - while ((toDownloadReplacements = TryCalculateModdedDictionary(_cache[_lastAppliedEquipmentHash], out moddedPaths)).Count > 0 && attempts++ <= 10) + while ((toDownloadReplacements = TryCalculateModdedDictionary(out moddedPaths)).Count > 0 && attempts++ <= 10) { - Logger.Debug("Downloading missing files for player " + PlayerName); + Logger.Debug("Downloading missing files for player " + PlayerName + ", kind: " + objectKind); await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken); if (downloadToken.IsCancellationRequested) { @@ -126,7 +119,7 @@ public class CachedPlayer return; } - if ((TryCalculateModdedDictionary(_cache[_lastAppliedEquipmentHash], out moddedPaths)).All(c => _apiController.ForbiddenTransfers.Any(f => f.Hash == c.Hash))) + if ((TryCalculateModdedDictionary(out moddedPaths)).All(c => _apiController.ForbiddenTransfers.Any(f => f.Hash == c.Hash))) { break; } @@ -142,7 +135,12 @@ public class CachedPlayer } } - ApplyCharacterData(_cache[_lastAppliedEquipmentHash], moddedPaths); + ApplyBaseData(moddedPaths); + + foreach (var kind in objectKind) + { + ApplyCustomizationData(kind); + } }, downloadToken).ContinueWith(task => { if (!task.IsCanceled) return; @@ -152,22 +150,21 @@ public class CachedPlayer }); } - private List TryCalculateModdedDictionary(CharacterCacheDto cache, - out Dictionary moddedDictionary) + private List TryCalculateModdedDictionary(out Dictionary moddedDictionary) { List missingFiles = new(); moddedDictionary = new Dictionary(); try { using var db = new FileCacheContext(); - foreach (var item in cache.FileReplacements) + foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value).ToList()) { foreach (var gamePath in item.GamePaths) { var fileCache = db.FileCaches.FirstOrDefault(f => f.Hash == item.Hash); if (fileCache != null) { - moddedDictionary.Add(gamePath, fileCache.Filepath); + moddedDictionary[gamePath] = fileCache.Filepath; } else { @@ -184,24 +181,102 @@ public class CachedPlayer return missingFiles; } - private unsafe void ApplyCharacterData(CharacterCacheDto cache, Dictionary moddedPaths) + private void ApplyBaseData(Dictionary moddedPaths) { - if (PlayerCharacter is null) return; _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName!); var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(PlayerName!); - _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter!.Address); - RequestedPenumbraRedraw = true; - Logger.Debug( - $"Request Redraw for {PlayerName}"); - _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, cache.ManipulationData); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, PlayerCharacter!); - var minion = ((Character*)PlayerCharacter.Address)->CompanionObject; - if (minion != null) + _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, _cachedData.ManipulationData); + } + + private unsafe void ApplyCustomizationData(ObjectKind objectKind) + { + if (PlayerCharacter is null) return; + + if (objectKind == ObjectKind.Player) { - var compName = new Utf8String(minion->Character.GameObject.GetName()).ToString(); - if (compName != null) + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter!.Address); + RequestedPenumbraRedraw = true; + Logger.Debug( + $"Request Redraw for {PlayerName}"); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], PlayerCharacter!); + } + else if (objectKind == ObjectKind.Minion) + { + var minion = ((Character*)PlayerCharacter.Address)->CompanionObject; + if (minion != null) { - _ipcManager.PenumbraRedraw(compName); + Logger.Debug($"Request Redraw for Minion"); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], obj: (IntPtr)minion); + } + } + else if (objectKind == ObjectKind.Pet) + { + var pet = _dalamudUtil.GetPet(PlayerCharacter.Address); + if (pet != IntPtr.Zero) + { + Logger.Debug("Request Redraw for Pet"); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], pet); + } + } + else if (objectKind == ObjectKind.Companion) + { + var companion = _dalamudUtil.GetCompanion(PlayerCharacter.Address); + if (companion != IntPtr.Zero) + { + Logger.Debug("Request Redraw for Companion"); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], companion); + } + } + else if (objectKind == ObjectKind.Mount) + { + var mount = ((CharaExt*)PlayerCharacter.Address)->Mount; + if (mount != null) + { + Logger.Debug($"Request Redraw for Mount"); + _ipcManager.PenumbraRedraw((IntPtr)mount); + } + } + } + + private unsafe void RevertCustomizationData(ObjectKind objectKind) + { + if (PlayerCharacter is null) return; + + if (objectKind == ObjectKind.Player) + { + _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter); + _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter); + } + else if (objectKind == ObjectKind.Minion) + { + var minion = ((Character*)PlayerCharacter.Address)->CompanionObject; + if (minion != null) + { + _ipcManager.PenumbraRedraw((IntPtr)minion); + } + } + else if (objectKind == ObjectKind.Pet) + { + var pet = _dalamudUtil.GetPet(PlayerCharacter.Address); + if (pet != IntPtr.Zero) + { + _ipcManager.PenumbraRedraw(pet); + } + } + else if (objectKind == ObjectKind.Companion) + { + var companion = _dalamudUtil.GetCompanion(PlayerCharacter.Address); + if (companion != IntPtr.Zero) + { + _ipcManager.PenumbraRedraw(companion); + } + } + else if (objectKind == ObjectKind.Mount) + { + var mount = ((CharaExt*)PlayerCharacter.Address)->Mount; + if (mount != null) + { + _ipcManager.PenumbraRedraw((IntPtr)mount); } } } @@ -217,15 +292,15 @@ public class CachedPlayer Logger.Verbose("Restoring state for " + PlayerName); _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; - _apiController.CharacterReceived -= ApiControllerOnCharacterReceived; _downloadCancellationTokenSource?.Cancel(); _downloadCancellationTokenSource?.Dispose(); - _downloadCancellationTokenSource = null; _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); if (PlayerCharacter != null && PlayerCharacter.IsValid()) { - _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter); - _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter); + foreach (var item in _cachedData.FileReplacements) + { + RevertCustomizationData(item.Key); + } } } catch (Exception ex) @@ -283,9 +358,9 @@ public class CachedPlayer private Task? _penumbraRedrawEventTask; - private void IpcManagerOnPenumbraRedrawEvent(object? sender, EventArgs e) + private void IpcManagerOnPenumbraRedrawEvent(IntPtr address, int idx) { - var player = _dalamudUtil.GetPlayerCharacterFromObjectTableByIndex((int)sender!); + var player = _dalamudUtil.GetCharacterFromObjectTableByIndex(idx); if (player == null || player.Name.ToString() != PlayerName) return; if (!_penumbraRedrawEventTask?.IsCompleted ?? false) return; @@ -294,10 +369,10 @@ public class CachedPlayer PlayerCharacter = player; _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter.Address); - if (RequestedPenumbraRedraw == false && !string.IsNullOrEmpty(_lastAppliedEquipmentHash)) + if (RequestedPenumbraRedraw == false) { Logger.Warn("Unauthorized character change detected"); - DownloadAndApplyCharacter(); + ApplyCustomizationData(ObjectKind.Player); } else { diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 22598bd..1f9cb1c 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -8,6 +8,7 @@ using MareSynchronos.WebAPI; namespace MareSynchronos.Managers { + public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); public class IpcManager : IDisposable { private readonly ICallGateSubscriber _glamourerApiVersion; @@ -23,13 +24,16 @@ namespace MareSynchronos.Managers private readonly ICallGateSubscriber _penumbraDispose; private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; private readonly ICallGateSubscriber? _penumbraRedraw; + private readonly ICallGateSubscriber? _penumbraRedrawObject; private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; private readonly ICallGateSubscriber? _penumbraResolveModDir; private readonly ICallGateSubscriber? _penumbraResolvePlayer; private readonly ICallGateSubscriber? _reverseResolvePlayer; private readonly ICallGateSubscriber, string, int, int> _penumbraSetTemporaryMod; - public IpcManager(DalamudPluginInterface pi) + private readonly DalamudUtil _dalamudUtil; + + public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) { Logger.Verbose("Creating " + nameof(IpcManager)); @@ -38,6 +42,7 @@ namespace MareSynchronos.Managers _penumbraResolvePlayer = pi.GetIpcSubscriber("Penumbra.ResolvePlayerPath"); _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); + _penumbraRedrawObject = pi.GetIpcSubscriber("Penumbra.RedrawObject"); _reverseResolvePlayer = pi.GetIpcSubscriber("Penumbra.ReverseResolvePlayerPath"); _penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions"); _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); @@ -69,11 +74,13 @@ namespace MareSynchronos.Managers { PenumbraInitialized?.Invoke(); } + + this._dalamudUtil = dalamudUtil; } public event VoidDelegate? PenumbraInitialized; public event VoidDelegate? PenumbraDisposed; - public event EventHandler? PenumbraRedrawEvent; + public event PenumbraRedrawEvent? PenumbraRedrawEvent; public bool Initialized => CheckPenumbraApi(); public bool CheckGlamourerApi() @@ -92,7 +99,7 @@ namespace MareSynchronos.Managers { try { - return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >=11 }; + return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 11 }; } catch { @@ -109,10 +116,19 @@ namespace MareSynchronos.Managers _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); } + public void GlamourerApplyAll(string customization, IntPtr obj) + { + if (!CheckGlamourerApi()) return; + var gameObj = _dalamudUtil.CreateGameObject(obj); + if (gameObj != null) + { + _glamourerApplyAll!.InvokeAction(customization, gameObj); + } + } + public void GlamourerApplyAll(string customization, GameObject character) { if (!CheckGlamourerApi()) return; - Logger.Verbose("Glamourer apply all to " + character); _glamourerApplyAll!.InvokeAction(customization, character); } @@ -133,7 +149,12 @@ namespace MareSynchronos.Managers public string GlamourerGetCharacterCustomization(GameObject character) { if (!CheckGlamourerApi()) return string.Empty; - return _glamourerGetAllCustomization!.InvokeFunc(character); + var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(character); + byte[] bytes = Convert.FromBase64String(glamourerString); + // ignore transparency + bytes[88] = 128; + bytes[89] = 63; + return Convert.ToBase64String(bytes); } public void GlamourerRevertCharacterCustomization(GameObject character) @@ -162,6 +183,16 @@ namespace MareSynchronos.Managers return _penumbraResolveModDir!.InvokeFunc(); } + public void PenumbraRedraw(IntPtr obj) + { + if (!CheckPenumbraApi()) return; + var gameObj = _dalamudUtil.CreateGameObject(obj); + if (gameObj != null) + { + _penumbraRedrawObject!.InvokeAction(gameObj, 0); + } + } + public void PenumbraRedraw(string actorName) { if (!CheckPenumbraApi()) return; @@ -179,7 +210,7 @@ namespace MareSynchronos.Managers { if (!CheckPenumbraApi()) return null; var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); - Logger.Verbose("Resolved " + path + "=>" + string.Join(", ", resolvedPath)); + //Logger.Verbose("Resolved " + path + "=>" + string.Join(", ", resolvedPath)); return resolvedPath; } @@ -191,7 +222,7 @@ namespace MareSynchronos.Managers { resolvedPaths = new[] { path }; } - Logger.Verbose("Reverse Resolved " + path + "=>" + string.Join(", ", resolvedPaths)); + //Logger.Verbose("Reverse Resolved " + path + "=>" + string.Join(", ", resolvedPaths)); return resolvedPaths; } @@ -209,7 +240,7 @@ namespace MareSynchronos.Managers private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) { - PenumbraRedrawEvent?.Invoke(objectTableIndex, EventArgs.Empty); + PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); } private void PenumbraInit() diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index f2ecb60..d20575b 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -250,12 +250,11 @@ public class OnlinePlayerManager : IDisposable private void PushCharacterData(List visiblePlayers) { - if (visiblePlayers.Any() && _playerManager.LastSentCharacterData != null) + if (visiblePlayers.Any() && _playerManager.LastCreatedCharacterData != null) { Task.Run(async () => { - Logger.Verbose(JsonConvert.SerializeObject(_playerManager.LastSentCharacterData, Formatting.Indented)); - await _apiController.PushCharacterData(_playerManager.LastSentCharacterData, + await _apiController.PushCharacterData(_playerManager.LastCreatedCharacterData!, visiblePlayers); }); } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index a433022..01b7b16 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -1,15 +1,16 @@ using MareSynchronos.Factories; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -using Newtonsoft.Json; using System; using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; using Penumbra.GameData.Structs; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.GameData.ByteString; +using System.Collections.Generic; +using System.Linq; +using MareSynchronos.Models; +using MareSynchronos.Interop; namespace MareSynchronos.Managers { @@ -22,15 +23,17 @@ namespace MareSynchronos.Managers private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; public event PlayerHasChanged? PlayerHasChanged; - public bool SendingData { get; private set; } - public CharacterCacheDto? LastSentCharacterData { get; private set; } + public CharacterCacheDto? LastCreatedCharacterData { get; private set; } + public CharacterData PermanentDataCache { get; private set; } = new(); + private readonly Dictionary> objectKindsToUpdate = new(); - private CancellationTokenSource? _playerChangedCts; + private CancellationTokenSource? _playerChangedCts = new(); private DateTime _lastPlayerObjectCheck; - private CharacterEquipment? _currentCharacterEquipment; - private string _lastMinionName = string.Empty; + private CharacterEquipment? _currentCharacterEquipment = new(); - public PlayerManager(ApiController apiController, IpcManager ipcManager, + private List playerAttachedObjects = new List(); + + public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) { Logger.Verbose("Creating " + nameof(PlayerManager)); @@ -42,12 +45,22 @@ namespace MareSynchronos.Managers _apiController.Connected += ApiControllerOnConnected; _apiController.Disconnected += ApiController_Disconnected; + _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); if (_apiController.IsConnected) { ApiControllerOnConnected(); } + + playerAttachedObjects = new List() + { + new PlayerOrRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), + new PlayerOrRelatedObject(ObjectKind.Minion, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), + new PlayerOrRelatedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), + new PlayerOrRelatedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), + new PlayerOrRelatedObject(ObjectKind.Mount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((CharaExt*)_dalamudUtil.PlayerPointer)->Mount), + }; } public void Dispose() @@ -63,22 +76,14 @@ namespace MareSynchronos.Managers private unsafe void DalamudUtilOnFrameworkUpdate() { - if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized || !_apiController.IsConnected) return; + if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(0.25)) return; - var minion = ((Character*)_dalamudUtil.PlayerPointer)->CompanionObject; - string minionName = ""; - if (minion != null) + playerAttachedObjects.ForEach(k => k.CheckAndUpdateObject()); + if (playerAttachedObjects.Any(c => c.HasUnprocessedUpdate && !c.IsProcessing)) { - minionName = new Utf8String(minion->Character.GameObject.GetName()).ToString(); - } - - if (_dalamudUtil.IsPlayerPresent - && (!_currentCharacterEquipment!.CompareAndUpdate(_dalamudUtil.PlayerCharacter) || minionName != _lastMinionName)) - { - _lastMinionName = minionName; - OnPlayerChanged(); + OnPlayerOrAttachedObjectsChanged(); } _lastPlayerObjectCheck = DateTime.Now; @@ -89,10 +94,6 @@ namespace MareSynchronos.Managers Logger.Debug("ApiController Connected"); _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - - _currentCharacterEquipment = new CharacterEquipment(_dalamudUtil.PlayerCharacter); - PlayerChanged(); } private void ApiController_Disconnected() @@ -100,47 +101,60 @@ namespace MareSynchronos.Managers Logger.Debug(nameof(ApiController_Disconnected)); _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; - LastSentCharacterData = null; } - private async Task CreateFullCharacterCache(CancellationToken token) + private async Task CreateFullCharacterCacheDto(CancellationToken token) { - var cache = _characterDataFactory.BuildCharacterData(); - if (cache == null) return null; - CharacterCacheDto? cacheDto = null; - - await Task.Run(async () => + foreach (var unprocessedObject in playerAttachedObjects.Where(c => c.HasUnprocessedUpdate).ToList()) { - while (!cache.IsReady && !token.IsCancellationRequested) - { - await Task.Delay(50, token); - } + Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); + PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject.ObjectKind, unprocessedObject.Address); + unprocessedObject.HasUnprocessedUpdate = false; + unprocessedObject.IsProcessing = false; + token.ThrowIfCancellationRequested(); + } - if (token.IsCancellationRequested) return; + while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) + { + await Task.Delay(50, token); + } - cacheDto = cache.ToCharacterCacheDto(); - var json = JsonConvert.SerializeObject(cacheDto); + if (token.IsCancellationRequested) return null; - cacheDto.Hash = Crypto.GetHash(json); - }, token); + Logger.Verbose("Cache creation complete"); - return cacheDto; + return PermanentDataCache.ToCharacterCacheDto(); } - private void IpcManager_PenumbraRedrawEvent(object? objectTableIndex, EventArgs e) + private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) { - var player = _dalamudUtil.GetPlayerCharacterFromObjectTableByIndex((int)objectTableIndex!); - if (player != null && player.Name.ToString() != _dalamudUtil.PlayerName) return; - Logger.Debug("Penumbra Redraw Event for " + _dalamudUtil.PlayerName); - PlayerChanged(); + Logger.Verbose("RedrawEvent for addr " + address); + + foreach (var item in playerAttachedObjects) + { + if (address == item.Address) + { + Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); + item.HasUnprocessedUpdate = true; + } + } + + if (playerAttachedObjects.Any(c => c.HasUnprocessedUpdate)) + { + OnPlayerOrAttachedObjectsChanged(); + } } - private void PlayerChanged() + private void OnPlayerOrAttachedObjectsChanged() { if (_dalamudUtil.IsInGpose) return; - Logger.Debug("Player changed: " + _dalamudUtil.PlayerName); + var unprocessedObjects = playerAttachedObjects.Where(c => c.HasUnprocessedUpdate); + foreach (var unprocessedObject in unprocessedObjects) + { + unprocessedObject.IsProcessing = true; + } + Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); _playerChangedCts?.Cancel(); _playerChangedCts = new CancellationTokenSource(); var token = _playerChangedCts.Token; @@ -166,44 +180,27 @@ namespace MareSynchronos.Managers Task.Run(async () => { - SendingData = true; - int attempts = 0; - while (!_apiController.IsConnected && attempts < 10 && !token.IsCancellationRequested) - { - Logger.Warn("No connection to the API"); - await Task.Delay(TimeSpan.FromSeconds(1), token); - attempts++; - } - - if (attempts == 10 || token.IsCancellationRequested) return; - _dalamudUtil.WaitWhileSelfIsDrawing(token); - var characterCache = (await CreateFullCharacterCache(token)); + CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); + if (cacheDto == null || token.IsCancellationRequested) return; - if (characterCache == null || token.IsCancellationRequested) return; - - if (characterCache.Hash == (LastSentCharacterData?.Hash ?? "-")) + if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) { Logger.Debug("Not sending data, already sent"); return; } + else + { + LastCreatedCharacterData = cacheDto; + } - LastSentCharacterData = characterCache; - PlayerHasChanged?.Invoke(characterCache); - SendingData = false; + if (_apiController.IsConnected) + { + Logger.Verbose("Invoking PlayerHasChanged"); + PlayerHasChanged?.Invoke(cacheDto); + } }, token); } - - private void OnPlayerChanged() - { - Task.Run(() => - { - Logger.Debug("Watcher: PlayerChanged"); - PlayerChanged(); - }); - } - - } } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index b050221..b5af78d 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 0.1.18.0 + 0.2.0.0 https://github.com/Penumbra-Sync/client diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index b46b0cd..8075aac 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -9,27 +9,31 @@ namespace MareSynchronos.Models [JsonObject(MemberSerialization.OptIn)] public class CharacterData { - public List FileReplacements { get; set; } = new(); + [JsonProperty] + public Dictionary> FileReplacements { get; set; } = new(); [JsonProperty] - public string GlamourerString { get; set; } = string.Empty; + public Dictionary GlamourerString { get; set; } = new(); - public bool IsReady => FileReplacements.All(f => f.Computed); + public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); + [JsonProperty] public string ManipulationString { get; set; } = string.Empty; - public void AddFileReplacement(FileReplacement fileReplacement) + public void AddFileReplacement(ObjectKind objectKind, FileReplacement fileReplacement) { if (!fileReplacement.HasFileReplacement) return; - var existingReplacement = FileReplacements.SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); + if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); + + var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); if (existingReplacement != null) { existingReplacement.GamePaths.AddRange(fileReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e))); } else { - FileReplacements.Add(fileReplacement); + FileReplacements[objectKind].Add(fileReplacement); } } @@ -37,14 +41,14 @@ namespace MareSynchronos.Models { return new CharacterCacheDto() { - FileReplacements = FileReplacements.Where(f => f.HasFileReplacement).GroupBy(f => f.Hash).Select(g => + FileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement).GroupBy(f => f.Hash).Select(g => { return new FileReplacementDto() { GamePaths = g.SelectMany(g => g.GamePaths).Distinct().ToArray(), Hash = g.First().Hash }; - }).ToList(), + }).ToList()), GlamourerData = GlamourerString, ManipulationData = ManipulationString }; @@ -53,7 +57,7 @@ namespace MareSynchronos.Models public override string ToString() { StringBuilder stringBuilder = new(); - foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePaths[0])) + foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths[0])) { stringBuilder.AppendLine(fileReplacement.ToString()); } diff --git a/MareSynchronos/Models/PlayerAttachedObject.cs b/MareSynchronos/Models/PlayerAttachedObject.cs new file mode 100644 index 0000000..87d06e5 --- /dev/null +++ b/MareSynchronos/Models/PlayerAttachedObject.cs @@ -0,0 +1,100 @@ +using System; +using MareSynchronos.API; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using System.Runtime.InteropServices; +using MareSynchronos.Utils; +using Penumbra.GameData.ByteString; + +namespace MareSynchronos.Models +{ + internal class PlayerOrRelatedObject + { + private readonly Func getAddress; + + public unsafe Character* Character => (Character*)Address; + + private string _name; + + public ObjectKind ObjectKind { get; } + public IntPtr Address { get; set; } + public IntPtr DrawObjectAddress { get; set; } + + private IntPtr CurrentAddress => getAddress.Invoke(); + + public PlayerOrRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + { + ObjectKind = objectKind; + Address = address; + DrawObjectAddress = drawObjectAddress; + this.getAddress = getAddress; + _name = string.Empty; + } + + public byte[] EquipSlotData { get; set; } = new byte[40]; + public byte[] CustomizeData { get; set; } = new byte[26]; + + public bool HasUnprocessedUpdate { get; set; } = false; + public bool IsProcessing { get; set; } = false; + + public unsafe void CheckAndUpdateObject() + { + var curPtr = CurrentAddress; + if (curPtr != IntPtr.Zero) + { + var chara = (Character*)curPtr; + bool addr = Address == IntPtr.Zero || Address != curPtr; + bool equip = CompareAndUpdateByteData(chara->EquipSlotData, chara->CustomizeData); + bool drawObj = (chara->GameObject.DrawObject != null && (IntPtr)chara->GameObject.DrawObject != DrawObjectAddress); + var name = new Utf8String(chara->GameObject.Name).ToString(); + bool nameChange = (name != _name); + if (addr || equip || drawObj || nameChange) + { + _name = name; + Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + curPtr + ", " + (IntPtr)chara->GameObject.DrawObject); + + Address = curPtr; + DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; + HasUnprocessedUpdate = true; + } + } + else + { + if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) + { + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + HasUnprocessedUpdate = true; + } + + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + } + } + + private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) + { + bool hasChanges = false; + for (int i = 0; i < EquipSlotData.Length; i++) + { + var data = Marshal.ReadByte((IntPtr)equipSlotData, i); + if (EquipSlotData[i] != data) + { + EquipSlotData[i] = data; + hasChanges = true; + } + } + + for (int i = 0; i < CustomizeData.Length; i++) + { + var data = Marshal.ReadByte((IntPtr)customizeData, i); + if (CustomizeData[i] != data) + { + CustomizeData[i] = data; + hasChanges = true; + } + } + + return hasChanges; + } + } +} diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 5388591..1502066 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -61,7 +61,7 @@ namespace MareSynchronos _dalamudUtil = new DalamudUtil(clientState, objectTable, framework); _apiController = new ApiController(_configuration, _dalamudUtil); - _ipcManager = new IpcManager(PluginInterface); + _ipcManager = new IpcManager(PluginInterface, _dalamudUtil); _fileCacheManager = new FileCacheManager(_ipcManager, _configuration); _fileDialogManager = new FileDialogManager(); diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index f110654..b8f2318 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -7,11 +7,12 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace MareSynchronos.Utils { - public delegate void PlayerChange(Character actor); + public delegate void PlayerChange(Dalamud.Game.ClientState.Objects.Types.Character actor); public delegate void LogIn(); public delegate void LogOut(); @@ -56,10 +57,39 @@ namespace MareSynchronos.Utils LogIn?.Invoke(); } + public Dalamud.Game.ClientState.Objects.Types.GameObject? CreateGameObject(IntPtr reference) + { + return _objectTable.CreateObjectReference(reference); + } + public bool IsLoggedIn => _clientState.IsLoggedIn; public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); + public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.GameObject? obj) + { + return obj != null && obj.IsValid(); + } + + public unsafe IntPtr GetMinion() + { + return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)PlayerPointer)->CompanionObject; + } + + public unsafe IntPtr GetPet(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + if (playerPointer == null) playerPointer = PlayerPointer; + return (IntPtr)mgr->LookupPetByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); + } + + public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) + { + var mgr = CharacterManager.Instance(); + if (playerPointer == null) playerPointer = PlayerPointer; + return (IntPtr)mgr->LookupBuddyByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); + } + public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; public IntPtr PlayerPointer => _clientState.LocalPlayer!.Address; @@ -77,11 +107,23 @@ namespace MareSynchronos.Utils obj.Name.ToString() != PlayerName).Select(p => (PlayerCharacter)p).ToList(); } - public PlayerCharacter? GetPlayerCharacterFromObjectTableByIndex(int index) + public Dalamud.Game.ClientState.Objects.Types.Character? GetCharacterFromObjectTableByIndex(int index) { var objTableObj = _objectTable[index]; if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; - return (PlayerCharacter)objTableObj; + return (Dalamud.Game.ClientState.Objects.Types.Character)objTableObj; + } + + internal unsafe int GetIdxBasedOnPtr(FFXIVClientStructs.FFXIV.Client.Game.Character.Character* pet) + { + var idx = 0; + foreach (var item in _objectTable) + { + if (item.Address == (IntPtr)pet) return idx; + idx++; + } + + return -1; } public PlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) @@ -95,6 +137,12 @@ namespace MareSynchronos.Utils return null; } + public unsafe void DebugPrintRenderFlags(IntPtr characterAddress) + { + var obj = (GameObject*)characterAddress; + Logger.Verbose("RenderFlags for " + characterAddress + ": " + Convert.ToString(obj->RenderFlags, 2)); + } + public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress, CancellationToken? ct = null) { if (!_clientState.IsLoggedIn) return; diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs index 2ae03d0..110de12 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -19,10 +19,10 @@ namespace MareSynchronos.WebAPI private int _downloadId = 0; public void CancelUpload() { - if (_uploadCancellationTokenSource != null) + if (_uploadToken != null) { - Logger.Warn("Cancelling upload"); - _uploadCancellationTokenSource?.Cancel(); + Logger.Debug("Cancelling upload"); + _uploadToken?.Cancel(); _mareHub!.SendAsync(Api.SendFileAbortUpload); CurrentUploads.Clear(); } @@ -40,7 +40,6 @@ namespace MareSynchronos.WebAPI await using var fs = File.OpenWrite(fileName); await foreach (var data in reader.WithCancellation(ct)) { - //Logger.Debug("Getting chunk of " + hash); CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred += data.Length; await fs.WriteAsync(data, ct); } @@ -124,11 +123,11 @@ namespace MareSynchronos.WebAPI Logger.Debug("Sending Character data to service " + ApiUri); CancelUpload(); - _uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = _uploadCancellationTokenSource.Token; + _uploadToken = new CancellationTokenSource(); + var uploadToken = _uploadToken.Token; Logger.Verbose("New Token Created"); - var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); + var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, character.FileReplacements.SelectMany(c => c.Value.Select(v => v.Hash)).Distinct(), uploadToken); foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) { @@ -164,7 +163,7 @@ namespace MareSynchronos.WebAPI var totalSize = CurrentUploads.Sum(c => c.Total); Logger.Verbose("Compressing and uploading files"); - foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred)) + foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) { Logger.Verbose("Compressing and uploading " + file); var data = await GetCompressedFileData(file.Hash, uploadToken); @@ -204,8 +203,8 @@ namespace MareSynchronos.WebAPI Logger.Warn("=== Upload operation was cancelled ==="); } - Logger.Verbose("Upload complete for " + character.Hash); - _uploadCancellationTokenSource = null; + Logger.Verbose("Upload complete for " + character.GetHashCode()); + _uploadToken = null; } private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs index 0019acb..7130caf 100644 --- a/MareSynchronos/WebAPI/ApiController.Connectivity.cs +++ b/MareSynchronos/WebAPI/ApiController.Connectivity.cs @@ -37,7 +37,7 @@ namespace MareSynchronos.WebAPI private HubConnection? _mareHub; - private CancellationTokenSource? _uploadCancellationTokenSource; + private CancellationTokenSource? _uploadToken = new(); private ConnectionDto? _connectionDto; public SystemInfoDto SystemInfoDto { get; private set; } = new(); @@ -268,7 +268,7 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); + _uploadToken?.Cancel(); Logger.Debug("Connection closed"); Disconnected?.Invoke(); return Task.CompletedTask; @@ -286,7 +286,7 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); + _uploadToken?.Cancel(); Logger.Debug("Connection closed... Reconnecting"); Disconnected?.Invoke(); return Task.CompletedTask;