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>
This commit is contained in:
parent
277ef5d032
commit
874d94bf34
1 changed files with 221 additions and 0 deletions
221
docs/research/2026-08-02-set-description-float-gates.md
Normal file
221
docs/research/2026-08-02-set-description-float-gates.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# 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 (`0x00514F40`–`0x00515153`) were dumped and
|
||||
hand-disassembled instruction-by-instruction, cross-checked line-by-line
|
||||
against `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines
|
||||
283130–283251 (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 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
|
||||
|
||||
```c
|
||||
// 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
|
||||
|
||||
```c
|
||||
// inner compare: only assign if friction also passes the upper bound
|
||||
if (esi->friction <= 1.0f)
|
||||
this->friction = esi->friction;
|
||||
```
|
||||
|
||||
### Combined (conditionals 1+2)
|
||||
|
||||
```c
|
||||
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
|
||||
|
||||
```c
|
||||
// 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:
|
||||
```csharp
|
||||
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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue