fix(world): restore portal and distant use lifecycles
This commit is contained in:
parent
c730632075
commit
1a14812c44
18 changed files with 401 additions and 196 deletions
|
|
@ -42,6 +42,32 @@ public sealed class LocalPlayerControllerSlotTests
|
|||
Assert.Same(controller.Motion, source.Motion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionPreparation_DrainsPriorAnimationBeforePublishingCandidate()
|
||||
{
|
||||
var slot = new LocalPlayerControllerSlot();
|
||||
ILocalPlayerMotionSource source = slot;
|
||||
var prior = new PlayerMovementController(new PhysicsEngine());
|
||||
var candidate = new PlayerMovementController(new PhysicsEngine());
|
||||
slot.Controller = prior;
|
||||
var order = new List<string>();
|
||||
|
||||
using (slot.BeginMotionPreparation(
|
||||
candidate,
|
||||
() =>
|
||||
{
|
||||
order.Add("drain");
|
||||
Assert.Same(prior.Motion, source.Motion);
|
||||
}))
|
||||
{
|
||||
order.Add("candidate");
|
||||
Assert.Same(candidate.Motion, source.Motion);
|
||||
}
|
||||
|
||||
Assert.Equal(["drain", "candidate"], order);
|
||||
Assert.Same(prior.Motion, source.Motion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionPreparation_RejectsConcurrentCandidate()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -295,15 +295,15 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CloseUseTurnsFirstThenSendsExactlyOnceOnNaturalCompletion()
|
||||
public void CloseUseSendsImmediatelyWithoutSpeculativeMovement()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
||||
h.Controller.SendUse(Target);
|
||||
|
||||
Assert.Single(h.Movement.Approaches);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Empty(h.Movement.Approaches);
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
|
||||
h.Controller.OnNaturalMoveToComplete();
|
||||
h.Controller.OnNaturalMoveToComplete();
|
||||
|
|
@ -312,7 +312,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CancelledDeferredUseReleasesBusyWithoutServerCompletion()
|
||||
public void AcceptedUseRemainsBusyUntilAuthoritativeUseDone()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
|
@ -322,18 +322,22 @@ public sealed class SelectionInteractionControllerTests
|
|||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Equal(1, h.Items.BusyCount);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
|
||||
h.CompletionLifetime.PublishCancellation(WeenieError.ActionCancelled);
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Equal(1, h.Items.BusyCount);
|
||||
Assert.False(h.Items.CanMakeInventoryRequest);
|
||||
|
||||
h.Items.CompleteUse(0u);
|
||||
|
||||
Assert.Equal(0, h.Items.BusyCount);
|
||||
Assert.True(h.Items.CanMakeInventoryRequest);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatchedDeferredUseRemainsBusyUntilAuthoritativeUseDone()
|
||||
public void DispatchedWorldUseRemainsBusyUntilAuthoritativeUseDone()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
|
@ -341,8 +345,6 @@ public sealed class SelectionInteractionControllerTests
|
|||
|
||||
h.Controller.HandleInputAction(InputAction.UseSelected);
|
||||
h.Controller.DrainOutbound();
|
||||
h.CompletionLifetime.PublishNaturalCompletion();
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
Assert.Equal(1, h.Items.BusyCount);
|
||||
|
|
@ -354,7 +356,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedDeferredUseStartReleasesItsBusyReservation()
|
||||
public void WorldUseDoesNotDependOnLocalApproachInstallation()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
|
@ -364,32 +366,27 @@ public sealed class SelectionInteractionControllerTests
|
|||
h.Controller.HandleInputAction(InputAction.UseSelected);
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Equal(0, h.Items.BusyCount);
|
||||
Assert.True(h.Items.CanMakeInventoryRequest);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Equal(1, h.Items.BusyCount);
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
Assert.Empty(h.Movement.Approaches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaleDeferredUseReleasesItsBusyReservation()
|
||||
public void RejectedWorldUseReleasesItsBusyReservation()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
h.Query.Useable = false;
|
||||
h.Selection.Select(Target, SelectionChangeSource.World);
|
||||
|
||||
h.Controller.HandleInputAction(InputAction.UseSelected);
|
||||
h.Controller.DrainOutbound();
|
||||
Assert.Equal(1, h.Items.BusyCount);
|
||||
|
||||
h.Query.Current = false;
|
||||
h.CompletionLifetime.PublishNaturalCompletion();
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Equal(0, h.Items.BusyCount);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronousTurnCompletionTransfersBusyOwnershipToUseDone()
|
||||
public void SynchronousMovementCallbackCannotDuplicateWorldUse()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
|
@ -408,7 +405,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void FarUseSendsImmediatelyAndNaturalCompletionDoesNotRetry()
|
||||
public void FarUseSendsImmediatelyWithoutClientApproachAndDoesNotRetry()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: false);
|
||||
|
|
@ -416,7 +413,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
h.Controller.SendUse(Target);
|
||||
h.Controller.OnNaturalMoveToComplete();
|
||||
|
||||
Assert.Single(h.Movement.Approaches);
|
||||
Assert.Empty(h.Movement.Approaches);
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
}
|
||||
|
||||
|
|
@ -448,21 +445,21 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenOrStaleIncarnationCancelsDeferredAction()
|
||||
public void HiddenOrStaleNotificationsCannotRetractOrDuplicateDispatchedUse()
|
||||
{
|
||||
var hidden = new Harness();
|
||||
hidden.SetApproach(closeRange: true);
|
||||
hidden.Controller.SendUse(Target);
|
||||
hidden.Controller.OnEntityHidden(Target);
|
||||
hidden.Controller.OnNaturalMoveToComplete();
|
||||
Assert.Empty(hidden.Transport.Uses);
|
||||
Assert.Equal(new[] { Target }, hidden.Transport.Uses);
|
||||
|
||||
var stale = new Harness();
|
||||
stale.SetApproach(closeRange: true);
|
||||
stale.Controller.SendUse(Target);
|
||||
stale.Query.Current = false;
|
||||
stale.Controller.OnNaturalMoveToComplete();
|
||||
Assert.Empty(stale.Transport.Uses);
|
||||
Assert.Equal(new[] { Target }, stale.Transport.Uses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -487,7 +484,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
h.SetApproach(closeRange: true);
|
||||
h.Selection.Select(Target, SelectionChangeSource.World);
|
||||
h.Controller.HandleInputAction(InputAction.SelectionPickUp);
|
||||
h.Controller.SendUse(Target);
|
||||
h.Controller.HandleInputAction(InputAction.UseSelected);
|
||||
h.Items.InteractionState.EnterExamine();
|
||||
|
||||
h.Controller.ResetSession();
|
||||
|
|
@ -501,12 +498,12 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void OldRemovalClearsCapturedActionButDoesNotClearReplacementSelection()
|
||||
public void OldRemovalClearsCapturedPickupButDoesNotClearReplacementSelection()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true, localEntityId: 101u);
|
||||
h.Selection.Select(Target, SelectionChangeSource.World);
|
||||
h.Controller.SendUse(Target);
|
||||
Assert.True(h.Items.PlaceWorldItemInBackpack(Target));
|
||||
var oldRecord = new LiveEntityRecord(Spawn(Target, instance: 1));
|
||||
oldRecord.WorldEntity = new WorldEntity
|
||||
{
|
||||
|
|
@ -522,11 +519,12 @@ public sealed class SelectionInteractionControllerTests
|
|||
h.Controller.OnNaturalMoveToComplete();
|
||||
|
||||
Assert.Equal(Target, h.Selection.SelectedObjectId);
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Empty(h.Transport.Pickups);
|
||||
Assert.Equal(new[] { Target }, h.CancelledPlacements.Select(p => p.ItemId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronousNaturalCompletionObservesTheArmedCloseAction()
|
||||
public void SynchronousNaturalCompletionCannotDuplicateUse()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
|
@ -539,16 +537,16 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronousResetCannotResurrectTheCloseAction()
|
||||
public void SynchronousResetCannotResurrectACancelledPickup()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
h.Movement.AfterArm = h.Controller.ResetSession;
|
||||
|
||||
h.Controller.SendUse(Target);
|
||||
Assert.True(h.Items.PlaceWorldItemInBackpack(Target));
|
||||
h.Controller.OnNaturalMoveToComplete();
|
||||
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Empty(h.Transport.Pickups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -569,7 +567,7 @@ public sealed class SelectionInteractionControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrorCompletionClearsCloseActionBeforeALaterSuccess()
|
||||
public void ErrorCompletionCannotRetractOrDuplicateDispatchedUse()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
|
|
@ -578,48 +576,45 @@ public sealed class SelectionInteractionControllerTests
|
|||
h.Controller.OnMoveToCancelled(WeenieError.NoObject);
|
||||
h.Controller.OnNaturalMoveToComplete();
|
||||
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DeferredCompletionFromPriorApproachCannotAffectReplacement(
|
||||
bool priorCompletedNaturally)
|
||||
[Fact]
|
||||
public void CompletionFromPriorPickupCannotAffectReplacement()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
h.Controller.SendUse(Target);
|
||||
if (priorCompletedNaturally)
|
||||
h.CompletionLifetime.PublishNaturalCompletion();
|
||||
else
|
||||
h.CompletionLifetime.PublishCancellation(WeenieError.ActionCancelled);
|
||||
Assert.True(h.Items.PlaceWorldItemInBackpack(Target));
|
||||
h.CompletionLifetime.PublishCancellation(WeenieError.ActionCancelled);
|
||||
|
||||
h.Controller.SendUse(Target);
|
||||
h.Selection.Select(Target, SelectionChangeSource.World);
|
||||
h.Controller.HandleInputAction(InputAction.UseSelected);
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
Assert.Empty(h.Transport.Pickups);
|
||||
|
||||
h.CompletionLifetime.PublishNaturalCompletion();
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Equal(new[] { Target }, h.Transport.Uses);
|
||||
Assert.Empty(h.Transport.Pickups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetiringControllerLifetimeInvalidatesQueuedArrival()
|
||||
public void RetiringControllerLifetimeInvalidatesQueuedPickupArrival()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SetApproach(closeRange: true);
|
||||
h.Controller.SendUse(Target);
|
||||
Assert.True(h.Items.PlaceWorldItemInBackpack(Target));
|
||||
h.CompletionLifetime.PublishNaturalCompletion();
|
||||
|
||||
h.Completions.RetireControllerLifetime(h.CompletionLifetime);
|
||||
h.Controller.DrainOutbound();
|
||||
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Empty(h.Transport.Pickups);
|
||||
h.Controller.OnNaturalMoveToComplete();
|
||||
Assert.Empty(h.Transport.Uses);
|
||||
Assert.Empty(h.Transport.Pickups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -219,6 +219,26 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
Assert.Same(fixture.Record, context.LastRecord);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MotionDone_CurrentLogicalOwnerResolvesWhileSpatialProjectionIsWithdrawn()
|
||||
{
|
||||
var fixture = Build(partCount: 1, withSequencer: true);
|
||||
var interpreter = new MotionInterpreter(new PhysicsBody());
|
||||
interpreter.AddToQueue(0, MotionCommand.Ready, 0);
|
||||
var context = new Context { Motion = interpreter };
|
||||
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), context);
|
||||
|
||||
Assert.True(fixture.Live.WithdrawLiveEntityProjection(Guid));
|
||||
Assert.False(fixture.Record.IsSpatiallyProjected);
|
||||
presenter.PrepareAnimation(fixture.Record, fixture.State);
|
||||
Assert.NotNull(fixture.State.Sequencer!.MotionDoneTarget);
|
||||
fixture.State.Sequencer!.MotionDoneTarget!(MotionCommand.Ready, true);
|
||||
|
||||
Assert.Equal(1, context.ResolveCount);
|
||||
Assert.Same(fixture.Record, context.LastRecord);
|
||||
Assert.False(interpreter.MotionsPending());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectPoseCallback_NestedRuntimeSnapshotDoesNotTruncatePresentation()
|
||||
{
|
||||
|
|
@ -535,13 +555,14 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
private sealed class Context : ILiveAnimationPresentationContext
|
||||
{
|
||||
public uint LocalPlayerGuid => 0x50000001u;
|
||||
public MotionInterpreter? Motion { get; init; }
|
||||
public int ResolveCount { get; private set; }
|
||||
public LiveEntityRecord? LastRecord { get; private set; }
|
||||
public MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record)
|
||||
{
|
||||
ResolveCount++;
|
||||
LastRecord = record;
|
||||
return null;
|
||||
return Motion;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.App.Tests.Streaming;
|
||||
|
|
@ -155,7 +156,7 @@ public sealed class WorldRevealCoordinatorTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void PortalViewportReveal_ReopensWorldWithoutCompletingProtocolTail()
|
||||
public void Materialization_ReopensWorldBeforePortalViewportRelease()
|
||||
{
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
var world = new GpuWorldState(availability: availability);
|
||||
|
|
@ -180,6 +181,16 @@ public sealed class WorldRevealCoordinatorTests
|
|||
WorldRevealKind.Portal,
|
||||
0x3032001Cu);
|
||||
|
||||
Assert.False(availability.IsWorldAvailable);
|
||||
|
||||
coordinator.ObserveMaterialized();
|
||||
|
||||
Assert.True(availability.IsWorldAvailable);
|
||||
Assert.False(coordinator.Snapshot.WorldViewportObserved);
|
||||
Assert.False(coordinator.Snapshot.Completed);
|
||||
Assert.Equal(1, audio.ResumeCalls);
|
||||
Assert.Empty(scheduler.Ends);
|
||||
|
||||
coordinator.RevealWorldViewport();
|
||||
coordinator.RevealWorldViewport();
|
||||
|
||||
|
|
@ -195,6 +206,53 @@ public sealed class WorldRevealCoordinatorTests
|
|||
Assert.Equal([generation], scheduler.Ends);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TunnelContinue_TicksDestinationObjectsBehindPortalViewport()
|
||||
{
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
var world = new GpuWorldState(availability: availability);
|
||||
var quiescence = new WorldGenerationQuiescence(
|
||||
availability,
|
||||
new SelectionState(),
|
||||
world,
|
||||
audio: null);
|
||||
var scheduler = new RecordingDestinationScheduler();
|
||||
var coordinator = new WorldRevealCoordinator(
|
||||
isRenderNeighborhoodReady: (_, _) => true,
|
||||
isSpawnCellReady: _ => true,
|
||||
isTerrainNeighborhoodReady: (_, _) => true,
|
||||
areCompositeTexturesReady: () => true,
|
||||
prepareCompositeTextures: (_, _) => { },
|
||||
invalidateCompositeTextures: () => { },
|
||||
isSpawnClaimUnhydratable: _ => false,
|
||||
quiescence: quiescence,
|
||||
streaming: scheduler);
|
||||
var calls = new List<string>();
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
new global::AcDream.App.Tests.TestLiveObjectFramePhase(
|
||||
_ => calls.Add("objects")),
|
||||
world,
|
||||
new global::AcDream.App.Tests.TestLiveSessionFramePhase(
|
||||
() => calls.Add("network")),
|
||||
new global::AcDream.App.Tests.TestPostNetworkCommandFramePhase(
|
||||
() => calls.Add("commands")),
|
||||
new global::AcDream.App.Tests.TestLiveSpatialReconcilePhase(
|
||||
() => calls.Add("spatial")),
|
||||
availability);
|
||||
|
||||
coordinator.Begin(WorldRevealKind.Portal, 0x3032001Cu);
|
||||
frame.Tick(1f / 60f);
|
||||
Assert.Equal(["network", "commands"], calls);
|
||||
|
||||
calls.Clear();
|
||||
coordinator.ObserveMaterialized();
|
||||
frame.Tick(1f / 60f);
|
||||
|
||||
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
||||
Assert.Empty(scheduler.Ends);
|
||||
Assert.False(coordinator.Snapshot.WorldViewportObserved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue