feat(cdb): A6.P1 — decode_retail_hex.py hex→float decoder
Python tool that decodes the retail.log hex-bits float fields produced
by a6-probe.cdb v4 into IEEE 754 single-precision values. Required
because cdb's .printf %f doesn't reliably format floats from dwo()
reads — v4 works around this by emitting 32-bit hex, this script
reinterprets via struct.unpack('<f', struct.pack('<I', value)).
Verified against scen1 retail.log:
BP6 threshold_h=0x3F2A0751 → threshold=0.6642 (= FloorZ exactly)
BP5 hit#1 Nz_h=0x3F800000 → Nz=1.0 (ground normal)
9,517 float fields decoded across 9,331 lines.
Output written next to input as .decoded.log. Format matches
acdream-side [push-back] probe (4-decimal floats), so A6.P2
analysis can compare line-for-line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8ca718a56d
commit
194ed3ef21
2 changed files with 20036 additions and 0 deletions
File diff suppressed because it is too large
Load diff
98
tools/cdb/decode_retail_hex.py
Normal file
98
tools/cdb/decode_retail_hex.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env python3
|
||||
"""decode_retail_hex.py — A6.P1 retail-log hex→float decoder.
|
||||
|
||||
cdb's `.printf %f` doesn't reliably format floats from `dwo()` reads
|
||||
(see history of a6-probe.cdb v2→v3→v4). The v4 probe prints all float
|
||||
fields as 32-bit hex bits (`_h=0xHHHHHHHH`), and this script decodes
|
||||
them via IEEE 754 single-precision reinterpretation.
|
||||
|
||||
Usage:
|
||||
py tools/cdb/decode_retail_hex.py <retail.log path>
|
||||
|
||||
Output:
|
||||
Writes a sibling file `<input>.decoded.log` with all `_h=0x...`
|
||||
fields replaced by `=<float-value>`.
|
||||
|
||||
Example:
|
||||
[BP6] check_walkable hit#1 threshold_h=0x3F2A0751
|
||||
→
|
||||
[BP6] check_walkable hit#1 threshold=0.66417414
|
||||
"""
|
||||
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HEX_FIELD_RE = re.compile(r'(\w+)_h=0x([0-9A-Fa-f]{8})')
|
||||
|
||||
|
||||
def decode_hex_float(hex_str: str) -> float:
|
||||
"""Decode 8 hex chars as IEEE 754 single-precision little-endian float.
|
||||
|
||||
Note: cdb prints the dword in big-endian byte order (most significant
|
||||
byte first), but IEEE 754 single is little-endian in memory. So we
|
||||
decode the 4 bytes as if read directly from memory: take the hex
|
||||
string, convert to bytes, reverse byte order (since x86 is LE), then
|
||||
unpack as little-endian float.
|
||||
|
||||
Actually simpler: cdb's %X prints the dword value, which is already
|
||||
interpreted as a uint32. To get the float, we re-pack the uint32 as
|
||||
bytes (in any consistent order) and unpack as float with matching
|
||||
order. Using struct.pack/unpack with the same byte order ensures
|
||||
round-trip correctness.
|
||||
"""
|
||||
n = int(hex_str, 16)
|
||||
# Pack as little-endian uint32, unpack as little-endian float.
|
||||
# The byte order cancels out — what matters is that pack + unpack
|
||||
# agree, which they do via the same '<' specifier.
|
||||
return struct.unpack('<f', struct.pack('<I', n))[0]
|
||||
|
||||
|
||||
def decode_line(line: str) -> str:
|
||||
"""Replace all `name_h=0xHHHHHHHH` occurrences with `name=<float>`."""
|
||||
def repl(m):
|
||||
name = m.group(1)
|
||||
hex_str = m.group(2)
|
||||
try:
|
||||
value = decode_hex_float(hex_str)
|
||||
# Match the acdream-side formatting: 4 decimal places.
|
||||
return f'{name}={value:.4f}'
|
||||
except (ValueError, struct.error):
|
||||
# Keep original on decode failure.
|
||||
return m.group(0)
|
||||
return HEX_FIELD_RE.sub(repl, line)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print('Usage: py tools/cdb/decode_retail_hex.py <retail.log>',
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
in_path = Path(sys.argv[1])
|
||||
if not in_path.exists():
|
||||
print(f'Error: {in_path} not found', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
out_path = in_path.with_suffix('.decoded.log')
|
||||
|
||||
lines_decoded = 0
|
||||
fields_decoded = 0
|
||||
|
||||
with in_path.open('r', encoding='ascii', errors='replace') as fin, \
|
||||
out_path.open('w', encoding='utf-8') as fout:
|
||||
for line in fin:
|
||||
decoded = decode_line(line)
|
||||
if decoded != line:
|
||||
lines_decoded += 1
|
||||
fields_decoded += len(HEX_FIELD_RE.findall(line))
|
||||
fout.write(decoded)
|
||||
|
||||
print(f'Decoded {lines_decoded} lines, {fields_decoded} float fields')
|
||||
print(f'Output: {out_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue