2.0.0 #92
@@ -1,36 +1,38 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// orchestrates the lifecycle of a paired character
|
/// orchestrates the lifecycle of a paired character
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||||
{
|
{
|
||||||
new string Ident { get; }
|
new string Ident { get; }
|
||||||
bool Initialized { get; }
|
bool Initialized { get; }
|
||||||
bool IsVisible { get; }
|
bool IsVisible { get; }
|
||||||
bool ScheduledForDeletion { get; set; }
|
bool ScheduledForDeletion { get; set; }
|
||||||
CharacterData? LastReceivedCharacterData { get; }
|
CharacterData? LastReceivedCharacterData { get; }
|
||||||
long LastAppliedDataBytes { get; }
|
long LastAppliedDataBytes { get; }
|
||||||
new string? PlayerName { get; }
|
new string? PlayerName { get; }
|
||||||
string PlayerNameHash { get; }
|
string PlayerNameHash { get; }
|
||||||
uint PlayerCharacterId { get; }
|
uint PlayerCharacterId { get; }
|
||||||
DateTime? LastDataReceivedAt { get; }
|
DateTime? LastDataReceivedAt { get; }
|
||||||
DateTime? LastApplyAttemptAt { get; }
|
DateTime? LastApplyAttemptAt { get; }
|
||||||
DateTime? LastSuccessfulApplyAt { get; }
|
DateTime? LastSuccessfulApplyAt { get; }
|
||||||
string? LastFailureReason { get; }
|
string? LastFailureReason { get; }
|
||||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||||
bool IsApplying { get; }
|
bool IsApplying { get; }
|
||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
|
DateTime? InvisibleSinceUtc { get; }
|
||||||
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
void ApplyLastReceivedData(bool forced = false);
|
void ApplyLastReceivedData(bool forced = false);
|
||||||
bool FetchPerformanceMetricsFromCache();
|
bool FetchPerformanceMetricsFromCache();
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetUploading(bool uploading);
|
void SetUploading(bool uploading);
|
||||||
void SetPaused(bool paused);
|
void SetPaused(bool paused);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,9 +194,13 @@ public class Pair
|
|||||||
{
|
{
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
if (handler is null)
|
if (handler is null)
|
||||||
{
|
|
||||||
return PairDebugInfo.Empty;
|
return PairDebugInfo.Empty;
|
||||||
}
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||||
|
var remainingSeconds = dueAt.HasValue
|
||||||
|
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||||
|
: (double?)null;
|
||||||
|
|
||||||
return new PairDebugInfo(
|
return new PairDebugInfo(
|
||||||
true,
|
true,
|
||||||
@@ -206,6 +210,9 @@ public class Pair
|
|||||||
handler.LastDataReceivedAt,
|
handler.LastDataReceivedAt,
|
||||||
handler.LastApplyAttemptAt,
|
handler.LastApplyAttemptAt,
|
||||||
handler.LastSuccessfulApplyAt,
|
handler.LastSuccessfulApplyAt,
|
||||||
|
handler.InvisibleSinceUtc,
|
||||||
|
handler.VisibilityEvictionDueAtUtc,
|
||||||
|
remainingSeconds,
|
||||||
handler.LastFailureReason,
|
handler.LastFailureReason,
|
||||||
handler.LastBlockingConditions,
|
handler.LastBlockingConditions,
|
||||||
handler.IsApplying,
|
handler.IsApplying,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public sealed record PairDebugInfo(
|
|||||||
DateTime? LastDataReceivedAt,
|
DateTime? LastDataReceivedAt,
|
||||||
DateTime? LastApplyAttemptAt,
|
DateTime? LastApplyAttemptAt,
|
||||||
DateTime? LastSuccessfulApplyAt,
|
DateTime? LastSuccessfulApplyAt,
|
||||||
|
DateTime? InvisibleSinceUtc,
|
||||||
|
DateTime? VisibilityEvictionDueAtUtc,
|
||||||
|
double? VisibilityEvictionRemainingSeconds,
|
||||||
string? LastFailureReason,
|
string? LastFailureReason,
|
||||||
IReadOnlyList<string> BlockingConditions,
|
IReadOnlyList<string> BlockingConditions,
|
||||||
bool IsApplying,
|
bool IsApplying,
|
||||||
@@ -24,6 +27,9 @@ public sealed record PairDebugInfo(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -70,7 +70,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private DateTime? _lastSuccessfulApplyAt;
|
private DateTime? _lastSuccessfulApplyAt;
|
||||||
private string? _lastFailureReason;
|
private string? _lastFailureReason;
|
||||||
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
||||||
|
private readonly object _visibilityGraceGate = new();
|
||||||
|
private CancellationTokenSource? _visibilityGraceCts;
|
||||||
|
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||||
|
private DateTime? _invisibleSinceUtc;
|
||||||
|
private DateTime? _visibilityEvictionDueAtUtc;
|
||||||
|
|
||||||
|
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||||
|
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||||
public string Ident { get; }
|
public string Ident { get; }
|
||||||
public bool Initialized { get; private set; }
|
public bool Initialized { get; private set; }
|
||||||
public bool ScheduledForDeletion { get; set; }
|
public bool ScheduledForDeletion { get; set; }
|
||||||
@@ -80,24 +87,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
get => _isVisible;
|
get => _isVisible;
|
||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
if (_isVisible != value)
|
if (_isVisible == value) return;
|
||||||
|
|
||||||
|
_isVisible = value;
|
||||||
|
|
||||||
|
if (!_isVisible)
|
||||||
{
|
{
|
||||||
_isVisible = value;
|
DisableSync();
|
||||||
if (!_isVisible)
|
|
||||||
{
|
_invisibleSinceUtc = DateTime.UtcNow;
|
||||||
DisableSync();
|
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
|
||||||
ResetPenumbraCollection(reason: "VisibilityLost");
|
|
||||||
}
|
StartVisibilityGraceTask();
|
||||||
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
_ = EnsurePenumbraCollection();
|
|
||||||
}
|
|
||||||
var user = GetPrimaryUserData();
|
|
||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
|
||||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
|
||||||
Mediator.Publish(new RefreshUiMessage());
|
|
||||||
Mediator.Publish(new VisibilityChange());
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CancelVisibilityGraceTask();
|
||||||
|
|
||||||
|
_invisibleSinceUtc = null;
|
||||||
|
_visibilityEvictionDueAtUtc = null;
|
||||||
|
|
||||||
|
ScheduledForDeletion = false;
|
||||||
|
|
||||||
|
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||||
|
_ = EnsurePenumbraCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = GetPrimaryUserData();
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||||
|
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||||
|
Mediator.Publish(new RefreshUiMessage());
|
||||||
|
Mediator.Publish(new VisibilityChange());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,6 +938,46 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelVisibilityGraceTask()
|
||||||
|
{
|
||||||
|
lock (_visibilityGraceGate)
|
||||||
|
{
|
||||||
|
_visibilityGraceCts?.CancelDispose();
|
||||||
|
_visibilityGraceCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartVisibilityGraceTask()
|
||||||
|
{
|
||||||
|
CancellationToken token;
|
||||||
|
lock (_visibilityGraceGate)
|
||||||
|
{
|
||||||
|
_visibilityGraceCts = _visibilityGraceCts?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
|
token = _visibilityGraceCts.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
_visibilityGraceTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(VisibilityEvictionGrace, token).ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (IsVisible) return;
|
||||||
|
|
||||||
|
ScheduledForDeletion = true;
|
||||||
|
ResetPenumbraCollection(reason: "VisibilityLostTimeout");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// operation cancelled, do nothing
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Visibility grace task failed for {handler}", GetLogIdentifier());
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
@@ -936,7 +996,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadCancellationTokenSource = null;
|
_downloadCancellationTokenSource = null;
|
||||||
_downloadManager.Dispose();
|
_downloadManager.Dispose();
|
||||||
_charaHandler?.Dispose();
|
_charaHandler?.Dispose();
|
||||||
|
CancelVisibilityGraceTask();
|
||||||
_charaHandler = null;
|
_charaHandler = null;
|
||||||
|
_invisibleSinceUtc = null;
|
||||||
|
_visibilityEvictionDueAtUtc = null;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
@@ -1265,6 +1328,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task? _pairDownloadTask;
|
private Task? _pairDownloadTask;
|
||||||
|
private Task _visibilityGraceTask;
|
||||||
|
|
||||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private readonly object _pendingGate = new();
|
private readonly object _pendingGate = new();
|
||||||
|
private readonly object _visibilityGate = new();
|
||||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||||
@@ -144,6 +146,37 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PairOperationResult CancelAllInvisibleEvictions()
|
||||||
|
{
|
||||||
|
List<CancellationTokenSource> snapshot;
|
||||||
|
lock (_visibilityGate)
|
||||||
|
{
|
||||||
|
snapshot = [.. _pendingInvisibleEvictions.Values];
|
||||||
|
_pendingInvisibleEvictions.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string>? errors = null;
|
||||||
|
|
||||||
|
foreach (var cts in snapshot)
|
||||||
|
{
|
||||||
|
try { cts.Cancel(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try { cts.Dispose(); }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors is null
|
||||||
|
? PairOperationResult.Ok()
|
||||||
|
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
|
||||||
|
}
|
||||||
|
|
||||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||||
{
|
{
|
||||||
if (registration.CharacterIdent is null)
|
if (registration.CharacterIdent is null)
|
||||||
@@ -300,6 +333,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
handlers = _entriesByHandler.Keys.ToList();
|
handlers = _entriesByHandler.Keys.ToList();
|
||||||
|
CancelAllInvisibleEvictions();
|
||||||
_entriesByIdent.Clear();
|
_entriesByIdent.Clear();
|
||||||
_entriesByHandler.Clear();
|
_entriesByHandler.Clear();
|
||||||
}
|
}
|
||||||
@@ -332,6 +366,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
handlers = _entriesByHandler.Keys.ToList();
|
handlers = _entriesByHandler.Keys.ToList();
|
||||||
|
CancelAllInvisibleEvictions();
|
||||||
_entriesByIdent.Clear();
|
_entriesByIdent.Clear();
|
||||||
_entriesByHandler.Clear();
|
_entriesByHandler.Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1463,7 +1463,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
||||||
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
||||||
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
||||||
|
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
|
||||||
|
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
|
||||||
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
||||||
|
|
||||||
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
@@ -1698,6 +1701,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? FormatCountdown(double? remainingSeconds)
|
||||||
|
{
|
||||||
|
if (!remainingSeconds.HasValue)
|
||||||
|
return "No";
|
||||||
|
|
||||||
|
var secs = Math.Max(0, remainingSeconds.Value);
|
||||||
|
var t = TimeSpan.FromSeconds(secs);
|
||||||
|
|
||||||
|
return t.TotalHours >= 1
|
||||||
|
? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}"
|
||||||
|
: $"{(int)t.TotalMinutes:00}:{t.Seconds:00}";
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
||||||
|
|
||||||
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
||||||
|
|||||||
Reference in New Issue
Block a user