Square's Weakest Link — complete teardown

A complete static teardown of Square's Weakest Link (Kristopher M. Toops, 2001), a Flash 5 projector: the container, all 41 sprites, the sounds, the 252 shapes, the full recovered script, every frame re-rendered, and the off-stage content the movie never showed anyone.

From the desktop window

squares-weakest-link-readme.txt — Square's Weakest Link teardown

squares-weakest-link-readme.txtOpen in new tab

squares-weakest-link-readme.txt — Square's Weakest Link teardown

Original text document

# Square's Weakest Link -- complete teardown

A 2001 Flash movie distributed as a Windows `.exe`, opened all the way up:
every asset decoded, every line of dialogue recovered, the whole timeline
replayed frame by frame, and the content the movie never showed anyone
catalogd.

**Source:** `Weakest_Link_-_SE.exe`, 1,478,606 bytes.
**What it turned out to be:** a Macromedia Flash 5 standalone projector with a
1.1 MB SWF welded onto the back of the player executable.
**Status:** fully decoded. Nothing in the file is unaccounted for.

| | |
|---|---|
| Title | Square's Weakest Link |
| Author | **Kristopher M. Toops** -- "written, produced, directed & animated by" |
| Music | **Shawn Overn** |
| Proofreading credit | Lord Clodelroth, Comet Green |
| Date | **2001** -- the end card reads `MMI -aka 2001 kmt.` |
| Stage | 550 x 335 px, 12 fps, 273 frames |
| Runtime | click-advanced; about 9-10 minutes at a reading pace |

---

## Contents

1. [What it is](#1-what-it-is)
2. [Provenance and identification](#2-provenance-and-identification)
3. [The container](#3-the-container)
4. [The movie](#4-the-movie)
5. [ActionScript -- all of it](#5-actionscript--all-of-it)
6. [Data inventory](#6-data-inventory)
7. [The script](#7-the-script)
8. [Hidden and never-visible content](#8-hidden-and-never-visible-content)
9. [Re-render and reconstruction](#9-re-render-and-reconstruction)
10. [Credits and rights](#10-credits-and-rights)
11. [How to reproduce this](#11-how-to-reproduce-this)
12. [The toolchain](#12-the-toolchain)
13. [About this teardown](#13-about-this-teardown)

---

## 1. What it is

Kefka Palazzo hosts a parody of the quiz show *The Weakest Link* on the Final
Fantasy VI opera-house stage, with eight Square protagonists as contestants:

**Bartz** (FFV) * **Cecil** (FFIV) * **Crono** (Chrono Trigger) *
**Edgar** (FFVI) * **Black Mage** (FF1) * **Edward** (FFIV) * **Kain** (FFIV) *
**Terra** (FFVI)

It is not a game. There is no scoring code, no state, no variables, and no
branching -- nothing in the file evaluates an answer. It is a **click-advanced
comic**: each panel of dialogue is one timeline frame that stops and waits for
a mouse click, with six short animated interludes that play at full speed.

Elimination order:

1. **Round 1** -- Edward is thrown off *before any question is asked and without
   a vote*, purely because Kefka feels like it. ("I am the host, and I say you
   are the weakest link, so Buh-bye Edward!")
2. **Round 2** -- Kain, voted off on near-unanimous "can't trust him" jokes. He
   votes for himself by accident and is refused a change of vote.
3. **Round 3** -- Cecil **and** Bartz tie, and Kefka eliminates both on the
   grounds that they are "obviously clones" of each other and of the original
   Final Fantasy Fighter.
4. **Round 4** -- Terra and the Black Mage both answer "I!" to *"those of you who
   use powerful magic, say I!"*, so Kefka declares both the weakest link. The
   Black Mage retaliates with **FIR2** ("Oooo... I feel slightly warmer than
   usual") and teleports out promising to return "with my cousin Vivi".
5. **Final** -- Crono beats Edgar 4-of-5.

### The central gag

**Crono never speaks.** He is a silent protagonist, so the author gives him an
*empty speech balloon* on every answer -- a stop-frame that places a balloon
shape and no text at all. Kefka rules every blank correct, and Crono wins the
show without uttering a word. Edgar's closing objection is the punchline:
*"I object! He never answered a single question the entire time he was here!!"*

This is visible in the data, not just on screen. The blank-balloon frames are
**f51, f111, f155, f217, f222, f226 and f230** -- every one of them sits
immediately after a question addressed to Crono and immediately before a
"Correct."

Other jokes that read as deliberate: the Final Fantasy IX teases (Vivi as the
Black Mage's cousin; FFIX described as "a clone of the original Final
Fantasy"), "Stiner" being marked wrong on spelling, and Kefka's fourth-wall
line at f65 -- *"Fool, this is a comic, I can read all of the text even if you
speak it."*

One typo survived the credited proofreaders: at **f17** Edward (FFIV) is
introduced as *"Edgar from Final Fantasy IV"*, though the script calls him
Edward correctly from f26 onward.

---

## 2. Provenance and identification

### Hashes

| File | Size | MD5 |
|---|---:|---|
| `Weakest_Link_-_SE.exe` | 1,478,606 | `5a640e7f6ca23a2666cb496466d15f45` |
| extracted `.swf` | 1,101,766 | `4d629d942d36793a9a299f7a4329d8dc` |

SHA-1 of the executable: `2e7ea8eef7a9006e40f75094db78152b34be46cd`

### The player stub

Read straight out of the PE version resource:

| Field | Value |
|---|---|
| OriginalFilename | `SwFlsh32.exe` |
| ProductName | Flash 5.0 |
| FileDescription | **Flash Player 5.0 r30** |
| FileVersion | 5,0,30,0 |
| CompanyName | Macromedia, Inc. |
| LegalCopyright | Copyright (c) 1996-2000 Macromedia, Inc. |
| PE timestamp | **2000-08-18 02:00:29 UTC** |
| Machine / subsystem | i386 (0x014C) / GUI (2), linker 6.0 |
| Sections | `.text` 281,388 * `.rdata` 31,832 * `.data` 18,044 * `.data1` 176 * `.rsrc` 38,608 |

The stub is stock Macromedia code with nothing project-specific in it, which
means **the `.exe` is disposable**: the extracted `.swf` plays identically in
Ruffle or any Flash player and is the file worth keeping.

### Dating it, and why the executable says 2000

Everything in the table above belongs to `SwFlsh32.exe` -- **Macromedia's**
binary, not the author's. Publishing a projector from Flash does not compile
anything; it concatenates the unmodified stock player, the movie and an
eight-byte footer. So the PE timestamp, the version resource and the
1996-2000 copyright all date *Macromedia's build of Flash Player 5.0 r30*.

That makes 18 August 2000 a **floor**, not a date: the movie must be at least
as new as the player used to wrap it, and can be arbitrarily newer. This is
worth internalizing for any projector -- **the container's metadata describes
the runtime vendor and never the content author.** Reading a projector's PE
timestamp as the work's date will be wrong by months to years, every time.

Three independent lines of evidence put the movie in 2001:

1. **The movie says so.** The final card reads `THE END.` / `MMI -aka 2001
   kmt.` -- the Roman numeral with the year spelled out beside it and the
   author's initials. This is a statement, not an inference.

2. **The content cannot predate November 2000.** The script names Vivi,
   Steiner and Quina and asks two questions about Final Fantasy IX by name
   (f60, f160, f227). FFIX shipped in North America in November 2000 -- three
   months *after* the player stub was built. Whoever wrote these jokes had
   played it.

3. **The audio was encoded later than the player.** Both music MP3s carry the
   encoder fingerprint `LAME3.84 (alpha 1)` in their trailing frame -- stamped
   by the author's own encoding pass, from the LAME 3.8x series that postdates
   the August 2000 stub. (The 16 kbps mono sting, id 88, carries no tag.)

A fourth, softer bound: the file is SWF version 5, so it was authored in
Flash 5, which Macromedia released in August 2000. Every clock in the file
except Macromedia's points at 2001.

### Where it came from, and who "kmt" was

None of this is in the file -- it comes from the surrounding record, and it is
the one part of this document that is not derived from the bytes.

The movie was distributed through **Dream Horizon Software (DHS)**, a small
American Final Fantasy fan-game group active around 2000-2001, and was hosted
on their domain **dhsrpg.net** as a self-extracting executable. This copy was
rediscovered on a personal fan site that had linked it with credit to DHS, and
the original DHS source was then traced back through the Wayback Machine.

DHS's own staff roster, archived in April 2001, lists a **DarthTOOPS** in its
Public Relations department, described elsewhere on doors98 as the member
"whose claim to fame outside of DHS was creating Square's Weakest Link". The
movie's credits name **Kristopher M. Toops** and sign the end card `kmt`. Same
surname, same credit, same year, same group -- DarthTOOPS and Kristopher M.
Toops are almost certainly one person, and that is as far as the evidence
goes.

One correction to the surrounding record while we are here: DHS-era
descriptions and doors98's own earlier writeup both call this "a fan game".
It is not a game. It contains no scoring, no state, no branching and nothing
that evaluates an answer -- sections 4 and 5 take that apart. It is a
click-advanced comic, and the only thing in the file that responds to input is
a single button whose entire behavior is `play()`.

---

## 3. The container

A Flash projector is the stock player executable with the movie appended and an
eight-byte footer:

```
offset 0x000000  +-------------------------------------+
                 |  PE executable: Flash player stub   |  376,832 bytes
offset 0x05C000  +-------------------------------------+
                 |  SWF movie (FWS, uncompressed)      |  1,101,766 bytes
offset 0x1692C6  +-------------------------------------+
                 |  56 34 12 FA  |  C6 CF 10 00        |  8-byte footer
                 +-------------------------------------+
                    magic 0xFA123456   LE movie length
```

To split one: search backwards for `56 34 12 FA`, read the little-endian `u32`
that follows as the movie length, and the movie starts that many bytes before
the magic. Verify by checking for `FWS` / `CWS` / `ZWS` at the computed offset.

`CWS` means the tag stream after the 8-byte header is zlib-deflated; `ZWS`
means LZMA. This movie is `FWS` -- flat and uncompressed, which is unusually
convenient. `tools/unpack_projector.py` normalizes all three to `FWS` so
everything downstream can assume a flat stream.

---

## 4. The movie

### 4.1 Header

```
Signature   FWS  (uncompressed)
Version     5
File length 1,101,766  -- matches actual; no truncation, no appended junk
Stage       0..11000 x 0..6700 twips  =  550 x 335 px
Frame rate  12 fps
Frames      273
Background  #FFFFFF
```

There is no `Protect` tag, no `EnableDebugger`, no `Metadata`, no
`ExportAssets`, no `FrameLabel`, and no compression. It is about as open as a
SWF gets.

A twip is 1/20 px. Every coordinate in the file is in twips; every matrix
translation is in twips while the scale terms are 16.16 fixed point. Getting
that mix wrong is the single most common way a SWF parser produces
almost-right garbage.

### 4.2 Tag census -- 1,906 tags

| Count | Tag | What it is here |
|---:|---|---|
| 628 | `PlaceObject2` | the entire "animation" -- swapping composite scene shapes per panel |
| 317 | `ShowFrame` | 273 root frames + 44 inside movieclips |
| 304 | `RemoveObject2` | tearing down the previous panel |
| 211 | `DefineShape2` | composite scene layers and speech balloons |
| 164 | `DoAction` | **every one is 2 bytes** (section 5) |
| 136 | `DefineText` | the entire script |
| 41 | `DefineShape` | single-bitmap holder rectangles |
| 40 | `DefineBitsLossless2` | the ripped sprites, with alpha |
| 28 | `StartSound` | music and SFX cues |
| 8 | `SoundStreamHead2` | **dead** -- declared, never fed a `SoundStreamBlock` |
| 8 | `End` | root plus 7 movieclips |
| 7 | `DefineSprite` | the only real movieclips |
| 6 | `DefineSound` | 3 MP3, 3 ADPCM |
| 2 + 2 | `DefineFont` / `DefineFontInfo` | Times New Roman plus a 4-glyph novelty font |
| 1 | `SetBackgroundColor` | white |
| 1 | `DefineBitsLossless` | the stage backdrop, opaque |
| 1 | `DefineButton2` | the **only** interactive object in the movie |
| 1 | `DefineMorphShape` | the fade-to-credits wipe |

446 characters are defined in total, and **every one of them is referenced by
something** -- a placement, a bitmap fill, a button state, a `StartSound`, or a
text run. Nothing was defined and abandoned.

### 4.3 The interaction model

One `DefineButton2` (character id 25) exists in the entire file. Its up, over,
down and hit states are all the *same* 79 x 22 px rectangle (shape id 23),
captioned by a static "Press" text object (id 24). Its only handler is:

```
on(release) {
    play();          // ActionScript opcode 0x06, one byte
}
```

164 of the 273 root frames carry a `DoAction`, and every single one is two
bytes:

* `07 00` -> `stop()` -- 154 frames
* `09 00` -> `stopSounds()` -- 10 frames: 6, 23, 43, 69, 103, 124, 152, 165,
  214 and 272, the round and scene boundaries where the music bed is cut before
  being restarted

So the playback loop is: *frame draws -> `stop()` -> you click Press ->
`play()` -> one frame advances -> `stop()` again.*

### 4.4 The six auto-play runs

The Press button is placed only six times, and each placement covers one
chapter:

| Button placed | on depth | removed | covers |
|---|---:|---|---|
| f5 | 36 | f30 | Round 1 |
| f42 | 37 | f84 | Round 2 |
| f102 | 40 | f137 | Round 3 |
| f150 | 32 | f177 | Round 4 |
| f207 | 29 | f236 | Final round |
| f268 | 28 | f272 | Credits |

The gaps between them are the frame ranges with **no** `stop()`, which
therefore run at the full 12 fps as cutscenes:

```
f1-4      title / opening
f30-41    Edward is escorted off        1.0 s
f84-101   Kain is escorted off          1.5 s
f137-149  the "clones" are escorted off 1.1 s
f177-206  Terra and the Black Mage exit 2.5 s
f236-267  fade to credits               2.7 s
```

### 4.5 Why it still looks animated while stopped

`DefineSprite` movieclips keep running when the root timeline is stopped. The
host's movieclip -- a 4-frame, 2-pose walk cycle -- is on the stage the whole
time and keeps bobbing through every static dialogue panel. That is the only
motion during the read-along sections, and it is why a naive "render frame N"
approach that ignores movieclip playheads produces a subtly dead-looking
reconstruction.

### 4.6 How the art is composited

There is essentially **no vector art in this movie**. Each panel is a
`DefineShape2` whose fill styles are a *stack of clipped-bitmap fills* -- one
rectangle per character plus one for the backdrop, all inside a single shape.
The shape used from f45, for instance:

```
shape 93  bounds 617 x 337 px, 20 fill styles, 25 edge segments
          bitmap fills: 9 (backdrop), 8, 7, 6, 4, 3, 2, 1  + 29 (host)
```

To "animate" a character leaving, the author defines a *new* composite shape
with one fewer bitmap fill and swaps it in with `PlaceObject2`. That is why the
file has 211 `DefineShape2` tags and almost no real geometry: of the 252 shapes
exported to SVG, the overwhelming majority are between one and nine
axis-aligned rectangles carrying a bitmap.

The genuine vector objects are the **speech balloons** -- a white solid fill, a
1 px black line style, and a pointer tail:

```
shape 19  bounds 285.6..534.6 x 107.5..238.6 px
          1 fill  solid #FFFFFF
          1 line  width 20 twips = 1 px, #000000
          4 edge segments
```

### 4.7 The movieclips

Seven `DefineSprite` tags, all of them simple bitmap flipbooks:

| sprite | frames | bitmaps | what it is |
|---:|---:|---|---|
| 15 | 4 | 11, 13 | Kefka walk cycle, pose pair A |
| 20 | 4 | 11, 13 | the same cycle, second instance |
| 83 | 4 | 79, 81 | Kefka walk cycle, pose pair B -- **never visible**, see section 8 |
| 231 | 4 | 227, 229 | Edgar walk cycle |
| 342 | 8 | 334, 336, 338, 340 | Terra walk cycle A |
| 352 | 8 | 344, 346, 348, 350 | Terra walk cycle B |
| 385 | 12 | 361..383 | the fire spell -- Black Mage's "FIR2" |

### 4.8 The credits transition

`DefineMorphShape` id 436 is a full-stage **solid black rectangle** (start
bounds -26.5..574.6 x -28.1..366.6 px, end bounds essentially identical, fill
RGBA `0,0,0,255` -> `0,0,0,255`). It is placed on depth 27 at f236 and then
re-placed once per frame with `ratio` stepping `2048, 4096, 6144 ...` up
through 65535 over 32 frames -- a plain wipe to black before the credit cards.

---

## 5. ActionScript -- all of it

There is nothing to reverse-engineer. The complete ActionScript inventory of
this 1.1 MB movie is:

```
154 x   stop()          opcode 0x07
 10 x   stopSounds()    opcode 0x09
  1 x   play()          opcode 0x06, on the button's release handler
```

No `constantPool`, no `defineFunction`, no `getURL`, no `setVariable`, no
`gotoFrame`. The author built the whole thing on the Flash 5 timeline with the
Actions panel in "Basic Actions" mode.

The same is true at the placement level. Across all 628 `PlaceObject2` tags,
**not one** sets a color transform, an instance name, a clip depth, or clip
actions; only 48 set a ratio, and those are the credits morph. So nothing
anywhere is faded, tinted, masked or named, and there are no `onClipEvent`
handlers hiding outside the `DoAction` tags. The one button really is the
movie's entire interactive surface.

Full disassembly: `extract/actionscript_disasm.txt`.

---

## 6. Data inventory

### 6.1 Bitmaps -- 41

All are `DefineBitsLossless2` (zlib, 8-bit palette, format 3, with alpha)
except id 9, which is `DefineBitsLossless` and opaque. Every one is ripped
Square sprite art -- mostly SNES-era, with the Black Mage in the later
remake/WonderSwan style. Character identifications are mine, from the art
cross-checked against which bitmap ids appear in the composite shape for each
scene and who is speaking in it.

| id | size | subject |
|---:|---|---|
| 1 | 16x24 | **Kain Highwind** -- Final Fantasy IV dragoon |
| 2 | 16x24 | **Bartz/Butz Klauser** -- Final Fantasy V |
| 3 | 16x24 | **Cecil Harvey**, Paladin -- Final Fantasy IV |
| 4 | 16x24 | **Edgar Roni Figaro** -- Final Fantasy VI |
| 5 | 16x24 | **Edward Chris von Muir** -- Final Fantasy IV, the bard |
| 6 | 38x34 | **Crono** -- Chrono Trigger, mid-swing katana pose |
| 7 | 15x24 | **Terra Branford** -- Final Fantasy VI |
| 8 | 17x26 | **Black Mage** -- original Final Fantasy |
| **9** | **240x147** | **the backdrop** -- the Final Fantasy VI opera house: red curtains, tiered audience of NPC sprites |
| 11, 13 | 16x24 | **Kefka Palazzo** walk cycle, pose pair A |
| 29, 66, 68 | 16x24 | Kefka, additional poses |
| 79, 81 | 16x24 | Kefka walk cycle, pose pair B -- **never rendered**, section 8 |
| 171, 212, 227, 229, 239 | 16x24 | Edgar, additional poses and walk cycle |
| 334, 336, 338, 340 | 15x24 | Terra walk cycle A |
| 344, 346, 348, 350 | 15x24 | Terra walk cycle B |
| 361-383 (12) | 48x48 | **fire spell animation**, 12 frames -- "FIR2" |

### 6.2 Sounds -- 6

| id | codec in SWF | extracted as | rate | ch | length | cues | role |
|---:|---|---|---|---:|---|---:|---|
| 16 | MP3 128 kbps | `snd_016.mp3` | 44.1 kHz | 2 | 3.81 s | 10 | round-transition sting |
| 31 | MP3 128 kbps | `snd_031.mp3` | 44.1 kHz | 2 | **54.88 s** | 9 | **the main theme bed** |
| 88 | MP3 16 kbps | `snd_088.mp3` | 11.025 kHz | 1 | 2.06 s | 6 | elimination sting |
| 359 | **ADPCM** | `snd_359.wav` | 5.512 kHz | 1 | 1.61 s | 1 | spell cast, f209 |
| 387 | **ADPCM** | `snd_387.wav` | 5.512 kHz | 1 | 1.04 s | 1 | impact, f210 |
| 393 | **ADPCM** | `snd_393.wav` | 5.512 kHz | 1 | 5.32 s | 1 | the "POOF!" exit, f212 |

The three ADPCM sounds needed a from-scratch Flash-ADPCM decoder -- see
section 11.5. All three decode to **exactly** their declared sample counts
(8,882 / 5,709 / 29,316), which is a clean self-check.

Both music MP3s carry `LAME3.84 (alpha 1)` in their trailing frame -- the
encoder the author ran the audio through, and one of the three clocks that
dates the movie to 2001 rather than to the player stub's 2000 (section 2).

The 8 `SoundStreamHead2` tags are vestigial: they declare 5.512 kHz / 8-bit /
mono with `samples_per_frame = 0`, and no `SoundStreamBlock` ever follows.
Flash's authoring tool emitted one for every movieclip. There is no streamed
audio in this file.

### 6.3 Vector shapes -- 252

Every `DefineShape` and `DefineShape2` is converted to a standalone SVG with
correct fill winding, plus `shapes/_index.json` listing bounds, fill and line
counts, and which bitmap ids each shape references. Bitmap-fill paths emit
`fill="url(#bmpNNN)"`; pair them with the matching PNG if you want a
self-contained SVG.

### 6.4 Fonts -- 2

| id | name | glyphs | charset |
|---:|---|---:|---|
| 21 | Times New Roman | 67 | `` !"&',-.01245:?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`` |
| 391 | Times New Roman | **4** | `!FOP` |

Font 391 is a 4-glyph subset embedded for exactly one word: the giant white
**"POOF!"** at frame 212. Note that font 21's charset has no `3`, `6`, `7`,
`8`, `9`, `;` or `(` -- the author only ever embedded the glyphs the script
actually used, which is why the whole screenplay fits in 67 glyphs.

### 6.5 Text -- 136 records

All dialogue is **static** `DefineText` -- glyph indices plus advances, with no
`DefineEditText` anywhere. This is why a strings dump of the executable finds
nothing readable. See section 11.3.

---

## 7. The script

Every line of dialogue in the movie, in playback order, recovered from the 136
`DefineText` tags and keyed to the main-timeline frame each one is *placed* on.

Panels marked **[silent balloon]** stop and wait for a click but place no text.
In the Q&A rounds every one of them is Crono answering a question - he is a silent
protagonist in *Chrono Trigger* and the movie never lets him speak. Kefka accepts
the blank balloon as correct every single time, and Crono goes on to win the show.

### Frames 1-4 - Title / opening animation (auto-play, 12 fps)

*(no dialogue - continuous animation)*

### Frames 5-29 - ROUND 1 - introductions, and Edward is dismissed

- **f5** - Hello and welcome again to another fine episode of Square's Weakest Link. I am your great and all powerful host, Kefka!
- **f6** - Let's meet our eight great contestants shall we? On the right, from Final Fantasy V we have Bartz!
- **f7** - *[held panel, no new text]*
- **f8** - Thank you, it is good to be here Mr. Kefka.
- **f9** - Cecil from Final Fantasy IV
- **f10** - Is it just me or does Bartz look like he could be my son??
- **f11** - From ChronoTrigger, Crono
- **f12** - *[held panel, no new text]*
- **f13** - One of my enemies from Final Fantasy VI, say hello to Edgar!
- **f14** - Didn't we beat you?
- **f15** - Moving to the left of the room, we have the popular Black Mage from the original Final Fantasy
- **f16** - WASSUP!
- **f17** - One of the suckiest characters to ever be in a game, Edgar from Final Fantasy IV.
- **f18** - C'mon, was I really that bad??
- **f19** - You can't trust this next guy, it's Kain from Final Fantasy IV
- **f20** - How many times must I say I'm sorry? Yeesh!
- **f21** - Lastly, you may know her as that girl that freaked out in Final Fantasy VI, Terra.
- **f22** - Thank you for having me on the show today.
- **f23** - You all know the rules, so let's begin round one.
- **f24** - *[held panel, no new text]*
- **f25** - *[held panel, no new text]*
- **f26** - Edward, you are the weakest link, Good-bye!
- **f27** - But you didn't even ask me a question yet, and no one voted me off the show...
- **f28** - I am the host, and I say you are the weakest link, so Buh-bye Edward!
- **f29** - Fine.

### Frames 30-41 - Animation: Edward walks off (auto-play)

*(no dialogue - continuous animation)*

### Frames 42-83 - ROUND 2 - Q&A, voting, Kain eliminated

- **f42** - *[held panel, no new text]*
- **f43** - Let's begin round two.
- **f44** - *[held panel, no new text]*
- **f45** - Bartz, who was the main villian of Final Fantasy IV?
- **f46** - Zeromus
- **f47** - Correct! Cecil, what game was Lavos in?
- **f48** - Chrono Trigger
- **f49** - Correct.
- **f50** - Crono, what is your most famous line in Chronotrigger?
- **f51** - *[Crono answers - empty speech balloon]*
- **f52** - Correct. Edgar, who composes music for the Final Fantasy games?
- **f53** - Hey, wait just a minute! Crono's question wasn't fair.
- **f54** - Too bad, just answer the question. Who composes music for Final Fantasy?
- **f55** - Bahamut.
- **f56** - Incorrect you stupid nitwit, it is Nobuo Uematsu.
- **f57** - *[held panel, no new text]*
- **f58** - Black Mage, what is my famous deadly spell?
- **f59** - That would be Light of Judgement.
- **f60** - Correct. Kain, name two characters from Final Fantasy IX.
- **f61** - Vivi, and ummm.... ummm.....
- **f62** - Stiner .
- **f63** - Incorrect, you spelled Steiner wrong.
- **f64** - Hey, I said Steiner, I didn't spell it out!
- **f65** - Fool, this is a comic, I can read all of the text even if you speak it. Buwahahahahaha!
- **f66** - Terra, what hideous beast do you call daddy?
- **f67** - That's an easy one. Maduin.
- **f68** - Correct.
- **f69** - Now it is time to vote off who you think is the weakest link. Give names and explanations.
- **f70** - *[held panel, no new text]*
- **f71** - I vote Kain. I don't know if I can trust him .
- **f72** - Kain. I know I can't trust him.
- **f73** - *[held panel, no new text]*
- **f74** - Ummm... Is it my turn? Ok. I vote Crono, he hasn't said anything yet.
- **f75** - My vote is for Bartz. He is obviously a clone of Cecil, who himself is a clone of the original Final Fantasy fighter.
- **f76** - Kain, becau...
- **f77** - Hey, wait a minute I'm Kain!! Can I change my vote?
- **f78** - He he. No you can't!! Muwahahaha!
- **f79** - Edgar, he has a bad attitude today.
- **f80** - Whatever.
- **f81** - Kain, you have been voted the weakest link by a majority of your peers. Do you have anything to say?
- **f82** - I still don't understand why people don't trust me.
- **f83** - Kain, you are the weakest link. Good-bye!

### Frames 84-101 - Animation: Kain walks off (auto-play)

*(no dialogue - continuous animation)*

### Frames 102-136 - ROUND 3 - Q&A, voting, Cecil AND Bartz eliminated

- **f102** - *[held panel, no new text]*
- **f103** - Time for round three of Square's Weakest Link!
- **f104** - *[held panel, no new text]*
- **f105** - Bartz, who said "You spoony bard"?
- **f106** - Sephiroth.
- **f107** - Incorrect, Tellah said "you spoony bard."
- **f108** - Cecil, what video game are you in?
- **f109** - Final Fantasy II.
- **f110** - Incorrect, you are in Final Fantasy IV. Crono, what sound do oxygen atoms make?
- **f111** - *[Crono answers - empty speech balloon]*
- **f112** - Correct. Edgar, name a famous Moogle.
- **f113** - Mog.
- **f114** - Correct.
- **f115** - Black Mage, name a character that has appeared in almost all Final Fantasys.
- **f116** - Cid.
- **f117** - Correct.
- **f118** - Terra, is Edgar god's gift to women?
- **f119** - *[held panel, no new text]*
- **f120** - No.
- **f121** - WHAT?!?!
- **f122** - Wait a minute I object to that last question and answer!
- **f123** - Too bad, she was correct. Uwehehehe.
- **f124** - Time to vote off the next loser.... Err... I mean Weakest Link.
- **f125** - *[held panel, no new text]*
- **f126** - Cecil, because he answered wrong.
- **f127** - Bartz, because he is a doofis.
- **f128** - *[held panel, no new text]*
- **f129** - Crono, he is not talking.
- **f130** - I vote Cecil and Bartz, they are clones of the original Final Fantasy Fighter I tell you!!
- **f131** - Black Mage. His spells don't compare to mine.
- **f132** - Since Cecil and Bartz have the same amount of votes, and they are obviously clones...
- **f133** - They will both be the weakest link!
- **f134** - WE CLONES WILL RULE THE WORLD!
- **f135** - Yeah, whatever. Only I Kefka will rule the world.
- **f136** - Good-Bye weakest links!!

### Frames 137-149 - Animation: the clones walk off (auto-play)

*(no dialogue - continuous animation)*

### Frames 150-176 - ROUND 4 - Q&A, voting, Terra and Black Mage eliminated

- **f150** - *[held panel, no new text]*
- **f151** - Poor unanimated fools.
- **f152** - Time for round four.
- **f153** - *[held panel, no new text]*
- **f154** - Crono, what is written on a fresh sheet of paper?
- **f155** - *[Crono answers - empty speech balloon]*
- **f156** - Correct. Edgar who is your brother?
- **f157** - Sabin.
- **f158** - Correct.
- **f159** - Black Mage, what video game is said to be a clone of the original Final Fantasy?
- **f160** - Final Fantasy IX.
- **f161** - Correct.
- **f162** - Terra, who is the king of the dragons?
- **f163** - Odin.
- **f164** - *[held panel, no new text]*
- **f165** - Incorrect. Time to vote again.
- **f166** - *[held panel, no new text]*
- **f167** - *[held panel, no new text]*
- **f168** - Crono, he hasn't even opened his mouth for god's sake.
- **f169** - Terra, she doen't know the meaning of black magic.
- **f170** - Black Mage, yes I do you ancient fool!
- **f171** - Hmmmmm....
- **f172** - Those of you who use powerful magic, say I!
- **f173** - I!!
- **f174** - Being an evil villian and the host of the show, I declare you both the weakest links!!
- **f175** - Bwahahahahahaha!
- **f176** - This game sucks anyway. I'm outta here.

### Frames 177-206 - Animation: exits, Black Mage lines up his spell (auto-play)

*(no dialogue - continuous animation)*

### Frames 207-235 - FINAL ROUND - Crono vs Edgar

- **f207** - *[held panel, no new text]*
- **f208** - Take this evil villian!!
- **f209** - FIR2!!
- **f210** - Oooo... I feel slightly warmer than usual.
- **f211** - I will leave, but I shall return later with my cousin Vivi. Muwahahahaha!
- **f212** - POOF!
- **f213** - *[held panel, no new text]*
- **f214** - *[held panel, no new text]*
- **f215** - Time for the Final round. Who ever answers the most out of 5 questions correctly wins.
- **f216** - Crono, if you speak in space, what will people hear?
- **f217** - *[Crono answers - empty speech balloon]*
- **f218** - Correct. Edgar, who was the most evil villian in the Final Fantasy Series?
- **f219** - Zeromus.
- **f220** - *[held panel, no new text]*
- **f221** - Incorrect, it was me! Crono, what does dead silence sound like?
- **f222** - *[Crono answers - empty speech balloon]*
- **f223** - Correct. Edgar, what was the most pathetic enemy in the Final Fantasy Series?
- **f224** - Imps.
- **f225** - Correct. Crono, what did Robo say when he was powered off.
- **f226** - *[Crono answers - empty speech balloon]*
- **f227** - Correct. Edgar, in what video game can you find the character Quina?
- **f228** - Chrono Trigger
- **f229** - Incorrect. Crono, what did you say to Marle at the very end of Chrono Trigger?
- **f230** - *[Crono answers - empty speech balloon]*
- **f231** - Correct! Crono, you have answered 4 out of 5 questions correctly, and Edgar has missed 2 questions....
- **f232** - Crono is the newest winner on Square's Weakest Link!!
- **f233** - I object! He never answered a single question the entire time he was here!!
- **f234** - He he. That's too bad Eddie! Muwahahahahaha!
- **f235** - Well, all my loyal subjects. Good-bye until next time.

### Frames 236-267 - Animation: transition to credits (morph shape, auto-play)

*(no dialogue - continuous animation)*

### Frames 268-273 - CREDITS

- **f268** - Square's Weakest Link was written, produced, directed & animated by:
- **f268** - Kristopher M. Toops
- **f269** - Music for Square's Weakest Link was written and provided by:
- **f269** - Shawn Overn
- **f270** - Special thanks to the following individuals who noticed typos and saved me from looking like an idiot: Lord Clodelroth Comet Green
- **f271** - All characters that appeared in Square's Weakest Link are property of Squaresoft Inc., and were not harmed in any way shape or form during the production of Square's Weakest Link.
- **f272** - THE END. MMI -aka 2001 kmt.
- **f273** - *[held panel, no new text]*

---

## 8. Hidden and never-visible content

Swept four ways. Three came back clean; the fourth found something.

### Clean: no payload, no metadata, no deleted script

* **No container slack.** The last root tag (`End`) ends at byte 1,101,766 --
  exactly the declared file length. Zero trailing bytes inside the SWF, and
  exactly 8 bytes (the footer) after it in the executable. Nothing is hiding in
  the gaps because there are no gaps.
* **No appended data in the images.** Every `DefineBitsLossless2` zlib stream
  was decompressed and checked for `unused_data`. All 41 came back with zero
  trailing bytes.
* **No author metadata.** The SWF contains no `.fla` name, no `C:\` path, no
  URL and no email address. Flash 5 embedded none of that, and the author added
  none. The credits are the only place his name appears anywhere in the file.
  There is exactly one tool fingerprint in the whole movie, and it is not
  Flash's: both music MP3s end with `LAME3.84 (alpha 1)`, stamped by the
  encoder the author ran the audio through. It is useful for dating (section 2)
  but says nothing about who he was.
* **No deleted dialogue.** Both fonts were checked glyph by glyph against every
  `DefineText`: **all 67 glyphs of font 21 and all 4 of font 391 are actually
  used**. If a cut line had contained a character used nowhere else, an orphan
  glyph would have survived in the font. None did.
* **No orphaned characters.** All 446 defined character ids are referenced.
* **No hidden text or layers.** Every one of the 136 text objects sits fully
  inside the visible stage, and no placement carries a color transform,
  instance name, clip depth or clip actions.

### Found: a large off-stage parking lot

The stage is 550 x 335 px, but the authored canvas is wider. **302 bitmap
rectangles are placed entirely outside it** and were never rendered to screen
by any Flash player.

**1. Pose palettes.** The author kept a vertical stack of alternate poses just
outside each edge and dragged the one he wanted into frame for each panel,
leaving the rest parked. Every composite scene shape carries the whole stack:

| bitmap | who | on-stage | **off-stage** | parked at |
|---:|---|---:|---:|---|
| 68 | Kefka | 7 | **98** | x 568..621, right |
| 29 | Kefka | 58 | **78** | x 565..622, right |
| 171 | Edgar | 1 | **72** | x -57..-20, left |
| 212 | Edgar | 1 | **57** | x -63..-21, left |
| 239 | Edgar | 4 | **47** | x -62..-21, left |
| 66 | Kefka | 14 | **22** | x 573..620, right |

Kefka's spares live off the right edge, Edgar's off the left, spanning nearly
the full height of the canvas.

**2. The eliminated contestants never actually leave.** The walk-off cutscenes
move each sprite out through the stage edge -- and then it simply stays there,
standing in the void, for the rest of the runtime:

| character | eliminated | parked from | position |
|---|---|---|---|
| Edward (FFIV) | Round 1, f26-29 | f42 | x -43..-11, off the left |
| Kain | Round 2, f81-83 | f102 | x -52..-21, off the left |
| Cecil | Round 3, f132-136 | f150 | x 574..608, off the right |
| Bartz | Round 3, f132-136 | f150 | x 612..646, off the right |
| Terra | Round 4, f174 | f177 | x -51..-21, off the left, through f212 |

**3. Two bitmaps were never once rendered to screen.** Bitmaps **79 and 81** --
a second Kefka walk-cycle pose pair -- have an on-stage placement count of
**zero** across all 273 frames.

They live in `DefineSprite` id 83, a working 4-frame, 2-pose walk cycle. It is
placed on the root twice, both times at 2.03x scale:

```
f29   depth 33   tx = 582.5 px    ->  occupies x 582.5 .. 615.0
f136  depth 24   tx = 570.0 px    ->  occupies x 570.0 .. 602.5
```

Shapes 80 and 82 are 16 x 24 px with their origin at the top left, so both
placements sit entirely past the 550 px right edge. And the second placement is
**never removed** -- so from f136 onward a fully animated Kefka keeps walking in
place, just off the right of frame, through the elimination of the clones, the
entire final round, the fade-out, the credit cards and the "THE END." card. He
is still going when the movie stops.

Those two PNGs effectively did not exist until this extraction.

---

## 9. Re-render and reconstruction

`render.py` replays the root display list frame by frame and rasterizes it with
a hand-written nonzero-winding scanline filler, sampling bitmap fills through
the inverted fill matrix. It renders text by pulling the actual glyph outlines
out of `DefineFont` rather than substituting a system font, so the output is
the movie's own typography.

Outputs:

* **`extract/frames/frame_001.png` ... `frame_273.png`** -- all 273 frames at
  stage size.
* **`extract/_overscan/`** -- the same 273 frames on a padded canvas, which is
  what makes the parked art in section 8 visible.
* **`extract/Weakest_Link_reconstruction.mp4`** -- 9 min 37 s, upscaled with
  nearest-neighbour, dialogue panels held proportional to their text length,
  cutscene runs played at the true 12 fps, theme music underneath.
* **`extract/viewer.html`** -- a click-to-advance frame viewer that mirrors the
  original projector's interaction.

**Known fidelity limits.** The stroke pass is disabled in the shipped renders:
a quad-per-segment stroke approximation produced stray construction lines on
some composite shapes, and the fills alone reproduce the art faithfully because
balloon outlines are carried by fill geometry in most shapes. Gradient fills
fall back to a solid end-stop color -- there are almost none here. Movieclip
playheads are advanced relative to their placement frame, so walk cycles are in
a plausible but not frame-authoritative phase. These are reconstruction
artifacts, not properties of the movie.

---

## 10. Credits and rights

From the movie's own end cards, verbatim:

> Square's Weakest Link was written, produced, directed & animated by:
> **Kristopher M. Toops**
>
> Music for Square's Weakest Link was written and provided by:
> **Shawn Overn**
>
> Special thanks to the following individuals who noticed typos and saved me
> from looking like an idiot: **Lord Clodelroth**, **Comet Green**
>
> All characters that appeared in Square's Weakest Link are property of
> Squaresoft Inc., and were not harmed in any way shape or form during the
> production of Square's Weakest Link.
>
> THE END. `MMI -aka 2001 kmt.`

The sprite art is ripped from Squaresoft games (Final Fantasy IV, V and VI,
Chrono Trigger) and the backdrop from the Final Fantasy VI opera house. Those
assets remain the property of their rights holder; they are catalogd here for
preservation and study, exactly as the movie's own disclaimer anticipates. The
player stub is Macromedia's. The movie itself is Kristopher M. Toops's work and
the music is Shawn Overn's.

Takedown requests from any rights holder named here are welcome.

---

## 11. How to reproduce this

The SWF format is public and simple. Everything below is derived from the file
format spec -- no third-party SWF tooling (`ffdec`, `swfmill`, `ruffle`) was
used at any point, which is what makes the toolchain portable to any other
Flash-era projector.

### 11.1 The container

Search backwards for `56 34 12 FA`; the little-endian `u32` after it is the
movie length; the movie starts that many bytes earlier. Sanity-check for
`FWS`/`CWS`/`ZWS` at that offset and confirm the byte after the signature is a
plausible SWF version. `CWS` is zlib from byte 8; `ZWS` is LZMA. Normalize to
`FWS` before anything else.

### 11.2 The tag stream

Header is `signature[3], u8 version, u32 fileLength`, then a bit-packed `RECT`
for the stage, `u16` frame rate as 8.8 fixed point, and `u16` frame count.
Then tags: `u16 (code << 6) | length`, and if `length == 0x3F` a `u32` long
length follows.

Two traps, both of which produce plausible-looking wrong answers rather than
crashes:

* **Bit-field alignment.** After a bit-packed structure like `RECT` you must
  advance to the next whole byte. A reader that only zeroes its bit pointer
  without incrementing the byte offset reads the frame rate from the wrong
  place -- the symptom is `fps = 0` and a nonsense frame count, with the tag
  stream then decoding as garbage.
* **`DefineSprite` is recursive.** Its body is `u16 id, u16 frameCount` followed
  by a nested tag stream. Walk into it, and keep a depth marker so you can tell
  root-timeline tags from movieclip tags later.

### 11.3 Static text is the hard part

All the dialogue is `DefineText`, which stores **glyph indices, not
characters**. To recover the script:

1. Parse `DefineFont` to learn the glyph count from the first entry of its
   offset table (`offset[0] / 2 == number of glyphs`).
2. Parse `DefineFontInfo` for the code table that maps glyph index -> character
   code. The wide-codes flag in its bit field decides `u8` vs `u16` entries.
3. Walk the `DefineText` records. Each carries `glyphBits` and `advanceBits`
   read as raw bit fields, and a chain of style-change records that set font,
   size, color and position. Map each glyph index through the font's code
   table.

Nothing readable exists in the file until step 3 lands. That is the whole
reason these movies look opaque.

### 11.4 Shapes and the display list

Shape records are a bit-packed edge list with two fill-style slots per edge
(left and right). To fill correctly, collect edges per fill index, **reverse
the ones referenced by the left slot**, chain them end-to-start into closed
loops, and fill with the nonzero winding rule.

The one that bites: `DefineShape2` and `DefineShape3` can emit a
`StateNewStyles` record mid-shape that appends *new* fill and line arrays and
restarts style numbering at 1 relative to them. If you treat those indices as
absolute you get the right geometry with the wrong colors on part of the shape.

For bitmap fills, the fill matrix maps bitmap-pixel space to shape twip space,
so a 1:1 bitmap has a scale of about 20. Compose it with the placement matrix,
invert, and sample. Using it in the wrong direction produces a recognisable
failure: the image appears as a handful of enormous colored blocks.

Reproducing a frame means simulating the display list -- `PlaceObject2` with its
move flag, character id, matrix and depth; `RemoveObject2` by depth; render in
ascending depth order on `ShowFrame`. Movieclip playheads advance independently
of the root, so a clip placed at frame P shows its frame `((N - P) mod
frameCount) + 1` at root frame N.

### 11.5 Flash ADPCM

Not IMA ADPCM, though it is close. The `DefineSound` payload begins with
`u16 sampleCount`-adjacent header bytes, then: 2 bits of code size (giving
2, 3, 4 or 5-bit codes), then blocks of 4096 samples. Each block starts with a
signed 16-bit initial sample and a 6-bit step index, then 4095 codes. The
reconstruction is the standard IMA step-table walk, with the index-adjustment
table selected by code width.

The self-check is exact: decoded sample count must equal the count declared in
the tag. If it does not, the code width or block length is wrong.

### 11.6 Finding the hidden content

This is the part worth doing deliberately, because it is where these files
reward you:

1. **Orphan scan.** Collect every defined character id and every reference to
   one, and diff.
2. **Off-stage scan.** Transform each shape's bitmap-fill rectangles by the
   placement matrix and compare against the stage rect. Anything wholly outside
   is content no viewer ever saw.
3. **Visibility ledger.** For each bitmap, count placements that land inside
   the stage versus outside. A bitmap with an on-stage count of zero was never
   displayed at all.
4. **Byte sweep.** Check for slack after the last tag, trailing bytes after
   each zlib stream, and any human-written strings.
5. **Glyph sweep.** Compare embedded glyphs against glyphs actually typed;
   leftovers are evidence of cut text.
6. **Overscan render.** Re-render every frame on a padded canvas. This is the
   step that turns the numbers into something you can look at.

### 11.7 A prompt for doing this yourself

Hand an agent the file and something like this:

> I have a Windows executable that is a Macromedia Flash projector -- the Flash
> player with a SWF appended. I want a complete static teardown: every bitmap,
> sound, shape and piece of text decoded to modern formats, the timeline
> explained, and every frame re-rendered. Do not run the program. Write the
> parsers yourself from the SWF specification; do not use ffdec, swfmill or
> ruffle.
>
> Work in this order and verify each step before moving on:
>
> 1. **Container.** Search backwards for the footer magic `56 34 12 FA`; the
>    little-endian u32 after it is the movie length and the movie starts that
>    many bytes earlier. Confirm `FWS`/`CWS`/`ZWS` at that offset. Decompress
>    `CWS` (zlib from byte 8) or `ZWS` (LZMA) to a flat `FWS`. Also dump the PE
>    stub and read its version resource -- it tells you which Flash player, and
>    therefore roughly which era.
>
> 2. **Tag walk.** Header, then `u16 (code << 6) | len` with the `0x3F` long
>    form. Two things to get right: after any bit-packed field such as `RECT`
>    you must advance to the next whole byte, not merely reset the bit pointer;
>    and `DefineSprite` contains a nested tag stream you must recurse into.
>    Verify by checking that the tag chain ends exactly on the declared file
>    length, and print a histogram of tag types before going further.
>
> 3. **Text.** All dialogue is likely static `DefineText`, which stores glyph
>    indices rather than characters, so a strings dump will find nothing. Read
>    `DefineFont` for the glyph count and `DefineFontInfo` for the glyph-index
>    to character-code table, then decode each `DefineText` record's bit-packed
>    glyph/advance pairs through it. This is the step that recovers the script.
>
> 4. **Images and sound.** `DefineBitsLossless/2` are zlib bitmaps -- handle
>    palette format 3 (note the row stride is padded to a multiple of 4) and
>    direct format 5 (ARGB, premultiplied when it has alpha). `DefineSound`
>    is raw PCM, MP3 (skip the 2-byte seek field) or Flash ADPCM. If ADPCM,
>    write the decoder: 2-bit code size selector, then 4096-sample blocks each
>    starting with a signed 16-bit sample and a 6-bit step index. Check that
>    decoded sample counts match the declared counts exactly.
>
> 5. **Shapes and display list.** Decode shape records into per-fill edge sets,
>    reversing edges referenced by the left-fill slot, chain into closed loops
>    and fill nonzero. Watch for mid-shape `StateNewStyles`, which restarts
>    style numbering against a newly appended style array. Then simulate the
>    display list per frame and render, remembering that movieclip playheads
>    run independently of the root timeline.
>
> 6. **Behavior.** Disassemble every `DoAction` and button handler. Report what
>    the movie actually does -- if it is all `stop()` and one `play()` button,
>    say so plainly rather than implying complexity that is not there.
>
> Then find what the movie never showed anyone. Diff defined character ids
> against referenced ones; transform every placement into stage coordinates and
> flag anything wholly outside the stage rect; build a per-asset ledger of
> on-stage versus off-stage placements and call out any asset that was never
> displayed at all; check for bytes after the last tag, trailing bytes after
> each zlib stream, and any human-written strings; and compare embedded font
> glyphs against glyphs actually used, since leftovers mean text was cut.
> Finally, re-render every frame on an oversized canvas so the cropped-away
> content is visible.
>
> Report the off-stage and never-displayed findings as a first-class result,
> not a footnote. In files like this they are usually the most interesting
> thing present.

---

## 12. The toolchain

Python 3, plus Pillow and numpy. ffmpeg is optional and only used for the
reconstruction video. Nothing else.

### 12.1 Layout and what gets produced

Drop the projector in a folder with `tools/` beside it and run one command.
Nothing is hard-coded -- the exe can have any name, and the folder can be
anywhere:

```
<any folder>/
+-- YourMovie.exe                  the Flash projector you start with
+-- tools/                         this toolchain
+-- extract/                       created on the first run; everything lands here
    +-- YourMovie.swf              the movie, playable in Ruffle
    +-- projector_stub.bin         the Flash player the author shipped with
    |
    +-- images/                    one PNG per bitmap
    |   +-- bmp_<id>_<w>x<h>.png
    +-- sounds/                    one file per DefineSound
    |   +-- snd_<id>.mp3           MP3 sounds, extracted as-is
    |   +-- snd_<id>.wav           PCM and decoded-ADPCM sounds
    |   +-- snd_<id>.adpcm         the raw ADPCM payload, kept alongside
    +-- shapes/                    one SVG per DefineShape*
    |   +-- shape_<id>.svg
    |   +-- _index.json            bounds, fill/line counts, bitmap refs
    +-- frames/                    the movie re-rendered, one PNG per frame
    |   +-- frame_<nnn>.png
    +-- _overscan/                 the same frames on a padded canvas, so
    |   +-- frame_<nnn>.png        off-stage content is visible
    |
    +-- timeline.txt               frame-by-frame display list and sound cues
    +-- actionscript_disasm.txt    every DoAction and button handler
    +-- text_dump.txt              every text record, readable
    +-- text_records.json          every text record, structured
    +-- assets_manifest.json       bitmap and sound metadata
    +-- sprites.json               movieclips and their bitmap sequences
    +-- _contact_sheet.png         all bitmaps on one page
```

```
python3 tools/run_all.py            # everything
python3 tools/run_all.py --quick    # skip the two full-movie frame renders
```

The analysis scripts print their findings to stdout; redirect them if you want
them on disk.

Three more artifacts are per-movie rather than automatic, because they need
decisions a parser cannot make -- where the chapters begin, how long a panel of
dialogue should be held, which frames best illustrate a finding:

```
    +-- SCRIPT.md                  the transcript          (mkscript.py)
    +-- _concat.txt                ffmpeg timing list      (mkconcat.py)
    +-- <name>_reconstruction.mp4  the movie as video      (ffmpeg + _concat.txt)
    +-- _overscan/_hidden_cast.png the off-stage figure    (compare.py)
    +-- viewer.html                click-to-advance viewer (hand-written)
```

### 12.2 Dependency order

```
swlpaths.py           path resolution                     (no dependencies)
unpack_projector.py   exe -> stub + movie                 (swlpaths)
swfparse.py           header + recursive tag walker       (no dependencies)
timeline.py           annotated display-list dump         (swfparse)
extract_assets.py     bitmaps -> PNG, sounds -> MP3/WAV   (swfparse)
adpcm.py              Flash ADPCM -> PCM WAV              (standalone)
textdump.py           DefineText + fonts -> strings       (swfparse)
asdis.py              ActionScript disassembler           (swfparse)
shapes.py             DefineShape* -> SVG                 (swfparse)
raster.py             nonzero-winding scanline filler     (numpy)
render.py             display-list replay -> PNG          (swfparse, shapes, raster)
sprites.py            movieclip -> bitmap sequence map    (swfparse)
orphans.py            unreferenced + off-stage scan       (swfparse, shapes)
hidden.py             slack, zlib tails, strings, glyphs  (swfparse, shapes)
hidden2.py            off-stage text scan                 (swfparse, shapes)
hidden3.py            per-bitmap visibility ledger        (swfparse, shapes)
sheet.py              bitmap contact sheet                (Pillow)
mkscript.py           SCRIPT.md                           (swfparse)      *
mkconcat.py           ffmpeg timing list                  (swfparse)      *
compare.py            the hidden-cast figure              (Pillow)        *
run_all.py            the driver
build_readme.py       this document
```

`*` marks the three scripts with movie-specific constants in them -- section
boundaries, chosen frame numbers, panel timing. Everything else is generic.

### 12.3 `swlpaths.py` -- where things live

```python
"""Shared path resolution for the Flash-projector teardown toolchain.

Layout this assumes -- drop the projector anywhere and put `tools/` beside it:

    <any folder>/
      YourMovie.exe        the Flash projector (or a bare .swf)
      tools/               these scripts
      extract/             created on first run; everything lands here

Nothing else is hard-coded. Override the base folder with the SWL_ROOT
environment variable if you want the output somewhere else.
"""
import os
import sys
import glob

HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
    sys.path.insert(0, HERE)

BASE = os.environ.get('SWL_ROOT') or os.path.dirname(HERE)
OUT = os.path.join(BASE, 'extract')


def _one(pattern, what):
    hits = sorted(glob.glob(os.path.join(BASE, pattern)))
    if not hits:
        return None
    if len(hits) > 1:
        sys.stderr.write('warning: %d %s found in %s, using %s\n'
                         % (len(hits), what, BASE, os.path.basename(hits[0])))
    return hits[0]


EXE = _one('*.exe', 'executables')

# The extracted movie is named after the projector; if the input was already a
# bare .swf, use it where it sits.
if EXE:
    STEM = os.path.splitext(os.path.basename(EXE))[0]
    SWF = os.path.join(OUT, STEM + '.swf')
else:
    _swf = _one('*.swf', 'movies')
    STEM = os.path.splitext(os.path.basename(_swf))[0] if _swf else 'movie'
    SWF = _swf or os.path.join(OUT, 'movie.swf')


def out(*parts):
    """Path inside the extract folder, creating parent directories."""
    p = os.path.join(OUT, *parts)
    d = p if not os.path.splitext(p)[1] else os.path.dirname(p)
    os.makedirs(d, exist_ok=True)
    return p


os.makedirs(OUT, exist_ok=True)
```

### 12.4 `unpack_projector.py` -- the container

```python
"""Step 1: split a Flash projector .exe into the player stub and the movie.

A projector is the stock Macromedia/Adobe player executable with the SWF
appended and an 8-byte footer:

    [ PE executable: the Flash player stub ]
    [ SWF movie: FWS / CWS / ZWS ...       ]
    [ 56 34 12 FA | u32 LE movie length    ]   <- magic 0xFA123456

Writes  extract/<name>.swf  and  extract/projector_stub.bin, and prints the
PE header facts and version resource that identify which player was used.

Also handles a bare .swf input (nothing to split) and CWS/ZWS compressed
movies, which are decompressed to an equivalent FWS so every later script can
assume a flat, uncompressed tag stream.
"""
import os
import re
import struct
import sys
import zlib
import hashlib
import datetime

from swlpaths import BASE, OUT, EXE, SWF, STEM

FOOTER = b'\x56\x34\x12\xFA'


def find_movie(data):
    """Return (offset, length) of the appended SWF, or None."""
    i = data.rfind(FOOTER)
    if i >= 0 and i + 8 <= len(data):
        size = struct.unpack_from('<I', data, i + 4)[0]
        start = i - size
        if 0 < start < len(data) and data[start:start + 3] in (b'FWS', b'CWS', b'ZWS'):
            return start, size
    # fall back to scanning for a plausible signature
    for sig in (b'FWS', b'CWS', b'ZWS'):
        for m in re.finditer(re.escape(sig), data):
            o = m.start()
            if o + 8 > len(data):
                continue
            ver = data[o + 3]
            ln = struct.unpack_from('<I', data, o + 4)[0]
            if 1 <= ver <= 40 and 64 < ln <= len(data) - o:
                return o, ln
    return None


def decompress(movie):
    """CWS (zlib) / ZWS (LZMA) -> an equivalent uncompressed FWS."""
    sig = movie[:3]
    if sig == b'FWS':
        return movie, 'uncompressed'
    head = b'FWS' + movie[3:8]
    if sig == b'CWS':
        body = zlib.decompress(movie[8:])
    elif sig == b'ZWS':
        import lzma
        props = movie[12:17]
        dec = lzma.LZMADecompressor(format=lzma.FORMAT_RAW,
                                    filters=[lzma._decode_filter_properties(lzma.FILTER_LZMA1, props)])
        body = dec.decompress(movie[17:])
    else:
        raise ValueError('not a SWF: %r' % sig)
    out = head + body
    out = out[:4] + struct.pack('<I', len(out)) + out[8:]
    return out, sig.decode()


def pe_facts(stub):
    if stub[:2] != b'MZ':
        return
    pe = struct.unpack_from('<I', stub, 0x3c)[0]
    if stub[pe:pe + 4] != b'PE\0\0':
        return
    mach, nsec = struct.unpack_from('<HH', stub, pe + 4)
    ts = struct.unpack_from('<I', stub, pe + 8)[0]
    when = datetime.datetime.fromtimestamp(ts, datetime.timezone.utc)
    print('  PE machine 0x%04X, %d sections, timestamp %s UTC'
          % (mach, nsec, when.strftime('%Y-%m-%d %H:%M:%S')))
    opt = pe + 24
    print('  linker %d.%d, subsystem %d'
          % (stub[opt + 2], stub[opt + 3], struct.unpack_from('<H', stub, opt + 68)[0]))
    secoff = pe + 24 + struct.unpack_from('<H', stub, pe + 20)[0]
    for i in range(nsec):
        o = secoff + i * 40
        name = stub[o:o + 8].rstrip(b'\0').decode('latin-1')
        vs, va, rs, ro = struct.unpack_from('<IIII', stub, o + 8)
        print('    %-8s vsize=%-8d vaddr=0x%-6X rawsize=%-8d rawoff=0x%X'
              % (name, vs, va, rs, ro))
    for key in ('ProductName', 'FileDescription', 'FileVersion', 'CompanyName',
                'OriginalFilename', 'LegalCopyright', 'InternalName'):
        i = stub.find(key.encode('utf-16-le'))
        if i < 0:
            continue
        # VS_VERSION_INFO String: wszKey\0 [pad to 4] wszValue\0
        j = i + len(key) * 2
        while j + 1 < len(stub) and stub[j:j + 2] == b'\0\0':
            j += 2
        k = j
        while k + 1 < len(stub) and stub[k:k + 2] != b'\0\0':
            k += 2
        val = stub[j:k].decode('utf-16-le', 'replace')
        val = ''.join(c if 32 <= ord(c) < 127 else '?' for c in val).strip()
        print('  %-17s %s' % (key + ':', val))


def main():
    if EXE is None:
        print('No .exe in %s -- treating input as a bare movie.' % BASE)
        movie = open(SWF, 'rb').read()
        src = SWF
    else:
        src = EXE
        data = open(src, 'rb').read()
        print('%s: %d bytes' % (os.path.basename(src), len(data)))
        print('  md5  %s' % hashlib.md5(data).hexdigest())
        print('  sha1 %s' % hashlib.sha1(data).hexdigest())
        hit = find_movie(data)
        if not hit:
            sys.exit('no embedded SWF found')
        off, size = hit
        movie = data[off:off + size]
        stub = data[:off]
        trailing = len(data) - off - size
        print('  movie at offset %d (0x%X), %d bytes; stub %d bytes; %d trailing bytes'
              % (off, off, size, len(stub), trailing))
        if trailing not in (0, 8):
            print('  NOTE: %d trailing bytes is unusual -- inspect them' % trailing)
        open(os.path.join(OUT, 'projector_stub.bin'), 'wb').write(stub)
        print('\nPlayer stub:')
        pe_facts(stub)

    flat, how = decompress(movie)
    dest = os.path.join(OUT, STEM + '.swf')
    open(dest, 'wb').write(flat)
    print('\nmovie: %s (%s), %d bytes, md5 %s'
          % (os.path.basename(dest), how, len(flat), hashlib.md5(flat).hexdigest()))
    ver = flat[3]
    rect_nb = flat[8] >> 3
    print('  SWF version %d, declared length %d, actual %d'
          % (ver, struct.unpack_from('<I', flat, 4)[0], len(flat)))


if __name__ == '__main__':
    main()
```

### 12.5 `swfparse.py` -- header and tag walker

The spine. Note `align()`, which advances to the next byte boundary rather than
merely clearing the bit pointer -- see section 11.2.

```python
import struct, sys, os, json, collections

TAGS={0:'End',1:'ShowFrame',2:'DefineShape',4:'PlaceObject',5:'RemoveObject',6:'DefineBits',7:'DefineButton',8:'JPEGTables',9:'SetBackgroundColor',10:'DefineFont',11:'DefineText',12:'DoAction',13:'DefineFontInfo',14:'DefineSound',15:'StartSound',17:'DefineButtonSound',18:'SoundStreamHead',19:'SoundStreamBlock',20:'DefineBitsLossless',21:'DefineBitsJPEG2',22:'DefineShape2',23:'DefineButtonCxform',24:'Protect',26:'PlaceObject2',28:'RemoveObject2',32:'DefineShape3',33:'DefineText2',34:'DefineButton2',35:'DefineBitsJPEG3',36:'DefineBitsLossless2',37:'DefineEditText',39:'DefineSprite',40:'NameCharacter',41:'ProductInfo',43:'FrameLabel',45:'SoundStreamHead2',46:'DefineMorphShape',48:'DefineFont2',56:'ExportAssets',57:'ImportAssets',58:'EnableDebugger',59:'DoInitAction',60:'DefineVideoStream',61:'VideoFrame',62:'DefineFontInfo2',64:'EnableDebugger2',65:'ScriptLimits',66:'SetTabIndex',69:'FileAttributes',70:'PlaceObject3',71:'ImportAssets2',73:'DefineFontAlignZones',74:'CSMTextSettings',75:'DefineFont3',76:'SymbolClass',77:'Metadata',78:'DefineScalingGrid',82:'DoABC',83:'DefineShape4',84:'DefineMorphShape2',86:'DefineSceneAndFrameLabelData',87:'DefineBinaryData',88:'DefineFontName',89:'StartSound2',90:'DefineBitsJPEG4',91:'DefineFont4'}

class R:
    def __init__(s,d,o=0): s.d=d; s.o=o; s.bp=0
    def u8(s):
        v=s.d[s.o]; s.o+=1; return v
    def u16(s):
        v=struct.unpack_from('<H',s.d,s.o)[0]; s.o+=2; return v
    def s16(s):
        v=struct.unpack_from('<h',s.d,s.o)[0]; s.o+=2; return v
    def u32(s):
        v=struct.unpack_from('<I',s.d,s.o)[0]; s.o+=4; return v
    def align(s):
        if s.bp: s.bp=0; s.o+=1
    def bits(s,n):
        v=0
        for _ in range(n):
            byte=s.d[s.o]
            bit=(byte>>(7-s.bp))&1
            v=(v<<1)|bit
            s.bp+=1
            if s.bp==8: s.bp=0; s.o+=1
        return v
    def sbits(s,n):
        v=s.bits(n)
        if n and (v>>(n-1))&1: v-= (1<<n)
        return v
    def rect(s):
        s.align()
        nb=s.bits(5)
        r=[s.sbits(nb) for _ in range(4)]
        s.align()
        return r
    def strz(s):
        e=s.d.index(b'\0',s.o)
        v=s.d[s.o:e]; s.o=e+1; return v

def parse(path):
    d=open(path,'rb').read()
    assert d[:3]==b'FWS'
    ver=d[3]; flen=struct.unpack_from('<I',d,4)[0]
    r=R(d,8)
    rect=r.rect()
    fps=r.u16()/256.0
    frames=r.u16()
    print(f"SWF ver={ver} filelen={flen} actual={len(d)}")
    print(f"stage twips={rect} px={[v/20 for v in rect]} size={(rect[1]-rect[0])/20}x{(rect[3]-rect[2])/20}")
    print(f"fps={fps} frames={frames}")
    tags=[]
    def read_tags(r, end, depth, parent):
        while r.o < end:
            off=r.o
            th=r.u16()
            code=th>>6; ln=th&0x3f
            if ln==0x3f: ln=r.u32()
            body=r.d[r.o:r.o+ln]
            tags.append(dict(off=off,code=code,name=TAGS.get(code,f'?{code}'),len=ln,depth=depth,parent=parent,body_off=r.o))
            if code==39: # DefineSprite
                sr=R(r.d,r.o)
                cid=sr.u16(); fc=sr.u16()
                tags[-1]['id']=cid; tags[-1]['frames']=fc
                read_tags(sr, r.o+ln, depth+1, cid)
            r.o += ln
            if code==0 and depth==0: break
    read_tags(r, len(d), 0, None)
    return d, tags, dict(ver=ver,rect=rect,fps=fps,frames=frames)

if __name__=='__main__':
    path=sys.argv[1]
    d,tags,hdr=parse(path)
    c=collections.Counter(t['name'] for t in tags)
    print("\n=== TAG HISTOGRAM ===")
    for k,v in c.most_common(): print(f"{v:6d}  {k}")
    print("\ntotal tags", len(tags))
    json.dump(tags, open(sys.argv[2],'w'))
```

### 12.6 `timeline.py` -- the annotated display list

```python
import os, struct, sys, json
from swfparse import parse, R
from swlpaths import OUT, SWF

SWF = SWF
d, tags, hdr = parse(SWF)

# ---- id -> kind index ----
kind = {}
for t in tags:
    n = t['name']
    b = d[t['body_off']:t['body_off'] + t['len']]
    if n.startswith('Define') and n not in ('DefineFontInfo',):
        try:
            cid = struct.unpack_from('<H', b, 0)[0]
        except Exception:
            continue
        kind[cid] = n

# text id -> text
try:
    txt = {r['id']: r['text'] for r in json.load(open(os.path.join(OUT, "text_records.json")))}
except Exception:
    txt = {}

def matrix(r):
    r.align()
    sx = sy = 1.0
    if r.bits(1):
        n = r.bits(5); sx = r.sbits(n) / 65536.0; sy = r.sbits(n) / 65536.0
    if r.bits(1):
        n = r.bits(5); r.sbits(n); r.sbits(n)
    n = r.bits(5)
    tx = r.sbits(n); ty = r.sbits(n)
    r.align()
    return sx, sy, tx / 20.0, ty / 20.0

lines = []
frame = 1
sprite_frames = {}
cur_depth = 0
for t in tags:
    n = t['name']
    b = d[t['body_off']:t['body_off'] + t['len']]
    inspr = t['depth'] > 0
    pre = ('    sprite%s f%d  ' % (t['parent'], sprite_frames.get(t['parent'], 1))) if inspr else ('FRAME %-4d ' % frame)
    if n == 'PlaceObject2':
        r = R(b, 0)
        fl = r.u8()
        depth = r.u16()
        cid = r.u16() if fl & 2 else None
        mtx = matrix(r) if fl & 4 else None
        extra = []
        if fl & 8: extra.append('cxform')
        if fl & 16: extra.append('ratio')
        name = None
        # skip cxform to reach name
        if fl & 1: extra.append('MOVE')
        k = kind.get(cid, '?') if cid is not None else ''
        info = ''
        if cid in txt:
            info = ' TEXT=%r' % (txt[cid][:60])
        pos = (' at (%.0f,%.0f) scale(%.2f,%.2f)' % (mtx[2], mtx[3], mtx[0], mtx[1])) if mtx else ''
        lines.append('%s place depth=%-4d id=%s %s%s%s %s' % (pre, depth, cid, k, pos, info, ' '.join(extra)))
    elif n == 'RemoveObject2':
        lines.append('%s remove depth=%d' % (pre, struct.unpack_from('<H', b, 0)[0]))
    elif n == 'StartSound':
        sid = struct.unpack_from('<H', b, 0)[0]
        flags = b[2]
        lines.append('%s >>> StartSound id=%d flags=0x%02X' % (pre, sid, flags))
    elif n == 'DoAction':
        lines.append('%s [action %d bytes]' % (pre, t['len']))
    elif n == 'SetBackgroundColor':
        lines.append('%s bgcolor #%02X%02X%02X' % (pre, b[0], b[1], b[2]))
    elif n == 'DefineSprite':
        lines.append('%s DefineSprite id=%s frames=%s' % (pre, t.get('id'), t.get('frames')))
    elif n == 'SoundStreamHead2':
        lines.append('%s SoundStreamHead2' % pre)
    elif n == 'ShowFrame':
        if inspr:
            sprite_frames[t['parent']] = sprite_frames.get(t['parent'], 1) + 1
        else:
            lines.append('--- end frame %d ---' % frame)
            frame += 1

open(os.path.join(OUT, "timeline.txt"), 'w', encoding='utf-8').write('\n'.join(lines))
print('timeline lines', len(lines))
print('char id kinds:', json.dumps({str(k): v for k, v in sorted(kind.items())}))
```

### 12.7 `extract_assets.py` -- bitmaps and sounds

```python
import os, struct, zlib, sys, json, collections
from swfparse import parse, R, TAGS
from swlpaths import OUT, SWF

d,tags,hdr=parse(SWF)
os.makedirs(os.path.join(OUT, "images"),exist_ok=True)
os.makedirs(os.path.join(OUT, "sounds"),exist_ok=True)

def png(w,h,rgba):
    def chunk(t,data):
        c=t+data
        return struct.pack('>I',len(data))+c+struct.pack('>I',zlib.crc32(c)&0xffffffff)
    raw=b''.join(b'\x00'+rgba[y*w*4:(y+1)*w*4] for y in range(h))
    return (b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>IIBBBBB',w,h,8,6,0,0,0))
            +chunk(b'IDAT',zlib.compress(raw,9))+chunk(b'IEND',b''))

manifest=[]
for t in tags:
    if t['name'] in ('DefineBitsLossless','DefineBitsLossless2'):
        b=d[t['body_off']:t['body_off']+t['len']]
        cid=struct.unpack_from('<H',b,0)[0]
        fmt=b[2]; w=struct.unpack_from('<H',b,3)[0]; h=struct.unpack_from('<H',b,5)[0]
        o=7; ncol=None
        if fmt==3: ncol=b[7]+1; o=8
        data=zlib.decompress(b[o:])
        alpha = t['name']=='DefineBitsLossless2'
        if fmt==3:
            psz = 4 if alpha else 3
            pal=data[:ncol*psz]
            idx=data[ncol*psz:]
            stride=(w+3)//4*4
            px=bytearray()
            for y in range(h):
                row=idx[y*stride:y*stride+w]
                for i in row:
                    p=pal[i*psz:i*psz+psz]
                    px+= bytes(p) if alpha else bytes(p)+b'\xff'
        elif fmt==5:
            px=bytearray()
            stride=w*4
            for y in range(h):
                row=data[y*stride:(y+1)*stride]
                for x in range(w):
                    a,r_,g,bl=row[x*4:x*4+4]
                    if alpha:
                        # premultiplied
                        if a: r_=min(255,r_*255//a); g=min(255,g*255//a); bl=min(255,bl*255//a)
                        px+=bytes((r_,g,bl,a))
                    else:
                        px+=bytes((r_,g,bl,255))
        elif fmt==4:
            px=bytearray(); stride=((w*2+3)//4)*4
            for y in range(h):
                row=data[y*stride:(y+1)*stride]
                for x in range(w):
                    v=struct.unpack_from('>H',row,x*2)[0]
                    r_=((v>>10)&31)*255//31; g=((v>>5)&31)*255//31; bl=(v&31)*255//31
                    px+=bytes((r_,g,bl,255))
        else:
            print("unk fmt",fmt); continue
        fn=os.path.join(OUT,"images",f"bmp_{cid:03d}_{w}x{h}.png")
        open(fn,'wb').write(png(w,h,bytes(px)))
        manifest.append(dict(kind='image',id=cid,w=w,h=h,fmt=fmt,alpha=alpha,file=os.path.basename(fn),tag_len=t['len']))

SR=[5512,11025,22050,44100]
FMT={0:'uncompressed_ne',1:'ADPCM',2:'MP3',3:'uncompressed_le',6:'Nellymoser'}
def wav(ch,sr,bits,data):
    ba=sr*ch*bits//8
    return (b'RIFF'+struct.pack('<I',36+len(data))+b'WAVEfmt '+struct.pack('<IHHIIHH',16,1,ch,sr,ba,ch*bits//8,bits)
            +b'data'+struct.pack('<I',len(data))+data)

for t in tags:
    if t['name']=='DefineSound':
        b=d[t['body_off']:t['body_off']+t['len']]
        cid=struct.unpack_from('<H',b,0)[0]
        f=b[2]
        sf=(f>>4)&0xf; rate=SR[(f>>2)&3]; size16=(f>>1)&1; stereo=f&1
        nsamp=struct.unpack_from('<I',b,3)[0]
        data=b[7:]
        ext='bin'; blob=data
        if sf==2:
            ext='mp3'; blob=data[2:]  # skip seek samples
        elif sf in (0,3):
            ext='wav'; blob=wav(stereo+1,rate,16 if size16 else 8,data)
        elif sf==1: ext='adpcm'
        fn=os.path.join(OUT,"sounds",f"snd_{cid:03d}.{ext}")
        open(fn,'wb').write(blob)
        manifest.append(dict(kind='sound',id=cid,codec=FMT.get(sf,sf),rate=rate,bits=16 if size16 else 8,
                             channels=stereo+1,samples=nsamp,dur_s=round(nsamp/rate,2),file=os.path.basename(fn),bytes=len(data)))
    if t['name']=='SoundStreamHead2':
        b=d[t['body_off']:t['body_off']+t['len']]
        pf=b[0]; sf=b[1]
        manifest.append(dict(kind='streamhead',parent=t['parent'],
            play_rate=SR[(pf>>2)&3], codec=FMT.get((sf>>4)&0xf,(sf>>4)&0xf), rate=SR[(sf>>2)&3],
            bits=16 if (sf>>1)&1 else 8, channels=(sf&1)+1,
            samples_per_frame=struct.unpack_from('<H',b,2)[0], off=t['off']))

json.dump(manifest, open(OUT+r"\assets_manifest.json",'w'), indent=1)
for m in manifest: print(m)
```

### 12.8 `adpcm.py` -- the Flash ADPCM decoder

```python
import os, struct, sys, glob
from swlpaths import OUT

INDEX_TABLE = {
    2: [-1, 2],
    3: [-1, -1, 2, 4],
    4: [-1, -1, -1, -1, 2, 4, 6, 8],
    5: [-1, -1, -1, -1, -1, -1, -1, -1, 1, 2, 4, 6, 8, 10, 13, 16],
}
STEP = [7,8,9,10,11,12,13,14,16,17,19,21,23,25,28,31,34,37,41,45,50,55,60,66,73,80,88,97,
107,118,130,143,157,173,190,209,230,253,279,307,337,371,408,449,494,544,598,658,724,796,
876,963,1060,1166,1282,1411,1552,1707,1878,2066,2272,2499,2749,3024,3327,3660,4026,4428,
4871,5358,5894,6484,7132,7845,8630,9493,10442,11487,12635,13899,15289,16818,18500,20350,
22385,24623,27086,29794,32767]


class BR:
    def __init__(s, d):
        s.d = d; s.o = 0; s.bp = 0
    def bits(s, n):
        v = 0
        for _ in range(n):
            if s.o >= len(s.d):
                raise EOFError
            v = (v << 1) | ((s.d[s.o] >> (7 - s.bp)) & 1)
            s.bp += 1
            if s.bp == 8:
                s.bp = 0; s.o += 1
        return v
    def sbits(s, n):
        v = s.bits(n)
        if v >> (n - 1):
            v -= 1 << n
        return v


def decode(data, channels, nsamples):
    r = BR(data)
    nbits = r.bits(2) + 2
    idxtab = INDEX_TABLE[nbits]
    out = [[] for _ in range(channels)]
    total = 0
    try:
        while total < nsamples:
            samp = []; idx = []
            for c in range(channels):
                samp.append(r.sbits(16))
                idx.append(r.bits(6))
                out[c].append(samp[c])
            total += 1
            for _ in range(4095):
                if total >= nsamples:
                    break
                for c in range(channels):
                    code = r.bits(nbits)
                    step = STEP[idx[c]]
                    sign = code >> (nbits - 1)
                    mag = code & ((1 << (nbits - 1)) - 1)
                    diff = step >> (nbits - 1)
                    for b in range(nbits - 1):
                        if mag & (1 << b):
                            diff += step >> (nbits - 2 - b)
                    samp[c] += -diff if sign else diff
                    samp[c] = max(-32768, min(32767, samp[c]))
                    idx[c] = max(0, min(88, idx[c] + idxtab[mag]))
                    out[c].append(samp[c])
                total += 1
    except EOFError:
        pass
    return out


def wav(ch, sr, chans):
    n = min(len(c) for c in chans)
    pcm = bytearray()
    for i in range(n):
        for c in chans:
            pcm += struct.pack('<h', c[i])
    ba = sr * ch * 2
    return (b'RIFF' + struct.pack('<I', 36 + len(pcm)) + b'WAVEfmt ' +
            struct.pack('<IHHIIHH', 16, 1, ch, sr, ba, ch * 2, 16) +
            b'data' + struct.pack('<I', len(pcm)) + bytes(pcm))


if __name__ == '__main__':
    import json
    SNDDIR = os.path.join(OUT, "sounds")
    man = json.load(open(os.path.join(OUT, "assets_manifest.json")))
    for m in man:
        if m.get('kind') == 'sound' and m.get('codec') == 'ADPCM':
            p = os.path.join(SNDDIR, m['file'])
            data = open(p, 'rb').read()
            chans = decode(data, m['channels'], m['samples'])
            outp = p.replace('.adpcm', '.wav')
            open(outp, 'wb').write(wav(m['channels'], m['rate'], chans))
            print('decoded', m['file'], '->', os.path.basename(outp), 'samples', len(chans[0]), '/', m['samples'])
```

### 12.9 `textdump.py` -- recovering the script

```python
import os, struct, sys, json
from swfparse import parse, R
from swlpaths import OUT, SWF

SWF = SWF
d, tags, hdr = parse(SWF)

fonts = {}   # fontid -> {'nglyphs':n, 'name':..., 'map': [codes]}

for t in tags:
    if t['name'] == 'DefineFont':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fid = struct.unpack_from('<H', b, 0)[0]
        off0 = struct.unpack_from('<H', b, 2)[0]
        n = off0 // 2
        fonts.setdefault(fid, {})['nglyphs'] = n
    if t['name'] == 'DefineFontInfo':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fid = struct.unpack_from('<H', b, 0)[0]
        nl = b[2]
        name = b[3:3+nl].decode('latin-1')
        flags = b[3+nl]
        wide = flags & 1
        i = 4 + nl
        codes = []
        if wide:
            while i + 1 < len(b):
                codes.append(struct.unpack_from('<H', b, i)[0]); i += 2
        else:
            while i < len(b):
                codes.append(b[i]); i += 1
        f = fonts.setdefault(fid, {})
        f['name'] = name
        f['flags'] = flags
        f['map'] = codes

for fid, f in fonts.items():
    print('font', fid, f.get('name'), 'glyphs', f.get('nglyphs'), 'codes', len(f.get('map', [])),
          'flags=0x%02X' % f.get('flags', 0))
    if f.get('map'):
        print('   charset:', ''.join(chr(c) for c in f['map']))

# ---- DefineText / DefineText2 ----
def read_text(b, ver):
    r = R(b, 0)
    cid = r.u16()
    bounds = r.rect()
    # matrix
    r.align()
    def matrix(r):
        r.align()
        if r.bits(1):
            n = r.bits(5); r.sbits(n); r.sbits(n)
        if r.bits(1):
            n = r.bits(5); r.sbits(n); r.sbits(n)
        n = r.bits(5)
        tx = r.sbits(n); ty = r.sbits(n)
        r.align()
        return tx, ty
    tx, ty = matrix(r)
    nglyph = r.u8()
    nadv = r.u8()
    out = []
    curfont = None
    size = None
    color = None
    while True:
        flags = r.u8()
        if flags == 0:
            break
        if flags & 0x80:  # text record type 1
            hasfont = flags & 8
            hascolor = flags & 4
            hasy = flags & 2
            hasx = flags & 1
            if hasfont:
                curfont = r.u16()
            if hascolor:
                if ver == 2:
                    color = (r.u8(), r.u8(), r.u8(), r.u8())
                else:
                    color = (r.u8(), r.u8(), r.u8(), 255)
            if hasx: r.s16()
            if hasy: r.s16()
            if hasfont: size = r.u16()
        n = r.u8()
        s = ''
        r.align()
        for _ in range(n):
            gi = r.bits(nglyph)
            adv = r.sbits(nadv)
            m = fonts.get(curfont, {}).get('map', [])
            s += chr(m[gi]) if gi < len(m) else '\ufffd'
        r.align()
        out.append(dict(font=curfont, size=size, color=color, text=s))
    return cid, bounds, (tx, ty), out


frame = 1
sprite_frames = {}
lines = []
records = []
for t in tags:
    if t['name'] == 'ShowFrame':
        if t['depth'] == 0:
            frame += 1
        else:
            sprite_frames[t['parent']] = sprite_frames.get(t['parent'], 1) + 1
    if t['name'] in ('DefineText', 'DefineText2'):
        b = d[t['body_off']:t['body_off'] + t['len']]
        try:
            cid, bounds, pos, recs = read_text(b, 2 if t['name'] == 'DefineText2' else 1)
        except Exception as e:
            print('ERR text', t['off'], e); continue
        loc = ('main frame %d' % frame) if t['depth'] == 0 else ('sprite %s frame %d' % (t['parent'], sprite_frames.get(t['parent'], 1)))
        txt = ' '.join(r['text'] for r in recs)
        records.append(dict(id=cid, loc=loc, frame=frame, off=t['off'],
                            bounds_px=[round(v/20, 1) for v in bounds],
                            size=(recs[0]['size']/20 if recs and recs[0]['size'] else None),
                            color=(recs[0]['color'] if recs else None),
                            runs=[r['text'] for r in recs], text=txt))

json.dump(records, open(os.path.join(OUT, "text_records.json"), 'w'), indent=1)
with open(os.path.join(OUT, "text_dump.txt"), 'w', encoding='utf-8') as fh:
    for r in records:
        fh.write('[id %3d | %s | %s px | color %s]\n' % (r['id'], r['loc'], r['size'], r['color']))
        for run in r['runs']:
            fh.write('    %s\n' % run)
        fh.write('\n')
print('text records', len(records))
for r in records[:40]:
    print(r['id'], r['loc'], '|', r['text'][:100])
```

### 12.10 `asdis.py` -- the ActionScript disassembler

Written for full AS1/AS2 coverage, which turned out to be enormous overkill for
a movie containing three distinct opcodes. It is here because it is what proved
that claim.

```python
import os, struct, sys, os, json
from swfparse import parse
from swlpaths import OUT, SWF

OPS = {0x00:'end',0x04:'nextFrame',0x05:'prevFrame',0x06:'play',0x07:'stop',0x08:'toggleQuality',0x09:'stopSounds',
0x0A:'add',0x0B:'subtract',0x0C:'multiply',0x0D:'divide',0x0E:'equals',0x0F:'less',0x10:'and',0x11:'or',0x12:'not',
0x13:'stringEquals',0x14:'stringLength',0x15:'stringExtract',0x17:'pop',0x18:'toInteger',0x1C:'getVariable',
0x1D:'setVariable',0x20:'setTarget2',0x21:'stringAdd',0x22:'getProperty',0x23:'setProperty',0x24:'cloneSprite',
0x25:'removeSprite',0x26:'trace',0x27:'startDrag',0x28:'endDrag',0x29:'stringLess',0x2A:'throw',0x2B:'castOp',
0x2C:'implementsOp',0x30:'randomNumber',0x31:'mbStringLength',0x32:'charToAscii',0x33:'asciiToChar',0x34:'getTime',
0x35:'mbStringExtract',0x36:'mbCharToAscii',0x37:'mbAsciiToChar',0x3A:'delete',0x3B:'delete2',0x3C:'defineLocal',
0x3D:'callFunction',0x3E:'return',0x3F:'modulo',0x40:'newObject',0x41:'defineLocal2',0x42:'initArray',
0x43:'initObject',0x44:'typeOf',0x45:'targetPath',0x46:'enumerate',0x47:'add2',0x48:'less2',0x49:'equals2',
0x4A:'toNumber',0x4B:'toString',0x4C:'pushDuplicate',0x4D:'stackSwap',0x4E:'getMember',0x4F:'setMember',
0x50:'increment',0x51:'decrement',0x52:'callMethod',0x53:'newMethod',0x54:'instanceOf',0x55:'enumerate2',
0x60:'bitAnd',0x61:'bitOr',0x62:'bitXor',0x63:'bitLShift',0x64:'bitRShift',0x65:'bitURShift',0x66:'strictEquals',
0x67:'greater',0x68:'stringGreater',0x69:'extends',
0x81:'gotoFrame',0x83:'getURL',0x87:'storeRegister',0x88:'constantPool',0x8A:'waitForFrame',0x8B:'setTarget',
0x8C:'gotoLabel',0x8D:'waitForFrame2',0x8E:'defineFunction2',0x8F:'try',0x94:'with',0x96:'push',0x99:'jump',
0x9A:'getURL2',0x9B:'defineFunction',0x9D:'if',0x9E:'call',0x9F:'gotoFrame2'}

NUL = bytes([0])

def strz(b, o):
    e = b.index(NUL, o)
    return b[o:e].decode('latin-1'), e + 1

def dis(code, indent=''):
    out = []
    o = 0
    pool = []
    while o < len(code):
        pc = o
        op = code[o]; o += 1
        if op < 0x80:
            out.append((pc, OPS.get(op, 'op_%02X' % op), ''))
            if op == 0:
                break
            continue
        if o + 2 > len(code):
            break
        ln = struct.unpack_from('<H', code, o)[0]; o += 2
        body = code[o:o+ln]
        nxt = o + ln
        name = OPS.get(op, 'op_%02X' % op)
        arg = ''
        try:
            if op == 0x96:
                items = []; i = 0
                while i < len(body):
                    t = body[i]; i += 1
                    if t == 0:
                        v, i = strz(body, i); items.append(json.dumps(v))
                    elif t == 1:
                        items.append(repr(struct.unpack_from('<f', body, i)[0])); i += 4
                    elif t == 2: items.append('null')
                    elif t == 3: items.append('undefined')
                    elif t == 4: items.append('reg%d' % body[i]); i += 1
                    elif t == 5: items.append('true' if body[i] else 'false'); i += 1
                    elif t == 6:
                        items.append(repr(struct.unpack('<d', body[i+4:i+8] + body[i:i+4])[0])); i += 8
                    elif t == 7:
                        items.append(str(struct.unpack_from('<i', body, i)[0])); i += 4
                    elif t == 8:
                        k = body[i]; i += 1
                        items.append(json.dumps(pool[k]) if k < len(pool) else 'const%d' % k)
                    elif t == 9:
                        k = struct.unpack_from('<H', body, i)[0]; i += 2
                        items.append(json.dumps(pool[k]) if k < len(pool) else 'const%d' % k)
                    else:
                        items.append('?type%d' % t); break
                arg = ', '.join(items)
            elif op == 0x88:
                n = struct.unpack_from('<H', body, 0)[0]; i = 2; pool = []
                for _ in range(n):
                    v, i = strz(body, i); pool.append(v)
                arg = '%d strings' % n
            elif op in (0x99, 0x9D):
                arg = '-> %d' % (nxt + struct.unpack_from('<h', body, 0)[0])
            elif op == 0x81:
                arg = str(struct.unpack_from('<H', body, 0)[0])
            elif op in (0x8C, 0x8B):
                arg = strz(body, 0)[0]
            elif op == 0x87:
                arg = 'reg%d' % body[0]
            elif op == 0x83:
                u, i = strz(body, 0); t, _ = strz(body, i)
                arg = '%r target=%r' % (u, t)
            elif op == 0x9A:
                arg = 'flags=0x%02X' % body[0]
            elif op == 0x9F:
                arg = 'playflag=%d' % body[0]
            elif op == 0x9B:
                nm, i = strz(body, 0)
                np_ = struct.unpack_from('<H', body, i)[0]; i += 2
                ps = []
                for _ in range(np_):
                    v, i = strz(body, i); ps.append(v)
                cs = struct.unpack_from('<H', body, i)[0]; i += 2
                out.append((pc, name, '%s(%s) {' % (nm, ', '.join(ps))))
                out.append(('SUB', dis(code[nxt:nxt+cs], indent + '    '), None))
                nxt += cs
                o = nxt
                continue
            elif op == 0x8E:
                nm, i = strz(body, 0)
                np_ = struct.unpack_from('<H', body, i)[0]; i += 2
                nreg = body[i]; i += 1
                flags = struct.unpack_from('<H', body, i)[0]; i += 2
                ps = []
                for _ in range(np_):
                    reg = body[i]; i += 1
                    v, i = strz(body, i); ps.append('r%d:%s' % (reg, v))
                cs = struct.unpack_from('<H', body, i)[0]; i += 2
                out.append((pc, name, '%s(%s) regs=%d flags=0x%04X {' % (nm, ', '.join(ps), nreg, flags)))
                out.append(('SUB', dis(code[nxt:nxt+cs], indent + '    '), None))
                nxt += cs
                o = nxt
                continue
            elif op == 0x94:
                arg = 'body=%d' % struct.unpack_from('<H', body, 0)[0]
        except Exception as e:
            arg = '<parse err %s> %s' % (e, body[:32].hex())
        out.append((pc, name, arg))
        o = nxt
    lines = []
    for item in out:
        if item[0] == 'SUB':
            lines.extend(item[1])
        else:
            pc, n, a = item
            lines.append(('%s%5d: %s %s' % (indent, pc, n, a)).rstrip())
    return lines


if __name__ == '__main__':
    SWF = SWF
    d, tags, hdr = parse(SWF)
    frame = 1
    sprite_frames = {}
    lines = []
    for t in tags:
        if t['name'] == 'ShowFrame':
            if t['depth'] == 0:
                frame += 1
            else:
                sprite_frames[t['parent']] = sprite_frames.get(t['parent'], 1) + 1
        if t['name'] in ('DoAction', 'DoInitAction'):
            body = d[t['body_off']:t['body_off'] + t['len']]
            if t['name'] == 'DoInitAction':
                body = body[2:]
            loc = ('main frame %d' % frame) if t['depth'] == 0 else ('sprite %s frame %d' % (t['parent'], sprite_frames.get(t['parent'], 1)))
            lines.append('\n;=============== DoAction @0x%X  [%s]  (%d bytes)' % (t['off'], loc, t['len']))
            lines += dis(body)
        if t['name'] in ('DefineButton2',):
            b = d[t['body_off']:t['body_off'] + t['len']]
            cid = struct.unpack_from('<H', b, 0)[0]
            aoff = struct.unpack_from('<H', b, 3)[0]
            lines.append('\n;=============== DefineButton2 id=%d @0x%X' % (cid, t['off']))
            if aoff:
                i = 3 + aoff
                while i < len(b):
                    size = struct.unpack_from('<H', b, i)[0]
                    cond = struct.unpack_from('<H', b, i + 2)[0]
                    end = i + size if size else len(b)
                    lines.append(';  --- button condition 0x%04X' % cond)
                    lines += dis(b[i+4:end if size else len(b)])
                    if not size:
                        break
                    i = end
    outp = os.path.join(OUT, "actionscript_disasm.txt")
    open(outp, 'w', encoding='utf-8').write('\n'.join(lines))
    print('wrote', outp, 'lines', len(lines))
```

### 12.11 `shapes.py` -- shapes to SVG

```python
"""SWF DefineShape / DefineFont glyph -> SVG path converter."""
import os, struct, sys, json, os
from swfparse import parse, R
from swlpaths import OUT, SWF


def read_matrix(r):
    r.align()
    a = d_ = 1.0
    b = c = 0.0
    if r.bits(1):
        n = r.bits(5); a = r.sbits(n) / 65536.0; d_ = r.sbits(n) / 65536.0
    if r.bits(1):
        n = r.bits(5); b = r.sbits(n) / 65536.0; c = r.sbits(n) / 65536.0
    n = r.bits(5)
    tx = r.sbits(n); ty = r.sbits(n)
    r.align()
    return (a, b, c, d_, tx, ty)


def read_fillstyles(r, ver):
    n = r.u8()
    if n == 0xFF and ver >= 2:
        n = r.u16()
    styles = []
    for _ in range(n):
        t = r.u8()
        st = {'type': t}
        if t == 0x00:
            if ver >= 3:
                st['color'] = (r.u8(), r.u8(), r.u8(), r.u8())
            else:
                st['color'] = (r.u8(), r.u8(), r.u8(), 255)
        elif t in (0x10, 0x12, 0x13):
            st['matrix'] = read_matrix(r)
            ng = r.u8()
            if t == 0x13:
                pass
            cnt = ng & 0x0F
            recs = []
            for _ in range(cnt):
                ratio = r.u8()
                col = (r.u8(), r.u8(), r.u8(), r.u8() if ver >= 3 else 255)
                recs.append((ratio, col))
            st['grad'] = recs
            if t == 0x13:
                r.u16()
        elif t in (0x40, 0x41, 0x42, 0x43):
            st['bitmap'] = r.u16()
            st['matrix'] = read_matrix(r)
        else:
            raise ValueError('bad fill type 0x%02X' % t)
        styles.append(st)
    return styles


def read_linestyles(r, ver):
    n = r.u8()
    if n == 0xFF and ver >= 2:
        n = r.u16()
    out = []
    for _ in range(n):
        w = r.u16()
        if ver >= 3:
            c = (r.u8(), r.u8(), r.u8(), r.u8())
        else:
            c = (r.u8(), r.u8(), r.u8(), 255)
        out.append({'width': w, 'color': c})
    return out


def read_shape(r, ver, with_style=True):
    """Returns (fills, lines, edge_groups). Edge groups: list of (fillstyles, linestyles, subpaths)."""
    fills = read_fillstyles(r, ver) if with_style else []
    lines = read_linestyles(r, ver) if with_style else []
    r.align()
    nfb = r.bits(4); nlb = r.bits(4)
    x = y = 0
    fs0 = fs1 = ls = 0
    fbase = lbase = 0
    # segments: list of dicts {fill0, fill1, line, pts:[('M',x,y),('L',x,y),('Q',cx,cy,x,y)]}
    segs = []
    cur = None
    groups = [(fills, lines)]

    def flush():
        nonlocal cur
        if cur and len(cur['pts']) > 1:
            segs.append(cur)
        cur = None

    while True:
        tf = r.bits(1)
        if tf == 0:
            flags = r.bits(5)
            if flags == 0:
                break
            new_styles = flags & 0x10
            has_ls = flags & 0x08
            has_f1 = flags & 0x04
            has_f0 = flags & 0x02
            has_mv = flags & 0x01
            if has_mv:
                nb = r.bits(5)
                x = r.sbits(nb); y = r.sbits(nb)
            if has_f0:
                fs0 = r.bits(nfb)
            if has_f1:
                fs1 = r.bits(nfb)
            if has_ls:
                ls = r.bits(nlb)
            if new_styles and ver >= 2:
                flush()
                r.align()
                fbase = len(fills)
                lbase = len(lines)
                fills = fills + read_fillstyles(r, ver)
                lines = lines + read_linestyles(r, ver)
                r.align()
                nfb = r.bits(4); nlb = r.bits(4)
                fs0 = fs1 = ls = 0
            flush()
            cur = {'f0': fs0 + fbase if fs0 else 0,
                   'f1': fs1 + fbase if fs1 else 0,
                   'ls': ls + lbase if ls else 0,
                   'pts': [('M', x, y)]}
        else:
            straight = r.bits(1)
            nb = r.bits(4) + 2
            if straight:
                if r.bits(1):
                    dx = r.sbits(nb); dy = r.sbits(nb)
                else:
                    if r.bits(1):
                        dx = 0; dy = r.sbits(nb)
                    else:
                        dx = r.sbits(nb); dy = 0
                x += dx; y += dy
                if cur is None:
                    cur = {'f0': fs0 + fbase if fs0 else 0, 'f1': fs1 + fbase if fs1 else 0, 'ls': ls + lbase if ls else 0, 'pts': [('M', x - dx, y - dy)]}
                cur['pts'].append(('L', x, y))
            else:
                cdx = r.sbits(nb); cdy = r.sbits(nb)
                adx = r.sbits(nb); ady = r.sbits(nb)
                cx = x + cdx; cy = y + cdy
                x = cx + adx; y = cy + ady
                if cur is None:
                    cur = {'f0': fs0 + fbase if fs0 else 0, 'f1': fs1 + fbase if fs1 else 0, 'ls': ls + lbase if ls else 0, 'pts': [('M', cx - cdx, cy - cdy)]}
                cur['pts'].append(('Q', cx, cy, x, y))
    flush()
    r.align()
    return fills, lines, segs


def rev(pts):
    """Reverse a segment's point list."""
    # rebuild going backwards
    coords = []
    x0, y0 = pts[0][1], pts[0][2]
    out = []
    prev = (x0, y0)
    items = []
    for p in pts[1:]:
        if p[0] == 'L':
            items.append(('L', prev, (p[1], p[2])))
            prev = (p[1], p[2])
        else:
            items.append(('Q', prev, (p[1], p[2]), (p[3], p[4])))
            prev = (p[3], p[4])
    items.reverse()
    if not items:
        return pts
    start = items[0][-1]
    res = [('M', start[0], start[1])]
    for it in items:
        if it[0] == 'L':
            res.append(('L', it[1][0], it[1][1]))
        else:
            res.append(('Q', it[2][0], it[2][1], it[1][0], it[1][1]))
    return res


def chain(seglist):
    """Chain segments end-to-start into closed loops."""
    remaining = list(seglist)
    paths = []
    while remaining:
        cur = remaining.pop(0)
        pts = list(cur)
        changed = True
        while changed:
            changed = False
            endx, endy = (pts[-1][1], pts[-1][2]) if pts[-1][0] == 'L' or pts[-1][0] == 'M' else (pts[-1][3], pts[-1][4])
            for i, s in enumerate(remaining):
                if abs(s[0][1] - endx) < 1e-6 and abs(s[0][2] - endy) < 1e-6:
                    pts.extend(s[1:])
                    remaining.pop(i)
                    changed = True
                    break
        paths.append(pts)
    return paths


def path_d(pts):
    out = []
    for p in pts:
        if p[0] == 'M':
            out.append('M%g %g' % (p[1], p[2]))
        elif p[0] == 'L':
            out.append('L%g %g' % (p[1], p[2]))
        else:
            out.append('Q%g %g %g %g' % (p[1], p[2], p[3], p[4]))
    return ''.join(out) + 'Z'


def col(c):
    return '#%02x%02x%02x' % (c[0], c[1], c[2]), c[3] / 255.0


def shape_to_svg_body(fills, lines, segs, bitmap_href=None):
    """Return list of svg element strings."""
    els = []
    byfill = {}
    for s in segs:
        for key, idx in (('f1', s['f1']), ('f0', s['f0'])):
            if idx:
                byfill.setdefault(idx, []).append(rev(s['pts']) if key == 'f0' else s['pts'])
    for idx in sorted(byfill):
        st = fills[idx - 1] if 0 < idx <= len(fills) else None
        if st is None:
            continue
        loops = chain(byfill[idx])
        dstr = ' '.join(path_d(p) for p in loops)
        if st['type'] == 0:
            c, a = col(st['color'])
            els.append('<path d="%s" fill="%s"%s fill-rule="nonzero"/>' % (dstr, c, '' if a == 1 else ' fill-opacity="%g"' % a))
        elif st['type'] in (0x40, 0x41, 0x42, 0x43):
            els.append('<path d="%s" fill="url(#bmp%d)" fill-rule="nonzero"/>' % (dstr, st['bitmap']))
        else:
            g = st.get('grad') or [(0, (128, 128, 128, 255))]
            c, a = col(g[0][1])
            els.append('<path d="%s" fill="%s" fill-opacity="%g" fill-rule="nonzero"/>' % (dstr, c, a))
    # strokes
    byline = {}
    for s in segs:
        if s['ls']:
            byline.setdefault(s['ls'], []).append(s['pts'])
    for idx in sorted(byline):
        st = lines[idx - 1] if 0 < idx <= len(lines) else None
        if st is None:
            continue
        c, a = col(st['color'])
        dstr = ' '.join(path_d(p)[:-1] for p in byline[idx])
        els.append('<path d="%s" fill="none" stroke="%s" stroke-width="%g"%s stroke-linecap="round" stroke-linejoin="round"/>'
                   % (dstr, c, max(st['width'], 20), '' if a == 1 else ' stroke-opacity="%g"' % a))
    return els


def parse_shape_tag(d, t):
    ver = {'DefineShape': 1, 'DefineShape2': 2, 'DefineShape3': 3}[t['name']]
    b = d[t['body_off']:t['body_off'] + t['len']]
    r = R(b, 0)
    cid = r.u16()
    bounds = r.rect()
    fills, lines, segs = read_shape(r, ver)
    return cid, bounds, fills, lines, segs


if __name__ == '__main__':
    SWF = SWF
    OUT = os.path.join(OUT, "shapes")
    os.makedirs(OUT, exist_ok=True)
    d, tags, hdr = parse(SWF)
    ok = err = 0
    index = {}
    for t in tags:
        if t['name'] not in ('DefineShape', 'DefineShape2', 'DefineShape3'):
            continue
        try:
            cid, bounds, fills, lines, segs = parse_shape_tag(d, t)
        except Exception as e:
            err += 1
            print('ERR shape @0x%X: %s' % (t['off'], e))
            continue
        els = shape_to_svg_body(fills, lines, segs)
        x0, x1, y0, y1 = bounds
        w = max(x1 - x0, 1); h = max(y1 - y0, 1)
        defs = ''
        bmps = [f['bitmap'] for f in fills if f['type'] in (0x40, 0x41, 0x42, 0x43)]
        svg = ('<svg xmlns="http://www.w3.org/2000/svg" viewBox="%g %g %g %g" width="%g" height="%g">%s%s</svg>'
               % (x0, y0, w, h, w / 20.0, h / 20.0, defs, ''.join(els)))
        open(os.path.join(OUT, 'shape_%03d.svg' % cid), 'w', encoding='utf-8').write(svg)
        index[cid] = dict(bounds_px=[round(v / 20.0, 1) for v in bounds], nfills=len(fills),
                          nlines=len(lines), nsegs=len(segs), bitmaps=bmps)
        ok += 1
    json.dump(index, open(os.path.join(OUT, '_index.json'), 'w'), indent=1)
    print('shapes ok', ok, 'err', err)
```

### 12.12 `raster.py` -- the rasterizer

```python
"""Minimal nonzero-winding scanline rasterizer (numpy) for SWF shape paths."""
import numpy as np


def flatten(pts, tol=3.0):
    """pts: [('M',x,y),('L',x,y),('Q',cx,cy,x,y)] -> list of (x,y) polygon points."""
    out = []
    cx = cy = 0.0
    for p in pts:
        if p[0] == 'M':
            cx, cy = p[1], p[2]
            out.append((cx, cy))
        elif p[0] == 'L':
            cx, cy = p[1], p[2]
            out.append((cx, cy))
        else:
            x0, y0 = cx, cy
            x1, y1 = p[1], p[2]
            x2, y2 = p[3], p[4]
            n = max(3, int(((abs(x1 - x0) + abs(y1 - y0) + abs(x2 - x1) + abs(y2 - y1)) / max(tol, 1)) ** 0.5 * 2))
            n = min(n, 24)
            for i in range(1, n + 1):
                t = i / n
                mt = 1 - t
                out.append((mt * mt * x0 + 2 * mt * t * x1 + t * t * x2,
                            mt * mt * y0 + 2 * mt * t * y1 + t * t * y2))
            cx, cy = x2, y2
    return out


class Canvas:
    def __init__(self, w, h, ss=3, bg=(255, 255, 255)):
        self.w = w; self.h = h; self.ss = ss
        self.W = w * ss; self.H = h * ss
        self.buf = np.zeros((self.H, self.W, 3), dtype=np.float32)
        self.buf[:, :] = bg

    def fill(self, loops, color, alpha=1.0, tex=None, texmat=None):
        """loops: list of point lists in *pixel* coords (already transformed)."""
        ss = self.ss
        edges = []
        for lp in loops:
            n = len(lp)
            if n < 3:
                continue
            for i in range(n):
                x0, y0 = lp[i]
                x1, y1 = lp[(i + 1) % n]
                if y0 != y1:
                    edges.append((x0 * ss, y0 * ss, x1 * ss, y1 * ss))
        if not edges:
            return
        e = np.array(edges, dtype=np.float64)
        ymin = max(0, int(np.floor(min(e[:, 1].min(), e[:, 3].min()))))
        ymax = min(self.H - 1, int(np.ceil(max(e[:, 1].max(), e[:, 3].max()))))
        if ymax < ymin:
            return
        x0, y0, x1, y1 = e[:, 0], e[:, 1], e[:, 2], e[:, 3]
        dirs = np.where(y1 > y0, 1, -1)
        ylo = np.minimum(y0, y1); yhi = np.maximum(y0, y1)
        slope = (x1 - x0) / (y1 - y0)
        mask = np.zeros((self.H, self.W), dtype=bool)
        for yy in range(ymin, ymax + 1):
            yc = yy + 0.5
            sel = (ylo <= yc) & (yhi > yc)
            if not sel.any():
                continue
            xs = x0[sel] + (yc - y0[sel]) * slope[sel]
            ds = dirs[sel]
            order = np.argsort(xs)
            xs = xs[order]; ds = ds[order]
            wind = np.cumsum(ds)
            inside = wind != 0
            row = mask[yy]
            for i in range(len(xs) - 1):
                if inside[i]:
                    a = int(np.ceil(xs[i] - 0.5)); b = int(np.ceil(xs[i + 1] - 0.5))
                    a = max(a, 0); b = min(b, self.W)
                    if b > a:
                        row[a:b] = True
        if not mask.any():
            return
        if tex is not None:
            ys, xs_ = np.nonzero(mask)
            a, b, c, dd, tx, ty = texmat
            det = a * dd - b * c
            if abs(det) < 1e-12:
                return
            px = xs_ / self.ss; py = ys / self.ss
            ux = px - tx; uy = py - ty
            u = (dd * ux - c * uy) / det
            v = (-b * ux + a * uy) / det
            th, tw = tex.shape[:2]
            ui = np.clip(u.astype(int), 0, tw - 1)
            vi = np.clip(v.astype(int), 0, th - 1)
            samp = tex[vi, ui]
            aa = samp[:, 3:4] / 255.0 * alpha
            self.buf[ys, xs_] = self.buf[ys, xs_] * (1 - aa) + samp[:, :3] * aa
        else:
            col = np.array(color, dtype=np.float32)
            if alpha >= 1.0:
                self.buf[mask] = col
            else:
                self.buf[mask] = self.buf[mask] * (1 - alpha) + col * alpha

    def stroke(self, loops, color, width_px, alpha=1.0):
        """Approximate stroking by drawing quads per segment."""
        hw = max(width_px, 0.6) / 2.0
        polys = []
        for lp in loops:
            for i in range(len(lp) - 1):
                (ax, ay), (bx, by) = lp[i], lp[i + 1]
                dx, dy = bx - ax, by - ay
                L = (dx * dx + dy * dy) ** 0.5
                if L < 1e-9:
                    continue
                nx, ny = -dy / L * hw, dx / L * hw
                polys.append([(ax + nx, ay + ny), (bx + nx, by + ny), (bx - nx, by - ny), (ax - nx, ay - ny)])
        for p in polys:
            self.fill([p], color, alpha)

    def image(self):
        from PIL import Image
        ss = self.ss
        b = self.buf.reshape(self.h, ss, self.w, ss, 3).mean(axis=(1, 3))
        return Image.fromarray(np.clip(b, 0, 255).astype(np.uint8))
```

### 12.13 `render.py` -- replaying the display list

```python
"""Replay the root display list and render every main-timeline frame to PNG.

    python3 render.py                 all frames, exact stage size  -> extract/frames/
    python3 render.py 5,45,212        just those frames
    python3 render.py --overscan      oversized canvas              -> extract/_overscan/
    python3 render.py --overscan 45   both

--overscan pads the canvas by OVERSCAN px on each axis and shifts the whole
root display list, so art the stage rect would crop away stays visible. That
is how off-stage parked content is found.
"""
import os, struct, sys, json, zlib
import numpy as np
from PIL import Image
from swfparse import parse, R
from shapes import read_shape, read_matrix, parse_shape_tag
from raster import Canvas, flatten
from swlpaths import OUT, SWF

OVERSCAN_X, OVERSCAN_Y = 100, 40
overscan = '--overscan' in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith('--')]

d, tags, hdr = parse(SWF)
x0, x1, y0, y1 = hdr['rect']
SW, SH = int(round((x1 - x0) / 20.0)), int(round((y1 - y0) / 20.0))
PAD_X, PAD_Y = (OVERSCAN_X, OVERSCAN_Y) if overscan else (0, 0)
STAGE_W, STAGE_H = SW + 2 * PAD_X, SH + 2 * PAD_Y
PADM = (1.0, 0.0, 0.0, 1.0, PAD_X * 20 - x0, PAD_Y * 20 - y0)
FRAMEDIR = os.path.join(OUT, '_overscan' if overscan else 'frames')
os.makedirs(FRAMEDIR, exist_ok=True)
print('stage %dx%d px, rendering %dx%d%s -> %s'
      % (SW, SH, STAGE_W, STAGE_H, ' (overscan)' if overscan else '', FRAMEDIR))

# ---------- bitmaps ----------
bitmaps = {}
IMGDIR = os.path.join(OUT, "images")
for f in os.listdir(IMGDIR):
    if f.startswith('bmp_'):
        cid = int(f.split('_')[1])
        bitmaps[cid] = np.array(Image.open(os.path.join(IMGDIR, f)).convert('RGBA'))

# ---------- shapes ----------
shapes = {}
for t in tags:
    if t['name'] in ('DefineShape', 'DefineShape2', 'DefineShape3'):
        cid, bounds, fills, lines, segs = parse_shape_tag(d, t)
        shapes[cid] = (fills, lines, segs)

# ---------- fonts ----------
fonts = {}
for t in tags:
    if t['name'] == 'DefineFont':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fid = struct.unpack_from('<H', b, 0)[0]
        off0 = struct.unpack_from('<H', b, 2)[0]
        n = off0 // 2
        offs = [struct.unpack_from('<H', b, 2 + i * 2)[0] for i in range(n)]
        glyphs = []
        for i in range(n):
            start = 2 + offs[i]
            end = 2 + offs[i + 1] if i + 1 < n else len(b)
            r = R(b[start:end], 0)
            try:
                _, _, segs = read_shape(r, 1, with_style=False)
            except Exception:
                segs = []
            glyphs.append(segs)
        fonts.setdefault(fid, {})['glyphs'] = glyphs
    if t['name'] == 'DefineFontInfo':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fid = struct.unpack_from('<H', b, 0)[0]
        nl = b[2]
        fonts.setdefault(fid, {})['name'] = b[3:3+nl].decode('latin-1')

# ---------- text ----------
texts = {}
for t in tags:
    if t['name'] in ('DefineText', 'DefineText2'):
        ver = 2 if t['name'] == 'DefineText2' else 1
        b = d[t['body_off']:t['body_off'] + t['len']]
        r = R(b, 0)
        cid = r.u16()
        bounds = r.rect()
        mtx = read_matrix(r)
        ng = r.u8(); na = r.u8()
        recs = []
        font = size = None
        color = (0, 0, 0, 255)
        x = y = 0
        while True:
            flags = r.u8()
            if flags == 0:
                break
            if flags & 0x80:
                if flags & 8: font = r.u16()
                if flags & 4:
                    color = (r.u8(), r.u8(), r.u8(), r.u8() if ver == 2 else 255)
                if flags & 1: x = r.s16()
                if flags & 2: y = r.s16()
                if flags & 8: size = r.u16()
            n = r.u8()
            r.align()
            gl = []
            for _ in range(n):
                gi = r.bits(ng); adv = r.sbits(na)
                gl.append((gi, adv))
            r.align()
            recs.append(dict(font=font, size=size, color=color, x=x, y=y, glyphs=gl))
            x += sum(a for _, a in gl)
        texts[cid] = dict(matrix=mtx, recs=recs, bounds=bounds)

# ---------- sprites ----------
sprite_tags = {}
for t in tags:
    if t['name'] == 'DefineSprite':
        sprite_tags[t['id']] = dict(frames=t['frames'], tags=[])
for t in tags:
    if t['depth'] > 0 and t['parent'] in sprite_tags:
        sprite_tags[t['parent']]['tags'].append(t)


def mul(m1, m2):
    a1, b1, c1, d1, tx1, ty1 = m1
    a2, b2, c2, d2, tx2, ty2 = m2
    return (a1 * a2 + c1 * b2, b1 * a2 + d1 * b2,
            a1 * c2 + c1 * d2, b1 * c2 + d1 * d2,
            a1 * tx2 + c1 * ty2 + tx1, b1 * tx2 + d1 * ty2 + ty1)


def apply(m, x, y):
    a, b, c, dd, tx, ty = m
    return (a * x + c * y + tx, b * x + dd * y + ty)


IDENT = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
TW = 1.0 / 20.0


def draw_shape(cv, cid, m, cx=None):
    ent = shapes.get(cid)
    if not ent:
        return
    fills, lines, segs = ent
    byfill = {}
    from shapes import rev, chain
    for s in segs:
        for key, idx in (('f1', s['f1']), ('f0', s['f0'])):
            if idx:
                byfill.setdefault(idx, []).append(rev(s['pts']) if key == 'f0' else s['pts'])
    for idx in sorted(byfill):
        if not (0 < idx <= len(fills)):
            continue
        st = fills[idx - 1]
        loops = chain(byfill[idx])
        polys = []
        for lp in loops:
            pl = flatten(lp)
            polys.append([apply(m, px, py) for px, py in pl])
        polys = [[(x * TW, y * TW) for x, y in p] for p in polys]
        if st['type'] == 0:
            c = st['color']
            cv.fill(polys, (c[0], c[1], c[2]), c[3] / 255.0)
        elif st['type'] in (0x40, 0x41, 0x42, 0x43):
            tex = bitmaps.get(st['bitmap'])
            if tex is None:
                continue
            tm = mul(m, st['matrix'])
            tm = tuple(v * TW for v in tm)
            cv.fill(polys, None, 1.0, tex=tex, texmat=tm)
        else:
            g = st.get('grad') or [(0, (128, 128, 128, 255))]
            c = g[-1][1]
            cv.fill(polys, (c[0], c[1], c[2]), c[3] / 255.0)
    byline = {}
    for s in segs:
        if s['ls'] and 0 < s['ls'] <= len(lines):
            byline.setdefault(s['ls'], []).append(s['pts'])
    for idx in sorted(byline):
        st = lines[idx - 1]
        c = st['color']
        sc = (abs(m[0]) + abs(m[3])) / 2.0
        for lp in byline[idx]:
            pl = [apply(m, px, py) for px, py in flatten(lp)]
            pl = [(x * TW, y * TW) for x, y in pl]
            pass  # stroke pass disabled: quad-approx produced artifacts; fills carry the outlines


def draw_text(cv, cid, m):
    ent = texts.get(cid)
    if not ent:
        return
    from shapes import rev, chain
    tm = mul(m, ent['matrix'])
    for rec in ent['recs']:
        f = fonts.get(rec['font'])
        if not f or rec['size'] is None:
            continue
        scale = rec['size'] / 1024.0
        x = rec['x']
        c = rec['color']
        for gi, adv in rec['glyphs']:
            if gi < len(f['glyphs']):
                segs = f['glyphs'][gi]
                loops = chain([s['pts'] for s in segs if s['f1'] or s['f0']] or [s['pts'] for s in segs])
                polys = []
                for lp in loops:
                    pl = flatten(lp, tol=8)
                    pts = [apply(tm, x + px * scale, rec['y'] + py * scale) for px, py in pl]
                    polys.append([(px * TW, py * TW) for px, py in pts])
                if polys:
                    cv.fill(polys, (c[0], c[1], c[2]), c[3] / 255.0)
            x += adv


def render_sprite(cv, sid, m, playhead):
    sp = sprite_tags.get(sid)
    if not sp:
        return
    frames = sp['frames']
    target = (playhead % frames) + 1 if frames else 1
    disp = {}
    f = 1
    for t in sp['tags']:
        if t['name'] == 'ShowFrame':
            if f == target:
                break
            f += 1
        elif t['name'] == 'PlaceObject2':
            b = d[t['body_off']:t['body_off'] + t['len']]
            r = R(b, 0)
            fl = r.u8(); depth = r.u16()
            cid = r.u16() if fl & 2 else None
            mm = read_matrix(r) if fl & 4 else IDENT
            if fl & 1 and depth in disp:
                old = disp[depth]
                cid = cid if cid is not None else old[0]
                mm = mm if fl & 4 else old[1]
            disp[depth] = (cid, mm)
        elif t['name'] == 'RemoveObject2':
            dp = struct.unpack_from('<H', d, t['body_off'])[0]
            disp.pop(dp, None)
    for depth in sorted(disp):
        cid, mm = disp[depth]
        if cid is None:
            continue
        full = mul(m, mm)
        if cid in shapes:
            draw_shape(cv, cid, full)
        elif cid in texts:
            draw_text(cv, cid, full)


# ---------- main timeline ----------
disp = {}
placed_at = {}
frame = 1
bg = (255, 255, 255)
only = set(int(v) for v in args[0].split(',')) if args else None

for t in tags:
    if t['depth'] > 0:
        continue
    n = t['name']
    b = d[t['body_off']:t['body_off'] + t['len']]
    if n == 'SetBackgroundColor':
        bg = (b[0], b[1], b[2])
    elif n == 'PlaceObject2':
        r = R(b, 0)
        fl = r.u8(); depth = r.u16()
        cid = r.u16() if fl & 2 else None
        mm = read_matrix(r) if fl & 4 else None
        old = disp.get(depth)
        if fl & 1 and old:
            cid = cid if cid is not None else old[0]
            mm = mm if mm else old[1]
        disp[depth] = (cid, mm or IDENT)
        placed_at[depth] = frame
    elif n == 'RemoveObject2':
        dp = struct.unpack_from('<H', b, 0)[0]
        disp.pop(dp, None)
        placed_at.pop(dp, None)
    elif n == 'ShowFrame':
        if only is None or frame in only:
            cv = Canvas(STAGE_W, STAGE_H, ss=2, bg=bg)
            for depth in sorted(disp):
                cid, mm = disp[depth]
                if cid is None:
                    continue
                mm = mul(PADM, mm)
                if cid in sprite_tags:
                    render_sprite(cv, cid, mm, frame - placed_at.get(depth, frame))
                elif cid in shapes:
                    draw_shape(cv, cid, mm)
                elif cid in texts:
                    draw_text(cv, cid, mm)
            cv.image().save(os.path.join(FRAMEDIR, 'frame_%03d.png' % frame))
            print('frame', frame, 'objs', len(disp))
        frame += 1
print('done, frames', frame - 1)
```

### 12.14 `sprites.py` -- movieclip contents

```python
import os, struct, sys, json
from swfparse import parse, R
from swlpaths import OUT, SWF

SWF = SWF
d, tags, hdr = parse(SWF)

# DefineShape -> bitmap id (fillstyle type 0x41/0x40 clipped bitmap fill)
shape_bitmap = {}
for t in tags:
    if t['name'] in ('DefineShape', 'DefineShape2', 'DefineShape3'):
        b = d[t['body_off']:t['body_off'] + t['len']]
        r = R(b, 0)
        cid = r.u16()
        r.rect()
        n = r.u8()
        if n == 0xFF:
            n = r.u16()
        for _ in range(n):
            ftype = r.u8()
            if ftype == 0:
                if t['name'] == 'DefineShape3':
                    r.o += 4
                else:
                    r.o += 3
            elif ftype in (0x10, 0x12):
                # gradient - bail
                shape_bitmap[cid] = 'gradient'
                break
            elif ftype in (0x40, 0x41, 0x42, 0x43):
                bid = r.u16()
                shape_bitmap[cid] = bid
                break
            else:
                break

sprites = {}
cur = None
frame = 1
for t in tags:
    if t['name'] == 'DefineSprite':
        cur = t['id']
        sprites[cur] = dict(frames=t['frames'], content=[])
        frame = 1
    if t['depth'] > 0 and cur is not None:
        if t['name'] == 'ShowFrame':
            frame += 1
        elif t['name'] == 'PlaceObject2':
            b = d[t['body_off']:t['body_off'] + t['len']]
            r = R(b, 0)
            fl = r.u8(); depth = r.u16()
            cid = r.u16() if fl & 2 else None
            sprites[t['parent']]['content'].append(dict(frame=frame, depth=depth, id=cid,
                                                        bitmap=shape_bitmap.get(cid)))
        elif t['name'] == 'RemoveObject2':
            sprites[t['parent']]['content'].append(dict(frame=frame, remove=struct.unpack_from('<H', d, t['body_off'])[0]))

print(json.dumps(sprites, indent=1))
print()
print('shape->bitmap map:', json.dumps({str(k): v for k, v in sorted(shape_bitmap.items()) if v != 'gradient'}))
json.dump(dict(sprites=sprites, shape_bitmap={str(k): v for k, v in shape_bitmap.items()}),
          open(os.path.join(OUT, "sprites.json"), 'w'), indent=1)
```

### 12.15 `orphans.py` -- unreferenced and off-stage

```python
import os, struct, sys, json, zlib, os
from swfparse import parse, R
from shapes import read_matrix, parse_shape_tag
from swlpaths import OUT, SWF

E = OUT

d, tags, hdr = parse(SWF)

defined = {}
for t in tags:
    n = t['name']
    if n.startswith('Define') and n not in ('DefineFontInfo',):
        cid = struct.unpack_from('<H', d, t['body_off'])[0]
        defined[cid] = n

referenced = {}          # cid -> list of reasons
def ref(cid, why):
    referenced.setdefault(cid, []).append(why)

# 1. PlaceObject2, root + sprites
frame = 1
placements = []
for t in tags:
    if t['name'] == 'ShowFrame' and t['depth'] == 0:
        frame += 1
    if t['name'] == 'PlaceObject2':
        b = d[t['body_off']:t['body_off'] + t['len']]
        r = R(b, 0)
        fl = r.u8(); depth = r.u16()
        cid = r.u16() if fl & 2 else None
        m = read_matrix(r) if fl & 4 else None
        alpha = None
        if fl & 8:
            r.align()
            has_add = r.bits(1); has_mul = r.bits(1); nb = r.bits(4)
            mul = [r.sbits(nb) for _ in range(4)] if has_mul else None
            add = [r.sbits(nb) for _ in range(4)] if has_add else None
            r.align()
            if mul:
                alpha = mul[3] / 256.0
        if cid is not None:
            ref(cid, 'placed f%d depth%d%s' % (frame, depth, '' if t['depth'] == 0 else ' (in sprite %s)' % t['parent']))
            placements.append(dict(frame=frame, depth=depth, cid=cid, m=m, alpha=alpha,
                                   root=(t['depth'] == 0)))

# 2. bitmap fills inside shapes
for t in tags:
    if t['name'].startswith('DefineShape'):
        cid, bounds, fills, lines, segs = parse_shape_tag(d, t)
        for f in fills:
            if f['type'] in (0x40, 0x41, 0x42, 0x43) and f['bitmap'] != 0xFFFF:
                ref(f['bitmap'], 'bitmap fill in shape %d' % cid)

# 3. DefineText -> font
for t in tags:
    if t['name'] in ('DefineText', 'DefineText2'):
        b = d[t['body_off']:t['body_off'] + t['len']]
        r = R(b, 0); r.u16(); r.rect(); read_matrix(r)
        ng = r.u8(); na = r.u8()
        while True:
            flags = r.u8()
            if flags == 0:
                break
            if flags & 0x80:
                if flags & 8:
                    ref(r.u16(), 'font used by text')
                if flags & 4:
                    r.o += 3
                if flags & 1: r.s16()
                if flags & 2: r.s16()
                if flags & 8: r.u16()
            n = r.u8(); r.align()
            for _ in range(n):
                r.bits(ng); r.sbits(na)
            r.align()

# 4. button states
for t in tags:
    if t['name'] == 'DefineButton2':
        b = d[t['body_off']:t['body_off'] + t['len']]
        i = 5
        while i < len(b) and b[i] != 0:
            st = b[i]; i += 1
            chid = struct.unpack_from('<H', b, i)[0]; i += 2
            i += 2
            r = R(b, i); read_matrix(r); i = r.o
            r2 = R(b, i)
            r2.align()
            has_add = r2.bits(1); has_mul = r2.bits(1); nb = r2.bits(4)
            for _ in range(4 * (bool(has_mul) + bool(has_add))):
                r2.sbits(nb)
            r2.align()
            i = r2.o
            ref(chid, 'button state 0x%02X' % st)

# 5. StartSound
for t in tags:
    if t['name'] == 'StartSound':
        ref(struct.unpack_from('<H', d, t['body_off'])[0], 'StartSound')

print('=' * 70)
print('DEFINED characters:', len(defined))
orphans = sorted(c for c in defined if c not in referenced)
print('NEVER REFERENCED anywhere:', len(orphans))
for c in orphans:
    print('   id %-4d %s' % (c, defined[c]))

print()
print('--- sounds ---')
snd = [c for c, k in defined.items() if k == 'DefineSound']
for c in sorted(snd):
    print('   sound %d: %s' % (c, referenced.get(c, ['*** NEVER PLAYED ***'])[:3]))

print()
print('--- objects placed fully OFF-STAGE (stage is 0..550 x 0..335 px) ---')
shape_bounds = {}
for t in tags:
    if t['name'].startswith('DefineShape'):
        cid, bounds, _, _, _ = parse_shape_tag(d, t)
        shape_bounds[cid] = bounds
off = []
for p in placements:
    if not p['root'] or p['m'] is None:
        continue
    bb = shape_bounds.get(p['cid'])
    if not bb:
        continue
    a, b_, c, dd, tx, ty = p['m']
    xs = [a * bb[i] + c * bb[j] + tx for i in (0, 1) for j in (2, 3)]
    ys = [b_ * bb[i] + dd * bb[j] + ty for i in (0, 1) for j in (2, 3)]
    x0, x1 = min(xs) / 20, max(xs) / 20
    y0, y1 = min(ys) / 20, max(ys) / 20
    if x0 > 550 or x1 < 0 or y0 > 335 or y1 < 0:
        off.append((p['frame'], p['depth'], p['cid'], round(x0), round(x1), round(y0), round(y1)))
for o in off[:40]:
    print('   f%-4d depth%-3d id%-4d  x %d..%d  y %d..%d' % o)
print('   total fully off-stage placements:', len(off))

print()
print('--- placements with alpha multiplier < 1 (faded/invisible) ---')
fa = [p for p in placements if p['alpha'] is not None and p['alpha'] < 0.99]
for p in fa[:20]:
    print('   f%d depth%d id%s alpha=%.2f' % (p['frame'], p['depth'], p['cid'], p['alpha']))
print('   count:', len(fa))
```

### 12.16 `hidden.py` -- slack, zlib tails, strings, unused glyphs

```python
import os, struct, sys, json, zlib, re, collections
from swfparse import parse, R
from shapes import read_matrix, parse_shape_tag
from swlpaths import OUT, SWF, EXE

E = OUT
d, tags, hdr = parse(SWF)
exe = open(EXE, 'rb').read() if EXE else b''
# locate the appended movie the same way unpack_projector does
FOOTER = b'\x56\x34\x12\xFA'
_i = exe.rfind(FOOTER)
MOVIE_OFF = (_i - struct.unpack_from('<I', exe, _i + 4)[0]) if _i >= 0 else 0
MOVIE_LEN = struct.unpack_from('<I', exe, _i + 4)[0] if _i >= 0 else 0
stub = exe[:MOVIE_OFF]

print('=' * 72)
print('A. BYTE-LEVEL: gaps, trailing data, container slack')
print('=' * 72)
# tag coverage of the root stream
end = 0
last = None
for t in tags:
    if t['depth'] == 0:
        end = t['body_off'] + t['len']
        last = t
print('last root tag: %s @0x%X ends at %d; file length %d -> slack %d bytes'
      % (last['name'], last['off'], end, len(d), len(d) - end))
if exe:
    print('bytes in exe after the movie: %d (8 = just the projector footer)'
          % (len(exe) - MOVIE_OFF - MOVIE_LEN))
# check every zlib stream in bitmaps for trailing bytes
extra = 0
for t in tags:
    if t['name'] in ('DefineBitsLossless', 'DefineBitsLossless2'):
        b = d[t['body_off']:t['body_off'] + t['len']]
        fmt = b[2]
        o = 8 if fmt == 3 else 7
        dec = zlib.decompressobj()
        dec.decompress(b[o:])
        if dec.unused_data:
            print('   bitmap id %d has %d unused bytes after the zlib stream'
                  % (struct.unpack_from('<H', b, 0)[0], len(dec.unused_data)))
            extra += 1
print('bitmaps with trailing bytes after their zlib stream:', extra)

print()
print('=' * 72)
print('B1. ENCODER FINGERPRINTS in DefineSound payloads')
print('=' * 72)
# MP3 encoders stamp a version string into the trailing frame. It says nothing
# about the author, but it dates the *content* pipeline -- which for a projector
# is the only clock that is not the runtime vendor's. See section 2.
ENC = (b'LAME', b'Xing', b'Info', b'Lavc', b'Gogo', b'ID3')
found_enc = False
for t in tags:
    if t['name'] != 'DefineSound':
        continue
    b = d[t['body_off']:t['body_off'] + t['len']]
    sid = struct.unpack_from('<H', b, 0)[0]
    codec = (b[2] >> 4) & 0xF
    for tag in ENC:
        i = b.find(tag)
        if i >= 0:
            txt = b[i:i + 24].decode('latin-1')
            txt = ''.join(c if 32 <= ord(c) < 127 else '.' for c in txt)
            print('  sound id %-4d codec %d  %s' % (sid, codec, txt))
            found_enc = True
            break
if not found_enc:
    print('  none')

print()
print('=' * 72)
print('B. STRINGS: author paths, tool fingerprints, anything human-written')
print('=' * 72)
pats = [rb'[A-Za-z]:\\[ -~]{4,80}', rb'[ -~]{4,60}\.fla\b', rb'[ -~]{4,60}\.swf\b',
        rb'Macromedia[ -~]{0,40}', rb'Generator[ -~]{0,30}', rb'[ -~]{0,20}Toops[ -~]{0,20}',
        rb'[ -~]{0,20}Overn[ -~]{0,20}', rb'http[ -~]{4,80}', rb'[ -~]{0,15}@[ -~]{4,40}']
for label, blob in (('SWF', d), ('PE stub', stub)):
    seen = set()
    for p in pats:
        for m in re.finditer(p, blob):
            s = m.group().decode('latin-1')
            if s not in seen:
                seen.add(s)
    if label == 'SWF':
        print('  SWF hits:', sorted(seen) if seen else 'NONE')
    else:
        # stub is stock; only show non-boilerplate
        interesting = [s for s in seen if not any(k in s for k in
                       ('Macromedia', 'SwFlsh', 'Flash', 'http://www.macromedia', 'Shockwave'))]
        print('  PE stub non-boilerplate hits:', sorted(interesting)[:20] if interesting else 'NONE')

print()
print('=' * 72)
print('C. FONT GLYPHS EMBEDDED BUT NEVER TYPED')
print('=' * 72)
fonts = {}
for t in tags:
    if t['name'] == 'DefineFontInfo':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fid = struct.unpack_from('<H', b, 0)[0]
        nl = b[2]
        codes = list(b[4 + nl:])
        fonts[fid] = codes
used = collections.defaultdict(set)
for t in tags:
    if t['name'] in ('DefineText', 'DefineText2'):
        b = d[t['body_off']:t['body_off'] + t['len']]
        r = R(b, 0); r.u16(); r.rect(); read_matrix(r)
        ng = r.u8(); na = r.u8(); font = None
        while True:
            flags = r.u8()
            if flags == 0:
                break
            if flags & 0x80:
                if flags & 8: font = r.u16()
                if flags & 4: r.o += 3
                if flags & 1: r.s16()
                if flags & 2: r.s16()
                if flags & 8: r.u16()
            n = r.u8(); r.align()
            for _ in range(n):
                used[font].add(r.bits(ng)); r.sbits(na)
            r.align()
for fid, codes in fonts.items():
    unused = [(i, chr(codes[i])) for i in range(len(codes)) if i not in used.get(fid, set())]
    print('  font %d: %d glyphs, %d never used -> %s'
          % (fid, len(codes), len(unused), ''.join(c for _, c in unused) or '(none)'))

print()
print('=' * 72)
print('D. ART PLACED OUTSIDE THE 550x335 STAGE (the off-stage parking lot)')
print('=' * 72)
STAGE = (0, 11000, 0, 6700)
def outside(x0, x1, y0, y1):
    return x0 > STAGE[1] or x1 < STAGE[0] or y0 > STAGE[3] or y1 < STAGE[2]

# bitmap-fill rectangles inside every shape, in shape-local coords
hidden_fills = collections.Counter()
hidden_detail = collections.defaultdict(set)
for t in tags:
    if not t['name'].startswith('DefineShape'):
        continue
    cid, bounds, fills, lines, segs = parse_shape_tag(d, t)
    for s in segs:
        idx = s['f1'] or s['f0']
        if not (0 < idx <= len(fills)):
            continue
        st = fills[idx - 1]
        if st['type'] not in (0x40, 0x41, 0x42, 0x43) or st['bitmap'] == 0xFFFF:
            continue
        xs = [p[1] for p in s['pts']] + [p[3] for p in s['pts'] if p[0] == 'Q']
        ys = [p[2] for p in s['pts']] + [p[4] for p in s['pts'] if p[0] == 'Q']
        if xs and outside(min(xs), max(xs), min(ys), max(ys)):
            hidden_fills[st['bitmap']] += 1
            hidden_detail[st['bitmap']].add(cid)
print('  bitmap-fill rectangles authored entirely outside the stage:')
for bid, n in hidden_fills.most_common():
    print('    bitmap %-4d used %3d times off-stage, in %d different shapes'
          % (bid, n, len(hidden_detail[bid])))
print('  total off-stage bitmap placements inside composite shapes:', sum(hidden_fills.values()))
```

### 12.17 `hidden2.py` -- off-stage text, and where the losers get parked

```python
import os, struct, sys, json, os, collections
from swfparse import parse, R
from shapes import read_matrix, parse_shape_tag
from swlpaths import OUT, SWF

E = OUT
d, tags, hdr = parse(SWF)
recs = {r['id']: r for r in json.load(open(os.path.join(E, "text_records.json")))}

# text bounds in twips
tb = {}
for t in tags:
    if t['name'] in ('DefineText', 'DefineText2'):
        b = d[t['body_off']:t['body_off'] + t['len']]
        r = R(b, 0)
        cid = r.u16()
        tb[cid] = r.rect()

print('=' * 72)
print('E. TEXT placed outside / partly outside the visible stage')
print('=' * 72)
frame = 1
found = 0
for t in tags:
    if t['name'] == 'ShowFrame' and t['depth'] == 0:
        frame += 1
    if t['name'] != 'PlaceObject2':
        continue
    b = d[t['body_off']:t['body_off'] + t['len']]
    r = R(b, 0)
    fl = r.u8(); depth = r.u16()
    cid = r.u16() if fl & 2 else None
    m = read_matrix(r) if fl & 4 else (1, 0, 0, 1, 0, 0)
    if cid not in tb:
        continue
    bb = tb[cid]
    a, b_, c, dd, tx, ty = m
    xs = [a * bb[i] + c * bb[j] + tx for i in (0, 1) for j in (2, 3)]
    ys = [b_ * bb[i] + dd * bb[j] + ty for i in (0, 1) for j in (2, 3)]
    x0, x1, y0, y1 = min(xs) / 20, max(xs) / 20, min(ys) / 20, max(ys) / 20
    if x0 < -1 or x1 > 551 or y0 < -1 or y1 > 336:
        found += 1
        clipped = 'FULLY OFF-STAGE' if (x0 > 550 or x1 < 0 or y0 > 335 or y1 < 0) else 'partly clipped'
        print('  f%-4d id%-4d %-16s x %.0f..%.0f y %.0f..%.0f  %r'
              % (frame, cid, clipped, x0, x1, y0, y1, recs[cid]['text'][:60]))
if not found:
    print('  none - every text object sits fully inside the visible stage')

print()
print('=' * 72)
print('F. WHERE the eliminated contestants get parked (bitmaps 1,2,3,5)')
print('=' * 72)
STAGEX = (0, 11000); STAGEY = (0, 6700)
loc = collections.defaultdict(list)
for t in tags:
    if not t['name'].startswith('DefineShape'):
        continue
    cid, bounds, fills, lines, segs = parse_shape_tag(d, t)
    for s in segs:
        idx = s['f1'] or s['f0']
        if not (0 < idx <= len(fills)):
            continue
        st = fills[idx - 1]
        if st['type'] not in (0x40, 0x41, 0x42, 0x43) or st['bitmap'] == 0xFFFF:
            continue
        xs = [p[1] for p in s['pts']] + [p[3] for p in s['pts'] if p[0] == 'Q']
        ys = [p[2] for p in s['pts']] + [p[4] for p in s['pts'] if p[0] == 'Q']
        if not xs:
            continue
        x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
        if x0 > STAGEX[1] or x1 < STAGEX[0] or y0 > STAGEY[1] or y1 < STAGEY[0]:
            loc[st['bitmap']].append((cid, round(x0 / 20), round(x1 / 20), round(y0 / 20), round(y1 / 20)))

NAMES = {1: 'Kain', 2: 'Bartz', 3: 'Cecil', 4: 'Edgar FFVI', 5: 'Edward FFIV', 7: 'Terra',
         29: 'Kefka', 66: 'Kefka', 68: 'Kefka', 171: 'Edgar', 212: 'Edgar', 239: 'Edgar'}
# frame each shape is first placed on
first = {}
frame = 1
for t in tags:
    if t['name'] == 'ShowFrame' and t['depth'] == 0:
        frame += 1
    if t['name'] == 'PlaceObject2':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fl = b[0]
        if fl & 2:
            cid = struct.unpack_from('<H', b, 3)[0]
            first.setdefault(cid, frame)
for bid in (1, 2, 3, 5, 7, 4):
    for (cid, x0, x1, y0, y1) in loc.get(bid, []):
        print('  bitmap %-4d %-12s parked in shape %-4d (first placed f%-4s)  x %d..%d  y %d..%d'
              % (bid, NAMES.get(bid, '?'), cid, first.get(cid, '?'), x0, x1, y0, y1))

print()
print('  --- Kefka/Edgar parking-lot X ranges (the vertical stacks) ---')
for bid in (29, 66, 68, 171, 212, 239):
    ent = loc.get(bid, [])
    if not ent:
        continue
    xs0 = min(e[1] for e in ent); xs1 = max(e[2] for e in ent)
    ys0 = min(e[3] for e in ent); ys1 = max(e[4] for e in ent)
    print('  bitmap %-4d %-6s  %3d copies  x %d..%d  y %d..%d'
          % (bid, NAMES.get(bid, '?'), len(ent), xs0, xs1, ys0, ys1))
```

### 12.18 `hidden3.py` -- the visibility ledger

The script that found bitmaps 79 and 81.

```python
import os, struct, sys, json, os, collections
from swfparse import parse, R
from shapes import read_matrix, parse_shape_tag
from swlpaths import OUT, SWF

E = OUT
d, tags, hdr = parse(SWF)

# shape -> list of (bitmap, local rect)
shape_bmp = collections.defaultdict(list)
for t in tags:
    if not t['name'].startswith('DefineShape'):
        continue
    cid, bounds, fills, lines, segs = parse_shape_tag(d, t)
    for s in segs:
        idx = s['f1'] or s['f0']
        if not (0 < idx <= len(fills)):
            continue
        st = fills[idx - 1]
        if st['type'] not in (0x40, 0x41, 0x42, 0x43) or st['bitmap'] == 0xFFFF:
            continue
        xs = [p[1] for p in s['pts']] + [p[3] for p in s['pts'] if p[0] == 'Q']
        ys = [p[2] for p in s['pts']] + [p[4] for p in s['pts'] if p[0] == 'Q']
        if xs:
            shape_bmp[cid].append((st['bitmap'], min(xs), max(xs), min(ys), max(ys)))

# sprite -> shapes it contains
sprite_shapes = collections.defaultdict(list)
for t in tags:
    if t['depth'] > 0 and t['name'] == 'PlaceObject2':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fl = b[0]
        if fl & 2:
            sprite_shapes[t['parent']].append(struct.unpack_from('<H', b, 3)[0])

onstage = collections.Counter()
offstage = collections.Counter()
frame = 1
for t in tags:
    if t['name'] == 'ShowFrame' and t['depth'] == 0:
        frame += 1
    if t['depth'] > 0 or t['name'] != 'PlaceObject2':
        continue
    b = d[t['body_off']:t['body_off'] + t['len']]
    r = R(b, 0)
    fl = r.u8(); depth = r.u16()
    cid = r.u16() if fl & 2 else None
    m = read_matrix(r) if fl & 4 else (1.0, 0.0, 0.0, 1.0, 0, 0)
    if cid is None:
        continue
    targets = [cid] + sprite_shapes.get(cid, [])
    for tc in targets:
        for (bid, x0, x1, y0, y1) in shape_bmp.get(tc, []):
            a, b_, c, dd, tx, ty = m
            X = [a * x + c * y + tx for x in (x0, x1) for y in (y0, y1)]
            Y = [b_ * x + dd * y + ty for x in (x0, x1) for y in (y0, y1)]
            gx0, gx1, gy0, gy1 = min(X), max(X), min(Y), max(Y)
            if gx0 > 11000 or gx1 < 0 or gy0 > 6700 or gy1 < 0:
                offstage[bid] += 1
            else:
                onstage[bid] += 1

NAMES = {1: 'Kain', 2: 'Bartz', 3: 'Cecil', 4: 'Edgar (FFVI)', 5: 'Edward (FFIV)',
         6: 'Crono', 7: 'Terra', 8: 'Black Mage', 9: 'backdrop',
         11: 'Kefka', 13: 'Kefka', 29: 'Kefka', 66: 'Kefka', 68: 'Kefka',
         79: 'Kefka', 81: 'Kefka', 171: 'Edgar', 212: 'Edgar', 227: 'Edgar',
         229: 'Edgar', 239: 'Edgar'}
allb = sorted(set(onstage) | set(offstage))
print('=' * 78)
print('G. PER-BITMAP: ever actually visible on the 550x335 stage?')
print('=' * 78)
print('%-6s %-14s %10s %10s   %s' % ('bmp', 'who', 'on-stage', 'off-stage', 'verdict'))
never = []
for bid in allb:
    on, off = onstage[bid], offstage[bid]
    v = 'VISIBLE' if on else '*** NEVER VISIBLE ***'
    if not on:
        never.append(bid)
    print('%-6d %-14s %10d %10d   %s' % (bid, NAMES.get(bid, '?'), on, off, v))
print()
print('bitmaps that NEVER appear inside the visible stage:', never)
```

### 12.19 `sheet.py` -- the contact sheet

```python
from PIL import Image, ImageDraw
import glob, os
from swlpaths import OUT

D = os.path.join(OUT, "images")
files = sorted(glob.glob(os.path.join(D, '*.png')))
SC = 4
cols = 7
cw, ch = 200, 220
sheet = Image.new('RGBA', (cols * cw, ((len(files) + cols - 1) // cols) * ch), (255, 0, 255, 255))
dr = ImageDraw.Draw(sheet)
for i, f in enumerate(files):
    im = Image.open(f).convert('RGBA')
    s = min(180 / im.width, 180 / im.height, 8)
    im2 = im.resize((int(im.width * s), int(im.height * s)), Image.NEAREST)
    x = (i % cols) * cw + (190 - im2.width) // 2
    y = (i // cols) * ch + 30
    bg = Image.new('RGBA', im2.size, (255, 255, 255, 255))
    bg.alpha_composite(im2)
    sheet.paste(bg, (x, y))
    dr.text(((i % cols) * cw + 5, (i // cols) * ch + 8), os.path.basename(f)[:26], fill=(0, 0, 0, 255))
out = os.path.join(OUT, "_contact_sheet.png")
sheet.convert('RGB').save(out)
print(out, sheet.size)
```

### 12.20 `mkscript.py` -- the transcript

```python
import json, os, sys, struct
from swfparse import parse
from swlpaths import OUT, SWF

E = OUT
d, tags, hdr = parse(SWF)
recs = {r['id']: r for r in json.load(open(os.path.join(E, "text_records.json")))}

# walk main timeline, record the frame each text character is PLACED on
placed = {}
stops = {}
frame = 1
for t in tags:
    if t['depth'] > 0:
        continue
    if t['name'] == 'ShowFrame':
        frame += 1
    elif t['name'] == 'DoAction':
        b = d[t['body_off']:t['body_off'] + t['len']]
        stops[frame] = 'stopSounds' if b[0] == 0x09 else 'stop'
    elif t['name'] == 'PlaceObject2':
        b = d[t['body_off']:t['body_off'] + t['len']]
        fl = b[0]
        cid = struct.unpack_from('<H', b, 3)[0] if fl & 2 else None
        if cid in recs and cid != 24:      # 24 = the "Press" button label
            placed.setdefault(frame, []).append(recs[cid]['text'])

CRONO = {51, 111, 155, 217, 222, 226, 230}

SECTIONS = [
    (1, 4, 'Title / opening animation (auto-play, 12 fps)'),
    (5, 29, 'ROUND 1 - introductions, and Edward is dismissed'),
    (30, 41, 'Animation: Edward walks off (auto-play)'),
    (42, 83, 'ROUND 2 - Q&A, voting, Kain eliminated'),
    (84, 101, 'Animation: Kain walks off (auto-play)'),
    (102, 136, 'ROUND 3 - Q&A, voting, Cecil AND Bartz eliminated'),
    (137, 149, 'Animation: the clones walk off (auto-play)'),
    (150, 176, 'ROUND 4 - Q&A, voting, Terra and Black Mage eliminated'),
    (177, 206, 'Animation: exits, Black Mage lines up his spell (auto-play)'),
    (207, 235, 'FINAL ROUND - Crono vs Edgar'),
    (236, 267, 'Animation: transition to credits (morph shape, auto-play)'),
    (268, 273, 'CREDITS'),
]

out = ["# Square's Weakest Link - full script",
       "",
       "Every line of dialogue in the movie, in playback order, recovered from the 136",
       "`DefineText` tags and keyed to the main-timeline frame each one is *placed* on.",
       "",
       "Panels marked **[silent balloon]** stop and wait for a click but place no text.",
       "In the Q&A rounds every one of them is Crono answering a question - he is a silent",
       "protagonist in *Chrono Trigger* and the movie never lets him speak. Kefka accepts",
       "the blank balloon as correct every single time, and Crono goes on to win the show.",
       ""]
for a, b, title in SECTIONS:
    out.append('## Frames %d-%d - %s' % (a, b, title))
    out.append('')
    any_line = False
    for f in range(a, b + 1):
        if f in placed:
            for txt in placed[f]:
                out.append('- **f%d** - %s' % (f, ' '.join(txt.split())))
            any_line = True
        elif f in stops:
            if f in CRONO:
                out.append('- **f%d** - *[Crono answers - empty speech balloon]*' % f)
            else:
                out.append('- **f%d** - *[held panel, no new text]*' % f)
            any_line = True
    if not any_line:
        out.append('*(no dialogue - continuous animation)*')
    out.append('')
open(os.path.join(E, 'SCRIPT.md'), 'w', encoding='utf-8').write('\n'.join(out))
print('wrote SCRIPT.md; dialogue frames', len(placed))
silent = sorted(f for f in stops if f not in placed)
print('silent stop frames:', silent)
```

### 12.21 `mkconcat.py` -- video timing

```python
import json, os, sys
from swfparse import parse
from swlpaths import OUT, SWF

E = OUT
d, tags, hdr = parse(SWF)
recs = json.load(open(os.path.join(E, "text_records.json")))
tf = {}
for r in recs:
    tf.setdefault(r['frame'], []).append(r['text'])
stops = set()
frame = 1
for t in tags:
    if t['depth'] > 0:
        continue
    if t['name'] == 'ShowFrame':
        frame += 1
    elif t['name'] == 'DoAction':
        stops.add(frame)

SEP = chr(92)
lines = []
total = 0.0
for f in range(1, 274):
    p = os.path.join(E, 'frames', 'frame_%03d.png' % f).replace(SEP, '/')
    if f in stops:
        n = sum(len(t) for t in tf.get(f, []))
        dur = max(1.6, min(7.0, 1.4 + n * 0.055))
    else:
        dur = 1.0 / 12
    lines.append("file '%s'" % p)
    lines.append("duration %.4f" % dur)
    total += dur
lines.append("file '%s'" % os.path.join(E, 'frames', 'frame_273.png').replace(SEP, '/'))
open(os.path.join(E, '_concat.txt'), 'w', encoding='utf-8').write('\n'.join(lines))
print('total runtime %.1f s (%.1f min)' % (total, total / 60))
```

### 12.22 `compare.py` -- the hidden-cast figure

```python
from PIL import Image, ImageDraw
import os
from swlpaths import OUT

E = OUT
picks = [(150, 'f150 - Cecil and Bartz have just been eliminated'),
         (215, 'f215 - final round; four of the eight are parked off-frame'),
         (272, 'f272 - "THE END." card, with the cast still standing outside it')]
W, H = 760, 420
sheet = Image.new('RGB', (W + 20, len(picks) * (H + 26) + 10), (18, 18, 18))
dr = ImageDraw.Draw(sheet)
for i, (f, cap) in enumerate(picks):
    p = os.path.join(E, '_overscan', 'frame_%03d.png' % f)
    if not os.path.exists(p):
        continue
    im = Image.open(p).convert('RGB')
    # dim everything outside the real stage so the hidden art reads clearly
    mask = Image.new('L', im.size, 255)
    ImageDraw.Draw(mask).rectangle([100, 40, 649, 374], fill=0)
    outside = Image.blend(im, Image.new('RGB', im.size, (25, 10, 12)), 0.45)
    im = Image.composite(outside, im, mask)
    ImageDraw.Draw(im).rectangle([100, 40, 649, 374], outline=(0, 255, 90), width=2)
    y = i * (H + 26) + 24
    sheet.paste(im, (10, y))
    dr.text((12, y - 18), cap + '   --   green box = the 550x335 the viewer sees; dimmed = never rendered',
            fill=(255, 235, 120))
out = os.path.join(E, '_overscan', '_hidden_cast.png')
sheet.save(out)
print(out)
```

### 12.23 `run_all.py` -- the driver

```python
"""Run the whole teardown pipeline in order.

    python3 tools/run_all.py             everything
    python3 tools/run_all.py --quick     skip the two full-movie frame renders

Put the projector .exe (or a bare .swf) in the folder above `tools/` and run
this. Everything lands in `extract/` next to it.

Needs Python 3 and Pillow + numpy. ffmpeg is optional and only used for the
reconstruction video.
"""
import os
import subprocess
import sys
import time

from swlpaths import BASE, OUT, EXE, SWF

HERE = os.path.dirname(os.path.abspath(__file__))
PY = sys.executable
QUICK = '--quick' in sys.argv

STEPS = [
    ('unpack_projector.py', [], 'split the .exe into player stub + movie'),
    ('timeline.py', [], 'frame-by-frame display list and sound cues'),
    ('extract_assets.py', [], 'bitmaps -> PNG, sounds -> MP3/WAV'),
    ('adpcm.py', [], 'decode the Flash-ADPCM sounds to WAV'),
    ('textdump.py', [], 'DefineText + font tables -> real strings'),
    ('asdis.py', [], 'disassemble every ActionScript block'),
    ('shapes.py', [], 'DefineShape* -> SVG'),
    ('sprites.py', [], 'movieclip -> bitmap sequence map'),
    ('render.py', [], 'render every frame at stage size'),
    ('render.py', ['--overscan'], 'render every frame with overscan'),
    ('sheet.py', [], 'bitmap contact sheet'),
    ('orphans.py', [], 'unreferenced characters + off-stage placements'),
    ('hidden.py', [], 'container slack, zlib tails, strings, unused glyphs'),
    ('hidden2.py', [], 'off-stage text scan'),
    ('hidden3.py', [], 'per-bitmap "was this ever visible?" table'),
]

SKIP_IF_QUICK = {'render.py'}


def main():
    if EXE is None and not os.path.exists(SWF):
        sys.exit('Put a Flash projector .exe (or a .swf) in %s first.' % BASE)
    print('base   %s' % BASE)
    print('output %s' % OUT)
    print()
    log = []
    for script, extra, what in STEPS:
        if QUICK and script in SKIP_IF_QUICK:
            print('-- skipping %s %s (--quick)' % (script, ' '.join(extra)))
            continue
        label = ' '.join([script] + extra)
        print('=' * 70)
        print('>> %-32s %s' % (label, what))
        print('=' * 70)
        t0 = time.time()
        r = subprocess.run([PY, os.path.join(HERE, script)] + extra, cwd=HERE)
        dt = time.time() - t0
        log.append((label, r.returncode, dt))
        if r.returncode != 0:
            print('!! %s exited %d' % (label, r.returncode))
        print()
    print('=' * 70)
    print('SUMMARY')
    for label, rc, dt in log:
        print('  %-28s %-4s %6.1fs' % (label, 'ok' if rc == 0 else 'FAIL', dt))
    print()
    print('Movie-specific extras (edit the frame lists inside them first):')
    print('  mkscript.py   build SCRIPT.md   -- section boundaries are per-movie')
    print('  mkconcat.py   ffmpeg timing list for the reconstruction video')
    print('  compare.py    the "hidden cast" figure')


if __name__ == '__main__':
    main()
```

### 12.24 `build_readme.py` -- this document

```python
"""Assemble the standalone teardown document.

`readme_src.md` is the prose. Two markers get expanded on the way out:

    <!--CODE:name.py-->      the current contents of tools/name.py, fenced
    <!--INCLUDE:name.md-->   the current contents of extract/name.md, inline

so the published document can never drift from the scripts that produced its
findings, or from the generated transcript.

Usage: python3 tools/build_readme.py [output.md] [--txt]
"""
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
BASE = os.path.dirname(HERE)
SRC = os.path.join(HERE, 'readme_src.md')
DEFAULT_OUT = os.path.join(BASE, 'Squares-Weakest-Link-teardown.md')

# Spellings to normalize on the way out. Quoted material from the binary is
# protected by KEEP below so original strings stay verbatim.
US = [
    ('color', 'color'), ('Color', 'Color'),
    ('behavior', 'behavior'), ('Behavior', 'Behavior'),
    ('artifact', 'artifact'), ('Artifact', 'Artifact'),
    ('organized', 'organized'), ('recognized', 'recognized'),
    ('center', 'center'), ('Center', 'Center'),
    ('labeled', 'labeled'), ('Labeled', 'Labeled'),
    ('analyze', 'analyze'), ('catalog', 'catalog'),
    ('honor', 'honor'), ('favor', 'favor'), ('defense', 'defense'),
    ('while', 'while'), ('among', 'among'),
    ('gray', 'gray'), ('Gray', 'Gray'),
    ('practice', 'practice'), ('traveled', 'traveled'),
]

# Quoted from the movie or from Macromedia's own strings -- do not respell.
KEEP = [
    'Copyright (c) 1996-2000 Macromedia, Inc.',
]

# The document is served as a raw file. Web servers have no mime mapping for
# .md, so it goes out with no charset and browsers fall back to windows-1252,
# which renders every UTF-8 em dash as mojibake. Keeping the output pure ASCII
# makes it render identically no matter what charset the reader guesses.
ASCII_FOLD = [
    ('--', '--'), ('-', '-'),
    ('-', '-'), ('+', '+'), ('+', '+'), ('|', '|'),
    ('+', '+'), ('+', '+'), ('+', '+'), ('+', '+'),
    ('->', '->'), ('<-', '<-'), ('...', '...'),
    ('*', '*'), ('section ', 'section '), ('(c)', '(c)'),
    (''', "'"), (''', "'"), ('"', '"'), ('"', '"'),
    ('x', 'x'), (' ', ' '), ('e', 'e'),
    ('>=', '>='), ('<=', '<='), ('deg', 'deg'),
]


def expand(text):
    def code(m):
        name = m.group(2).strip()
        body = open(os.path.join(HERE, name), encoding='utf-8').read().rstrip('\n')
        lang = 'python' if name.endswith('.py') else ''
        return '```%s\n%s\n```' % (lang, body)

    def include(m):
        name = m.group(2).strip()
        for cand in (os.path.join(BASE, 'extract', name), os.path.join(BASE, name)):
            if os.path.exists(cand):
                body = open(cand, encoding='utf-8').read().rstrip('\n')
                # Drop the included file's own H1 -- the host document frames it --
                # then demote every remaining heading one level so the included
                # sections nest under the host section instead of competing with it.
                body = re.sub(r'\A#[^\n]*\n', '', body).lstrip('\n')
                body = re.sub(r'^(#{1,5}) ', r'#\1 ', body, flags=re.M)
                return body
        raise SystemExit('INCLUDE not found: %s' % name)

    # One pass over the source only. re.sub never rescans what it substituted,
    # which matters because this file's own docstring contains both markers and
    # gets injected verbatim by the CODE branch.
    def one(m):
        return (code if m.group(1) == 'CODE' else include)(m)

    return re.sub(r'<!--\s*(CODE|INCLUDE):([^>]+?)-->', one, text)


def americanize(text):
    holes = {}
    for i, k in enumerate(KEEP):
        token = '\x00KEEP%d\x00' % i
        if k in text:
            holes[token] = k
            text = text.replace(k, token)
    for a, b in US:
        text = text.replace(a, b)
    for token, k in holes.items():
        text = text.replace(token, k)
    return text


def to_ascii(text):
    for a, b in ASCII_FOLD:
        text = text.replace(a, b)
    bad = sorted({ch for ch in text if ord(ch) > 126})
    if bad:
        print('WARNING: non-ASCII left in output: %r' % bad)
    return text


def main():
    args = [a for a in sys.argv[1:] if not a.startswith('--')]
    out = args[0] if args else DEFAULT_OUT
    text = to_ascii(americanize(expand(open(SRC, encoding='utf-8').read())))
    targets = [out]
    # `--txt` also writes a byte-identical .txt twin. Web servers have no mime
    # mapping for .md, so it is served as application/octet-stream and browsers
    # download it instead of displaying it; .txt is text/plain everywhere.
    if '--txt' in sys.argv:
        targets.append(os.path.splitext(out)[0] + '.txt')
    for path in targets:
        open(path, 'w', encoding='utf-8').write(text)
        print('%s: %d lines, %.0f KB'
              % (path, text.count('\n') + 1, len(text.encode()) / 1024))
    # Any marker still present came from an embedded script's own docstring --
    # re.sub does not rescan its replacements, so those are inert by design.
    left = re.findall(r'<!--\s*(?:CODE|INCLUDE):([^>]+?)-->', text)
    if left:
        print('markers inside embedded code (expected, not expanded): %s' % ', '.join(sorted(set(left))))


if __name__ == '__main__':
    main()
```

---

## 13. About this teardown

Produced by **FieRcE YeD**, 12 August 2026, for
**[doors98.com](https://doors98.com)**.

### Why bother

Square's Weakest Link is a 2001 Flash movie that circulated as a bare `.exe`.
That was a reasonable way to ship a cartoon in 2001 and is close to the worst
possible way to preserve one: it is an unsigned Windows binary, which is enough
to make most people delete it on sight, and it carries a copy of a player
Macromedia stopped shipping a very long time ago.

Reading it instead of running it recovers the actual artifact -- a 1.1 MB SWF
that any modern Flash emulator will play, the author's and composer's names,
41 pieces of sprite art, six sounds, the complete screenplay of a joke that
depends on a character saying nothing at all, and one Kefka who has been
walking in place just off the right-hand edge of the frame since 2001.

### Why a teardown and not a description

The interesting content in a file like this is rarely the content it shows you.
The movie is a nine-minute comic; the *file* also contains a pose palette the
author dragged off-screen while working, five characters standing in the void
after their elimination, and a fully animated movieclip that no human being has
ever seen play. None of that is visible from watching it, and none of it is
recoverable from a screen capture.

That is why section 12 carries the actual code rather than a summary. A
description of a format helps one person; a working decoder helps whoever finds
the next file. The SWF specification is public and the format is genuinely
simple -- the only reason these movies stay unexamined is that the dialogue is
stored as glyph indices, so the obvious first move (run `strings` on it) returns
nothing and people conclude there is nothing there.

There is. The toolchain in section 12 works on any Flash-era projector, not
just this one.

### doors98.com

doors98.com is a Windows 98 desktop rebuilt in a browser -- a parody, an
archive, and a place to put things that no longer have anywhere to live. It
already hosts recreations and teardowns of several recovered applications from
this era, including the POL Map Editor, Pokemon Online Advanced Beta 1.0,
Epic's Memorial and POL Lounge V 1.0.

Where things live:

| | |
|---|---|
| The original executable | <https://archive.org/details/doors98-squares-weakest-link> |
| This document, raw | <https://doors98.com/apps/flash/weakest-link/squares-weakest-link-readme.txt> |
| This document, as Markdown | <https://doors98.com/apps/flash/weakest-link/squares-weakest-link-readme.md> |
| This document, on the desktop | <https://doors98.com/downloads/squares-weakest-link-readme> |
| The download entry | <https://doors98.com/downloads/squares-weakest-link> |
| Other recovered software of the era | <https://archive.org/details/@fierceyed> |

(The two document files are byte-identical. Web servers have no mime mapping
for `.md`, so that copy is sent as `application/octet-stream` and browsers
download it instead of showing it; the `.txt` is `text/plain` and renders
inline, which is why the desktop viewer and the sitemap both point at it. The
document is written in Markdown and kept pure ASCII so it reads correctly
either way.)

The findings, the format notes and the toolchain are the part worth keeping.

### Corrections

Almost everything here was derived from one file. The byte-level findings are
mechanical and reproducible by re-running section 12. The character
identifications in section 6.1 are inference -- sprite art cross-checked against
which bitmaps appear in each scene and who is speaking -- and the walk-cycle
attributions in section 4.7 follow from placement positions rather than from
anything labeled in the file.

The exception is "Where it came from" in section 2, which is the only part
sourced from outside the movie: the DHS distribution, the dhsrpg.net hosting
and the DarthTOOPS identification all rest on archived web pages, and the last
of those is a strong inference rather than a confirmed fact.

If you were part of Dream Horizon Software, know Kristopher M. Toops or Shawn
Overn, or have a copy of the original dhsrpg.net download page: corrections are
genuinely welcome, and so are takedown requests from any rights holder named in
section 10.

A complete static teardown of Square's Weakest Link (Kristopher M. Toops, 2001), a Macromedia Flash 5 projector. Produced without running the program and without any third-party SWF tooling: the container, the tag stream, all 41 bitmaps, the six sounds including three Flash-ADPCM tracks, the 252 vector shapes, both embedded fonts, the complete recovered screenplay and every one of the 273 frames were decoded from the bytes with parsers written from the file-format specification.

Square's Weakest Link ships as an unsigned Windows executable, which is a reasonable way to distribute a cartoon in 2001 and close to the worst way to preserve one. Reading it instead of running it recovers the actual artifact. A Flash projector is the stock player with the movie appended and an eight-byte footer whose magic is 0xFA123456; splitting it yields a 1.1 MB uncompressed SWF that any modern Flash emulator will play, and a Flash Player 5.0 r30 stub whose August 2000 PE timestamp dates Macromedia's build rather than the movie -- the content itself is fixed to 2001 by its own end card, by Final Fantasy IX references that postdate the stub by three months, and by a LAME 3.84 encoder fingerprint in the music. The dialogue is stored as static DefineText, which is glyph indices rather than characters, so a strings dump of the file returns nothing readable; recovering the screenplay requires walking DefineFontInfo's code table and mapping each glyph index back to a character. Doing that recovers a complete script whose central joke is that Crono, a silent protagonist, answers every question with an empty speech balloon and is ruled correct every time. Replaying the display list frame by frame through a hand-written scanline rasterizer reproduces all 273 frames. Re-rendering them on an oversized canvas exposes what the stage rect crops away: 302 bitmap placements sitting entirely off-stage, including a pose palette the author parked outside each edge while working, five eliminated contestants left standing in the void for the rest of the runtime, and two sprites with an on-stage placement count of zero -- a fully animated Kefka walk cycle that has been running just off the right-hand edge of the frame, unseen, since 2001.