acdream/src/AcDream.Core/Physics/Motion/ConstraintDistance.cs
Erik e0629145ef feat(physics): P5 commit 1 - port ConstraintManager leash distance constants (#167)
Add ConstraintDistance (outdoor/indoor start=10/5, max=50/20), byte-decoded
from the matching retail binary (GetStartConstraintDistance 0x0050ebc0,
GetMaxConstraintDistance 0x0050ec10 - both x87-return functions BN elided).
Deliberately omits the vestigial player-vs-remote branch the disassembly
shows loads identical constants either way. Pins the ACE-inversion (ACE's
start mapping is outdoor 5/indoor 10, the opposite of the binary - the
binary wins). Adds a full-chain conformance test proving an armed,
over-strained leash actually blocks jump_is_allowed (0x47), not just the
bare stub-property regression already covered.

See docs/research/2026-07-30-constraint-leash-constants.md.
2026-07-30 11:54:35 +02:00

55 lines
2.9 KiB
C#

namespace AcDream.Core.Physics.Motion;
/// <summary>
/// Campaign P P5 (#167 / register TS-35) — retail
/// <c>CPhysicsObj::GetStartConstraintDistance</c> (0x0050ebc0) and
/// <c>GetMaxConstraintDistance</c> (0x0050ec10): the leash's start/max band,
/// keyed by whether the object's OWN current cell is outdoor or indoor.
///
/// <para><b>Byte-decoded, not guessed</b> — BN elided both getters' return
/// value as a bare <c>this-&gt;m_position;</c> expression (the classic x87
/// FPU-return decompile artifact). The actual values were recovered by
/// disassembling the matching binary's raw machine code (see
/// <c>docs/research/2026-07-30-constraint-leash-constants.md</c> §1):
/// outdoor start = <b>10.0</b>, indoor start = <b>5.0</b>; outdoor max =
/// <b>50.0</b>, indoor max = <b>20.0</b>. Indoor is decided by the object's
/// full cell id's low 16 bits: <c>&gt;= 0x0100</c> = EnvCell (indoor).</para>
///
/// <para><b>The retail player-vs-remote branch is INTENTIONALLY OMITTED.</b>
/// The disassembly shows both getters branch on <c>this == player_object</c>
/// AND load byte-identical constants either way — the branch is vestigial
/// (both sides return the same four numbers). Porting that dead branch would
/// only add a phantom "is this the player" parameter with zero behavioral
/// effect, so this class keys purely on the object's own cell id.</para>
///
/// <para><b>ACE-inversion pin.</b> ACE's <c>PhysicsObj.GetStartConstraintDistance</c>
/// (<c>PhysicsObj.cs:620</c>) maps outdoor → 5, indoor → 10 — the OPPOSITE of
/// the binary. ACE's max mapping (outdoor 50 / indoor 20) matches. Do not
/// "fix" this class to match ACE's start mapping; the disassembly is the
/// oracle here (feedback_acme_oracle: binary wins over ACE on disagreement).
/// </para>
/// </summary>
public static class ConstraintDistance
{
private const float OutdoorStart = 10.0f;
private const float IndoorStart = 5.0f;
private const float OutdoorMax = 50.0f;
private const float IndoorMax = 20.0f;
/// <summary>Retail <c>cmp eax, 0x100; jae indoor</c> on the object's own
/// <c>m_position.objcell_id &amp; 0xFFFF</c> — low 16 bits ≥ 0x0100 is an
/// EnvCell (indoor); below that is an outdoor landblock cell index.</summary>
public static bool IsIndoorCell(uint objCellId) => (objCellId & 0xFFFFu) >= 0x0100u;
/// <summary>Retail <c>CPhysicsObj::GetStartConstraintDistance</c>
/// (0x0050ebc0) — the near edge of the leash's brake band, keyed by the
/// object's own current cell.</summary>
public static float GetStartConstraintDistance(uint objCellId) =>
IsIndoorCell(objCellId) ? IndoorStart : OutdoorStart;
/// <summary>Retail <c>CPhysicsObj::GetMaxConstraintDistance</c>
/// (0x0050ec10) — the far edge (full clamp), keyed by the object's own
/// current cell.</summary>
public static float GetMaxConstraintDistance(uint objCellId) =>
IsIndoorCell(objCellId) ? IndoorMax : OutdoorMax;
}