# GNZ 1.3 recovery notes

## Animation audit following the uploaded screenshot

The original files were intact. The mistakes were in the browser renderer: it
guessed actor frame grids, reused the player's facing convention for zombies,
cycled frames using elapsed game time, and omitted native animation states.
The earlier smoke tests verified movement/ammunition/loading, not the rectangles
and transforms actually drawn. A rectangle being inside an image is not enough:
the old 64px zombie crop was in bounds but crossed the 80px cell boundaries.

`tools/extract_animations.py` now executes the isolated ARM animation builders
with framework calls stubbed, capturing CGRect arguments from registers **and
the stack**. It emits `animation-trace.json` and `assets/animations.js`. These
traces establish frame order, dimensions, and delays; they do not emulate the
whole iOS application or prove every aspect of the browser port correct.

Corrections in this pass:

- Zombie legs: 80×64, three columns, six poses; sequence 0,1,2,3,4,5,4,3,2,1 at .045s/frame. Body: 80×128, six columns; walking 0,1,2,3,4,3,2,1 at .045s/frame (`changeWalkAnim`, 0x9db8).
- Zombie sprites face left unmirrored, opposite the player (`moveToPlayer:`, 0x8e34). Both body halves now use the same correct transform. Attack reach also follows the recovered asymmetric -47/+67 bounds.
- Attack body frames 9,10,11,10; hit reaction 6,7,8,7,6, both .045s/frame (`StateZombieAttack getAttackAnimate`, 0x36200; `StateZombieHitted doHittedAction:`, 0x363b8). Legs stop cycling while attacking/hit. Walking restarts with its own clock.
- Player walk: 0,1,2,3,4,3,2,1 at .05s/frame; standing is [128,64,128,64], not a walking pose. Upper-body bob is 5px down/up over .2s each (`walk:`, 0x5d44; `start`, 0x63c8). Dead-leg crop is also recovered.
- Player firing: frames 1 through the recovered inclusive end index at .04s/frame, followed by idle, instead of truncating the sequence to the muzzle-flash timer.
- Draw order: player 18/19, pickups 23, zombies 26/27. Fast zombies use the recovered RGB tint (255,204,234). Nested sprite opacity now respects its parent.
- Muzzle flashes: three shrinking frames at .05s/frame, individual weapon muzzle coordinates, and player offsets relative to the original x=83 reference (`Player showMuzzleFlash`, 0x45a0). The invented long tracer line was removed. Hit-flash duration is .1s, not .4s.
- Browser input regression found by the new hit-animation test: very short Space/F taps could fall entirely between animation frames. Key-down now fires immediately; holding still uses the existing browser repeat behavior.
- Up-stair transition: 1s scene fade, building/floor-marker/climbing-feet story with 1s opening, 2s marker movement of 2.48px, 1s closing, then 1s scene reveal. Crops and positions come from `StoryLayer init` (0x7aaf8); feet sequence 0,1,2,1 at .1s/frame from `getLegAnimation` (0x7a5c8). Combat/movement are locked during the sequence; pausing freezes it. Exploration intentionally skips it. The floor-99 helicopter ending remains simplified.

Regression tests now compare animation tables against the native trace and
inspect actual browser canvas draw calls for frame crops and mirror direction.
Visual captures are saved as `animation-facing.png` and `animation-stairs.png`.
Remaining work includes the full weapon-specific input state machines, exact
damage scheduling, and original HUD layout; these checks do not establish full
equivalence to the original game.

### Follow-up: death effects, reload poses and heart masks

- `Zombie dieAction` (0x9454) creates eight pieces in order: head, eye, eye,
  body, arm, arm, leg, leg. Their original MoveBy offsets are recovered into
  `assets/animations.js`. Each starts at the upper-body position, moves over
  .5s, rotates by a random 0–180 degrees, and fades from .4 to .5s. The second
  eye scales to twice its size. Right-facing deaths mirror both art and x
  displacement. This is a tween, not a gravity simulation: `EaseOut update:`
  (0x42a50) uses `pow(t,1/3)`. The extractor stubs enumeration, so the piece
  files/offsets are emulated while the action loop was manually disassembled.
- `ExplosionBodyBlood makeAnimation` (0x131c4) uses four 128×128 frames at .07s
  on layer 28. `ExplosionGroundBlood makeAnimation` (0x13384) uses two 128×32
  frames at .1s, waits 2s, then fades for 1.5s. The two floor splashes are
  placed 60px either side, with the right one mirrored, on layer 5.
- `Heart initHeartWithIndex:` (0x12f08) uses a 25×25 outline and left-anchored
  22×21 fill. `setRate:` (0x12dec) masks fill width, with each heart covering
  .33 of total health. Correct crops and partial fills replace full/empty
  32px guesses. The surrounding browser HUD layout is still not native.
- `assets/player-poses.js` records the nine weapon-specific upper-body builders,
  including reverse frame ranges, appended idle poses, and additional frames
  passed to `addPlayerAnimationFrame:w:h:`. For example the pump return is
  9,10,11,10,9,0, not simply 9,10,11. Eight weapons now show their recovered
  magazine and slide poses at .04s/frame. The sniper's two-atlas bolt state is
  not applied yet. Browser auto-reload stage timing and the common gestures
  are still adaptations, not the original nine input state machines.
- Pose clocks pause with gameplay, reset on room/weapon changes and firing,
  and manual gestures cannot change the gun while paused or climbing stairs.
- Hit flashes now draw at full opacity for their recovered .1s lifetime;
  the old renderer accidentally dimmed them to at most 30%.

`tests/fidelity_smoke.py` checks actual canvas crops and transforms under a
controlled clock: eight death pieces in both directions, exact intermediate
movement, scale/mirror signs, fades, lingering floor splashes, partial hearts,
and all eight supported reload pose sets. It also runs against the public site.
Evidence screenshots: `recovery/reports/death-facing--1.png` and
`recovery/reports/death-facing-1.png`.

## Fidelity pass: September 2026

The following corrections are traced to named methods in `recovery/reports/disassembly.txt`:

- `SO_Diamond pick` (0x6bf88) and `SO_Emerald pick` (0x6c014): money values 30 and 15.
- `SO_HelthPack pick` (0x6c0a0): do not consume at 100 health; otherwise heal 30, capped at 100.
- `shotgun_first fire` (0x14a24): waits for manual slide/pump operation after firing. Drag the slide left, or use the browser's R shortcut to pump before the next shot.
- `BaseEnemyManager hitted` (0x33f3c): non-piercing weapons damage the first enemy, not three targets for shotguns. The browser still approximates obstruction by scenery and firing range.
- `StageEnemyManager changeSpawnRule` (0x36008) and `playerMoved:playerDir:` (0x35d00): hallway spawns use separate left/right movement counters, reset to 240–479 pixels, and a population gate of at most four existing enemies. Spawn group composition and placement are still approximations.
- `StageEnemyManager makeZombieVelocity:` (0x35fb0): random speed around 37.3 with a 10-point spread on either side, multiplied by three for fast zombies.

The phone stage preserves the original 480×320 (3:2) landscape coordinate system; wider modern phones have letterboxing. Full-screen mode uses the browser API where available and a focused viewport fallback otherwise. The manifest and Apple standalone metadata support launching from the iPhone Home Screen without Safari toolbars. This does not add offline caching.

This is not yet a perfect port. Weapon state machines, burst timing, juice mixing/effects, enemy animation/attack timing, drop probabilities, and portions of the original UI still need translation. In particular, the native violet juice suppresses ammunition consumption; the current simplified shared power-up system does not reproduce that system. Existing saved currency and room state are preserved, not retroactively rebalanced.

## Source

- Catalog: https://relikd.github.io/ipa-archive/#bundleid=com.BoyAndWitch.GNZ
- Download: https://archive.org/download/iOSObscura/iPhoneOS%202/com.BoyAndWitch.GNZ/GNZ-(com.BoyAndWitch.GNZ)-1.3-(iOS_2.2.1)-589abf6c96c377ba1211f3824136fb00.ipa
- Bundle: `com.BoyAndWitch.GNZ`; display name `GNZ`; version `1.3`; minimum system `2.2.1`.
- IPA SHA-256: `0b1980010161e93f0a29eea90e15dc864520a76e0a328b7338e8d1a910b3d836`.

The downloaded IPA is preserved at `recovery/downloads/GNZ-1.3.ipa`; its contents are preserved under `recovery/unpacked`. The archive has two entries for this bundle/version. This recovery used the iOSObscura entry.

## What was recovered directly

The executable is a 32-bit ARMv6 Mach-O using Objective-C, OpenGL ES, and an early Cocos2D engine. Its `LC_ENCRYPTION_INFO.cryptid` is already **0** in this archive. No device-side decryption was necessary. No bundled executable, patch utility, or old network client was launched on the host.

The file contains 17,784 symbol-table entries, including debug and duplicate entries. `tools/disassemble.py` identifies 2,702 distinct function addresses with names and emits annotated ARM/Thumb assembly, including literal values and resolvable selector/string references. The disassembly can include literal pools decoded as instructions after function returns; it should be read as an analysis aid, not reassemblable source.

`tools/extract_constants.py` runs selected initializer code inside isolated Unicorn ARM memory. iOS/Objective-C calls are stubbed and their arguments recorded. Direct field assignments recover sprite sheets, rectangles, drawing order, broken-object variants, sound names, weapon capacities, prices, and animation dimensions. Captured `numberWithInt:` calls recover the three damage tiers. Weapon-part constructors provide filenames and positions. Raw reports can include placeholder pointers and fields that depend on stubbed framework behavior; the website consumes a selected subset, not entire emulated objects.

`recovery/decompiled/recovered_rules.c` contains selected routines manually translated from assembly into readable C. **This is partial decompilation; the original Objective-C project and full source code have not been recreated.** The web engine is newly written JavaScript.

| Recovered material | Result |
| --- | --- |
| Apple CgBI PNGs | 109 converted PNGs; raw DEFLATE, PNG filtering, BGRA → RGBA, alpha unpremultiplication |
| Audio | 51 WAV files copied unchanged and decoded successfully by Chrome |
| Room data | 113 templates, 3,286 object rows, preserved positions, scales, and sixth-column values |
| Static objects | 42 atlas definitions with exact initializer crops and drawing order |
| Weapons | All nine capacities, starting reserves, shop prices, damage tiers, and player-frame sizes |
| Gun assembly | Part files and constructor/default positions; single-piece body origin is reconstructed |
| Native analysis | Symbol table, named methods, annotated disassembly, selected readable C translations |

The room files' sixth column has two meanings: for doors/stairs it is the destination; for containers it can be a fixed item or zombie type. Both are used in the rebuild, including the original `SO_fastZombie` spelling variant. Runtime-generated drops and spawn rules are separate from these rows.

The original renderer uses a 480 × 320 landscape coordinate system with its origin at the bottom left. Browser drawing converts each object's Y with `320 - y`, mirrors by the stored scale, and crops the recovered atlases. The player's upper-body anchor is 42 pixels from the left of a frame, at original Y 227; the leg sprite is centered at Y 160.

## Progression and weapon evidence

`+[MapFileManager getPushedFloor:]` at `0x78d08` maps floors above 20 back to templates 1–20. `convertReadStage:stageName:` at `0x78d40` offsets the destination to the actual floor and changes destination floor 100 to 0. `SO_UpStairs tapped` at `0x671b4` treats `A0000` as the ending. The reconstruction follows this 99-floor route.

`Player start` initializes health to 100. `Player moveStarted:` increments horizontal position by 3, scheduled at 1/60 second by `Player walk:`; the web version integrates 180 units/second. `StageEnemyManager makeZombieHelth:isFast:` contains the recovered health tiers and random offset −2 through +2. Fast zombies use a base of 30. Zombie velocity contains constants 37.3, 10, and a 3× fast multiplier; the exact original random expression has not been reproduced.

| Internal weapon | Magazine | Starting reserve | Weapon price | Ammo price | Damage far / middle / near |
| --- | ---: | ---: | ---: | ---: | --- |
| pistol_first | 10 | 100 | 0 | 20 | 6 / 8 / 10 |
| pistol_second | 6 | 54 | 650 | 20 | 10 / 20 / 30 |
| shotgun_first | 12 | 36 | 300 | 40 | 10 / 20 / 40 |
| shotgun_second | 6 | 18 | 800 | 30 | 20 / 40 / 80 |
| subgun_first | 30 | 150 | 125 | 20 | 4 / 6 / 9 |
| subgun_second | 20 | 80 | 650 | 30 | 6 / 8 / 11 |
| assaultrifle_first | 24 | 72 | 500 | 50 | 8 / 10 / 13 |
| assaultrifle_second | 30 | 60 | 700 | 60 | 11 / 12 / 13 |
| sniper_first | 5 | 15 | 1200 | 40 | 35 / 40 / 45 |

These numeric values come from initializers, not guesses based on weapon appearance. The human-readable weapon labels are new. The original assault rifles have `numShotUnit=3`; the browser currently uses single-round firing at its reconstructed cadence rather than the original burst state machine.

## Reconstructed / simplified behavior

- This does not execute the native app in the browser or emulate the Objective-C/Cocos2D runtime.
- The original weapon-specific trigger, shotgun loading, magazine, bolt, and rotation state machines are only partially reconstructed. Tap/hold firing, a common magazine/slide gesture, and instant reload shortcuts are provided. Fire rates and reload durations are chosen for the browser.
- Enemy spawn timing, spawn placement, damage-distance thresholds, attack timing, and some animation sequences are approximate. Original collision and particle systems are replaced with simpler ones. A full playthrough has not been compared against a running iPod touch.
- Fixed container contents are preserved; random drops, monetary rewards, and a guaranteed key fallback are new balancing choices. Nearby containers can be searched with E/tap so an empty gun need not strand the player.
- The ingredient/blender system is simplified to one ingredient count and a 12-second power-up. First aid purchases and their healing amount are browser additions.
- All room templates can be inspected, including the B-series rooms. Survival uses the corridor layout with a new increasing spawn schedule. The original online scoreboard is omitted; a local best kill count is recorded.
- The ending uses a result panel and the recovered helicopter sound. Original cutscene art is available in the gallery, but the complete opening/ending choreography is not ported.
- The PVR glow texture, compiled nib, executable patch, signing material, and iTunes artwork remain in the raw recovery and are not required by the website.
- Local saves use a new JSON schema and cannot import the original iOS save files.

## Validation

`node tests/core.test.cjs` checks sprite bounds, every adventure portal across 99 floors, movement/pause behavior, combat, ammo conservation, manual reload, locked doors, shop prices, saves with dropped items, new-game reset, the ending route, and all 113 room templates.

`tests/browser_smoke.py` checks direct `file://` loading in desktop Chrome and a mobile touch viewport; exercises movement, firing, reload, inventory, save/continue, room exploration, and the gallery; decodes all 51 sounds; and rejects JavaScript page errors. Screenshots and JSON results are retained under `recovery/reports`.

`tests/static_smoke.py` additionally serves the packaged site over HTTP and checks the menu, all nine weapon renderings, manual magazine gestures, and multi-touch movement. These are functional checks, not a claim of full behavioral equivalence to the original game.

## Reproduction

1. Download the exact IPA above and compare its SHA-256.
2. Inspect and unpack the ZIP into `recovery/unpacked`.
3. Run `tools/recover.py` with Pillow to convert images, copy sounds, and parse levels / Mach-O metadata.
4. Run `tools/disassemble.py` with Capstone for annotated disassembly.
5. Run `tools/extract_constants.py` with Capstone and Unicorn for object/weapon data.
6. Run `tools/extract_animations.py` with Capstone and Unicorn for animation and pose traces.
7. Run the tests, then `tools/package.py` for the standalone website archive.

The website itself only needs a browser. Recovery tools and the original IPA are not shipped in the website ZIP.
