Move local teleport, player-mode, animation, shadow, and sealed-dungeon lifetimes out of GameWindow. Make player-mode entry transactional and isolate approach completions by controller lifetime and approach generation so stale callbacks cannot affect replacements. Co-authored-by: Codex <noreply@openai.com>
747 lines
26 KiB
C#
747 lines
26 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.Update;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LocalPlayerTeleportControllerTests
|
|
{
|
|
[Fact]
|
|
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
|
|
{
|
|
var harness = new Harness();
|
|
WorldSession.EntityPositionUpdate destination = Position(
|
|
cellId: 0x20210001u,
|
|
teleportSequence: 7,
|
|
x: 12f,
|
|
y: 34f,
|
|
z: 5f);
|
|
|
|
harness.Controller.OfferDestination(
|
|
destination,
|
|
teleportTimestampAdvanced: true);
|
|
Assert.Empty(harness.Streaming.PriorityWrites);
|
|
|
|
harness.Controller.OnTeleportStarted(7);
|
|
|
|
Assert.Equal(1, harness.Input.EndCount);
|
|
Assert.Equal(1, harness.Mode.EnterPortalCount);
|
|
Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection);
|
|
Assert.Equal((0x2021FFFFu, LocalPlayerTeleportController.NearRingRadius),
|
|
Assert.Single(harness.Streaming.PriorityWrites));
|
|
Assert.True(harness.Reveal.Snapshot.IsActive);
|
|
Assert.Equal(WorldRevealKind.Portal, harness.Reveal.Snapshot.Kind);
|
|
|
|
harness.Controller.OfferDestination(
|
|
destination,
|
|
teleportTimestampAdvanced: false);
|
|
Assert.Single(harness.Streaming.PriorityWrites);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingPlayerController_KeepsStartAndDestinationPendingUntilProjectionExists()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Mode.Controller = null;
|
|
|
|
harness.Controller.OnTeleportStarted(2);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 2, 7f, 8f, 9f),
|
|
teleportTimestampAdvanced: true);
|
|
|
|
Assert.False(harness.Controller.IsActive);
|
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
|
Assert.Null(harness.Presentation.BeginProjection);
|
|
|
|
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
|
|
harness.Mode.Controller.SetPosition(
|
|
Vector3.Zero,
|
|
0x20210001u,
|
|
Vector3.Zero);
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.True(harness.Controller.IsActive);
|
|
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
|
Assert.NotNull(harness.Presentation.BeginProjection);
|
|
Assert.Single(harness.Streaming.PriorityWrites);
|
|
}
|
|
|
|
[Fact]
|
|
public void ControllerWithdrawnAfterActivation_KeepsAcceptedDestinationUntilItReturns()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Controller.OnTeleportStarted(14);
|
|
Assert.True(harness.Controller.IsActive);
|
|
|
|
harness.Mode.Controller = null;
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 14, 7f, 8f, 9f),
|
|
teleportTimestampAdvanced: true);
|
|
|
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
|
Assert.Empty(harness.Streaming.PriorityWrites);
|
|
|
|
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
|
|
harness.Mode.Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
|
|
Assert.Single(harness.Streaming.PriorityWrites);
|
|
}
|
|
|
|
[Fact]
|
|
public void ControllerWithdrawnAfterAim_HoldsPresentationUntilModeRebuildsIt()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Controller.OnTeleportStarted(15);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 15, 7f, 8f, 9f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Mode.Controller = null;
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
|
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.Equal(default, harness.Placement.Position);
|
|
Assert.Empty(harness.Presentation.WorldReadyValues);
|
|
|
|
harness.Mode.RebuildOnEnter = () =>
|
|
{
|
|
var controller = new PlayerMovementController(new PhysicsEngine());
|
|
controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
|
return controller;
|
|
};
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.NotNull(harness.Mode.Controller);
|
|
Assert.Equal(new Vector3(7f, 8f, 9f), harness.Placement.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition()
|
|
{
|
|
var harness = new Harness(centerX: 0x20, centerY: 0x21);
|
|
harness.Controller.OnTeleportStarted(3);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210123u, 3, 20f, 30f, 4f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Presentation.EmitPlaceWhenReady = true;
|
|
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.Empty(harness.Streaming.Recenters);
|
|
Assert.Equal(new Vector3(20f, 30f, 4f), harness.Placement.Position);
|
|
Assert.Equal(0x20210123u, harness.Placement.CellId);
|
|
Assert.False(harness.Placement.Forced);
|
|
}
|
|
|
|
[Fact]
|
|
public void CrossLandblockDestination_RecentersBeforeItCanBecomeReady()
|
|
{
|
|
var harness = new Harness(centerX: 0x20, centerY: 0x21);
|
|
harness.Mode.Controller!.SetPosition(
|
|
Vector3.Zero,
|
|
0x20210001u,
|
|
Vector3.Zero);
|
|
harness.Controller.OnTeleportStarted(4);
|
|
harness.Streaming.RecenterPending = true;
|
|
harness.Controller.OfferDestination(
|
|
Position(0x30310100u, 4, -2f, 8f, 9f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Presentation.EmitPlaceWhenReady = true;
|
|
|
|
harness.Controller.Tick(1f);
|
|
|
|
Assert.Equal((0x30, 0x31, true), Assert.Single(harness.Streaming.Recenters));
|
|
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
|
|
Assert.Equal(default, harness.Placement.Position);
|
|
|
|
harness.Streaming.RecenterPending = false;
|
|
harness.Controller.Tick(0.016f);
|
|
Assert.Equal(new Vector3(-2f, 8f, 9f), harness.Placement.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReadinessHold_UsesWallClockAndForcesExactlyAtTenSeconds()
|
|
{
|
|
var harness = new Harness(worldReady: false);
|
|
harness.Controller.OnTeleportStarted(5);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 5, 1f, 2f, 3f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Presentation.EmitPlaceWhenReady = true;
|
|
|
|
harness.Controller.Tick(9.5f);
|
|
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
|
|
Assert.Equal(default, harness.Placement.Position);
|
|
|
|
harness.Controller.Tick(0.5f);
|
|
|
|
Assert.True(harness.Presentation.WorldReadyValues[^1]);
|
|
Assert.True(harness.Placement.Forced);
|
|
Assert.Equal(new Vector3(1f, 2f, 3f), harness.Placement.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void Place_ReconcilesInsidePlacementBeforeRevealMaterialized()
|
|
{
|
|
var order = new List<string>();
|
|
var harness = new Harness(order: order);
|
|
harness.Controller.OnTeleportStarted(8);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 8, 4f, 5f, 6f),
|
|
teleportTimestampAdvanced: true);
|
|
order.Clear();
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
|
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.True(Index(order, "placement") < Index(order, "[world-reveal] event=materialized"));
|
|
Assert.True(Index(order, "[world-reveal] event=materialized") < Index(order, "priority-clear"));
|
|
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets()
|
|
{
|
|
var order = new List<string>();
|
|
var harness = new Harness(order: order);
|
|
harness.Controller.OnTeleportStarted(9);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 9, 4f, 5f, 6f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
|
harness.Controller.Tick(0.016f);
|
|
order.Clear();
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete);
|
|
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.True(Index(order, "enter-world") < Index(order, "login-complete"));
|
|
Assert.True(Index(order, "login-complete") < Index(order, "[world-reveal] event=complete"));
|
|
Assert.True(Index(order, "[world-reveal] event=complete") < Index(order, "presentation-reset"));
|
|
Assert.False(harness.Controller.IsActive);
|
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
|
Assert.Equal(1, harness.Session.LoginCompleteCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void NewerStart_ReplacesOldDestinationWithoutReusingIt()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Controller.OnTeleportStarted(10);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 10, 1f, 1f, 1f),
|
|
teleportTimestampAdvanced: true);
|
|
|
|
harness.Controller.OnTeleportStarted(11);
|
|
harness.Controller.Tick(LocalPlayerTeleportController.MaximumWorldHoldSeconds);
|
|
|
|
Assert.Equal(default, harness.Placement.Position);
|
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
|
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 11, 2f, 2f, 2f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
|
harness.Controller.Tick(0.016f);
|
|
Assert.Equal(new Vector3(2f), harness.Placement.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionReset_ClearsTransitCancelsRevealAndRequiresRecenterConvergence()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Controller.OnTeleportStarted(12);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 12, 1f, 2f, 3f),
|
|
teleportTimestampAdvanced: true);
|
|
|
|
harness.Controller.ResetSession();
|
|
|
|
Assert.False(harness.Controller.IsActive);
|
|
Assert.True(harness.Reveal.Snapshot.Cancelled);
|
|
Assert.True(harness.Streaming.LastResetWasSessionEnding);
|
|
|
|
var failed = new Harness();
|
|
failed.Streaming.ResetResult = false;
|
|
Assert.Throws<InvalidOperationException>(() => failed.Controller.ResetSession());
|
|
}
|
|
|
|
[Fact]
|
|
public void StaleStart_DoesNotMutateInputModeOrPresentation()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Authority.IsFresh = false;
|
|
|
|
harness.Controller.OnTeleportStarted(13);
|
|
|
|
Assert.Equal(0, harness.Input.EndCount);
|
|
Assert.Equal(0, harness.Mode.EnterPortalCount);
|
|
Assert.Null(harness.Presentation.BeginProjection);
|
|
Assert.False(harness.Controller.IsActive);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReentrantNewStartDuringPlacement_CannotBeCompletedByOldTail()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Controller.OnTeleportStarted(20);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 20, 1f, 2f, 3f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Streaming.PriorityClearCount = 0;
|
|
harness.Placement.OnPlace = () => harness.Controller.OnTeleportStarted(21);
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
|
|
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.True(harness.Controller.IsActive);
|
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
|
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
|
|
Assert.Equal(1, harness.Streaming.PriorityClearCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReentrantNewStartDuringLoginComplete_PreservesReplacementLifetime()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Controller.OnTeleportStarted(30);
|
|
harness.Controller.OfferDestination(
|
|
Position(0x20210001u, 30, 1f, 2f, 3f),
|
|
teleportTimestampAdvanced: true);
|
|
harness.Session.OnSend = () => harness.Controller.OnTeleportStarted(31);
|
|
harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete);
|
|
|
|
harness.Controller.Tick(0.016f);
|
|
|
|
Assert.True(harness.Controller.IsActive);
|
|
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
|
|
Assert.False(harness.Reveal.Snapshot.Completed);
|
|
Assert.Equal(1, harness.Session.LoginCompleteCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposeFailure_CanBeRetriedWithoutDoubleDisposingAfterSuccess()
|
|
{
|
|
var harness = new Harness();
|
|
harness.Presentation.DisposeFailuresRemaining = 1;
|
|
|
|
Assert.Throws<InvalidOperationException>(() => harness.Controller.Dispose());
|
|
harness.Controller.Dispose();
|
|
harness.Controller.Dispose();
|
|
|
|
Assert.Equal(2, harness.Presentation.DisposeCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConcretePlacement_CommitsEntityControllerAndOneSpatialReconcile()
|
|
{
|
|
const uint guid = 0x50000001u;
|
|
const uint cell = 0x20210001u;
|
|
var world = new GpuWorldState();
|
|
world.AddLandblock(new LoadedLandblock(
|
|
0x2021FFFFu,
|
|
new DatReaderWriter.DBObjs.LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
var runtime = new LiveEntityRuntime(world, new NullResources());
|
|
runtime.RegisterLiveEntity(Spawn(guid, cell));
|
|
WorldEntity entity = runtime.MaterializeLiveEntity(
|
|
guid,
|
|
cell,
|
|
id => new WorldEntity
|
|
{
|
|
Id = id,
|
|
ServerGuid = guid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
})!;
|
|
var identity = new LocalPlayerIdentityState { ServerGuid = guid };
|
|
var controllerSlot = new LocalPlayerControllerSlot
|
|
{
|
|
Controller = new PlayerMovementController(new PhysicsEngine()),
|
|
};
|
|
controllerSlot.Controller.SetPosition(Vector3.Zero, cell, Vector3.Zero);
|
|
var cameras = new ChaseCameraInputState
|
|
{
|
|
Legacy = new ChaseCamera(),
|
|
Retail = new RetailChaseCamera(),
|
|
};
|
|
var origin = new LiveWorldOriginState();
|
|
origin.SetPlaceholder(0x20, 0x21);
|
|
var spatial = new FakeSpatialReconcile(() => new PlacementSnapshot(
|
|
entity.Position,
|
|
entity.ParentCellId ?? 0u,
|
|
entity.Rotation,
|
|
controllerSlot.Controller.Position,
|
|
controllerSlot.Controller.CellId,
|
|
controllerSlot.Controller.BodyOrientation));
|
|
var placement = new LocalPlayerTeleportPlacement(
|
|
new PhysicsEngine(),
|
|
runtime,
|
|
identity,
|
|
controllerSlot,
|
|
new LocalPlayerPhysicsHostSlot(),
|
|
cameras,
|
|
origin,
|
|
spatial);
|
|
var position = new Vector3(12f, 24f, 6f);
|
|
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
|
|
|
|
placement.Place(position, cell, rotation, forced: false);
|
|
|
|
Assert.Equal(entity.Position, controllerSlot.Controller.Position);
|
|
Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId);
|
|
Assert.Equal(cell & 0xFFFF0000u, entity.ParentCellId & 0xFFFF0000u);
|
|
Assert.Equal(rotation, entity.Rotation);
|
|
Assert.Equal(rotation, controllerSlot.Controller.BodyOrientation);
|
|
Assert.Equal(1, spatial.Count);
|
|
Assert.Equal(entity.Position, spatial.Snapshot.EntityPosition);
|
|
Assert.Equal(entity.ParentCellId, spatial.Snapshot.EntityCell);
|
|
Assert.Equal(entity.Rotation, spatial.Snapshot.EntityRotation);
|
|
Assert.Equal(
|
|
controllerSlot.Controller.Position,
|
|
spatial.Snapshot.ControllerPosition);
|
|
Assert.Equal(
|
|
controllerSlot.Controller.CellId,
|
|
spatial.Snapshot.ControllerCell);
|
|
Assert.Equal(
|
|
controllerSlot.Controller.BodyOrientation,
|
|
spatial.Snapshot.ControllerRotation);
|
|
}
|
|
|
|
private static int Index(IReadOnlyList<string> events, string prefix)
|
|
{
|
|
for (int i = 0; i < events.Count; i++)
|
|
{
|
|
if (events[i].StartsWith(prefix, StringComparison.Ordinal))
|
|
return i;
|
|
}
|
|
|
|
return int.MaxValue;
|
|
}
|
|
|
|
private static WorldSession.EntityPositionUpdate Position(
|
|
uint cellId,
|
|
ushort teleportSequence,
|
|
float x,
|
|
float y,
|
|
float z) => new(
|
|
0x50000001u,
|
|
new CreateObject.ServerPosition(
|
|
cellId,
|
|
x,
|
|
y,
|
|
z,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f),
|
|
null,
|
|
null,
|
|
true,
|
|
1,
|
|
1,
|
|
teleportSequence,
|
|
1);
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new(
|
|
Guid: guid,
|
|
Position: new CreateObject.ServerPosition(
|
|
cell,
|
|
0f,
|
|
0f,
|
|
0f,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f),
|
|
SetupTableId: 0x02000001u,
|
|
AnimPartChanges: [],
|
|
TextureChanges: [],
|
|
SubPalettes: [],
|
|
BasePaletteId: null,
|
|
ObjScale: null,
|
|
Name: "player",
|
|
ItemType: null,
|
|
MotionState: null,
|
|
MotionTableId: null);
|
|
|
|
private sealed class NullResources : ILiveEntityResourceLifecycle
|
|
{
|
|
public void Register(WorldEntity entity) { }
|
|
public void Unregister(WorldEntity entity) { }
|
|
}
|
|
|
|
private sealed class FakeSpatialReconcile : ILiveSpatialReconcilePhase
|
|
{
|
|
private readonly Func<PlacementSnapshot> _capture;
|
|
|
|
public FakeSpatialReconcile(Func<PlacementSnapshot> capture) =>
|
|
_capture = capture;
|
|
|
|
public int Count;
|
|
public PlacementSnapshot Snapshot;
|
|
|
|
public void Reconcile()
|
|
{
|
|
Snapshot = _capture();
|
|
Count++;
|
|
}
|
|
}
|
|
|
|
private readonly record struct PlacementSnapshot(
|
|
Vector3 EntityPosition,
|
|
uint EntityCell,
|
|
Quaternion EntityRotation,
|
|
Vector3 ControllerPosition,
|
|
uint ControllerCell,
|
|
Quaternion ControllerRotation);
|
|
|
|
private sealed class Harness
|
|
{
|
|
public readonly FakeAuthority Authority = new();
|
|
public readonly FakeInput Input = new();
|
|
public readonly FakeMode Mode;
|
|
public readonly FakeStreaming Streaming;
|
|
public readonly FakePlacement Placement;
|
|
public readonly FakeSession Session;
|
|
public readonly FakePresentation Presentation;
|
|
public readonly WorldRevealCoordinator Reveal;
|
|
public readonly LocalPlayerTeleportController Controller;
|
|
|
|
public Harness(
|
|
int centerX = 0x20,
|
|
int centerY = 0x21,
|
|
bool worldReady = true,
|
|
List<string>? order = null)
|
|
{
|
|
order ??= new List<string>();
|
|
Mode = new FakeMode(order);
|
|
Streaming = new FakeStreaming(centerX, centerY, order);
|
|
Placement = new FakePlacement(order);
|
|
Session = new FakeSession(order);
|
|
Presentation = new FakePresentation(order);
|
|
Reveal = new WorldRevealCoordinator(
|
|
isRenderNeighborhoodReady: (_, _) => worldReady,
|
|
isSpawnCellReady: _ => worldReady,
|
|
isTerrainNeighborhoodReady: (_, _) => worldReady,
|
|
areCompositeTexturesReady: () => worldReady,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
log: order.Add);
|
|
Controller = new LocalPlayerTeleportController(
|
|
Authority,
|
|
Input,
|
|
Mode,
|
|
Streaming,
|
|
Reveal,
|
|
Placement,
|
|
Session,
|
|
Presentation);
|
|
}
|
|
}
|
|
|
|
private sealed class FakeAuthority : ILocalPlayerTeleportAuthority
|
|
{
|
|
public bool IsFresh = true;
|
|
public bool IsFreshStart(ushort sequence) => IsFresh;
|
|
}
|
|
|
|
private sealed class FakeInput : ILocalPlayerTeleportInputLifetime
|
|
{
|
|
public int EndCount;
|
|
public void EndMouseLook() => EndCount++;
|
|
}
|
|
|
|
private sealed class FakeMode : ILocalPlayerTeleportModeOperations
|
|
{
|
|
private readonly List<string> _order;
|
|
|
|
public FakeMode(List<string> order)
|
|
{
|
|
_order = order;
|
|
Controller = new PlayerMovementController(new PhysicsEngine());
|
|
Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero);
|
|
}
|
|
|
|
public PlayerMovementController? Controller { get; set; }
|
|
public Matrix4x4 Projection => Matrix4x4.Identity;
|
|
public int EnterPortalCount;
|
|
public Func<PlayerMovementController?>? RebuildOnEnter;
|
|
|
|
public bool TryEnterPortalSpace()
|
|
{
|
|
EnterPortalCount++;
|
|
Controller ??= RebuildOnEnter?.Invoke();
|
|
if (Controller is null)
|
|
return false;
|
|
Controller.State = PlayerState.PortalSpace;
|
|
return true;
|
|
}
|
|
|
|
public void EnterWorld()
|
|
{
|
|
_order.Add("enter-world");
|
|
if (Controller is { } controller)
|
|
controller.State = PlayerState.InWorld;
|
|
}
|
|
}
|
|
|
|
private sealed class FakeStreaming : ILocalPlayerTeleportStreamingOperations
|
|
{
|
|
private readonly List<string> _order;
|
|
|
|
public FakeStreaming(int centerX, int centerY, List<string> order)
|
|
{
|
|
CenterX = centerX;
|
|
CenterY = centerY;
|
|
_order = order;
|
|
}
|
|
|
|
public int CenterX { get; }
|
|
public int CenterY { get; }
|
|
public bool IsRecenterPending => RecenterPending;
|
|
public bool RecenterPending;
|
|
public bool ResetResult = true;
|
|
public bool LastResetWasSessionEnding;
|
|
public int PriorityClearCount;
|
|
public readonly List<(int X, int Y, bool Sealed)> Recenters = new();
|
|
public readonly List<(uint Landblock, int Radius)> PriorityWrites = new();
|
|
|
|
public bool BeginRecenter(int x, int y, bool isSealedDungeon)
|
|
{
|
|
Recenters.Add((x, y, isSealedDungeon));
|
|
return !RecenterPending;
|
|
}
|
|
|
|
public bool ResetRecenter(bool sessionEnding)
|
|
{
|
|
LastResetWasSessionEnding = sessionEnding;
|
|
return ResetResult;
|
|
}
|
|
|
|
public bool IsSealedDungeon(uint cellId) =>
|
|
(cellId & 0xFFFFu) >= 0x0100u;
|
|
|
|
public void SetPriority(uint landblockId, int radius) =>
|
|
PriorityWrites.Add((landblockId, radius));
|
|
|
|
public void ClearPriority()
|
|
{
|
|
PriorityClearCount++;
|
|
_order.Add("priority-clear");
|
|
}
|
|
}
|
|
|
|
private sealed class FakePlacement : ILocalPlayerTeleportPlacement
|
|
{
|
|
private readonly List<string> _order;
|
|
|
|
public FakePlacement(List<string> order) => _order = order;
|
|
|
|
public Vector3 Position;
|
|
public uint CellId;
|
|
public Quaternion Rotation;
|
|
public bool Forced;
|
|
public Action? OnPlace;
|
|
|
|
public void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced)
|
|
{
|
|
_order.Add("placement");
|
|
Position = position;
|
|
CellId = cellId;
|
|
Rotation = rotation;
|
|
Forced = forced;
|
|
OnPlace?.Invoke();
|
|
}
|
|
}
|
|
|
|
private sealed class FakeSession : ILocalPlayerTeleportSession
|
|
{
|
|
private readonly List<string> _order;
|
|
|
|
public FakeSession(List<string> order) => _order = order;
|
|
|
|
public int LoginCompleteCount;
|
|
public Action? OnSend;
|
|
|
|
public void SendLoginComplete()
|
|
{
|
|
_order.Add("login-complete");
|
|
LoginCompleteCount++;
|
|
OnSend?.Invoke();
|
|
}
|
|
}
|
|
|
|
private sealed class FakePresentation : ILocalPlayerTeleportPresentation
|
|
{
|
|
private readonly List<string> _order;
|
|
private readonly Queue<IReadOnlyList<TeleportAnimEvent>> _events = new();
|
|
|
|
public FakePresentation(List<string> order) => _order = order;
|
|
|
|
public bool IsPortalViewportVisible { get; private set; }
|
|
public int CurrentTunnelFrame => 72;
|
|
public Matrix4x4? BeginProjection;
|
|
public bool EmitPlaceWhenReady;
|
|
public readonly List<bool> WorldReadyValues = new();
|
|
public int DisposeFailuresRemaining;
|
|
public int DisposeCount;
|
|
|
|
public void Enqueue(params TeleportAnimEvent[] events) => _events.Enqueue(events);
|
|
|
|
public void Begin(Matrix4x4 projection)
|
|
{
|
|
BeginProjection = projection;
|
|
_order.Add("presentation-begin");
|
|
}
|
|
|
|
public (TeleportAnimSnapshot Snapshot, IReadOnlyList<TeleportAnimEvent> Events)
|
|
Tick(float deltaSeconds, bool worldReady)
|
|
{
|
|
WorldReadyValues.Add(worldReady);
|
|
if (_events.Count > 0)
|
|
return (default, _events.Dequeue());
|
|
if (EmitPlaceWhenReady && worldReady)
|
|
{
|
|
EmitPlaceWhenReady = false;
|
|
return (default, new[] { TeleportAnimEvent.Place });
|
|
}
|
|
|
|
return (default, Array.Empty<TeleportAnimEvent>());
|
|
}
|
|
|
|
public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick");
|
|
public void EnterTunnel() => IsPortalViewportVisible = true;
|
|
public void ExitTunnel() => IsPortalViewportVisible = false;
|
|
|
|
public void Reset()
|
|
{
|
|
_order.Add("presentation-reset");
|
|
IsPortalViewportVisible = false;
|
|
BeginProjection = null;
|
|
_events.Clear();
|
|
}
|
|
|
|
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) => projection;
|
|
public ICamera ApplyViewPlane(ICamera camera) => camera;
|
|
public void DrawPortalViewport(int width, int height, Matrix4x4 projection) { }
|
|
public void Dispose()
|
|
{
|
|
DisposeCount++;
|
|
if (DisposeFailuresRemaining-- > 0)
|
|
throw new InvalidOperationException("injected disposal failure");
|
|
}
|
|
}
|
|
}
|