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/.cxx
|
||||||
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-src
|
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-src
|
||||||
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-libs
|
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-libs
|
||||||
|
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-src
|
||||||
|
/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-libs
|
||||||
|
|
||||||
/SchwertUndMagieOnPebbleWatchApp/build
|
/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 |
|
| `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 |
|
| `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/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
|
### Disk images
|
||||||
|
|
||||||
@@ -127,3 +128,34 @@ same external files directory as the ROMs (via USB or adb), then load via:
|
|||||||
```kotlin
|
```kotlin
|
||||||
engine.loadDisk(getExternalFilesDir(null)!!.absolutePath + "/schwert_und_magie_1.d64")
|
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 {
|
tasks.whenTaskAdded {
|
||||||
if (name.startsWith("configureCMake") || name.startsWith("buildCMake") ||
|
if (name.startsWith("configureCMake") || name.startsWith("buildCMake") ||
|
||||||
name.startsWith("externalNativeBuild")) {
|
name.startsWith("externalNativeBuild")) {
|
||||||
dependsOn("buildVice")
|
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'. */
|
/** Current 40x25 C64 text screen as ASCII, rows separated by '\n'. */
|
||||||
external fun getScreenText(): String
|
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 {
|
companion object {
|
||||||
init { System.loadLibrary("vice_jni") }
|
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 ----------------------------------------------------------
|
// ---- lifecycle ----------------------------------------------------------
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
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_help).setOnClickListener { showHelp() }
|
||||||
|
findViewById<Button>(R.id.btn_anleitung).setOnClickListener { showAnleitung() }
|
||||||
findViewById<Button>(R.id.btn_savestate).setOnClickListener { showSaveStatePanel() }
|
findViewById<Button>(R.id.btn_savestate).setOnClickListener { showSaveStatePanel() }
|
||||||
|
|
||||||
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
||||||
launchPicker(diskPickerLauncher, getString(R.string.picker_title), multiSelect = true)
|
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)
|
val btnSound = findViewById<Button>(R.id.btn_sound)
|
||||||
btnSound.setOnClickListener {
|
btnSound.setOnClickListener {
|
||||||
@@ -548,7 +666,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun diskFile(slot: DiskSlot): File? {
|
private fun diskFile(slot: DiskSlot): File? {
|
||||||
val assetDir = getExternalFilesDir(null) ?: filesDir
|
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() {
|
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) {
|
private fun appendLog(entry: String) {
|
||||||
val ts = timeFormat.format(Date())
|
val ts = timeFormat.format(Date())
|
||||||
tvLog.text = "[$ts] $entry\n${tvLog.text}"
|
tvLog.text = "[$ts] $entry\n${tvLog.text}"
|
||||||
|
|||||||
@@ -14,8 +14,19 @@ else()
|
|||||||
message(STATUS "No libvice.a found — building stub (placeholder framebuffer)")
|
message(STATUS "No libvice.a found — building stub (placeholder framebuffer)")
|
||||||
endif()
|
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 -------------------------------------------------
|
# ---- 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)
|
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}")
|
target_link_libraries(vice_jni "${VICE_LIB}")
|
||||||
endif()
|
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:text="💾"
|
||||||
android:textSize="16sp" />
|
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
|
<Button
|
||||||
android:id="@+id/btn_help"
|
android:id="@+id/btn_help"
|
||||||
style="?attr/materialButtonOutlinedStyle"
|
style="?attr/materialButtonOutlinedStyle"
|
||||||
@@ -259,14 +270,38 @@
|
|||||||
android:background="#333355"
|
android:background="#333355"
|
||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="4dp">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_import_disks"
|
android:id="@+id/btn_import_disks"
|
||||||
style="?attr/materialButtonOutlinedStyle"
|
style="?attr/materialButtonOutlinedStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
android:text="@string/btn_import_disks"
|
android:text="@string/btn_import_disks"
|
||||||
android:textSize="12sp" />
|
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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -11,12 +11,19 @@
|
|||||||
|
|
||||||
<!-- Bottom drawer buttons -->
|
<!-- Bottom drawer buttons -->
|
||||||
<string name="btn_import_disks">Disketten importieren</string>
|
<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_off">Ton: AUS</string>
|
||||||
<string name="sound_on">Ton: AN</string>
|
<string name="sound_on">Ton: AN</string>
|
||||||
|
|
||||||
<!-- Disk picker -->
|
<!-- Disk picker -->
|
||||||
<string name="picker_title">SCHWUM-Disketten auswählen</string>
|
<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 -->
|
<!-- ROM import -->
|
||||||
<string name="roms_missing_title">C64-ROMs erforderlich</string>
|
<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>
|
<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 -->
|
<!-- Bottom drawer buttons -->
|
||||||
<string name="btn_import_disks">Import Disks</string>
|
<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_off">Sound: OFF</string>
|
||||||
<string name="sound_on">Sound: ON</string>
|
<string name="sound_on">Sound: ON</string>
|
||||||
|
|
||||||
<!-- Disk picker -->
|
<!-- Disk picker -->
|
||||||
<string name="picker_title">Select SCHWUM disk images</string>
|
<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 -->
|
<!-- ROM import -->
|
||||||
<string name="roms_missing_title">C64 ROMs required</string>
|
<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>
|
<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