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;