mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 00:00:17 +02:00
Added Retro Rewind Quest Support
This commit is contained in:
1 parent
0bf2675db1
commit
58c0890983
18 files changed
+378
-168
No files matched your search
@@ -72,8 +72,16 @@ internal sealed class QuestBuildService
|
||||
var questRoot = Path.Combine(installation.Root, QuestDirectoryName);
|
||||
var kitDirectory = Path.Combine(questRoot, "kit");
|
||||
_reporter.Progress(InstallStages.QuestKit, "Reading the game kit from the Quest app...", 2);
|
||||
var kitFingerprint = ExtractKit(apkPath, kitDirectory, cancellationToken);
|
||||
_reporter.Diagnostic($"Quest game kit {kitFingerprint}");
|
||||
var kit = ExtractKit(apkPath, kitDirectory, cancellationToken);
|
||||
_reporter.Diagnostic($"Quest game kit {kit.Fingerprint} ({kit.Product})");
|
||||
// A Retro Rewind app needs this installation's Retro Rewind translation, which only exists
|
||||
// once the mod has been built here at least once.
|
||||
if (kit.Product == "retro_rewind" && !HasRetroRewindShards(generated))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"This Quest app is the Retro Rewind one, and this installation has no Retro Rewind " +
|
||||
"translation. Install or repair Retro Rewind, then build for Quest again.");
|
||||
}
|
||||
|
||||
var toolchain = await EnsureToolchainAsync(questRoot, cancellationToken);
|
||||
|
||||
@@ -116,7 +124,7 @@ internal sealed class QuestBuildService
|
||||
TryDelete(temporaryOutput);
|
||||
throw;
|
||||
}
|
||||
return new Result(outputPath, kitFingerprint, includeGameFiles, new FileInfo(outputPath).Length);
|
||||
return new Result(outputPath, kit.Fingerprint, includeGameFiles, new FileInfo(outputPath).Length);
|
||||
}
|
||||
|
||||
private void TryDelete(string path)
|
||||
@@ -147,13 +155,24 @@ internal sealed class QuestBuildService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether emit-build-shards recorded a Retro Rewind translation in this workspace.</summary>
|
||||
private static bool HasRetroRewindShards(string generated)
|
||||
{
|
||||
var shards = Path.Combine(generated, "build_shards", "shards.cmake");
|
||||
return File.Exists(shards) &&
|
||||
File.ReadAllText(shards).Contains("set(MKW_HAVE_RETRO_REWIND_SHARDS ON)", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsGameData(string directory) =>
|
||||
Directory.Exists(Path.Combine(directory, "files")) &&
|
||||
File.Exists(Path.Combine(directory, "sys", "fst.bin")) &&
|
||||
File.Exists(Path.Combine(directory, "sys", "main.dol"));
|
||||
|
||||
/// <summary>Extracts <c>assets/game_kit</c> from the Quest app's APK and returns its fingerprint.</summary>
|
||||
internal static string ExtractKit(string apkPath, string destination, CancellationToken cancellationToken)
|
||||
/// <summary>What the extracted kit says it builds: its fingerprint and product ("base" or "retro_rewind").</summary>
|
||||
internal sealed record Kit(string Fingerprint, string Product);
|
||||
|
||||
/// <summary>Extracts <c>assets/game_kit</c> from the Quest app's APK and reads what it is for.</summary>
|
||||
internal static Kit ExtractKit(string apkPath, string destination, CancellationToken cancellationToken)
|
||||
{
|
||||
const string prefix = "assets/game_kit/";
|
||||
if (Directory.Exists(destination)) Directory.Delete(destination, recursive: true);
|
||||
@@ -179,10 +198,16 @@ internal sealed class QuestBuildService
|
||||
throw new InvalidDataException(
|
||||
"This APK is not a WiiCompiled Quest app with a game kit. Choose the Quest app installed on the headset.");
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(kitJson));
|
||||
return document.RootElement.TryGetProperty("fingerprint", out var fingerprint) &&
|
||||
fingerprint.ValueKind == JsonValueKind.String
|
||||
? fingerprint.GetString()!
|
||||
: throw new InvalidDataException("The Quest app's game kit has no fingerprint.");
|
||||
if (!document.RootElement.TryGetProperty("fingerprint", out var fingerprint) ||
|
||||
fingerprint.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new InvalidDataException("The Quest app's game kit has no fingerprint.");
|
||||
}
|
||||
var product = document.RootElement.TryGetProperty("product", out var value) &&
|
||||
value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()!
|
||||
: "base";
|
||||
return new Kit(fingerprint.GetString()!, product);
|
||||
}
|
||||
|
||||
private async Task<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
|
||||
|
||||
@@ -369,14 +369,15 @@ internal static class SelfTests
|
||||
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
|
||||
writer.Write(text);
|
||||
}
|
||||
Add("assets/game_kit/kit.json", "{\"schema\":1,\"fingerprint\":\"abc123\"}");
|
||||
Add("assets/game_kit/kit.json", "{\"schema\":2,\"product\":\"retro_rewind\",\"fingerprint\":\"abc123\"}");
|
||||
Add("assets/game_kit/include/memory.h", "#pragma once");
|
||||
Add("assets/runtime_resources/dsp_coef.bin", "not the kit");
|
||||
Add("lib/arm64-v8a/libSDL3.so", "not the kit");
|
||||
}
|
||||
var kit = Path.Combine(temp, "kit");
|
||||
if (QuestBuildService.ExtractKit(apk, kit, CancellationToken.None) != "abc123")
|
||||
throw new Exception("The kit fingerprint was not read.");
|
||||
var extracted = QuestBuildService.ExtractKit(apk, kit, CancellationToken.None);
|
||||
if (extracted.Fingerprint != "abc123" || extracted.Product != "retro_rewind")
|
||||
throw new Exception("The kit fingerprint or product was not read.");
|
||||
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
|
||||
Directory.GetFiles(kit, "*", SearchOption.AllDirectories).Length != 2)
|
||||
throw new Exception("The kit extraction took the wrong files.");
|
||||
|
||||
+18
-14
@@ -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 +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()
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+33
-8
@@ -234,11 +234,23 @@ The kit is exported from CMake's own build graph, so its flags cannot drift from
|
||||
this way had the same 61,975 defined and 785 undefined dynamic symbols, 29,995 translated
|
||||
functions, `NEEDED` list and soname as the CMake-built one, in 2.4 minutes.
|
||||
|
||||
`Build-Quest.ps1` refuses a base APK that contains any `libmain*.so` or lacks the kit, and the
|
||||
base variant excludes `**/libmain*.so` from packaging, since AGP packages every library left in
|
||||
the CMake output directory. The `func_8…` symbols the kit's runtime objects define are
|
||||
`Build-Quest.ps1` refuses an APK that contains any `libmain*.so` or lacks the kit, and every
|
||||
variant excludes `**/libmain*.so` and the probes from packaging, since AGP packages every library
|
||||
left in the CMake output directory. The `func_8…` symbols the kit's runtime objects define are
|
||||
hand-written HLE overrides (`PPC_NATIVE_OVERRIDE_*` in `hle_stubs.h`), not translated code.
|
||||
The Retro Rewind flavour has not moved to the kit yet and still bundles its library.
|
||||
|
||||
**Retro Rewind has its own kit** (`mkw_quest_kit_probe_retro`, RetroRewind without any translated
|
||||
code), so that APK ships no game either. It needs one more link slot than the base flavour, because
|
||||
the modded product links more kinds of translated code: `{game:runtime}` (the disc-generated
|
||||
sources), `{game:product}` (the mod's registration and dispatch shards), `{game:mod}` (the mod's own
|
||||
shards, the profile-sensitive base shards it replaces, and the mod's data patches) and
|
||||
`{game:translated}` (the shared base shards, with the archive semantics `libmkw_base_shared.a` has).
|
||||
Rather than teach each builder which flavour it is building, `kit.json` now carries a `sources`
|
||||
map naming the `shards.cmake` list behind each slot, and the builders just follow it (kit schema 2).
|
||||
Its game is built on a PC for now: building it on the headset would also need the mod's `Code.pul`
|
||||
and its 2 GB pack there, so only the base APK carries the on-device toolchain
|
||||
(`BuildConfig.ON_DEVICE_BUILD`). The Retro Rewind app writes `[paths] retro_rewind_root` into its
|
||||
first `Config.toml` and Home says so when that pack is missing.
|
||||
|
||||
### Game packages (.wcgame) and Import from computer
|
||||
|
||||
@@ -365,7 +377,8 @@ Android facts this design rests on, all measured on a Quest 3:
|
||||
another host.
|
||||
- `android/`: the Gradle project. `app/src/main/cpp/CMakeLists.txt` adds the
|
||||
repository's `runtime/` as a subdirectory with those Android choices;
|
||||
flavours `base` and `retroRewind` pick the product target and library name.
|
||||
flavours `base` and `retroRewind` each build their own game kit probe, and only `base` carries
|
||||
the on-headset toolchain.
|
||||
- `android/nod-jni`: Gradle's `buildNodJni` task runs `cargo build --release
|
||||
--locked --target aarch64-linux-android` with the NDK's clang as linker and C
|
||||
compiler. `stageNodJni` puts `libnod_jni.so` into the APK's `arm64-v8a`
|
||||
@@ -384,7 +397,8 @@ installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline)
|
||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # SDL3 3.4.4 AAR into android/app/libs
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Flavor retroRewind # Retro Rewind (needs translate-mod output)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Flavor retroRewind # Retro Rewind app and its kit (needs translate-mod output)
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Flavor retroRewind -Data <dir> -Install # its game, with the disc files
|
||||
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
||||
```
|
||||
|
||||
@@ -446,6 +460,14 @@ What has been verified on the development machine (September 2026):
|
||||
byte-identical to one the same toolchain built from a shell, and against the PC-built library it
|
||||
has the same soname, `NEEDED` list, 785 undefined symbols and 29,995 translated functions
|
||||
(61,974 defined against 61,975: the PC's older clang keeps one inline helper out of line).
|
||||
- **Device, 2026-09-17: Retro Rewind from its own kit.** The Retro Rewind APK carries a kit and no
|
||||
game (66 MB). Its game built from that kit on the PC in 2.9 minutes (166 generated sources:
|
||||
72 base shards, 24 profile-sensitive, 48 mod shards, the mod's data patches, 17 registration
|
||||
shards and the 3 disc-generated sources), linked to a 157.6 MB `libmain.so` with the same import
|
||||
list as the base one, 67,872 defined symbols against the base library's 61,975, and 5,905 symbols
|
||||
the base library does not have. Packaged with the disc files (2.55 GB), imported on the Quest 3 in
|
||||
under two minutes, and the mod runs: its own title screen, "Press the A Button", and the licence
|
||||
menu, with the pack read from `retro_rewind_root`.
|
||||
|
||||
Bring-up fixes that only a device could reveal:
|
||||
|
||||
@@ -556,7 +578,10 @@ or `EndAccess` errors); a black mirror too points at Aurora itself.
|
||||
- **Input.** D-pad (trick inputs) is not bound; remap in `Config.toml` or bind
|
||||
the thumbstick directions in a follow-up. Haptics are wired but nothing calls
|
||||
them yet.
|
||||
- **Retro Rewind on device** needs the mod's translation and its extracted
|
||||
content pushed next to `DATA`, exactly like the desktop product.
|
||||
- **Retro Rewind on the headset** runs from a kit-built library (below), but its game must be built
|
||||
on a PC and its pack copied next to `DATA` by hand. `adb push` cannot create directories inside
|
||||
an app's external files directory (`secure_mkdirs failed`), so push the pack to `Download` and
|
||||
copy it over on the device, then `chmod -R a+rX` it. The launcher does not fetch or update the
|
||||
pack, and cannot build the mod on the headset.
|
||||
- **Release signing and store packaging** are out of scope; `Build-Quest.ps1`
|
||||
produces debug-signed APKs for sideloading.
|
||||
@@ -415,6 +415,14 @@ if(MKW_HAVE_RETRO_REWIND)
|
||||
if(MKW_RETRO_BLOB_OBJECTS)
|
||||
target_sources(RetroRewind PRIVATE ${MKW_RETRO_BLOB_OBJECTS})
|
||||
endif()
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
# Retro Rewind's game kit, the same idea as the base probe above: RetroRewind without any
|
||||
# translated code, so the APK carries neither the game nor the mod.
|
||||
add_library(mkw_quest_kit_probe_retro SHARED "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
|
||||
mkw_configure_product(mkw_quest_kit_probe_retro WITHOUT_GAME)
|
||||
target_precompile_headers(mkw_quest_kit_probe_retro REUSE_FROM WiiCompiled)
|
||||
target_link_options(mkw_quest_kit_probe_retro PRIVATE "-Wl,--unresolved-symbols=ignore-all")
|
||||
endif()
|
||||
add_custom_target(mkw_release DEPENDS WiiCompiled RetroRewind)
|
||||
else()
|
||||
add_custom_target(mkw_release DEPENDS WiiCompiled)
|
||||
@@ -440,7 +448,7 @@ endif()
|
||||
|
||||
set(MKW_ALL_BUILD_TARGETS
|
||||
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind mkw_quest_kit_probe)
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind mkw_quest_kit_probe mkw_quest_kit_probe_retro)
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
|
||||
Reference in new issue
Block a user