fix(runtime): isolate Campaign LA7b delete state
This commit is contained in:
parent
1b9e7e41f9
commit
ff40656293
2 changed files with 139 additions and 4 deletions
|
|
@ -466,7 +466,8 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
||||||
_restoreResponseArmed = false;
|
_restoreResponseArmed = false;
|
||||||
_restoreCorrelationStartedTimestamp = 0;
|
_restoreCorrelationStartedTimestamp = 0;
|
||||||
_flagOnlyRestoreResponseAmbiguous = true;
|
_flagOnlyRestoreResponseAmbiguous = true;
|
||||||
_operation = RuntimeCharacterSelectionOperation.None;
|
if (!HasConfirmedDelete())
|
||||||
|
_operation = RuntimeCharacterSelectionOperation.None;
|
||||||
_revision++;
|
_revision++;
|
||||||
}
|
}
|
||||||
Publish(
|
Publish(
|
||||||
|
|
@ -533,6 +534,7 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
||||||
{
|
{
|
||||||
uint characterId;
|
uint characterId;
|
||||||
uint errorCode = 0u;
|
uint errorCode = 0u;
|
||||||
|
bool deleteInFlight;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_disposed
|
if (_disposed
|
||||||
|
|
@ -553,6 +555,7 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
||||||
_restoreCorrelationStartedTimestamp = 0;
|
_restoreCorrelationStartedTimestamp = 0;
|
||||||
characterId = response.Guid
|
characterId = response.Guid
|
||||||
?? _lastRestoreRequestedCharacterId;
|
?? _lastRestoreRequestedCharacterId;
|
||||||
|
deleteInFlight = HasConfirmedDelete();
|
||||||
|
|
||||||
if (response.IsOk
|
if (response.IsOk
|
||||||
&& response.Guid is { } guid
|
&& response.Guid is { } guid
|
||||||
|
|
@ -573,13 +576,21 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
||||||
string.CompareOrdinal(left.Name, right.Name));
|
string.CompareOrdinal(left.Name, right.Name));
|
||||||
_entries = StablePartitionGreyedToTail(_entries);
|
_entries = StablePartitionGreyedToTail(_entries);
|
||||||
}
|
}
|
||||||
_operation = RuntimeCharacterSelectionOperation.RestoreSucceeded;
|
if (!deleteInFlight)
|
||||||
|
{
|
||||||
|
_operation =
|
||||||
|
RuntimeCharacterSelectionOperation.RestoreSucceeded;
|
||||||
|
}
|
||||||
_error = null;
|
_error = null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
errorCode = response.VerificationFlag;
|
errorCode = response.VerificationFlag;
|
||||||
_operation = RuntimeCharacterSelectionOperation.RestoreRejected;
|
if (!deleteInFlight)
|
||||||
|
{
|
||||||
|
_operation =
|
||||||
|
RuntimeCharacterSelectionOperation.RestoreRejected;
|
||||||
|
}
|
||||||
_error = new RuntimeCharacterSelectionError(
|
_error = new RuntimeCharacterSelectionError(
|
||||||
response.VerificationFlag,
|
response.VerificationFlag,
|
||||||
CharacterError.Code.Undefined,
|
CharacterError.Code.Undefined,
|
||||||
|
|
@ -804,7 +815,10 @@ public sealed class RuntimeCharacterSelectionState : IDisposable
|
||||||
|
|
||||||
private bool HasDeleteModalOrRequest() =>
|
private bool HasDeleteModalOrRequest() =>
|
||||||
_pendingDeleteCharacterId != 0u
|
_pendingDeleteCharacterId != 0u
|
||||||
|| _operation is RuntimeCharacterSelectionOperation.DeleteRequested
|
|| HasConfirmedDelete();
|
||||||
|
|
||||||
|
private bool HasConfirmedDelete() =>
|
||||||
|
_operation is RuntimeCharacterSelectionOperation.DeleteRequested
|
||||||
or RuntimeCharacterSelectionOperation.DeleteAcknowledged;
|
or RuntimeCharacterSelectionOperation.DeleteAcknowledged;
|
||||||
|
|
||||||
private static RuntimeCharacterSelectionEntry[] StablePartitionGreyedToTail(
|
private static RuntimeCharacterSelectionEntry[] StablePartitionGreyedToTail(
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,127 @@ public sealed class LiveSessionControllerTests
|
||||||
Assert.False(controller.IsInWorld);
|
Assert.False(controller.IsInWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void RestoreCompletionDuringConfirmedDelete_PreservesDeleteUntilAck(
|
||||||
|
bool acknowledgeBeforeCompletion)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var operations = new TestOperations(calls);
|
||||||
|
var host = new TestHost(calls);
|
||||||
|
var controller = new LiveSessionController(operations);
|
||||||
|
Assert.Equal(
|
||||||
|
LiveSessionStartStatus.AwaitingCharacterSelection,
|
||||||
|
controller.Start(LiveOptions(awaitSelection: true), host).Status);
|
||||||
|
RuntimeGenerationToken generation = controller.Generation;
|
||||||
|
|
||||||
|
Assert.True(controller.Highlight(generation, 0x50000001u).Accepted);
|
||||||
|
Assert.True(controller.Restore(generation).Accepted);
|
||||||
|
Assert.True(controller.Highlight(generation, 0x50000002u).Accepted);
|
||||||
|
Assert.True(controller.RequestDelete(generation).Accepted);
|
||||||
|
Assert.True(controller.ConfirmDelete(generation).Accepted);
|
||||||
|
Assert.Equal([("Canonical", 1)], operations.DeleteRequests);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionOperation.DeleteRequested,
|
||||||
|
controller.CharacterSelection.Snapshot.Operation);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionButtons.None,
|
||||||
|
controller.CharacterSelection.Snapshot.Buttons);
|
||||||
|
|
||||||
|
if (acknowledgeBeforeCompletion)
|
||||||
|
controller.CharacterSelectionState.ApplyDeleteAcknowledged();
|
||||||
|
|
||||||
|
controller.CharacterSelectionState.ApplyRestore(
|
||||||
|
new CharacterRestore.Parsed(
|
||||||
|
VerificationFlag: 1u,
|
||||||
|
Guid: 0x50000001u,
|
||||||
|
Name: "Restored",
|
||||||
|
SecondsGreyedOut: 0u));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
acknowledgeBeforeCompletion
|
||||||
|
? RuntimeCharacterSelectionOperation.DeleteAcknowledged
|
||||||
|
: RuntimeCharacterSelectionOperation.DeleteRequested,
|
||||||
|
controller.CharacterSelection.Snapshot.Operation);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionButtons.None,
|
||||||
|
controller.CharacterSelection.Snapshot.Buttons);
|
||||||
|
Assert.True(controller.CharacterSelection.TryGet(
|
||||||
|
0x50000001u,
|
||||||
|
out RuntimeCharacterSelectionEntry restored));
|
||||||
|
Assert.False(restored.IsPendingDelete);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCommandStatus.Rejected,
|
||||||
|
controller.ConfirmDelete(generation).Status);
|
||||||
|
Assert.Equal([("Canonical", 1)], operations.DeleteRequests);
|
||||||
|
|
||||||
|
if (!acknowledgeBeforeCompletion)
|
||||||
|
controller.CharacterSelectionState.ApplyDeleteAcknowledged();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionOperation.DeleteAcknowledged,
|
||||||
|
controller.CharacterSelection.Snapshot.Operation);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionButtons.None,
|
||||||
|
controller.CharacterSelection.Snapshot.Buttons);
|
||||||
|
Assert.Equal([("Canonical", 1)], operations.DeleteRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void RestoreTimeoutDuringConfirmedDelete_PreservesDeleteUntilAck(
|
||||||
|
bool acknowledgeBeforeTimeout)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var operations = new TestOperations(calls);
|
||||||
|
var host = new TestHost(calls);
|
||||||
|
var time = new ManualTimeProvider();
|
||||||
|
var controller = new LiveSessionController(operations, time);
|
||||||
|
Assert.Equal(
|
||||||
|
LiveSessionStartStatus.AwaitingCharacterSelection,
|
||||||
|
controller.Start(LiveOptions(awaitSelection: true), host).Status);
|
||||||
|
RuntimeGenerationToken generation = controller.Generation;
|
||||||
|
|
||||||
|
Assert.True(controller.Highlight(generation, 0x50000001u).Accepted);
|
||||||
|
Assert.True(controller.Restore(generation).Accepted);
|
||||||
|
Assert.True(controller.Highlight(generation, 0x50000002u).Accepted);
|
||||||
|
Assert.True(controller.RequestDelete(generation).Accepted);
|
||||||
|
Assert.True(controller.ConfirmDelete(generation).Accepted);
|
||||||
|
Assert.Equal([("Canonical", 1)], operations.DeleteRequests);
|
||||||
|
|
||||||
|
if (acknowledgeBeforeTimeout)
|
||||||
|
controller.CharacterSelectionState.ApplyDeleteAcknowledged();
|
||||||
|
|
||||||
|
time.Advance(RuntimeCharacterSelectionState.RestoreCorrelationTimeout);
|
||||||
|
controller.Tick();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
acknowledgeBeforeTimeout
|
||||||
|
? RuntimeCharacterSelectionOperation.DeleteAcknowledged
|
||||||
|
: RuntimeCharacterSelectionOperation.DeleteRequested,
|
||||||
|
controller.CharacterSelection.Snapshot.Operation);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionButtons.None,
|
||||||
|
controller.CharacterSelection.Snapshot.Buttons);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCommandStatus.Rejected,
|
||||||
|
controller.ConfirmDelete(generation).Status);
|
||||||
|
Assert.Equal([("Canonical", 1)], operations.DeleteRequests);
|
||||||
|
|
||||||
|
if (!acknowledgeBeforeTimeout)
|
||||||
|
controller.CharacterSelectionState.ApplyDeleteAcknowledged();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionOperation.DeleteAcknowledged,
|
||||||
|
controller.CharacterSelection.Snapshot.Operation);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeCharacterSelectionButtons.None,
|
||||||
|
controller.CharacterSelection.Snapshot.Buttons);
|
||||||
|
Assert.Equal([("Canonical", 1)], operations.DeleteRequests);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CharacterCommands_AreGenerationGatedAcrossReconnectAndStop()
|
public void CharacterCommands_AreGenerationGatedAcrossReconnectAndStop()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue