re: recover the IDXD record-key / field-tag hash (8643/8643)

Closes the 4-byte record key. tag_hash is name_hash's shape -- byte-sum
checksum in the top byte over a 24-bit modular polynomial -- with two different
constants: modulus 0x00FFFFDF (2^24-33, prime) instead of 0x00FFF9D7, and no
lowercasing, so tags are case-sensitive. name_hash explains 0 of 8643.

Recovered from the tables rather than the executable: every inline field name
is a known (name -> tag) pair, and comparing names differing in one character
gives the per-position weights 1, 0x100, 0x10000, 0x21, 0x2100, ... -- a byte
leaving bit 24 re-enters as 33, i.e. reduction mod 2^24-33. Holds where it is
easy to get wrong (distance 8 and 9 carry correctly).

A record's key is the tag of its own name: FormationSet rosters 362/362,
UnitGroup rosters 281/281, S02 squadron names 111/111 -- so records can be
addressed by name without reading the roster first.

Implemented in Python (unitgroup.tag_hash) and Rust
(sylpheed_formats::hash::tag_hash) with 3 new unit tests carrying disc-derived
vectors; cargo test -p sylpheed-formats --lib hash is 8/8 green.

Not settled: the guest routine is unlocated, so this uses exact modular
arithmetic where the game may use a Barrett step without final fixup.
This commit is contained in:
Sylpheed RE agent
2026-08-25 10:38:18 +00:00
parent 6903b19a27
commit cbf52ba9f9
6 changed files with 251 additions and 0 deletions

View File

@@ -48,6 +48,24 @@ def name_hash(s):
a = (a - (q * MODULUS)) & 0xFFFFFFFF
return (((b << 24) & 0xFF000000) | (a & 0x00FFFFFF)) & 0xFFFFFFFF
TAG_MODULUS = (1 << 24) - 33 # 0x00FFFFDF, prime
def tag_hash(s):
"""IDXD record key / field tag -- NOT name_hash.
Same shape as name_hash (8-bit byte-sum checksum over a 24-bit modular
polynomial) but modulo 0x00FFFFDF instead of 0x00FFF9D7, and NOT
lowercased, so tags are case-sensitive. Recovered empirically from the 8643
(name -> tag) pairs the tables themselves carry; `unitgroup.py --checktags`
re-verifies all of them. A record's key is the tag of its own name, which
each table lists in an in-table roster record.
"""
b = s.encode()
lo = 0
for c in b:
lo = (lo * 256 + c) % TAG_MODULUS
return ((sum(b) & 0xFF) << 24) | lo
def read_entry(pak, h):
idx = open(pak, 'rb').read()
base = pak[:-4]