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);
|
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>
|
/// <summary>Remove an entity from all cells it was registered in.</summary>
|
||||||
public void Deregister(uint entityId)
|
public void Deregister(uint entityId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -252,4 +253,52 @@ public class ShadowObjectRegistryTests
|
||||||
Assert.Equal(0u, entry.State);
|
Assert.Equal(0u, entry.State);
|
||||||
Assert.Equal(EntityCollisionFlags.None, entry.Flags);
|
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