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>
231 lines
9.5 KiB
Markdown
231 lines
9.5 KiB
Markdown
# 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).
|
||
|
||
## 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.
|