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

+18 -14
View File
@@ -87,23 +87,27 @@ $apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1
if (-not $apk) { throw "No APK under $apkDir" }
Write-Host "APK: $($apk.FullName)"
# The base APK must be distributable: no translated game inside, only the game kit.
if ($Flavor -eq 'base') {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
try {
$gameLibraries = @($archive.Entries | Where-Object { $_.FullName -match '^lib/[^/]+/libmain[^/]*\.so$' })
$hasKit = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/game_kit/kit.json' }).Count -gt 0
$hasToolchain = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/quest_toolchain/files.zip' }).Count -gt 0
} finally { $archive.Dispose() }
if ($gameLibraries.Count -gt 0) { throw "The APK contains a translated game library: $($gameLibraries.FullName -join ', ')" }
if (-not $hasKit) { throw 'The APK has no game kit (assets/game_kit/kit.json)' }
if (-not $hasToolchain) { throw 'The APK has no build toolchain (assets/quest_toolchain/files.zip)' }
}
# Every APK must be distributable: no translated game inside, only the game kit. The toolchain
# that builds one on the headset travels in the base APK only.
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
try {
$gameLibraries = @($archive.Entries | Where-Object { $_.FullName -match '^lib/[^/]+/libmain[^/]*\.so$' })
$hasKit = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/game_kit/kit.json' }).Count -gt 0
$hasToolchain = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/quest_toolchain/files.zip' }).Count -gt 0
} finally { $archive.Dispose() }
if ($gameLibraries.Count -gt 0) { throw "The APK contains a translated game library: $($gameLibraries.FullName -join ', ')" }
if (-not $hasKit) { throw 'The APK has no game kit (assets/game_kit/kit.json)' }
if ($Flavor -eq 'base' -and -not $hasToolchain) { throw 'The APK has no build toolchain (assets/quest_toolchain/files.zip)' }
if ($Flavor -ne 'base' -and $hasToolchain) { throw 'Only the base APK carries the on-headset build toolchain' }
if ($Install) {
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
& $adb install -r $apk.FullName
if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" }
Write-Host 'Installed. Build the game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.'
if ($Flavor -eq 'base') {
Write-Host 'Installed. Build the game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.'
} else {
Write-Host "Installed. Build its game on this PC with android/Build-QuestGame.ps1 -Flavor $Flavor -Install."
}
}
+13 -7
View File
@@ -13,7 +13,8 @@
# downloaded, the toolkit's ninja and the workspace's recomp.yml.
#
# -Generated is the translator output (the dev workspace's generated/ by default). -Kit is the game
# kit the Quest app carries; it defaults to the one the last APK build exported. -Data adds the
# kit the Quest app carries; it defaults to the one the last APK build of -Flavor (base or
# retroRewind, whose translation must include the mod) exported. -Data adds the
# extracted disc (the directory holding sys/ and files/) so the headset needs nothing else.
# -Install pushes the package into the app's Import folder over adb, where the launcher picks it
# up the next time it opens.
@@ -30,6 +31,7 @@ param(
[string]$Sysroot = '',
[string]$Ninja = '',
[string]$BuiltBy = 'android/Build-QuestGame.ps1',
[ValidateSet('base', 'retroRewind')] [string]$Flavor = 'base',
[int]$TranslatedJobs = 0,
[switch]$Install
)
@@ -40,15 +42,18 @@ $root = $PSScriptRoot
$workspace = (Resolve-Path (Join-Path $root '..')).Path
if (-not $Generated) { $Generated = Join-Path $workspace '.scratch\vr-build-workspace\BuildWorkspace\generated' }
if (-not $Manifest) { $Manifest = Join-Path $workspace 'projects\mkwii\recomp.yml' }
if (-not $BuildDir) { $BuildDir = Join-Path $root 'app\build\questGame' }
if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$Flavor" }
if (-not $Kit) {
# The kit the last APK build packaged (the exportBase*QuestGameKit tasks), so the game
# matches the app it will be imported into.
# The kit this flavour's last APK build packaged (its export<Flavor>*QuestGameKit task), so the
# game matches the app it will be imported into.
$exportTask = 'export' + $Flavor.Substring(0, 1).ToUpper() + $Flavor.Substring(1)
$Kit = Get-ChildItem -Path (Join-Path $root 'app\build\generated') -Filter kit.json -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Directory.Name -eq 'game_kit' } |
Where-Object { $_.Directory.Name -eq 'game_kit' -and $_.FullName -like "*$exportTask*" } |
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
}
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) { throw 'No game kit; run Build-Quest.ps1 first or pass -Kit' }
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) {
throw "No $Flavor game kit; run Build-Quest.ps1 -Flavor $Flavor first, or pass -Kit"
}
if (-not (Test-Path (Join-Path $Generated 'build_shards\shards.cmake'))) { throw "No translated graph at $Generated" }
$sdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
@@ -91,7 +96,8 @@ Write-Host "Game package: $Output (kit $($game.kitFingerprint))"
if ($Install) {
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
$import = '/sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/Import'
$package = if ($Flavor -eq 'retroRewind') { 'org.wiicompiled.quest.retrorewind' } else { 'org.wiicompiled.quest' }
$import = "/sdcard/Android/data/$package/files/WiiCompiledOpenXRVR/Import"
# The launcher creates Import itself and must own it to remove imported packages (see
# Run-Quest.ps1 on adb-created directories), so only push into one that exists.
$exists = ((& $adb shell "test -d '$import' && echo yes") | Out-String).Trim()
+6 -5
View File
@@ -1,16 +1,17 @@
# Exports the Quest game kit (see QuestGameKit.psm1) from a configured and built Android CMake
# tree. android/app/build.gradle.kts runs this after the native build and packages the result as
# the APK's game_kit assets.
# the APK's game_kit assets, once per app flavour.
#
# powershell -ExecutionPolicy Bypass -File android/Export-QuestGameKit.ps1 -CMakeBinaryDir <dir> -OutputDir <dir> -LlvmStrip <llvm-strip>
# powershell -ExecutionPolicy Bypass -File android/Export-QuestGameKit.ps1 -CMakeBinaryDir <dir> -OutputDir <dir> -LlvmStrip <llvm-strip> [-Product base|retro_rewind]
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
[Parameter(Mandatory)] [string]$OutputDir,
[Parameter(Mandatory)] [string]$LlvmStrip
[Parameter(Mandatory)] [string]$LlvmStrip,
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'QuestGameKit.psm1') -Force
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$fingerprint = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip
Write-Host "Quest game kit $fingerprint -> $OutputDir"
$fingerprint = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip -Product $Product
Write-Host "Quest game kit ($Product) $fingerprint -> $OutputDir"
+5
View File
@@ -271,6 +271,11 @@ function Publish-Translator([string]$Destination, [string]$Work) {
& (Join-Path $env:SystemRoot 'System32\robocopy.exe') (Join-Path $repo $tree) (Join-Path $stage $tree) /E /XD bin obj /NFL /NDL /NJH /NJS /NP | Out-Null
if ($LASTEXITCODE -ge 8) { throw "Staging $tree failed (robocopy $LASTEXITCODE)" }
}
# Build output the checkout may hold for another target framework would be picked up here and
# make the retargeted publish resolve the wrong references.
Get-ChildItem -Recurse -Directory $stage |
Where-Object { $_.Name -eq 'bin' -or $_.Name -eq 'obj' } |
ForEach-Object { if (Test-Path $_.FullName) { Remove-Item -Recurse -Force $_.FullName } }
foreach ($project in Get-ChildItem -Recurse -Filter *.csproj $stage) {
$text = [IO.File]::ReadAllText($project.FullName)
$retargeted = $text.Replace('<TargetFramework>net8.0</TargetFramework>', '<TargetFramework>net10.0</TargetFramework>')
+113 -54
View File
@@ -8,12 +8,14 @@
# exported from CMake's own build graph so its flags and link order cannot drift from a normal
# build:
#
# * the link line of mkw_quest_kit_probe (runtime/cmake/PublicProducts.cmake), which is
# WiiCompiled without the game, becomes kit.json's link inputs, with {game:*} markers where
# the game's objects sat in WiiCompiled's own link;
# * the link line of the flavour's probe (runtime/cmake/PublicProducts.cmake), which is that
# product without any translated code, becomes kit.json's link inputs, with {game:*} markers
# where the game's objects sat in the product's own link;
# * the compile commands CMake recorded for one source of each generated kind (translated shard,
# registration shard, disc-generated runtime source, blob assembly) become kit.json's
# compile flags, with include paths reduced to the kit's copy of runtime/include.
# compile flags, with include paths reduced to the kit's copy of runtime/include;
# * kit.json's "sources" says which of the translator's build_shards lists fill each slot, so a
# builder never has to know which flavour it is building.
#
# Placeholders in kit.json: {kit} the extracted kit, {sysroot} the NDK sysroot the compiler
# uses, {workspace} the directory holding the translator's generated/ tree.
@@ -22,9 +24,42 @@
Set-StrictMode -Version Latest
$script:KitSchema = 1
$script:KitSchema = 2
$script:GameSchema = 1
# What each app flavour's kit is made of. Slots name the game's objects in the probe's link line,
# and Sources says which of the translator's build_shards lists ("@NAME") or generated files fill
# each of them, in link order. Retro Rewind adds a slot: the mod's own shards and the
# profile-sensitive base shards it replaces the base flavour's with, linked as objects rather
# than through the base archive.
$script:KitProfiles = @{
base = @{
Probe = 'libmkw_quest_kit_probe'
ProductObjectPattern = '/base_product\.cpp\.o$'
RegistrationPattern = '/build_shards/base_registration/[^/]+\.cpp$'
Slots = @('runtime', 'product', 'translated')
Sources = [ordered]@{
runtime = @('{workspace}/generated/data_sections_init.cpp', '{workspace}/generated/guest_symbol_table.cpp',
'{workspace}/generated/data_sections_init_blobs.S')
product = @('@MKW_BASE_REGISTRATION_SOURCES')
translated = @('@MKW_BASE_COMMON_SHARDS', '@MKW_BASE_PORTABLE_SENSITIVE_SHARDS')
}
}
retro_rewind = @{
Probe = 'libmkw_quest_kit_probe_retro'
ProductObjectPattern = '/retro_rewind_product\.cpp\.o$'
RegistrationPattern = '/build_shards/retro_rewind_registration/[^/]+\.cpp$'
Slots = @('runtime', 'product', 'mod', 'translated')
Sources = [ordered]@{
runtime = @('{workspace}/generated/data_sections_init.cpp', '{workspace}/generated/guest_symbol_table.cpp',
'{workspace}/generated/data_sections_init_blobs.S')
product = @('@MKW_RETRO_REGISTRATION_SOURCES')
mod = @('@MKW_RETRO_PORTABLE_SENSITIVE_SHARDS', '@MKW_RETRO_MOD_SHARDS', '@MKW_RETRO_EXTRA_SOURCES')
translated = @('@MKW_BASE_COMMON_SHARDS')
}
}
}
function ConvertTo-ForwardPath([string]$Path) {
return [IO.Path]::GetFullPath($Path).Replace('\', '/').TrimEnd('/')
}
@@ -165,7 +200,7 @@ function Split-DriverCommand([string]$Line) {
# sysroot/ and lib/clang/), {output}, and {game:runtime}, {game:product}, {game:translated}, each
# replaced by its object files (the translated ones already sit inside --start-lib/--end-lib).
function Get-KitLldArguments {
param([string]$ClangCxx, [string]$KitDir, [string[]]$Flags, [string[]]$Inputs)
param([string]$ClangCxx, [string]$KitDir, [string[]]$Flags, [string[]]$Inputs, [string[]]$Slots)
$kit = ConvertTo-ForwardPath $KitDir
$ndk = ConvertTo-ForwardPath (Join-Path (Split-Path -Parent $ClangCxx) '..')
# Forward slashes throughout: clang reads response files with GNU quoting, where '\' escapes.
@@ -173,7 +208,7 @@ function Get-KitLldArguments {
New-Item -ItemType Directory $probe | Out-Null
try {
$markers = [ordered]@{}
foreach ($slot in 'runtime', 'product', 'translated') {
foreach ($slot in $Slots) {
$markers[$slot] = ConvertTo-ForwardPath (Join-Path $probe "game_$slot.o")
[IO.File]::WriteAllBytes($markers[$slot], [byte[]]@())
}
@@ -182,12 +217,12 @@ function Get-KitLldArguments {
foreach ($flag in $Flags) { $arguments.Add((& $real $flag)) }
$arguments.Add('-o'); $arguments.Add("$probe/libmain.so")
foreach ($linkInput in $Inputs) {
switch ($linkInput) {
'{game:runtime}' { $arguments.Add($markers.runtime) }
'{game:product}' { $arguments.Add($markers.product) }
'{game:translated}' { $arguments.Add('-Wl,--start-lib'); $arguments.Add($markers.translated); $arguments.Add('-Wl,--end-lib') }
default { $arguments.Add((& $real $linkInput)) }
}
$slot = [regex]::Match($linkInput, '^\{game:(\w+)\}$').Groups[1].Value
if (-not $slot) { $arguments.Add((& $real $linkInput)); continue }
# Only the base archive's slot keeps archive semantics; the rest are plain objects.
if ($slot -eq 'translated') { $arguments.Add('-Wl,--start-lib') }
$arguments.Add($markers[$slot])
if ($slot -eq 'translated') { $arguments.Add('-Wl,--end-lib') }
}
$rsp = Join-Path $probe 'link.rsp'
[IO.File]::WriteAllText($rsp, (($arguments | ForEach-Object { ConvertTo-QuotedArgument $_ }) -join "`n"), (New-Object Text.UTF8Encoding $false))
@@ -218,7 +253,7 @@ function Get-KitLldArguments {
if ($argument -match '[A-Za-z]:/') { throw "The kit link names a path outside the kit and the NDK: $argument" }
$lld.Add($argument)
}
foreach ($required in '{output}', '{game:runtime}', '{game:product}', '{game:translated}') {
foreach ($required in @('{output}') + @($Slots | ForEach-Object { "{game:$_}" })) {
if (-not $lld.Contains($required)) { throw "The expanded kit link lacks $required" }
}
return , $lld.ToArray()
@@ -233,16 +268,18 @@ function Export-QuestGameKit {
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
[Parameter(Mandatory)] [string]$OutputDir,
[Parameter(Mandatory)] [string]$RepoRoot,
[Parameter(Mandatory)] [string]$LlvmStrip
[Parameter(Mandatory)] [string]$LlvmStrip,
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
)
$ErrorActionPreference = 'Stop'
$binary = ConvertTo-ForwardPath $CMakeBinaryDir
$runtimeInclude = ConvertTo-ForwardPath (Join-Path $RepoRoot 'runtime/include')
$kitProfile = $script:KitProfiles[$Product]
# The probe's link edge: explicit inputs are the objects, LINK_LIBRARIES the archives.
$ninja = [IO.File]::ReadAllText("$binary/build.ninja")
$edge = [regex]::Match($ninja, '(?m)^build (\S*libmkw_quest_kit_probe\.so): (\S+) ([^\r\n]*)\r?\n((?: [^\r\n]*\r?\n)+)')
if (-not $edge.Success) { throw "No mkw_quest_kit_probe link edge in $binary/build.ninja" }
$edge = [regex]::Match($ninja, "(?m)^build (\S*$([regex]::Escape($kitProfile.Probe))\.so): (\S+) ([^\r\n]*)\r?\n((?: [^\r\n]*\r?\n)+)")
if (-not $edge.Success) { throw "No $($kitProfile.Probe) link edge in $binary/build.ninja" }
$explicit = ($edge.Groups[3].Value.Replace('$ ', '<ninja-space>') -split ' \|')[0]
$objects = @($explicit -split ' ' | Where-Object { $_ } | ForEach-Object { ConvertFrom-NinjaPath $_.Replace('<ninja-space>', '$ ') })
$vars = @{}
@@ -272,7 +309,7 @@ function Export-QuestGameKit {
}
$compile = [ordered]@{
translated = & $flagsFor $translatedEntry
product = & $flagsFor (Find-Command '/build_shards/base_registration/[^/]+\.cpp$')
product = & $flagsFor (Find-Command $kitProfile.RegistrationPattern)
runtime = & $flagsFor (Find-Command '/generated/data_sections_init\.cpp$')
asm = & $flagsFor (Find-Command 'data_sections_init_blobs[^/]*\.S$')
}
@@ -306,11 +343,17 @@ function Export-QuestGameKit {
}
for ($i = 0; $i -lt $objects.Count; $i++) {
$inputs.Add((& $copy $objects[$i] 'objects'))
if ($i -eq $lastRuntime) { $inputs.Add('{game:runtime}') }
if ($objects[$i] -match '/base_product\.cpp\.o$') { $inputs.Add('{game:product}') }
if ($objects[$i] -match $kitProfile.ProductObjectPattern) { $inputs.Add('{game:product}') }
if ($i -eq $lastRuntime) {
$inputs.Add('{game:runtime}')
# Retro Rewind links the mod's own objects after the runtime's, as its CMake target does.
if ($kitProfile.Slots -contains 'mod') { $inputs.Add('{game:mod}') }
}
}
if (-not $inputs.Contains('{game:runtime}') -or -not $inputs.Contains('{game:product}')) {
throw 'The probe link line lacks the runtime or product objects the game slots follow'
foreach ($slot in $kitProfile.Slots) {
if ($slot -ne 'translated' -and -not $inputs.Contains("{game:$slot}")) {
throw "The probe link line lacks the objects the {game:$slot} slot follows"
}
}
foreach ($token in ((ConvertFrom-NinjaPath $vars['LINK_LIBRARIES']) -split ' ' | Where-Object { $_ })) {
if ($token.StartsWith('-')) { $inputs.Add($token); continue }
@@ -334,7 +377,7 @@ function Export-QuestGameKit {
}
$linkFlags.Add('-Wl,-soname,libmain.so')
$lld = Get-KitLldArguments -ClangCxx (Join-Path (Split-Path -Parent $LlvmStrip) 'clang++.exe') -KitDir $OutputDir `
-Flags $linkFlags.ToArray() -Inputs $inputs.ToArray()
-Flags $linkFlags.ToArray() -Inputs $inputs.ToArray() -Slots $kitProfile.Slots
# What the headset's translator needs besides the disc: the game manifest and symbol map, and
# the runtime sources it indexes for native registrations, which must be the ones compiled into
@@ -346,11 +389,12 @@ function Export-QuestGameKit {
$recipe = [ordered]@{
schema = $script:KitSchema
product = 'base'
product = $Product
output = 'libmain.so'
target = $target
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
compile = $compile
sources = $kitProfile.Sources
link = [ordered]@{ flags = $linkFlags.ToArray(); inputs = $inputs.ToArray(); lld = $lld }
}
$recipeJson = $recipe | ConvertTo-Json -Depth 8 -Compress
@@ -409,21 +453,37 @@ function Invoke-QuestGameBuild {
$linkLines = @($recipe.link.flags | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) })
[IO.File]::WriteAllText("$build/link.rsp", ($linkLines -join "`n"), (New-Object Text.UTF8Encoding $false))
# The Windows installer generates the blob assembly for PE/COFF; rewrite it for ELF exactly
# as runtime/cmake/PublicProducts.cmake does.
$blob = [IO.File]::ReadAllText("$generated/data_sections_init_blobs.S")
$elf = $blob.Replace('.section .rdata,"dr"', '.section .rodata,"a",@progbits')
if ($elf -ne $blob) { $elf += "`n.section .note.GNU-stack,`"`",@progbits`n" }
[IO.File]::WriteAllText("$build/data_sections_init_blobs_android.S", $elf, (New-Object Text.UTF8Encoding $false))
# The Windows installer generates blob assembly for PE/COFF; every one of them is rewritten for
# ELF exactly as runtime/cmake/PublicProducts.cmake does, into the build directory.
$toElf = {
param([string]$Source)
$text = [IO.File]::ReadAllText($Source)
$elf = $text.Replace('.section .rdata,"dr"', '.section .rodata,"a",@progbits')
if ($elf -ne $text) { $elf += "`n.section .note.GNU-stack,`"`",@progbits`n" }
$rewritten = "$build/$([IO.Path]::GetFileNameWithoutExtension($Source))_android.S"
[IO.File]::WriteAllText($rewritten, $elf, (New-Object Text.UTF8Encoding $false))
return $rewritten
}
# Each slot's sources, as the recipe names them: a build_shards list ("@NAME") or one generated
# file.
$shards = "$generated/build_shards/shards.cmake"
$sources = [ordered]@{
runtime = @("$generated/data_sections_init.cpp", "$generated/guest_symbol_table.cpp")
runtimeAsm = @("$build/data_sections_init_blobs_android.S")
product = @(Read-ShardList $shards 'MKW_BASE_REGISTRATION_SOURCES')
translated = @((Read-ShardList $shards 'MKW_BASE_COMMON_SHARDS') + (Read-ShardList $shards 'MKW_BASE_PORTABLE_SENSITIVE_SHARDS'))
$sources = [ordered]@{}
foreach ($slot in $recipe.sources.PSObject.Properties.Name) {
$slotSources = New-Object System.Collections.Generic.List[string]
foreach ($entry in $recipe.sources.$slot) {
if ($entry.StartsWith('@')) {
Read-ShardList $shards $entry.Substring(1) | ForEach-Object { $slotSources.Add($_) }
} else {
$slotSources.Add((& $expand $entry))
}
}
$sources[$slot] = @($slotSources | ForEach-Object { if ($_.EndsWith('.S')) { & $toElf $_ } else { $_ } })
}
if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" }
foreach ($slot in $sources.Keys) {
if ($sources[$slot].Count -eq 0) { throw "The $slot sources of a $($recipe.product) game are missing from $shards" }
}
$ninjaText = New-Object System.Text.StringBuilder
$escape = { param([string]$p) $p.Replace('$', '$$').Replace(':', '$:').Replace(' ', '$ ') }
@@ -432,37 +492,36 @@ function Invoke-QuestGameBuild {
[void]$ninjaText.AppendLine("rule cxx`n command = $(ConvertTo-QuotedArgument $ClangCxx) @`$flags -c `$in -o `$out`n description = Compiling `$in")
[void]$ninjaText.AppendLine("rule asm`n command = $(ConvertTo-QuotedArgument $ClangC) @`$flags -c `$in -o `$out`n description = Assembling `$in")
[void]$ninjaText.AppendLine("rule link`n command = $(ConvertTo-QuotedArgument $ClangCxx) @`$flags -o `$out @`$out.rsp`n rspfile = `$out.rsp`n rspfile_content = `$inputs`n description = Linking `$out")
$objectsOf = @{ runtime = @(); product = @(); translated = @() }
$objectsOf = @{}
$seen = @{}
foreach ($kind in 'runtime', 'runtimeAsm', 'product', 'translated') {
foreach ($source in $sources[$kind]) {
foreach ($slot in $sources.Keys) {
$objectsOf[$slot] = @()
foreach ($source in $sources[$slot]) {
$stem = [IO.Path]::GetFileNameWithoutExtension($source)
$object = "obj/$stem.o"
if ($seen.ContainsKey($object)) { throw "Two generated sources share the object name $object" }
$seen[$object] = $true
$rule = if ($kind -eq 'runtimeAsm') { 'asm' } else { 'cxx' }
$flags = switch ($kind) { 'runtimeAsm' { 'asm.rsp' } default { "$kind.rsp" } }
# Assembly is assembled; a mod shard compiles with the same flags as a translated one.
$assembly = $source.EndsWith('.S')
$rule = if ($assembly) { 'asm' } else { 'cxx' }
$flags = if ($assembly) { 'asm.rsp' } elseif ($slot -eq 'mod') { 'translated.rsp' } else { "$slot.rsp" }
[void]$ninjaText.AppendLine("build $(& $escape $object): $rule $(& $escape $source)`n flags = $(& $escape "$build/$flags")")
if ($kind -eq 'translated') { [void]$ninjaText.AppendLine(' pool = translated') }
$slot = if ($kind -eq 'runtimeAsm') { 'runtime' } else { $kind }
if ($slot -eq 'translated' -or $slot -eq 'mod') { [void]$ninjaText.AppendLine(' pool = translated') }
$objectsOf[$slot] += "$build/$object"
}
}
$linkInputs = New-Object System.Collections.Generic.List[string]
foreach ($linkInput in $recipe.link.inputs) {
switch ($linkInput) {
'{game:runtime}' { $objectsOf.runtime | ForEach-Object { $linkInputs.Add($_) } }
'{game:product}' { $objectsOf.product | ForEach-Object { $linkInputs.Add($_) } }
'{game:translated}' {
# Archive semantics, as libmkw_base_shared.a has in a CMake build.
$linkInputs.Add('-Wl,--start-lib')
$objectsOf.translated | ForEach-Object { $linkInputs.Add($_) }
$linkInputs.Add('-Wl,--end-lib')
}
default { $linkInputs.Add((& $expand $linkInput)) }
}
$slot = [regex]::Match($linkInput, '^\{game:(\w+)\}$').Groups[1].Value
if (-not $slot) { $linkInputs.Add((& $expand $linkInput)); continue }
if (-not $objectsOf.ContainsKey($slot)) { throw "The kit recipe links a {game:$slot} slot it names no sources for" }
# The base shards keep the archive semantics libmkw_base_shared.a has in a CMake build;
# every other slot is linked as plain objects, as the product's own CMake target does.
if ($slot -eq 'translated') { $linkInputs.Add('-Wl,--start-lib') }
$objectsOf[$slot] | ForEach-Object { $linkInputs.Add($_) }
if ($slot -eq 'translated') { $linkInputs.Add('-Wl,--end-lib') }
}
$allObjects = @($objectsOf.runtime + $objectsOf.product + $objectsOf.translated | ForEach-Object { & $escape $_.Substring($build.Length + 1) })
$allObjects = @($sources.Keys | ForEach-Object { $objectsOf[$_] } | ForEach-Object { & $escape $_.Substring($build.Length + 1) })
$rspInputs = ($linkInputs | ForEach-Object { ConvertTo-QuotedArgument $_ }) -join ' '
[void]$ninjaText.AppendLine("build libmain.so: link $($allObjects -join ' ')`n flags = $(& $escape "$build/link.rsp")`n inputs = $($rspInputs.Replace('$', '$$'))")
[IO.File]::WriteAllText("$build/build.ninja", $ninjaText.ToString(), (New-Object Text.UTF8Encoding $false))
+36 -23
View File
@@ -130,6 +130,8 @@ android {
// The APK carries the game kit, not the game: the player's libmain.so is built from
// their own disc and loaded from private storage (see android/QuestGameKit.psm1).
buildConfigField("boolean", "EXTERNAL_GAME", "true")
// And it carries the toolchain that builds it on the headset (Prepare-QuestToolchain.ps1).
buildConfigField("boolean", "ON_DEVICE_BUILD", "true")
externalNativeBuild { cmake { targets += "mkw_quest_kit_probe" } }
}
create("retroRewind") {
@@ -138,10 +140,11 @@ android {
versionNameSuffix = "-retro-rewind"
buildConfigField("String", "MAIN_LIBRARY", "\"main_retro_rewind\"")
buildConfigField("String", "PROFILE", "\"retro_rewind\"")
// Not moved to the game kit yet: this flavour still bundles its translated library,
// so a Retro Rewind APK is for local use only.
buildConfigField("boolean", "EXTERNAL_GAME", "false")
externalNativeBuild { cmake { targets += "RetroRewind" } }
buildConfigField("boolean", "EXTERNAL_GAME", "true")
// Retro Rewind is built on a PC and imported: building it on the headset would also
// need the mod's Code.pul and its content there, which the launcher cannot fetch yet.
buildConfigField("boolean", "ON_DEVICE_BUILD", "false")
externalNativeBuild { cmake { targets += "mkw_quest_kit_probe_retro" } }
}
}
@@ -170,8 +173,8 @@ android {
packaging {
jniLibs {
useLegacyPackaging = false
// Built only to produce the game kit; its objects travel as assets/game_kit.
excludes += "**/libmkw_quest_kit_probe.so"
// Built only to produce each flavour's game kit; their objects travel as assets/game_kit.
excludes += "**/libmkw_quest_kit_probe*.so"
}
}
lint {
@@ -201,6 +204,10 @@ abstract class ExportQuestGameKit : DefaultTask() {
@get:Internal
abstract val llvmStrip: RegularFileProperty
/** "base" or "retro_rewind": which product's kit this flavour carries. */
@get:Internal
abstract val product: Property<String>
@get:Inject
abstract val execOperations: ExecOperations
@@ -212,10 +219,11 @@ abstract class ExportQuestGameKit : DefaultTask() {
@TaskAction
fun export() {
val app = appDir.get().asFile
val library = if (product.get() == "base") "libmkw_quest_kit_probe.so" else "libmkw_quest_kit_probe_retro.so"
val probe = File(app, "build/intermediates/cxx").walkTopDown()
.filter { it.name == "libmkw_quest_kit_probe.so" && it.parentFile.name == "arm64-v8a" }
.filter { it.name == library && it.parentFile.name == "arm64-v8a" }
.maxByOrNull { it.lastModified() }
?: throw GradleException("No libmkw_quest_kit_probe.so; the native build did not produce the game kit probe")
?: throw GradleException("No $library; the native build did not produce the game kit probe")
val configuration = probe.parentFile.parentFile.parentFile // <BuildType>/<hash>
val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
if (!File(binaryDir, "build.ninja").isFile) throw GradleException("No CMake tree at $binaryDir")
@@ -224,6 +232,7 @@ abstract class ExportQuestGameKit : DefaultTask() {
commandLine(
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
"-Product", product.get(),
)
}
}
@@ -263,32 +272,36 @@ abstract class PrepareQuestToolchain : DefaultTask() {
}
androidComponents {
onVariants(selector().withFlavor("profile" to "base")) { variant ->
val prepareToolchain = tasks.register<PrepareQuestToolchain>(
"prepare${variant.name.replaceFirstChar { it.uppercase() }}QuestToolchain"
) {
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
sources.from(fileTree(File(mkwRepoRoot, "translator/src")) { exclude("**/bin/**", "**/obj/**") })
sources.from(fileTree(File(mkwRepoRoot, "Launcher/WiiCompiled.Setup.Common")) { exclude("**/bin/**", "**/obj/**") })
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
ndkLlvm.set(sdkComponents.ndkDirectory.map { it.dir("toolchains/llvm/prebuilt/$host") })
}
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
onVariants { variant ->
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
// AGP packages every library in the CMake output directory, so a game library left over
// from a build that still linked the game would otherwise ship in the APK.
// Both flavours ship a kit instead of the game. AGP packages every library in the CMake
// output directory, so a game library left over from an earlier build would otherwise
// travel in the APK.
variant.packaging.jniLibs.excludes.add("**/libmain*.so")
val export = tasks.register<ExportQuestGameKit>("export${capitalized}QuestGameKit") {
dependsOn("merge${capitalized}NativeLibs")
appDir.set(layout.projectDirectory)
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
product.set(if (variant.flavorName == "retroRewind") "retro_rewind" else "base")
llvmStrip.set(sdkComponents.ndkDirectory.map {
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
})
}
variant.sources.assets?.addGeneratedSourceDirectory(export, ExportQuestGameKit::outputDir)
// Only the base flavour builds the game on the headset, so only it carries the toolchain:
// Retro Rewind also needs the mod's own content and translation, which is a PC build for now.
if (variant.flavorName != "retroRewind") {
val prepareToolchain = tasks.register<PrepareQuestToolchain>("prepare${capitalized}QuestToolchain") {
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
sources.from(fileTree(File(mkwRepoRoot, "translator/src")) { exclude("**/bin/**", "**/obj/**") })
sources.from(fileTree(File(mkwRepoRoot, "Launcher/WiiCompiled.Setup.Common")) { exclude("**/bin/**", "**/obj/**") })
ndkLlvm.set(sdkComponents.ndkDirectory.map { it.dir("toolchains/llvm/prebuilt/$host") })
}
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
}
}
}
+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