diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index d8d8a4ab..bb0b073e 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -120,7 +120,7 @@ authored version can be deleted. ⚠️ **Correlation only — untested.** Nothing yet shows it *means* additive; the test is to blend bit-`0x02` sprites additively and re-correlate the title against the capture. Note `ptlogo_back2eff` is `0x8830` despite its name, so - this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: **not** "the name contains `eff`" (`ptlogo_back2eff` is an `eff` name with the bit clear — confirmed by a validated name→header pairing, not a size guess) and **not** "the element is transient" (`pteff03`/`pteff03a` carry the bit and persist to `t=250`). ⚠️ Practical note for an asset pipeline: `T8aD` headers appear in **RATC child order** (18/18 verified on build 4 against decoded dimensions), which is the only sound way to attribute a header to a name when two sprites share a size — and two here do. + this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: **not** "the name contains `eff`" — and not even the one-way "bit ⇒ `eff` name", which held 10/10 on the title but fails on **2 657 of 4 995** bit-set sprites disc-wide (`P(eff|set) = 0.468`). What survives is a 3.3× association, and the counterexamples are rings, glows and lights — effect-like art without the naming convention and **not** "the element is transient" (`pteff03`/`pteff03a` carry the bit and persist to `t=250`). ⚠️ Practical note for an asset pipeline: `T8aD` headers appear in **RATC child order** (18/18 verified on build 4 against decoded dimensions), which is the only sound way to attribute a header to a name when two sprites share a size — and two here do. ⚠️ **The rotated draw is NOT the swoosh.** It was identified as the swoosh by elimination; that is **refuted** — its quads span y −209…925 in screen space, the swoosh is a band at y 126…360. ✅ **It is the two `ptloop` sweeps** diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index a7b31c19..394a0e6c 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -687,3 +687,16 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the "the bundle stores this texture twice". Pixel-comparing them gives max abs diff **21**: two different renditions of one image. Three matching summaries over 300 000 pixels is weak evidence of identity; the comparison costs one line. +* **A pattern that is perfect on one screen can be near-chance on the disc.** The + `0x02` bit matched `eff` names 10/10 on `GP_TITLE` build 4, which is exactly + the sample size that makes a rule feel found. Disc-wide it holds 46.8 % of the + time — the build's artists simply named their effect sprites consistently. An + 18-element bundle cannot distinguish a format rule from a local naming habit; + before reporting an implication, run it over the corpus, not the example that + suggested it. +* **When an association survives a refuted implication, the counterexamples are + the finding.** The bit is 3.3× enriched for `eff` names and the sprites that + break the rule are `pv_loading_ring0`, `pv_loading_light0`–`3`, + `px_bunk_line` — rings, glows, lights, thin lines. That the exceptions are all + effect-like artwork *without* the naming convention says more about the field + than the rule it broke did. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 387fb0f6..2d090562 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -505,3 +505,9 @@ neighbourhood, not just the line. * "the bit `0x02` marks a transient element" → **refuted.** `pteff03` and `pteff03a` carry the bit and run to `t=250`, ramping to `a=255` and holding. [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) +* "bit `0x02` set ⇒ the sprite's name contains `eff`" (the one-way reading that + survived the biconditional's refutation) → **mine, and refuted disc-wide the + next iteration.** True 10/10 on `GP_TITLE` build 4; over 14 709 sprites it + fails on **2 657 of 4 995** bit-set ones. `P(eff|set) = 0.468` against + `P(eff|clear) = 0.144` — an association, not an implication. + [`ui-paint-order-key.md`](structures/ui-paint-order-key.md) diff --git a/docs/re/data/eff-bit-census.txt b/docs/re/data/eff-bit-census.txt new file mode 100644 index 00000000..ad8eb0e5 --- /dev/null +++ b/docs/re/data/eff-bit-census.txt @@ -0,0 +1,24 @@ +# tools/re-capture/eff_bit_census.py over dat/GP_*.pak -- 2026-08-29 + +sprites with a resolvable preceding name: 14709 + bit SET & name has 'eff' : 2338 + bit SET & name lacks 'eff': 2657 <-- counterexamples to 'set => eff' + bit clear & name has 'eff' : 1399 + bit clear & name lacks 'eff': 8315 + + P(name has 'eff' | bit set) = 0.468 + P(name has 'eff' | bit clear) = 0.144 + + counterexamples (bit set, no 'eff'): + ('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126') + ('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27') + ('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126') + ('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27') + ('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101') + ('GP_BUNK.pak', 'pv_loading_ring0.t32', '00000832', '142x142') + ('GP_BUNK.pak', 'pv_loading_light0.t32', '00000832', '29x25') + ('GP_BUNK.pak', 'pv_loading_light1.t32', '00000832', '30x29') + ('GP_BUNK.pak', 'pv_loading_light2.t32', '00000832', '29x30') + ('GP_BUNK.pak', 'pv_loading_light3.t32', '00000832', '25x29') + ('GP_BUNK.pak', 'pv_loading_ring1.t32', '00000832', '72x72') + ('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101') diff --git a/docs/re/structures/ui-paint-order-key.md b/docs/re/structures/ui-paint-order-key.md index 4f990ea9..15b1eb5b 100644 --- a/docs/re/structures/ui-paint-order-key.md +++ b/docs/re/structures/ui-paint-order-key.md @@ -528,7 +528,8 @@ evidence rather than on a guess between two same-sized sprites. | hypothesis | verdict | |---|---| -| bit ⟺ the name contains `eff` | **refuted** — `ptlogo_back2eff` is an `eff` name with the bit clear. All 10 bit-set sprites *are* `eff` names, so the implication holds one way only | +| bit ⟺ the name contains `eff` | **refuted** — `ptlogo_back2eff` is an `eff` name with the bit clear | +| bit ⇒ the name contains `eff` (the surviving one-way reading) | **also refuted, disc-wide — see below.** It held 10/10 on build 4 and fails on **2 657 of 4 995** bit-set sprites across the disc | | bit ⟺ the element is transient (gone before the screen settles) | **refuted** — `pteff03`/`pteff03a` carry the bit and run to `t=250`, ramping to `a=255` and holding, i.e. they persist | ### 🟡 What the exception looks like up close @@ -549,3 +550,35 @@ of one image at one size. So the bit tracks something that also distinguishes two versions of the same artwork, alongside a different layer key. ❔ **Still not diagnosed**, but the search space is smaller by two and the attribution underneath it is now sound. + + +### 🔴 The one-way implication is refuted too — build 4 was a local pattern + +Having fixed the attribution, the surviving reading was *"bit set ⇒ the name +contains `eff`"*, true 10/10 on `GP_TITLE` build 4. Checked disc-wide with +[`tools/re-capture/eff_bit_census.py`](../../../tools/re-capture/eff_bit_census.py) +([data](../data/eff-bit-census.txt)) over 14 709 sprites whose preceding name +resolves: + +| | count | +|---|---| +| bit SET & name has `eff` | 2 338 | +| **bit SET & name lacks `eff`** | **2 657** | +| bit clear & name has `eff` | 1 399 | +| bit clear & name lacks `eff` | 8 315 | + +`P(eff | set) = 0.468` against `P(eff | clear) = 0.144`. **The implication fails +more often than it holds.** What survives is an *association* — the bit is 3.3× +enriched for `eff` names — and one build's 10/10 was a local naming habit, not a +rule. + +🟡 The counterexamples are the interesting part: `pv_loading_ring0`, +`pv_loading_light0`–`3`, `pv_loading_line`, `px_bunk_line`, `px_top_extra` — +rings, glows, lights and thin lines. **Effect-like artwork that does not carry +the `eff` naming convention.** Consistent with the bit marking effect sprites by +authoring intent rather than by name, which is a description, not a decode. + +⚠️ Name caveat: these names come from the string **immediately preceding** each +`T8aD`, validated 17/18 on build 4 against the child order. The one mismatch is +the known `pteff04.t32` → registered as `8AX` case, so this is the **element** +(`opt `) name, not the sprite's registered name. diff --git a/tools/re-capture/eff_bit_census.py b/tools/re-capture/eff_bit_census.py new file mode 100755 index 00000000..1149a8be --- /dev/null +++ b/tools/re-capture/eff_bit_census.py @@ -0,0 +1,60 @@ +"""Disc-wide: does T8aD flag bit 0x02 imply an `eff` name? + +Names come from the NAME IMMEDIATELY PRECEDING each T8aD chunk -- validated +17/18 on GP_TITLE build 4 against the known RATC child order. The single +mismatch is the documented `pteff04.t32` -> registered as `8AX` case, i.e. the +preceding name is the ELEMENT's (opt) name and the child list is the SPRITE's. +This test uses the element name and says so. +""" +import struct, zlib, glob, os, re, collections +NAME = re.compile(rb'[A-Za-z0-9_.]{2,31}\x00') + +def entries(base): + stub = open(base + ".pak", "rb").read() + if stub[:4] != b"IPFB": return + n = struct.unpack_from(">I", stub, 4)[0] + segs = sorted(glob.glob(base + ".p[0-9][0-9]")) + if not segs: return + blob = b"".join(open(s, "rb").read() for s in segs) + for i in range(n): + h, off, sz = struct.unpack_from(">III", stub, 0x10 + 12 * i) + st = blob[off:off + sz] + if len(st) < 10: continue + try: yield (zlib.decompress(st[10:]) if st[:2] == b"Z1" else st) + except Exception: continue + +set_eff = set_noneff = clear_eff = clear_noneff = 0 +examples = [] +for pak in sorted(glob.glob("/work/sylph_extract/dat/GP_*.pak")): + for d in entries(pak[:-4]): + for m in re.finditer(b"T8aD", d): + o = m.start() + try: + fl = struct.unpack_from(">I", d, o + 4)[0] + w = struct.unpack_from(">I", d, o + 0x14)[0] + h = struct.unpack_from(">I", d, o + 0x18)[0] + except Exception: continue + if not (0 < w <= 4096 and 0 < h <= 4096): continue + ms = list(NAME.finditer(d[max(0, o - 64):o])) + if not ms: continue + nm = ms[-1].group()[:-1].decode("latin1") + eff = "eff" in nm.lower() + if fl & 2: + if eff: set_eff += 1 + else: + set_noneff += 1 + if len(examples) < 12: examples.append((os.path.basename(pak), nm, f"{fl:08x}", f"{w}x{h}")) + else: + clear_eff += 1 if eff else 0 + clear_noneff += 0 if eff else 1 +tot = set_eff + set_noneff + clear_eff + clear_noneff +print(f"sprites with a resolvable preceding name: {tot}") +print(f" bit SET & name has 'eff' : {set_eff}") +print(f" bit SET & name lacks 'eff': {set_noneff} <-- counterexamples to 'set => eff'") +print(f" bit clear & name has 'eff' : {clear_eff}") +print(f" bit clear & name lacks 'eff': {clear_noneff}") +if tot: print(f"\n P(name has 'eff' | bit set) = {set_eff/max(set_eff+set_noneff,1):.3f}") +if tot: print(f" P(name has 'eff' | bit clear) = {clear_eff/max(clear_eff+clear_noneff,1):.3f}") +if examples: + print("\n counterexamples (bit set, no 'eff'):") + for e in examples: print(" ", e)