# SyncRPG door (EasyRPG) Implementation Plan — M1–M4

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** A Synchronet door that plays RPG Maker 2000/2003 games over a terminal
via a vendored EasyRPG Player, driving the shared `termgfx_termio` truecolor path,
with the official English Yume Nikki 0.10a playable end-to-end (movement, the
effects/menu system, save/load) as the v1 proof.

**Architecture:** A dedicated door mirroring syncscumm: a vendored+pruned EasyRPG
Player under `src/doors/syncrpg/easyrpg/`, and a `door/` backend
(`BaseUi_termgfx`, the analogue of syncscumm's `OSystem_Synchronet`) that
subclasses EasyRPG's `BaseUi` and drives `termgfx_termio` — handing its 32-bit
screen surface to `present_rgbx` (zero-copy), translating `termgfx` key events to
EasyRPG `Input::Keys`, and feeding EasyRPG's `GenericAudio` PCM to the shared
`termgfx_stream`. Game data + install ship the syncscumm way (per-game
`getdata.js` + `install-xtrn.ini`).

**Tech Stack:** EasyRPG Player + inline liblcf + hand-built inih (C++), CMake for
the vendored engine, the sbbs door build harness (`build.sh`/`module.mk`), the
shared `libtermgfx` (present_rgbx, keymode, audio stream, door32, sbbs_node),
Synchronet JS (`HTTPRequest` + native `Archive`) for `getdata.js`.

## Global Constraints

- **The shared `termgfx_termio` is FROZEN API here** — this plan consumes
  `termgfx_termio_present_rgbx`, `_init`/`_shutdown`/`_next_event`/`_tick`/
  `_quit_requested`/`_hung_up`/`_menu_requested`/`_set_menu_key`, `TERMGFX_KEY_*`,
  `termgfx_input_event_t`, and the shared `termgfx_stream_*`. It does NOT modify
  termgfx except where a carry-forward item (below) explicitly requires it, and
  any such change re-runs syncscumm's regression gate (boot 9/9, unit 20/20, PPM
  golden `b2cbf878…` — NOT raw sixel bytes).
- **Frame hand-off is zero-copy R,G,B,X.** The backend sets EasyRPG's
  `main_surface` format via `Bitmap::SetFormat(Bitmap::ChooseFormat(
  format_R8G8B8A8_n()))` (or the BGRA sibling that yields R,G,B,X in memory —
  pin during M1 to match `present_rgbx`'s byte order) so `GetDisplaySurface()`'s
  buffer feeds `present_rgbx(xrgb, w, h)` with no repack.
- **CARRY-FORWARD from M0 (rgbx frames are dropped, not retained).**
  `present_rgbx` has no `tick()` retry buffer, so a back-pressure-gated frame on a
  *static* screen (an EasyRPG menu that stops redrawing) can show stale content
  until the engine's next redraw. M1 MUST determine EasyRPG's redraw cadence for
  static screens; if it does not re-present them, add an rgbx retain/tick path to
  termgfx (a termgfx change → re-run syncscumm's regression gate).
- **getdata.js is portable Synchronet JS**, NOT a shell script — it runs on
  Windows and Linux via `jsexec`. Use `HTTPRequest.Download` + `new
  Archive(zip).extract(...)` (the `syncdrascula`/`synclure` pattern). Never
  re-host game data; the fetched tree is git-ignored (only a sha256 manifest is
  committed).
- **Pinned flagship source:** rmarchiv.de "Yume Nikki — English 0.10a (Steam)",
  a plain ZIP, page `rmarchiv.de/games/1089`, download id `4174`, zip sha256
  `82af2204e2bb39597ac2454d53fc15b2651ea8ec8a5c7258c1ea45ac98e2124d` (~79 MB),
  tree under `yumenikki/`, ASCII asset filenames (no transcode/`--encoding`).
  The direct URL is **tokened** (`/games/download/4174/<token>`; bare
  `/games/download/4174` returns HTML) — M4 resolves a stable URL or scrapes it.
- **Per-user saves** at `####>/syncrpg/<game>/` (zero-padded user
  number from the DOOR32 drop file). Locate all dirs via `system.*_dir`, never
  hardcoded.
- **Portable C/C++** (GCC/Clang/MSVC). Only GCC is exercised here; the Windows
  MSVC project is generated by EasyRPG's build tooling — flag it unverified.
- **Hand-formatted** `door/*.cpp/.h`, `test/*.sh` (tabs, match syncscumm's style);
  do NOT run uncrustify on them. Reserved-identifier rule (no leading-underscore-
  capital / `__` guards). Commit direct to master; body wrapped ≤78 (verify with
  `awk 'length>78'`); do NOT push without the human's OK; never `--amend` the
  shared index.

## File Structure

```
src/doors/syncrpg/
  easyrpg/                vendored + pruned EasyRPG Player + inline liblcf + inih
  door/
    syncrpg.cpp           main(): DOOR32 parse, termgfx init, EasyRPG Player run loop
    ui_term.{cpp,h}       BaseUi_termgfx : BaseUi (CreateUi factory + the 3 hot paths)
    video_term.{cpp,h}    UpdateDisplay: main_surface -> termgfx_termio_present_rgbx
    input_term.{cpp,h}    ProcessEvents: termgfx key events -> EasyRPG Input::Keys
    audio_term.{cpp,h}    GenericAudio PCM -> termgfx_stream_*
    module.mk, CMakeLists.txt
  build.sh, deploy.js, PROVENANCE.md, README.md, msvc/
  getdata.js, install-xtrn.ini, syncrpg.example.ini
  test/  boot_yumenikki.sh, games/ (gitignored)
```

---

## Milestone M1 — vendor + boot + video

### Task 1: Vendor and prune EasyRPG into the tree

**Files:**
- Create: `src/doors/syncrpg/easyrpg/` (vendored tree),
  `src/doors/syncrpg/PROVENANCE.md`, `src/doors/syncrpg/.gitignore`

**Interfaces:**
- Produces: a buildable EasyRPG source tree + inline liblcf under `easyrpg/`,
  with the SDL platform backend retained as a reference for `ui_term`.

- [ ] **Step 1: Copy the scouted clone in, at a pinned commit.** From the working
  EasyRPG clone (scout used `github.com/EasyRPG/Player` + inline liblcf), record
  the exact Player and liblcf commit SHAs, then copy the source into
  `src/doors/syncrpg/easyrpg/`. Do NOT copy `.git/`.

- [ ] **Step 2: Prune.** Remove, from the vendored copy: `src/platform/*` EXCEPT
  `src/platform/sdl/` (kept as the `ui_term` reference), `resources/`, `tests/`,
  `docs/`, `.github/`, `builds/`, and any bundled game data. Keep `src/` (engine),
  `lib/liblcf`, `CMakeLists.txt`, and the inih sources. Target ≈6.3 MB.

- [ ] **Step 3: PROVENANCE.md** — record upstream URLs, the pinned Player +
  liblcf commit SHAs, the license (GPLv3), what was pruned, and that no game data
  is bundled. Mirror `src/doors/syncscumm/PROVENANCE.md`'s format.

- [ ] **Step 4: `.gitignore`** the build output + `test/games/` (fetched data).

- [ ] **Step 5: Commit** the vendored tree (one commit; large but mechanical).
  Message notes the pinned upstream SHAs.

### Task 2: Door build scaffold — build EasyRPG + link libtermgfx

**Files:**
- Create: `src/doors/syncrpg/build.sh`, `door/module.mk`,
  `door/CMakeLists.txt` (or the EasyRPG-CMake integration), `door/syncrpg.cpp`
  (a minimal `main` stub)

**Interfaces:**
- Consumes: the vendored EasyRPG CMake, `libtermgfx.a`.
- Produces: a `build/syncrpg` binary that links EasyRPG + libtermgfx (may not yet
  drive a game).

- [ ] **Step 1: `build.sh`** mirroring `syncscumm/build.sh`: stage-1 builds
  `libtermgfx` + xpdev; then configure EasyRPG with the scouted flags
  (`-DPLAYER_TARGET_PLATFORM=SDL2`, `-DPLAYER_BUILD_LIBLCF=ON`, inih from source),
  but pointed at our `door/` backend instead of the SDL `main`. Link `libtermgfx.a`
  + `libxpdev_static.a`.

- [ ] **Step 2: minimal `syncrpg.cpp`** — a `main()` that just calls
  `termgfx_termio_init(argc, argv)` and `termgfx_termio_shutdown()` and exits, to
  prove the link.

- [ ] **Step 3: Build.** Run: `./build.sh`. Expected: `build/syncrpg` links clean.
  Note MSVC unverified.

- [ ] **Step 4: Commit.**

### Task 3: `BaseUi_termgfx` skeleton — boot a game headless

**Files:**
- Create: `door/ui_term.{cpp,h}`; Modify: `door/syncrpg.cpp`

**Interfaces:**
- Consumes: EasyRPG `BaseUi` (`baseui.h`: `ProcessEvents()`, `UpdateDisplay()`,
  `GetDisplaySurface()` → `main_surface`, `CreateUi()` factory), `Player::Init`/
  `Player::Run` entry points.
- Produces: `class BaseUi_termgfx : public BaseUi` + a `BaseUi::CreateUi`
  path that returns it; `main()` runs `Player::Init(args); Player::Run();`.

- [ ] **Step 1: `BaseUi_termgfx`** subclass: constructor calls the base with
  the game resolution and creates `main_surface` via
  `Bitmap::Create(w, h, …)` in the format from Step 2 of the constraints; stub
  `ProcessEvents()` (drives `termgfx_termio_quit_requested()`/`_hung_up()` →
  `Player::exit_flag`) and `UpdateDisplay()` (empty for now); wire the
  `CreateUi()` factory (EasyRPG selects the UI via the platform build — since we
  dropped the SDL `main`, our `main` sets up the synchronet UI directly, or the
  `BaseUi::CreateUi` switch returns ours). Determine the exact hook while reading
  `baseui.cpp`/`player.cpp`; document it in the report.

- [ ] **Step 2: `main()`** — parse DOOR32 via `termgfx_termio_init`, build the
  EasyRPG argv (`--project-path <game>`, `--save-path <per-user>`, no-audio for
  now, `SDL_VIDEODRIVER=dummy`-equivalent since our UI is headless), then
  `Player::Init` + `Player::Run`; `termgfx_termio_shutdown()` on exit.

- [ ] **Step 3: Boot test.** With a fetched Yume Nikki tree, run `build/syncrpg`
  headless against it; confirm it reaches the title without crashing (stderr log
  clean; the engine loads `RPG_RT.ldb`/`.lmt`). Nothing renders yet.

- [ ] **Step 4: Commit.**

### Task 4: Video — `present_rgbx` the screen; Yume Nikki renders

**Files:**
- Create: `door/video_term.{cpp,h}`; Modify: `door/ui_term.cpp`
  (`UpdateDisplay`), `door/CMakeLists.txt`/`module.mk`

**Interfaces:**
- Consumes: `GetDisplaySurface()`'s 32-bit `main_surface` (R,G,B,X),
  `termgfx_termio_present_rgbx(const uint8_t*, int, int)`,
  `termgfx_termio_tick()`.
- Produces: `video_term_present(const Bitmap& surf)` → `present_rgbx`.

- [ ] **Step 1: Pin the pixel format** — confirm which `format_*_n` constant
  yields R,G,B,X in memory for `present_rgbx` (write a 1-pixel probe: set a known
  color, read the 4 bytes). Set `main_surface` to that format in the ctor.

- [ ] **Step 2: `UpdateDisplay()`** — `video_term_present(*GetDisplaySurface())`:
  hand the surface's pixel pointer + `w`/`h` to `present_rgbx`. Call
  `termgfx_termio_tick()` each `ProcessEvents` for pacing.

- [ ] **Step 3: Resolve the static-screen carry-forward.** Verify (from the boot
  test's frame stream) whether EasyRPG re-calls `UpdateDisplay` on a static menu.
  If it does NOT re-present a gated static frame, add an rgbx retain/tick to
  termgfx (separate sub-task with the syncscumm regression gate) — else document
  that the engine's cadence covers it.

- [ ] **Step 4: Boot render test** `test/boot_yumenikki.sh` — headless, capture
  the terminal output (the `SYNCSCUMM_SIXELOUT`/dump mechanism termgfx already
  provides), decode the first title frame, assert it is 320×240 non-blank and
  matches the known title (the scout's `yn_title.png` is the reference). Frame
  count ≥ N.

- [ ] **Step 5: Commit.** M1 deliverable: Yume Nikki renders over the terminal.

---

## Milestone M2 — keyboard

### Task 5: `input_term` — termgfx keys → EasyRPG `Input::Keys`

**Files:**
- Create: `door/input_term.{cpp,h}`; Modify: `door/ui_term.cpp` (`ProcessEvents`)

**Interfaces:**
- Consumes: `termgfx_termio_next_event(termgfx_input_event_t*)` (returns 0 when
  empty), `TERMGFX_KEY_*`, `termgfx_termio_menu_requested()`; EasyRPG's key state
  array that `BaseUi` exposes (`keys[]` indexed by `Input::Keys::InputKey`).
- Produces: `input_term_pump()` translating the termgfx FIFO into EasyRPG key
  up/down state.

- [ ] **Step 1: The mapping table** (from Yume Nikki's README + EasyRPG
  `Input::Keys`): arrows → `UP/DOWN/LEFT/RIGHT`; Enter / `Z` / Space →
  `DECISION`; Esc → `CANCEL`; and the raw number keys EasyRPG exposes for the
  game's own numpad reads (Numpad 9 wake, 1/3 effect actions, 5 drop) → the
  corresponding `Input::Keys` (`N9`/`N1`/`N3`/`N5` or `KP9`… — pin against
  `input_buttons.h`). Reserve termgfx's Ctrl-S/Ctrl-Q hotkeys (termgfx consumes
  them before the FIFO).

- [ ] **Step 2: `ProcessEvents()`** drains `termgfx_termio_next_event` and sets
  the EasyRPG key-state booleans (press on key-down event, release on key-up; if
  termgfx delivers discrete presses only, synthesize a 1-frame press+release as
  syncscumm's mouse tap does).

- [ ] **Step 3: Test** — a `test/` input unit test feeding known `TERMGFX_KEY_*`
  events and asserting the resulting EasyRPG key state, plus a manual movement
  check in the boot harness (walk the character). Movement + menu open/close work.

- [ ] **Step 4: Commit.** M2 deliverable: Yume Nikki is explorable.

---

## Milestone M3 — audio

### Task 6: `audio_term` — EasyRPG audio → termgfx_stream

**Files:**
- Create: `door/audio_term.{cpp,h}`; Modify: `door/ui_term.cpp` (audio wiring),
  build files

**Interfaces:**
- Consumes: EasyRPG's `GenericAudio` (software mixer producing S16 stereo PCM),
  the shared `termgfx_stream_*` module (the same one syncretro/syncscumm feed).
- Produces: `audio_term` pulling mixed PCM each tick and pushing it to
  `termgfx_stream`.

- [ ] **Step 1: Select `GenericAudio`** as EasyRPG's audio backend (it mixes to a
  PCM buffer with no platform audio device — the headless path). Use the built-in
  **fmmidi** MIDI synth so the door is self-contained (no soundfont). Confirm
  Yume Nikki's formats (largely WAV) play.

- [ ] **Step 2: Pump** — each frame, pull `GenericAudio`'s mixed PCM (rate/format
  it produces) and feed `termgfx_stream_*` (resample/convert to the stream's
  contract as syncscumm's `audio_term` does). Gate on
  `termgfx_termio_audio_available()`.

- [ ] **Step 3: Test** — extend `boot_yumenikki.sh` to assert audio bytes reach
  the wire on a scene with BGM; a live check confirms music/SFX. 

- [ ] **Step 4: Commit.** M3 deliverable: Yume Nikki has sound.

---

## Milestone M4 — package + live

### Task 7: `getdata.js` — portable fetch of the pinned source

**Files:**
- Create: `src/doors/syncrpg/getdata.js`, `test/games/.gitkeep`

**Interfaces:**
- Consumes: Synchronet JS `HTTPRequest` + `Archive` (the `syncdrascula`/
  `synclure` pattern — `load('http.js')`, `req.Download`, `new
  Archive(tmp).extract(root, with_path, overwrite, 0, files…)`).

- [ ] **Step 1: Resolve the tokened URL.** The direct link
  `rmarchiv.de/games/download/4174/<token>` is not stable; the bare id returns
  HTML. `getdata.js` first `HTTPRequest.Get`s the download/landing page for id
  4174, regex-extracts the current direct-download href, then `Download`s it —
  OR uses a confirmed-stable URL if one is found. Verify the fetched zip's sha256
  equals `82af2204…` (the committed manifest) and fail loudly on mismatch.

- [ ] **Step 2: Extract** with `new Archive(zip).extract(<gamedir>, …)` into the
  game data dir (located via `system.*_dir`), yielding the `yumenikki/` tree.
  ASCII filenames — no transcode. Verify `RPG_RT.ldb` landed.

- [ ] **Step 3: Run** `jsexec ../xtrn/syncrpg/getdata.js` (mirroring the sibling
  fetchers' CLI); confirm the game tree materializes and the marker file exists.

- [ ] **Step 4: Commit** `getdata.js` + the sha256 manifest (NOT the game data).

### Task 8: install-xtrn + per-user saves + example config

**Files:**
- Create: `src/doors/syncrpg/install-xtrn.ini`, `syncrpg.example.ini`,
  `deploy.js`, `README.md`; Modify: `door/syncrpg.cpp` (save-path wiring)

- [ ] **Step 1: Per-user save path** — in `syncrpg.cpp`, build
  `####>/syncrpg/<game>/` (zero-padded user number from the DOOR32
  drop file via termgfx; `system.data_dir`) and pass it to EasyRPG as
  `--save-path`. Create the dir if absent.

- [ ] **Step 2: `install-xtrn.ini`** registering Yume Nikki as its own `xtrn`
  menu entry (mirror `xtrn/synclure/install-xtrn.ini`: `type=12`, native/binary/
  multiuser/nodisplay settings, the launch command with the game path;
  ≤`LEN_CMD` 100 chars). `deploy.js`/`README.md` mirror syncscumm.

- [ ] **Step 3: Save/load round-trip test** — start, save in-game, quit, relaunch,
  load; confirm the save lands under the per-user dir and restores.

- [ ] **Step 4: Commit.**

### Task 9: Live sysop confirm

- [ ] **Step 1:** The sysop installs via `getdata.js` + `install-xtrn`, plays Yume
  Nikki on the real BBS: movement, the effects/menu system, save/load, quit,
  Ctrl-S stats, Ctrl-Q quit. Confirm render/input/audio all behave.
- [ ] **Step 2:** Address any live findings; then the final whole-branch review.

---

## Verification summary

- **M1:** `test/boot_yumenikki.sh` (headless title render, decoded-frame check vs
  the scout reference); the static-screen carry-forward resolved.
- **M2:** input unit test + manual movement/menu.
- **M3:** audio-on-the-wire assertion + live check.
- **M4:** `getdata.js` fetch+sha, save/load round-trip, sysop live play-test.
- **Any termgfx change** (only if the rgbx-retain carry-forward forces one)
  re-runs syncscumm's regression gate (boot 9/9, unit 20/20, PPM golden — not raw
  sixel bytes).
- **Portability:** the door is C++ behind EasyRPG's build; MSVC (create_project)
  is generated but not verified on this host — flag it, as syncscumm does.
