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:
+3
@@ -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") }
|
||||
|
||||
|
||||
+37
-2
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user