From 489ea1275973f6ff9d3cfc2bc39546132d36ea10 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Sat, 29 Aug 2026 03:20:24 +0000 Subject: [PATCH] re(ui): the eff-name implication for bit 0x02 is refuted disc-wide Last iteration I killed the biconditional and reported that the one-way reading survived: all 10 bit-set sprites on GP_TITLE build 4 are eff names, so "bit set => eff name". Checked over the disc, that is false. sprites with a resolvable preceding name 14 709 bit SET & name has 'eff' 2 338 bit SET & name lacks 'eff' 2 657 <-- counterexamples bit clear & name has 'eff' 1 399 bit clear & name lacks 'eff' 8 315 P(eff | set) = 0.468 P(eff | clear) = 0.144 The implication fails more often than it holds. What survives is an association -- 3.3x enrichment -- and build 4's 10/10 was a local naming habit in an 18-element bundle, not a format rule. The counterexamples are the useful part: pv_loading_ring0, pv_loading_light0-3, pv_loading_line, px_bunk_line, px_top_extra. Rings, glows, lights, 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 and not a decode, and is labelled as such. Names here come from the string immediately preceding each T8aD, validated 17/18 on build 4 against the RATC child order; the single mismatch is the known pteff04.t32 -> registered as 8AX case, so this is the element (opt) name rather than the sprite's registered name. That mismatch is itself an independent confirmation of the 8AX finding, reached from the opposite direction. METHOD: a pattern perfect on one screen can be near-chance on the disc; and when an association survives a refuted implication, the counterexamples are the finding. --- docs/port/HANDOFF.md | 2 +- docs/re/METHOD.md | 13 +++++ docs/re/REFUTED.md | 6 +++ docs/re/data/eff-bit-census.txt | 24 ++++++++++ docs/re/structures/ui-paint-order-key.md | 35 +++++++++++++- tools/re-capture/eff_bit_census.py | 60 ++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 docs/re/data/eff-bit-census.txt create mode 100755 tools/re-capture/eff_bit_census.py diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index d8d8a4a..bb0b073 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 a7b31c1..394a0e6 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 387fb0f..2d09056 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 0000000..ad8eb0e --- /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 4f990ea..15b1eb5 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 0000000..1149a8b --- /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)