Added Retro Rewind Quest Support

This commit is contained in:
iChris4 committed 2026-09-17 21:36:00 +02:00
1 parent 0bf2675db1
commit 58c0890983
18 files changed
+378 -168

No files matched your search

+1 -1
View File
@@ -51,7 +51,7 @@ add_subdirectory("${MKW_REPO_ROOT}/runtime" wiicompiled-runtime)
# App-owned JNI glue lives in each product library SDLActivity loads, and in the game kit
# probe (runtime/cmake/PublicProducts.cmake) whose objects the player's game is linked with.
foreach(product IN ITEMS WiiCompiled RetroRewind mkw_quest_kit_probe)
foreach(product IN ITEMS WiiCompiled RetroRewind mkw_quest_kit_probe mkw_quest_kit_probe_retro)
if(TARGET ${product})
target_sources(${product} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/quest_surface_jni.cpp")
target_include_directories(${product} PRIVATE "${MKW_REPO_ROOT}/aurora-main/include")
@@ -17,6 +17,8 @@ object GameStorage {
// Must match kApplicationDirectoryName in runtime/include/runtime_config.h.
const val APP_DIRECTORY = "WiiCompiledOpenXRVR"
const val DISC_DIRECTORY = "DATA"
/** The Retro Rewind pack the modded product reads (`[paths] retro_rewind_root`). */
const val MOD_DIRECTORY = "RetroRewind6"
enum class DiscStatus { Missing, Incomplete, Ready }
@@ -26,6 +28,12 @@ object GameStorage {
fun discDirectory(context: Context): File = File(gameRoot(context), DISC_DIRECTORY)
fun modDirectory(context: Context): File = File(gameRoot(context), MOD_DIRECTORY)
/** Whether this app needs the Retro Rewind pack and has it. Always true for the base game. */
fun modContentReady(context: Context): Boolean =
BuildConfig.PROFILE != "retro_rewind" || File(modDirectory(context), "Binaries/Code.pul").isFile
fun configFile(context: Context): File = File(gameRoot(context), "Config.toml")
fun logsDirectory(context: Context): File = File(gameRoot(context), "Logs")
@@ -60,22 +68,28 @@ object GameStorage {
importDirectory(context).mkdirs()
val config = configFile(context)
if (!config.isFile) {
val disc = discDirectory(context).absolutePath
config.writeText(
"""
# WiiCompiled Quest configuration. Edit with the launcher, the in-game panel or adb pull/push.
[paths]
dvd_root = "$disc"
[video]
widescreen = true
resolution_multiplier = 1.0
[vr]
enabled = true
render_scale = 1.0
""".trimIndent() + "\n",
// Written line by line: trimIndent runs after interpolation, so an interpolated line
// would take the indent off every other one.
val lines = mutableListOf(
"# WiiCompiled Quest configuration. Edit with the launcher, the in-game panel or adb pull/push.",
"[paths]",
"dvd_root = \"${discDirectory(context).absolutePath}\"",
)
// The modded product reads its pack from retro_rewind_root, which sits next to DATA here.
if (BuildConfig.PROFILE == "retro_rewind") {
lines += "retro_rewind_root = \"${modDirectory(context).absolutePath}\""
}
lines += listOf(
"",
"[video]",
"widescreen = true",
"resolution_multiplier = 1.0",
"",
"[vr]",
"enabled = true",
"render_scale = 1.0",
)
config.writeText(lines.joinToString("\n", postfix = "\n"))
}
return gameRoot
}
@@ -9,10 +9,6 @@ import java.security.MessageDigest
*/
object BuildRecipe {
/** The translated source lists emit-build-shards writes into shards.cmake, in link order. */
val TRANSLATED_LISTS = listOf("MKW_BASE_COMMON_SHARDS", "MKW_BASE_PORTABLE_SENSITIVE_SHARDS")
const val REGISTRATION_LIST = "MKW_BASE_REGISTRATION_SOURCES"
/**
* One argument of a response file for clang or lld on Android, which read them with GNU
* quoting: a backslash escapes the next character.
@@ -53,6 +53,7 @@ object GameBuild {
}
private const val TAG = "WiiCompiledLauncher"
private const val KIT_SCHEMA = 2
private const val TOOLCHAIN_ASSETS = "quest_toolchain"
private const val KIT_ASSETS = "game_kit"
private const val MARKER = ".complete"
@@ -66,6 +67,9 @@ object GameBuild {
/** Null on success, otherwise the message to show. */
fun run(context: Context, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? {
if (!BuildConfig.ON_DEVICE_BUILD) {
return "This app cannot build its game on the headset. Build it on a computer and use Import from computer."
}
val disc = GameStorage.discDirectory(context)
if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) {
return "The game files (DATA) are needed to build the game. Select your disc image first."
@@ -120,6 +124,9 @@ object GameBuild {
val kitFingerprint = GameLibrary.kitFingerprint(context) ?: return "This app carries no game kit."
unpackKit(kitFingerprint)
val recipe = JSONObject(File(kit, "kit.json").readText())
if (recipe.optInt("schema") != KIT_SCHEMA) {
return "This app's game kit is version ${recipe.optInt("schema")}, which this builder does not know."
}
report(40, Step.Prepare)
downloadNdk(JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/${toolchainManifest.getString("ndk")}").reader().use { it.readText() }))
@@ -336,21 +343,42 @@ object GameBuild {
stamp.writeText(identity)
}
val blob = File(generated, "data_sections_init_blobs.S")
val elfBlob = File(work, "data_sections_init_blobs_android.S")
val blobText = blob.readText()
val elfText = BuildRecipe.elfBlobAssembly(blobText)
if (!elfBlob.isFile || elfBlob.readText() != elfText) elfBlob.writeText(elfText)
// Blob assembly generated on Windows is rewritten for ELF, as PublicProducts.cmake does.
fun elfAssembly(source: File): File {
val rewritten = File(work, source.nameWithoutExtension + "_android.S")
val text = BuildRecipe.elfBlobAssembly(source.readText())
if (!rewritten.isFile || rewritten.readText() != text) rewritten.writeText(text)
return rewritten
}
// Which sources fill each link slot is the recipe's to say, so this builds either flavour.
data class Job(val kind: String, val source: File, val slot: String)
val jobs = ArrayList<Job>()
jobs += Job("runtime", File(generated, "data_sections_init.cpp"), "runtime")
jobs += Job("runtime", File(generated, "guest_symbol_table.cpp"), "runtime")
jobs += Job("asm", elfBlob, "runtime")
BuildRecipe.sourceList(shards, BuildRecipe.REGISTRATION_LIST).forEach { jobs += Job("product", File(it), "product") }
val translated = BuildRecipe.TRANSLATED_LISTS.flatMap { BuildRecipe.sourceList(shards, it) }
if (translated.isEmpty()) throw IOException("The translation produced no game code.")
translated.forEach { jobs += Job("translated", File(it), "translated") }
val sources = recipe.getJSONObject("sources")
for (slot in sources.keys()) {
val entries = sources.getJSONArray(slot)
val slotSources = ArrayList<File>()
for (i in 0 until entries.length()) {
val entry = entries.getString(i)
when {
entry.startsWith("@") -> BuildRecipe.sourceList(shards, entry.substring(1)).forEach { slotSources += File(it) }
else -> slotSources += File(BuildRecipe.expand(entry, mapOf("workspace" to workspace.absolutePath)))
}
}
if (slotSources.isEmpty()) {
throw IOException("The translation has no $slot sources for a ${recipe.getString("product")} game.")
}
// Assembly is assembled; a mod shard compiles with the same flags as a translated one.
slotSources.forEach { source ->
val assembly = source.name.endsWith(".S")
val kind = when {
assembly -> "asm"
slot == "mod" -> "translated"
else -> slot
}
jobs += Job(kind, if (assembly) elfAssembly(source) else source, slot)
}
}
if (jobs.map { BuildRecipe.objectName(it.source.path) }.toSet().size != jobs.size) {
throw IOException("Two generated sources share an object name.")
}
@@ -361,7 +389,7 @@ object GameBuild {
"workspace" to workspace.absolutePath,
)
val compileFlags = recipe.getJSONObject("compile")
for (kind in listOf("runtime", "asm", "product", "translated")) {
for (kind in jobs.map { it.kind }.distinct()) {
val flags = compileFlags.getJSONArray(kind).let { array -> (0 until array.length()).map { BuildRecipe.expand(array.getString(it), values) } }
File(work, "$kind.rsp").writeText(BuildRecipe.responseFile(flags))
}
@@ -211,11 +211,12 @@ class LauncherActivity : Activity() {
mainAction = when {
running -> Action.Resume
discStatus != GameStorage.DiscStatus.Ready -> Action.SelectDisc
gameStatus != GameLibrary.Status.Ready -> Action.BuildGame
gameStatus != GameLibrary.Status.Ready -> if (BuildConfig.ON_DEVICE_BUILD) Action.BuildGame else Action.ImportGame
else -> Action.Play
}
secondaryAction = when {
settingUp || running -> null
mainAction == Action.ImportGame -> null
gameStatus != GameLibrary.Status.Ready || mainAction == Action.SelectDisc -> Action.ImportGame
else -> null
}
@@ -279,10 +280,14 @@ class LauncherActivity : Activity() {
setup is GameSetup.State.Failed && building -> showBanner(getString(R.string.home_build_failed, setup.message), warning = true)
setup is GameSetup.State.Failed -> showBanner(getString(R.string.home_setup_failed, setup.message), warning = true)
discStatus == GameStorage.DiscStatus.Incomplete -> showBanner(getString(R.string.home_data_incomplete, disc), warning = true)
gameStatus == GameLibrary.Status.Stale -> showBanner(getString(R.string.home_game_stale), warning = true)
!GameStorage.modContentReady(this) ->
showBanner(getString(R.string.home_mod_missing, GameStorage.modDirectory(this).absolutePath), warning = true)
gameStatus == GameLibrary.Status.Stale ->
showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_game_stale else R.string.home_game_stale_pc), warning = true)
gameStatus == GameLibrary.Status.Missing && discStatus == GameStorage.DiscStatus.Missing ->
showBanner(getString(R.string.home_setup_intro), warning = false)
gameStatus == GameLibrary.Status.Missing -> showBanner(getString(R.string.home_game_missing), warning = false)
showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_setup_intro else R.string.home_setup_intro_pc), warning = false)
gameStatus == GameLibrary.Status.Missing ->
showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_game_missing else R.string.home_game_missing_pc), warning = false)
discStatus == GameStorage.DiscStatus.Missing -> showBanner(getString(R.string.home_data_missing, disc), warning = false)
else -> showBanner(null)
}
@@ -322,7 +327,7 @@ class LauncherActivity : Activity() {
/** Builds the game on this headset from DATA, after saying what that takes. */
private fun buildGame() {
if (GameSetup.isRunning || GameStorage.discStatus(this) != GameStorage.DiscStatus.Ready) return
if (!BuildConfig.ON_DEVICE_BUILD || GameSetup.isRunning || GameStorage.discStatus(this) != GameStorage.DiscStatus.Ready) return
val message = if (GameLibrary.status(this) == GameLibrary.Status.Ready) R.string.home_build_replace_message else R.string.home_build_message
confirm(R.string.home_build_title, message, R.string.home_build) {
GameSetupService.startBuild(this)
@@ -282,11 +282,13 @@ class SettingsPage(
GameLibrary.Status.Missing -> activity.getString(R.string.about_game_missing)
}
info(R.string.about_game, gameText, stacked = true)
action(
R.string.home_build, R.string.about_build_helper, R.string.home_build,
enabled = !GameSetup.isRunning && GameStorage.discStatus(activity) == GameStorage.DiscStatus.Ready,
) {
buildGame()
if (BuildConfig.ON_DEVICE_BUILD) {
action(
R.string.home_build, R.string.about_build_helper, R.string.home_build,
enabled = !GameSetup.isRunning && GameStorage.discStatus(activity) == GameStorage.DiscStatus.Ready,
) {
buildGame()
}
}
action(R.string.home_import, R.string.about_import_helper, R.string.home_import, enabled = !GameSetup.isRunning) {
importGame()
@@ -24,6 +24,9 @@
<string name="home_setup_intro">Nothing from Mario Kart Wii comes with this app: the game is built from your own PAL (RMCP01) disc.\n\nOn this headset: copy your disc image (ISO, WBFS, RVZ…) here, for example into Download, press Select disc image, then Build on this Quest.\n\nWith a computer: in the WiiCompiled VR launcher, use Build for Quest with your disc image, copy the .wcgame file it makes to this headset, then press Import from computer.</string>
<string name="home_game_missing">The game files are ready. Now build the game from them: Build on this Quest takes about 30–40 minutes on a Quest 3. With a computer, use Build for Quest in the WiiCompiled VR launcher instead, copy the .wcgame file to this headset and press Import from computer.</string>
<string name="home_game_stale">This app was updated, so the game must be built again for it: press Build on this Quest, or use Build for Quest in the WiiCompiled VR launcher again and Import from computer.</string>
<string name="home_setup_intro_pc">Nothing from Mario Kart Wii or Retro Rewind comes with this app: the game is built from your own PAL (RMCP01) disc.\n\nCopy your disc image (ISO, WBFS, RVZ…) here, for example into Download, and press Select disc image. Then, on a computer, use Build for Quest in the WiiCompiled VR launcher with this app’s APK, copy the .wcgame file it makes to this headset and press Import from computer.</string>
<string name="home_game_missing_pc">The game files are ready. The game itself is built on a computer: in the WiiCompiled VR launcher, use Build for Quest with this app’s APK, copy the .wcgame file to this headset, then press Import from computer.</string>
<string name="home_game_stale_pc">This app was updated, so the game must be built again for it: use Build for Quest in the WiiCompiled VR launcher again, then Import from computer.</string>
<string name="home_build">Build on this Quest</string>
<string name="home_build_preparing">Preparing…</string>
<string name="home_building">Building %1$d%%</string>
@@ -46,6 +49,7 @@
<string name="home_replace_game_title">Replace the game?</string>
<string name="home_replace_game_message">The game installed now stays until the new game file has been checked and completely imported.</string>
<string name="home_data_incomplete">DATA is there but incomplete or unreadable. Select your disc image to extract it again, or copy the whole extracted partition (sys/ and files/, not just files/) to:\n%1$s</string>
<string name="home_mod_missing">The Retro Rewind pack is missing, so the game cannot start. Copy the RetroRewind6 folder (the one with Binaries, Tracks and Assets in it) to:\n%1$s</string>
<string name="home_checking">Checking disc…</string>
<string name="home_extracting">Extracting %1$d%%</string>
<string name="home_finishing">Finishing…</string>
@@ -22,6 +22,17 @@ class BuildRecipeTest {
assertThrows(IllegalArgumentException::class.java) { BuildRecipe.expand("-I{workspace}", values) }
}
@Test
fun expandsAnyLinkSlotTheRecipeNames() {
// Retro Rewind adds a mod slot; a builder only has to expand what the recipe lists.
val arguments = BuildRecipe.linkArguments(
listOf("{game:product}", "{game:mod}", "--start-lib", "{game:translated}", "--end-lib"),
emptyMap(),
mapOf("product" to listOf("/o/reg.o"), "mod" to listOf("/o/m1.o", "/o/m2.o"), "translated" to listOf("/o/base.o")),
)
assertEquals(listOf("/o/reg.o", "/o/m1.o", "/o/m2.o", "--start-lib", "/o/base.o", "--end-lib"), arguments)
}
@Test
fun expandsTheLinkSlotsInPlace() {
val template = listOf("-o", "{output}", "{kit}/objects/001_a.o", "{game:runtime}", "--start-lib", "{game:translated}", "--end-lib", "-lm")
@@ -59,7 +70,10 @@ class BuildRecipeTest {
)
assertEquals(emptyList<String>(), BuildRecipe.sourceList(shards, "MKW_BASE_PORTABLE_SENSITIVE_SHARDS"))
assertEquals(emptyList<String>(), BuildRecipe.sourceList(shards, "MKW_RETRO_MOD_SHARDS"))
assertEquals(listOf("C:/w/generated/build_shards/base_registration/registration_00.cpp"), BuildRecipe.sourceList(shards, BuildRecipe.REGISTRATION_LIST))
assertEquals(
listOf("C:/w/generated/build_shards/base_registration/registration_00.cpp"),
BuildRecipe.sourceList(shards, "MKW_BASE_REGISTRATION_SOURCES"),
)
}
@Test