minion/pet/companion/mount rework

This commit is contained in:
Stanley Dimant
2022-07-22 02:11:35 +02:00
parent 75885473ad
commit 9d9aac6bb3
14 changed files with 403 additions and 152 deletions

Submodule MareAPI updated: af8516d44a...ffc0a48fda

View File

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

View File

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

View File

@@ -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<ObjectKind, CancellationTokenSource?> _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<string, CharacterCacheDto> _cache = new();
private readonly Dictionary<ObjectKind, CharacterCacheDto> _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<string, string> 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<FileReplacementDto> TryCalculateModdedDictionary(CharacterCacheDto cache,
out Dictionary<string, string> moddedDictionary)
private List<FileReplacementDto> TryCalculateModdedDictionary(out Dictionary<string, string> moddedDictionary)
{
List<FileReplacementDto> missingFiles = new();
moddedDictionary = new Dictionary<string, string>();
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<string, string> moddedPaths)
private unsafe void ApplyCharacterData(ObjectKind objectKind, Dictionary<string, string> 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
{

View File

@@ -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<int> _glamourerApiVersion;
@@ -23,13 +24,16 @@ namespace MareSynchronos.Managers
private readonly ICallGateSubscriber<object> _penumbraDispose;
private readonly ICallGateSubscriber<IntPtr, int, object?> _penumbraObjectIsRedrawn;
private readonly ICallGateSubscriber<string, int, object>? _penumbraRedraw;
private readonly ICallGateSubscriber<GameObject, int, object>? _penumbraRedrawObject;
private readonly ICallGateSubscriber<string, int> _penumbraRemoveTemporaryCollection;
private readonly ICallGateSubscriber<string>? _penumbraResolveModDir;
private readonly ICallGateSubscriber<string, string>? _penumbraResolvePlayer;
private readonly ICallGateSubscriber<string, string[]>? _reverseResolvePlayer;
private readonly ICallGateSubscriber<string, string, Dictionary<string, string>, 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<string, string>("Penumbra.ResolvePlayerPath");
_penumbraResolveModDir = pi.GetIpcSubscriber<string>("Penumbra.GetModDirectory");
_penumbraRedraw = pi.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
_penumbraRedrawObject = pi.GetIpcSubscriber<GameObject, int, object>("Penumbra.RedrawObject");
_reverseResolvePlayer = pi.GetIpcSubscriber<string, string[]>("Penumbra.ReverseResolvePlayerPath");
_penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions");
_penumbraObjectIsRedrawn = pi.GetIpcSubscriber<IntPtr, int, object?>("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()

View File

@@ -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<string>() { characterNameHash });
PushCharacterData(new List<string>() { 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<string> visiblePlayers)
private void PushCharacterData(List<string> 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);
});
}

View File

@@ -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<ObjectKind, CharacterCacheDto?> LastSentCharacterData { get; private set; } = new();
private CancellationTokenSource? _playerChangedCts;
private Dictionary<ObjectKind, CancellationTokenSource?> _playerChangedCts = new();
private DateTime _lastPlayerObjectCheck;
private CharacterEquipment? _currentCharacterEquipment;
private string _lastMinionName = string.Empty;
private Dictionary<ObjectKind, CharacterEquipment?> _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<CharacterCacheDto?> CreateFullCharacterCache(CancellationToken token)
private async Task<CharacterCacheDto?> 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);
});
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>0.1.18.0</Version>
<Version>0.2.0.0</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>

View File

@@ -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<FileReplacement> 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()

View File

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

View File

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

View File

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

View File

@@ -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<List<UploadFileDto>>(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)

View File

@@ -37,7 +37,7 @@ namespace MareSynchronos.WebAPI
private HubConnection? _mareHub;
private CancellationTokenSource? _uploadCancellationTokenSource;
private Dictionary<ObjectKind, CancellationTokenSource?> _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;