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>
9 KiB
CPhysicsObj::set_description @ 0x00514F40 — three FPU-elided gates recovered
Verification chain
- 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 ===(GUID9e847e2f-777c-4bd9-886c-22256bb87f32, linker timestamp 2013-09-06T00:17:56Z). Confirmed before any byte reads. - PE section mapping (hand-parsed via a one-off script,
scratchpad/pe_read.py): image base0x00400000;.textVA=0x00401000 RawPtr=0x00001000;.rdataVA=0x00792000 RawPtr=0x00392000 (holds the FP constants below). VA→file-offset:file_off = raw_ptr + (VA - image_base - section_virt_addr). - Raw bytes of the function (
0x00514F40–0x00515153) were dumped and hand-disassembled instruction-by-instruction, cross-checked line-by-line againstdocs/research/named-retail/acclient_2013_pseudo_c.txtlines 283130–283251 (function body) so every address in the trace lines up with a named pseudo-C statement. - 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.
- 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 3557–3568. 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 00 → 0.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 3f → 1.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 00 → 0.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).