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