Last commit for this. will stop

This commit is contained in:
cake
2026-01-17 22:31:50 +01:00
parent 828705cbfb
commit 699535b68b
5 changed files with 324 additions and 57 deletions

View File

@@ -8,6 +8,9 @@ using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Handlers;
/// <summary>
/// Game object handler for managing game object state and updates
/// </summary>
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
{
private readonly DalamudUtilService _dalamudUtil;

View File

@@ -13,6 +13,9 @@ using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
namespace LightlessSync.PlayerData.Handlers;
/// <summary>
/// Owned object handler for applying changes to owned objects.
/// </summary>
internal sealed class OwnedObjectHandler
{
// Debug information for owned object resolution

View File

@@ -1,4 +1,5 @@
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
namespace LightlessSync.PlayerData.Pairs;
@@ -39,7 +40,9 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
DateTime? VisibilityEvictionDueAtUtc { get; }
string? MinionAddressHex { get; }
ushort? MinionObjectIndex { get; }
DateTime? MinionResolvedAtUtc { get; }
string? MinionResolveStage { get; }
string? MinionResolveFailureReason { get; }
@@ -51,9 +54,14 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
Guid OwnedPenumbraCollectionId { get; }
bool NeedsCollectionRebuildDebug { get; }
uint MinionOrMountCharacterId { get; }
uint PetCharacterId { get; }
uint CompanionCharacterId { get; }
void Initialize();
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
void HardReapplyLastData();
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);

View File

@@ -82,60 +82,114 @@ public class Pair
public void AddContextMenu(IMenuOpenedArgs args)
{
var handler = TryGetHandler();
if (handler is null)
return;
if (args.Target is not MenuTargetDefault target)
return;
var obj = target.TargetObject;
if (obj is null)
return;
var eid = obj.EntityId;
var isPlayerTarget = eid != 0 && eid != uint.MaxValue && eid == handler.PlayerCharacterId;
if (!(isPlayerTarget))
return;
if (isPlayerTarget)
{
if (!IsPaused)
{
UiSharedService.AddContextMenuItem(
args,
name: "Open Profile",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
return Task.CompletedTask;
});
UiSharedService.AddContextMenuItem(
args,
name: "(Soft) - Reapply last data",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
ApplyLastReceivedData(forced: true);
return Task.CompletedTask;
});
UiSharedService.AddContextMenuItem(
args,
name: "(Hard) - Reapply last data",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
HardApplyLastReceivedData();
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(
args,
name: "Change Permissions",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
_mediator.Publish(new OpenPermissionWindow(this));
return Task.CompletedTask;
});
if (IsPaused)
{
UiSharedService.AddContextMenuItem(
args,
name: "Toggle Unpause State",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
_ = _apiController.Value.UnpauseAsync(UserData);
return Task.CompletedTask;
});
}
else
{
UiSharedService.AddContextMenuItem(
args,
name: "Toggle Pause State",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
_ = _apiController.Value.PauseAsync(UserData);
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(
args,
name: "Cycle Pause State",
prefixChar: 'L',
colorMenuItem: _lightlessPrefixColor,
onClick: () =>
{
TriggerCyclePause();
return Task.CompletedTask;
});
return;
}
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
{
return;
}
if (!IsPaused)
{
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
return Task.CompletedTask;
});
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
ApplyLastReceivedData(forced: true);
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_mediator.Publish(new OpenPermissionWindow(this));
return Task.CompletedTask;
});
if (IsPaused)
{
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_ = _apiController.Value.UnpauseAsync(UserData);
return Task.CompletedTask;
});
}
else
{
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_ = _apiController.Value.PauseAsync(UserData);
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
TriggerCyclePause();
return Task.CompletedTask;
});
}
public void ApplyData(OnlineUserCharaDataDto data)
@@ -160,6 +214,18 @@ public class Pair
handler.ApplyLastReceivedData(forced);
}
public void HardApplyLastReceivedData()
{
var handler = TryGetHandler();
if (handler is null)
{
_logger.LogTrace("ApplyLastReceivedData skipped for {Uid}: handler missing.", UserData.UID);
return;
}
handler.HardReapplyLastData();
}
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
{
var handler = TryGetHandler();

View File

@@ -208,7 +208,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
public DateTime? MinionResolvedAtUtc => _ownedObjectHandler.MinionResolveDebug.ResolvedAtUtc;
public string? MinionResolveStage => string.IsNullOrEmpty(_ownedObjectHandler.MinionResolveDebug.Stage) ? null : _ownedObjectHandler.MinionResolveDebug.Stage;
public string? MinionResolveFailureReason => _ownedObjectHandler.MinionResolveDebug.FailureReason;
public uint MinionOrMountCharacterId { get; private set; } = uint.MaxValue;
public uint PetCharacterId { get; private set; } = uint.MaxValue;
public uint CompanionCharacterId { get; private set; } = uint.MaxValue;
public bool MinionPendingRetry
{
get
@@ -225,9 +227,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
lock (_ownedRetryGate)
{
if (_pendingOwnedChanges.TryGetValue(ObjectKind.MinionOrMount, out var set))
return set.Select(s => s.ToString()).ToArray();
return [.. set.Select(static s => s.ToString())];
return Array.Empty<string>();
return [];
}
}
}
@@ -531,6 +533,44 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
}
private void RefreshOwnedTargetIds()
{
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
{
MinionOrMountCharacterId = uint.MaxValue;
PetCharacterId = uint.MaxValue;
CompanionCharacterId = uint.MaxValue;
return;
}
var playerPtr = _charaHandler.Address;
_ = _dalamudUtil.RunOnFrameworkThread(() =>
{
try
{
var minPtr = _dalamudUtil.GetMinionOrMountPtr(playerPtr);
var petPtr = _dalamudUtil.GetPetPtr(playerPtr);
var compPtr = _dalamudUtil.GetCompanionPtr(playerPtr);
var minObj = _dalamudUtil.CreateGameObject(minPtr);
var petObj = _dalamudUtil.CreateGameObject(petPtr);
var compObj = _dalamudUtil.CreateGameObject(compPtr);
MinionOrMountCharacterId = minObj?.EntityId ?? uint.MaxValue;
PetCharacterId = petObj?.EntityId ?? uint.MaxValue;
CompanionCharacterId = compObj?.EntityId ?? uint.MaxValue;
}
catch
{
// dont let this throw from framework thread
MinionOrMountCharacterId = uint.MaxValue;
PetCharacterId = uint.MaxValue;
CompanionCharacterId = uint.MaxValue;
}
});
}
private Guid EnsureOwnedPenumbraCollection()
{
if (!IsVisible)
@@ -800,6 +840,137 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ApplyCharacterData(Guid.NewGuid(), sanitized, forceApplyCustomization, suppressForcedModRedraw);
}
public void HardReapplyLastData()
{
EnsureInitialized();
if (LastReceivedCharacterData is null && _cachedData is null)
return;
_ = Task.Run(async () =>
{
try
{
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return;
if (!_ipcManager.Penumbra.APIAvailable)
{
ApplyLastReceivedData(forced: true);
return;
}
_needsCollectionRebuild = true;
_lastAppliedModdedPaths = null;
_forceApplyMods = true;
_forceFullReapply = true;
var flushId = Guid.NewGuid();
var playerCollection = EnsurePenumbraCollection();
if (playerCollection != Guid.Empty)
{
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() =>
_charaHandler.GetGameObject()?.ObjectIndex).ConfigureAwait(false);
if (objIndex.HasValue)
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, playerCollection, objIndex.Value)
.ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(
Logger, flushId, playerCollection,
new Dictionary<string, string>(StringComparer.Ordinal),
scope: "Player")
.ConfigureAwait(false);
await _ipcManager.Penumbra.SetManipulationDataAsync(
Logger, flushId, playerCollection, string.Empty)
.ConfigureAwait(false);
await _ipcManager.Penumbra.RedrawAsync(Logger, _charaHandler, flushId, CancellationToken.None)
.ConfigureAwait(false);
}
var ownedCollection = EnsureOwnedPenumbraCollection();
if (ownedCollection != Guid.Empty)
{
await _ipcManager.Penumbra.SetTemporaryModsAsync(
Logger, flushId, ownedCollection,
new Dictionary<string, string>(StringComparer.Ordinal),
scope: "Owned")
.ConfigureAwait(false);
}
ApplyLastReceivedData(forced: true);
await Task.Delay(900).ConfigureAwait(false);
ApplyLastReceivedData(forced: true);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Hard reapply failed for {handler}", GetLogIdentifier());
}
});
}
private static readonly TimeSpan FileReadyTimeout = TimeSpan.FromSeconds(8);
private static readonly TimeSpan FileReadyPoll = TimeSpan.FromMilliseconds(75);
private static bool IsCriticalVisualPath(string gamePath)
=> gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)
|| gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|| gamePath.EndsWith(".mtrl", StringComparison.OrdinalIgnoreCase);
private static async Task WaitForFilesReadyAsync(
ILogger logger,
Guid appId,
IEnumerable<(string GamePath, string FilePath)> entries,
CancellationToken token)
{
var list = entries
.Where(e => !string.IsNullOrEmpty(e.FilePath) && Path.IsPathRooted(e.FilePath))
.Select(e => (e.GamePath, e.FilePath))
.DistinctBy(e => e.FilePath, StringComparer.OrdinalIgnoreCase)
.Take(200)
.ToList();
foreach (var (gamePath, filePath) in list)
{
token.ThrowIfCancellationRequested();
var sw = Stopwatch.StartNew();
while (sw.Elapsed < FileReadyTimeout && !token.IsCancellationRequested)
{
try
{
using var fs = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete);
if (fs.Length > 0)
break;
}
catch (IOException)
{
// locked or being swapped
}
catch (UnauthorizedAccessException)
{
// transient access issues, treat like locked
}
await Task.Delay(FileReadyPoll, token).ConfigureAwait(false);
}
if (sw.Elapsed >= FileReadyTimeout)
{
logger.LogDebug(
"[{appId}] File still not ready after {ms}ms: {gamePath} -> {filePath}",
appId, (int)sw.Elapsed.TotalMilliseconds, gamePath, filePath);
}
}
}
public bool FetchPerformanceMetricsFromCache()
{
EnsureInitialized();
@@ -2727,16 +2898,22 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
foreach (var kv in papOnly)
merged[kv.Key] = kv.Value;
// Apply mods via IPC
await WaitForFilesReadyAsync(
Logger,
_applicationId,
merged.Select(kv => (kv.Key.GamePath, kv.Value))
.Where(x => IsCriticalVisualPath(x.GamePath)),
token)
.ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(
Logger, _applicationId, playerCollection,
merged.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal),
scope: "Player")
.ConfigureAwait(false);
// Final redraw
await _ipcManager.Penumbra.RedrawAsync(Logger, handlerForApply, _applicationId, token).ConfigureAwait(false);
if (handlerForApply.Address != nint.Zero)
{
await _actorObjectService
@@ -2771,11 +2948,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
// Apply owned mods via IPC
if (ownedModded.Count > 0)
{
await WaitForFilesReadyAsync(
Logger,
_applicationId,
ownedModded.Select(kv => (kv.Key.GamePath, kv.Value)).Where(x => IsCriticalVisualPath(x.GamePath)),
token).ConfigureAwait(false);
await _ipcManager.Penumbra.SetTemporaryModsAsync(
Logger, _applicationId, ownedCollection,
ownedModded.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal),
scope: "Owned")
.ConfigureAwait(false);
scope: "Owned").ConfigureAwait(false);
}
}
@@ -2992,6 +3174,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
HandleVisibilityLoss(logChange: true);
}
if (_charaHandler?.Address != nint.Zero && IsVisible)
RefreshOwnedTargetIds();
TryApplyQueuedData();
}
@@ -3414,6 +3599,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
UpdateLastKnownActor(descriptor);
RefreshTrackedHandler(descriptor);
QueueActorInitialization(descriptor);
RefreshOwnedTargetIds();
return;
}