Commodore ROMs are copyrighted and can no longer ship inside the APK. MainActivity now detects missing ROMs on first launch and blocks startup with a dialog offering two paths: pick files via the system file picker, or download the official VICE 3.8 tarball and extract the four ROMs from it client-side (minimal USTAR reader, no extra deps). - Drop the extractViceRoms Gradle task and asset bundling; add res/extract_roms.sh for local sideloading during development instead - gitignore keystores/keystore.properties ahead of a signed release - Move architecture.md into docs/, refresh it for the screen-mirroring and watch-input additions, and add accompanying Mermaid diagrams - Add docs/debugging.md and docs/publish.md (Play Store release notes, ROM-import compliance rationale)
5.3 KiB
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
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:
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.1notlocalhost— Core for Pebble's JS runtime does not resolvelocalhost. - Use numeric keys in
sendAppMessage({ 0: value }not{ 'TIME': value }) — symbolic key resolution frompackage.jsonmessageKeys 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:
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:
- Calls
app/src/main/jni/build_vice.shwith$NDKset from the Android Studio SDK config - The script unpacks the tarball, cross-compiles VICE headless for ARM64 and x86_64, and produces
vice-libs/<abi>/libvice.a CMakeLists.txtauto-detects the library viaEXISTSand 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/CMakeLists.txt |
NDK build; conditionally links libvice.a when HAVE_VICE_SRC=1 |
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:
engine.loadDisk(getExternalFilesDir(null)!!.absolutePath + "/schwert_und_magie_1.d64")