feat(phys L.2g slice 1): ShadowObjectRegistry.UpdatePhysicsState

New mutator that overwrites cached PhysicsState bits on every shadow copy
of the named entity. The existing CollisionExemption.ShouldSkip(...) check
(acclient_2013_pseudo_c.txt:276782) reads the same cached field, so a
post-spawn ETHEREAL flip is now honored on the next resolver tick without
any resolver-path change.

Retail anchor: CPhysicsObj::set_state at acclient_2013_pseudo_c.txt:283044.
Slice 1 scopes to the bare state-write — retail's cosmetic side-effect
handlers (0x800 lighting, 0x20 nodraw, 0x4000 hidden) don't fire for the
ETHEREAL bit and stay deferred.

Three TDD tests cover: ETHEREAL flip from 0->0x4; unregistered-entity
no-op; entity spanning multiple cells gets all copies updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-12 22:22:32 +02:00
parent 2459f287e4
commit d53891557d
2 changed files with 90 additions and 0 deletions

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
@ -252,4 +253,52 @@ public class ShadowObjectRegistryTests
Assert.Equal(0u, entry.State);
Assert.Equal(EntityCollisionFlags.None, entry.Flags);
}
// -----------------------------------------------------------------------
// UpdatePhysicsState — L.2g slice 1 (doors flip ETHEREAL post-spawn)
// -----------------------------------------------------------------------
[Fact]
public void UpdatePhysicsState_FlipsEthereal_NextLookupSeesNewBits()
{
var reg = new ShadowObjectRegistry();
const uint doorId = 0x000F4244u;
reg.Register(doorId, 0x020019FFu, new Vector3(12f, 12f, 50f),
Quaternion.Identity, 1f, OffX, OffY, LbId,
state: 0u, flags: EntityCollisionFlags.None);
var before = reg.AllEntriesForDebug().Single(e => e.EntityId == doorId);
Assert.Equal(0u, before.State);
reg.UpdatePhysicsState(doorId, 0x00000004u);
var after = reg.AllEntriesForDebug().Single(e => e.EntityId == doorId);
Assert.Equal(0x00000004u, after.State);
}
[Fact]
public void UpdatePhysicsState_UnregisteredEntity_IsNoOp()
{
var reg = new ShadowObjectRegistry();
reg.UpdatePhysicsState(0xDEADBEEFu, 0x00000004u);
Assert.Equal(0, reg.TotalRegistered);
}
[Fact]
public void UpdatePhysicsState_EntitySpanningMultipleCells_AllCellsUpdated()
{
var reg = new ShadowObjectRegistry();
reg.Register(99u, 0x01000099u, new Vector3(24f, 12f, 50f),
Quaternion.Identity, 2f, OffX, OffY, LbId,
state: 0u);
reg.UpdatePhysicsState(99u, 0x00000004u);
uint cellA = LbId | 1u;
uint cellB = LbId | (1u*8 + 0 + 1);
var inA = reg.GetObjectsInCell(cellA).Single(e => e.EntityId == 99u);
var inB = reg.GetObjectsInCell(cellB).Single(e => e.EntityId == 99u);
Assert.Equal(0x00000004u, inA.State);
Assert.Equal(0x00000004u, inB.State);
}
}