rework cache creation conditions

clean up some stuff

revert pet clearing

fix initial cache creation not happening without changes/redraws

fix draw conditions when framework inactive
This commit is contained in:
Stanley Dimant
2025-02-23 03:04:08 +01:00
committed by Loporrit
parent c1940767bf
commit f033b1aa74
13 changed files with 299 additions and 321 deletions

View File

@@ -246,7 +246,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void DalamudUtil_FrameworkUpdate()
{
_cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.CurrentAddress(), c => c.ObjectKind));
_cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.Address, c => c.ObjectKind));
lock (_cacheAdditionLock)
{
_cachedHandledPaths.Clear();
@@ -371,7 +371,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (isAdded)
{
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
SendTransients(gameObjectAddress);
SendTransients(gameObjectAddress, objectKind);
}
}
}
@@ -382,7 +382,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
}
}
private void SendTransients(nint gameObject)
private void SendTransients(nint gameObject, ObjectKind objectKind)
{
_ = Task.Run(async () =>
{
@@ -391,7 +391,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_sendTransientCts = new();
var token = _sendTransientCts.Token;
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
foreach (var kvp in TransientResources)
{
if (TransientResources.TryGetValue(objectKind, out var values) && values.Any())
{
Logger.LogTrace("Sending Transients for {kind}", objectKind);
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
}
}
});
}

View File

@@ -15,6 +15,32 @@ public class CharacterData
public string PetNamesData { get; set; } = string.Empty;
public string MoodlesData { get; set; } = string.Empty;
public void SetFragment(ObjectKind kind, CharacterDataFragment? fragment)
{
if (kind == ObjectKind.Player)
{
var playerFragment = (fragment as CharacterDataFragmentPlayer);
HeelsData = playerFragment?.HeelsData ?? string.Empty;
HonorificData = playerFragment?.HonorificData ?? string.Empty;
ManipulationString = playerFragment?.ManipulationString ?? string.Empty;
MoodlesData = playerFragment?.MoodlesData ?? string.Empty;
PetNamesData = playerFragment?.PetNamesData ?? string.Empty;
}
if (fragment is null)
{
CustomizePlusScale.Remove(kind);
FileReplacements.Remove(kind);
GlamourerString.Remove(kind);
}
else
{
CustomizePlusScale[kind] = fragment.CustomizePlusScale;
FileReplacements[kind] = fragment.FileReplacements;
GlamourerString[kind] = fragment.GlamourerString;
}
}
public API.Data.CharacterData ToAPI()
{
Dictionary<ObjectKind, List<FileReplacementData>> fileReplacements =

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.PlayerData.Data;
public class CharacterDataFragment
{
public string CustomizePlusScale { get; set; } = string.Empty;
public HashSet<FileReplacement> FileReplacements { get; set; } = [];
public string GlamourerString { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,10 @@
namespace MareSynchronos.PlayerData.Data;
public class CharacterDataFragmentPlayer : CharacterDataFragment
{
public string HeelsData { get; set; } = string.Empty;
public string HonorificData { get; set; } = string.Empty;
public string ManipulationString { get; set; } = string.Empty;
public string MoodlesData { get; set; } = string.Empty;
public string PetNamesData { get; set; } = string.Empty;
}

View File

@@ -38,14 +38,14 @@ public class PlayerDataFactory
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
}
public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
{
if (!_ipcManager.Initialized)
{
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
}
if (playerRelatedObject == null) return;
if (playerRelatedObject == null) return null;
bool pointerIsZero = true;
try
@@ -69,23 +69,15 @@ public class PlayerDataFactory
if (pointerIsZero)
{
_logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind);
previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind);
previousData.CustomizePlusScale.Remove(playerRelatedObject.ObjectKind);
return;
return null;
}
var previousFileReplacements = previousData.FileReplacements.ToDictionary(d => d.Key, d => d.Value);
var previousGlamourerData = previousData.GlamourerString.ToDictionary(d => d.Key, d => d.Value);
var previousCustomize = previousData.CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value);
try
{
await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
return await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
{
await CreateCharacterData(previousData, playerRelatedObject, token).ConfigureAwait(false);
return await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false);
}).ConfigureAwait(true);
return;
}
catch (OperationCanceledException)
{
@@ -97,9 +89,7 @@ public class PlayerDataFactory
_logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject);
}
previousData.FileReplacements = previousFileReplacements;
previousData.GlamourerString = previousGlamourerData;
previousData.CustomizePlusScale = previousCustomize;
return null;
}
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
@@ -112,32 +102,25 @@ public class PlayerDataFactory
return ((Character*)playerPointer)->GameObject.DrawObject == null;
}
private async Task<CharacterData> CreateCharacterData(CharacterData data, GameObjectHandler playerRelatedObject, CancellationToken token)
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
{
var objectKind = playerRelatedObject.ObjectKind;
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
if (!data.FileReplacements.TryGetValue(objectKind, out HashSet<FileReplacement>? value))
{
data.FileReplacements[objectKind] = new(FileReplacementComparer.Instance);
}
else
{
value.Clear();
}
data.CustomizePlusScale.Remove(objectKind);
// wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false);
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
int totalWaitTime = 10000;
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
{
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50, token).ConfigureAwait(false);
await Task.Delay(50, ct).ConfigureAwait(false);
totalWaitTime -= 50;
}
ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player
? null
@@ -151,24 +134,29 @@ public class PlayerDataFactory
resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
data.FileReplacements[objectKind] =
ct.ThrowIfCancellationRequested();
fragment.FileReplacements =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet();
data.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
ct.ThrowIfCancellationRequested();
_logger.LogDebug("== Static Replacements ==");
foreach (var replacement in data.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{
_logger.LogDebug("=> {repl}", replacement);
ct.ThrowIfCancellationRequested();
}
await _transientResourceManager.WaitForRecording(token).ConfigureAwait(false);
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
// or we get into redraw city for every change and nothing works properly
if (objectKind == ObjectKind.Pet)
{
foreach (var item in data.FileReplacements[ObjectKind.Pet].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
foreach (var item in fragment.FileReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
{
if (_transientResourceManager.AddTransientResource(objectKind, item))
{
@@ -176,14 +164,16 @@ public class PlayerDataFactory
}
}
_logger.LogTrace("Clearing {count} Static Replacements for Pet", data.FileReplacements[ObjectKind.Pet].Count);
data.FileReplacements[ObjectKind.Pet].Clear();
_logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
fragment.FileReplacements.Clear();
}
ct.ThrowIfCancellationRequested();
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
_transientResourceManager.ClearTransientPaths(objectKind, data.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList());
_transientResourceManager.ClearTransientPaths(objectKind, fragment.FileReplacements.SelectMany(c => c.GamePaths).ToList());
// get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind);
@@ -193,63 +183,67 @@ public class PlayerDataFactory
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{
_logger.LogDebug("=> {repl}", replacement);
data.FileReplacements[objectKind].Add(replacement);
fragment.FileReplacements.Add(replacement);
}
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. data.FileReplacements[objectKind]]);
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
ct.ThrowIfCancellationRequested();
// make sure we only return data that actually has file replacements
foreach (var item in data.FileReplacements)
{
data.FileReplacements[item.Key] = new HashSet<FileReplacement>(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
}
fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
// gather up data from ipc
data.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
data.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", data.GlamourerString[playerRelatedObject.ObjectKind]);
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
var customizeScale = await getCustomizeData.ConfigureAwait(false);
data.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty;
_logger.LogDebug("Customize is now: {data}", data.CustomizePlusScale[playerRelatedObject.ObjectKind]);
data.HonorificData = await getHonorificTitle.ConfigureAwait(false);
_logger.LogDebug("Honorific is now: {data}", data.HonorificData);
data.HeelsData = await getHeelsOffset.ConfigureAwait(false);
_logger.LogDebug("Heels is now: {heels}", data.HeelsData);
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
if (objectKind == ObjectKind.Player)
{
data.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
_logger.LogDebug("Moodles is now: {moodles}", data.MoodlesData);
var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
data.PetNamesData = _ipcManager.PetNames.GetLocalNames();
_logger.LogDebug("Pet Nicknames is now: {petnames}", data.PetNamesData);
playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
playerFragment!.HeelsData = await getHeelsOffset.ConfigureAwait(false);
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
playerFragment!.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
playerFragment!.PetNamesData = _ipcManager.PetNames.GetLocalNames();
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
}
if (data.FileReplacements.TryGetValue(objectKind, out HashSet<FileReplacement>? fileReplacements))
{
var toCompute = fileReplacements.Where(f => !f.IsFileSwap).ToArray();
ct.ThrowIfCancellationRequested();
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
foreach (var file in toCompute)
{
ct.ThrowIfCancellationRequested();
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
}
var removed = fileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
if (removed > 0)
{
_logger.LogDebug("Removed {amount} of invalid files", removed);
}
}
if (objectKind == ObjectKind.Player)
{
try
{
await VerifyPlayerAnimationBones(boneIndices, data, objectKind).ConfigureAwait(false);
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
}
catch (Exception e)
{
@@ -259,10 +253,10 @@ public class PlayerDataFactory
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
return data;
return fragment;
}
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterData previousData, ObjectKind objectKind)
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
{
if (boneIndices == null) return;
@@ -274,8 +268,10 @@ public class PlayerDataFactory
if (boneIndices.All(u => u.Value.Count == 0)) return;
int noValidationFailed = 0;
foreach (var file in previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
{
ct.ThrowIfCancellationRequested();
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
bool validationFailed = false;
if (skeletonIndices != null)
@@ -305,10 +301,10 @@ public class PlayerDataFactory
{
noValidationFailed++;
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
previousData.FileReplacements[objectKind].Remove(file);
fragment.FileReplacements.Remove(file);
foreach (var gamePath in file.GamePaths)
{
_transientResourceManager.RemoveTransientResource(objectKind, gamePath);
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
}
}

View File

@@ -2,7 +2,6 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
@@ -10,18 +9,15 @@ using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
namespace MareSynchronos.PlayerData.Handlers;
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
{
private readonly DalamudUtilService _dalamudUtil;
private readonly Func<IntPtr> _getAddress;
private readonly bool _isOwnedObject;
private readonly PerformanceCollectorService _performanceCollector;
private CancellationTokenSource? _clearCts = new();
private byte _classJob = 0;
private Task? _delayedZoningTask;
private bool _haltProcessing = false;
private bool _ignoreSendAfterRedraw = false;
private int _ptrNullCounter = 0;
private byte _classJob = 0;
private CancellationTokenSource _zoningCts = new();
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
@@ -76,12 +72,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
if (msg.Address == Address)
{
_haltProcessing = false;
_ = Task.Run(async () =>
{
_ignoreSendAfterRedraw = true;
await Task.Delay(500).ConfigureAwait(false);
_ignoreSendAfterRedraw = false;
});
}
});
@@ -90,22 +80,23 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
}
private enum DrawCondition
public enum DrawCondition
{
None,
ObjectZero,
DrawObjectZero,
RenderFlags,
ModelInSlotLoaded,
ModelFilesInSlotLoaded
}
public byte RaceId { get; private set; }
public byte Gender { get; private set; }
public byte TribeId { get; private set; }
public IntPtr Address { get; private set; }
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
public byte Gender { get; private set; }
public string Name { get; private set; }
public ObjectKind ObjectKind { get; }
public byte RaceId { get; private set; }
public byte TribeId { get; private set; }
private byte[] CustomizeData { get; set; } = new byte[26];
private IntPtr DrawObjectAddress { get; set; }
private byte[] EquipSlotData { get; set; } = new byte[40];
@@ -116,7 +107,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
{
while (await _dalamudUtil.RunOnFrameworkThread(() =>
{
if (IsBeingDrawn()) return true;
if (_haltProcessing) CheckAndUpdateObject();
if (CurrentDrawCondition != DrawCondition.None) return true;
var gameObj = _dalamudUtil.CreateGameObject(Address);
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
{
@@ -141,12 +133,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
}
}
public IntPtr CurrentAddress()
{
_dalamudUtil.EnsureIsOnFramework();
return _getAddress.Invoke();
}
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
{
return _dalamudUtil.CreateGameObject(Address);
@@ -185,15 +171,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
Address = _getAddress();
if (Address != IntPtr.Zero)
{
_ptrNullCounter = 0;
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
DrawObjectAddress = drawObjAddr;
CurrentDrawCondition = DrawCondition.None;
}
else
{
DrawObjectAddress = IntPtr.Zero;
CurrentDrawCondition = DrawCondition.DrawObjectZero;
}
CurrentDrawCondition = IsBeingDrawnUnsafe();
if (_haltProcessing) return;
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
@@ -201,12 +190,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
{
if (_clearCts != null)
{
Logger.LogDebug("[{this}] Cancelling Clear Task", this);
_clearCts.CancelDispose();
_clearCts = null;
}
var chara = (Character*)Address;
var name = chara->GameObject.NameString;
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
@@ -244,7 +227,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
}
if (equipDiff && !_isOwnedObject && !_ignoreSendAfterRedraw) // send the message out immediately and cancel out, no reason to continue if not self
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
{
Logger.LogTrace("[{this}] Changed", this);
Mediator.Publish(new CharacterChangedMessage(this));
@@ -288,24 +271,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
}
else if (addrDiff || drawObjDiff)
{
CurrentDrawCondition = DrawCondition.DrawObjectZero;
Logger.LogTrace("[{this}] Changed", this);
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
{
_clearCts?.CancelDispose();
_clearCts = new();
var token = _clearCts.Token;
_ = Task.Run(() => ClearAsync(token), token);
}
}
}
private async Task ClearAsync(CancellationToken token)
{
Logger.LogDebug("[{this}] Running Clear Task", this);
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
Logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this);
Mediator.Publish(new ClearCacheForObjectMessage(this));
_clearCts = null;
}
}
}
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
@@ -382,31 +354,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
}
}
private unsafe IntPtr GetDrawObjUnsafe(nint curPtr)
{
return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->DrawObject;
}
private bool IsBeingDrawn()
{
var curPtr = _getAddress();
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr: {ptr}", this, curPtr.ToString("X"));
if (curPtr == IntPtr.Zero && _ptrNullCounter < 2)
{
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, counter is {cnt}", this, _ptrNullCounter);
_ptrNullCounter++;
return true;
}
if (curPtr == IntPtr.Zero)
{
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, returning", this);
Address = IntPtr.Zero;
DrawObjectAddress = IntPtr.Zero;
throw new ArgumentNullException($"CurPtr for {this} turned ZERO");
}
if (_haltProcessing) CheckAndUpdateObject();
if (_dalamudUtil.IsAnythingDrawing)
{
@@ -414,27 +364,23 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
return true;
}
var drawObj = GetDrawObjUnsafe(curPtr);
Logger.LogTrace("[{this}] IsBeingDrawn, DrawObjPtr: {ptr}", this, drawObj.ToString("X"));
var isDrawn = IsBeingDrawnUnsafe(drawObj, curPtr);
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, isDrawn);
return isDrawn != DrawCondition.None;
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, CurrentDrawCondition);
return CurrentDrawCondition != DrawCondition.None;
}
private unsafe DrawCondition IsBeingDrawnUnsafe(IntPtr drawObj, IntPtr curPtr)
private unsafe DrawCondition IsBeingDrawnUnsafe()
{
var drawObjZero = drawObj == IntPtr.Zero;
if (drawObjZero) return DrawCondition.DrawObjectZero;
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->RenderFlags) != 0x0;
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0;
if (renderFlags) return DrawCondition.RenderFlags;
if (ObjectKind == ObjectKind.Player)
{
var modelInSlotLoaded = (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0);
var modelInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelInSlotLoaded != 0);
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
var modelFilesInSlotLoaded = (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0);
var modelFilesInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelFilesInSlotLoaded != 0);
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
return DrawCondition.None;
}
return DrawCondition.None;
@@ -442,11 +388,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
private void ZoneSwitchEnd()
{
if (!_isOwnedObject || _haltProcessing) return;
if (!_isOwnedObject) return;
_clearCts?.Cancel();
_clearCts?.Dispose();
_clearCts = null;
try
{
_zoningCts?.CancelAfter(2500);
@@ -463,7 +406,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
private void ZoneSwitchStart()
{
if (!_isOwnedObject || _haltProcessing) return;
if (!_isOwnedObject) return;
_zoningCts = new();
Logger.LogDebug("[{obj}] Starting Delay After Zoning", this);

View File

@@ -8,37 +8,26 @@ using Microsoft.Extensions.Logging;
namespace MareSynchronos.PlayerData.Services;
#pragma warning disable MA0040
public sealed class CacheCreationService : DisposableMediatorSubscriberBase
{
private readonly SemaphoreSlim _cacheCreateLock = new(1);
private readonly Dictionary<ObjectKind, GameObjectHandler> _cachesToCreate = [];
private readonly HashSet<ObjectKind> _cachesToCreate = [];
private readonly PlayerDataFactory _characterDataFactory;
private readonly CancellationTokenSource _cts = new();
private readonly HashSet<ObjectKind> _currentlyCreating = [];
private readonly HashSet<ObjectKind> _debouncedObjectCache = [];
private readonly CharacterData _playerData = new();
private readonly Dictionary<ObjectKind, GameObjectHandler> _playerRelatedObjects = [];
private Task? _cacheCreationTask;
private CancellationTokenSource _honorificCts = new();
private CancellationTokenSource _petNicknamesCts = new();
private CancellationTokenSource _moodlesCts = new();
private bool _isZoning = false;
private readonly CancellationTokenSource _runtimeCts = new();
private CancellationTokenSource _creationCts = new();
private CancellationTokenSource _debounceCts = new();
private bool _haltCharaDataCreation;
private readonly Dictionary<ObjectKind, CancellationTokenSource> _glamourerCts = new();
private bool _isZoning = false;
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator)
{
_characterDataFactory = characterDataFactory;
Mediator.Subscribe<CreateCacheForObjectMessage>(this, (msg) =>
{
Logger.LogDebug("Received CreateCacheForObject for {handler}, updating", msg.ObjectToCreateFor);
_cacheCreateLock.Wait();
_cachesToCreate[msg.ObjectToCreateFor.ObjectKind] = msg.ObjectToCreateFor;
_cacheCreateLock.Release();
});
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (msg) => _isZoning = true);
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false);
@@ -47,6 +36,12 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
_haltCharaDataCreation = !msg.Resume;
});
Mediator.Subscribe<CreateCacheForObjectMessage>(this, (msg) =>
{
Logger.LogDebug("Received CreateCacheForObject for {handler}, updating", msg.ObjectToCreateFor);
AddCacheToCreate(msg.ObjectToCreateFor.ObjectKind);
});
_playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true)
.GetAwaiter().GetResult();
_playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true)
@@ -58,77 +53,64 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
{
if (msg.GameObjectHandler != _playerRelatedObjects[ObjectKind.Player]) return;
Logger.LogTrace("Removing pet data for {obj}", msg.GameObjectHandler);
_playerData.FileReplacements.Remove(ObjectKind.Pet);
_playerData.GlamourerString.Remove(ObjectKind.Pet);
_playerData.CustomizePlusScale.Remove(ObjectKind.Pet);
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
if (msg.GameObjectHandler == _playerRelatedObjects[ObjectKind.Player])
{
AddCacheToCreate(ObjectKind.Player);
AddCacheToCreate(ObjectKind.Pet);
}
});
Mediator.Subscribe<ClearCacheForObjectMessage>(this, (msg) =>
{
// ignore pets
if (msg.ObjectToCreateFor == _playerRelatedObjects[ObjectKind.Pet]) return;
_ = Task.Run(() =>
if (msg.ObjectToCreateFor.ObjectKind == ObjectKind.Pet)
{
Logger.LogTrace("Clearing cache for {obj}", msg.ObjectToCreateFor);
_playerData.FileReplacements.Remove(msg.ObjectToCreateFor.ObjectKind);
_playerData.GlamourerString.Remove(msg.ObjectToCreateFor.ObjectKind);
_playerData.CustomizePlusScale.Remove(msg.ObjectToCreateFor.ObjectKind);
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
});
Logger.LogTrace("Received clear cache for {obj}, ignoring", msg.ObjectToCreateFor);
return;
}
Logger.LogDebug("Clearing cache for {obj}", msg.ObjectToCreateFor);
AddCacheToCreate(msg.ObjectToCreateFor.ObjectKind);
});
Mediator.Subscribe<CustomizePlusMessage>(this, (msg) =>
{
if (_isZoning) return;
_ = Task.Run(async () =>
{
foreach (var item in _playerRelatedObjects
.Where(item => msg.Address == null
|| item.Value.Address == msg.Address).Select(k => k.Key))
{
Logger.LogDebug("Received CustomizePlus change, updating {obj}", item);
await AddPlayerCacheToCreate(item).ConfigureAwait(false);
AddCacheToCreate(item);
}
});
});
Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) =>
{
if (_isZoning) return;
Logger.LogDebug("Received Heels Offset change, updating player");
_ = AddPlayerCacheToCreate();
AddCacheToCreate();
});
Mediator.Subscribe<GlamourerChangedMessage>(this, (msg) =>
{
if (_isZoning) return;
var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address);
if (changedType.Key != default || changedType.Value != default)
{
GlamourerChanged(changedType.Key);
Logger.LogDebug("Received GlamourerChangedMessage for {kind}", changedType);
AddCacheToCreate(changedType.Key);
}
});
Mediator.Subscribe<HonorificMessage>(this, (msg) =>
{
if (_isZoning) return;
if (!string.Equals(msg.NewHonorificTitle, _playerData.HonorificData, StringComparison.Ordinal))
{
Logger.LogDebug("Received Honorific change, updating player");
HonorificChanged();
}
});
Mediator.Subscribe<PetNamesMessage>(this, (msg) =>
{
if (_isZoning) return;
if (!string.Equals(msg.PetNicknamesData, _playerData.PetNamesData, StringComparison.Ordinal))
{
Logger.LogDebug("Received Pet Nicknames change, updating player");
PetNicknamesChanged();
AddCacheToCreate(ObjectKind.Player);
}
});
Mediator.Subscribe<MoodlesMessage>(this, (msg) =>
{
if (_isZoning) return;
@@ -136,16 +118,30 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
if (changedType.Key == ObjectKind.Player && changedType.Value != default)
{
Logger.LogDebug("Received Moodles change, updating player");
MoodlesChanged();
AddCacheToCreate(ObjectKind.Player);
}
});
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (msg) =>
Mediator.Subscribe<PetNamesMessage>(this, (msg) =>
{
Logger.LogDebug("Received Penumbra Mod settings change, updating player");
AddPlayerCacheToCreate().GetAwaiter().GetResult();
if (_isZoning) return;
if (!string.Equals(msg.PetNicknamesData, _playerData.PetNamesData, StringComparison.Ordinal))
{
Logger.LogDebug("Received Pet Nicknames change, updating player");
AddCacheToCreate(ObjectKind.Player);
}
});
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (msg) => ProcessCacheCreation());
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (msg) =>
{
Logger.LogDebug("Received Penumbra Mod settings change, updating everything");
AddCacheToCreate(ObjectKind.Player);
AddCacheToCreate(ObjectKind.Pet);
AddCacheToCreate(ObjectKind.MinionOrMount);
AddCacheToCreate(ObjectKind.Companion);
});
Mediator.Subscribe<FrameworkUpdateMessage>(this, (msg) => ProcessCacheCreation());
}
protected override void Dispose(bool disposing)
@@ -153,96 +149,86 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
base.Dispose(disposing);
_playerRelatedObjects.Values.ToList().ForEach(p => p.Dispose());
_cts.Dispose();
_runtimeCts.Cancel();
_runtimeCts.Dispose();
_creationCts.Cancel();
_creationCts.Dispose();
}
private async Task AddPlayerCacheToCreate(ObjectKind kind = ObjectKind.Player)
private void AddCacheToCreate(ObjectKind kind = ObjectKind.Player)
{
await _cacheCreateLock.WaitAsync().ConfigureAwait(false);
_cachesToCreate[kind] = _playerRelatedObjects[kind];
_debounceCts.Cancel();
_debounceCts.Dispose();
_debounceCts = new();
var token = _debounceCts.Token;
_cacheCreateLock.Wait();
_debouncedObjectCache.Add(kind);
_cacheCreateLock.Release();
}
private void GlamourerChanged(ObjectKind kind)
{
if (_glamourerCts.TryGetValue(kind, out var cts))
{
_glamourerCts[kind]?.Cancel();
_glamourerCts[kind]?.Dispose();
}
_glamourerCts[kind] = new();
var token = _glamourerCts[kind].Token;
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(500), token).ConfigureAwait(false);
await AddPlayerCacheToCreate(kind).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
Logger.LogTrace("Debounce complete, inserting objects to create for: {obj}", string.Join(", ", _debouncedObjectCache));
await _cacheCreateLock.WaitAsync(token).ConfigureAwait(false);
foreach (var item in _debouncedObjectCache)
{
_cachesToCreate.Add(item);
}
_debouncedObjectCache.Clear();
_cacheCreateLock.Release();
});
}
private void HonorificChanged()
{
_honorificCts?.Cancel();
_honorificCts?.Dispose();
_honorificCts = new();
var token = _honorificCts.Token;
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
await AddPlayerCacheToCreate().ConfigureAwait(false);
}, token);
}
private void PetNicknamesChanged()
{
_petNicknamesCts?.Cancel();
_petNicknamesCts?.Dispose();
_petNicknamesCts = new();
var token = _petNicknamesCts.Token;
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
await AddPlayerCacheToCreate().ConfigureAwait(false);
}, token);
}
private void MoodlesChanged()
{
_moodlesCts?.Cancel();
_moodlesCts?.Dispose();
_moodlesCts = new();
var token = _moodlesCts.Token;
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
await AddPlayerCacheToCreate().ConfigureAwait(false);
}, token);
}
private void ProcessCacheCreation()
{
if (_isZoning || _haltCharaDataCreation) return;
if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true))
if (_cachesToCreate.Count == 0) return;
if (_playerRelatedObjects.Any(p => p.Value.CurrentDrawCondition is
not (GameObjectHandler.DrawCondition.None or GameObjectHandler.DrawCondition.DrawObjectZero or GameObjectHandler.DrawCondition.ObjectZero)))
{
Logger.LogDebug("Waiting for draw to finish before executing cache creation");
return;
}
_creationCts.Cancel();
_creationCts.Dispose();
_creationCts = new();
_cacheCreateLock.Wait();
var toCreate = _cachesToCreate.ToList();
var objectKindsToCreate = _cachesToCreate.ToList();
foreach (var creationObj in objectKindsToCreate)
{
_currentlyCreating.Add(creationObj);
}
_cachesToCreate.Clear();
_cacheCreateLock.Release();
_cacheCreationTask = Task.Run(async () =>
_ = Task.Run(async () =>
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_creationCts.Token, _runtimeCts.Token);
Logger.LogDebug("Creating Caches for {objectKinds}", string.Join(", ", objectKindsToCreate));
try
{
foreach (var obj in toCreate)
Dictionary<ObjectKind, CharacterDataFragment?> createdData = [];
foreach (var objectKind in objectKindsToCreate)
{
await _characterDataFactory.BuildCharacterData(_playerData, obj.Value, _cts.Token).ConfigureAwait(false);
createdData[objectKind] = await _characterDataFactory.BuildCharacterData(_playerRelatedObjects[objectKind], linkedCts.Token).ConfigureAwait(false);
}
foreach (var kvp in createdData)
{
_playerData.SetFragment(kvp.Key, kvp.Value);
}
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
_currentlyCreating.Clear();
}
catch (OperationCanceledException)
{
Logger.LogDebug("Cache Creation cancelled");
}
catch (Exception ex)
{
@@ -252,12 +238,6 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
{
Logger.LogDebug("Cache Creation complete");
}
}, _cts.Token);
}
else if (_cachesToCreate.Any())
{
Logger.LogDebug("Cache Creation stored until previous creation finished");
}
});
}
}
#pragma warning restore MA0040

View File

@@ -150,6 +150,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<UidDisplayHandler>();
collection.AddSingleton<PluginWatcherService>();
collection.AddSingleton<PlayerPerformanceService>();
collection.AddSingleton<TransientResourceManager>();
collection.AddSingleton<CharaDataManager>();
collection.AddSingleton<CharaDataFileHandler>();
@@ -224,7 +225,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<IPopupHandler, ReportPopupHandler>();
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
collection.AddScoped<CacheCreationService>();
collection.AddScoped<TransientResourceManager>();
collection.AddScoped<PlayerDataFactory>();
collection.AddScoped<OnlinePlayerManager>();
collection.AddScoped<UiService>();

View File

@@ -88,7 +88,8 @@ public sealed class CharaDataFileHandler : IDisposable
using var tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(chara.ObjectIndex)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false);
PlayerData.Data.CharacterData newCdata = new();
await _playerDataFactory.BuildCharacterData(newCdata, tempHandler, CancellationToken.None).ConfigureAwait(false);
var fragment = await _playerDataFactory.BuildCharacterData(tempHandler, CancellationToken.None).ConfigureAwait(false);
newCdata.SetFragment(ObjectKind.Player, fragment);
if (newCdata.FileReplacements.TryGetValue(ObjectKind.Player, out var playerData) && playerData != null)
{
foreach (var data in playerData.Select(g => g.GamePaths))

View File

@@ -493,15 +493,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
{
if (!_clientState.IsLoggedIn) return;
if (ct == null)
ct = CancellationToken.None;
const int tick = 250;
int curWaitTime = 0;
try
{
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
await Task.Delay(tick).ConfigureAwait(true);
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
curWaitTime += tick;
while ((!ct?.IsCancellationRequested ?? true)
while ((!ct.Value.IsCancellationRequested)
&& curWaitTime < timeOut
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
{

View File

@@ -0,0 +1,3 @@
namespace MareSynchronos.Services.Mediator;
public interface IHighPriorityMediatorSubscriber : IMediatorSubscriber { }

View File

@@ -91,6 +91,7 @@ public sealed class MareMediator : IHostedService
{
_messageQueue.Clear();
_loopCts.Cancel();
_loopCts.Dispose();
return Task.CompletedTask;
}
@@ -157,7 +158,7 @@ public sealed class MareMediator : IHostedService
List<SubscriberAction> subscribersCopy = [];
lock (_addRemoveLock)
{
subscribersCopy = subscribers?.Where(s => s.Subscriber != null).ToList() ?? [];
subscribersCopy = subscribers?.Where(s => s.Subscriber != null).OrderBy(k => k.Subscriber is IHighPriorityMediatorSubscriber ? 0 : 1).ToList() ?? [];
}
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
@@ -165,7 +166,7 @@ public sealed class MareMediator : IHostedService
if (!_genericExecuteMethods.TryGetValue((msgType, message.SubscriberKey), out var methodInfo))
{
_genericExecuteMethods[(msgType, message.SubscriberKey)] = methodInfo = GetType()
.GetMethod(nameof(ExecuteReflected), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
.GetMethod(nameof(ExecuteReflected), BindingFlags.NonPublic | BindingFlags.Instance)?
.MakeGenericMethod(msgType);
}

View File

@@ -52,8 +52,8 @@ public record HaltScanMessage(string Source) : MessageBase;
public record ResumeScanMessage(string Source) : MessageBase;
public record NotificationMessage
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;