acdream/tools/verify_un2_fmul.py
Erik 0cb97aa594 UN-2 RESOLVED: GetMaxSpeed x4 is byte-verified retail; doc-comment was the misread
The register's UN-2 row recorded a contradiction: the GetMaxSpeed XML doc
claimed the bare run rate was retail-correct (~5.9 m/s catch-up, calling
the xRunAnimSpeed multiply a misread), while the implementation multiplied
by RunAnimSpeed citing ACE. Settled against the binary, not the pseudo-C:

- BN pseudo-C (acclient_2013_pseudo_c.txt:305127) renders get_max_speed as
  void with a bare `this->my_run_rate;` because it DROPS x87 instructions.
- Disassembling the PDB-matched v11.4186 binary at VA 0x00527cb0: all THREE
  return paths end `fld <rate>; fmul dword ptr [0x007C8918]; ret`, and the
  .rdata dword at 0x007C8918 is 4.0f. Sibling get_adjusted_max_speed
  (0x00527d00) carries the same trailing fmul. Verifier committed at
  tools/verify_un2_fmul.py (PE parse + byte decode, rerunnable).
- Retail paths: weenie null -> 1.0 x4; InqRunRate ok -> queried x4;
  InqRunRate failed -> my_run_rate x4. ACE MotionInterp.cs:665-676 matches.

Changes:
- Doc-comment rewritten: the implementation is retail-correct; the catch-up
  speed 2 x get_max_speed ~= 23.5 m/s at run 200 IS retail. The 1-Hz
  remote-blip symptom the old comment attributed to this multiply is
  therefore UNEXPLAINED by it (if it recurs: #41 family, not this).
- Weenie-null path aligned to retail's LITERAL 1.0 default (was MyRunRate).
- Tests re-pinned to the three retail paths (the old NoWeenie test pinned
  the non-retail fallback).
- Register: UN-2 row deleted per the retire rule (6 -> 5 UN rows);
  shortlist renumbered.

This is the 2nd confirmed instance of the BN x87-dropout artifact class
(memory: feedback_bn_decomp_field_names) deciding a register row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:17:50 +02:00

40 lines
1.5 KiB
Python

# UN-2 verification: prove/disprove that retail CMotionInterp::get_max_speed
# (VA 0x00527cb0) multiplies by the 4.0f constant at VA 0x007C8918 on its
# return paths (the fmul the BN pseudo-C drops). Throwaway apparatus.
import struct
p = r"C:\Turbine\Asheron's Call\acclient.exe"
data = open(p, 'rb').read()
pe_off = struct.unpack_from('<I', data, 0x3C)[0]
nsec = struct.unpack_from('<H', data, pe_off + 6)[0]
opt_size = struct.unpack_from('<H', data, pe_off + 20)[0]
sec0 = pe_off + 24 + opt_size
imgbase = struct.unpack_from('<I', data, pe_off + 24 + 28)[0]
def va2off(va):
rva = va - imgbase
for i in range(nsec):
o = sec0 + i * 40
name = data[o:o + 8].rstrip(b'\x00').decode()
vsz, vaddr, rsz, roff = struct.unpack_from('<IIII', data, o + 8)
if vaddr <= rva < vaddr + max(vsz, rsz):
return roff + (rva - vaddr), name
return None, None
print('imgbase', hex(imgbase))
off, sec = va2off(0x00527CB0)
print('get_max_speed VA 0x527cb0 -> file', hex(off), 'sec', sec)
code = data[off:off + 0x50]
print('bytes:', code.hex())
FMUL = bytes.fromhex('d80d18897c00') # fmul dword ptr [0x007C8918]
print('fmul [0x7C8918] count in get_max_speed:', code.count(FMUL))
off2, sec2 = va2off(0x007C8918)
print('dword @0x7C8918 sec', sec2, '=', struct.unpack_from('<f', data, off2)[0])
off3, sec3 = va2off(0x007928B0)
print('dword @0x7928B0 sec', sec3, '=', struct.unpack_from('<f', data, off3)[0])
off4, _ = va2off(0x00527D00)
code4 = data[off4:off4 + 0x70]
print('get_adjusted_max_speed fmul count:', code4.count(FMUL))