Add screen mirroring, watch input, splash screen, and architecture docs

Mirrors the C64 text screen to the Pebble watch (with game-specific
  umlaut handling), adds a SELECT-triggered key wheel for sending
  navigation input back to the emulator, shows a splash screen on watch
  launch, and documents the resulting end-to-end architecture.
This commit is contained in:
ml
2026-06-21 19:15:19 +02:00
parent 463c85e62f
commit 89c99b50e2
9 changed files with 545 additions and 42 deletions
@@ -58,6 +58,9 @@ class C64Engine {
/** Monotonically increasing frame counter — incremented once per rendered C64 frame. */
external fun getFrameCount(): Int
/** Current 40x25 C64 text screen as ASCII, rows separated by '\n'. */
external fun getScreenText(): String
companion object {
init { System.loadLibrary("vice_jni") }
@@ -29,6 +29,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import fi.iki.elonen.NanoHTTPD
import org.json.JSONObject
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
@@ -292,6 +293,33 @@ class MainActivity : AppCompatActivity() {
}
}
// ---- watch key wheel ------------------------------------------------
// Labels sent by the watch's SELECT key wheel (see k_wheel_items in
// SchwertUndMagieOnPebbleFrontend.c) — movement in this game is mostly
// done with number keys, plus RETURN/SPACE to confirm.
private val watchKeyMap: Map<String, List<Int>> = mapOf(
"1" to listOf(C64Engine.KEY_1), "2" to listOf(C64Engine.KEY_2),
"3" to listOf(C64Engine.KEY_3), "4" to listOf(C64Engine.KEY_4),
"5" to listOf(C64Engine.KEY_5), "6" to listOf(C64Engine.KEY_6),
"7" to listOf(C64Engine.KEY_7), "8" to listOf(C64Engine.KEY_8),
"9" to listOf(C64Engine.KEY_9), "0" to listOf(C64Engine.KEY_0),
"RETURN" to listOf(C64Engine.KEY_RETURN),
"SPACE" to listOf(C64Engine.KEY_SPACE),
)
// Pulses a key press+release in response to a watch wheel selection.
// The companion HTTP server calls this off the main thread, so the
// release is posted via mainHandler like the rest of this class's
// cross-thread UI/engine touchpoints.
private fun injectWatchKey(label: String) {
val codes = watchKeyMap[label] ?: return
for (code in codes) engine.injectKey(code, true)
mainHandler.postDelayed({
for (code in codes) engine.injectKey(code, false)
}, 80)
}
// ---- HTTP server (watch bridge) -----------------------------------------
private fun startHttpServer() {
@@ -300,7 +328,9 @@ class MainActivity : AppCompatActivity() {
onKey = { cmd ->
Log.d(TAG, "Watch key: $cmd")
mainHandler.post { appendLog("WATCH: $cmd") }
}
injectWatchKey(cmd)
},
getScreenText = { engine.getScreenText() }
)
try {
server?.start()
@@ -827,7 +857,8 @@ class CompanionServer(
port: Int,
private val timeFormat: SimpleDateFormat,
private val onTimeFetched: (String) -> Unit,
private val onKey: (String) -> Unit
private val onKey: (String) -> Unit,
private val getScreenText: () -> String
) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession): Response {
@@ -844,6 +875,10 @@ class CompanionServer(
if (cmd.isNotEmpty()) onKey(cmd)
newFixedLengthResponse(Response.Status.OK, "text/plain", "ok")
}
"/screen" -> {
val body = JSONObject().put("text", getScreenText()).toString()
newFixedLengthResponse(Response.Status.OK, "application/json", body)
}
else -> newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "not found")
}
response.addHeader("Access-Control-Allow-Origin", "*")
@@ -78,6 +78,8 @@ extern int __real_console_init(void);
#include "autostart.h"
#include "attach.h"
#include "interrupt.h"
#include "charset.h"
#include "mem.h"
/* Drive LED state.
* g_io_frames: bumped by __wrap_serial_trap_receive; keeps LED lit ~100 ms after
@@ -808,6 +810,64 @@ JNI_FN(jint, getFrameCount)(JNIEnv *env, jobject obj) {
return (jint)g_frame_count;
}
/* Schwert und Magie uploads a custom character set that redefines a handful of
* otherwise-unused PETSCII screen codes (punctuation/graphics glyphs nobody
* needs in German text) to draw umlauts instead. VICE's stock charset tables
* know nothing about this game-specific remap, so those codes must be
* special-cased to the correct UTF-8 character before falling back to the
* standard screencode->PETSCII->ASCII conversion.
* Confirmed by observation — a consecutive run in the unshifted symbol row:
* screencode 0x1B (stock '[') -> ä, 0x1C (stock '£') -> ö, 0x1D (stock ']') -> ü,
* 0x1E (stock '↑') -> ß.
* TODO: add the uppercase variants (Ä, Ö, Ü) once their screen codes are
* identified — likely in the shifted/alternate charset bank. */
static const struct { uint8_t screencode; const char *utf8; } k_char_overrides[] = {
{ 0x1B, "\xC3\xA4" }, /* ä */
{ 0x1C, "\xC3\xB6" }, /* ö */
{ 0x1D, "\xC3\xBC" }, /* ü */
{ 0x1E, "\xC3\x9F" }, /* ß */
};
/* Returns the 40x25 C64 text screen as UTF-8, rows separated by '\n'.
* Reads the fixed default screen address $0400 via mem_read_screen — the same
* helper autostart.c uses to check for KERNAL "READY." text — since this game
* never relocates the VIC-II screen pointer. Screen codes are converted to
* PETSCII then ASCII via VICE's own charset tables (after k_char_overrides is
* checked first); unmappable glyphs (the C64's graphics characters) become '.'. */
JNI_FN(jstring, getScreenText)(JNIEnv *env, jobject obj) {
(void)obj;
#ifdef HAVE_VICE_SRC
/* Worst case every cell is a 2-byte UTF-8 override: 40*25*2 + 25 newlines + NUL. */
char text[40 * 25 * 2 + 25 + 1];
int pos = 0;
for (int row = 0; row < 25; row++) {
for (int col = 0; col < 40; col++) {
uint8_t screencode = mem_read_screen((uint16_t)(0x0400 + row * 40 + col));
const char *override = NULL;
for (size_t i = 0; i < sizeof(k_char_overrides) / sizeof(k_char_overrides[0]); i++) {
if (k_char_overrides[i].screencode == screencode) {
override = k_char_overrides[i].utf8;
break;
}
}
if (override) {
size_t len = strlen(override);
memcpy(text + pos, override, len);
pos += (int)len;
} else {
uint8_t petscii = charset_screencode_to_petcii(screencode);
text[pos++] = (char)charset_p_toascii(petscii, CONVERT_WITHOUT_CTRLCODES);
}
}
text[pos++] = '\n';
}
text[pos] = '\0';
return (*env)->NewStringUTF(env, text);
#else
return (*env)->NewStringUTF(env, "");
#endif
}
/* In thread mode runFrame() is a no-op: VICE drives its own loop. */
JNI_FN(void, runFrame)(JNIEnv *env, jobject obj) {
(void)env; (void)obj;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB