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;
|
namespace LightlessSync.PlayerData.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Game object handler for managing game object state and updates
|
||||||
|
/// </summary>
|
||||||
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
||||||
{
|
{
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|||||||
|
|
||||||
namespace LightlessSync.PlayerData.Handlers;
|
namespace LightlessSync.PlayerData.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owned object handler for applying changes to owned objects.
|
||||||
|
/// </summary>
|
||||||
internal sealed class OwnedObjectHandler
|
internal sealed class OwnedObjectHandler
|
||||||
{
|
{
|
||||||
// Debug information for owned object resolution
|
// Debug information for owned object resolution
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
using LightlessSync.API.Data.Enum;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
@@ -39,7 +40,9 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
|||||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
|
|
||||||
string? MinionAddressHex { get; }
|
string? MinionAddressHex { get; }
|
||||||
|
|
||||||
ushort? MinionObjectIndex { get; }
|
ushort? MinionObjectIndex { get; }
|
||||||
|
|
||||||
DateTime? MinionResolvedAtUtc { get; }
|
DateTime? MinionResolvedAtUtc { get; }
|
||||||
string? MinionResolveStage { get; }
|
string? MinionResolveStage { get; }
|
||||||
string? MinionResolveFailureReason { get; }
|
string? MinionResolveFailureReason { get; }
|
||||||
@@ -51,9 +54,14 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
|||||||
Guid OwnedPenumbraCollectionId { get; }
|
Guid OwnedPenumbraCollectionId { get; }
|
||||||
bool NeedsCollectionRebuildDebug { get; }
|
bool NeedsCollectionRebuildDebug { get; }
|
||||||
|
|
||||||
|
uint MinionOrMountCharacterId { get; }
|
||||||
|
uint PetCharacterId { get; }
|
||||||
|
uint CompanionCharacterId { get; }
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
void ApplyLastReceivedData(bool forced = false);
|
void ApplyLastReceivedData(bool forced = false);
|
||||||
|
void HardReapplyLastData();
|
||||||
bool FetchPerformanceMetricsFromCache();
|
bool FetchPerformanceMetricsFromCache();
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetUploading(bool uploading);
|
void SetUploading(bool uploading);
|
||||||
|
|||||||
@@ -82,60 +82,114 @@ public class Pair
|
|||||||
|
|
||||||
public void AddContextMenu(IMenuOpenedArgs args)
|
public void AddContextMenu(IMenuOpenedArgs args)
|
||||||
{
|
{
|
||||||
|
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
if (handler is null)
|
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;
|
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)
|
public void ApplyData(OnlineUserCharaDataDto data)
|
||||||
@@ -160,6 +214,18 @@ public class Pair
|
|||||||
handler.ApplyLastReceivedData(forced);
|
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)
|
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||||
{
|
{
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
|
|||||||
@@ -208,7 +208,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
public DateTime? MinionResolvedAtUtc => _ownedObjectHandler.MinionResolveDebug.ResolvedAtUtc;
|
public DateTime? MinionResolvedAtUtc => _ownedObjectHandler.MinionResolveDebug.ResolvedAtUtc;
|
||||||
public string? MinionResolveStage => string.IsNullOrEmpty(_ownedObjectHandler.MinionResolveDebug.Stage) ? null : _ownedObjectHandler.MinionResolveDebug.Stage;
|
public string? MinionResolveStage => string.IsNullOrEmpty(_ownedObjectHandler.MinionResolveDebug.Stage) ? null : _ownedObjectHandler.MinionResolveDebug.Stage;
|
||||||
public string? MinionResolveFailureReason => _ownedObjectHandler.MinionResolveDebug.FailureReason;
|
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
|
public bool MinionPendingRetry
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -225,9 +227,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
lock (_ownedRetryGate)
|
lock (_ownedRetryGate)
|
||||||
{
|
{
|
||||||
if (_pendingOwnedChanges.TryGetValue(ObjectKind.MinionOrMount, out var set))
|
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()
|
private Guid EnsureOwnedPenumbraCollection()
|
||||||
{
|
{
|
||||||
if (!IsVisible)
|
if (!IsVisible)
|
||||||
@@ -800,6 +840,137 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
ApplyCharacterData(Guid.NewGuid(), sanitized, forceApplyCustomization, suppressForcedModRedraw);
|
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()
|
public bool FetchPerformanceMetricsFromCache()
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
@@ -2727,14 +2898,20 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
foreach (var kv in papOnly)
|
foreach (var kv in papOnly)
|
||||||
merged[kv.Key] = kv.Value;
|
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(
|
await _ipcManager.Penumbra.SetTemporaryModsAsync(
|
||||||
Logger, _applicationId, playerCollection,
|
Logger, _applicationId, playerCollection,
|
||||||
merged.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal),
|
merged.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal),
|
||||||
scope: "Player")
|
scope: "Player")
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// Final redraw
|
|
||||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handlerForApply, _applicationId, token).ConfigureAwait(false);
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handlerForApply, _applicationId, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (handlerForApply.Address != nint.Zero)
|
if (handlerForApply.Address != nint.Zero)
|
||||||
@@ -2771,11 +2948,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
// Apply owned mods via IPC
|
// Apply owned mods via IPC
|
||||||
if (ownedModded.Count > 0)
|
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(
|
await _ipcManager.Penumbra.SetTemporaryModsAsync(
|
||||||
Logger, _applicationId, ownedCollection,
|
Logger, _applicationId, ownedCollection,
|
||||||
ownedModded.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal),
|
ownedModded.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal),
|
||||||
scope: "Owned")
|
scope: "Owned").ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2992,6 +3174,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
HandleVisibilityLoss(logChange: true);
|
HandleVisibilityLoss(logChange: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_charaHandler?.Address != nint.Zero && IsVisible)
|
||||||
|
RefreshOwnedTargetIds();
|
||||||
|
|
||||||
TryApplyQueuedData();
|
TryApplyQueuedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3414,6 +3599,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
UpdateLastKnownActor(descriptor);
|
UpdateLastKnownActor(descriptor);
|
||||||
RefreshTrackedHandler(descriptor);
|
RefreshTrackedHandler(descriptor);
|
||||||
QueueActorInitialization(descriptor);
|
QueueActorInitialization(descriptor);
|
||||||
|
|
||||||
|
RefreshOwnedTargetIds();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user