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:
parent
2459f287e4
commit
d53891557d
2 changed files with 90 additions and 0 deletions
|
|
@ -126,6 +126,47 @@ public sealed class ShadowObjectRegistry
|
|||
t.State, t.Flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cached <see cref="ShadowEntry.State"/> bits for an
|
||||
/// already-registered entity. Called by the inbound
|
||||
/// <c>SetState (0xF74B)</c> dispatcher when the server broadcasts a
|
||||
/// post-spawn <c>PhysicsState</c> change — chiefly doors flipping
|
||||
/// <c>ETHEREAL_PS = 0x4</c> on Use, so the
|
||||
/// <see cref="CollisionExemption.ShouldSkip"/> short-circuit can honor
|
||||
/// the new state on the next resolve.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail equivalent: <c>CPhysicsObj::set_state</c> at
|
||||
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:283044</c>
|
||||
/// — direct write `this->state = arg2`. Retail also fires side-effect
|
||||
/// handlers for the 0x800 (lighting), 0x20 (nodraw), 0x4000 (hidden)
|
||||
/// changed bits; ETHEREAL (0x4) doesn't trigger any of them, so slice 1
|
||||
/// scopes to the bare state-write.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Implementation: <see cref="ShadowEntry"/> is a value-type record
|
||||
/// copied into per-cell lists, so we rewrite the copy in each cell the
|
||||
/// entity occupies. Unregistered entities are a no-op (callers don't
|
||||
/// have to gate).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void UpdatePhysicsState(uint entityId, uint newState)
|
||||
{
|
||||
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||
return; // not registered — no-op
|
||||
|
||||
foreach (var cellId in cellIds)
|
||||
{
|
||||
if (!_cells.TryGetValue(cellId, out var list)) continue;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i].EntityId == entityId)
|
||||
list[i] = list[i] with { State = newState };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
||||
public void Deregister(uint entityId)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue