fix(runtime): close Campaign LA7b review findings
This commit is contained in:
parent
0e82cbf700
commit
1b9e7e41f9
11 changed files with 700 additions and 28 deletions
|
|
@ -20,6 +20,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
{
|
||||
private readonly GameRuntime _runtime;
|
||||
private readonly CurrentGameRuntimeCommandAdapter _commands;
|
||||
private readonly CharacterSelectionProjection _characterSelection;
|
||||
private readonly IDisposable _hostLease;
|
||||
private readonly object _subscriptionGate = new();
|
||||
private readonly HashSet<AdapterSubscription> _subscriptions = [];
|
||||
|
|
@ -40,6 +41,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
"graphical game-runtime command adapter");
|
||||
try
|
||||
{
|
||||
_characterSelection = new CharacterSelectionProjection(this);
|
||||
_commands = new CurrentGameRuntimeCommandAdapter(
|
||||
runtime.Session,
|
||||
sessionHost,
|
||||
|
|
@ -61,7 +63,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
}
|
||||
|
||||
private bool IsActive =>
|
||||
!_disposed
|
||||
!Volatile.Read(ref _disposed)
|
||||
&& !_runtime.Session.IsDisposalComplete;
|
||||
|
||||
public RuntimeGenerationToken Generation => _runtime.Generation;
|
||||
|
|
@ -90,7 +92,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
public IRuntimeSocialView Social => _runtime.Social;
|
||||
public IRuntimeChatView Chat => _runtime.Chat;
|
||||
public IRuntimeCharacterSelectionView CharacterSelection =>
|
||||
_runtime.CharacterSelection;
|
||||
_characterSelection;
|
||||
public IRuntimeFellowshipView Fellowship => _runtime.Fellowship;
|
||||
public IRuntimeAllegianceView Allegiance => _runtime.Allegiance;
|
||||
public IRuntimeActionView Actions => _runtime.Actions;
|
||||
|
|
@ -100,9 +102,9 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
|
||||
public IRuntimeSessionCommands Session => _commands;
|
||||
public IRuntimeCharacterSelectionCommands CharacterSelectionCommands =>
|
||||
_runtime.Session;
|
||||
_characterSelection;
|
||||
IRuntimeCharacterSelectionCommands IGameRuntimeCommands.CharacterSelection =>
|
||||
_runtime.Session;
|
||||
_characterSelection;
|
||||
public IRuntimeSelectionCommands Selection => _commands;
|
||||
public IRuntimeCombatCommands Combat => _commands;
|
||||
public IRuntimeMagicCommands Magic => _commands;
|
||||
|
|
@ -166,6 +168,187 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
_subscriptions.Remove(subscription);
|
||||
}
|
||||
|
||||
private RuntimeCharacterSelectionSnapshot CharacterSelectionSnapshot()
|
||||
{
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
if (IsActive)
|
||||
return _runtime.CharacterSelection.Snapshot;
|
||||
return new RuntimeCharacterSelectionSnapshot(
|
||||
_runtime.Generation,
|
||||
RuntimeCharacterSelectionLifecycle.Inactive,
|
||||
Revision: 0,
|
||||
AccountName: string.Empty,
|
||||
SlotCount: 0,
|
||||
RosterCount: 0,
|
||||
HighlightedCharacterId: 0u,
|
||||
HighlightedDisplayIndex: -1,
|
||||
PendingDeleteCharacterId: 0u,
|
||||
LastRestoreRequestedCharacterId: 0u,
|
||||
Operation: RuntimeCharacterSelectionOperation.None,
|
||||
Error: null,
|
||||
Buttons: RuntimeCharacterSelectionButtons.None);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetCharacterSelectionAt(
|
||||
int displayIndex,
|
||||
out RuntimeCharacterSelectionEntry character)
|
||||
{
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
if (IsActive)
|
||||
{
|
||||
return _runtime.CharacterSelection.TryGetAt(
|
||||
displayIndex,
|
||||
out character);
|
||||
}
|
||||
character = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetCharacterSelection(
|
||||
uint characterId,
|
||||
out RuntimeCharacterSelectionEntry character)
|
||||
{
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
if (IsActive)
|
||||
{
|
||||
return _runtime.CharacterSelection.TryGet(
|
||||
characterId,
|
||||
out character);
|
||||
}
|
||||
character = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void VisitCharacterSelection(
|
||||
IRuntimeCharacterSelectionVisitor visitor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(visitor);
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
if (IsActive)
|
||||
_runtime.CharacterSelection.Visit(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
private IDisposable SubscribeCharacterSelection(
|
||||
IRuntimeCharacterSelectionObserver observer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(observer);
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
var gated = new AdapterCharacterSelectionObserver(this, observer);
|
||||
IDisposable runtimeSubscription =
|
||||
_runtime.CharacterSelection.Subscribe(gated);
|
||||
var subscription = new AdapterSubscription(
|
||||
this,
|
||||
runtimeSubscription);
|
||||
_subscriptions.Add(subscription);
|
||||
return subscription;
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeCommandResult ExecuteCharacterSelection(
|
||||
Func<IRuntimeCharacterSelectionCommands, RuntimeCommandResult> execute)
|
||||
{
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return new RuntimeCommandResult(
|
||||
RuntimeCommandStatus.Inactive,
|
||||
_runtime.Generation);
|
||||
}
|
||||
return execute(_runtime.Session);
|
||||
}
|
||||
}
|
||||
|
||||
private void ForwardCharacterSelection(
|
||||
IRuntimeCharacterSelectionObserver observer,
|
||||
in RuntimeCharacterSelectionDelta delta)
|
||||
{
|
||||
lock (_subscriptionGate)
|
||||
{
|
||||
if (IsActive)
|
||||
observer.OnCharacterSelectionChanged(in delta);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CharacterSelectionProjection(
|
||||
CurrentGameRuntimeAdapter owner)
|
||||
: IRuntimeCharacterSelectionView,
|
||||
IRuntimeCharacterSelectionCommands
|
||||
{
|
||||
public RuntimeCharacterSelectionSnapshot Snapshot =>
|
||||
owner.CharacterSelectionSnapshot();
|
||||
|
||||
public bool TryGetAt(
|
||||
int displayIndex,
|
||||
out RuntimeCharacterSelectionEntry character) =>
|
||||
owner.TryGetCharacterSelectionAt(displayIndex, out character);
|
||||
|
||||
public bool TryGet(
|
||||
uint characterId,
|
||||
out RuntimeCharacterSelectionEntry character) =>
|
||||
owner.TryGetCharacterSelection(characterId, out character);
|
||||
|
||||
public void Visit(IRuntimeCharacterSelectionVisitor visitor) =>
|
||||
owner.VisitCharacterSelection(visitor);
|
||||
|
||||
public IDisposable Subscribe(
|
||||
IRuntimeCharacterSelectionObserver observer) =>
|
||||
owner.SubscribeCharacterSelection(observer);
|
||||
|
||||
public RuntimeCommandResult Highlight(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint characterId) =>
|
||||
owner.ExecuteCharacterSelection(
|
||||
commands => commands.Highlight(
|
||||
expectedGeneration,
|
||||
characterId));
|
||||
|
||||
public RuntimeCommandResult Enter(
|
||||
RuntimeGenerationToken expectedGeneration) =>
|
||||
owner.ExecuteCharacterSelection(
|
||||
commands => commands.Enter(expectedGeneration));
|
||||
|
||||
public RuntimeCommandResult RequestDelete(
|
||||
RuntimeGenerationToken expectedGeneration) =>
|
||||
owner.ExecuteCharacterSelection(
|
||||
commands => commands.RequestDelete(expectedGeneration));
|
||||
|
||||
public RuntimeCommandResult ConfirmDelete(
|
||||
RuntimeGenerationToken expectedGeneration) =>
|
||||
owner.ExecuteCharacterSelection(
|
||||
commands => commands.ConfirmDelete(expectedGeneration));
|
||||
|
||||
public RuntimeCommandResult Restore(
|
||||
RuntimeGenerationToken expectedGeneration) =>
|
||||
owner.ExecuteCharacterSelection(
|
||||
commands => commands.Restore(expectedGeneration));
|
||||
|
||||
public RuntimeCommandResult Cancel(
|
||||
RuntimeGenerationToken expectedGeneration) =>
|
||||
owner.ExecuteCharacterSelection(
|
||||
commands => commands.Cancel(expectedGeneration));
|
||||
}
|
||||
|
||||
private sealed class AdapterCharacterSelectionObserver(
|
||||
CurrentGameRuntimeAdapter owner,
|
||||
IRuntimeCharacterSelectionObserver observer)
|
||||
: IRuntimeCharacterSelectionObserver
|
||||
{
|
||||
public void OnCharacterSelectionChanged(
|
||||
in RuntimeCharacterSelectionDelta delta) =>
|
||||
owner.ForwardCharacterSelection(observer, in delta);
|
||||
}
|
||||
|
||||
private sealed class AdapterSubscription(
|
||||
CurrentGameRuntimeAdapter owner,
|
||||
IDisposable runtimeSubscription) : IDisposable
|
||||
|
|
|
|||
|
|
@ -1125,11 +1125,12 @@ public sealed class WorldSession : IDisposable
|
|||
{
|
||||
var opcodes = new List<uint>();
|
||||
ProcessDatagram(datagram.Memory, opcodes);
|
||||
SweepTransport();
|
||||
return opcodes.Contains(0xF7DFu)
|
||||
|| _lastCharacterSelectionError is not null;
|
||||
},
|
||||
ReturnInboundDatagram);
|
||||
ReturnInboundDatagram,
|
||||
SweepTransport,
|
||||
TimeSpan.FromMilliseconds(25));
|
||||
}
|
||||
if (_lastCharacterSelectionError is { } selectionError)
|
||||
{
|
||||
|
|
@ -1817,6 +1818,12 @@ public sealed class WorldSession : IDisposable
|
|||
{
|
||||
continue;
|
||||
}
|
||||
// CharacterError::NumErrors is the enum-count sentinel, not
|
||||
// a server rejection. Retail never presents it, and treating
|
||||
// it as an EnterWorld failure would abort either handshake
|
||||
// pump before a valid ServerReady later in the same packet.
|
||||
if (parsed.AsCode == CharacterError.Code.NumErrors)
|
||||
continue;
|
||||
_lastCharacterSelectionError = parsed;
|
||||
CharacterErrorReceived?.Invoke(parsed);
|
||||
}
|
||||
|
|
@ -3339,10 +3346,15 @@ public sealed class WorldSession : IDisposable
|
|||
ChannelReader<T> reader,
|
||||
TimeSpan timeout,
|
||||
Func<T, bool> processAndCheckConfirmation,
|
||||
Action<T>? release = null)
|
||||
Action<T>? release = null,
|
||||
Action? periodicWork = null,
|
||||
TimeSpan? periodicInterval = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
ArgumentNullException.ThrowIfNull(processAndCheckConfirmation);
|
||||
TimeSpan cadence = periodicInterval ?? TimeSpan.FromMilliseconds(25);
|
||||
if (periodicWork is not null && cadence <= TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(periodicInterval));
|
||||
using var timeoutSource = new CancellationTokenSource(timeout);
|
||||
|
||||
// The deadline is also read straight off the monotonic clock, not only
|
||||
|
|
@ -3382,10 +3394,52 @@ public sealed class WorldSession : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
bool canRead = reader.WaitToReadAsync(timeoutSource.Token)
|
||||
.AsTask()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
periodicWork?.Invoke();
|
||||
if (timeoutSource.IsCancellationRequested || Expired())
|
||||
return false;
|
||||
|
||||
bool canRead;
|
||||
if (periodicWork is null)
|
||||
{
|
||||
canRead = reader.WaitToReadAsync(timeoutSource.Token)
|
||||
.AsTask()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeSpan wait = cadence;
|
||||
if (bounded)
|
||||
{
|
||||
TimeSpan remaining = timeout
|
||||
- Stopwatch.GetElapsedTime(started);
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
return false;
|
||||
if (remaining < wait)
|
||||
wait = remaining;
|
||||
}
|
||||
|
||||
using var sliceSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(
|
||||
timeoutSource.Token);
|
||||
sliceSource.CancelAfter(wait);
|
||||
try
|
||||
{
|
||||
canRead = reader.WaitToReadAsync(sliceSource.Token)
|
||||
.AsTask()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
when (!timeoutSource.IsCancellationRequested
|
||||
&& !Expired())
|
||||
{
|
||||
// This cadence is the paused selector's frame edge:
|
||||
// keep reliable transport work moving even when no
|
||||
// datagram arrives to wake the inbound queue.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!canRead)
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,8 +178,12 @@ public sealed class GameRuntime
|
|||
faultInjection);
|
||||
|
||||
context.Session = dependencies.SessionOperations is null
|
||||
? new LiveSessionController()
|
||||
: new LiveSessionController(dependencies.SessionOperations);
|
||||
? new LiveSessionController(
|
||||
ProductionLiveSessionOperations.Instance,
|
||||
dependencies.TimeProvider)
|
||||
: new LiveSessionController(
|
||||
dependencies.SessionOperations,
|
||||
dependencies.TimeProvider);
|
||||
construction.Own(context.Session);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.SessionCreated,
|
||||
|
|
|
|||
|
|
@ -376,14 +376,17 @@ public sealed class LiveSessionController
|
|||
private Action<WorldSession>? _preLogoffFlushHook;
|
||||
|
||||
public LiveSessionController()
|
||||
: this(ProductionLiveSessionOperations.Instance)
|
||||
: this(ProductionLiveSessionOperations.Instance, null)
|
||||
{
|
||||
}
|
||||
|
||||
public LiveSessionController(ILiveSessionOperations operations)
|
||||
public LiveSessionController(
|
||||
ILiveSessionOperations operations,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
|
||||
CharacterSelectionState = new RuntimeCharacterSelectionState();
|
||||
CharacterSelectionState = new RuntimeCharacterSelectionState(
|
||||
timeProvider);
|
||||
}
|
||||
|
||||
public RuntimeCharacterSelectionState CharacterSelectionState { get; }
|
||||
|
|
@ -583,6 +586,7 @@ public sealed class LiveSessionController
|
|||
_operations.Tick(scope.Session);
|
||||
if (!IsCurrent(scope, generation))
|
||||
return;
|
||||
CharacterSelectionState.SweepRestoreCorrelation();
|
||||
}
|
||||
catch (Exception tickError)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public enum RuntimeCharacterSelectionDeltaKind
|
|||
DeleteAcknowledged,
|
||||
RestoreRequested,
|
||||
RestoreCompleted,
|
||||
RestoreCorrelationExpired,
|
||||
ErrorChanged,
|
||||
EnteringWorld,
|
||||
EnteredWorld,
|
||||
|
|
@ -165,6 +166,9 @@ public interface IRuntimeCharacterSelectionCommands
|
|||
/// </summary>
|
||||
public sealed class RuntimeCharacterSelectionState : IDisposable
|
||||
{
|
||||
internal static readonly TimeSpan RestoreCorrelationTimeout =
|
||||
TimeSpan.FromSeconds(5);
|
||||
|
||||
private sealed class ViewProjection(RuntimeCharacterSelectionState owner)
|
||||
: IRuntimeCharacterSelectionView
|
||||
{
|
||||
|
|
@ -191,6 +195,7 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
private readonly object _gate = new();
|
||||
private readonly RuntimeCharacterSelectionEventStream _events = new();
|
||||
private readonly ViewProjection _view;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private RuntimeCharacterSelectionEntry[] _entries = [];
|
||||
private RuntimeGenerationToken _generation;
|
||||
private RuntimeCharacterSelectionLifecycle _lifecycle;
|
||||
|
|
@ -201,12 +206,15 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
private uint _pendingDeleteCharacterId;
|
||||
private uint _lastRestoreRequestedCharacterId;
|
||||
private bool _restoreResponseArmed;
|
||||
private long _restoreCorrelationStartedTimestamp;
|
||||
private bool _flagOnlyRestoreResponseAmbiguous;
|
||||
private RuntimeCharacterSelectionOperation _operation;
|
||||
private RuntimeCharacterSelectionError? _error;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeCharacterSelectionState()
|
||||
public RuntimeCharacterSelectionState(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_view = new ViewProjection(this);
|
||||
}
|
||||
|
||||
|
|
@ -303,6 +311,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
_pendingDeleteCharacterId = 0u;
|
||||
_lastRestoreRequestedCharacterId = 0u;
|
||||
_restoreResponseArmed = false;
|
||||
_restoreCorrelationStartedTimestamp = 0;
|
||||
_flagOnlyRestoreResponseAmbiguous = false;
|
||||
_operation = RuntimeCharacterSelectionOperation.None;
|
||||
_error = null;
|
||||
_highlightedCharacterId = Contains(previous)
|
||||
|
|
@ -408,6 +418,7 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
if (_disposed
|
||||
|| _lifecycle != RuntimeCharacterSelectionLifecycle.AwaitingSelection
|
||||
|| HasDeleteModalOrRequest()
|
||||
|| _restoreResponseArmed
|
||||
|| index < 0
|
||||
|| !_entries[index].IsPendingDelete)
|
||||
{
|
||||
|
|
@ -419,6 +430,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
_pendingDeleteCharacterId = 0u;
|
||||
_lastRestoreRequestedCharacterId = character.CharacterId;
|
||||
_restoreResponseArmed = true;
|
||||
_restoreCorrelationStartedTimestamp =
|
||||
_timeProvider.GetTimestamp();
|
||||
_operation = RuntimeCharacterSelectionOperation.RestoreRequested;
|
||||
_error = null;
|
||||
_revision++;
|
||||
|
|
@ -429,6 +442,39 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the one outstanding restore correlation after a bounded local
|
||||
/// window. ACE can intentionally send no response for an unknown GUID;
|
||||
/// later flag-only responses carry no GUID, so once a request expires
|
||||
/// they remain ambiguous and are never attributed to a newer request.
|
||||
/// </summary>
|
||||
internal bool SweepRestoreCorrelation()
|
||||
{
|
||||
uint characterId;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed
|
||||
|| !_restoreResponseArmed
|
||||
|| _timeProvider.GetElapsedTime(
|
||||
_restoreCorrelationStartedTimestamp)
|
||||
< RestoreCorrelationTimeout)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
characterId = _lastRestoreRequestedCharacterId;
|
||||
_restoreResponseArmed = false;
|
||||
_restoreCorrelationStartedTimestamp = 0;
|
||||
_flagOnlyRestoreResponseAmbiguous = true;
|
||||
_operation = RuntimeCharacterSelectionOperation.None;
|
||||
_revision++;
|
||||
}
|
||||
Publish(
|
||||
RuntimeCharacterSelectionDeltaKind.RestoreCorrelationExpired,
|
||||
characterId);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool Cancel()
|
||||
{
|
||||
RuntimeCharacterSelectionDeltaKind kind;
|
||||
|
|
@ -493,12 +539,18 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
|| _lifecycle != RuntimeCharacterSelectionLifecycle.AwaitingSelection
|
||||
|| !_restoreResponseArmed)
|
||||
return;
|
||||
if (response.Guid is null
|
||||
&& _flagOnlyRestoreResponseAmbiguous)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (response.Guid is { } responseGuid
|
||||
&& responseGuid != _lastRestoreRequestedCharacterId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_restoreResponseArmed = false;
|
||||
_restoreCorrelationStartedTimestamp = 0;
|
||||
characterId = response.Guid
|
||||
?? _lastRestoreRequestedCharacterId;
|
||||
|
||||
|
|
@ -555,7 +607,10 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
or RuntimeCharacterSelectionLifecycle.EnteringWorld))
|
||||
return;
|
||||
_pendingDeleteCharacterId = 0u;
|
||||
if (_restoreResponseArmed)
|
||||
_flagOnlyRestoreResponseAmbiguous = true;
|
||||
_restoreResponseArmed = false;
|
||||
_restoreCorrelationStartedTimestamp = 0;
|
||||
_operation = RuntimeCharacterSelectionOperation.None;
|
||||
_error = new RuntimeCharacterSelectionError(
|
||||
error.RawErrorCode,
|
||||
|
|
@ -588,6 +643,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
_pendingDeleteCharacterId = 0u;
|
||||
_lastRestoreRequestedCharacterId = 0u;
|
||||
_restoreResponseArmed = false;
|
||||
_restoreCorrelationStartedTimestamp = 0;
|
||||
_flagOnlyRestoreResponseAmbiguous = false;
|
||||
_operation = RuntimeCharacterSelectionOperation.None;
|
||||
_error = null;
|
||||
_lifecycle = RuntimeCharacterSelectionLifecycle.EnteringWorld;
|
||||
|
|
@ -717,7 +774,7 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
return new RuntimeCharacterSelectionButtons(
|
||||
CanEnter: false,
|
||||
CanDelete: false,
|
||||
CanRestore: true,
|
||||
CanRestore: !_restoreResponseArmed,
|
||||
DeleteVisible: false,
|
||||
RestoreVisible: true);
|
||||
}
|
||||
|
|
@ -833,6 +890,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
|||
_pendingDeleteCharacterId = 0u;
|
||||
_lastRestoreRequestedCharacterId = 0u;
|
||||
_restoreResponseArmed = false;
|
||||
_restoreCorrelationStartedTimestamp = 0;
|
||||
_flagOnlyRestoreResponseAmbiguous = false;
|
||||
_operation = RuntimeCharacterSelectionOperation.None;
|
||||
_error = null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue