refactor(net): converge live session state reset

This commit is contained in:
Erik 2026-07-21 12:00:48 +02:00
parent 78a9223b65
commit 4f31a5085f
35 changed files with 1460 additions and 83 deletions

View file

@ -66,6 +66,17 @@ public sealed class GameplayConfirmationController : IDisposable
return _dialogs.CloseDialog(_dialogContext);
}
/// <summary>
/// Forget any tuple left after the dialog factory has completed its normal
/// retail close/reset callbacks.
/// </summary>
public void ResetSession()
{
_dialogContext = 0u;
_serverType = 0u;
_serverContext = 0u;
}
public void Dispose()
{
if (_disposed) return;

View file

@ -42,6 +42,31 @@ public sealed class InteractionState
public bool Clear() => Set(InteractionMode.None);
/// <summary>
/// Publishes the session-reset edge even when the mode is already clear so
/// a retry can repair any observer that missed an earlier notification.
/// </summary>
public void ResetSession()
{
InteractionMode previous = Current;
Current = InteractionMode.None;
Action<InteractionModeTransition>? listeners = Changed;
if (listeners is null)
return;
var transition = new InteractionModeTransition(previous, InteractionMode.None);
List<Exception>? failures = null;
foreach (Action<InteractionModeTransition> listener in listeners.GetInvocationList())
{
try { listener(transition); }
catch (Exception error) { (failures ??= []).Add(error); }
}
if (failures is not null)
throw new AggregateException(
"One or more interaction-mode reset observers failed.",
failures);
}
private bool Set(InteractionMode mode)
{
if (Current == mode)

View file

@ -1009,7 +1009,14 @@ public sealed class ItemInteractionController : IDisposable
};
private void OnInteractionModeChanged(InteractionModeTransition _)
=> StateChanged?.Invoke();
{
List<Exception> failures = [];
DispatchAll(StateChanged, failures);
if (failures.Count != 0)
throw new AggregateException(
"One or more item-interaction observers failed.",
failures);
}
private void OnInventoryObjectMoved(ClientObjectMove move)
{
@ -1124,17 +1131,50 @@ public sealed class ItemInteractionController : IDisposable
/// </summary>
public void ResetSession()
{
CancelPendingBackpackPlacement();
bool busyChanged = _busyCount != 0 || _pendingInventoryRequest is not null;
bool modeChanged = IsAnyTargetModeActive;
PendingBackpackPlacement? pendingPlacement = _pendingBackpackPlacement;
_pendingBackpackPlacement = null;
_busyCount = 0;
_pendingInventoryRequest = null;
_lastUseMs = long.MinValue / 2;
_consumedPrimaryClickTarget = 0u;
_consumedPrimaryClickMs = long.MinValue / 2;
_interactionState.Clear();
if (busyChanged && !modeChanged)
StateChanged?.Invoke();
List<Exception> failures = [];
try { _interactionState.ResetSession(); }
catch (Exception error) { failures.Add(error); }
if (pendingPlacement is { } pending)
DispatchAll(PendingBackpackPlacementCancelled, pending, failures);
if (failures.Count != 0)
throw new AggregateException(
"One or more item-interaction reset observers failed.",
failures);
}
private static void DispatchAll(Action? listeners, List<Exception> failures)
{
if (listeners is null)
return;
foreach (Action listener in listeners.GetInvocationList())
{
try { listener(); }
catch (Exception error) { failures.Add(error); }
}
}
private static void DispatchAll<T>(
Action<T>? listeners,
T value,
List<Exception> failures)
{
if (listeners is null)
return;
foreach (Action<T> listener in listeners.GetInvocationList())
{
try { listener(value); }
catch (Exception error) { failures.Add(error); }
}
}
private bool ConsumeUseThrottle()

View file

@ -26,6 +26,7 @@ public sealed class RetailDialogFactory : IDisposable
private readonly Dictionary<uint, LinkedList<DialogInfo>> _pending = new();
private readonly List<DialogInfo> _openOrder = new();
private uint _globalContext;
private bool _resetting;
private bool _disposed;
public RetailDialogFactory(
@ -192,18 +193,46 @@ public sealed class RetailDialogFactory : IDisposable
/// </summary>
public void Reset()
{
DialogInfo[] infos = _activeNonQueued.Values
.Concat(_activeQueued.Values)
.Concat(_pending.Values.SelectMany(static queue => queue))
.Distinct()
.ToArray();
if (_resetting)
return;
_activeNonQueued.Clear();
_activeQueued.Clear();
_pending.Clear();
foreach (DialogInfo info in infos)
DialogDone(info);
RefreshModal();
_resetting = true;
List<Exception>? failures = null;
try
{
while (true)
{
DialogInfo[] infos = _activeNonQueued.Values
.Concat(_activeQueued.Values)
.Concat(_pending.Values.SelectMany(static queue => queue))
.Distinct()
.ToArray();
if (infos.Length == 0)
break;
// Callbacks may synchronously create another dialog. Retire
// this exact snapshot, then loop until those reentrant infos
// have also completed through DialogDone.
_activeNonQueued.Clear();
_activeQueued.Clear();
_pending.Clear();
foreach (DialogInfo info in infos)
{
try { DialogDone(info); }
catch (Exception error) { (failures ??= []).Add(error); }
}
}
}
finally
{
_resetting = false;
RefreshModal();
}
if (failures is not null)
throw new AggregateException(
"One or more dialogs failed while the factory reset.",
failures);
}
public void Dispose()
@ -283,10 +312,10 @@ public sealed class RetailDialogFactory : IDisposable
{
if (info.View is { } view)
{
info.View = null;
view.DetachHandlers();
_openOrder.Remove(info);
_host.RemoveChild(view.Root);
info.View = null;
_openOrder.Remove(info);
}
RefreshModal();
}

View file

@ -386,6 +386,25 @@ public sealed class RetailUiRuntime : IDisposable
data => completed(data.GetBoolean(RetailDialogProperty.ConfirmationResult))) ?? 0u;
}
/// <summary>
/// Close character-session dialogs through retail's normal callback and
/// close-notice path, then forget the gameplay confirmation tuple.
/// <c>gmGamePlayUI::~gmGamePlayUI @ 0x004EA2A0</c> closes its retained
/// contexts before <c>DialogFactory::Reset @ 0x00477950</c> drains the
/// remaining factory entries.
/// </summary>
public void ResetSessionDialogs()
{
try
{
DialogFactory?.Reset();
}
finally
{
_gameplayConfirmationController?.ResetSession();
}
}
public void UpdateCursor(IEnumerable<IMouse> mice)
{
CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root);