refactor(physics): hoist the live-entity collision builder to Runtime (#330 groundwork)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
LiveEntityCollisionBuilder and LiveEntityDefaultPoseResolver move from AcDream.App.Physics to AcDream.Runtime.Physics with no behaviour change — diff-verified byte-identical shape math by both review lenses. The Build signature's App-record parameter is replaced by presentation-free primitives with identical guard semantics, INCLUDING the FinalPhysicsState read the contract had missed and the implementer surfaced rather than dropped. Visibility stays internal: Runtime's existing InternalsVisibleTo grants already cover every consumer, so the implementation's public widening is reverted per the architecture review's finding 11. The registration WIRING is deliberately WITHHELD. Both Opus lenses failed it, converging: a shadow registered at spawn freezes there (RuntimeRemotePhysicsUpdater is Runtime-homed but App-driven — nothing headless ticks it), so a walking NPC becomes a phantom obstacle at its spawn point while the real NPC still passes through the bot; three of five shadow-lifetime edges leaked (pickup leaves a permanent invisible collider, supersession orphans a duplicate, generation reset never unregisters and the K-ledger convergence oracle only checks retained shadows AFTER disposal clears them); and headless cannot resolve BSP collision assets at all, so doors and chests would still be walk-through. The frozen-shadow root was the SESSION LEAD's contract error (fact 3), not the implementer's. #330 stays OPEN, rewritten as the seven-point scope map the reviews produced — the honest overnight deliverable is that map, not a half-mechanism carrying new divergences. Suite 11,235 passed / 4 skipped / 0 failed (the withheld seam's two tests account for the delta from the implementation run's 11,237). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5629e2cb12
commit
55b07f6a62
12 changed files with 255 additions and 35 deletions
|
|
@ -803,7 +803,62 @@ If the one-frame deletion is ever judged unacceptable, the lever is the
|
|||
|
||||
## #330 — The headless host registers no live-entity collision at all: a bot walks through every NPC and every server-spawned object
|
||||
|
||||
**Status:** OPEN
|
||||
**Status:** OPEN — **SCOPE MAPPED 2026-08-07 by a dual Opus review of a
|
||||
withheld implementation; the builder hoist landed, the wiring did not.**
|
||||
|
||||
An overnight implementation attempt wired spawn-time registration into the
|
||||
no-window route. Both review lenses failed it, converging (two reviewers
|
||||
converging = near-proof, per the project's own rule), and the findings ARE
|
||||
the issue's true scope — recorded here so the next attempt starts from the
|
||||
map instead of rediscovering it:
|
||||
|
||||
1. **A headless remote-motion tick does not exist.** `RuntimeRemotePhysicsUpdater`
|
||||
is Runtime-HOMED but App-DRIVEN (constructed only at
|
||||
`RemotePhysicsUpdater.cs:46`, ticked only from
|
||||
`LiveEntityAnimationScheduler.cs:351`), and `GetOrCreateRemoteMotion` has
|
||||
one production caller, in App. A shadow registered at spawn therefore
|
||||
FREEZES at the unsettled wire pose: a walking NPC becomes a phantom
|
||||
obstacle at its spawn point while the real NPC still passes through the
|
||||
bot — strictly worse than the honest gap. This is the load-bearing
|
||||
prerequisite.
|
||||
2. **Live collision-asset publication does not exist headless-side.** The
|
||||
headless `PhysicsDataCache` holds only the per-landblock static closure;
|
||||
there is no `IPreparedCollisionSource` on-demand pull
|
||||
(graphical: `LiveCollisionAssetPublisher`). Without it,
|
||||
`_physicsBspBounds` is null for live-entity parts, BSP dispatch never
|
||||
fires, and doors/chests/statues/portals get the wrong shape or none.
|
||||
3. **Degrade resolution:** the graphical route resolves collision part ids
|
||||
through `GfxObjDegradeResolver` slot-0 walking; raw `setup.Parts` is the
|
||||
wrong id for every humanoid part.
|
||||
4. **Shadow lifetime hangs off FIVE edges, not two:** wire delete, pickup
|
||||
(`TryApplyPickup` leaves a permanent invisible collider from a looted
|
||||
item), same-guid generation supersession (`RetireCanonicalOnly` path has
|
||||
no unregister → duplicate phantom), GENERATION RESET
|
||||
(`HeadlessGenerationResetHost.RetireEntityProjection` is an empty no-op
|
||||
and `ResetSessionPhysics` does not clear `ShadowObjects` — registrations
|
||||
leak across reconnects), and hidden/withdrawn suspension.
|
||||
5. **The K-ledger cannot see any of this:** `RuntimePhysicsOwnershipSnapshot.IsConverged`
|
||||
checks retained shadow count only AFTER disposal, and disposal clears the
|
||||
registry. Extending the convergence oracle to pre-disposal retained
|
||||
counts is part of this issue's test work, or every leak above ships green.
|
||||
6. **Ordering + retry:** registration must follow `ProjectSpawn` (a throw
|
||||
after `ApplyAcceptedSpawn` leaves a committed-but-unprojected entity),
|
||||
and a spawn arriving before the world frame publishes needs a retry pump
|
||||
— the withheld code silently dropped it for the incarnation's lifetime.
|
||||
7. **The appearance route EXISTS** (`OnAppearanceUpdated`) and must rebuild
|
||||
collision, as the graphical binding does.
|
||||
|
||||
**What DID land 2026-08-07:** the builder hoist —
|
||||
`LiveEntityCollisionBuilder` + `LiveEntityDefaultPoseResolver` moved to
|
||||
`AcDream.Runtime.Physics` (internal + existing IVT), `Build(...)`'s
|
||||
App-record parameter replaced by presentation-free primitives including the
|
||||
`FinalPhysicsState` the contract had missed. Both reviewers passed the hoist
|
||||
explicitly; the graphical host is diff-verified unchanged. The wiring
|
||||
attempt itself is preserved in the review transcripts, not in the tree.
|
||||
|
||||
**Original entry below.**
|
||||
|
||||
**Status (original):** OPEN
|
||||
**Severity:** HIGH for headless gameplay fidelity; zero impact on the graphical client.
|
||||
**Filed:** 2026-08-06, from the AP-22 deletion's blast-radius survey (§5 of
|
||||
[`docs/research/2026-08-06-ap22-contract.md`](research/2026-08-06-ap22-contract.md)).
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ using AcDream.Core.Spells;
|
|||
using AcDream.Core.Vfx;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter;
|
||||
using Silk.NET.Input;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AcDream.Core.Physics;
|
|||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -834,7 +835,10 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
setup,
|
||||
collisionPartGfxObjIds,
|
||||
spawn,
|
||||
expectedRecord,
|
||||
expectedRecord.ServerGuid,
|
||||
expectedRecord.Generation,
|
||||
expectedRecord.WorldEntity!,
|
||||
expectedRecord.FinalPhysicsState,
|
||||
worldOrigin) is { } collision)
|
||||
{
|
||||
LiveEntityCollisionBuilder.Register(_shadows, collision);
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ public sealed class GameWindow :
|
|||
private readonly AcDream.App.Rendering.Wb.EntityClassificationCache _classificationCache = new();
|
||||
|
||||
private AcDream.Core.Physics.IAnimationLoader? _animLoader;
|
||||
private AcDream.App.Physics.LiveEntityCollisionBuilder? _liveEntityCollisionBuilder;
|
||||
private AcDream.Runtime.Physics.LiveEntityCollisionBuilder? _liveEntityCollisionBuilder;
|
||||
|
||||
// Phase E.1: central fan-out for animation hooks. Audio (E.2),
|
||||
// particles (E.3), combat (E.4), and renderer state mutators all
|
||||
|
|
@ -818,7 +818,7 @@ public sealed class GameWindow :
|
|||
PublishCompositionOwner(ref _animLoader, value, "animation loader");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishLiveEntityCollisionBuilder(
|
||||
AcDream.App.Physics.LiveEntityCollisionBuilder value) =>
|
||||
AcDream.Runtime.Physics.LiveEntityCollisionBuilder value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _liveEntityCollisionBuilder,
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
|
@ -95,7 +95,10 @@ internal static class LiveEntityAppearanceBinding
|
|||
setup,
|
||||
effectivePartGfxObjIds,
|
||||
spawn,
|
||||
record,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
worldOrigin,
|
||||
retainEmptyPayload: true));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -7,7 +6,7 @@ using AcDream.Core.World;
|
|||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
namespace AcDream.Runtime.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable input to the canonical shadow registry for one live object.
|
||||
|
|
@ -45,6 +44,23 @@ internal sealed record LiveEntityCollisionRegistration(
|
|||
/// set — <c>Setup.Radius</c>/<c>Height</c> serve attack cones,
|
||||
/// <c>cylinder_distance</c>, and MoveTo, never collision geometry.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// #330 hoist (2026-08-07): moved from <c>AcDream.App.Physics</c> to
|
||||
/// <c>AcDream.Runtime.Physics</c> so a no-window host can build the same
|
||||
/// collision shapes a graphical host does
|
||||
/// (<c>src/AcDream.Headless/Hosting/HeadlessLiveEntityCollisionRegistrar.cs</c>).
|
||||
/// <see cref="Build"/>'s identity guard used to take the App-only
|
||||
/// <c>LiveEntityRecord exactRecord</c> parameter; it now takes the four
|
||||
/// presentation-free primitives that record exposed
|
||||
/// (<paramref name="Build"/>'s <c>expectedServerGuid</c>/
|
||||
/// <c>expectedGeneration</c>/<c>expectedEntity</c>/
|
||||
/// <c>expectedFinalPhysicsState</c> below) with IDENTICAL guard and
|
||||
/// registration-state semantics — no behaviour change. Both graphical call
|
||||
/// sites (<c>DatLiveEntityProjectionMaterializer</c>,
|
||||
/// <c>LiveEntityAppearanceBinding.PrepareCollision</c>) pass
|
||||
/// <c>record.ServerGuid, record.Generation, record.WorldEntity!,
|
||||
/// record.FinalPhysicsState</c> for these four parameters.
|
||||
/// </remarks>
|
||||
internal sealed class LiveEntityCollisionBuilder
|
||||
{
|
||||
private readonly Func<uint, ShadowPartGeometry?> _physicsBspBounds;
|
||||
|
|
@ -110,24 +126,47 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <param name="expectedServerGuid">Identity guard term 1: must equal
|
||||
/// <paramref name="spawn"/>'s wire guid. Presentation-free replacement for
|
||||
/// the App-only <c>LiveEntityRecord.ServerGuid</c> this parameter used to
|
||||
/// read (#330 hoist).</param>
|
||||
/// <param name="expectedGeneration">Identity guard term 2: must equal
|
||||
/// <paramref name="spawn"/>'s wire instance sequence. Replaces
|
||||
/// <c>LiveEntityRecord.Generation</c> (<see langword="ushort"/>) —
|
||||
/// widened to <see langword="ulong"/> so a Runtime canonical caller can
|
||||
/// pass its own generation counter without a narrowing cast; the
|
||||
/// comparison below is exact either way.</param>
|
||||
/// <param name="expectedEntity">Identity guard term 3: must be reference-
|
||||
/// equal to <paramref name="entity"/>. Replaces
|
||||
/// <c>LiveEntityRecord.WorldEntity</c>.</param>
|
||||
/// <param name="expectedFinalPhysicsState">Replaces
|
||||
/// <c>LiveEntityRecord.FinalPhysicsState</c>, which the original
|
||||
/// <c>exactRecord</c> parameter also supplied for the emitted
|
||||
/// registration's <see cref="LiveEntityCollisionRegistration.State"/> —
|
||||
/// this is NOT part of the identity guard, only of the registration
|
||||
/// payload, but it was only reachable through the same record and so
|
||||
/// travels alongside the guard's three primitives.</param>
|
||||
public LiveEntityCollisionRegistration? Build(
|
||||
WorldEntity entity,
|
||||
Setup setup,
|
||||
IReadOnlyList<uint> effectivePartGfxObjIds,
|
||||
WorldSession.EntitySpawn spawn,
|
||||
LiveEntityRecord exactRecord,
|
||||
uint expectedServerGuid,
|
||||
ulong expectedGeneration,
|
||||
WorldEntity expectedEntity,
|
||||
PhysicsStateFlags expectedFinalPhysicsState,
|
||||
Vector3 worldOrigin,
|
||||
bool retainEmptyPayload = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
ArgumentNullException.ThrowIfNull(effectivePartGfxObjIds);
|
||||
ArgumentNullException.ThrowIfNull(exactRecord);
|
||||
ArgumentNullException.ThrowIfNull(expectedEntity);
|
||||
if (spawn.Position is not { } position)
|
||||
return null;
|
||||
if (spawn.Guid != exactRecord.ServerGuid
|
||||
|| spawn.InstanceSequence != exactRecord.Generation
|
||||
|| !ReferenceEquals(exactRecord.WorldEntity, entity))
|
||||
if (spawn.Guid != expectedServerGuid
|
||||
|| spawn.InstanceSequence != expectedGeneration
|
||||
|| !ReferenceEquals(expectedEntity, entity))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Live collision construction requires the exact materialized record.");
|
||||
|
|
@ -164,7 +203,7 @@ internal sealed class LiveEntityCollisionBuilder
|
|||
entity.Position,
|
||||
entity.Rotation,
|
||||
shapes,
|
||||
(uint)exactRecord.FinalPhysicsState,
|
||||
(uint)expectedFinalPhysicsState,
|
||||
flags,
|
||||
worldOrigin.X,
|
||||
worldOrigin.Y,
|
||||
|
|
@ -3,7 +3,7 @@ using AcDream.Core.Physics.Motion;
|
|||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
namespace AcDream.Runtime.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the motion table's authored default-state part pose used by a
|
||||
|
|
@ -13,6 +13,12 @@ namespace AcDream.App.Physics;
|
|||
/// The retained default-pose snapshot approximation is tracked as AP-84 in
|
||||
/// the retail divergence register until live per-frame collision poses land.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// #330 hoist (2026-08-07): moved from <c>AcDream.App.Physics</c> to
|
||||
/// <c>AcDream.Runtime.Physics</c> so a no-window host can build the same
|
||||
/// collision shapes a graphical host does. No behaviour change - this class
|
||||
/// was already presentation-free (DAT/physics-only dependencies).
|
||||
/// </remarks>
|
||||
internal sealed class LiveEntityDefaultPoseResolver
|
||||
{
|
||||
private readonly Func<uint, MotionTable?> _loadMotionTable;
|
||||
|
|
@ -16,6 +16,7 @@ using AcDream.Core.Rendering;
|
|||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenAL;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
|
|
@ -55,7 +55,16 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
var builder = Builder();
|
||||
|
||||
LiveEntityCollisionRegistration registration = Assert.IsType<LiveEntityCollisionRegistration>(
|
||||
builder.Build(entity, setup, Array.Empty<uint>(), spawn, record, new Vector3(192f, -192f, 0f)));
|
||||
builder.Build(
|
||||
entity,
|
||||
setup,
|
||||
Array.Empty<uint>(),
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
new Vector3(192f, -192f, 0f)));
|
||||
|
||||
ShadowShape shape = Assert.Single(registration.Shapes);
|
||||
Assert.Equal(ShadowCollisionType.Cylinder, shape.CollisionType);
|
||||
|
|
@ -94,7 +103,10 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
setup,
|
||||
Array.Empty<uint>(),
|
||||
spawn,
|
||||
record,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
new Vector3(192f, -192f, 0f)));
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +137,16 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
PoseResolver());
|
||||
|
||||
LiveEntityCollisionRegistration registration = Assert.IsType<LiveEntityCollisionRegistration>(
|
||||
builder.Build(entity, setup, [part], spawn, record, Vector3.Zero));
|
||||
builder.Build(
|
||||
entity,
|
||||
setup,
|
||||
[part],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
|
||||
ShadowShape shape = Assert.Single(registration.Shapes);
|
||||
Assert.Equal(ShadowCollisionType.BSP, shape.CollisionType);
|
||||
|
|
@ -168,7 +189,15 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
|
||||
LiveEntityCollisionRegistration registration =
|
||||
Assert.IsType<LiveEntityCollisionRegistration>(builder.Build(
|
||||
entity, setup, [part], spawn, record, Vector3.Zero));
|
||||
entity,
|
||||
setup,
|
||||
[part],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
|
||||
ShadowShape shape = Assert.Single(registration.Shapes);
|
||||
Assert.Equal(ShadowCollisionType.BSP, shape.CollisionType);
|
||||
|
|
@ -197,7 +226,10 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
setup,
|
||||
[replacement],
|
||||
spawn,
|
||||
record,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +254,10 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
setup,
|
||||
[replacement],
|
||||
spawn,
|
||||
record,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
|
||||
ShadowShape shape = Assert.Single(registration.Shapes);
|
||||
|
|
@ -246,12 +281,28 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
PoseResolver());
|
||||
LiveEntityCollisionRegistration initial =
|
||||
Assert.IsType<LiveEntityCollisionRegistration>(builder.Build(
|
||||
entity, setup, [basePart], spawn, record, Vector3.Zero));
|
||||
entity,
|
||||
setup,
|
||||
[basePart],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
var registry = new ShadowObjectRegistry();
|
||||
LiveEntityCollisionBuilder.Register(registry, initial);
|
||||
|
||||
LiveEntityCollisionRegistration? changed = builder.Build(
|
||||
entity, setup, [replacement], spawn, record, Vector3.Zero,
|
||||
entity,
|
||||
setup,
|
||||
[replacement],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero,
|
||||
retainEmptyPayload: true);
|
||||
LiveEntityCollisionBuilder.ReconcileAppearance(
|
||||
registry, entity.Id, changed, suspendIfNew: false);
|
||||
|
|
@ -278,13 +329,29 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
PoseResolver());
|
||||
LiveEntityCollisionRegistration initial =
|
||||
Assert.IsType<LiveEntityCollisionRegistration>(builder.Build(
|
||||
entity, setup, [basePart], spawn, record, Vector3.Zero));
|
||||
entity,
|
||||
setup,
|
||||
[basePart],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
var registry = new ShadowObjectRegistry();
|
||||
LiveEntityCollisionBuilder.Register(registry, initial);
|
||||
Assert.True(registry.Suspend(entity.Id));
|
||||
LiveEntityCollisionRegistration changed =
|
||||
Assert.IsType<LiveEntityCollisionRegistration>(builder.Build(
|
||||
entity, setup, [replacement], spawn, record, Vector3.Zero));
|
||||
entity,
|
||||
setup,
|
||||
[replacement],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
|
||||
LiveEntityCollisionBuilder.ReconcileAppearance(
|
||||
registry, entity.Id, changed, suspendIfNew: true);
|
||||
|
|
@ -301,7 +368,10 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
setup,
|
||||
[0x0100AC13u],
|
||||
spawn,
|
||||
record,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero,
|
||||
retainEmptyPayload: true));
|
||||
LiveEntityCollisionBuilder.ReconcileAppearance(
|
||||
|
|
@ -342,12 +412,28 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
PoseResolver());
|
||||
LiveEntityCollisionRegistration initial =
|
||||
Assert.IsType<LiveEntityCollisionRegistration>(builder.Build(
|
||||
entity, setup, [basePart], spawn, record, Vector3.Zero));
|
||||
entity,
|
||||
setup,
|
||||
[basePart],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
var registry = new ShadowObjectRegistry();
|
||||
LiveEntityCollisionBuilder.Register(registry, initial);
|
||||
LiveEntityCollisionRegistration changed =
|
||||
Assert.IsType<LiveEntityCollisionRegistration>(builder.Build(
|
||||
entity, setup, [replacement], spawn, record, Vector3.Zero))
|
||||
entity,
|
||||
setup,
|
||||
[replacement],
|
||||
spawn,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero))
|
||||
with
|
||||
{
|
||||
LandblockId = 0u,
|
||||
|
|
@ -376,7 +462,10 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
new Setup(),
|
||||
Array.Empty<uint>(),
|
||||
spawn,
|
||||
record,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
}
|
||||
|
||||
|
|
@ -396,7 +485,10 @@ public sealed class LiveEntityCollisionBuilderTests
|
|||
new Setup { Radius = 1f },
|
||||
Array.Empty<uint>(),
|
||||
spawn,
|
||||
other,
|
||||
other.ServerGuid,
|
||||
other.Generation,
|
||||
other.WorldEntity!,
|
||||
other.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -9,6 +8,7 @@ using AcDream.Core.Physics;
|
|||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Physics;
|
||||
|
|
@ -155,7 +155,16 @@ public sealed class PvpBitfieldSurvivesAppearanceRebuildTests
|
|||
|
||||
// Initial shadow registration, mirroring CreateObject-time behavior.
|
||||
LiveEntityCollisionRegistration initial = Assert.IsType<LiveEntityCollisionRegistration>(
|
||||
builder.Build(entity, setup, [], record.Snapshot, record, Vector3.Zero));
|
||||
builder.Build(
|
||||
entity,
|
||||
setup,
|
||||
[],
|
||||
record.Snapshot,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
LiveEntityCollisionBuilder.Register(registry, initial);
|
||||
ShadowEntry beforeObjDesc = Assert.Single(registry.GetObjectsInCell(Cell));
|
||||
Assert.True(beforeObjDesc.Flags.HasFlag(EntityCollisionFlags.IsPKLite));
|
||||
|
|
@ -184,7 +193,16 @@ public sealed class PvpBitfieldSurvivesAppearanceRebuildTests
|
|||
// LiveEntityAppearanceBinding.PrepareCollision/CommitCollision ->
|
||||
// LiveEntityCollisionBuilder.ReconcileAppearance).
|
||||
LiveEntityCollisionRegistration rebuilt = Assert.IsType<LiveEntityCollisionRegistration>(
|
||||
builder.Build(entity, setup, [], accepted, record, Vector3.Zero));
|
||||
builder.Build(
|
||||
entity,
|
||||
setup,
|
||||
[],
|
||||
accepted,
|
||||
record.ServerGuid,
|
||||
record.Generation,
|
||||
record.WorldEntity!,
|
||||
record.FinalPhysicsState,
|
||||
Vector3.Zero));
|
||||
LiveEntityCollisionBuilder.ReconcileAppearance(
|
||||
registry, entity.Id, rebuilt, suspendIfNew: false);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.App.World;
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Physics;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue