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() 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) lock (_cacheAdditionLock)
{ {
_cachedHandledPaths.Clear(); _cachedHandledPaths.Clear();
@@ -371,7 +371,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (isAdded) if (isAdded)
{ {
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath); 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 () => _ = Task.Run(async () =>
{ {
@@ -391,7 +391,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_sendTransientCts = new(); _sendTransientCts = new();
var token = _sendTransientCts.Token; var token = _sendTransientCts.Token;
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
Mediator.Publish(new TransientResourceChangedMessage(gameObject)); 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 PetNamesData { get; set; } = string.Empty;
public string MoodlesData { 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() public API.Data.CharacterData ToAPI()
{ {
Dictionary<ObjectKind, List<FileReplacementData>> fileReplacements = 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)); _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) if (!_ipcManager.Initialized)
{ {
throw new InvalidOperationException("Penumbra or Glamourer is not connected"); throw new InvalidOperationException("Penumbra or Glamourer is not connected");
} }
if (playerRelatedObject == null) return; if (playerRelatedObject == null) return null;
bool pointerIsZero = true; bool pointerIsZero = true;
try try
@@ -69,23 +69,15 @@ public class PlayerDataFactory
if (pointerIsZero) if (pointerIsZero)
{ {
_logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind); _logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind); return null;
previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind);
previousData.CustomizePlusScale.Remove(playerRelatedObject.ObjectKind);
return;
} }
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 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); }).ConfigureAwait(true);
return;
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -97,9 +89,7 @@ public class PlayerDataFactory
_logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject); _logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject);
} }
previousData.FileReplacements = previousFileReplacements; return null;
previousData.GlamourerString = previousGlamourerData;
previousData.CustomizePlusScale = previousCustomize;
} }
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer) private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
@@ -112,32 +102,25 @@ public class PlayerDataFactory
return ((Character*)playerPointer)->GameObject.DrawObject == null; 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; var objectKind = playerRelatedObject.ObjectKind;
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
_logger.LogDebug("Building character data for {obj}", playerRelatedObject); _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 // 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; int totalWaitTime = 10000;
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0) 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"); _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; totalWaitTime -= 50;
} }
ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices = Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player objectKind != ObjectKind.Player
? null ? null
@@ -151,24 +134,29 @@ public class PlayerDataFactory
resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false); resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data"); 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) new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet(); .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 =="); _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); _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 // 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 // or we get into redraw city for every change and nothing works properly
if (objectKind == ObjectKind.Pet) 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)) 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); _logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
data.FileReplacements[ObjectKind.Pet].Clear(); fragment.FileReplacements.Clear();
} }
ct.ThrowIfCancellationRequested();
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject); _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving // 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 // get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind); 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)) foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{ {
_logger.LogDebug("=> {repl}", replacement); _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) // 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 // make sure we only return data that actually has file replacements
foreach (var item in data.FileReplacements) fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
{
data.FileReplacements[item.Key] = new HashSet<FileReplacement>(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
}
// gather up data from ipc // gather up data from ipc
data.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync(); Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address); Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address); Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle(); Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
data.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false); fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", data.GlamourerString[playerRelatedObject.ObjectKind]); _logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
var customizeScale = await getCustomizeData.ConfigureAwait(false); var customizeScale = await getCustomizeData.ConfigureAwait(false);
data.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty; fragment.CustomizePlusScale = customizeScale ?? string.Empty;
_logger.LogDebug("Customize is now: {data}", data.CustomizePlusScale[playerRelatedObject.ObjectKind]); _logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
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);
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
data.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty; var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
_logger.LogDebug("Moodles is now: {moodles}", data.MoodlesData); playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
data.PetNamesData = _ipcManager.PetNames.GetLocalNames(); playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
_logger.LogDebug("Pet Nicknames is now: {petnames}", data.PetNamesData); _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)) 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)
{ {
var toCompute = fileReplacements.Where(f => !f.IsFileSwap).ToArray(); ct.ThrowIfCancellationRequested();
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length); file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray()); }
foreach (var file in toCompute) var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
{ if (removed > 0)
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty; {
} _logger.LogDebug("Removed {amount} of invalid files", removed);
var removed = fileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
if (removed > 0)
{
_logger.LogDebug("Removed {amount} of invalid files", removed);
}
} }
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
try try
{ {
await VerifyPlayerAnimationBones(boneIndices, data, objectKind).ConfigureAwait(false); await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
} }
catch (Exception e) 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); _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; if (boneIndices == null) return;
@@ -274,8 +268,10 @@ public class PlayerDataFactory
if (boneIndices.All(u => u.Value.Count == 0)) return; if (boneIndices.All(u => u.Value.Count == 0)) return;
int noValidationFailed = 0; 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); var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
bool validationFailed = false; bool validationFailed = false;
if (skeletonIndices != null) if (skeletonIndices != null)
@@ -305,10 +301,10 @@ public class PlayerDataFactory
{ {
noValidationFailed++; noValidationFailed++;
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath); _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) 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 FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
@@ -10,18 +9,15 @@ using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
namespace MareSynchronos.PlayerData.Handlers; namespace MareSynchronos.PlayerData.Handlers;
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
{ {
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly Func<IntPtr> _getAddress; private readonly Func<IntPtr> _getAddress;
private readonly bool _isOwnedObject; private readonly bool _isOwnedObject;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private CancellationTokenSource? _clearCts = new(); private byte _classJob = 0;
private Task? _delayedZoningTask; private Task? _delayedZoningTask;
private bool _haltProcessing = false; private bool _haltProcessing = false;
private bool _ignoreSendAfterRedraw = false;
private int _ptrNullCounter = 0;
private byte _classJob = 0;
private CancellationTokenSource _zoningCts = new(); private CancellationTokenSource _zoningCts = new();
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector, public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
@@ -76,12 +72,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
if (msg.Address == Address) if (msg.Address == Address)
{ {
_haltProcessing = false; _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(); _dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
} }
private enum DrawCondition public enum DrawCondition
{ {
None, None,
ObjectZero,
DrawObjectZero, DrawObjectZero,
RenderFlags, RenderFlags,
ModelInSlotLoaded, ModelInSlotLoaded,
ModelFilesInSlotLoaded 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 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 string Name { get; private set; }
public ObjectKind ObjectKind { get; } 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 byte[] CustomizeData { get; set; } = new byte[26];
private IntPtr DrawObjectAddress { get; set; } private IntPtr DrawObjectAddress { get; set; }
private byte[] EquipSlotData { get; set; } = new byte[40]; private byte[] EquipSlotData { get; set; } = new byte[40];
@@ -116,7 +107,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
{ {
while (await _dalamudUtil.RunOnFrameworkThread(() => while (await _dalamudUtil.RunOnFrameworkThread(() =>
{ {
if (IsBeingDrawn()) return true; if (_haltProcessing) CheckAndUpdateObject();
if (CurrentDrawCondition != DrawCondition.None) return true;
var gameObj = _dalamudUtil.CreateGameObject(Address); var gameObj = _dalamudUtil.CreateGameObject(Address);
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara) 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() public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
{ {
return _dalamudUtil.CreateGameObject(Address); return _dalamudUtil.CreateGameObject(Address);
@@ -185,15 +171,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
Address = _getAddress(); Address = _getAddress();
if (Address != IntPtr.Zero) if (Address != IntPtr.Zero)
{ {
_ptrNullCounter = 0;
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject; var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
DrawObjectAddress = drawObjAddr; DrawObjectAddress = drawObjAddr;
CurrentDrawCondition = DrawCondition.None;
} }
else else
{ {
DrawObjectAddress = IntPtr.Zero; DrawObjectAddress = IntPtr.Zero;
CurrentDrawCondition = DrawCondition.DrawObjectZero;
} }
CurrentDrawCondition = IsBeingDrawnUnsafe();
if (_haltProcessing) return; if (_haltProcessing) return;
bool drawObjDiff = DrawObjectAddress != prevDrawObj; bool drawObjDiff = DrawObjectAddress != prevDrawObj;
@@ -201,12 +190,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero) 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 chara = (Character*)Address;
var name = chara->GameObject.NameString; var name = chara->GameObject.NameString;
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal); 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); 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); Logger.LogTrace("[{this}] Changed", this);
Mediator.Publish(new CharacterChangedMessage(this)); Mediator.Publish(new CharacterChangedMessage(this));
@@ -288,26 +271,15 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
} }
else if (addrDiff || drawObjDiff) else if (addrDiff || drawObjDiff)
{ {
CurrentDrawCondition = DrawCondition.DrawObjectZero;
Logger.LogTrace("[{this}] Changed", this); Logger.LogTrace("[{this}] Changed", this);
if (_isOwnedObject && ObjectKind != ObjectKind.Player) if (_isOwnedObject && ObjectKind != ObjectKind.Player)
{ {
_clearCts?.CancelDispose(); Mediator.Publish(new ClearCacheForObjectMessage(this));
_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) private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
{ {
bool hasChanges = false; bool hasChanges = false;
@@ -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() private bool IsBeingDrawn()
{ {
var curPtr = _getAddress(); if (_haltProcessing) CheckAndUpdateObject();
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 (_dalamudUtil.IsAnythingDrawing) if (_dalamudUtil.IsAnythingDrawing)
{ {
@@ -414,27 +364,23 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
return true; return true;
} }
var drawObj = GetDrawObjUnsafe(curPtr); Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, CurrentDrawCondition);
Logger.LogTrace("[{this}] IsBeingDrawn, DrawObjPtr: {ptr}", this, drawObj.ToString("X")); return CurrentDrawCondition != DrawCondition.None;
var isDrawn = IsBeingDrawnUnsafe(drawObj, curPtr);
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, isDrawn);
return isDrawn != DrawCondition.None;
} }
private unsafe DrawCondition IsBeingDrawnUnsafe(IntPtr drawObj, IntPtr curPtr) private unsafe DrawCondition IsBeingDrawnUnsafe()
{ {
var drawObjZero = drawObj == IntPtr.Zero; if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
if (drawObjZero) return DrawCondition.DrawObjectZero; if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->RenderFlags) != 0x0; var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0;
if (renderFlags) return DrawCondition.RenderFlags; if (renderFlags) return DrawCondition.RenderFlags;
if (ObjectKind == ObjectKind.Player) if (ObjectKind == ObjectKind.Player)
{ {
var modelInSlotLoaded = (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0); var modelInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelInSlotLoaded != 0);
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded; if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
var modelFilesInSlotLoaded = (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0); var modelFilesInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelFilesInSlotLoaded != 0);
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded; if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
return DrawCondition.None;
} }
return DrawCondition.None; return DrawCondition.None;
@@ -442,11 +388,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
private void ZoneSwitchEnd() private void ZoneSwitchEnd()
{ {
if (!_isOwnedObject || _haltProcessing) return; if (!_isOwnedObject) return;
_clearCts?.Cancel();
_clearCts?.Dispose();
_clearCts = null;
try try
{ {
_zoningCts?.CancelAfter(2500); _zoningCts?.CancelAfter(2500);
@@ -463,7 +406,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase
private void ZoneSwitchStart() private void ZoneSwitchStart()
{ {
if (!_isOwnedObject || _haltProcessing) return; if (!_isOwnedObject) return;
_zoningCts = new(); _zoningCts = new();
Logger.LogDebug("[{obj}] Starting Delay After Zoning", this); Logger.LogDebug("[{obj}] Starting Delay After Zoning", this);

View File

@@ -8,37 +8,26 @@ using Microsoft.Extensions.Logging;
namespace MareSynchronos.PlayerData.Services; namespace MareSynchronos.PlayerData.Services;
#pragma warning disable MA0040
public sealed class CacheCreationService : DisposableMediatorSubscriberBase public sealed class CacheCreationService : DisposableMediatorSubscriberBase
{ {
private readonly SemaphoreSlim _cacheCreateLock = new(1); private readonly SemaphoreSlim _cacheCreateLock = new(1);
private readonly Dictionary<ObjectKind, GameObjectHandler> _cachesToCreate = []; private readonly HashSet<ObjectKind> _cachesToCreate = [];
private readonly PlayerDataFactory _characterDataFactory; 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 CharacterData _playerData = new();
private readonly Dictionary<ObjectKind, GameObjectHandler> _playerRelatedObjects = []; private readonly Dictionary<ObjectKind, GameObjectHandler> _playerRelatedObjects = [];
private Task? _cacheCreationTask; private readonly CancellationTokenSource _runtimeCts = new();
private CancellationTokenSource _honorificCts = new(); private CancellationTokenSource _creationCts = new();
private CancellationTokenSource _petNicknamesCts = new(); private CancellationTokenSource _debounceCts = new();
private CancellationTokenSource _moodlesCts = new();
private bool _isZoning = false;
private bool _haltCharaDataCreation; private bool _haltCharaDataCreation;
private readonly Dictionary<ObjectKind, CancellationTokenSource> _glamourerCts = new(); private bool _isZoning = false;
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory, public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator) PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator)
{ {
_characterDataFactory = characterDataFactory; _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<ZoneSwitchStartMessage>(this, (msg) => _isZoning = true);
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false); Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false);
@@ -47,6 +36,12 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
_haltCharaDataCreation = !msg.Resume; _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) _playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
_playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true) _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) => Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
{ {
if (msg.GameObjectHandler != _playerRelatedObjects[ObjectKind.Player]) return; if (msg.GameObjectHandler == _playerRelatedObjects[ObjectKind.Player])
{
Logger.LogTrace("Removing pet data for {obj}", msg.GameObjectHandler); AddCacheToCreate(ObjectKind.Player);
_playerData.FileReplacements.Remove(ObjectKind.Pet); AddCacheToCreate(ObjectKind.Pet);
_playerData.GlamourerString.Remove(ObjectKind.Pet); }
_playerData.CustomizePlusScale.Remove(ObjectKind.Pet);
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
}); });
Mediator.Subscribe<ClearCacheForObjectMessage>(this, (msg) => Mediator.Subscribe<ClearCacheForObjectMessage>(this, (msg) =>
{ {
// ignore pets if (msg.ObjectToCreateFor.ObjectKind == ObjectKind.Pet)
if (msg.ObjectToCreateFor == _playerRelatedObjects[ObjectKind.Pet]) return;
_ = Task.Run(() =>
{ {
Logger.LogTrace("Clearing cache for {obj}", msg.ObjectToCreateFor); Logger.LogTrace("Received clear cache for {obj}, ignoring", msg.ObjectToCreateFor);
_playerData.FileReplacements.Remove(msg.ObjectToCreateFor.ObjectKind); return;
_playerData.GlamourerString.Remove(msg.ObjectToCreateFor.ObjectKind); }
_playerData.CustomizePlusScale.Remove(msg.ObjectToCreateFor.ObjectKind); Logger.LogDebug("Clearing cache for {obj}", msg.ObjectToCreateFor);
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); AddCacheToCreate(msg.ObjectToCreateFor.ObjectKind);
});
}); });
Mediator.Subscribe<CustomizePlusMessage>(this, (msg) => Mediator.Subscribe<CustomizePlusMessage>(this, (msg) =>
{ {
if (_isZoning) return; 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);
foreach (var item in _playerRelatedObjects AddCacheToCreate(item);
.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);
}
});
}); });
Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) => Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) =>
{ {
if (_isZoning) return; if (_isZoning) return;
Logger.LogDebug("Received Heels Offset change, updating player"); Logger.LogDebug("Received Heels Offset change, updating player");
_ = AddPlayerCacheToCreate(); AddCacheToCreate();
}); });
Mediator.Subscribe<GlamourerChangedMessage>(this, (msg) => Mediator.Subscribe<GlamourerChangedMessage>(this, (msg) =>
{ {
if (_isZoning) return; if (_isZoning) return;
var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address); var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address);
if (changedType.Key != default || changedType.Value != default) 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) => Mediator.Subscribe<HonorificMessage>(this, (msg) =>
{ {
if (_isZoning) return; if (_isZoning) return;
if (!string.Equals(msg.NewHonorificTitle, _playerData.HonorificData, StringComparison.Ordinal)) if (!string.Equals(msg.NewHonorificTitle, _playerData.HonorificData, StringComparison.Ordinal))
{ {
Logger.LogDebug("Received Honorific change, updating player"); Logger.LogDebug("Received Honorific change, updating player");
HonorificChanged(); AddCacheToCreate(ObjectKind.Player);
}
});
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();
} }
}); });
Mediator.Subscribe<MoodlesMessage>(this, (msg) => Mediator.Subscribe<MoodlesMessage>(this, (msg) =>
{ {
if (_isZoning) return; if (_isZoning) return;
@@ -136,16 +118,30 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
if (changedType.Key == ObjectKind.Player && changedType.Value != default) if (changedType.Key == ObjectKind.Player && changedType.Value != default)
{ {
Logger.LogDebug("Received Moodles change, updating player"); 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"); if (_isZoning) return;
AddPlayerCacheToCreate().GetAwaiter().GetResult(); 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) protected override void Dispose(bool disposing)
@@ -153,111 +149,95 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
base.Dispose(disposing); base.Dispose(disposing);
_playerRelatedObjects.Values.ToList().ForEach(p => p.Dispose()); _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); _debounceCts.Cancel();
_cachesToCreate[kind] = _playerRelatedObjects[kind]; _debounceCts.Dispose();
_debounceCts = new();
var token = _debounceCts.Token;
_cacheCreateLock.Wait();
_debouncedObjectCache.Add(kind);
_cacheCreateLock.Release(); _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 () => _ = Task.Run(async () =>
{ {
await Task.Delay(TimeSpan.FromMilliseconds(500), token).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
await AddPlayerCacheToCreate(kind).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() private void ProcessCacheCreation()
{ {
if (_isZoning || _haltCharaDataCreation) return; if (_isZoning || _haltCharaDataCreation) return;
if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true)) if (_cachesToCreate.Count == 0) return;
{
_cacheCreateLock.Wait();
var toCreate = _cachesToCreate.ToList();
_cachesToCreate.Clear();
_cacheCreateLock.Release();
_cacheCreationTask = Task.Run(async () => 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 objectKindsToCreate = _cachesToCreate.ToList();
foreach (var creationObj in objectKindsToCreate)
{
_currentlyCreating.Add(creationObj);
}
_cachesToCreate.Clear();
_cacheCreateLock.Release();
_ = Task.Run(async () =>
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_creationCts.Token, _runtimeCts.Token);
Logger.LogDebug("Creating Caches for {objectKinds}", string.Join(", ", objectKindsToCreate));
try
{ {
try Dictionary<ObjectKind, CharacterDataFragment?> createdData = [];
foreach (var objectKind in objectKindsToCreate)
{ {
foreach (var obj in toCreate) createdData[objectKind] = await _characterDataFactory.BuildCharacterData(_playerRelatedObjects[objectKind], linkedCts.Token).ConfigureAwait(false);
{ }
await _characterDataFactory.BuildCharacterData(_playerData, obj.Value, _cts.Token).ConfigureAwait(false);
}
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); foreach (var kvp in createdData)
}
catch (Exception ex)
{ {
Logger.LogCritical(ex, "Error during Cache Creation Processing"); _playerData.SetFragment(kvp.Key, kvp.Value);
} }
finally
{ Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
Logger.LogDebug("Cache Creation complete"); _currentlyCreating.Clear();
} }
}, _cts.Token); catch (OperationCanceledException)
} {
else if (_cachesToCreate.Any()) Logger.LogDebug("Cache Creation cancelled");
{ }
Logger.LogDebug("Cache Creation stored until previous creation finished"); catch (Exception ex)
} {
Logger.LogCritical(ex, "Error during Cache Creation Processing");
}
finally
{
Logger.LogDebug("Cache Creation complete");
}
});
} }
} }
#pragma warning restore MA0040

View File

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

View File

@@ -88,7 +88,8 @@ public sealed class CharaDataFileHandler : IDisposable
using var tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, using var tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(chara.ObjectIndex)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false); () => _dalamudUtilService.GetCharacterFromObjectTableByIndex(chara.ObjectIndex)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false);
PlayerData.Data.CharacterData newCdata = new(); 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) if (newCdata.FileReplacements.TryGetValue(ObjectKind.Player, out var playerData) && playerData != null)
{ {
foreach (var data in playerData.Select(g => g.GamePaths)) 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 (!_clientState.IsLoggedIn) return;
if (ct == null)
ct = CancellationToken.None;
const int tick = 250; const int tick = 250;
int curWaitTime = 0; int curWaitTime = 0;
try try
{ {
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler); 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; curWaitTime += tick;
while ((!ct?.IsCancellationRequested ?? true) while ((!ct.Value.IsCancellationRequested)
&& curWaitTime < timeOut && curWaitTime < timeOut
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something && 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(); _messageQueue.Clear();
_loopCts.Cancel(); _loopCts.Cancel();
_loopCts.Dispose();
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -157,7 +158,7 @@ public sealed class MareMediator : IHostedService
List<SubscriberAction> subscribersCopy = []; List<SubscriberAction> subscribersCopy = [];
lock (_addRemoveLock) 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 #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)) if (!_genericExecuteMethods.TryGetValue((msgType, message.SubscriberKey), out var methodInfo))
{ {
_genericExecuteMethods[(msgType, message.SubscriberKey)] = methodInfo = GetType() _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); .MakeGenericMethod(msgType);
} }

View File

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