Last commit for this. will stop
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
// don’t 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user