acdream/docs/research/2026-08-02-set-description-float-gates.md
Erik 874d94bf34 docs(research): byte-decode set_description's three elided float gates
C3b's blocking retail question, resolved byte-certain from the
PDB-paired v11.4186 binary: CPhysicsObj::set_description applies the
desc's friction only when 0.0 <= friction <= 1.0 (outer JNP-on-parity
gate vs 0.0 double at .rdata 0x00794610; inner <= 1.0 vs 0x3FF0... at
0x007928c0), and applies live translucency + the CPartArray propagation
only when translucency != 0.0f (FCOMP m32 vs 0.0f at 0x007c6a80;
translucencyOriginal is written unconditionally before the gate). Every
FLD/FCOM operand address read from .rdata and every FNSTSW/TEST/Jcc
decoded by hand; ACE PhysicsObj.cs:3557-3568 independently reproduces
all three predicates as the cross-check. Unblocks the C3b remote
body-construction port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 08:21:15 +02:00

9 KiB
Raw Blame History

CPhysicsObj::set_description @ 0x00514F40 — three FPU-elided gates recovered

Verification chain

  1. Binary/PDB pairing: py tools/pdb-extract/check_exe_pdb.py "C:/Users/erikn/Downloads/acclient.exe"=== MATCH: this exe pairs with our acclient.pdb === (GUID 9e847e2f-777c-4bd9-886c-22256bb87f32, linker timestamp 2013-09-06T00:17:56Z). Confirmed before any byte reads.
  2. PE section mapping (hand-parsed via a one-off script, scratchpad/pe_read.py): image base 0x00400000; .text VA=0x00401000 RawPtr=0x00001000; .rdata VA=0x00792000 RawPtr=0x00392000 (holds the FP constants below). VA→file-offset: file_off = raw_ptr + (VA - image_base - section_virt_addr).
  3. Raw bytes of the function (0x00514F400x00515153) were dumped and hand-disassembled instruction-by-instruction, cross-checked line-by-line against docs/research/named-retail/acclient_2013_pseudo_c.txt lines 283130283251 (function body) so every address in the trace lines up with a named pseudo-C statement.
  4. Ghidra MCP: not available this session — no CodeBrowser open on port 8080/8081 (both probes returned empty). Not needed; binary + ACE agreement below is already two independent confirmations.
  5. ACE cross-check: references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs (main checkout, not the af5e worktree — ACE isn't vendored there), set_description, lines 35573568. ACE's C# independently reproduces all three predicates exactly as decoded from the binary below. Binary is the ground truth per project policy; ACE here is 100% consistent with it, so no conflict to adjudicate.

Confidence: byte-certain for all three. Every constant was read directly from .rdata, every comparison/jump opcode was decoded from the raw instruction stream, and the result matches ACE's independent port line-for-line.


Conditional 1 — friction OUTER gate (pseudo-C line 283219, VA 0x0051505a)

Bytes

0051504f: d9 46 68                    FLD  DWORD PTR [ESI+0x68]      ; ST(0) = (double)esi->friction  (PhysicsDesc.friction @ +0x68)
00515052: dc 15 10 46 79 00           FCOM QWORD PTR [0x00794610]    ; compare ST(0) vs constant, no pop (value reused below)
00515058: df e0                       FNSTSW AX
0051505a: f6 c4 05                    TEST AH, 0x05                  ; mask = C0(bit0) | C2(bit2)
0051505d: 7b 15                       JNP  0x00515074                ; jump (skip friction block) iff PF=0

Constant

VA 0x00794610 (.rdata, file offset 0x00394610), 8 bytes: 00 00 00 00 00 00 00 000.0 (double, exact).

Decoding the jump

TEST AH,0x05 ANDs AH with the C0|C2 status bits, then the parity flag (PF) reflects the parity of that AND result. Case table for FCOM esi->friction, 0.0 (ST0=friction):

relation C0 C2 C3 AH&0x05 popcount PF
friction > 0.0 0 0 0 0x00 0 1
friction < 0.0 1 0 0 0x01 1 0
friction == 0.0 0 0 1 0x00 0 1
unordered (NaN) 1 1 1 0x05 2 1

JNP (jump on PF=0) only fires for the strict < case. So the jump (which SKIPS the whole friction reassignment block, landing at the shared cleanup at 0x00515074) is taken only when friction < 0.0; every other case (>= 0.0, and — as an accepted compiler-quirk edge case irrelevant to real game data — unordered/NaN) falls through into the block.

Recovered predicate

// outer gate: proceed to the friction-assignment logic only when friction is non-negative
if (esi->friction >= 0.0f) {
    // ... inner compare (Conditional 2) ...
}

Conditional 2 — friction INNER compare (pseudo-C line 283226, VA 0x0051506a)

Bytes

0051505f: dc 15 c0 28 79 00           FCOM QWORD PTR [0x007928c0]    ; compare ST(0)=friction vs constant, no pop
00515065: df e0                       FNSTSW AX
00515067: f6 c4 41                    TEST AH, 0x41                  ; mask = C0(bit0) | C3(bit6)
0051506a: 74 08                       JZ   0x00515074                ; jump (skip assignment) iff (AH&0x41)==0
0051506c: d9 9f bc 00 00 00           FSTP DWORD PTR [EDI+0xbc]      ; this->friction = friction  (field @ +0xbc), pops ST(0)

Pseudo-C had already fully rendered the C0/C2/C3 synthetic-byte construction for this one (only the final test ah,0x41→bool collapse was marked unimplemented), so the byte read is a confirmation rather than a fresh recovery.

Constant

VA 0x007928c0 (.rdata, file offset 0x003928c0), 8 bytes: 00 00 00 00 00 00 f0 3f1.0 (double, exact; IEEE-754 bit pattern 0x3FF0000000000000).

Decoding the jump

Mask 0x41 = C0(below) | C3(equal). JZ (jump when the TEST result is zero, i.e. neither bit set) skips the assignment when friction is strictly > 1.0. Falls through (assigns this->friction) when friction <= 1.0 (below-or-equal family, exactly as flagged in the task). This is the canonical jbe idiom.

Recovered predicate

// inner compare: only assign if friction also passes the upper bound
if (esi->friction <= 1.0f)
    this->friction = esi->friction;

Combined (conditionals 1+2)

if (esi->friction >= 0.0f && esi->friction <= 1.0f)
    this->friction = esi->friction;

This is byte-for-byte what ACE's port does at PhysicsObj.cs:3557-3558: if (desc.Friction >= 0.0f && desc.Friction <= 1.0f) Friction = desc.Friction;


Conditional 3 — translucency gate (pseudo-C line 283240, VA 0x0051509f)

Bytes

0051508b: d9 44 24 24                 FLD  DWORD PTR [ESP+0x24]      ; ST(0) = (float)translucency (local copy of esi->translucency, field @ esi+0x70)
0051508f: d8 1d 80 6a 7c 00           FCOMP DWORD PTR [0x007c6a80]   ; compare ST(0) vs constant, WITH pop (single precision, reg field=3)
00515095: 8b d1                       MOV  EDX, ECX
00515097: 89 97 b8 00 00 00           MOV  [EDI+0xb8], EDX           ; this->translucencyOriginal = translucency   (unconditional)
0051509d: df e0                       FNSTSW AX
0051509f: f6 c4 44                    TEST AH, 0x44                  ; mask = C2(bit2) | C3(bit6)
005150a2: 7b 15                       JNP  0x005150b9                ; jump (skip live-translucency apply) iff PF=0
005150a4: ...                                                        ; fallthrough: this->translucency = translucency; PartArray propagation

Constant

VA 0x007c6a80 (.rdata, file offset 0x003c6a80), 4 bytes: 00 00 00 000.0f (single-precision float, exact). Note this compare is single-precision (d8/FCOMP m32), unlike the two friction compares above which are double-precision (dc/FCOM m64) — matches the pseudo-C's ((long double)0f) literal notation (the f suffix is BN flagging a float-typed constant) versus ((long double)0.0) for the friction case.

Decoding the jump

Case table for FCOMP translucency, 0.0f (ST0=translucency), mask 0x44 = C2(unordered) | C3(equal):

relation C0 C2 C3 AH&0x44 popcount PF
translucency > 0.0 0 0 0 0x00 0 1
translucency < 0.0 1 0 0 0x00 0 1
translucency == 0.0 0 0 1 0x40 1 0
unordered (NaN) 1 1 1 0x44 2 1

JNP (PF=0) fires only for the exact-equal-to-zero case. So the jump — which skips applying live translucency/PartArray propagation, leaving only the unconditional translucencyOriginal write — is taken only when translucency == 0.0f. Every other case (>0, <0, and unordered/NaN as a compiler-quirk edge case) falls through and applies.

Recovered predicate

// translucencyOriginal is ALWAYS written (this happens before the gate, unconditionally)
this->translucencyOriginal = translucency;

// live translucency + PartArray propagation only when translucency is non-zero
if (translucency != 0.0f)
{
    this->translucency = translucency;
    if (this->part_array != 0)
        CPartArray::SetTranslucencyInternal(this->part_array, translucency);
}

Matches ACE's port at PhysicsObj.cs:3562-3568 exactly:

TranslucencyOriginal = desc.Translucency;
if (desc.Translucency != 0.0f)
{
    Translucency = desc.Translucency;
    if (PartArray != null)
        PartArray.SetTranslucencyInternal(desc.Translucency);
}

Summary table

# Gate Predicate (apply-when) Constant Cert.
1 friction outer friction >= 0.0f 0.0 (double) @ VA 0x00794610 byte-certain
2 friction inner friction <= 1.0f 1.0 (double) @ VA 0x007928c0 byte-certain
3 translucency translucency != 0.0f 0.0f (float) @ VA 0x007c6a80 byte-certain

All three: no unresolved cases. The only caveat on all three is a decompiler/compiler-codegen edge case around NaN (unordered operands fall into the "true"/apply bucket rather than IEEE-strict "always false"), which is a documented quirk of this exact MSVC x87 codegen pattern and not something the retail struct's float fields would ever hit in practice (friction/translucency are authored data, never NaN).