acdream/docs/research/2026-08-02-set-description-float-gates.md
Erik 78f1eb1896 docs(physics): record cutover slice C3b completion
C3b landed at 0934a121 with dual review PASS. The plan records the
remote-entry mechanism and its verified retail anchors; the float-gates
doc gains the port note pinning the NaN dispositions (friction's
sanctioned skip; elasticity and translucency routed exactly as the
binary; ACE's elasticity NaN divergence recorded). Every dormant C3
prerequisite is now complete — C3c, the host flip with the connected
gates, is the sole remaining piece of C3.

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

231 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
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 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).
## Port note (C3b, `RuntimeRemoteBodyDescription.cs`)
acdream's friction port uses `f >= 0.0f && f <= 1.0f`, which deliberately
SKIPS NaN rather than reproducing the unordered-goes-to-apply codegen quirk
tabled above the sanctioned modern-boundary deviation this doc
pre-declared. Elasticity's setter port (`!(e >= 0f)` first arm) and
translucency's `!= 0.0f` gate both route NaN exactly as the binary does
(0f and apply respectively); ACE's `set_elasticity` sends NaN to 0.1f and
is divergent from the binary on that edge.