fix(physics): AP-129 review fix - port CanMoveInto/IsAllowedIn, stop failing closed
Campaign P Slice P4 Opus review verdict: FIX-FIRST. RestrictionObjPrevalenceInspectionTests
(commit 3b5e0992) found 103,766 of 729,888 installed EnvCells (1,293 landblocks -
the whole housing estate) carry a baked RestrictionObj. The AP-71 gate's
unconditional fail-closed default (CanMoveInto unmodeled) would have locked
every apartment/cottage/villa interior for every player, including its own
owner - a live regression, not the "inert in dev content" the original
register row assumed.
Ports ACCWeenieObject::CanMoveInto (0x0058da40, pc:407982-408056) and
RestrictionDB::IsAllowedIn (0x005ae8f0, pc:444493-444516) verbatim into
ObjectInfo.CheckEntryRestrictions:
- owner_iid == 0 or == mover's own guid -> admit (open/owner)
- no RestrictionDB (retail _db == 0, i.e. never authored or not yet
received) -> admit
- present RestrictionDB -> IsAllowedIn: open-to-public flag, OR mover
shares the house's allegiance monarch, OR mover's own guid is a
guest-table member
- unresolved restriction object -> fails CLOSED, exactly retail's own
fallback when GetObjectA can't resolve it (pc:704-716)
Wire feed (Core.Net):
- CreateObject.cs: HouseOwner (WeenieHeaderFlag 0x02000000), HouseRestrictions
(0x04000000), and Monarch (0x40) PWD-tail fields were parsed-and-skipped;
now captured. Also fixes the HouseRestrictions PHashTable header
misconception: the wire is ONE packed u32 (low 24 bits = entry count),
not a separate count(u16)+numBuckets(u16) pair - verified against
Chorizite's RestrictionDB.generated.cs. The old skip's byte-count
happened to match for realistic guest-list sizes, but a future
numBuckets value >255 would have corrupted the parse; now correct
regardless.
- GameEvents.cs/GameEventWiring.cs: new House_UpdateRestrictions (0x0248)
parser + wiring - retail's live guest-list refresh, whole-unit replace.
No-ops if the house object hasn't arrived via CreateObject yet.
- ClientObject/WeenieData/ClientObjectTable: HouseOwnerId, MonarchId,
Restrictions (new HouseRestrictionRecord) fields + merge-preserving
Ingest + targeted UpdateHouseRestrictions.
Physics wiring:
- PhysicsEngine gains an Objects (ClientObjectTable?) property, mirroring
the existing DataCache pattern - acdream's GetObjectA equivalent, used
ONLY by the entry-restriction gate.
- RuntimeEntityObjectLifetime wires Physics.Engine.Objects = Objects in
all three constructors, right alongside the table's own construction -
the same canonical table every other subsystem borrows from, never a
second one. This is the production fix: without it the gate still fails
closed on every restricted cell (unresolvable object), so the wiring is
load-bearing, not cosmetic.
Register: AP-129 narrowed (not retired) to the genuine remaining residual -
House_UpdateRestrictions' Sequence byte isn't used for staleness/reordering
rejection (low-probability, self-correcting), and outdoor CLandCell
restriction (a separate DAT structure) remains unported and unaffected by
this fix.
Tests: 15 new/updated in Ap71EntryRestrictionGateTests.cs (resolved-unowned
admits, owner admits, present-list-excluded blocks, present-list-included
admits, open-to-public admits, shared-allegiance-monarch admits, unresolved
blocks via null and via an empty table, plus two new end-to-end
PhysicsEngine.Objects-wired scenarios); 2 new CreateObject parser tests +
2 new GameEventWiring tests for the wire feed.
AcDream.Core.Tests: 4049 passed, 2 skipped, 0 failed.
AcDream.Core.Net.Tests: 761 passed, 0 skipped, 0 failed.
Complete solution suite: 9,961 total, 9,956 passed, 5 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dc0468cc2b
commit
7a0f836af5
15 changed files with 704 additions and 81 deletions
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -125,6 +126,20 @@ public sealed class PhysicsEngine
|
|||
}
|
||||
private PhysicsDataCache? _dataCache;
|
||||
|
||||
/// <summary>
|
||||
/// AP-129 (Campaign P Slice P4 review fix, 2026-07-30): optional live
|
||||
/// weenie-object table, consulted ONLY by
|
||||
/// <see cref="Transition.FindEnvCollisions"/>'s entry-restriction gate to
|
||||
/// resolve a cell's <c>RestrictionObj</c> into its owner/guest-list data
|
||||
/// (retail <c>ACCWeenieObject::CanMoveInto</c>). Mirrors the
|
||||
/// <see cref="DataCache"/> pattern: nullable, settable, defaults null so
|
||||
/// every existing test/one-shot caller is unaffected. An unset table
|
||||
/// makes a restricted cell fail CLOSED — exactly retail's own fallback
|
||||
/// when the restriction weenie can't be resolved — so production MUST
|
||||
/// wire this to the live table for the fix to actually admit anyone.
|
||||
/// </summary>
|
||||
public ClientObjectTable? Objects { get; set; }
|
||||
|
||||
private sealed record LandblockPhysics(
|
||||
TerrainSurface Terrain,
|
||||
IReadOnlyList<CellSurface> Cells,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Items;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
|
@ -169,22 +170,51 @@ public sealed class ObjectInfo
|
|||
/// cannot bypass): retail resolves the live restriction weenie
|
||||
/// (<c>CPhysicsObj::GetObjectA</c>) and asks <c>CanMoveInto</c> (house
|
||||
/// owner IID + guest/ban list, <c>ACCWeenieObject::CanMoveInto</c>
|
||||
/// 0x0058da40). acdream has no restriction-weenie resolution or
|
||||
/// guest-list wire model yet (see the AP-71 register history for the
|
||||
/// narrower unfed-input row this leaves), so it cannot correctly answer
|
||||
/// <c>CanMoveInto</c> — it fails CLOSED (<see
|
||||
/// cref="TransitionState.Collided"/>), exactly matching retail's own
|
||||
/// fallback when the restriction object can't be resolved (pc:704-716
|
||||
/// falls through to <c>COLLIDED_TS</c>).
|
||||
/// 0x0058da40, named-retail pc:407982-408056). AP-129 (Campaign P Slice
|
||||
/// P4 review fix, 2026-07-30) ports this verbatim via
|
||||
/// <paramref name="objects"/> (retail's <c>GetObjectA</c> equivalent —
|
||||
/// the house object is an ordinary <c>CreateObject</c> entity in range):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Unresolved restriction object (<paramref name="objects"/> is
|
||||
/// null, or has no row for <paramref name="cellRestrictionObj"/>) —
|
||||
/// fails CLOSED (<see cref="TransitionState.Collided"/>), exactly
|
||||
/// retail's own fallback when <c>rObj == 0</c> (pc:704-716 falls
|
||||
/// through to <c>COLLIDED_TS</c>).</item>
|
||||
/// <item>Resolved, <c>HouseOwnerId</c> zero or equal to the mover's own
|
||||
/// id — OK (owner always in, or no owner set = open).</item>
|
||||
/// <item>Resolved, no <c>Restrictions</c> (retail <c>_db == 0</c>) — OK
|
||||
/// (an un-received or never-authored guest list is open, same as a
|
||||
/// retail client that hasn't been sent restrictions yet).</item>
|
||||
/// <item>Resolved with a <c>Restrictions</c> list — delegates to
|
||||
/// <see cref="HouseRestrictionRecord.IsAllowedIn"/> (retail
|
||||
/// <c>RestrictionDB::IsAllowedIn</c> 0x005ae8f0): open-to-public,
|
||||
/// OR mover shares the house's allegiance monarch, OR the mover's
|
||||
/// own guid is a guest-table member.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public TransitionState CheckEntryRestrictions(uint cellRestrictionObj)
|
||||
public TransitionState CheckEntryRestrictions(
|
||||
uint cellRestrictionObj,
|
||||
ClientObjectTable? objects)
|
||||
{
|
||||
if (!IsPlayer) return TransitionState.OK; // NPCs/props bypass entirely
|
||||
if (CanBypassMoveRestrictions) return TransitionState.OK;
|
||||
if (cellRestrictionObj == 0) return TransitionState.OK; // ordinary cell — no-op
|
||||
|
||||
return TransitionState.Collided;
|
||||
ClientObject? restrictionObject = objects?.Get(cellRestrictionObj);
|
||||
if (restrictionObject is null) return TransitionState.Collided; // retail: rObj==0 -> COLLIDED
|
||||
|
||||
uint moverId = SelfEntityId;
|
||||
uint houseOwnerId = restrictionObject.HouseOwnerId ?? 0;
|
||||
if (houseOwnerId == 0 || houseOwnerId == moverId) return TransitionState.OK;
|
||||
|
||||
HouseRestrictionRecord? restrictions = restrictionObject.Restrictions;
|
||||
if (restrictions is null) return TransitionState.OK; // retail: _db==0 -> open
|
||||
|
||||
uint moverMonarchId = objects?.Get(moverId)?.MonarchId ?? 0;
|
||||
return restrictions.IsAllowedIn(moverId, moverMonarchId)
|
||||
? TransitionState.OK
|
||||
: TransitionState.Collided;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -3033,8 +3063,12 @@ public sealed class Transition
|
|||
// find_env_collisions (pc:309576) calls check_entry_restrictions
|
||||
// as its FIRST statement — before any HasPhysics/BSP dispatch.
|
||||
// cellPhysics is null-safe: an uncached/BSP-less cell has no
|
||||
// authored restriction data either.
|
||||
var restrictionState = ObjectInfo.CheckEntryRestrictions(cellPhysics?.RestrictionObj ?? 0);
|
||||
// authored restriction data either. AP-129 (review fix): pass
|
||||
// engine.Objects so the gate can resolve CanMoveInto instead of
|
||||
// failing closed on every restricted cell (see PhysicsEngine.Objects).
|
||||
var restrictionState = ObjectInfo.CheckEntryRestrictions(
|
||||
cellPhysics?.RestrictionObj ?? 0,
|
||||
engine.Objects);
|
||||
if (restrictionState != TransitionState.OK)
|
||||
{
|
||||
if ((ObjectInfo.State & ObjectInfoState.Contact) == 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue