From 678108530cfb21fa4145c553f767de00bfa416ee Mon Sep 17 00:00:00 2001 From: ml Date: Fri, 5 Jun 2026 09:44:06 +0200 Subject: [PATCH] Add in-app file import and episode picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assets (ROMs and disk images) now live in getExternalFilesDir, making them accessible via USB file transfer and any file manager app without adb or root. - "Import ROMs" button walks through kernal/basic/chargen picks in sequence and restarts the emulator when done - "Import Disks" button opens a multi-select picker; files are saved as SCHWUM

.D64 (uppercased for consistent lookup) - 2×4 episode grid (1A–4B) lights up each button as its disk image becomes available; tapping loads that disk into drive 8 --- CLAUDE.md | 27 +-- .../C64Engine.kt | 6 +- .../MainActivity.kt | 155 +++++++++++++++++- .../app/src/main/res/layout/activity_main.xml | 86 ++++++++++ 4 files changed, 253 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 22801ad..cdb5ed8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,17 +101,20 @@ The NDK must be installed: **Android Studio → SDK Manager → SDK Tools → ND After the first successful build, subsequent builds skip the VICE compilation step entirely (outputs are up-to-date). -**C64 ROM files** must be pushed to the device separately (not redistributable). -VICE expects files named exactly `kernal`, `basic`, `chargen` (no extensions) in the -app's private `filesDir`: `/data/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/` +**C64 ROM files** must be copied to the device separately (not redistributable). +VICE expects files named exactly `kernal`, `basic`, `chargen` (no extensions). +**Via USB file transfer (recommended):** connect the phone, open it in Windows Explorer or +Android File Transfer (Mac), and copy the files to: +``` +Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/ +``` + +**Via adb (alternative):** ```bash -adb push kernal /data/local/tmp/kernal -adb push basic /data/local/tmp/basic -adb push chargen /data/local/tmp/chargen -adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/kernal files/kernal -adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/basic files/basic -adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/chargen files/chargen +adb push kernal /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/kernal +adb push basic /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/basic +adb push chargen /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/chargen ``` ### Key Kotlin/C files @@ -127,8 +130,8 @@ adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tm ### Disk images -The four original `.d64` disk images are in `versions/`. Load via: +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(filesDir.absolutePath + "/schwert_und_magie_1.d64") +engine.loadDisk(getExternalFilesDir(null)!!.absolutePath + "/schwert_und_magie_1.d64") ``` -(Copy the relevant `.d64` to `filesDir` first via `adb push`.) diff --git a/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/C64Engine.kt b/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/C64Engine.kt index 5994b7e..4164203 100644 --- a/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/C64Engine.kt +++ b/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/C64Engine.kt @@ -9,8 +9,10 @@ import java.nio.ByteBuffer * VICE source must be placed at app/src/main/jni/vice-src/ before building * (download from https://vice-emu.sourceforge.io/). * - * ROM files (kernal, basic, chargen) must be copied to the app's filesDir - * before calling initEmulator(). + * ROM files (kernal, basic, chargen) and .d64 disk images must be copied to + * the app's external files directory before calling initEmulator(). + * That directory is accessible via USB file transfer (no adb needed) at: + * Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/ */ class C64Engine { diff --git a/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/MainActivity.kt b/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/MainActivity.kt index 65fc5d2..3f70210 100644 --- a/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/MainActivity.kt +++ b/SchwertUndMagieOnPebbleCompanionApp/app/src/main/java/de/ladkau/schwertundmagieonpebblecompanionapp/MainActivity.kt @@ -1,13 +1,22 @@ package de.ladkau.schwertundmagieonpebblecompanionapp +import android.app.Activity +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper +import android.provider.OpenableColumns import android.util.Log +import android.widget.Button import android.widget.ScrollView import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import fi.iki.elonen.NanoHTTPD +import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -15,6 +24,10 @@ import java.util.Locale private const val PORT = 8888 private const val TAG = "SuM" +private data class DiskSlot(val episode: Int, val part: Char, val btnId: Int) { + val fileName: String get() = "SCHWUM${episode}${part}.D64" +} + class MainActivity : AppCompatActivity() { private lateinit var tvStatus: TextView @@ -32,6 +45,66 @@ class MainActivity : AppCompatActivity() { private var emuThread: Thread? = null @Volatile private var emuRunning = false + // ---- disk slots --------------------------------------------------------- + + private val diskSlots = listOf( + DiskSlot(1, 'A', R.id.btn_ep_1a), DiskSlot(1, 'B', R.id.btn_ep_1b), + DiskSlot(2, 'A', R.id.btn_ep_2a), DiskSlot(2, 'B', R.id.btn_ep_2b), + DiskSlot(3, 'A', R.id.btn_ep_3a), DiskSlot(3, 'B', R.id.btn_ep_3b), + DiskSlot(4, 'A', R.id.btn_ep_4a), DiskSlot(4, 'B', R.id.btn_ep_4b), + ) + private val episodeButtons = mutableMapOf() + + // ---- ROM import (sequential: kernal → basic → chargen) ----------------- + + private val romNames = listOf("kernal", "basic", "chargen") + private var romImportIndex = 0 + + private val romPickerLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uri -> + val target = romNames[romImportIndex] + importFileToAssets(uri, target) + appendLog("Imported: $target") + romImportIndex++ + if (romImportIndex < romNames.size) { + launchPicker(romPickerLauncher, "Select '${romNames[romImportIndex]}' ROM file") + } else { + romImportIndex = 0 + Toast.makeText(this, "ROMs imported — restarting emulator", Toast.LENGTH_SHORT).show() + recreate() + } + } ?: run { romImportIndex = 0 } + } else { + romImportIndex = 0 + } + } + + // ---- Disk import (multi-select) ----------------------------------------- + + private val diskPickerLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val uris = mutableListOf() + result.data?.clipData?.let { clip -> + for (i in 0 until clip.itemCount) uris.add(clip.getItemAt(i).uri) + } ?: result.data?.data?.let { uris.add(it) } + + var count = 0 + for (uri in uris) { + val name = displayNameForUri(uri) ?: continue + importFileToAssets(uri, name.uppercase()) + appendLog("Imported: $name") + count++ + } + if (count > 0) refreshDiskButtons() + appendLog("Imported $count disk image(s)") + } + } + + // ---- lifecycle ---------------------------------------------------------- + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -43,17 +116,35 @@ class MainActivity : AppCompatActivity() { display = findViewById(R.id.c64_display) keyboard = findViewById(R.id.c64_keyboard) + findViewById