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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
@@ -40,7 +38,7 @@ public class CharacterDataFactory
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)
{
@@ -50,20 +48,20 @@ public class CharacterDataFactory
bool pointerIsZero = true;
try
{
pointerIsZero = CheckForPointer(playerPointer);
pointerIsZero = CheckForPointer(playerRelatedObject.Address);
}
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.StackTrace ?? string.Empty);
}
if (pointerIsZero)
{
Logger.Verbose("Pointer was zero for " + objectKind);
previousData.FileReplacements.Remove(objectKind);
previousData.GlamourerString.Remove(objectKind);
Logger.Verbose("Pointer was zero for " + playerRelatedObject.ObjectKind);
previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind);
previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind);
return previousData;
}
@@ -72,7 +70,7 @@ public class CharacterDataFactory
try
{
return CreateCharacterData(previousData, objectKind, playerPointer, token);
return CreateCharacterData(previousData, playerRelatedObject, token);
}
catch (OperationCanceledException)
{
@@ -80,7 +78,7 @@ public class CharacterDataFactory
}
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.StackTrace ?? string.Empty);
}
@@ -232,66 +230,75 @@ public class CharacterDataFactory
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))
{
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);
var objectKind = playerRelatedObject.ObjectKind;
var charaPointer = playerRelatedObject.Address;
Stopwatch st = Stopwatch.StartNew();
previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations();
previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer);
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
if (playerRelatedObject.HasUnprocessedUpdate)
{
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
Logger.Debug("Handling unprocessed update for " + objectKind);
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])
{
transientResourceManager.RemoveTransientResource(charaPointer, item);
}
var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject();
for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx)
{
var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
continue;
}
if (objectKind == ObjectKind.Player)
{
AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human);
}
token.ThrowIfCancellationRequested();
AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0);
}
if (objectKind == ObjectKind.Pet)
{
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);
st.Stop();
@@ -387,8 +394,6 @@ public class CharacterDataFactory
{
transientResourceManager.RemoveTransientResource(charaPointer, item);
}
previousData.HeelsOffset = _ipcManager.GetHeelsOffset();
}
private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache)

View File

@@ -117,6 +117,17 @@ public class CachedPlayer
continue;
}
}
if (objectKind == ObjectKind.Player)
{
bool heelsOffsetDifferent = _cachedData.HeelsOffset != characterData.HeelsOffset;
if (heelsOffsetDifferent)
{
Logger.Debug("Updating " + objectKind);
charaDataToUpdate.Add(objectKind);
continue;
}
}
}
_cachedData = characterData;
@@ -324,6 +335,7 @@ public class CachedPlayer
{
_ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter);
_ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter);
_ipcManager.HeelsRestoreOffsetForPlayer(PlayerCharacter);
}
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)
{
if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return;
actionQueue.Enqueue(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(obj);
if (gameObj != null)
if (gameObj is Character c)
{
Logger.Verbose("Glamourer applying for " + gameObj);
_glamourerApplyAll!.InvokeAction(customization, gameObj);
Logger.Verbose("Glamourer applying for " + c.Address.ToString("X"));
_glamourerApplyAll!.InvokeAction(customization, c);
}
});
}
@@ -227,10 +241,10 @@ namespace MareSynchronos.Managers
actionQueue.Enqueue(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
if (gameObj is Character c)
{
Logger.Verbose("Glamourer apply only equipment to " + character.ToString("X"));
_glamourerApplyOnlyEquipment!.InvokeAction(customization, gameObj);
Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X"));
_glamourerApplyOnlyEquipment!.InvokeAction(customization, c);
}
});
}
@@ -241,10 +255,10 @@ namespace MareSynchronos.Managers
actionQueue.Enqueue(() =>
{
var gameObj = _dalamudUtil.CreateGameObject(character);
if (gameObj != null)
if (gameObj is Character c)
{
Logger.Verbose("Glamourer apply only customization to " + character.ToString("X"));
_glamourerApplyOnlyCustomization!.InvokeAction(customization, gameObj);
Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X"));
_glamourerApplyOnlyCustomization!.InvokeAction(customization, c);
}
});
}
@@ -255,9 +269,9 @@ namespace MareSynchronos.Managers
try
{
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);
// ignore transparency
bytes[88] = 128;

View File

@@ -28,6 +28,7 @@ namespace MareSynchronos.Managers
private readonly Dictionary<ObjectKind, Func<bool>> objectKindsToUpdate = new();
private CancellationTokenSource? _playerChangedCts = new();
private CancellationTokenSource _transientUpdateCts = new();
private List<PlayerRelatedObject> playerRelatedObjects = new List<PlayerRelatedObject>();
@@ -69,8 +70,19 @@ namespace MareSynchronos.Managers
{
if (obj.Address == gameObj && !obj.HasUnprocessedUpdate)
{
obj.HasUnprocessedUpdate = true;
OnPlayerOrAttachedObjectsChanged();
_transientUpdateCts.Cancel();
_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;
}
}
@@ -82,7 +94,7 @@ namespace MareSynchronos.Managers
if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing)
{
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)
{
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);
PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject.ObjectKind, unprocessedObject.Address, token);
PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token);
if (!token.IsCancellationRequested)
{
unprocessedObject.HasUnprocessedUpdate = false;
unprocessedObject.IsProcessing = false;
unprocessedObject.HasTransientsUpdate = false;
}
token.ThrowIfCancellationRequested();
}
@@ -165,7 +178,6 @@ namespace MareSynchronos.Managers
if (address == item.Address)
{
Logger.Debug("Penumbra redraw Event for " + item.ObjectKind);
//_transientResourceManager.CleanSemiTransientResources(item.ObjectKind);
item.HasUnprocessedUpdate = true;
}
}
@@ -178,7 +190,7 @@ namespace MareSynchronos.Managers
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)
{
unprocessedObject.IsProcessing = true;

View File

@@ -2,8 +2,11 @@
using MareSynchronos.Models;
using MareSynchronos.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MareSynchronos.Managers
{
@@ -16,8 +19,9 @@ namespace MareSynchronos.Managers
public event TransientResourceLoadedEvent? TransientResourceLoaded;
private Dictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
private Dictionary<ObjectKind, HashSet<FileReplacement>> SemiTransientResources { get; } = new();
private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
private ConcurrentDictionary<ObjectKind, HashSet<FileReplacement>> SemiTransientResources { get; } = new();
private CancellationTokenSource transientInvokeDelayCts = new CancellationTokenSource();
public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil)
{
manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent;
@@ -42,7 +46,7 @@ namespace MareSynchronos.Managers
if (!dalamudUtil.IsGameObjectPresent(item.Key))
{
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());
}
if (!SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()))
try
{
Logger.Debug("Persisting " + item.ToLowerInvariant());
var fileReplacement = createFileReplacement(item.ToLowerInvariant(), true);
if (!fileReplacement.HasFileReplacement)
fileReplacement = createFileReplacement(item.ToLowerInvariant(), false);
SemiTransientResources[objectKind].Add(fileReplacement);
if (!SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()))
{
Logger.Debug("Persisting " + item.ToLowerInvariant());
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>
<Authors></Authors>
<Company></Company>
<Version>0.4.1</Version>
<Version>0.4.2</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>

View File

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

View File

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