From 9d9aac6bb374b71e0363f892813f2ca56272df4a Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Fri, 22 Jul 2022 02:11:35 +0200 Subject: [PATCH 1/3] minion/pet/companion/mount rework --- MareAPI | 2 +- .../Factories/CharacterDataFactory.cs | 55 ++++--- MareSynchronos/Interop/Weapon.cs | 8 + MareSynchronos/Managers/CachedPlayer.cs | 141 +++++++++-------- MareSynchronos/Managers/IpcManager.cs | 35 ++++- .../Managers/OnlinePlayerManager.cs | 21 ++- MareSynchronos/Managers/PlayerManager.cs | 145 +++++++++++++----- MareSynchronos/MareSynchronos.csproj | 2 +- MareSynchronos/Models/CharacterData.cs | 4 + MareSynchronos/Models/PlayerAttachedObject.cs | 57 +++++++ MareSynchronos/Plugin.cs | 2 +- MareSynchronos/Utils/DalamudUtil.cs | 54 ++++++- .../WebAPI/ApIController.Functions.Files.cs | 17 +- .../WebAPI/ApiController.Connectivity.cs | 12 +- 14 files changed, 403 insertions(+), 152 deletions(-) create mode 100644 MareSynchronos/Models/PlayerAttachedObject.cs diff --git a/MareAPI b/MareAPI index af8516d..ffc0a48 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit af8516d44a99d3a46e2a0de36c848732937f9c6a +Subproject commit ffc0a48fdadd71d33b015e4e46ef15303f1d2f60 diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index 9ad813a..c17c74c 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -16,6 +16,7 @@ using Penumbra.Interop.Structs; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; namespace MareSynchronos.Factories; + public class CharacterDataFactory { private readonly DalamudUtil _dalamudUtil; @@ -29,16 +30,21 @@ public class CharacterDataFactory _ipcManager = ipcManager; } - public CharacterData? BuildCharacterData() + public CharacterData? BuildCharacterData(IntPtr playerPointer) { if (!_ipcManager.Initialized) { throw new ArgumentException("Penumbra is not connected"); } + if (playerPointer == IntPtr.Zero) + { + return null; + } + try { - return CreateCharacterData(); + return CreateCharacterData(playerPointer); } catch (Exception e) { @@ -150,22 +156,32 @@ public class CharacterDataFactory cache.AddFileReplacement(texDx11Replacement); } - private unsafe CharacterData CreateCharacterData() + private unsafe CharacterData CreateCharacterData(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 + _dalamudUtil.WaitWhileCharacterIsDrawing(charaPointer); + var cache = new CharacterData() { - GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(_dalamudUtil.PlayerCharacter), - ManipulationString = _ipcManager.PenumbraGetMetaManipulations() + ManipulationString = _ipcManager.PenumbraGetMetaManipulations(), }; - var human = (Human*)((Character*)_dalamudUtil.PlayerPointer)->GameObject.GetDrawObject(); + try + { + cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(chara); + } + catch + { + // might not have glamourer data + } + + + var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) { var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; @@ -193,24 +209,15 @@ public class CharacterDataFactory } } - 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) + if (!string.IsNullOrEmpty(cache.GlamourerString)) { - var minionDrawObj = ((CharacterBase*)minion->Character.GameObject.GetDrawObject()); - for (var mdlIdx = 0; mdlIdx < minionDrawObj->SlotCount; mdlIdx++) + try { - var mdl = (RenderModel*)minionDrawObj->ModelArray[mdlIdx]; - if (mdl == null || mdl->ResourceHandle == null) - { - continue; - } - - AddReplacementsFromRenderModel(mdl, cache, 0, "Companion"); + AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, cache); + 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); } + catch { } } st.Stop(); 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..671899a 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 Dictionary _downloadCancellationTokenSources = 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 readonly Dictionary _cache = new(); private CharacterEquipment? _currentCharacterEquipment; @@ -69,46 +68,32 @@ public class CachedPlayer Logger.Debug("Checking for files to download for player " + PlayerName); Logger.Debug("Hash for data is " + characterData.Hash); - if (!_cache.ContainsKey(characterData.Hash)) + + if (!_cache.ContainsKey(characterData.ObjectKind)) + { + _cache.Add(characterData.ObjectKind, new()); + _downloadCancellationTokenSources.Add(characterData.ObjectKind, null); + } + + _cache.TryGetValue(characterData.ObjectKind, out var cachedDto); + if ((cachedDto?.Hash ?? string.Empty) != characterData.Hash) { Logger.Debug("Received total " + characterData.FileReplacements.Count + " file replacement data"); - _cache[characterData.Hash] = characterData; + _cache[characterData.ObjectKind] = characterData; } else { Logger.Debug("Had valid local cache for " + PlayerName); } - _lastAppliedEquipmentHash = characterData.Hash; - DownloadAndApplyCharacter(); + DownloadAndApplyCharacter(characterData.ObjectKind); } - private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) + private void DownloadAndApplyCharacter(ObjectKind objectKind) { - 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() - { - _downloadCancellationTokenSource?.Cancel(); - _downloadCancellationTokenSource = new CancellationTokenSource(); - var downloadToken = _downloadCancellationTokenSource.Token; + _downloadCancellationTokenSources[objectKind]?.Cancel(); + _downloadCancellationTokenSources[objectKind] = new CancellationTokenSource(); + var downloadToken = _downloadCancellationTokenSources[objectKind]!.Token; var downloadId = _apiController.GetDownloadId(); Task.Run(async () => { @@ -116,7 +101,7 @@ 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); await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken); @@ -126,7 +111,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 +127,7 @@ public class CachedPlayer } } - ApplyCharacterData(_cache[_lastAppliedEquipmentHash], moddedPaths); + ApplyCharacterData(objectKind, moddedPaths); }, downloadToken).ContinueWith(task => { if (!task.IsCanceled) return; @@ -152,22 +137,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 _cache.SelectMany(c => c.Value.FileReplacements).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 +168,57 @@ public class CachedPlayer return missingFiles; } - private unsafe void ApplyCharacterData(CharacterCacheDto cache, Dictionary moddedPaths) + private unsafe void ApplyCharacterData(ObjectKind objectKind, Dictionary moddedPaths) { if (PlayerCharacter is null) return; + var cache = _cache[objectKind]; + _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, _cache.First().Value.ManipulationData); + + 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(cache.GlamourerData, 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(cache.GlamourerData, 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(cache.GlamourerData, pet); + } + } + else if (objectKind == ObjectKind.Companion) + { + var companion = _dalamudUtil.GetCompanion(PlayerCharacter.Address); + if (companion != IntPtr.Zero) + { + Logger.Debug("Request Redraw for Companion"); + _ipcManager.GlamourerApplyAll(cache.GlamourerData, 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); } } } @@ -217,10 +234,11 @@ public class CachedPlayer Logger.Verbose("Restoring state for " + PlayerName); _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; - _apiController.CharacterReceived -= ApiControllerOnCharacterReceived; - _downloadCancellationTokenSource?.Cancel(); - _downloadCancellationTokenSource?.Dispose(); - _downloadCancellationTokenSource = null; + foreach (var token in _downloadCancellationTokenSources) + { + token.Value?.Cancel(); + token.Value?.Dispose(); + } _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); if (PlayerCharacter != null && PlayerCharacter.IsValid()) { @@ -283,9 +301,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 +312,11 @@ 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(); + // todo for everything I guess + DownloadAndApplyCharacter(ObjectKind.Player); } else { diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 22598bd..b6ee87a 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,6 +116,16 @@ 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; @@ -162,6 +179,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; @@ -209,7 +236,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..7fc8bd2 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -71,9 +71,9 @@ public class OnlinePlayerManager : IDisposable } } - private void PlayerManagerOnPlayerHasChanged(CharacterCacheDto characterCache) + private void PlayerManagerOnPlayerHasChanged(CharacterCacheDto characterCache, ObjectKind objectKind) { - PushCharacterData(OnlineVisiblePlayerHashes); + PushCharacterData(OnlineVisiblePlayerHashes, objectKind); } private void ApiControllerOnConnected() @@ -173,7 +173,7 @@ public class OnlinePlayerManager : IDisposable { if (_onlineCachedPlayers.Any(p => p.PlayerNameHash == characterNameHash)) { - PushCharacterData(new List() { characterNameHash }); + PushCharacterData(new List() { characterNameHash }, ObjectKind.Player); _playerTokenDisposal.TryGetValue(_onlineCachedPlayers.Single(p => p.PlayerNameHash == characterNameHash), out var cancellationTokenSource); cancellationTokenSource?.Cancel(); return; @@ -242,20 +242,25 @@ public class OnlinePlayerManager : IDisposable if (newlyVisiblePlayers.Any()) { Logger.Verbose("Has new visible players, pushing character data"); - PushCharacterData(newlyVisiblePlayers); + PushCharacterData(newlyVisiblePlayers, ObjectKind.Player); + PushCharacterData(newlyVisiblePlayers, ObjectKind.Pet); + PushCharacterData(newlyVisiblePlayers, ObjectKind.Minion); + PushCharacterData(newlyVisiblePlayers, ObjectKind.Companion); + PushCharacterData(newlyVisiblePlayers, ObjectKind.Mount); } _lastPlayerObjectCheck = DateTime.Now; } - private void PushCharacterData(List visiblePlayers) + private void PushCharacterData(List visiblePlayers, ObjectKind objectKind) { - if (visiblePlayers.Any() && _playerManager.LastSentCharacterData != null) + _playerManager.LastSentCharacterData.TryGetValue(objectKind, out var characterData); + if (visiblePlayers.Any() && characterData != null) { Task.Run(async () => { - Logger.Verbose(JsonConvert.SerializeObject(_playerManager.LastSentCharacterData, Formatting.Indented)); - await _apiController.PushCharacterData(_playerManager.LastSentCharacterData, + Logger.Verbose(JsonConvert.SerializeObject(_playerManager.LastSentCharacterData[objectKind], Formatting.Indented)); + await _apiController.PushCharacterData(_playerManager.LastSentCharacterData[objectKind]!, visiblePlayers); }); } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index a433022..6e9726c 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -10,10 +10,14 @@ 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 { - public delegate void PlayerHasChanged(CharacterCacheDto characterCache); + public delegate void PlayerHasChanged(CharacterCacheDto characterCache, ObjectKind objectKind); public class PlayerManager : IDisposable { @@ -23,14 +27,18 @@ namespace MareSynchronos.Managers private readonly IpcManager _ipcManager; public event PlayerHasChanged? PlayerHasChanged; public bool SendingData { get; private set; } - public CharacterCacheDto? LastSentCharacterData { get; private set; } + public Dictionary LastSentCharacterData { get; private set; } = new(); - private CancellationTokenSource? _playerChangedCts; + private Dictionary _playerChangedCts = new(); private DateTime _lastPlayerObjectCheck; - private CharacterEquipment? _currentCharacterEquipment; - private string _lastMinionName = string.Empty; + private Dictionary _currentCharacterEquipment = new(); - public PlayerManager(ApiController apiController, IpcManager ipcManager, + private PlayerAttachedObject Minion; + private PlayerAttachedObject Pet; + private PlayerAttachedObject Companion; + private PlayerAttachedObject Mount; + + public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) { Logger.Verbose("Creating " + nameof(PlayerManager)); @@ -48,6 +56,11 @@ namespace MareSynchronos.Managers { ApiControllerOnConnected(); } + + Minion = new PlayerAttachedObject(ObjectKind.Minion, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject); + Pet = new PlayerAttachedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()); + Companion = new PlayerAttachedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()); + Mount = new PlayerAttachedObject(ObjectKind.Mount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((CharaExt*)_dalamudUtil.PlayerPointer)->Mount); } public void Dispose() @@ -61,25 +74,48 @@ namespace MareSynchronos.Managers _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; } + private unsafe void CheckAndUpdateObject(PlayerAttachedObject attachedObject) + { + var curPtr = attachedObject.CurrentAddress; + if (curPtr != IntPtr.Zero) + { + var chara = (Character*)curPtr; + if (attachedObject.Address == IntPtr.Zero || attachedObject.Address != curPtr + || attachedObject.CompareAndUpdateEquipment(chara->EquipSlotData, chara->CustomizeData) + || (chara->GameObject.DrawObject != null && (IntPtr)chara->GameObject.DrawObject != attachedObject.DrawObjectAddress)) + { + Logger.Verbose(attachedObject.ObjectKind + " Changed " + curPtr); + + attachedObject.Address = curPtr; + attachedObject.DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; + attachedObject.CompareAndUpdateEquipment(chara->EquipSlotData, chara->CustomizeData); + OnPlayerChanged(attachedObject.ObjectKind); + } + } + else + { + attachedObject.Address = IntPtr.Zero; + LastSentCharacterData[attachedObject.ObjectKind] = null; + } + } + private unsafe void DalamudUtilOnFrameworkUpdate() { if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized || !_apiController.IsConnected) return; + //_dalamudUtil.DebugPrintRenderFlags(_dalamudUtil.PlayerPointer); if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(0.25)) return; - var minion = ((Character*)_dalamudUtil.PlayerPointer)->CompanionObject; - string minionName = ""; - if (minion != null) + + if (_dalamudUtil.IsPlayerPresent && !_currentCharacterEquipment[ObjectKind.Player]!.CompareAndUpdate(_dalamudUtil.PlayerCharacter)) { - minionName = new Utf8String(minion->Character.GameObject.GetName()).ToString(); + OnPlayerChanged(ObjectKind.Player); } - if (_dalamudUtil.IsPlayerPresent - && (!_currentCharacterEquipment!.CompareAndUpdate(_dalamudUtil.PlayerCharacter) || minionName != _lastMinionName)) - { - _lastMinionName = minionName; - OnPlayerChanged(); - } + CheckAndUpdateObject(Minion); + CheckAndUpdateObject(Pet); + CheckAndUpdateObject(Companion); + CheckAndUpdateObject(Mount); _lastPlayerObjectCheck = DateTime.Now; } @@ -91,8 +127,8 @@ namespace MareSynchronos.Managers _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - _currentCharacterEquipment = new CharacterEquipment(_dalamudUtil.PlayerCharacter); - PlayerChanged(); + _currentCharacterEquipment[ObjectKind.Player] = new CharacterEquipment(_dalamudUtil.PlayerCharacter); + PlayerChanged(ObjectKind.Player); } private void ApiController_Disconnected() @@ -101,12 +137,22 @@ namespace MareSynchronos.Managers _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; - LastSentCharacterData = null; + LastSentCharacterData.Clear(); } - private async Task CreateFullCharacterCache(CancellationToken token) + private async Task CreateFullCharacterCache(CancellationToken token, ObjectKind objectKind) { - var cache = _characterDataFactory.BuildCharacterData(); + IntPtr pointer = objectKind switch + { + ObjectKind.Player => _dalamudUtil.PlayerPointer, + ObjectKind.Minion => Minion.Address, + ObjectKind.Pet => Pet.Address, + ObjectKind.Companion => Companion.Address, + ObjectKind.Mount => Mount.Address, + _ => throw new NotImplementedException() + }; + + var cache = _characterDataFactory.BuildCharacterData(pointer); if (cache == null) return null; CharacterCacheDto? cacheDto = null; @@ -118,7 +164,7 @@ namespace MareSynchronos.Managers } if (token.IsCancellationRequested) return; - + cache.Kind = objectKind; cacheDto = cache.ToCharacterCacheDto(); var json = JsonConvert.SerializeObject(cacheDto); @@ -128,22 +174,44 @@ namespace MareSynchronos.Managers return cacheDto; } - 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); + + if (address == _dalamudUtil.PlayerPointer) + { + Logger.Debug("Penumbra Redraw Event for " + _dalamudUtil.PlayerName); + PlayerChanged(ObjectKind.Player); + } + + if (address == Minion.Address) + { + Logger.Debug("Penumbra Redraw Event for Minion"); + PlayerChanged(ObjectKind.Minion); + } + + if (address == Pet.Address) + { + Logger.Debug("Penumbra Redraw Event for Pet"); + PlayerChanged(ObjectKind.Pet); + } + + if (address == Companion.Address) + { + Logger.Debug("Penumbra Redraw Event for Companion"); + PlayerChanged(ObjectKind.Companion); + } } - private void PlayerChanged() + private void PlayerChanged(ObjectKind objectKind) { if (_dalamudUtil.IsInGpose) return; - Logger.Debug("Player changed: " + _dalamudUtil.PlayerName); - _playerChangedCts?.Cancel(); - _playerChangedCts = new CancellationTokenSource(); - var token = _playerChangedCts.Token; + Logger.Debug("Object changed: " + objectKind.ToString()); + _playerChangedCts.TryGetValue(objectKind, out var cts); + cts?.Cancel(); + _playerChangedCts[objectKind] = new CancellationTokenSource(); + var token = _playerChangedCts[objectKind]!.Token; // fix for redraw from anamnesis while ((!_dalamudUtil.IsPlayerPresent || _dalamudUtil.PlayerName == "--") && !token.IsCancellationRequested) @@ -179,28 +247,29 @@ namespace MareSynchronos.Managers _dalamudUtil.WaitWhileSelfIsDrawing(token); - var characterCache = (await CreateFullCharacterCache(token)); + var characterCache = (await CreateFullCharacterCache(token, objectKind)); if (characterCache == null || token.IsCancellationRequested) return; - if (characterCache.Hash == (LastSentCharacterData?.Hash ?? "-")) + LastSentCharacterData.TryGetValue(objectKind, out var lastSentPlayerData); + if (characterCache.Hash == (lastSentPlayerData?.Hash ?? string.Empty)) { Logger.Debug("Not sending data, already sent"); return; } - LastSentCharacterData = characterCache; - PlayerHasChanged?.Invoke(characterCache); + LastSentCharacterData[objectKind] = characterCache; + PlayerHasChanged?.Invoke(characterCache, objectKind); SendingData = false; }, token); } - private void OnPlayerChanged() + private void OnPlayerChanged(ObjectKind objectKind) { Task.Run(() => { Logger.Debug("Watcher: PlayerChanged"); - PlayerChanged(); + PlayerChanged(objectKind); }); } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 47f2847..d953078 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..e683452 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -3,12 +3,15 @@ using System.Collections.Generic; using System.Linq; using System.Text; using MareSynchronos.API; +using MareSynchronos.Factories; namespace MareSynchronos.Models { [JsonObject(MemberSerialization.OptIn)] public class CharacterData { + [JsonProperty] + public ObjectKind Kind { get; set; } public List FileReplacements { get; set; } = new(); [JsonProperty] @@ -37,6 +40,7 @@ namespace MareSynchronos.Models { return new CharacterCacheDto() { + ObjectKind = Kind, FileReplacements = FileReplacements.Where(f => f.HasFileReplacement).GroupBy(f => f.Hash).Select(g => { return new FileReplacementDto() diff --git a/MareSynchronos/Models/PlayerAttachedObject.cs b/MareSynchronos/Models/PlayerAttachedObject.cs new file mode 100644 index 0000000..130b1fb --- /dev/null +++ b/MareSynchronos/Models/PlayerAttachedObject.cs @@ -0,0 +1,57 @@ +using System; +using MareSynchronos.API; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using System.Runtime.InteropServices; + +namespace MareSynchronos.Models +{ + internal class PlayerAttachedObject + { + private readonly Func getAddress; + + public unsafe Character* Character => (Character*)Address; + + public ObjectKind ObjectKind { get; } + public IntPtr Address { get; set; } + public IntPtr DrawObjectAddress { get; set; } + + public IntPtr CurrentAddress => getAddress.Invoke(); + + public PlayerAttachedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + { + ObjectKind = objectKind; + Address = address; + DrawObjectAddress = drawObjectAddress; + this.getAddress = getAddress; + } + + public byte[] EquipSlotData { get; set; } = new byte[40]; + public byte[] CustomizeData { get; set; } = new byte[26]; + + public unsafe bool CompareAndUpdateEquipment(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 81360dd..51c0738 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -57,7 +57,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..bdce71c 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -17,12 +17,13 @@ namespace MareSynchronos.WebAPI public partial class ApiController { private int _downloadId = 0; - public void CancelUpload() + public void CancelUpload(ObjectKind objectKind) { - if (_uploadCancellationTokenSource != null) + _uploadTokens.TryGetValue(objectKind, out var cts); + if (cts != null) { Logger.Warn("Cancelling upload"); - _uploadCancellationTokenSource?.Cancel(); + cts?.Cancel(); _mareHub!.SendAsync(Api.SendFileAbortUpload); CurrentUploads.Clear(); } @@ -123,9 +124,9 @@ namespace MareSynchronos.WebAPI if (!IsConnected || SecretKey == "-") return; Logger.Debug("Sending Character data to service " + ApiUri); - CancelUpload(); - _uploadCancellationTokenSource = new CancellationTokenSource(); - var uploadToken = _uploadCancellationTokenSource.Token; + CancelUpload(character.ObjectKind); + _uploadTokens[character.ObjectKind] = new CancellationTokenSource(); + var uploadToken = _uploadTokens[character.ObjectKind]!.Token; Logger.Verbose("New Token Created"); var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); @@ -164,7 +165,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); @@ -205,7 +206,7 @@ namespace MareSynchronos.WebAPI } Logger.Verbose("Upload complete for " + character.Hash); - _uploadCancellationTokenSource = null; + _uploadTokens.Remove(character.ObjectKind); } 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..951c3f0 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 Dictionary _uploadTokens = new(); private ConnectionDto? _connectionDto; public SystemInfoDto SystemInfoDto { get; private set; } = new(); @@ -268,7 +268,10 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); + foreach(var token in _uploadTokens.Values) + { + token?.Cancel(); + } Logger.Debug("Connection closed"); Disconnected?.Invoke(); return Task.CompletedTask; @@ -286,7 +289,10 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - _uploadCancellationTokenSource?.Cancel(); + foreach (var token in _uploadTokens.Values) + { + token?.Cancel(); + } Logger.Debug("Connection closed... Reconnecting"); Disconnected?.Invoke(); return Task.CompletedTask; From 0eb2ed639d5b9286637305a16c697d777e66b0a9 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sun, 24 Jul 2022 00:44:18 +0200 Subject: [PATCH 2/3] adjust submodule --- MareAPI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MareAPI b/MareAPI index ffc0a48..96d17b0 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit ffc0a48fdadd71d33b015e4e46ef15303f1d2f60 +Subproject commit 96d17b006e58614228f00a361b60f569b6e99445 From f39c085fbfd32ae473bc6f8bea3eeaaf1b034d05 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sun, 24 Jul 2022 14:34:26 +0200 Subject: [PATCH 3/3] adjust to send only one packet for all changes at once --- MareAPI | 2 +- .../Factories/CharacterDataFactory.cs | 103 +++++---- MareSynchronos/Managers/CachedPlayer.cs | 140 ++++++++---- MareSynchronos/Managers/IpcManager.cs | 12 +- .../Managers/OnlinePlayerManager.cs | 20 +- MareSynchronos/Managers/PlayerManager.cs | 200 ++++++------------ MareSynchronos/Models/CharacterData.cs | 24 +-- MareSynchronos/Models/PlayerAttachedObject.cs | 51 ++++- .../WebAPI/ApIController.Functions.Files.cs | 22 +- .../WebAPI/ApiController.Connectivity.cs | 12 +- 10 files changed, 300 insertions(+), 286 deletions(-) diff --git a/MareAPI b/MareAPI index 96d17b0..a649b36 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 96d17b006e58614228f00a361b60f569b6e99445 +Subproject commit a649b364955ce431f41c53c016823763febde708 diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index c17c74c..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; @@ -30,7 +31,7 @@ public class CharacterDataFactory _ipcManager = ipcManager; } - public CharacterData? BuildCharacterData(IntPtr playerPointer) + public CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer) { if (!_ipcManager.Initialized) { @@ -39,19 +40,22 @@ public class CharacterDataFactory if (playerPointer == IntPtr.Zero) { - return null; + Logger.Verbose("Pointer was zero for " + objectKind); + previousData.FileReplacements.Remove(objectKind); + previousData.GlamourerString.Remove(objectKind); + return previousData; } try { - return CreateCharacterData(playerPointer); + 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; } } @@ -60,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) { @@ -91,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 @@ -122,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++) @@ -131,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; @@ -144,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; @@ -153,10 +156,10 @@ public class CharacterDataFactory DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel); - cache.AddFileReplacement(texDx11Replacement); + cache.AddFileReplacement(objectKind, texDx11Replacement); } - private unsafe CharacterData CreateCharacterData(IntPtr charaPointer) + private unsafe CharacterData CreateCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer) { Stopwatch st = Stopwatch.StartNew(); var chara = _dalamudUtil.CreateGameObject(charaPointer)!; @@ -166,20 +169,18 @@ public class CharacterDataFactory Thread.Sleep(50); } _dalamudUtil.WaitWhileCharacterIsDrawing(charaPointer); - var cache = new CharacterData() - { - ManipulationString = _ipcManager.PenumbraGetMetaManipulations(), - }; - try + if (previousData.FileReplacements.ContainsKey(objectKind)) { - cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(chara); - } - catch - { - // might not have glamourer data + 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) @@ -190,54 +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); - if (!string.IsNullOrEmpty(cache.GlamourerString)) - { - try - { - AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, cache); - 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); + if (weaponObject->NextSibling != (IntPtr)weaponObject) + { + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1); + } } - catch { } + + 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/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index 671899a..ccbdf3a 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -42,7 +42,7 @@ public class CachedPlayer } private bool _isDisposed = false; - private Dictionary _downloadCancellationTokenSources = new(); + private CancellationTokenSource? _downloadCancellationTokenSource = new(); private string _lastGlamourerData = string.Empty; @@ -58,7 +58,7 @@ public class CachedPlayer public bool WasVisible { get; private set; } - private readonly Dictionary _cache = new(); + private CharacterCacheDto _cachedData = new(); private CharacterEquipment? _currentCharacterEquipment; @@ -67,33 +67,41 @@ 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); + Logger.Debug("Hash for data is " + characterData.GetHashCode()); - if (!_cache.ContainsKey(characterData.ObjectKind)) + if (characterData.GetHashCode() == _cachedData.GetHashCode()) return; + + List charaDataToUpdate = new List(); + foreach (var objectKind in Enum.GetValues()) { - _cache.Add(characterData.ObjectKind, new()); - _downloadCancellationTokenSources.Add(characterData.ObjectKind, null); + 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); + } } - _cache.TryGetValue(characterData.ObjectKind, out var cachedDto); - if ((cachedDto?.Hash ?? string.Empty) != characterData.Hash) - { - Logger.Debug("Received total " + characterData.FileReplacements.Count + " file replacement data"); - _cache[characterData.ObjectKind] = characterData; - } - else - { - Logger.Debug("Had valid local cache for " + PlayerName); - } + _cachedData = characterData; - DownloadAndApplyCharacter(characterData.ObjectKind); + DownloadAndApplyCharacter(charaDataToUpdate); } - private void DownloadAndApplyCharacter(ObjectKind objectKind) + private void DownloadAndApplyCharacter(List objectKind) { - _downloadCancellationTokenSources[objectKind]?.Cancel(); - _downloadCancellationTokenSources[objectKind] = new CancellationTokenSource(); - var downloadToken = _downloadCancellationTokenSources[objectKind]!.Token; + _downloadCancellationTokenSource?.Cancel(); + _downloadCancellationTokenSource = new CancellationTokenSource(); + var downloadToken = _downloadCancellationTokenSource.Token; var downloadId = _apiController.GetDownloadId(); Task.Run(async () => { @@ -103,7 +111,7 @@ public class CachedPlayer int attempts = 0; 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) { @@ -127,7 +135,12 @@ public class CachedPlayer } } - ApplyCharacterData(objectKind, moddedPaths); + ApplyBaseData(moddedPaths); + + foreach (var kind in objectKind) + { + ApplyCustomizationData(kind); + } }, downloadToken).ContinueWith(task => { if (!task.IsCanceled) return; @@ -144,7 +157,7 @@ public class CachedPlayer try { using var db = new FileCacheContext(); - foreach (var item in _cache.SelectMany(c => c.Value.FileReplacements).ToList()) + foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value).ToList()) { foreach (var gamePath in item.GamePaths) { @@ -168,14 +181,16 @@ public class CachedPlayer return missingFiles; } - private unsafe void ApplyCharacterData(ObjectKind objectKind, Dictionary moddedPaths) + private void ApplyBaseData(Dictionary moddedPaths) { - if (PlayerCharacter is null) return; - var cache = _cache[objectKind]; - _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName!); var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(PlayerName!); - _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, _cache.First().Value.ManipulationData); + _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, _cachedData.ManipulationData); + } + + private unsafe void ApplyCustomizationData(ObjectKind objectKind) + { + if (PlayerCharacter is null) return; if (objectKind == ObjectKind.Player) { @@ -183,7 +198,7 @@ public class CachedPlayer RequestedPenumbraRedraw = true; Logger.Debug( $"Request Redraw for {PlayerName}"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, PlayerCharacter!); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], PlayerCharacter!); } else if (objectKind == ObjectKind.Minion) { @@ -191,7 +206,7 @@ public class CachedPlayer if (minion != null) { Logger.Debug($"Request Redraw for Minion"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, obj: (IntPtr)minion); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], obj: (IntPtr)minion); } } else if (objectKind == ObjectKind.Pet) @@ -200,7 +215,7 @@ public class CachedPlayer if (pet != IntPtr.Zero) { Logger.Debug("Request Redraw for Pet"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, pet); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], pet); } } else if (objectKind == ObjectKind.Companion) @@ -209,7 +224,7 @@ public class CachedPlayer if (companion != IntPtr.Zero) { Logger.Debug("Request Redraw for Companion"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, companion); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], companion); } } else if (objectKind == ObjectKind.Mount) @@ -223,6 +238,49 @@ public class CachedPlayer } } + 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); + } + } + } + public void DisposePlayer() { if (_isDisposed) return; @@ -234,16 +292,15 @@ public class CachedPlayer Logger.Verbose("Restoring state for " + PlayerName); _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; - foreach (var token in _downloadCancellationTokenSources) - { - token.Value?.Cancel(); - token.Value?.Dispose(); - } + _downloadCancellationTokenSource?.Cancel(); + _downloadCancellationTokenSource?.Dispose(); _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) @@ -315,8 +372,7 @@ public class CachedPlayer if (RequestedPenumbraRedraw == false) { Logger.Warn("Unauthorized character change detected"); - // todo for everything I guess - DownloadAndApplyCharacter(ObjectKind.Player); + ApplyCustomizationData(ObjectKind.Player); } else { diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index b6ee87a..1f9cb1c 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -129,7 +129,6 @@ namespace MareSynchronos.Managers public void GlamourerApplyAll(string customization, GameObject character) { if (!CheckGlamourerApi()) return; - Logger.Verbose("Glamourer apply all to " + character); _glamourerApplyAll!.InvokeAction(customization, character); } @@ -150,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) @@ -206,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; } @@ -218,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; } diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index 7fc8bd2..d20575b 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -71,9 +71,9 @@ public class OnlinePlayerManager : IDisposable } } - private void PlayerManagerOnPlayerHasChanged(CharacterCacheDto characterCache, ObjectKind objectKind) + private void PlayerManagerOnPlayerHasChanged(CharacterCacheDto characterCache) { - PushCharacterData(OnlineVisiblePlayerHashes, objectKind); + PushCharacterData(OnlineVisiblePlayerHashes); } private void ApiControllerOnConnected() @@ -173,7 +173,7 @@ public class OnlinePlayerManager : IDisposable { if (_onlineCachedPlayers.Any(p => p.PlayerNameHash == characterNameHash)) { - PushCharacterData(new List() { characterNameHash }, ObjectKind.Player); + PushCharacterData(new List() { characterNameHash }); _playerTokenDisposal.TryGetValue(_onlineCachedPlayers.Single(p => p.PlayerNameHash == characterNameHash), out var cancellationTokenSource); cancellationTokenSource?.Cancel(); return; @@ -242,25 +242,19 @@ public class OnlinePlayerManager : IDisposable if (newlyVisiblePlayers.Any()) { Logger.Verbose("Has new visible players, pushing character data"); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Player); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Pet); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Minion); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Companion); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Mount); + PushCharacterData(newlyVisiblePlayers); } _lastPlayerObjectCheck = DateTime.Now; } - private void PushCharacterData(List visiblePlayers, ObjectKind objectKind) + private void PushCharacterData(List visiblePlayers) { - _playerManager.LastSentCharacterData.TryGetValue(objectKind, out var characterData); - if (visiblePlayers.Any() && characterData != null) + if (visiblePlayers.Any() && _playerManager.LastCreatedCharacterData != null) { Task.Run(async () => { - Logger.Verbose(JsonConvert.SerializeObject(_playerManager.LastSentCharacterData[objectKind], Formatting.Indented)); - await _apiController.PushCharacterData(_playerManager.LastSentCharacterData[objectKind]!, + await _apiController.PushCharacterData(_playerManager.LastCreatedCharacterData!, visiblePlayers); }); } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index 6e9726c..01b7b16 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -1,15 +1,12 @@ 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; @@ -17,7 +14,7 @@ using MareSynchronos.Interop; namespace MareSynchronos.Managers { - public delegate void PlayerHasChanged(CharacterCacheDto characterCache, ObjectKind objectKind); + public delegate void PlayerHasChanged(CharacterCacheDto characterCache); public class PlayerManager : IDisposable { @@ -26,17 +23,15 @@ namespace MareSynchronos.Managers private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; public event PlayerHasChanged? PlayerHasChanged; - public bool SendingData { get; private set; } - public Dictionary LastSentCharacterData { get; private set; } = new(); + public CharacterCacheDto? LastCreatedCharacterData { get; private set; } + public CharacterData PermanentDataCache { get; private set; } = new(); + private readonly Dictionary> objectKindsToUpdate = new(); - private Dictionary _playerChangedCts = new(); + private CancellationTokenSource? _playerChangedCts = new(); private DateTime _lastPlayerObjectCheck; - private Dictionary _currentCharacterEquipment = new(); + private CharacterEquipment? _currentCharacterEquipment = new(); - private PlayerAttachedObject Minion; - private PlayerAttachedObject Pet; - private PlayerAttachedObject Companion; - private PlayerAttachedObject Mount; + private List playerAttachedObjects = new List(); public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) @@ -50,6 +45,7 @@ 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) @@ -57,10 +53,14 @@ namespace MareSynchronos.Managers ApiControllerOnConnected(); } - Minion = new PlayerAttachedObject(ObjectKind.Minion, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject); - Pet = new PlayerAttachedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()); - Companion = new PlayerAttachedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()); - Mount = new PlayerAttachedObject(ObjectKind.Mount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((CharaExt*)_dalamudUtil.PlayerPointer)->Mount); + 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() @@ -74,49 +74,18 @@ namespace MareSynchronos.Managers _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; } - private unsafe void CheckAndUpdateObject(PlayerAttachedObject attachedObject) - { - var curPtr = attachedObject.CurrentAddress; - if (curPtr != IntPtr.Zero) - { - var chara = (Character*)curPtr; - if (attachedObject.Address == IntPtr.Zero || attachedObject.Address != curPtr - || attachedObject.CompareAndUpdateEquipment(chara->EquipSlotData, chara->CustomizeData) - || (chara->GameObject.DrawObject != null && (IntPtr)chara->GameObject.DrawObject != attachedObject.DrawObjectAddress)) - { - Logger.Verbose(attachedObject.ObjectKind + " Changed " + curPtr); - - attachedObject.Address = curPtr; - attachedObject.DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; - attachedObject.CompareAndUpdateEquipment(chara->EquipSlotData, chara->CustomizeData); - OnPlayerChanged(attachedObject.ObjectKind); - } - } - else - { - attachedObject.Address = IntPtr.Zero; - LastSentCharacterData[attachedObject.ObjectKind] = null; - } - } - private unsafe void DalamudUtilOnFrameworkUpdate() { - if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized || !_apiController.IsConnected) return; - //_dalamudUtil.DebugPrintRenderFlags(_dalamudUtil.PlayerPointer); + if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(0.25)) return; - - if (_dalamudUtil.IsPlayerPresent && !_currentCharacterEquipment[ObjectKind.Player]!.CompareAndUpdate(_dalamudUtil.PlayerCharacter)) + playerAttachedObjects.ForEach(k => k.CheckAndUpdateObject()); + if (playerAttachedObjects.Any(c => c.HasUnprocessedUpdate && !c.IsProcessing)) { - OnPlayerChanged(ObjectKind.Player); + OnPlayerOrAttachedObjectsChanged(); } - CheckAndUpdateObject(Minion); - CheckAndUpdateObject(Pet); - CheckAndUpdateObject(Companion); - CheckAndUpdateObject(Mount); - _lastPlayerObjectCheck = DateTime.Now; } @@ -125,10 +94,6 @@ namespace MareSynchronos.Managers Logger.Debug("ApiController Connected"); _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - - _currentCharacterEquipment[ObjectKind.Player] = new CharacterEquipment(_dalamudUtil.PlayerCharacter); - PlayerChanged(ObjectKind.Player); } private void ApiController_Disconnected() @@ -136,82 +101,63 @@ namespace MareSynchronos.Managers Logger.Debug(nameof(ApiController_Disconnected)); _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; - LastSentCharacterData.Clear(); } - private async Task CreateFullCharacterCache(CancellationToken token, ObjectKind objectKind) + private async Task CreateFullCharacterCacheDto(CancellationToken token) { - IntPtr pointer = objectKind switch + foreach (var unprocessedObject in playerAttachedObjects.Where(c => c.HasUnprocessedUpdate).ToList()) { - ObjectKind.Player => _dalamudUtil.PlayerPointer, - ObjectKind.Minion => Minion.Address, - ObjectKind.Pet => Pet.Address, - ObjectKind.Companion => Companion.Address, - ObjectKind.Mount => Mount.Address, - _ => throw new NotImplementedException() - }; + Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); + PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject.ObjectKind, unprocessedObject.Address); + unprocessedObject.HasUnprocessedUpdate = false; + unprocessedObject.IsProcessing = false; + token.ThrowIfCancellationRequested(); + } - var cache = _characterDataFactory.BuildCharacterData(pointer); - if (cache == null) return null; - CharacterCacheDto? cacheDto = null; - - await Task.Run(async () => + while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) { - while (!cache.IsReady && !token.IsCancellationRequested) - { - await Task.Delay(50, token); - } + await Task.Delay(50, token); + } - if (token.IsCancellationRequested) return; - cache.Kind = objectKind; - 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(IntPtr address, int idx) { Logger.Verbose("RedrawEvent for addr " + address); - if (address == _dalamudUtil.PlayerPointer) + foreach (var item in playerAttachedObjects) { - Logger.Debug("Penumbra Redraw Event for " + _dalamudUtil.PlayerName); - PlayerChanged(ObjectKind.Player); + if (address == item.Address) + { + Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); + item.HasUnprocessedUpdate = true; + } } - if (address == Minion.Address) + if (playerAttachedObjects.Any(c => c.HasUnprocessedUpdate)) { - Logger.Debug("Penumbra Redraw Event for Minion"); - PlayerChanged(ObjectKind.Minion); - } - - if (address == Pet.Address) - { - Logger.Debug("Penumbra Redraw Event for Pet"); - PlayerChanged(ObjectKind.Pet); - } - - if (address == Companion.Address) - { - Logger.Debug("Penumbra Redraw Event for Companion"); - PlayerChanged(ObjectKind.Companion); + OnPlayerOrAttachedObjectsChanged(); } } - private void PlayerChanged(ObjectKind objectKind) + private void OnPlayerOrAttachedObjectsChanged() { if (_dalamudUtil.IsInGpose) return; - Logger.Debug("Object changed: " + objectKind.ToString()); - _playerChangedCts.TryGetValue(objectKind, out var cts); - cts?.Cancel(); - _playerChangedCts[objectKind] = new CancellationTokenSource(); - var token = _playerChangedCts[objectKind]!.Token; + 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; // fix for redraw from anamnesis while ((!_dalamudUtil.IsPlayerPresent || _dalamudUtil.PlayerName == "--") && !token.IsCancellationRequested) @@ -234,45 +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, objectKind)); + CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); + if (cacheDto == null || token.IsCancellationRequested) return; - if (characterCache == null || token.IsCancellationRequested) return; - - LastSentCharacterData.TryGetValue(objectKind, out var lastSentPlayerData); - if (characterCache.Hash == (lastSentPlayerData?.Hash ?? string.Empty)) + if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) { Logger.Debug("Not sending data, already sent"); return; } + else + { + LastCreatedCharacterData = cacheDto; + } - LastSentCharacterData[objectKind] = characterCache; - PlayerHasChanged?.Invoke(characterCache, objectKind); - SendingData = false; + if (_apiController.IsConnected) + { + Logger.Verbose("Invoking PlayerHasChanged"); + PlayerHasChanged?.Invoke(cacheDto); + } }, token); } - - private void OnPlayerChanged(ObjectKind objectKind) - { - Task.Run(() => - { - Logger.Debug("Watcher: PlayerChanged"); - PlayerChanged(objectKind); - }); - } - - } } diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index e683452..8075aac 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; using MareSynchronos.API; -using MareSynchronos.Factories; namespace MareSynchronos.Models { @@ -11,28 +10,30 @@ namespace MareSynchronos.Models public class CharacterData { [JsonProperty] - public ObjectKind Kind { get; set; } - public List FileReplacements { get; set; } = new(); + 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); } } @@ -40,15 +41,14 @@ namespace MareSynchronos.Models { return new CharacterCacheDto() { - ObjectKind = Kind, - 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 }; @@ -57,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 index 130b1fb..87d06e5 100644 --- a/MareSynchronos/Models/PlayerAttachedObject.cs +++ b/MareSynchronos/Models/PlayerAttachedObject.cs @@ -2,33 +2,76 @@ 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 PlayerAttachedObject + 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; } - public IntPtr CurrentAddress => getAddress.Invoke(); + private IntPtr CurrentAddress => getAddress.Invoke(); - public PlayerAttachedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + 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 unsafe bool CompareAndUpdateEquipment(byte* equipSlotData, byte* customizeData) + 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++) diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs index bdce71c..110de12 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -17,13 +17,12 @@ namespace MareSynchronos.WebAPI public partial class ApiController { private int _downloadId = 0; - public void CancelUpload(ObjectKind objectKind) + public void CancelUpload() { - _uploadTokens.TryGetValue(objectKind, out var cts); - if (cts != null) + if (_uploadToken != null) { - Logger.Warn("Cancelling upload"); - cts?.Cancel(); + Logger.Debug("Cancelling upload"); + _uploadToken?.Cancel(); _mareHub!.SendAsync(Api.SendFileAbortUpload); CurrentUploads.Clear(); } @@ -41,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,12 +122,12 @@ namespace MareSynchronos.WebAPI if (!IsConnected || SecretKey == "-") return; Logger.Debug("Sending Character data to service " + ApiUri); - CancelUpload(character.ObjectKind); - _uploadTokens[character.ObjectKind] = new CancellationTokenSource(); - var uploadToken = _uploadTokens[character.ObjectKind]!.Token; + CancelUpload(); + _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)) { @@ -205,8 +203,8 @@ namespace MareSynchronos.WebAPI Logger.Warn("=== Upload operation was cancelled ==="); } - Logger.Verbose("Upload complete for " + character.Hash); - _uploadTokens.Remove(character.ObjectKind); + 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 951c3f0..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 Dictionary _uploadTokens = new(); + private CancellationTokenSource? _uploadToken = new(); private ConnectionDto? _connectionDto; public SystemInfoDto SystemInfoDto { get; private set; } = new(); @@ -268,10 +268,7 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - foreach(var token in _uploadTokens.Values) - { - token?.Cancel(); - } + _uploadToken?.Cancel(); Logger.Debug("Connection closed"); Disconnected?.Invoke(); return Task.CompletedTask; @@ -289,10 +286,7 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - foreach (var token in _uploadTokens.Values) - { - token?.Cancel(); - } + _uploadToken?.Cancel(); Logger.Debug("Connection closed... Reconnecting"); Disconnected?.Invoke(); return Task.CompletedTask;