0.4.2: fix heels integration, potentially fix crashes, delay handling of transient resource loads, change transients to concurrent dictionary

This commit is contained in:
Stanley Dimant
2022-09-11 01:23:41 +02:00
parent 23f01bc50c
commit a618fad7d9
8 changed files with 142 additions and 84 deletions

View File

@@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
@@ -40,7 +38,7 @@ public class CharacterDataFactory
return playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null; return playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null;
} }
public CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer, CancellationToken token) public CharacterData BuildCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token)
{ {
if (!_ipcManager.Initialized) if (!_ipcManager.Initialized)
{ {
@@ -50,20 +48,20 @@ public class CharacterDataFactory
bool pointerIsZero = true; bool pointerIsZero = true;
try try
{ {
pointerIsZero = CheckForPointer(playerPointer); pointerIsZero = CheckForPointer(playerRelatedObject.Address);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Warn("Could not create data for " + objectKind); Logger.Warn("Could not create data for " + playerRelatedObject.ObjectKind);
Logger.Warn(ex.Message); Logger.Warn(ex.Message);
Logger.Warn(ex.StackTrace ?? string.Empty); Logger.Warn(ex.StackTrace ?? string.Empty);
} }
if (pointerIsZero) if (pointerIsZero)
{ {
Logger.Verbose("Pointer was zero for " + objectKind); Logger.Verbose("Pointer was zero for " + playerRelatedObject.ObjectKind);
previousData.FileReplacements.Remove(objectKind); previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind);
previousData.GlamourerString.Remove(objectKind); previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind);
return previousData; return previousData;
} }
@@ -72,7 +70,7 @@ public class CharacterDataFactory
try try
{ {
return CreateCharacterData(previousData, objectKind, playerPointer, token); return CreateCharacterData(previousData, playerRelatedObject, token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -80,7 +78,7 @@ public class CharacterDataFactory
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Failed to create " + objectKind + " data"); Logger.Warn("Failed to create " + playerRelatedObject.ObjectKind + " data");
Logger.Warn(e.Message); Logger.Warn(e.Message);
Logger.Warn(e.StackTrace ?? string.Empty); Logger.Warn(e.StackTrace ?? string.Empty);
} }
@@ -232,66 +230,75 @@ public class CharacterDataFactory
cache.AddFileReplacement(objectKind, texDx11Replacement); cache.AddFileReplacement(objectKind, texDx11Replacement);
} }
private unsafe CharacterData CreateCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer, CancellationToken token) private unsafe CharacterData CreateCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token)
{ {
if (previousData.FileReplacements.ContainsKey(objectKind)) var objectKind = playerRelatedObject.ObjectKind;
{ var charaPointer = playerRelatedObject.Address;
previousData.FileReplacements[objectKind].Clear();
}
else
{
previousData.FileReplacements.Add(objectKind, new());
}
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(objectKind.ToString(), charaPointer);
Stopwatch st = Stopwatch.StartNew(); Stopwatch st = Stopwatch.StartNew();
previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); if (playerRelatedObject.HasUnprocessedUpdate)
previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer);
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{ {
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; Logger.Debug("Handling unprocessed update for " + objectKind);
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
if (previousData.FileReplacements.ContainsKey(objectKind))
{ {
continue; previousData.FileReplacements[objectKind].Clear();
}
else
{
previousData.FileReplacements.Add(objectKind, new());
} }
token.ThrowIfCancellationRequested(); var chara = _dalamudUtil.CreateGameObject(charaPointer)!;
while (!_dalamudUtil.IsObjectPresent(chara))
{
Logger.Verbose("Character is null but it shouldn't be, waiting");
Thread.Sleep(50);
}
AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); _dalamudUtil.WaitWhileCharacterIsDrawing(objectKind.ToString(), charaPointer);
}
foreach (var item in previousData.FileReplacements[objectKind]) var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
{ for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
transientResourceManager.RemoveTransientResource(charaPointer, item); {
} var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
continue;
}
if (objectKind == ObjectKind.Player) token.ThrowIfCancellationRequested();
{
AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human); AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0);
} }
if (objectKind == ObjectKind.Pet)
{
foreach (var item in previousData.FileReplacements[objectKind]) foreach (var item in previousData.FileReplacements[objectKind])
{ {
transientResourceManager.AddSemiTransientResource(objectKind, item); transientResourceManager.RemoveTransientResource(charaPointer, item);
} }
previousData.FileReplacements[objectKind].Clear(); if (objectKind == ObjectKind.Player)
{
AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human);
}
if (objectKind == ObjectKind.Pet)
{
foreach (var item in previousData.FileReplacements[objectKind])
{
transientResourceManager.AddSemiTransientResource(objectKind, item);
}
previousData.FileReplacements[objectKind].Clear();
}
} }
previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations();
previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer);
previousData.HeelsOffset = _ipcManager.GetHeelsOffset();
Logger.Debug("Handling transient update for " + objectKind);
ManageSemiTransientData(previousData, objectKind, charaPointer); ManageSemiTransientData(previousData, objectKind, charaPointer);
st.Stop(); st.Stop();
@@ -387,8 +394,6 @@ public class CharacterDataFactory
{ {
transientResourceManager.RemoveTransientResource(charaPointer, item); transientResourceManager.RemoveTransientResource(charaPointer, item);
} }
previousData.HeelsOffset = _ipcManager.GetHeelsOffset();
} }
private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache) private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache)

View File

@@ -117,6 +117,17 @@ public class CachedPlayer
continue; continue;
} }
} }
if (objectKind == ObjectKind.Player)
{
bool heelsOffsetDifferent = _cachedData.HeelsOffset != characterData.HeelsOffset;
if (heelsOffsetDifferent)
{
Logger.Debug("Updating " + objectKind);
charaDataToUpdate.Add(objectKind);
continue;
}
}
} }
_cachedData = characterData; _cachedData = characterData;
@@ -324,6 +335,7 @@ public class CachedPlayer
{ {
_ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter); _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter);
_ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter); _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter);
_ipcManager.HeelsRestoreOffsetForPlayer(PlayerCharacter);
} }
else else
{ {

View File

@@ -207,16 +207,30 @@ namespace MareSynchronos.Managers
}); });
} }
public void HeelsRestoreOffsetForPlayer(IntPtr character)
{
if (!CheckHeelsApi()) return;
actionQueue.Enqueue(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
{
Logger.Verbose("Restoring Heels data to " + character.ToString("X"));
_heelsUnregisterPlayer.InvokeAction(gameObj);
}
});
}
public void GlamourerApplyAll(string? customization, IntPtr obj) public void GlamourerApplyAll(string? customization, IntPtr obj)
{ {
if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return;
actionQueue.Enqueue(() => actionQueue.Enqueue(() =>
{ {
var gameObj = _dalamudUtil.CreateGameObject(obj); var gameObj = _dalamudUtil.CreateGameObject(obj);
if (gameObj != null) if (gameObj is Character c)
{ {
Logger.Verbose("Glamourer applying for " + gameObj); Logger.Verbose("Glamourer applying for " + c.Address.ToString("X"));
_glamourerApplyAll!.InvokeAction(customization, gameObj); _glamourerApplyAll!.InvokeAction(customization, c);
} }
}); });
} }
@@ -227,10 +241,10 @@ namespace MareSynchronos.Managers
actionQueue.Enqueue(() => actionQueue.Enqueue(() =>
{ {
var gameObj = _dalamudUtil.CreateGameObject(character); var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null) if (gameObj is Character c)
{ {
Logger.Verbose("Glamourer apply only equipment to " + character.ToString("X")); Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X"));
_glamourerApplyOnlyEquipment!.InvokeAction(customization, gameObj); _glamourerApplyOnlyEquipment!.InvokeAction(customization, c);
} }
}); });
} }
@@ -241,10 +255,10 @@ namespace MareSynchronos.Managers
actionQueue.Enqueue(() => actionQueue.Enqueue(() =>
{ {
var gameObj = _dalamudUtil.CreateGameObject(character); var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null) if (gameObj is Character c)
{ {
Logger.Verbose("Glamourer apply only customization to " + character.ToString("X")); Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X"));
_glamourerApplyOnlyCustomization!.InvokeAction(customization, gameObj); _glamourerApplyOnlyCustomization!.InvokeAction(customization, c);
} }
}); });
} }
@@ -255,9 +269,9 @@ namespace MareSynchronos.Managers
try try
{ {
var gameObj = _dalamudUtil.CreateGameObject(character); var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null) if (gameObj is Character c)
{ {
var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(gameObj); var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c);
byte[] bytes = Convert.FromBase64String(glamourerString); byte[] bytes = Convert.FromBase64String(glamourerString);
// ignore transparency // ignore transparency
bytes[88] = 128; bytes[88] = 128;

View File

@@ -28,6 +28,7 @@ namespace MareSynchronos.Managers
private readonly Dictionary<ObjectKind, Func<bool>> objectKindsToUpdate = new(); private readonly Dictionary<ObjectKind, Func<bool>> objectKindsToUpdate = new();
private CancellationTokenSource? _playerChangedCts = new(); private CancellationTokenSource? _playerChangedCts = new();
private CancellationTokenSource _transientUpdateCts = new();
private List<PlayerRelatedObject> playerRelatedObjects = new List<PlayerRelatedObject>(); private List<PlayerRelatedObject> playerRelatedObjects = new List<PlayerRelatedObject>();
@@ -69,8 +70,19 @@ namespace MareSynchronos.Managers
{ {
if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) if (obj.Address == gameObj && !obj.HasUnprocessedUpdate)
{ {
obj.HasUnprocessedUpdate = true; _transientUpdateCts.Cancel();
OnPlayerOrAttachedObjectsChanged(); _transientUpdateCts = new CancellationTokenSource();
var token = _transientUpdateCts.Token;
Task.Run(async () =>
{
Logger.Debug("Delaying transient resource load update");
await Task.Delay(750, token);
if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return;
Logger.Debug("Firing transient resource load update");
obj.HasTransientsUpdate = true;
OnPlayerOrAttachedObjectsChanged();
}, token);
return; return;
} }
} }
@@ -82,7 +94,7 @@ namespace MareSynchronos.Managers
if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing)
{ {
Logger.Debug("Heels offset changed to " + change); Logger.Debug("Heels offset changed to " + change);
playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasUnprocessedUpdate = true; playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasTransientsUpdate = true;
} }
} }
@@ -129,14 +141,15 @@ namespace MareSynchronos.Managers
private async Task<CharacterCacheDto?> CreateFullCharacterCacheDto(CancellationToken token) private async Task<CharacterCacheDto?> CreateFullCharacterCacheDto(CancellationToken token)
{ {
foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate).ToList()) foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList())
{ {
Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind);
PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject.ObjectKind, unprocessedObject.Address, token); PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token);
if (!token.IsCancellationRequested) if (!token.IsCancellationRequested)
{ {
unprocessedObject.HasUnprocessedUpdate = false; unprocessedObject.HasUnprocessedUpdate = false;
unprocessedObject.IsProcessing = false; unprocessedObject.IsProcessing = false;
unprocessedObject.HasTransientsUpdate = false;
} }
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
} }
@@ -165,7 +178,6 @@ namespace MareSynchronos.Managers
if (address == item.Address) if (address == item.Address)
{ {
Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); Logger.Debug("Penumbra redraw Event for " + item.ObjectKind);
//_transientResourceManager.CleanSemiTransientResources(item.ObjectKind);
item.HasUnprocessedUpdate = true; item.HasUnprocessedUpdate = true;
} }
} }
@@ -178,7 +190,7 @@ namespace MareSynchronos.Managers
private void OnPlayerOrAttachedObjectsChanged() private void OnPlayerOrAttachedObjectsChanged()
{ {
var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate).ToList(); var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList();
foreach (var unprocessedObject in unprocessedObjects) foreach (var unprocessedObject in unprocessedObjects)
{ {
unprocessedObject.IsProcessing = true; unprocessedObject.IsProcessing = true;

View File

@@ -2,8 +2,11 @@
using MareSynchronos.Models; using MareSynchronos.Models;
using MareSynchronos.Utils; using MareSynchronos.Utils;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MareSynchronos.Managers namespace MareSynchronos.Managers
{ {
@@ -16,8 +19,9 @@ namespace MareSynchronos.Managers
public event TransientResourceLoadedEvent? TransientResourceLoaded; public event TransientResourceLoadedEvent? TransientResourceLoaded;
private Dictionary<IntPtr, HashSet<string>> TransientResources { get; } = new(); private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
private Dictionary<ObjectKind, HashSet<FileReplacement>> SemiTransientResources { get; } = new(); private ConcurrentDictionary<ObjectKind, HashSet<FileReplacement>> SemiTransientResources { get; } = new();
private CancellationTokenSource transientInvokeDelayCts = new CancellationTokenSource();
public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil)
{ {
manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent;
@@ -42,7 +46,7 @@ namespace MareSynchronos.Managers
if (!dalamudUtil.IsGameObjectPresent(item.Key)) if (!dalamudUtil.IsGameObjectPresent(item.Key))
{ {
Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); Logger.Debug("Object not present anymore: " + item.Key.ToString("X"));
TransientResources.Remove(item.Key); TransientResources.TryRemove(item.Key, out _);
} }
} }
} }
@@ -138,13 +142,23 @@ namespace MareSynchronos.Managers
SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()); SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant());
} }
if (!SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant())) try
{ {
Logger.Debug("Persisting " + item.ToLowerInvariant()); if (!SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()))
var fileReplacement = createFileReplacement(item.ToLowerInvariant(), true); {
if (!fileReplacement.HasFileReplacement) Logger.Debug("Persisting " + item.ToLowerInvariant());
fileReplacement = createFileReplacement(item.ToLowerInvariant(), false);
SemiTransientResources[objectKind].Add(fileReplacement); var fileReplacement = createFileReplacement(item.ToLowerInvariant(), true);
if (!fileReplacement.HasFileReplacement)
fileReplacement = createFileReplacement(item.ToLowerInvariant(), false);
SemiTransientResources[objectKind].Add(fileReplacement);
}
}
catch (Exception ex)
{
Logger.Warn("Issue during transient file persistence");
Logger.Warn(ex.Message);
Logger.Warn(ex.StackTrace.ToString());
} }
} }

View File

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

View File

@@ -35,7 +35,7 @@ namespace MareSynchronos.Models
public void SetResolvedPath(string path) public void SetResolvedPath(string path)
{ {
ResolvedPath = path.ToLowerInvariant().Replace('/', '\\').Replace(_penumbraDirectory, "").Replace('\\', '/'); ResolvedPath = path.ToLowerInvariant();//.Replace('/', '\\').Replace(_penumbraDirectory, "").Replace('\\', '/');
if (!HasFileReplacement || IsFileSwap) return; if (!HasFileReplacement || IsFileSwap) return;
_ = Task.Run(() => _ = Task.Run(() =>

View File

@@ -7,7 +7,7 @@ using Penumbra.GameData.ByteString;
namespace MareSynchronos.Models namespace MareSynchronos.Models
{ {
internal class PlayerRelatedObject public class PlayerRelatedObject
{ {
private readonly Func<IntPtr> getAddress; private readonly Func<IntPtr> getAddress;
@@ -46,6 +46,7 @@ namespace MareSynchronos.Models
public byte? HatState { get; set; } public byte? HatState { get; set; }
public byte? VisorWeaponState { get; set; } public byte? VisorWeaponState { get; set; }
public bool HasTransientsUpdate { get; set; } = false;
public bool HasUnprocessedUpdate { get; set; } = false; public bool HasUnprocessedUpdate { get; set; } = false;
public bool DoNotSendUpdate { get; set; } = false; public bool DoNotSendUpdate { get; set; } = false;
public bool IsProcessing { get; set; } = false; public bool IsProcessing { get; set; } = false;