2.0.0 #92

Merged
defnotken merged 171 commits from 2.0.0 into master 2025-12-21 17:19:36 +00:00
6 changed files with 180 additions and 50 deletions
Showing only changes of commit 7b74fa7c4e - Show all commits

View File

@@ -25,6 +25,8 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
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);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,18 +87,32 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
get => _isVisible; get => _isVisible;
private set private set
{ {
if (_isVisible != value) if (_isVisible == value) return;
{
_isVisible = value; _isVisible = value;
if (!_isVisible) if (!_isVisible)
{ {
DisableSync(); DisableSync();
ResetPenumbraCollection(reason: "VisibilityLost");
_invisibleSinceUtc = DateTime.UtcNow;
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
StartVisibilityGraceTask();
} }
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero) else
{ {
CancelVisibilityGraceTask();
_invisibleSinceUtc = null;
_visibilityEvictionDueAtUtc = null;
ScheduledForDeletion = false;
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
_ = EnsurePenumbraCollection(); _ = EnsurePenumbraCollection();
} }
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible")))); EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
@@ -99,7 +120,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Mediator.Publish(new VisibilityChange()); Mediator.Publish(new VisibilityChange());
} }
} }
}
public long LastAppliedDataBytes { get; private set; } public long LastAppliedDataBytes { get; private set; }
public long LastAppliedDataTris { get; set; } = -1; public long LastAppliedDataTris { get; set; } = -1;
@@ -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)

View File

@@ -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();
} }

View File

@@ -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})";