daefed88b1
- Cross-compile nibtools for Android NDK; download all 8 episode disks
from the Internet Archive C64 Preservation Project as NBZ and convert
to G64 via JNI (throwaway-pthread to contain nibconv's exit() calls)
- Bundle manual transcription as APK asset; 📖 button opens scrollable
viewer with Seite→Absatz→Zeile→Wort lookup hierarchy documented
162 lines
7.3 KiB
Markdown
162 lines
7.3 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Project goal
|
||
|
||
Port the classic German C64 RPG series **Schwert und Magie** (8 episodes, German Design Group, 1989–1992) to a Pebble Time 2 smartwatch. The phone runs a VICE C64 emulator as a companion app; the watch displays the game and sends button presses back.
|
||
|
||
## Repository layout
|
||
|
||
```
|
||
SchwertUndMagieOnPebbleWatchApp/ Pebble watchapp (C + PebbleKit JS)
|
||
SchwertUndMagieOnPebbleCompanionApp/ Android companion app (Kotlin + NDK)
|
||
versions/ Original .d64 disk images (4 disks, 8 episodes)
|
||
docs/ Background notes
|
||
```
|
||
|
||
## Watch app
|
||
|
||
### Build & install
|
||
|
||
```bash
|
||
cd SchwertUndMagieOnPebbleWatchApp
|
||
pebble build # all platforms
|
||
pebble install --phone <phone-ip> # sideload via Pebble/Core app on the phone
|
||
pebble logs --phone <phone-ip> # stream JS + C app logs
|
||
```
|
||
|
||
In headless / CI environments, add `--vnc` to every emulator command:
|
||
```bash
|
||
pebble install --emulator emery --vnc
|
||
pebble screenshot --vnc --scale 6 --no-open screenshot.png
|
||
pebble emu-button --emulator emery --vnc click select
|
||
```
|
||
|
||
### Key files
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `src/c/SchwertUndMagieOnPebbleFrontend.c` | Single-file C watchapp |
|
||
| `src/pkjs/index.js` | PebbleKit JS — runs inside Core for Pebble on the phone |
|
||
| `package.json` | UUID, target platforms, `messageKeys` |
|
||
|
||
### Communication protocol
|
||
|
||
AppMessage keys (defined in both C and JS):
|
||
|
||
| Key | Direction | Value |
|
||
|---|---|---|
|
||
| `0` (TIME) | phone → watch | `"HH:mm:ss"` string (later: C64 framebuffer) |
|
||
| `1` (COMMAND) | watch → phone | button name string (`"UP"`, `"DOWN"`, `"SELECT"`) |
|
||
|
||
The watch UUID is `8039ba8c-f1e8-4620-838d-650fa35335b7`.
|
||
|
||
### PebbleKit JS notes (Core for Pebble quirks)
|
||
|
||
- **Use `127.0.0.1` not `localhost`** — Core for Pebble's JS runtime does not resolve `localhost`.
|
||
- **Use numeric keys in `sendAppMessage`** (`{ 0: value }` not `{ 'TIME': value }`) — symbolic key resolution from `package.json` messageKeys is unreliable with Core for Pebble.
|
||
- PebbleKit Android (`com.getpebble.android.*`) does **not** work with Core for Pebble; all phone↔watch communication goes through PebbleKit JS + the HTTP server.
|
||
|
||
## Android companion app
|
||
|
||
### Build
|
||
|
||
Open `SchwertUndMagieOnPebbleCompanionApp/` in Android Studio, or:
|
||
|
||
```bash
|
||
cd SchwertUndMagieOnPebbleCompanionApp
|
||
./gradlew assembleDebug
|
||
./gradlew installDebug
|
||
```
|
||
|
||
Android SDK is at `/opt/android-sdk`.
|
||
|
||
### Communication architecture
|
||
|
||
```
|
||
Watch (AppMessage BT)
|
||
↕
|
||
PebbleKit JS — index.js runs inside Core for Pebble
|
||
↕ HTTP on 127.0.0.1:8888
|
||
Android companion app — NanoHTTPD server
|
||
GET /time → {"time":"HH:mm:ss"}
|
||
GET /key?cmd=UP → logs keystroke
|
||
```
|
||
|
||
`MainActivity` starts both the NanoHTTPD server and the VICE emulator loop thread. All HTTP callbacks marshal to the main thread via `mainHandler`.
|
||
|
||
### VICE integration (NDK)
|
||
|
||
VICE 3.8 tarball is at `SchwertUndMagieOnPebbleCompanionApp/res/vice-3.8.tar.gz`.
|
||
|
||
**Without VICE built** (first state): `vice_jni.c` fills the framebuffer with a placeholder blue screen; the app builds and runs normally.
|
||
|
||
**VICE compilation is automatic.** The Gradle `buildVice` task runs before every native CMake build. On the first build it:
|
||
1. Calls `app/src/main/jni/build_vice.sh` with `$NDK` set from the Android Studio SDK config
|
||
2. The script unpacks the tarball, cross-compiles VICE headless for ARM64 and x86_64, and produces `vice-libs/<abi>/libvice.a`
|
||
3. `CMakeLists.txt` auto-detects the library via `EXISTS` and links it in — no manual flags needed
|
||
|
||
The NDK must be installed: **Android Studio → SDK Manager → SDK Tools → NDK (Side by side)**.
|
||
|
||
After the first successful build, subsequent builds skip the VICE compilation step entirely (outputs are up-to-date).
|
||
|
||
**C64 ROM files** (`kernal`, `basic`, `chargen`, `1541`) are copyrighted and are
|
||
**not** bundled in the APK. On first launch, `MainActivity` checks the app's
|
||
external files dir for these four files; if any are missing, it shows a blocking
|
||
dialog that lets the user import their own legally-obtained ROM dump via the
|
||
system file picker (see `docs/publish.md` §0). `res/extract_roms.sh` can pull
|
||
the ROMs out of `res/vice-3.8.tar.gz` into `res/roms/` for local sideloading
|
||
during development, but nothing in the Gradle build copies them into the APK.
|
||
|
||
### Key Kotlin/C files
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `MainActivity.kt` | Emulator loop thread (50 fps), NanoHTTPD server, keyboard event logging |
|
||
| `C64Engine.kt` | JNI interface to VICE — init, runFrame, getVideoBuffer, injectKey, loadDisk |
|
||
| `C64DisplayView.kt` | SurfaceView — blits 320×200 ARGB framebuffer, scaled with correct aspect ratio |
|
||
| `C64KeyboardView.kt` | Multi-touch virtual C64 keyboard; fires `KeyEventListener` on press/release |
|
||
| `jni/vice_jni.c` | JNI wrapper — custom VICE video canvas writes frames to `g_framebuf[320×200]` |
|
||
| `jni/nibconv_jni.c` | JNI wrapper around nibtools' nibconv — converts downloaded NIB/NBZ dumps to G64 |
|
||
| `jni/CMakeLists.txt` | NDK build; conditionally links `libvice.a`/`libnibtools.a` when their `HAVE_*` flags are set |
|
||
|
||
### Disk images
|
||
|
||
The four original `.d64` disk images are in `versions/`. Copy the relevant image to the
|
||
same external files directory as the ROMs (via USB or adb), then load via:
|
||
```kotlin
|
||
engine.loadDisk(getExternalFilesDir(null)!!.absolutePath + "/schwert_und_magie_1.d64")
|
||
```
|
||
|
||
Alternatively, the in-app **Download Disks** button (next to Import Disks) fetches all
|
||
8 episode disks from the Internet Archive's C64 Preservation Project and converts them
|
||
to G64 automatically — see "Disk download (nibtools)" below.
|
||
|
||
### Disk download (nibtools)
|
||
|
||
The Internet Archive's C64 Preservation Project only has these disks as `.nbz`
|
||
(nibtools' compressed raw-GCR NIB format) — not `.d64`. `MainActivity.downloadDisks()`
|
||
fetches each of the 8 `.nbz` files (one per disk side) and runs them through nibtools'
|
||
`nibconv` to produce G64 images (VICE attaches G64 exactly like D64, detecting the
|
||
format from file content, not the extension). G64 was chosen over the lossy D64
|
||
reconstruction nibconv also supports, since these old dumps aren't always cleanly
|
||
sector-readable — G64 preserves whatever the original drive actually saw.
|
||
|
||
Only `nibconv`'s pure file-format conversion code is built (`gcr.c prot.c fileio.c
|
||
crc.c md5.c lz.c nibconv.c`) — nibtools' hardware-access tools (`nibread`/`nibwrite`,
|
||
which talk to a real 1541 over OpenCBM) are not needed and not built. `jni/nibtools_android/`
|
||
stubs out the OpenCBM header so the unused declarations that pull it in still compile.
|
||
|
||
**nibtools compilation is automatic**, the same way as VICE:
|
||
1. The Gradle `buildNibtools` task runs before every native CMake build
|
||
2. It calls `app/src/main/jni/build_nibtools.sh`, which unpacks `res/nibtools-<rev>.tar.gz`
|
||
and cross-compiles the files above into `nibtools-libs/<abi>/libnibtools.a`
|
||
3. `CMakeLists.txt` auto-detects the library via `EXISTS` and links it in
|
||
|
||
nibconv's `main()` is renamed to `nibtools_nibconv_main` at compile time (`-Dmain=...`)
|
||
so it can coexist in the same shared library as the JNI entry points. It's invoked
|
||
from `nibconv_jni.c` on a throwaway pthread — nibconv calls `exit()` on malformed input,
|
||
which is wrapped (`-Wl,--wrap=exit`) to `pthread_exit()` so a bad conversion only kills
|
||
that disposable thread, never the app process or the calling JNI thread.
|