Add Fetch Disks button and Anleitung copy-protection viewer
- 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
This commit is contained in:
@@ -20,6 +20,8 @@ local.properties
|
||||
/SchwertUndMagieOnPebbleCompanionApp/app/.cxx
|
||||
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-src
|
||||
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-libs
|
||||
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-src
|
||||
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-libs
|
||||
|
||||
/SchwertUndMagieOnPebbleWatchApp/build
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ during development, but nothing in the Gradle build copies them into the APK.
|
||||
| `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` |
|
||||
| `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
|
||||
|
||||
@@ -127,3 +128,34 @@ 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.
|
||||
|
||||
@@ -122,11 +122,77 @@ tasks.register("buildVice") {
|
||||
}
|
||||
}
|
||||
|
||||
// Run buildVice before any CMake configure or build step.
|
||||
// ---------------------------------------------------------------------------
|
||||
// nibtools cross-compilation task
|
||||
//
|
||||
// Builds nibconv (NIB/NBZ -> G64/D64 disk image conversion, used by the
|
||||
// "Download Disks" feature) the same way buildVice builds VICE above.
|
||||
// Skipped entirely if nibtools-libs/<abi>/libnibtools.a already exists.
|
||||
// The tarball at <project>/res/nibtools-91344e0ee3.tar.gz is unpacked by
|
||||
// build_nibtools.sh.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
val nibtoolsLibArm = layout.projectDirectory.file("src/main/jni/nibtools-libs/arm64-v8a/libnibtools.a")
|
||||
val nibtoolsLibX86 = layout.projectDirectory.file("src/main/jni/nibtools-libs/x86_64/libnibtools.a")
|
||||
val nibtoolsTarball = layout.projectDirectory.file("../res/nibtools-91344e0ee3.tar.gz")
|
||||
|
||||
tasks.register("buildNibtools") {
|
||||
group = "build"
|
||||
description = "Cross-compile nibtools' nibconv for Android (ARM64 + x86_64)"
|
||||
|
||||
inputs.file(nibtoolsTarball)
|
||||
outputs.file(nibtoolsLibArm)
|
||||
outputs.file(nibtoolsLibX86)
|
||||
|
||||
doLast {
|
||||
if (nibtoolsLibArm.asFile.exists() && nibtoolsLibX86.asFile.exists()) {
|
||||
logger.lifecycle("nibtools already built — skipping")
|
||||
return@doLast
|
||||
}
|
||||
|
||||
val localProps = Properties().apply {
|
||||
rootProject.file("local.properties").takeIf { it.exists() }
|
||||
?.reader()?.use { load(it) }
|
||||
}
|
||||
val sdkDir = localProps.getProperty("sdk.dir")
|
||||
?: System.getenv("ANDROID_HOME")
|
||||
?: ""
|
||||
val ndkPath = localProps.getProperty("ndk.dir")
|
||||
?: System.getenv("ANDROID_NDK_HOME")
|
||||
?: System.getenv("ANDROID_NDK_ROOT")
|
||||
?: System.getenv("NDK")
|
||||
?: sdkDir.takeIf { it.isNotEmpty() }?.let { sdk ->
|
||||
file("$sdk/ndk").takeIf { it.isDirectory }
|
||||
?.listFiles()?.sorted()?.lastOrNull()?.absolutePath
|
||||
?: "$sdk/ndk-bundle".takeIf { file("$sdk/ndk-bundle").isDirectory }
|
||||
}
|
||||
?: throw GradleException(
|
||||
"Android NDK not found.\n" +
|
||||
"Install via: Android Studio → SDK Manager → SDK Tools → NDK (Side by side)"
|
||||
)
|
||||
logger.lifecycle("Building nibtools with NDK at $ndkPath …")
|
||||
|
||||
val proc = ProcessBuilder("bash", "build_nibtools.sh")
|
||||
.directory(layout.projectDirectory.dir("src/main/jni").asFile)
|
||||
.redirectErrorStream(true)
|
||||
.also { it.environment()["NDK"] = ndkPath }
|
||||
.start()
|
||||
proc.inputStream.bufferedReader().forEachLine { logger.lifecycle(it) }
|
||||
val exit = proc.waitFor()
|
||||
if (exit != 0) throw GradleException("build_nibtools.sh failed (exit $exit)")
|
||||
|
||||
layout.projectDirectory.file("src/main/jni/CMakeLists.txt")
|
||||
.asFile.setLastModified(System.currentTimeMillis())
|
||||
logger.lifecycle("nibtools build complete — CMakeLists.txt touched for re-evaluation")
|
||||
}
|
||||
}
|
||||
|
||||
// Run buildVice and buildNibtools before any CMake configure or build step.
|
||||
tasks.whenTaskAdded {
|
||||
if (name.startsWith("configureCMake") || name.startsWith("buildCMake") ||
|
||||
name.startsWith("externalNativeBuild")) {
|
||||
dependsOn("buildVice")
|
||||
dependsOn("buildNibtools")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
SCHWERT UND MAGIE - Spielanleitung
|
||||
====================================
|
||||
(abgetippt aus der Original-Anleitung in res/schwert-und-magie-anleitung.pdf)
|
||||
|
||||
Diese Abschrift behält die Absatz- und Zeilenumbrüche des Originals bei.
|
||||
Die Anleitung ist ein gefaltetes Blatt ohne Seitenzahlen - die drei Seiten
|
||||
heißen Vorderseite, linke Innenseite und rechte Innenseite (Abschnitte
|
||||
unten). Der Kopierschutz fragt ein Wort über diese Hierarchie ab:
|
||||
|
||||
Seite -> Absatz -> Zeile -> Wort
|
||||
|
||||
Seite = einer der drei Abschnitte (Vorderseite / linke Innenseite /
|
||||
rechte Innenseite) weiter unten.
|
||||
Absatz = durch Leerzeile getrennter Textblock innerhalb der Seite;
|
||||
beginnt auf jeder Seite neu bei 1.
|
||||
Zeile = gedruckte Zeile innerhalb des Absatzes, ab 1 gezählt.
|
||||
Wort = durch Leerzeichen getrenntes Wort innerhalb der Zeile, ab 1
|
||||
gezählt, wie im gedruckten Original.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
=== Vorderseite ===
|
||||
|
||||
Absatz 1:
|
||||
Zeile 1: Diese Abenteuerspielreihe ist im Fantasybereich angesiedelt. Das heißt, in
|
||||
Zeile 2: einer Welt, die etwa der unseres Mittelalters entspricht und in der oft das
|
||||
Zeile 3: Schwert regiert. In dieser Welt gibt es aber auch noch Magie, Dämonen,
|
||||
Zeile 4: Monster, Drachen und Zauberer.
|
||||
|
||||
Absatz 2:
|
||||
Zeile 1: Es sind Abenteuerspiele, bei denen der Spieler einen Helden (oder eine
|
||||
Zeile 2: Heldin) verkörpert, der eine bestimmte Aufgabe lösen muß. Anders als bei
|
||||
Zeile 3: den gängigen Abenteuerspielen werden hier in jeder Situation Vorschläge
|
||||
Zeile 4: gemacht, was Dein Held als nächstes tun kann, und Du mußt Dich für eine
|
||||
Zeile 5: Alternative entscheiden.
|
||||
|
||||
Absatz 3:
|
||||
Zeile 1: Das mag Dir vielleicht im ersten Moment unflexibler vorkommen, hat aber
|
||||
Zeile 2: zwei Vorteile. Einmal konnte der Speicherplatz für einen Parser
|
||||
Zeile 3: (Programmteil, der die Befehle des Spielers analysiert) auf ein Minimum
|
||||
Zeile 4: beschränkt, und so das Spiel selbst etwas umfangreicher gestaltet werden.
|
||||
Zeile 5: Zum anderen wird hier niemand scheitern, weil er nicht auf einen bestimmten
|
||||
Zeile 6: Begriff oder eine bestimmte Art zu handeln kommt: alle Möglichkeiten sind
|
||||
Zeile 7: sofort erkennbar.
|
||||
|
||||
Absatz 4:
|
||||
Zeile 1: Es ist ein reines Text-Abenteuer; eine schöne Grafik verbraucht zuviel
|
||||
Zeile 2: Speicherplatz, und grob gemachte Bilder oder ständiges Nachladen wollten
|
||||
Zeile 3: Dir ersparen. Wir haben dafür die Beschreibungstexte sehr ausführlich
|
||||
Zeile 4: gestaltet, was oft eine bessere Atmosphäre schafft, als es durch ein Bild
|
||||
Zeile 5: erreicht werden kann.
|
||||
|
||||
Absatz 5:
|
||||
Zeile 1: Es ist aber gleichzeitig auch ein Rollenspiel, denn Deine Spielfigur
|
||||
Zeile 2: bekommt zu Anfang bestimmte Eigenschaften zugewiesen, die sie im Verlauf
|
||||
Zeile 3: des Spieles oftmals unter Beweis stellen muß. Diese Eigenschaften sind im
|
||||
Zeile 4: Einzelnen:
|
||||
|
||||
Absatz 6:
|
||||
Zeile 1: Die Vitalität (Vit) gibt die körperliche Verfassung wieder. Wird Dein Held
|
||||
Zeile 2: verwundet, so sinkt sie. Fällt sie auf Null, ist Dein Held tot. Auch Deine
|
||||
Zeile 3: Gegner haben eine bestimmte Vitalität.
|
||||
|
||||
Absatz 7:
|
||||
Zeile 1: Die Tapferkeit (Ta) entspricht dem überlegten Mut ebenso wie der
|
||||
Zeile 2: Tollkühnheit Deines Helden. Tapferkeit entscheidet über die Initiative im
|
||||
Zeile 3: Kampf, ob man dem Anblick eines Monsters standhalten kann, sich traut,
|
||||
Zeile 4: unheimliche Orte zu betreten oder gefährliche Dinge zu tun etc.
|
||||
|
||||
Absatz 8:
|
||||
Zeile 1: Die Intelligenz (In) ist die Fähigkeit, Situationen zu erfassen und
|
||||
Zeile 2: logische Schlüsse daraus zu ziehen. Wer intelligent ist, kann Rätsel oder
|
||||
Zeile 3: Zusammenhänge besser verstehen und hat schon eher mal eine gute Idee.
|
||||
|
||||
Absatz 9:
|
||||
Zeile 1: Der Charme (Ch) ist die Fähigkeit, andere zu beeinflussen und für sich zu
|
||||
Zeile 2: gewinnen. Das ist von vielen Faktoren abhängig, z.B. Aussehen, Klang der
|
||||
Zeile 3: Stimme, Auftreten etc. Charme ist nützlich zum Gewinnen von Freunden,
|
||||
Zeile 4: beim Handeln oder verhandeln, bei Bitten etc.
|
||||
|
||||
=== linke Innenseite ===
|
||||
|
||||
Absatz 1:
|
||||
Zeile 1: Die Stärke (St) braucht wohl nicht weiter erläutert zu werden. Wer hier
|
||||
Zeile 2: einen Wert über 60 Prozent erreicht, kann im Kampf so kräftig zudreschen,
|
||||
Zeile 3: daß er für jede zusätzliche 10 Prozent ab 65 % zusätzliche Trefferwirkung
|
||||
Zeile 4: erzielt.
|
||||
|
||||
Absatz 2:
|
||||
Zeile 1: Die Geschicklichkeit (Ge) ist notwendig zum Ausweichen von Fallen, Öffnen
|
||||
Zeile 2: von Schlössern etc. Wer hier einen Wert über 60 % besitzt, dessen
|
||||
Zeile 3: Kampfwerte werden um 5 Prozent gesteigert.
|
||||
|
||||
Absatz 3:
|
||||
Zeile 1: Der Angriff (An) ist die Fähigkeit, im Kampf einen Schlag gegen den Gegner
|
||||
Zeile 2: zu führen; also die aggressive Fertigkeit seine Waffe zu führen.
|
||||
|
||||
Absatz 4:
|
||||
Zeile 1: Die Verteidigung (Ve) ist die Fähigkeit, im Kampf den Angriff eines
|
||||
Zeile 2: Gegners, der den Helden treffen würde, abzuwehren; also die defensive
|
||||
Zeile 3: Kampffertigkeit.
|
||||
|
||||
Absatz 5:
|
||||
Zeile 1: Die Eigenschaftswerte werden in Prozenten ausgedrückt, wobei der Wert 100 %
|
||||
Zeile 2: natürlich perfekt ist. Da aber niemand vollkommen ist, kann kein Wert 90 %
|
||||
Zeile 3: übersteigen. 50 % sind guter Durchschnitt. Wann immer eine heikle Situation
|
||||
Zeile 4: entsteht, bei der eine dieser Eigenschaften eine besondere Rolle spielt,
|
||||
Zeile 5: wird Dein Held einer Prüfung unterzogen. Dabei wird ein Wert bestimmt, der
|
||||
Zeile 6: von dem Eigenschaftswert des Helden und der Schwierigkeit der abzulegenden
|
||||
Zeile 7: Prüfung abhängt. Sodann wird durch Zufall ermittelt, wie gut sich Dein Held
|
||||
Zeile 8: dabei anstellt. Erreicht er den erforderlichen Wert, ist die Prüfung
|
||||
Zeile 9: gelungen, und das, was Dein Held vorhatte, klappt. Das Mißlingen einer
|
||||
Zeile 10: solchen Prüfung hat allerdings meist ziemlich unangenehme Konsequenzen.
|
||||
|
||||
Absatz 6:
|
||||
Zeile 1: Die folgenden Werte sind keine echten Eigenschaften, da sie von der
|
||||
Zeile 2: Ausrüstung abhängig sind:
|
||||
|
||||
Absatz 7:
|
||||
Zeile 1: Die Trefferwirkung (Tw), bzw. die Waffenklasse (Wk) ist ein Maßstab für den
|
||||
Zeile 2: Schaden, den ein durchschnittlich kräftiger Krieger mit der Waffe, die er
|
||||
Zeile 3: gerade führt, maximal anrichten kann. Nimmt man eine andere Waffe, kann
|
||||
Zeile 4: sich auch dieser Wert ändern.
|
||||
|
||||
Absatz 8:
|
||||
Zeile 1: Der Schutzfaktor (Sf) ist ein Index dafür, wie gut man vor gegnerischen
|
||||
Zeile 2: Hieben geschützt ist. Je besser die Rüstung, desto besser ist auch der
|
||||
Zeile 3: Schutz. Er gibt die Höhe der Trefferwirkung an, die die Rüstung
|
||||
Zeile 4: kompensiert. Ein Schild kann den Faktor erhöhen.
|
||||
|
||||
Absatz 9:
|
||||
Zeile 1: Die obige Beschreibung läßt Dich schon vermuten, daß es hier nicht
|
||||
Zeile 2: einfach der Befehl "Töte Monster" gegeben, sondern in der Tat ein richtiger
|
||||
Zeile 3: Kampf simuliert wird, dessen Ausgang höchst ungewiß ist. Natürlich sind
|
||||
Zeile 4: auch wir der Auffassung, daß es immer besser ist, Gewalt zu vermeiden und
|
||||
Zeile 5: Probleme besser mit dem Kopf als mit dem Schwert gelöst werden sollten,
|
||||
Zeile 6: aber manchmal geht es einfach nicht anders. Für diese Fälle wollen wir das
|
||||
Zeile 7: Gefecht so realistisch wie möglich ablaufen lassen.
|
||||
|
||||
Absatz 10:
|
||||
Zeile 1: Kommt es zum Kampf, steht Dein Held dem Gegner gegenüber, der auch gewisse
|
||||
Zeile 2: Eigenschaften haben wird, woraus Du ersehen kannst, wie gut er kämpft und
|
||||
Zeile 3: wie leicht oder schwierig er zu besiegen ist. Es können auch mehrere Gegner
|
||||
Zeile 4: auf einmal sein, die alle gleichzeitig angreifen. Die folgende Beschreibung
|
||||
Zeile 5: des Kampfablaufs gilt für beide Seiten gleichermaßen:
|
||||
|
||||
Absatz 11:
|
||||
Zeile 1: Wer einen Gegner angreift, muß eine Prüfung auf seinen Angriffswert
|
||||
Zeile 2: ablegen. Mißlingt diese, geht der Schlag fehl. Gelingt sie jedoch, ist der
|
||||
Zeile 3: Hieb so gut geführt, daß er den Gegner treffen würde - würde, weil dieser
|
||||
Zeile 4: nun versuchen wird, den Schlag abzuwehren. Dies kann er, wenn eine Prüfung
|
||||
Zeile 5: auf seine Verteidigung gelingt. Mißlingt diese Prüfung, kann er den Hieb
|
||||
Zeile 6: nicht abwenden und wird getroffen.
|
||||
|
||||
=== rechte Innenseite ===
|
||||
|
||||
Absatz 1:
|
||||
Zeile 1: Wenn man seinen Gegner trifft, wird die Wirkung ermittelt, die der Schlag
|
||||
Zeile 2: erzielt. Sie liegt zufällig zwischen der Hälfte und dem Maximum der Waffen-
|
||||
Zeile 3: klasse, da nicht jeder Hieb gleich kräftig geführt wird und gleich gut
|
||||
Zeile 4: trifft. Bei Deinem Helden kommt eventuell, noch ein Bonus für große Stärke
|
||||
Zeile 5: hinzu.
|
||||
|
||||
Absatz 2:
|
||||
Zeile 1: Nun wird von der Trefferwirkung noch der Schutzfaktor des Getroffenen
|
||||
Zeile 2: abgezogen. Das Ergebnis ist der Schaden, der von dessen Vitalität
|
||||
Zeile 3: subtrahiert wird.
|
||||
|
||||
Absatz 3:
|
||||
Zeile 1: Beispiel: Ein Schwerthieb (Tw max. 10) trifft mit der Wucht von 9. Der
|
||||
Zeile 2: Getroffene trägt eine Lederrüstung (Sf 3) und einen Holzschild (Sf 1), dann
|
||||
Zeile 3: werden 4 Punkte von der Wirkung abgezogen. Übrig bleiben 5 Punkte Schaden,
|
||||
Zeile 4: die von dem Vit abgezogen werden.
|
||||
|
||||
Absatz 4:
|
||||
Zeile 1: Für Deinen Helden gibt es zwei Sonderregeln bei seinen Angriffen:
|
||||
|
||||
Absatz 5:
|
||||
Zeile 1: Mit einer Wahrscheinlichkeit von 10 % gelingt ihm ein "guter Treffer". Das
|
||||
Zeile 2: ist ein Hieb, der so gut geführt ist, daß der Gegner keine Gelegenheit mehr
|
||||
Zeile 3: zur Abwehr hat.
|
||||
|
||||
Absatz 6:
|
||||
Zeile 1: Mit einer 5%igen Wahrscheinlichkeit gelingt ihm ein sogenannter
|
||||
Zeile 2: "Glückstreffer". Dies ist ein "guter Treffer", der durch Zufall eine
|
||||
Zeile 3: ungeschützte oder empfindliche Stelle des Gegners trifft und nicht von der
|
||||
Zeile 4: Rüstung abgeschwächt wird.
|
||||
|
||||
Absatz 7:
|
||||
Zeile 1: Deine Gegner können dies zum Glück nicht. Dafür haben sie einen Vorteil,
|
||||
Zeile 2: wenn sie zu mehreren sind. In diesem Fall kann in einem Kampfzug für jeden
|
||||
Zeile 3: Gegner ein Angriff gegen Dich geführt werden, während Du nur einmal
|
||||
Zeile 4: angreifen kannst.
|
||||
|
||||
Absatz 8:
|
||||
Zeile 1: Da es sich um eine Abenteuerreihe handelt, die auch weiter fortgesetzt
|
||||
Zeile 2: werden soll, wird ein erstellter Held auf Diskette gespeichert, und kann so
|
||||
Zeile 3: in mehreren Abenteuern seine "Karriere" aufbauen. Nach jedem bestandenen
|
||||
Zeile 4: Abenteuer werden nämlich seine Eigenschaften verbessert, was ihm das
|
||||
Zeile 5: nächste Abenteuer etwas leichter macht.
|
||||
|
||||
Absatz 9:
|
||||
Zeile 1: Stirbt ein Spieler jedoch auf irgendeine Weise im Spiel, so ist er wirklich
|
||||
Zeile 2: "tot", und seine Laufbahn ist für immer zu Ende, denn er ist auch auf
|
||||
Zeile 3: geheimnisvolle Weise von der Speicherdiskette ins Jenseits verschwunden.
|
||||
Zeile 4: Dann bleibt Dir nichts weiter übrig, als einen neuen Helden zu erstellen,
|
||||
Zeile 5: der dann natürlich noch entsprechend schwach ist. Daher solltest Du Helden,
|
||||
Zeile 6: die schon weit gekommen sind, mit entsprechender Umsicht führen, wenn Du
|
||||
Zeile 7: sie nicht verlieren möchtest.
|
||||
|
||||
Absatz 10:
|
||||
Zeile 1: Für jedes bestandene Abenteuer steigt Dein Held einen Grad höher. Je nach
|
||||
Zeile 2: der Schwierigkeit des Abenteuers erhöhen sich seine Eigenschaften und seine
|
||||
Zeile 3: Vitalität. Dies erleichtert das folgende Abenteuer bei den Prüfungen, aber
|
||||
Zeile 4: nicht immer im Kampf, wo sich die Gegner manchmal der Stufe des Spielers
|
||||
Zeile 5: angleichen.
|
||||
|
||||
Absatz 11:
|
||||
Zeile 1: Hebe Deine gespeicherten Helden gut auf, denn es werden bald weitere
|
||||
Zeile 2: Abenteuer dieser Reihe erscheinen...
|
||||
|
||||
Absatz 12:
|
||||
Zeile 1: Wir wünschen Dir spannende Unterhaltung!
|
||||
+7
@@ -61,6 +61,13 @@ class C64Engine {
|
||||
/** Current 40x25 C64 text screen as ASCII, rows separated by '\n'. */
|
||||
external fun getScreenText(): String
|
||||
|
||||
/**
|
||||
* Converts a disk image file at [inPath] to the format implied by [outPath]'s
|
||||
* extension (e.g. .nbz/.nib -> .g64/.d64) via nibtools' nibconv, run in-process.
|
||||
* Returns false (without writing [outPath]) if nibtools wasn't compiled in.
|
||||
*/
|
||||
external fun convertDiskImage(inPath: String, outPath: String): Boolean
|
||||
|
||||
companion object {
|
||||
init { System.loadLibrary("vice_jni") }
|
||||
|
||||
|
||||
+153
-1
@@ -133,6 +133,120 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Disk download (Internet Archive C64 Preservation Project) ----------
|
||||
|
||||
// Each entry is one side of one physical disk, dumped as a raw-GCR NIB by
|
||||
// the preservation project (these old disks aren't always cleanly
|
||||
// sector-readable, so the source format preserves whatever the original
|
||||
// drive actually saw rather than reconstructing a — possibly wrong — D64).
|
||||
// nibtools' nibconv (see jni/build_nibtools.sh) converts each to G64,
|
||||
// which VICE attaches exactly like a D64 (engine.loadDisk/attachDisk
|
||||
// detect the format from file content, not the extension).
|
||||
private data class DiskDownload(val baseName: String, val url: String)
|
||||
|
||||
private val diskDownloads = listOf(
|
||||
DiskDownload("SCHWUM1A", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_1_und_2_s1%5Bgdg_1989%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM1B", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_1_und_2_s2%5Bgdg_1989%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM2A", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_3_und_4_s1%5Bgdg_1989%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM2B", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_3_und_4_s2%5Bgdg_1989%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM3A", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_5_und_6_s1%5Bgdg_1991%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM3B", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_5_und_6_s2%5Bgdg_1991%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM4A", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_7_und_8_s1%5Bgdg_1991%5D%28german%29.nbz"),
|
||||
DiskDownload("SCHWUM4B", "https://archive.org/download/C64_Preservation_Project_10th_Anniversary_Collection/C64_Preservation_Project_10th_Anniversary_Collection.zip/c64pp%2Fnon-english%2Fschwert_und_magie_folge_7_und_8_s2%5Bgdg_1991%5D%28german%29.nbz"),
|
||||
)
|
||||
|
||||
private fun showDownloadDisksDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.btn_download_disks)
|
||||
.setMessage(R.string.disks_download_msg)
|
||||
.setPositiveButton(R.string.disks_download_confirm) { _, _ -> downloadDisks() }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun downloadDisks() {
|
||||
val assetDir = getExternalFilesDir(null) ?: filesDir
|
||||
val dp = { v: Int -> (v * resources.displayMetrics.density).toInt() }
|
||||
val progressBar = android.widget.ProgressBar(
|
||||
this, null, android.R.attr.progressBarStyleHorizontal
|
||||
)
|
||||
val statusTv = TextView(this).apply {
|
||||
textSize = 12f
|
||||
setTextColor(Color.parseColor("#CCCCCC"))
|
||||
setPadding(0, dp(8), 0, 0)
|
||||
}
|
||||
val content = android.widget.LinearLayout(this).apply {
|
||||
orientation = android.widget.LinearLayout.VERTICAL
|
||||
setPadding(dp(20), dp(16), dp(20), dp(8))
|
||||
setBackgroundColor(Color.parseColor("#1A1A2E"))
|
||||
addView(progressBar)
|
||||
addView(statusTv)
|
||||
}
|
||||
val progressDialog = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.btn_download_disks)
|
||||
.setView(content)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
|
||||
Thread {
|
||||
var successCount = 0
|
||||
for ((index, disk) in diskDownloads.withIndex()) {
|
||||
mainHandler.post {
|
||||
progressBar.isIndeterminate = false
|
||||
progressBar.progress = 0
|
||||
statusTv.text = getString(R.string.disks_downloading, index + 1, diskDownloads.size)
|
||||
}
|
||||
val tmpFile = File(cacheDir, "${disk.baseName}.nbz")
|
||||
try {
|
||||
val conn = (URL(disk.url).openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = 15000
|
||||
readTimeout = 30000
|
||||
instanceFollowRedirects = true
|
||||
}
|
||||
try {
|
||||
conn.connect()
|
||||
if (conn.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw IOException("HTTP ${conn.responseCode}")
|
||||
}
|
||||
val total = conn.contentLengthLong
|
||||
var lastPercent = -1
|
||||
val countingStream = CountingInputStream(conn.inputStream) { downloaded ->
|
||||
if (total > 0) {
|
||||
val percent = ((downloaded * 100) / total).toInt()
|
||||
if (percent != lastPercent) {
|
||||
lastPercent = percent
|
||||
mainHandler.post { progressBar.progress = percent }
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpFile.outputStream().use { out -> countingStream.copyTo(out) }
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
val finalFile = File(assetDir, "${disk.baseName}.G64")
|
||||
if (engine.convertDiskImage(tmpFile.absolutePath, finalFile.absolutePath)) {
|
||||
successCount++
|
||||
mainHandler.post { appendLog("Downloaded: ${disk.baseName}") }
|
||||
} else {
|
||||
mainHandler.post { appendLog("Conversion failed: ${disk.baseName}") }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Disk download failed: ${disk.baseName}", e)
|
||||
mainHandler.post { appendLog("Download failed: ${disk.baseName} (${e.message})") }
|
||||
} finally {
|
||||
tmpFile.delete()
|
||||
}
|
||||
}
|
||||
mainHandler.post {
|
||||
progressDialog.dismiss()
|
||||
refreshDiskButtons()
|
||||
val msg = getString(R.string.disks_download_done, successCount, diskDownloads.size)
|
||||
appendLog(msg)
|
||||
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
// ---- lifecycle ----------------------------------------------------------
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -177,11 +291,15 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btn_help).setOnClickListener { showHelp() }
|
||||
findViewById<Button>(R.id.btn_anleitung).setOnClickListener { showAnleitung() }
|
||||
findViewById<Button>(R.id.btn_savestate).setOnClickListener { showSaveStatePanel() }
|
||||
|
||||
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
||||
launchPicker(diskPickerLauncher, getString(R.string.picker_title), multiSelect = true)
|
||||
}
|
||||
findViewById<Button>(R.id.btn_download_disks).setOnClickListener {
|
||||
showDownloadDisksDialog()
|
||||
}
|
||||
|
||||
val btnSound = findViewById<Button>(R.id.btn_sound)
|
||||
btnSound.setOnClickListener {
|
||||
@@ -548,7 +666,13 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun diskFile(slot: DiskSlot): File? {
|
||||
val assetDir = getExternalFilesDir(null) ?: filesDir
|
||||
return assetDir.listFiles()?.firstOrNull { it.name.uppercase() == slot.fileName }
|
||||
// Episode disks may exist either as an imported/created D64 or as a
|
||||
// G64 produced by downloadDisks() (nibconv output) — same base name.
|
||||
val g64Name = slot.fileName.removeSuffix(".D64") + ".G64"
|
||||
return assetDir.listFiles()?.firstOrNull {
|
||||
val name = it.name.uppercase()
|
||||
name == slot.fileName || name == g64Name
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshDiskButtons() {
|
||||
@@ -1035,6 +1159,34 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Transcript of res/schwert-und-magie-anleitung.pdf, used to answer the
|
||||
// game's copy-protection prompt ("Nenne Wort X in Zeile Y von Absatz Z").
|
||||
// Paragraph/line breaks are preserved exactly as printed — see the note
|
||||
// at the top of the asset file for the counting convention.
|
||||
private fun showAnleitung() {
|
||||
val text = try {
|
||||
assets.open("anleitung.txt").bufferedReader().readText()
|
||||
} catch (e: Exception) { "Anleitung nicht verfügbar." }
|
||||
|
||||
val tv = TextView(this).apply {
|
||||
this.text = text
|
||||
typeface = android.graphics.Typeface.MONOSPACE
|
||||
textSize = 12f
|
||||
setTextColor(Color.parseColor("#DDDDDD"))
|
||||
val p = (16 * resources.displayMetrics.density).toInt()
|
||||
setPadding(p, p, p, p)
|
||||
}
|
||||
val scroll = ScrollView(this).apply {
|
||||
addView(tv)
|
||||
setBackgroundColor(Color.parseColor("#1A1A2E"))
|
||||
}
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Anleitung")
|
||||
.setView(scroll)
|
||||
.setPositiveButton("OK", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun appendLog(entry: String) {
|
||||
val ts = timeFormat.format(Date())
|
||||
tvLog.text = "[$ts] $entry\n${tvLog.text}"
|
||||
|
||||
@@ -14,8 +14,19 @@ else()
|
||||
message(STATUS "No libvice.a found — building stub (placeholder framebuffer)")
|
||||
endif()
|
||||
|
||||
# Auto-detect whether libnibtools.a was produced by the Gradle buildNibtools task.
|
||||
set(NIBTOOLS_LIB "${CMAKE_CURRENT_SOURCE_DIR}/nibtools-libs/${ANDROID_ABI}/libnibtools.a")
|
||||
|
||||
if(EXISTS "${NIBTOOLS_LIB}")
|
||||
set(HAVE_NIBTOOLS ON)
|
||||
message(STATUS "Found ${NIBTOOLS_LIB} — building with disk image download/conversion")
|
||||
else()
|
||||
set(HAVE_NIBTOOLS OFF)
|
||||
message(STATUS "No libnibtools.a found — Download Disks feature will be unavailable")
|
||||
endif()
|
||||
|
||||
# ---- JNI wrapper library -------------------------------------------------
|
||||
add_library(vice_jni SHARED vice_jni.c)
|
||||
add_library(vice_jni SHARED vice_jni.c nibconv_jni.c)
|
||||
|
||||
target_link_libraries(vice_jni android log c++_shared z OpenSLES)
|
||||
|
||||
@@ -48,3 +59,12 @@ if(HAVE_VICE)
|
||||
)
|
||||
target_link_libraries(vice_jni "${VICE_LIB}")
|
||||
endif()
|
||||
|
||||
if(HAVE_NIBTOOLS)
|
||||
target_compile_definitions(vice_jni PRIVATE HAVE_NIBTOOLS=1)
|
||||
# Redirect nibconv's exit() calls (malformed input) to __wrap_exit in
|
||||
# nibconv_jni.c, which calls pthread_exit() on the throwaway conversion
|
||||
# thread instead of killing the process.
|
||||
target_link_options(vice_jni PRIVATE -Wl,--wrap=exit)
|
||||
target_link_libraries(vice_jni "${NIBTOOLS_LIB}")
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-compile nibtools' nibconv (NIB/NBZ -> G64/D64 disk image converter)
|
||||
# as a static library for Android.
|
||||
# Called automatically by Gradle; can also be run manually:
|
||||
# export NDK=/path/to/android-ndk && bash build_nibtools.sh
|
||||
#
|
||||
# Output: nibtools-libs/arm64-v8a/libnibtools.a
|
||||
# nibtools-libs/x86_64/libnibtools.a
|
||||
#
|
||||
# Only nibconv's pure file-format conversion code is built — gcr.c, prot.c,
|
||||
# fileio.c, crc.c, md5.c, lz.c and nibconv.c itself. The hardware-access
|
||||
# parts of nibtools (nibread/nibwrite, talking to a real 1541 over OpenCBM)
|
||||
# are not built and not needed; see nibtools_android/opencbm.h for why that
|
||||
# dependency can be stubbed out instead of vendored.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
TARBALL="${SCRIPT_DIR}/../../../../res/nibtools-91344e0ee3.tar.gz"
|
||||
SRC="${SCRIPT_DIR}/nibtools-src"
|
||||
ANDROID_SHIM="${SCRIPT_DIR}/nibtools_android"
|
||||
|
||||
: "${NDK:?NDK env var must point to the Android NDK root}"
|
||||
|
||||
echo "=== build_nibtools.sh ==="
|
||||
echo " SCRIPT_DIR : ${SCRIPT_DIR}"
|
||||
echo " TARBALL : ${TARBALL}"
|
||||
echo " SRC : ${SRC}"
|
||||
echo " NDK : ${NDK}"
|
||||
|
||||
TOOLCHAIN="${NDK}/toolchains/llvm/prebuilt/linux-x86_64"
|
||||
if [ ! -d "${TOOLCHAIN}" ]; then
|
||||
echo "ERROR: NDK toolchain not found at ${TOOLCHAIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${TARBALL}" ]; then
|
||||
echo "ERROR: nibtools tarball not found at ${TARBALL}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${SRC}" ]; then
|
||||
echo "Unpacking nibtools..."
|
||||
tar -xzf "${TARBALL}" -C "${SCRIPT_DIR}"
|
||||
mv "${SCRIPT_DIR}"/nibtools-*/ "${SRC}"
|
||||
echo "Unpacked to ${SRC}"
|
||||
fi
|
||||
|
||||
# Pure conversion sources — no hardware/OpenCBM access (see header comment above).
|
||||
SOURCES="gcr.c prot.c fileio.c crc.c md5.c lz.c nibconv.c"
|
||||
|
||||
build_abi() {
|
||||
local ABI="$1"
|
||||
local HOST="$2"
|
||||
local API=24
|
||||
local CC="${TOOLCHAIN}/bin/${HOST}${API}-clang"
|
||||
local AR="${TOOLCHAIN}/bin/llvm-ar"
|
||||
local OUT="${SCRIPT_DIR}/nibtools-libs/${ABI}"
|
||||
local BUILD_DIR="/tmp/nibtools-android-${ABI}"
|
||||
|
||||
echo ""
|
||||
echo "--- ABI: ${ABI} ---"
|
||||
|
||||
if [ -f "${OUT}/libnibtools.a" ]; then
|
||||
echo " libnibtools.a already exists — skipping"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "${CC}" ]; then
|
||||
echo "ERROR: Clang not found at ${CC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${BUILD_DIR}" "${OUT}"
|
||||
|
||||
for src in ${SOURCES}; do
|
||||
local obj="${BUILD_DIR}/$(basename "${src}" .c).o"
|
||||
local extra_defs=""
|
||||
# nibconv.c's main() is renamed at compile time so it can be linked
|
||||
# into a shared library (which already has its own JNI entry points)
|
||||
# without clashing with libc's startup expectations for `main`.
|
||||
if [ "${src}" = "nibconv.c" ]; then
|
||||
extra_defs="-Dmain=nibtools_nibconv_main"
|
||||
fi
|
||||
"${CC}" -std=c99 -O2 -fPIC ${extra_defs} \
|
||||
-I "${ANDROID_SHIM}" -I "${SRC}" \
|
||||
-c "${SRC}/${src}" -o "${obj}"
|
||||
done
|
||||
|
||||
"${AR}" -crs "${OUT}/libnibtools.a" "${BUILD_DIR}"/*.o
|
||||
echo " Built ${OUT}/libnibtools.a"
|
||||
}
|
||||
|
||||
build_abi "arm64-v8a" "aarch64-linux-android"
|
||||
build_abi "x86_64" "x86_64-linux-android"
|
||||
|
||||
echo ""
|
||||
echo "=== nibtools build complete ==="
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* JNI bridge to nibtools' nibconv — converts a downloaded NIB/NBZ disk dump
|
||||
* (raw GCR preservation format) into a G64/D64 image VICE can attach.
|
||||
*
|
||||
* Without nibtools source (stub mode, HAVE_NIBTOOLS unset):
|
||||
* convertDiskImage() always returns false. The "Download Disks" feature
|
||||
* is unavailable but the rest of the app builds and runs normally.
|
||||
*
|
||||
* With nibtools source (HAVE_NIBTOOLS=1, set automatically by CMakeLists.txt
|
||||
* when nibtools-libs/<abi>/libnibtools.a exists):
|
||||
* nibtools_nibconv_main() — nibconv's own main(), renamed at compile time
|
||||
* (see build_nibtools.sh) — runs the real conversion. It is run on a
|
||||
* throwaway pthread rather than the calling JNI thread: nibconv calls
|
||||
* exit() on malformed input (e.g. a truncated download), and exit() is
|
||||
* wrapped below to pthread_exit() instead of killing the whole app
|
||||
* process. Isolating that to a disposable, JNI-unattached thread means a
|
||||
* bad conversion just fails this one call instead of detaching the
|
||||
* caller's JNI thread from underneath it.
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef HAVE_NIBTOOLS
|
||||
|
||||
extern int nibtools_nibconv_main(int argc, char **argv);
|
||||
|
||||
struct conv_args {
|
||||
char *in_path;
|
||||
char *out_path;
|
||||
int result;
|
||||
};
|
||||
|
||||
static void *run_nibconv(void *arg) {
|
||||
struct conv_args *a = (struct conv_args *)arg;
|
||||
char *argv[3] = { "nibconv", a->in_path, a->out_path };
|
||||
a->result = nibtools_nibconv_main(3, argv);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Redirects nibconv's error-path exit() calls so they only terminate the
|
||||
* throwaway conversion thread above, never the whole process. */
|
||||
void __wrap_exit(int status) {
|
||||
pthread_exit((void *)(long)status);
|
||||
}
|
||||
|
||||
#endif /* HAVE_NIBTOOLS */
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_de_ladkau_schwertundmagieonpebblecompanionapp_C64Engine_convertDiskImage(
|
||||
JNIEnv *env, jobject thiz, jstring inPath, jstring outPath)
|
||||
{
|
||||
(void)thiz;
|
||||
#ifdef HAVE_NIBTOOLS
|
||||
const char *inC = (*env)->GetStringUTFChars(env, inPath, NULL);
|
||||
const char *outC = (*env)->GetStringUTFChars(env, outPath, NULL);
|
||||
|
||||
struct conv_args args = { strdup(inC), strdup(outC), -1 };
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, inPath, inC);
|
||||
(*env)->ReleaseStringUTFChars(env, outPath, outC);
|
||||
|
||||
pthread_t t;
|
||||
pthread_create(&t, NULL, run_nibconv, &args);
|
||||
pthread_join(t, NULL);
|
||||
|
||||
free(args.in_path);
|
||||
free(args.out_path);
|
||||
return args.result == 0 ? JNI_TRUE : JNI_FALSE;
|
||||
#else
|
||||
(void)env; (void)inPath; (void)outPath;
|
||||
return JNI_FALSE;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Android replacement for nibtools' include/LINUX/mnibarch.h — same content,
|
||||
* minus the <opencbm.h> include (see opencbm.h stub in this directory).
|
||||
*/
|
||||
#ifndef NIBTOOLS_ANDROID_MNIBARCH_H
|
||||
#define NIBTOOLS_ANDROID_MNIBARCH_H
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#define delay(x) usleep((x) * 1000)
|
||||
#define msleep(x) delay(x)
|
||||
|
||||
#define ARCH_MAINDECL
|
||||
#define ARCH_SIGNALDECL
|
||||
|
||||
typedef unsigned char BYTE;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Stub replacing nibtools' dependency on libopencbm (the real 1541 hardware
|
||||
* driver). nibconv — the only nibtools program we build — never calls any
|
||||
* OpenCBM function; CBM_FILE only appears in unused declarations pulled in
|
||||
* transitively via nibtools.h/ihs.h, so a bare type is enough to satisfy
|
||||
* the compiler without linking the real library.
|
||||
*/
|
||||
#ifndef NIBTOOLS_ANDROID_OPENCBM_STUB_H
|
||||
#define NIBTOOLS_ANDROID_OPENCBM_STUB_H
|
||||
|
||||
typedef int CBM_FILE;
|
||||
|
||||
#endif
|
||||
@@ -62,6 +62,17 @@
|
||||
android:text="💾"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_anleitung"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:padding="0dp"
|
||||
android:text="📖"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_help"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
@@ -259,14 +270,38 @@
|
||||
android:background="#333355"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_import_disks"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/btn_import_disks"
|
||||
android:textSize="12sp" />
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_import_disks"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@string/btn_import_disks"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="11sp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_download_disks"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/btn_download_disks"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="11sp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -11,12 +11,19 @@
|
||||
|
||||
<!-- Bottom drawer buttons -->
|
||||
<string name="btn_import_disks">Disketten importieren</string>
|
||||
<string name="btn_download_disks">Disketten herunterladen</string>
|
||||
<string name="sound_off">Ton: AUS</string>
|
||||
<string name="sound_on">Ton: AN</string>
|
||||
|
||||
<!-- Disk picker -->
|
||||
<string name="picker_title">SCHWUM-Disketten auswählen</string>
|
||||
|
||||
<!-- Disk download (Internet Archive C64 Preservation Project) -->
|
||||
<string name="disks_download_msg">Lädt alle 8 Folgen-Disketten vom C64 Preservation Project des Internet Archive (von der Community erhaltene historische Disketten-Dumps) herunter und konvertiert sie zur Nutzung hier.</string>
|
||||
<string name="disks_download_confirm">Herunterladen</string>
|
||||
<string name="disks_downloading">Lade Diskette %1$d von %2$d…</string>
|
||||
<string name="disks_download_done">%1$d von %2$d Disketten heruntergeladen</string>
|
||||
|
||||
<!-- ROM import -->
|
||||
<string name="roms_missing_title">C64-ROMs erforderlich</string>
|
||||
<string name="roms_missing_msg">Diese App enthält keine Commodore-ROM-Dateien (sie sind urheberrechtlich geschützt). Bitte eigenen, legal erworbenen ROM-Dump bereitstellen. Fehlend: %1$s</string>
|
||||
|
||||
@@ -13,12 +13,19 @@
|
||||
|
||||
<!-- Bottom drawer buttons -->
|
||||
<string name="btn_import_disks">Import Disks</string>
|
||||
<string name="btn_download_disks">Fetch Disks</string>
|
||||
<string name="sound_off">Sound: OFF</string>
|
||||
<string name="sound_on">Sound: ON</string>
|
||||
|
||||
<!-- Disk picker -->
|
||||
<string name="picker_title">Select SCHWUM disk images</string>
|
||||
|
||||
<!-- Disk download (Internet Archive C64 Preservation Project) -->
|
||||
<string name="disks_download_msg">Downloads all 8 episode disks from the Internet Archive\'s C64 Preservation Project (community-preserved historical disk dumps) and converts them for use here.</string>
|
||||
<string name="disks_download_confirm">Download</string>
|
||||
<string name="disks_downloading">Downloading disk %1$d of %2$d…</string>
|
||||
<string name="disks_download_done">Downloaded %1$d of %2$d disks</string>
|
||||
|
||||
<!-- ROM import -->
|
||||
<string name="roms_missing_title">C64 ROMs required</string>
|
||||
<string name="roms_missing_msg">This app does not include Commodore ROM files (they\'re copyrighted). Please supply your own legally-obtained dump. Missing: %1$s</string>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user