mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 03:00:14 +02:00
Added Support for both Base and Retro Rewind in the launher
This commit is contained in:
1 parent
58c0890983
commit
321db74a0f
33 files changed
+681
-325
No files matched your search
+5
-1
@@ -25,7 +25,11 @@ dotnet test translator/Translator.sln -c Release
|
|||||||
|
|
||||||
`--version` still prints a plain version. `--info-json` additionally reports `productId` as
|
`--version` still prints a plain version. `--info-json` additionally reports `productId` as
|
||||||
`wiicompiled-openxr-vr`, `version`, `openxrD3D12: true`, and `questBuild: true` when the setup
|
`wiicompiled-openxr-vr`, `version`, `openxrD3D12: true`, and `questBuild: true` when the setup
|
||||||
supports `--build-quest` (see `docs/quest-port.md`). VR refuses installation, repair,
|
supports `--build-quest` (see `docs/quest-port.md`). From 0.2.39 every release declares it, and
|
||||||
|
`Verify-Release.ps1` refuses one whose payload lacks the `BuildWorkspace/android` scripts that
|
||||||
|
build runs, so a release that claims Quest support can always deliver it. WheelWizard offers
|
||||||
|
Build for Quest only once the installed setup reports `questBuild`, and asks the player to update
|
||||||
|
otherwise. VR refuses installation, repair,
|
||||||
launch, or uninstall of a normal or unidentified installation. Nonportable VR installations use
|
launch, or uninstall of a normal or unidentified installation. Nonportable VR installations use
|
||||||
`WiiCompiledOpenXRVR` application data and a distinct uninstall registration and shortcut.
|
`WiiCompiledOpenXRVR` application data and a distinct uninstall registration and shortcut.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>0.2.38</Version>
|
<Version>0.2.39</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -9,6 +9,9 @@ if ($SetupPath) {
|
|||||||
$info = (& $SetupPath --info-json | Out-String) | ConvertFrom-Json
|
$info = (& $SetupPath --info-json | Out-String) | ConvertFrom-Json
|
||||||
if ($LASTEXITCODE -ne 0 -or $info.productId -cne 'wiicompiled-openxr-vr' -or
|
if ($LASTEXITCODE -ne 0 -or $info.productId -cne 'wiicompiled-openxr-vr' -or
|
||||||
$info.version -cne $version -or $info.openxrD3D12 -ne $true) { throw 'Setup VR identity mismatch.' }
|
$info.version -cne $version -or $info.openxrD3D12 -ne $true) { throw 'Setup VR identity mismatch.' }
|
||||||
|
# WheelWizard offers Build for Quest only to a setup that declares it, so a release that says
|
||||||
|
# so must also carry what the build runs.
|
||||||
|
if ($info.questBuild -ne $true) { throw 'Setup does not declare Quest build support.' }
|
||||||
# Inspect the appended payload as well: a correctly versioned host can still wrap stale tools.
|
# Inspect the appended payload as well: a correctly versioned host can still wrap stale tools.
|
||||||
Add-Type -AssemblyName System.IO.Compression
|
Add-Type -AssemblyName System.IO.Compression
|
||||||
$stream = [IO.File]::OpenRead((Resolve-Path -LiteralPath $SetupPath))
|
$stream = [IO.File]::OpenRead((Resolve-Path -LiteralPath $SetupPath))
|
||||||
@@ -43,6 +46,12 @@ if ($SetupPath) {
|
|||||||
if ($manifest.ProductId -cne $info.productId -or $manifest.ProductVersion -cne $version) {
|
if ($manifest.ProductId -cne $info.productId -or $manifest.ProductVersion -cne $version) {
|
||||||
throw 'Payload identity/version does not match the executable.'
|
throw 'Payload identity/version does not match the executable.'
|
||||||
}
|
}
|
||||||
|
# The scripts --build-quest runs live in the installed workspace, not the toolkit.
|
||||||
|
foreach ($script in 'QuestGameKit.psm1', 'Build-QuestGame.ps1') {
|
||||||
|
if (-not $zip.GetEntry("BuildWorkspace/android/$script")) {
|
||||||
|
throw "The payload has no BuildWorkspace/android/$script, so --build-quest cannot run."
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally { $zip.Dispose() }
|
} finally { $zip.Dispose() }
|
||||||
} finally { $payload.Dispose(); $stream.Dispose() }
|
} finally { $payload.Dispose(); $stream.Dispose() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ internal sealed class CommandLine
|
|||||||
public string? QuestApkPath { get; private set; }
|
public string? QuestApkPath { get; private set; }
|
||||||
public string? OutputPath { get; private set; }
|
public string? OutputPath { get; private set; }
|
||||||
public bool IncludeGameFiles { get; private set; }
|
public bool IncludeGameFiles { get; private set; }
|
||||||
|
/// <summary>Which game the Quest app is to build: "base" (the default) or "retro_rewind".</summary>
|
||||||
|
public string QuestProduct { get; private set; } = "base";
|
||||||
|
public bool IncludeModContent { get; private set; }
|
||||||
|
|
||||||
public static bool WantsProgressJson(string[] args) =>
|
public static bool WantsProgressJson(string[] args) =>
|
||||||
args.Any(argument => argument.Equals("--progress-json", StringComparison.OrdinalIgnoreCase));
|
args.Any(argument => argument.Equals("--progress-json", StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -97,6 +100,8 @@ internal sealed class CommandLine
|
|||||||
case "--quest-apk": result.QuestApkPath = RequireValue(args, ref i); break;
|
case "--quest-apk": result.QuestApkPath = RequireValue(args, ref i); break;
|
||||||
case "--output": result.OutputPath = RequireValue(args, ref i); break;
|
case "--output": result.OutputPath = RequireValue(args, ref i); break;
|
||||||
case "--include-game-files": result.IncludeGameFiles = true; break;
|
case "--include-game-files": result.IncludeGameFiles = true; break;
|
||||||
|
case "--quest-product": result.QuestProduct = RequireValue(args, ref i); break;
|
||||||
|
case "--include-mod-content": result.IncludeModContent = true; break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException($"Unknown CLI option: {args[i]}");
|
throw new ArgumentException($"Unknown CLI option: {args[i]}");
|
||||||
}
|
}
|
||||||
@@ -149,7 +154,7 @@ internal sealed class CommandLine
|
|||||||
[AppMode.BuildQuest] = new ModeRules
|
[AppMode.BuildQuest] = new ModeRules
|
||||||
{
|
{
|
||||||
Flag = "--build-quest", AcceptsProgressJson = true, RequiresInstallDirectory = true,
|
Flag = "--build-quest", AcceptsProgressJson = true, RequiresInstallDirectory = true,
|
||||||
AcceptsQuestBuild = true
|
AcceptsQuestBuild = true, AcceptsRetroDirectory = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,6 +175,13 @@ internal sealed class CommandLine
|
|||||||
Reject(QuestApkPath is not null && !rules.AcceptsQuestBuild, "--quest-apk");
|
Reject(QuestApkPath is not null && !rules.AcceptsQuestBuild, "--quest-apk");
|
||||||
Reject(OutputPath is not null && !rules.AcceptsQuestBuild, "--output");
|
Reject(OutputPath is not null && !rules.AcceptsQuestBuild, "--output");
|
||||||
Reject(IncludeGameFiles && !rules.AcceptsQuestBuild, "--include-game-files");
|
Reject(IncludeGameFiles && !rules.AcceptsQuestBuild, "--include-game-files");
|
||||||
|
Reject(IncludeModContent && !rules.AcceptsQuestBuild, "--include-mod-content");
|
||||||
|
if (rules.AcceptsQuestBuild && QuestProduct is not ("base" or "retro_rewind"))
|
||||||
|
throw new ArgumentException("--quest-product must be base or retro_rewind.");
|
||||||
|
if (IncludeModContent && QuestProduct != "retro_rewind")
|
||||||
|
throw new ArgumentException("--include-mod-content needs --quest-product retro_rewind.");
|
||||||
|
if (IncludeModContent && string.IsNullOrWhiteSpace(RetroDirectoryPath))
|
||||||
|
throw new ArgumentException("--include-mod-content needs --retro-dir.");
|
||||||
if (rules.AcceptsQuestBuild && (string.IsNullOrWhiteSpace(QuestApkPath) || string.IsNullOrWhiteSpace(OutputPath)))
|
if (rules.AcceptsQuestBuild && (string.IsNullOrWhiteSpace(QuestApkPath) || string.IsNullOrWhiteSpace(OutputPath)))
|
||||||
throw new ArgumentException($"--quest-apk and --output are required with {rules.Flag}.");
|
throw new ArgumentException($"--quest-apk and --output are required with {rules.Flag}.");
|
||||||
Reject(RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable && !rules.AcceptsPayloadMode,
|
Reject(RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable && !rules.AcceptsPayloadMode,
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ internal static class ConsoleCommands
|
|||||||
Console.Out.WriteLine(" --repair-products --install-dir <dir> --retro-dir <folder> " +
|
Console.Out.WriteLine(" --repair-products --install-dir <dir> --retro-dir <folder> " +
|
||||||
"(--download-retro-wfc-payload | --skip-retro-wfc-payload) [--progress-json]");
|
"(--download-retro-wfc-payload | --skip-retro-wfc-payload) [--progress-json]");
|
||||||
Console.Out.WriteLine(" --build-quest --install-dir <dir> --quest-apk <apk> --output <file.wcgame> " +
|
Console.Out.WriteLine(" --build-quest --install-dir <dir> --quest-apk <apk> --output <file.wcgame> " +
|
||||||
"[--include-game-files] [--progress-json]");
|
"[--quest-product base|retro_rewind] [--include-game-files] " +
|
||||||
|
"[--retro-dir <folder> --include-mod-content] [--progress-json]");
|
||||||
Console.Out.WriteLine(" --launch-retro | --launch-base");
|
Console.Out.WriteLine(" --launch-retro | --launch-base");
|
||||||
Console.Out.WriteLine(" --uninstall --install-dir <dir>");
|
Console.Out.WriteLine(" --uninstall --install-dir <dir>");
|
||||||
Console.Out.WriteLine(" --version");
|
Console.Out.WriteLine(" --version");
|
||||||
@@ -293,7 +294,8 @@ internal static class ConsoleCommands
|
|||||||
using var operationLock = InstallOperationLock.Acquire(installation.Root, reporter);
|
using var operationLock = InstallOperationLock.Acquire(installation.Root, reporter);
|
||||||
var result = await new QuestBuildService(reporter).BuildAsync(installation,
|
var result = await new QuestBuildService(reporter).BuildAsync(installation,
|
||||||
Path.GetFullPath(command.QuestApkPath!), Path.GetFullPath(command.OutputPath!),
|
Path.GetFullPath(command.QuestApkPath!), Path.GetFullPath(command.OutputPath!),
|
||||||
command.IncludeGameFiles, cancellationToken);
|
command.IncludeGameFiles, cancellationToken, command.QuestProduct,
|
||||||
|
command.IncludeModContent ? Path.GetFullPath(command.RetroDirectoryPath!) : null);
|
||||||
if (ndjson is null)
|
if (ndjson is null)
|
||||||
Console.Out.WriteLine($"Quest game package: {result.PackagePath} ({result.SizeBytes / 1_000_000} MB)");
|
Console.Out.WriteLine($"Quest game package: {result.PackagePath} ({result.SizeBytes / 1_000_000} MB)");
|
||||||
ndjson?.QuestPackage(result);
|
ndjson?.QuestPackage(result);
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ internal sealed class QuestBuildService
|
|||||||
internal sealed record Toolchain(string ClangCxx, string ClangC, string Sysroot);
|
internal sealed record Toolchain(string ClangCxx, string ClangC, string Sysroot);
|
||||||
|
|
||||||
public async Task<Result> BuildAsync(Installation installation, string apkPath, string outputPath,
|
public async Task<Result> BuildAsync(Installation installation, string apkPath, string outputPath,
|
||||||
bool includeGameFiles, CancellationToken cancellationToken)
|
bool includeGameFiles, CancellationToken cancellationToken, string product = "base",
|
||||||
|
string? modContentDirectory = null)
|
||||||
{
|
{
|
||||||
_reporter.Progress(InstallStages.Validate, "Checking the installation and the Quest app...", 1);
|
_reporter.Progress(InstallStages.Validate, "Checking the installation and the Quest app...", 1);
|
||||||
var workspace = installation.WorkspaceDirectory;
|
var workspace = installation.WorkspaceDirectory;
|
||||||
@@ -73,14 +74,24 @@ internal sealed class QuestBuildService
|
|||||||
var kitDirectory = Path.Combine(questRoot, "kit");
|
var kitDirectory = Path.Combine(questRoot, "kit");
|
||||||
_reporter.Progress(InstallStages.QuestKit, "Reading the game kit from the Quest app...", 2);
|
_reporter.Progress(InstallStages.QuestKit, "Reading the game kit from the Quest app...", 2);
|
||||||
var kit = ExtractKit(apkPath, kitDirectory, cancellationToken);
|
var kit = ExtractKit(apkPath, kitDirectory, cancellationToken);
|
||||||
_reporter.Diagnostic($"Quest game kit {kit.Fingerprint} ({kit.Product})");
|
_reporter.Diagnostic($"Quest game kit {kit.Fingerprint} [{string.Join(", ", kit.Products)}]");
|
||||||
// A Retro Rewind app needs this installation's Retro Rewind translation, which only exists
|
if (!kit.Products.Contains(product))
|
||||||
// once the mod has been built here at least once.
|
|
||||||
if (kit.Product == "retro_rewind" && !HasRetroRewindShards(generated))
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"This Quest app is the Retro Rewind one, and this installation has no Retro Rewind " +
|
$"The Quest app cannot play {product}: its game kit carries {string.Join(", ", kit.Products)}.");
|
||||||
"translation. Install or repair Retro Rewind, then build for Quest again.");
|
}
|
||||||
|
// Retro Rewind needs this installation's own Retro Rewind translation, which only exists
|
||||||
|
// once the mod has been built here at least once.
|
||||||
|
if (product == "retro_rewind" && !HasRetroRewindShards(generated))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"This installation has no Retro Rewind translation. Install or repair Retro Rewind, " +
|
||||||
|
"then build for Quest again.");
|
||||||
|
}
|
||||||
|
if (modContentDirectory is not null && !File.Exists(Path.Combine(modContentDirectory, "Binaries", "Code.pul")))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The Retro Rewind content to put in the game file is missing its Binaries/Code.pul.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var toolchain = await EnsureToolchainAsync(questRoot, cancellationToken);
|
var toolchain = await EnsureToolchainAsync(questRoot, cancellationToken);
|
||||||
@@ -94,13 +105,18 @@ internal sealed class QuestBuildService
|
|||||||
"-Generated", generated, "-Kit", kitDirectory, "-Manifest", manifest,
|
"-Generated", generated, "-Kit", kitDirectory, "-Manifest", manifest,
|
||||||
"-BuildDir", Path.Combine(questRoot, "build"), "-Output", temporaryOutput,
|
"-BuildDir", Path.Combine(questRoot, "build"), "-Output", temporaryOutput,
|
||||||
"-ClangCxx", toolchain.ClangCxx, "-ClangC", toolchain.ClangC, "-Sysroot", toolchain.Sysroot,
|
"-ClangCxx", toolchain.ClangCxx, "-ClangC", toolchain.ClangC, "-Sysroot", toolchain.Sysroot,
|
||||||
"-Ninja", ninja, "-BuiltBy", $"WiiCompiled Setup {ProductInfo.Version}"
|
"-Ninja", ninja, "-BuiltBy", $"WiiCompiled Setup {ProductInfo.Version}", "-Product", product
|
||||||
};
|
};
|
||||||
if (includeGameFiles)
|
if (includeGameFiles)
|
||||||
{
|
{
|
||||||
arguments.Add("-Data");
|
arguments.Add("-Data");
|
||||||
arguments.Add(installation.GameDataDirectory);
|
arguments.Add(installation.GameDataDirectory);
|
||||||
}
|
}
|
||||||
|
if (modContentDirectory is not null)
|
||||||
|
{
|
||||||
|
arguments.Add("-Mod");
|
||||||
|
arguments.Add(modContentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
_reporter.Progress(InstallStages.QuestBuild, "Preparing the Quest build...", 40);
|
_reporter.Progress(InstallStages.QuestBuild, "Preparing the Quest build...", 40);
|
||||||
var powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),
|
var powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),
|
||||||
@@ -168,8 +184,8 @@ internal sealed class QuestBuildService
|
|||||||
File.Exists(Path.Combine(directory, "sys", "fst.bin")) &&
|
File.Exists(Path.Combine(directory, "sys", "fst.bin")) &&
|
||||||
File.Exists(Path.Combine(directory, "sys", "main.dol"));
|
File.Exists(Path.Combine(directory, "sys", "main.dol"));
|
||||||
|
|
||||||
/// <summary>What the extracted kit says it builds: its fingerprint and product ("base" or "retro_rewind").</summary>
|
/// <summary>What the extracted kit says it builds: its fingerprint and the products it carries.</summary>
|
||||||
internal sealed record Kit(string Fingerprint, string Product);
|
internal sealed record Kit(string Fingerprint, IReadOnlyCollection<string> Products);
|
||||||
|
|
||||||
/// <summary>Extracts <c>assets/game_kit</c> from the Quest app's APK and reads what it is for.</summary>
|
/// <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)
|
internal static Kit ExtractKit(string apkPath, string destination, CancellationToken cancellationToken)
|
||||||
@@ -203,11 +219,11 @@ internal sealed class QuestBuildService
|
|||||||
{
|
{
|
||||||
throw new InvalidDataException("The Quest app's game kit has no fingerprint.");
|
throw new InvalidDataException("The Quest app's game kit has no fingerprint.");
|
||||||
}
|
}
|
||||||
var product = document.RootElement.TryGetProperty("product", out var value) &&
|
var products = document.RootElement.TryGetProperty("products", out var value) &&
|
||||||
value.ValueKind == JsonValueKind.String
|
value.ValueKind == JsonValueKind.Object
|
||||||
? value.GetString()!
|
? value.EnumerateObject().Select(property => property.Name).ToArray()
|
||||||
: "base";
|
: throw new InvalidDataException("The Quest app's game kit lists no games it can build.");
|
||||||
return new Kit(fingerprint.GetString()!, product);
|
return new Kit(fingerprint.GetString()!, products);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
|
private async Task<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -302,9 +302,18 @@ internal static class SelfTests
|
|||||||
"--output", "D:\\MarioKartWii.wcgame", "--include-game-files", "--progress-json"
|
"--output", "D:\\MarioKartWii.wcgame", "--include-game-files", "--progress-json"
|
||||||
]);
|
]);
|
||||||
if (build.Mode != AppMode.BuildQuest || build.QuestApkPath != "quest.apk" ||
|
if (build.Mode != AppMode.BuildQuest || build.QuestApkPath != "quest.apk" ||
|
||||||
build.OutputPath != "D:\\MarioKartWii.wcgame" || !build.IncludeGameFiles || !build.ProgressJson)
|
build.OutputPath != "D:\\MarioKartWii.wcgame" || !build.IncludeGameFiles || !build.ProgressJson ||
|
||||||
|
build.QuestProduct != "base" || build.IncludeModContent)
|
||||||
throw new Exception("The Quest build contract did not parse.");
|
throw new Exception("The Quest build contract did not parse.");
|
||||||
|
|
||||||
|
var retro = CommandLine.Parse([
|
||||||
|
"--build-quest", "--install-dir", "C:\\Games\\MKW", "--quest-apk", "quest.apk",
|
||||||
|
"--output", "D:\\RetroRewind.wcgame", "--quest-product", "retro_rewind",
|
||||||
|
"--retro-dir", "D:\\RetroRewind6", "--include-mod-content"
|
||||||
|
]);
|
||||||
|
if (retro.QuestProduct != "retro_rewind" || !retro.IncludeModContent || retro.RetroDirectoryPath != "D:\\RetroRewind6")
|
||||||
|
throw new Exception("The Retro Rewind Quest build contract did not parse.");
|
||||||
|
|
||||||
void Rejects(string description, params string[] args)
|
void Rejects(string description, params string[] args)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -319,6 +328,12 @@ internal static class SelfTests
|
|||||||
Rejects("A Quest build without an installation", "--build-quest", "--quest-apk", "a.apk", "--output", "o");
|
Rejects("A Quest build without an installation", "--build-quest", "--quest-apk", "a.apk", "--output", "o");
|
||||||
Rejects("--quest-apk outside a Quest build", "--launch-base", "--quest-apk", "a.apk");
|
Rejects("--quest-apk outside a Quest build", "--launch-base", "--quest-apk", "a.apk");
|
||||||
Rejects("--include-game-files outside a Quest build", "--check-products", "--include-game-files");
|
Rejects("--include-game-files outside a Quest build", "--check-products", "--include-game-files");
|
||||||
|
Rejects("An unknown Quest product", "--build-quest", "--install-dir", "i", "--quest-apk", "a.apk",
|
||||||
|
"--output", "o.wcgame", "--quest-product", "mario-kart-8");
|
||||||
|
Rejects("The mod content of a base game", "--build-quest", "--install-dir", "i", "--quest-apk", "a.apk",
|
||||||
|
"--output", "o.wcgame", "--retro-dir", "r", "--include-mod-content");
|
||||||
|
Rejects("Mod content without its source", "--build-quest", "--install-dir", "i", "--quest-apk", "a.apk",
|
||||||
|
"--output", "o.wcgame", "--quest-product", "retro_rewind", "--include-mod-content");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TestQuestToolchainSelection()
|
private static void TestQuestToolchainSelection()
|
||||||
@@ -369,15 +384,16 @@ internal static class SelfTests
|
|||||||
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
|
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
|
||||||
writer.Write(text);
|
writer.Write(text);
|
||||||
}
|
}
|
||||||
Add("assets/game_kit/kit.json", "{\"schema\":2,\"product\":\"retro_rewind\",\"fingerprint\":\"abc123\"}");
|
Add("assets/game_kit/kit.json", "{\"schema\":3,\"fingerprint\":\"abc123\",\"products\":{\"base\":{},\"retro_rewind\":{}}}");
|
||||||
Add("assets/game_kit/include/memory.h", "#pragma once");
|
Add("assets/game_kit/include/memory.h", "#pragma once");
|
||||||
Add("assets/runtime_resources/dsp_coef.bin", "not the kit");
|
Add("assets/runtime_resources/dsp_coef.bin", "not the kit");
|
||||||
Add("lib/arm64-v8a/libSDL3.so", "not the kit");
|
Add("lib/arm64-v8a/libSDL3.so", "not the kit");
|
||||||
}
|
}
|
||||||
var kit = Path.Combine(temp, "kit");
|
var kit = Path.Combine(temp, "kit");
|
||||||
var extracted = QuestBuildService.ExtractKit(apk, kit, CancellationToken.None);
|
var extracted = QuestBuildService.ExtractKit(apk, kit, CancellationToken.None);
|
||||||
if (extracted.Fingerprint != "abc123" || extracted.Product != "retro_rewind")
|
if (extracted.Fingerprint != "abc123" || !extracted.Products.Contains("base") ||
|
||||||
throw new Exception("The kit fingerprint or product was not read.");
|
!extracted.Products.Contains("retro_rewind"))
|
||||||
|
throw new Exception("The kit fingerprint or its games were not read.");
|
||||||
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
|
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
|
||||||
Directory.GetFiles(kit, "*", SearchOption.AllDirectories).Length != 2)
|
Directory.GetFiles(kit, "*", SearchOption.AllDirectories).Length != 2)
|
||||||
throw new Exception("The kit extraction took the wrong files.");
|
throw new Exception("The kit extraction took the wrong files.");
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
64fdd492f6ce71fd223b49f493e0809d9a7f9ed45ebaa3605f626eb77282dbe4 WiiCompiled-Setup.exe
|
|
||||||
b319b86d46bea6ebfa52965c65706976d6004abc01914ff82a2b73c71de84def WheelWizardVRWindows.exe
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
946bb530c9deea52f397689d637a4e0c10070d9c1980d7d02c4acc78f8fb7e5d WheelWizardVRWindows.exe
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
46ab8a1d1c6d8af551cdd45808397a0eb4fbbc96bb97e25d7a5a70d6ccde01bf WheelWizardVRWindows.exe
|
|
||||||
+10
-13
@@ -1,8 +1,11 @@
|
|||||||
# Builds the standalone Meta Quest APK on a Windows host.
|
# Builds the standalone Meta Quest APK on a Windows host.
|
||||||
#
|
#
|
||||||
# powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated <dir>] [-Flavor base|retroRewind]
|
# powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated <dir>]
|
||||||
# [-Configuration debug|release] [-Install]
|
# [-Configuration debug|release] [-Install]
|
||||||
#
|
#
|
||||||
|
# One app offers both games: the APK carries a kit for the base game, and for Retro Rewind too
|
||||||
|
# when -Generated holds a translation that includes the mod.
|
||||||
|
#
|
||||||
# -Generated names the translator output for the disc you own: the directory
|
# -Generated names the translator output for the disc you own: the directory
|
||||||
# holding data_sections_init.cpp, RuntimeConfig.h and build_shards/shards.cmake.
|
# holding data_sections_init.cpp, RuntimeConfig.h and build_shards/shards.cmake.
|
||||||
# It defaults to the installer's build workspace next to this checkout. The
|
# It defaults to the installer's build workspace next to this checkout. The
|
||||||
@@ -17,7 +20,6 @@ param(
|
|||||||
[string]$Generated = '',
|
[string]$Generated = '',
|
||||||
[string]$Dependencies = '',
|
[string]$Dependencies = '',
|
||||||
[string]$CMakeDir = '',
|
[string]$CMakeDir = '',
|
||||||
[ValidateSet('base', 'retroRewind')] [string]$Flavor = 'base',
|
|
||||||
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
||||||
[switch]$Install
|
[switch]$Install
|
||||||
)
|
)
|
||||||
@@ -71,7 +73,7 @@ if (-not $Dependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$variant = (Get-Culture).TextInfo.ToTitleCase($Configuration)
|
$variant = (Get-Culture).TextInfo.ToTitleCase($Configuration)
|
||||||
$task = "app:assemble$((Get-Culture).TextInfo.ToTitleCase($Flavor))$variant"
|
$task = "app:assemble$variant"
|
||||||
$gradleArgs = @(
|
$gradleArgs = @(
|
||||||
'--project-dir', $root,
|
'--project-dir', $root,
|
||||||
"-PmkwGeneratedDir=$Generated"
|
"-PmkwGeneratedDir=$Generated"
|
||||||
@@ -82,13 +84,13 @@ Write-Host "gradlew $($gradleArgs -join ' ')"
|
|||||||
& (Join-Path $root 'gradlew.bat') @gradleArgs
|
& (Join-Path $root 'gradlew.bat') @gradleArgs
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Gradle failed ($LASTEXITCODE)" }
|
if ($LASTEXITCODE -ne 0) { throw "Gradle failed ($LASTEXITCODE)" }
|
||||||
|
|
||||||
$apkDir = Join-Path $root "app\build\outputs\apk\$Flavor\$Configuration"
|
$apkDir = Join-Path $root "app\build\outputs\apk\$Configuration"
|
||||||
$apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1
|
$apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1
|
||||||
if (-not $apk) { throw "No APK under $apkDir" }
|
if (-not $apk) { throw "No APK under $apkDir" }
|
||||||
Write-Host "APK: $($apk.FullName)"
|
Write-Host "APK: $($apk.FullName)"
|
||||||
|
|
||||||
# Every APK must be distributable: no translated game inside, only the game kit. The toolchain
|
# The APK must be distributable: no translated game inside, only the game kit and the toolchain
|
||||||
# that builds one on the headset travels in the base APK only.
|
# that builds one on the headset.
|
||||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
|
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
|
||||||
try {
|
try {
|
||||||
@@ -98,16 +100,11 @@ try {
|
|||||||
} finally { $archive.Dispose() }
|
} finally { $archive.Dispose() }
|
||||||
if ($gameLibraries.Count -gt 0) { throw "The APK contains a translated game library: $($gameLibraries.FullName -join ', ')" }
|
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 $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 (-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) {
|
if ($Install) {
|
||||||
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
||||||
& $adb install -r $apk.FullName
|
& $adb install -r $apk.FullName
|
||||||
if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" }
|
if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" }
|
||||||
if ($Flavor -eq 'base') {
|
Write-Host 'Installed. Build a game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.'
|
||||||
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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+14
-15
@@ -13,9 +13,10 @@
|
|||||||
# downloaded, the toolkit's ninja and the workspace's recomp.yml.
|
# 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
|
# -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 of -Flavor (base or
|
# kit the Quest app carries; it defaults to the one the last APK build exported. -Product picks the
|
||||||
# retroRewind, whose translation must include the mod) exported. -Data adds the
|
# game: base, or retro_rewind (whose translation must include the mod). -Data adds the extracted
|
||||||
# extracted disc (the directory holding sys/ and files/) so the headset needs nothing else.
|
# disc (the directory holding sys/ and files/) and -Mod the RetroRewind6 pack, so the headset needs
|
||||||
|
# nothing else.
|
||||||
# -Install pushes the package into the app's Import folder over adb, where the launcher picks it
|
# -Install pushes the package into the app's Import folder over adb, where the launcher picks it
|
||||||
# up the next time it opens.
|
# up the next time it opens.
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
@@ -31,7 +32,8 @@ param(
|
|||||||
[string]$Sysroot = '',
|
[string]$Sysroot = '',
|
||||||
[string]$Ninja = '',
|
[string]$Ninja = '',
|
||||||
[string]$BuiltBy = 'android/Build-QuestGame.ps1',
|
[string]$BuiltBy = 'android/Build-QuestGame.ps1',
|
||||||
[ValidateSet('base', 'retroRewind')] [string]$Flavor = 'base',
|
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base',
|
||||||
|
[string]$Mod = '',
|
||||||
[int]$TranslatedJobs = 0,
|
[int]$TranslatedJobs = 0,
|
||||||
[switch]$Install
|
[switch]$Install
|
||||||
)
|
)
|
||||||
@@ -42,17 +44,15 @@ $root = $PSScriptRoot
|
|||||||
$workspace = (Resolve-Path (Join-Path $root '..')).Path
|
$workspace = (Resolve-Path (Join-Path $root '..')).Path
|
||||||
if (-not $Generated) { $Generated = Join-Path $workspace '.scratch\vr-build-workspace\BuildWorkspace\generated' }
|
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 $Manifest) { $Manifest = Join-Path $workspace 'projects\mkwii\recomp.yml' }
|
||||||
if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$Flavor" }
|
if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$Product" }
|
||||||
if (-not $Kit) {
|
if (-not $Kit) {
|
||||||
# The kit this flavour's last APK build packaged (its export<Flavor>*QuestGameKit task), so the
|
# The kit the last APK build packaged, so the game matches the app it will be imported into.
|
||||||
# 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 |
|
$Kit = Get-ChildItem -Path (Join-Path $root 'app\build\generated') -Filter kit.json -Recurse -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $_.Directory.Name -eq 'game_kit' -and $_.FullName -like "*$exportTask*" } |
|
Where-Object { $_.Directory.Name -eq 'game_kit' } |
|
||||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
|
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
|
||||||
}
|
}
|
||||||
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) {
|
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"
|
throw 'No game kit; run Build-Quest.ps1 first, or pass -Kit'
|
||||||
}
|
}
|
||||||
if (-not (Test-Path (Join-Path $Generated 'build_shards\shards.cmake'))) { throw "No translated graph at $Generated" }
|
if (-not (Test-Path (Join-Path $Generated 'build_shards\shards.cmake'))) { throw "No translated graph at $Generated" }
|
||||||
|
|
||||||
@@ -85,19 +85,18 @@ if (-not $gameId -or -not $dolSha -or -not $relSha) { throw "Cannot read the dis
|
|||||||
|
|
||||||
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
|
$stopwatch = [Diagnostics.Stopwatch]::StartNew()
|
||||||
$library = Invoke-QuestGameBuild -KitDir $Kit -GeneratedDir $Generated -BuildDir $BuildDir `
|
$library = Invoke-QuestGameBuild -KitDir $Kit -GeneratedDir $Generated -BuildDir $BuildDir `
|
||||||
-ClangCxx $ClangCxx -ClangC $ClangC -Sysroot $Sysroot -Ninja $Ninja -TranslatedJobs $TranslatedJobs
|
-ClangCxx $ClangCxx -ClangC $ClangC -Sysroot $Sysroot -Ninja $Ninja -TranslatedJobs $TranslatedJobs -Product $Product
|
||||||
Write-Host ("Built {0} in {1:N1} min" -f $library, $stopwatch.Elapsed.TotalMinutes)
|
Write-Host ("Built {0} in {1:N1} min" -f $library, $stopwatch.Elapsed.TotalMinutes)
|
||||||
|
|
||||||
if (-not $Output) { $Output = Join-Path $BuildDir 'MarioKartWii.wcgame' }
|
if (-not $Output) { $Output = Join-Path $BuildDir 'MarioKartWii.wcgame' }
|
||||||
Write-Host 'MKWCBUILD:STEP:quest-package Packaging the game for Quest'
|
Write-Host 'MKWCBUILD:STEP:quest-package Packaging the game for Quest'
|
||||||
$game = New-QuestGamePackage -Library $library -DataDir $Data -KitDir $Kit -GameId $gameId -DolSha256 $dolSha -RelSha256 $relSha `
|
$game = New-QuestGamePackage -Library $library -DataDir $Data -ModDir $Mod -KitDir $Kit -GameId $gameId -DolSha256 $dolSha -RelSha256 $relSha `
|
||||||
-BuiltBy $BuiltBy -OutputPath $Output
|
-BuiltBy $BuiltBy -OutputPath $Output -Product $Product
|
||||||
Write-Host "Game package: $Output (kit $($game.kitFingerprint))"
|
Write-Host "Game package: $Output (kit $($game.kitFingerprint))"
|
||||||
|
|
||||||
if ($Install) {
|
if ($Install) {
|
||||||
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
||||||
$package = if ($Flavor -eq 'retroRewind') { 'org.wiicompiled.quest.retrorewind' } else { 'org.wiicompiled.quest' }
|
$import = '/sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/Import'
|
||||||
$import = "/sdcard/Android/data/$package/files/WiiCompiledOpenXRVR/Import"
|
|
||||||
# The launcher creates Import itself and must own it to remove imported packages (see
|
# 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.
|
# 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()
|
$exists = ((& $adb shell "test -d '$import' && echo yes") | Out-String).Trim()
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# Exports the Quest game kit (see QuestGameKit.psm1) from a configured and built Android CMake
|
# 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
|
# tree. android/app/build.gradle.kts runs this after the native build and packages the result as
|
||||||
# the APK's game_kit assets, once per app flavour.
|
# the APK's game_kit assets. One kit carries a recipe per product the tree built a probe for:
|
||||||
|
# the base game always, and Retro Rewind when the translation includes the mod.
|
||||||
#
|
#
|
||||||
# powershell -ExecutionPolicy Bypass -File android/Export-QuestGameKit.ps1 -CMakeBinaryDir <dir> -OutputDir <dir> -LlvmStrip <llvm-strip> [-Product base|retro_rewind]
|
# powershell -ExecutionPolicy Bypass -File android/Export-QuestGameKit.ps1 -CMakeBinaryDir <dir> -OutputDir <dir> -LlvmStrip <llvm-strip>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
||||||
[Parameter(Mandatory)] [string]$OutputDir,
|
[Parameter(Mandatory)] [string]$OutputDir,
|
||||||
[Parameter(Mandatory)] [string]$LlvmStrip,
|
[Parameter(Mandatory)] [string]$LlvmStrip
|
||||||
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
|
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Import-Module (Join-Path $PSScriptRoot 'QuestGameKit.psm1') -Force
|
Import-Module (Join-Path $PSScriptRoot 'QuestGameKit.psm1') -Force
|
||||||
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
$fingerprint = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip -Product $Product
|
$kit = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip
|
||||||
Write-Host "Quest game kit ($Product) $fingerprint -> $OutputDir"
|
Write-Host "Quest game kit $($kit.Fingerprint) [$($kit.Products -join ', ')] -> $OutputDir"
|
||||||
+84
-61
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
|
|
||||||
$script:KitSchema = 2
|
$script:KitSchema = 3
|
||||||
$script:GameSchema = 1
|
$script:GameSchema = 1
|
||||||
|
|
||||||
# What each app flavour's kit is made of. Slots name the game's objects in the probe's link line,
|
# What each app flavour's kit is made of. Slots name the game's objects in the probe's link line,
|
||||||
@@ -268,34 +268,15 @@ function Export-QuestGameKit {
|
|||||||
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
||||||
[Parameter(Mandatory)] [string]$OutputDir,
|
[Parameter(Mandatory)] [string]$OutputDir,
|
||||||
[Parameter(Mandatory)] [string]$RepoRoot,
|
[Parameter(Mandatory)] [string]$RepoRoot,
|
||||||
[Parameter(Mandatory)] [string]$LlvmStrip,
|
[Parameter(Mandatory)] [string]$LlvmStrip
|
||||||
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
|
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$binary = ConvertTo-ForwardPath $CMakeBinaryDir
|
$binary = ConvertTo-ForwardPath $CMakeBinaryDir
|
||||||
$runtimeInclude = ConvertTo-ForwardPath (Join-Path $RepoRoot 'runtime/include')
|
$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")
|
$ninja = [IO.File]::ReadAllText("$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 = @{}
|
|
||||||
foreach ($line in ($edge.Groups[4].Value -split "\r?\n")) {
|
|
||||||
if ($line -match '^ (\w+) = (.*)$') { $vars[$Matches[1]] = $Matches[2] }
|
|
||||||
}
|
|
||||||
$rule = [regex]::Match($ninja, "(?ms)^rule $([regex]::Escape($edge.Groups[2].Value))\r?\n.*?command = ([^\r\n]*)")
|
|
||||||
if (-not $rule.Success) {
|
|
||||||
$rules = [IO.File]::ReadAllText("$binary/CMakeFiles/rules.ninja")
|
|
||||||
$rule = [regex]::Match($rules, "(?ms)^rule $([regex]::Escape($edge.Groups[2].Value))\r?\n.*?command = ([^\r\n]*)")
|
|
||||||
}
|
|
||||||
$target = [regex]::Match($rule.Groups[1].Value, '--target=(\S+)').Groups[1].Value
|
|
||||||
if (-not $target) { throw "Cannot read the link target triple from rule $($edge.Groups[2].Value)" }
|
|
||||||
|
|
||||||
# Compile flags per generated kind, from CMake's own commands.
|
|
||||||
$commands = Read-CompileCommands "$binary/compile_commands.json"
|
$commands = Read-CompileCommands "$binary/compile_commands.json"
|
||||||
|
$rules = if (Test-Path "$binary/CMakeFiles/rules.ninja") { [IO.File]::ReadAllText("$binary/CMakeFiles/rules.ninja") } else { '' }
|
||||||
|
|
||||||
function Find-Command([string]$Pattern) {
|
function Find-Command([string]$Pattern) {
|
||||||
$entry = @($commands | Where-Object { $_.file.Replace('\', '/') -match $Pattern })
|
$entry = @($commands | Where-Object { $_.file.Replace('\', '/') -match $Pattern })
|
||||||
if ($entry.Count -eq 0) { throw "compile_commands.json has no source matching $Pattern" }
|
if ($entry.Count -eq 0) { throw "compile_commands.json has no source matching $Pattern" }
|
||||||
@@ -307,20 +288,15 @@ function Export-QuestGameKit {
|
|||||||
param($Entry)
|
param($Entry)
|
||||||
ConvertTo-KitCompileFlags -Arguments (Split-CommandLine $Entry.command) -RuntimeInclude $runtimeInclude -Workspace $workspace
|
ConvertTo-KitCompileFlags -Arguments (Split-CommandLine $Entry.command) -RuntimeInclude $runtimeInclude -Workspace $workspace
|
||||||
}
|
}
|
||||||
$compile = [ordered]@{
|
|
||||||
translated = & $flagsFor $translatedEntry
|
|
||||||
product = & $flagsFor (Find-Command $kitProfile.RegistrationPattern)
|
|
||||||
runtime = & $flagsFor (Find-Command '/generated/data_sections_init\.cpp$')
|
|
||||||
asm = & $flagsFor (Find-Command 'data_sections_init_blobs[^/]*\.S$')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $OutputDir) { Remove-Item -Recurse -Force $OutputDir }
|
if (Test-Path $OutputDir) { Remove-Item -Recurse -Force $OutputDir }
|
||||||
$objectsDir = Join-Path $OutputDir 'objects'
|
New-Item -ItemType Directory -Force (Join-Path $OutputDir 'objects'), (Join-Path $OutputDir 'libs') | Out-Null
|
||||||
$libsDir = Join-Path $OutputDir 'libs'
|
|
||||||
New-Item -ItemType Directory -Force $objectsDir, $libsDir | Out-Null
|
|
||||||
Copy-Item -Recurse (Join-Path $RepoRoot 'runtime/include') (Join-Path $OutputDir 'include')
|
Copy-Item -Recurse (Join-Path $RepoRoot 'runtime/include') (Join-Path $OutputDir 'include')
|
||||||
|
|
||||||
|
# One copy of every object and archive, however many products link it: the two probes differ
|
||||||
|
# only in their own product object.
|
||||||
$copied = @{}
|
$copied = @{}
|
||||||
|
$script:kitIndex = 0
|
||||||
$copy = {
|
$copy = {
|
||||||
param([string]$Source, [string]$Directory)
|
param([string]$Source, [string]$Directory)
|
||||||
$full = ConvertTo-ForwardPath $(if ([IO.Path]::IsPathRooted($Source)) { $Source } else { "$binary/$Source" })
|
$full = ConvertTo-ForwardPath $(if ([IO.Path]::IsPathRooted($Source)) { $Source } else { "$binary/$Source" })
|
||||||
@@ -333,9 +309,32 @@ function Export-QuestGameKit {
|
|||||||
$copied[$full] = "{kit}/$Directory/$name"
|
$copied[$full] = "{kit}/$Directory/$name"
|
||||||
return $copied[$full]
|
return $copied[$full]
|
||||||
}
|
}
|
||||||
$script:kitIndex = 0
|
|
||||||
|
|
||||||
# Link inputs in the probe's order, with the game's slots marked where WiiCompiled has them.
|
# One recipe per product this CMake tree built a probe for. The base one must be there; Retro
|
||||||
|
# Rewind's exists only once translate-mod and emit-build-shards have run.
|
||||||
|
$products = [ordered]@{}
|
||||||
|
foreach ($product in 'base', 'retro_rewind') {
|
||||||
|
$kitProfile = $script:KitProfiles[$product]
|
||||||
|
$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) {
|
||||||
|
if ($product -eq 'base') { throw "No $($kitProfile.Probe) link edge in $binary/build.ninja" }
|
||||||
|
Write-Host "No $($kitProfile.Probe) in this build; the kit carries the base game only."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$explicit = ($edge.Groups[3].Value.Replace('$ ', '<ninja-space>') -split ' \|')[0]
|
||||||
|
$objects = @($explicit -split ' ' | Where-Object { $_ } | ForEach-Object { ConvertFrom-NinjaPath $_.Replace('<ninja-space>', '$ ') })
|
||||||
|
$vars = @{}
|
||||||
|
foreach ($line in ($edge.Groups[4].Value -split "\r?\n")) {
|
||||||
|
if ($line -match '^ (\w+) = (.*)$') { $vars[$Matches[1]] = $Matches[2] }
|
||||||
|
}
|
||||||
|
$rule = [regex]::Match($ninja, "(?ms)^rule $([regex]::Escape($edge.Groups[2].Value))\r?\n.*?command = ([^\r\n]*)")
|
||||||
|
if (-not $rule.Success) {
|
||||||
|
$rule = [regex]::Match($rules, "(?ms)^rule $([regex]::Escape($edge.Groups[2].Value))\r?\n.*?command = ([^\r\n]*)")
|
||||||
|
}
|
||||||
|
$target = [regex]::Match($rule.Groups[1].Value, '--target=(\S+)').Groups[1].Value
|
||||||
|
if (-not $target) { throw "Cannot read the link target triple from rule $($edge.Groups[2].Value)" }
|
||||||
|
|
||||||
|
# Link inputs in the probe's order, with the game's slots marked where the product has them.
|
||||||
$inputs = New-Object System.Collections.Generic.List[string]
|
$inputs = New-Object System.Collections.Generic.List[string]
|
||||||
$lastRuntime = -1
|
$lastRuntime = -1
|
||||||
for ($i = 0; $i -lt $objects.Count; $i++) {
|
for ($i = 0; $i -lt $objects.Count; $i++) {
|
||||||
@@ -352,7 +351,7 @@ function Export-QuestGameKit {
|
|||||||
}
|
}
|
||||||
foreach ($slot in $kitProfile.Slots) {
|
foreach ($slot in $kitProfile.Slots) {
|
||||||
if ($slot -ne 'translated' -and -not $inputs.Contains("{game:$slot}")) {
|
if ($slot -ne 'translated' -and -not $inputs.Contains("{game:$slot}")) {
|
||||||
throw "The probe link line lacks the objects the {game:$slot} slot follows"
|
throw "The $product probe link line lacks the objects the {game:$slot} slot follows"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($token in ((ConvertFrom-NinjaPath $vars['LINK_LIBRARIES']) -split ' ' | Where-Object { $_ })) {
|
foreach ($token in ((ConvertFrom-NinjaPath $vars['LINK_LIBRARIES']) -split ' ' | Where-Object { $_ })) {
|
||||||
@@ -362,7 +361,7 @@ function Export-QuestGameKit {
|
|||||||
else { $inputs.Add((& $copy $token 'libs')) }
|
else { $inputs.Add((& $copy $token 'libs')) }
|
||||||
if ($path -match '/libmkw_platform\.a$') { $inputs.Add('{game:translated}') }
|
if ($path -match '/libmkw_platform\.a$') { $inputs.Add('{game:translated}') }
|
||||||
}
|
}
|
||||||
if (-not $inputs.Contains('{game:translated}')) { throw 'The probe link line lacks libmkw_platform.a' }
|
if (-not $inputs.Contains('{game:translated}')) { throw "The $product probe link line lacks libmkw_platform.a" }
|
||||||
|
|
||||||
$linkFlags = New-Object System.Collections.Generic.List[string]
|
$linkFlags = New-Object System.Collections.Generic.List[string]
|
||||||
$linkFlags.Add("--target=$target"); $linkFlags.Add('--sysroot={sysroot}'); $linkFlags.Add('-fPIC')
|
$linkFlags.Add("--target=$target"); $linkFlags.Add('--sysroot={sysroot}'); $linkFlags.Add('-fPIC')
|
||||||
@@ -376,8 +375,25 @@ function Export-QuestGameKit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$linkFlags.Add('-Wl,-soname,libmain.so')
|
$linkFlags.Add('-Wl,-soname,libmain.so')
|
||||||
$lld = Get-KitLldArguments -ClangCxx (Join-Path (Split-Path -Parent $LlvmStrip) 'clang++.exe') -KitDir $OutputDir `
|
|
||||||
|
$products[$product] = [ordered]@{
|
||||||
|
output = 'libmain.so'
|
||||||
|
target = $target
|
||||||
|
compile = [ordered]@{
|
||||||
|
translated = & $flagsFor $translatedEntry
|
||||||
|
product = & $flagsFor (Find-Command $kitProfile.RegistrationPattern)
|
||||||
|
runtime = & $flagsFor (Find-Command '/generated/data_sections_init\.cpp$')
|
||||||
|
asm = & $flagsFor (Find-Command 'data_sections_init_blobs[^/]*\.S$')
|
||||||
|
}
|
||||||
|
sources = $kitProfile.Sources
|
||||||
|
link = [ordered]@{
|
||||||
|
flags = $linkFlags.ToArray()
|
||||||
|
inputs = $inputs.ToArray()
|
||||||
|
lld = Get-KitLldArguments -ClangCxx (Join-Path (Split-Path -Parent $LlvmStrip) 'clang++.exe') -KitDir $OutputDir `
|
||||||
-Flags $linkFlags.ToArray() -Inputs $inputs.ToArray() -Slots $kitProfile.Slots
|
-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
|
# 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
|
# the runtime sources it indexes for native registrations, which must be the ones compiled into
|
||||||
@@ -389,13 +405,8 @@ function Export-QuestGameKit {
|
|||||||
|
|
||||||
$recipe = [ordered]@{
|
$recipe = [ordered]@{
|
||||||
schema = $script:KitSchema
|
schema = $script:KitSchema
|
||||||
product = $Product
|
|
||||||
output = 'libmain.so'
|
|
||||||
target = $target
|
|
||||||
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
|
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
|
||||||
compile = $compile
|
products = $products
|
||||||
sources = $kitProfile.Sources
|
|
||||||
link = [ordered]@{ flags = $linkFlags.ToArray(); inputs = $inputs.ToArray(); lld = $lld }
|
|
||||||
}
|
}
|
||||||
$recipeJson = $recipe | ConvertTo-Json -Depth 8 -Compress
|
$recipeJson = $recipe | ConvertTo-Json -Depth 8 -Compress
|
||||||
$files = Get-RelativeFiles $OutputDir | Sort-Object FullName | ForEach-Object {
|
$files = Get-RelativeFiles $OutputDir | Sort-Object FullName | ForEach-Object {
|
||||||
@@ -403,7 +414,7 @@ function Export-QuestGameKit {
|
|||||||
}
|
}
|
||||||
$recipe.fingerprint = Get-StringSha256Hex ($recipeJson + "`n" + ($files -join "`n"))
|
$recipe.fingerprint = Get-StringSha256Hex ($recipeJson + "`n" + ($files -join "`n"))
|
||||||
[IO.File]::WriteAllText((Join-Path $OutputDir 'kit.json'), ($recipe | ConvertTo-Json -Depth 8), (New-Object Text.UTF8Encoding $false))
|
[IO.File]::WriteAllText((Join-Path $OutputDir 'kit.json'), ($recipe | ConvertTo-Json -Depth 8), (New-Object Text.UTF8Encoding $false))
|
||||||
return $recipe.fingerprint
|
return [pscustomobject]@{ Fingerprint = $recipe.fingerprint; Products = @($products.Keys) }
|
||||||
}
|
}
|
||||||
|
|
||||||
# The generated source lists emit-build-shards wrote into shards.cmake.
|
# The generated source lists emit-build-shards wrote into shards.cmake.
|
||||||
@@ -425,7 +436,8 @@ function Invoke-QuestGameBuild {
|
|||||||
[Parameter(Mandatory)] [string]$ClangC,
|
[Parameter(Mandatory)] [string]$ClangC,
|
||||||
[Parameter(Mandatory)] [string]$Sysroot,
|
[Parameter(Mandatory)] [string]$Sysroot,
|
||||||
[Parameter(Mandatory)] [string]$Ninja,
|
[Parameter(Mandatory)] [string]$Ninja,
|
||||||
[Parameter(Mandatory)] [int]$TranslatedJobs
|
[Parameter(Mandatory)] [int]$TranslatedJobs,
|
||||||
|
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$kit = ConvertTo-ForwardPath $KitDir
|
$kit = ConvertTo-ForwardPath $KitDir
|
||||||
@@ -433,6 +445,10 @@ function Invoke-QuestGameBuild {
|
|||||||
$workspace = ConvertTo-ForwardPath (Split-Path -Parent $generated)
|
$workspace = ConvertTo-ForwardPath (Split-Path -Parent $generated)
|
||||||
$recipe = [IO.File]::ReadAllText("$kit/kit.json") | ConvertFrom-Json
|
$recipe = [IO.File]::ReadAllText("$kit/kit.json") | ConvertFrom-Json
|
||||||
if ($recipe.schema -ne $script:KitSchema) { throw "Unsupported game kit schema $($recipe.schema)" }
|
if ($recipe.schema -ne $script:KitSchema) { throw "Unsupported game kit schema $($recipe.schema)" }
|
||||||
|
if (-not $recipe.products.PSObject.Properties.Name.Contains($Product)) {
|
||||||
|
throw "This Quest app's game kit cannot build $Product (it has: $($recipe.products.PSObject.Properties.Name -join ', '))"
|
||||||
|
}
|
||||||
|
$productRecipe = $recipe.products.$Product
|
||||||
# The translation must come from the same release as the kit: its code is compiled against the
|
# The translation must come from the same release as the kit: its code is compiled against the
|
||||||
# kit's runtime headers and linked with the kit's runtime objects.
|
# kit's runtime headers and linked with the kit's runtime objects.
|
||||||
$workspaceInclude = Join-Path $workspace 'runtime/include'
|
$workspaceInclude = Join-Path $workspace 'runtime/include'
|
||||||
@@ -447,10 +463,10 @@ function Invoke-QuestGameBuild {
|
|||||||
$expand = { param([string]$s) $s.Replace('{kit}', $kit).Replace('{sysroot}', (ConvertTo-ForwardPath $Sysroot)).Replace('{workspace}', $workspace) }
|
$expand = { param([string]$s) $s.Replace('{kit}', $kit).Replace('{sysroot}', (ConvertTo-ForwardPath $Sysroot)).Replace('{workspace}', $workspace) }
|
||||||
|
|
||||||
foreach ($kind in 'translated', 'product', 'runtime', 'asm') {
|
foreach ($kind in 'translated', 'product', 'runtime', 'asm') {
|
||||||
$lines = @($recipe.compile.$kind | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) })
|
$lines = @($productRecipe.compile.$kind | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) })
|
||||||
[IO.File]::WriteAllText("$build/$kind.rsp", ($lines -join "`n"), (New-Object Text.UTF8Encoding $false))
|
[IO.File]::WriteAllText("$build/$kind.rsp", ($lines -join "`n"), (New-Object Text.UTF8Encoding $false))
|
||||||
}
|
}
|
||||||
$linkLines = @($recipe.link.flags | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) })
|
$linkLines = @($productRecipe.link.flags | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) })
|
||||||
[IO.File]::WriteAllText("$build/link.rsp", ($linkLines -join "`n"), (New-Object Text.UTF8Encoding $false))
|
[IO.File]::WriteAllText("$build/link.rsp", ($linkLines -join "`n"), (New-Object Text.UTF8Encoding $false))
|
||||||
|
|
||||||
# The Windows installer generates blob assembly for PE/COFF; every one of them is rewritten for
|
# The Windows installer generates blob assembly for PE/COFF; every one of them is rewritten for
|
||||||
@@ -469,9 +485,9 @@ function Invoke-QuestGameBuild {
|
|||||||
# file.
|
# file.
|
||||||
$shards = "$generated/build_shards/shards.cmake"
|
$shards = "$generated/build_shards/shards.cmake"
|
||||||
$sources = [ordered]@{}
|
$sources = [ordered]@{}
|
||||||
foreach ($slot in $recipe.sources.PSObject.Properties.Name) {
|
foreach ($slot in $productRecipe.sources.PSObject.Properties.Name) {
|
||||||
$slotSources = New-Object System.Collections.Generic.List[string]
|
$slotSources = New-Object System.Collections.Generic.List[string]
|
||||||
foreach ($entry in $recipe.sources.$slot) {
|
foreach ($entry in $productRecipe.sources.$slot) {
|
||||||
if ($entry.StartsWith('@')) {
|
if ($entry.StartsWith('@')) {
|
||||||
Read-ShardList $shards $entry.Substring(1) | ForEach-Object { $slotSources.Add($_) }
|
Read-ShardList $shards $entry.Substring(1) | ForEach-Object { $slotSources.Add($_) }
|
||||||
} else {
|
} else {
|
||||||
@@ -482,7 +498,7 @@ function Invoke-QuestGameBuild {
|
|||||||
}
|
}
|
||||||
if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" }
|
if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" }
|
||||||
foreach ($slot in $sources.Keys) {
|
foreach ($slot in $sources.Keys) {
|
||||||
if ($sources[$slot].Count -eq 0) { throw "The $slot sources of a $($recipe.product) game are missing from $shards" }
|
if ($sources[$slot].Count -eq 0) { throw "The $slot sources of a $Product game are missing from $shards" }
|
||||||
}
|
}
|
||||||
|
|
||||||
$ninjaText = New-Object System.Text.StringBuilder
|
$ninjaText = New-Object System.Text.StringBuilder
|
||||||
@@ -511,7 +527,7 @@ function Invoke-QuestGameBuild {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$linkInputs = New-Object System.Collections.Generic.List[string]
|
$linkInputs = New-Object System.Collections.Generic.List[string]
|
||||||
foreach ($linkInput in $recipe.link.inputs) {
|
foreach ($linkInput in $productRecipe.link.inputs) {
|
||||||
$slot = [regex]::Match($linkInput, '^\{game:(\w+)\}$').Groups[1].Value
|
$slot = [regex]::Match($linkInput, '^\{game:(\w+)\}$').Groups[1].Value
|
||||||
if (-not $slot) { $linkInputs.Add((& $expand $linkInput)); continue }
|
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" }
|
if (-not $objectsOf.ContainsKey($slot)) { throw "The kit recipe links a {game:$slot} slot it names no sources for" }
|
||||||
@@ -534,33 +550,38 @@ function Invoke-QuestGameBuild {
|
|||||||
return "$build/libmain.so"
|
return "$build/libmain.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
# A .wcgame: the built library and what it was built from, for the headset to import. With
|
# A .wcgame: the built library and what it was built from, for the headset to import. It can also
|
||||||
# -DataDir it also carries the extracted disc (DATA/...), written without compression, so a player who
|
# carry, written without compression, the extracted disc (-DataDir, as DATA/...) and the Retro
|
||||||
# builds on a PC copies one file to the headset and never needs the disc image there.
|
# Rewind pack (-ModDir, as MOD/...), so a player who builds on a PC copies one file to the headset
|
||||||
|
# and needs nothing else there.
|
||||||
function New-QuestGamePackage {
|
function New-QuestGamePackage {
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)] [string]$Library,
|
[Parameter(Mandatory)] [string]$Library,
|
||||||
[string]$DataDir = '',
|
[string]$DataDir = '',
|
||||||
|
[string]$ModDir = '',
|
||||||
[Parameter(Mandatory)] [string]$KitDir,
|
[Parameter(Mandatory)] [string]$KitDir,
|
||||||
[Parameter(Mandatory)] [string]$GameId,
|
[Parameter(Mandatory)] [string]$GameId,
|
||||||
[Parameter(Mandatory)] [string]$DolSha256,
|
[Parameter(Mandatory)] [string]$DolSha256,
|
||||||
[Parameter(Mandatory)] [string]$RelSha256,
|
[Parameter(Mandatory)] [string]$RelSha256,
|
||||||
[Parameter(Mandatory)] [string]$BuiltBy,
|
[Parameter(Mandatory)] [string]$BuiltBy,
|
||||||
[Parameter(Mandatory)] [string]$OutputPath
|
[Parameter(Mandatory)] [string]$OutputPath,
|
||||||
|
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$recipe = [IO.File]::ReadAllText((Join-Path $KitDir 'kit.json')) | ConvertFrom-Json
|
$recipe = [IO.File]::ReadAllText((Join-Path $KitDir 'kit.json')) | ConvertFrom-Json
|
||||||
|
if ($ModDir -and $Product -ne 'retro_rewind') { throw 'Only a Retro Rewind game carries the mod pack.' }
|
||||||
$game = [ordered]@{
|
$game = [ordered]@{
|
||||||
schema = $script:GameSchema
|
schema = $script:GameSchema
|
||||||
profile = $recipe.product
|
profile = $Product
|
||||||
gameId = $GameId
|
gameId = $GameId
|
||||||
dolSha256 = $DolSha256
|
dolSha256 = $DolSha256
|
||||||
relSha256 = $RelSha256
|
relSha256 = $RelSha256
|
||||||
kitFingerprint = $recipe.fingerprint
|
kitFingerprint = $recipe.fingerprint
|
||||||
library = $recipe.output
|
library = $recipe.products.$Product.output
|
||||||
librarySha256 = Get-Sha256Hex $Library
|
librarySha256 = Get-Sha256Hex $Library
|
||||||
includesData = [bool]$DataDir
|
includesData = [bool]$DataDir
|
||||||
|
includesMod = [bool]$ModDir
|
||||||
builtBy = $BuiltBy
|
builtBy = $BuiltBy
|
||||||
builtAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
builtAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||||
}
|
}
|
||||||
@@ -572,10 +593,12 @@ function New-QuestGamePackage {
|
|||||||
$entry = $zip.CreateEntry('game.json')
|
$entry = $zip.CreateEntry('game.json')
|
||||||
$writer = New-Object IO.StreamWriter($entry.Open(), (New-Object Text.UTF8Encoding $false))
|
$writer = New-Object IO.StreamWriter($entry.Open(), (New-Object Text.UTF8Encoding $false))
|
||||||
try { $writer.Write(($game | ConvertTo-Json)) } finally { $writer.Dispose() }
|
try { $writer.Write(($game | ConvertTo-Json)) } finally { $writer.Dispose() }
|
||||||
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $Library, $recipe.output, 'Optimal')
|
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $Library, $game.library, 'Optimal')
|
||||||
if ($DataDir) {
|
foreach ($tree in @{ Prefix = 'DATA'; Directory = $DataDir }, @{ Prefix = 'MOD'; Directory = $ModDir }) {
|
||||||
foreach ($file in Get-RelativeFiles $DataDir) {
|
if (-not $tree.Directory) { continue }
|
||||||
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $file.FullName, "DATA/$($file.Relative)", 'NoCompression')
|
foreach ($file in Get-RelativeFiles $tree.Directory) {
|
||||||
|
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
||||||
|
$zip, $file.FullName, "$($tree.Prefix)/$($file.Relative)", 'NoCompression')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally { $zip.Dispose() }
|
} finally { $zip.Dispose() }
|
||||||
|
|||||||
+20
-11
@@ -9,17 +9,25 @@ holds the Gradle project, its helper scripts, the game kit tooling
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # stages the SDL3 3.4.4 AAR once
|
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # stages the SDL3 3.4.4 AAR once
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # base game, debug-signed, installs over adb
|
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, debug-signed, installs over adb
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Flavor retroRewind # Retro Rewind product
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The translated game (the translator's `generated/` tree for **your own**
|
One app offers both games, with a toggle on Home: Mario Kart Wii and, when your
|
||||||
PAL `RMCP01` disc) is passed with `-Generated <dir>`; it defaults to the
|
translation includes the mod, Retro Rewind. The translated game (the translator's
|
||||||
installer workspace next to this checkout. No game data is ever part of the
|
`generated/` tree for **your own** PAL `RMCP01` disc) is passed with
|
||||||
APK, and neither is any translated game code: the base APK carries a game kit,
|
`-Generated <dir>`; it defaults to the installer workspace next to this checkout.
|
||||||
and your `libmain.so` is built from your own disc, either on the headset
|
No game data is ever part of the APK, and neither is any translated game code:
|
||||||
(**Build on this Quest**, about half an hour) or on a PC and imported
|
the APK carries one game kit holding a recipe per game, and your `libmain.so` is
|
||||||
(`Build-QuestGame.ps1 -Install`, or **Import from computer** with its `.wcgame`).
|
built from your own disc, either on the headset (**Build on this Quest**, about
|
||||||
|
half an hour) or on a PC and imported:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product base -Install
|
||||||
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install
|
||||||
|
```
|
||||||
|
|
||||||
|
`-Mod` puts the Retro Rewind pack in the package, which is how the headset gets
|
||||||
|
it; **Import from computer** takes the same `.wcgame` from the Import folder.
|
||||||
Copy your disc image to the headset (for example `adb push MarioKart.iso
|
Copy your disc image to the headset (for example `adb push MarioKart.iso
|
||||||
/sdcard/Download/`) and press **Select disc image** in the launcher, which checks
|
/sdcard/Download/`) and press **Select disc image** in the launcher, which checks
|
||||||
and extracts it with nod, as the PC installer does. Or push an already extracted
|
and extracts it with nod, as the PC installer does. Or push an already extracted
|
||||||
@@ -32,8 +40,9 @@ disc to
|
|||||||
`Config.toml`, saves and logs live in the same `WiiCompiledOpenXRVR` directory.
|
`Config.toml`, saves and logs live in the same `WiiCompiledOpenXRVR` directory.
|
||||||
|
|
||||||
The app opens on a 2D launcher panel modelled on the PC launcher (WheelWizard VR):
|
The app opens on a 2D launcher panel modelled on the PC launcher (WheelWizard VR):
|
||||||
**Home** starts the game in the headset and says when DATA is missing, and
|
**Home** picks the game with a toggle, starts it in the headset and says what is
|
||||||
|
still missing (the disc files, the game itself, or Retro Rewind's pack), and
|
||||||
**Settings** edits `Config.toml` (VR camera, render scale, virtual screen,
|
**Settings** edits `Config.toml` (VR camera, render scale, virtual screen,
|
||||||
resolution, controllers, audio). The game itself is `QuestActivity`, in its own
|
resolution, controllers, audio) and lists both games. The game itself is `QuestActivity`, in its own
|
||||||
`:game` process; `adb shell am start -n org.wiicompiled.quest/.QuestActivity`
|
`:game` process; `adb shell am start -n org.wiicompiled.quest/.QuestActivity`
|
||||||
still starts it directly.
|
still starts it directly.
|
||||||
@@ -42,6 +42,13 @@ val discPins: Map<String, String> = run {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the translation this build is pointed at includes the Retro Rewind mod, which decides
|
||||||
|
// whether the kit can carry that product (emit-build-shards records it in shards.cmake).
|
||||||
|
val hasRetroRewindShards: Boolean = run {
|
||||||
|
val shards = File(mkwGeneratedDir ?: return@run false, "build_shards/shards.cmake")
|
||||||
|
shards.isFile && shards.readText().contains("set(MKW_HAVE_RETRO_REWIND_SHARDS ON)")
|
||||||
|
}
|
||||||
|
|
||||||
// android/nod-jni: nod, the disc image library the PC installer runs as nodtool,
|
// android/nod-jni: nod, the disc image library the PC installer runs as nodtool,
|
||||||
// cross-compiled with cargo for the launcher's "Select disc image". Needs a Rust
|
// cross-compiled with cargo for the launcher's "Select disc image". Needs a Rust
|
||||||
// toolchain with the aarch64-linux-android target (see docs/quest-port.md).
|
// toolchain with the aarch64-linux-android target (see docs/quest-port.md).
|
||||||
@@ -100,12 +107,23 @@ android {
|
|||||||
for ((name, value) in discPins) {
|
for ((name, value) in discPins) {
|
||||||
buildConfigField("String", name, "\"$value\"")
|
buildConfigField("String", name, "\"$value\"")
|
||||||
}
|
}
|
||||||
|
// The APK carries game kits, not games: the player's libmain.so is built from their own
|
||||||
|
// disc and loaded from private storage (see android/QuestGameKit.psm1). One app offers
|
||||||
|
// both products, like the PC launcher, and the launcher's toggle picks which one to play.
|
||||||
|
buildConfigField("boolean", "EXTERNAL_GAME", "true")
|
||||||
|
buildConfigField("boolean", "ON_DEVICE_BUILD", "true")
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += "arm64-v8a"
|
abiFilters += "arm64-v8a"
|
||||||
}
|
}
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
|
// One probe per product the translation can build. Retro Rewind's exists only
|
||||||
|
// once translate-mod and emit-build-shards have run over the mod.
|
||||||
|
targets += "mkw_quest_kit_probe"
|
||||||
|
if (hasRetroRewindShards) {
|
||||||
|
targets += "mkw_quest_kit_probe_retro"
|
||||||
|
}
|
||||||
arguments += listOf(
|
arguments += listOf(
|
||||||
"-DANDROID_STL=c++_shared",
|
"-DANDROID_STL=c++_shared",
|
||||||
"-DANDROID_PLATFORM=android-29",
|
"-DANDROID_PLATFORM=android-29",
|
||||||
@@ -121,33 +139,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += "profile"
|
|
||||||
productFlavors {
|
|
||||||
create("base") {
|
|
||||||
dimension = "profile"
|
|
||||||
buildConfigField("String", "MAIN_LIBRARY", "\"main\"")
|
|
||||||
buildConfigField("String", "PROFILE", "\"base\"")
|
|
||||||
// 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") {
|
|
||||||
dimension = "profile"
|
|
||||||
applicationIdSuffix = ".retrorewind"
|
|
||||||
versionNameSuffix = "-retro-rewind"
|
|
||||||
buildConfigField("String", "MAIN_LIBRARY", "\"main_retro_rewind\"")
|
|
||||||
buildConfigField("String", "PROFILE", "\"retro_rewind\"")
|
|
||||||
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" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
// aurora-main needs CMake 3.25+, newer than the SDK's bundled 3.22.1;
|
// aurora-main needs CMake 3.25+, newer than the SDK's bundled 3.22.1;
|
||||||
@@ -204,10 +195,6 @@ abstract class ExportQuestGameKit : DefaultTask() {
|
|||||||
@get:Internal
|
@get:Internal
|
||||||
abstract val llvmStrip: RegularFileProperty
|
abstract val llvmStrip: RegularFileProperty
|
||||||
|
|
||||||
/** "base" or "retro_rewind": which product's kit this flavour carries. */
|
|
||||||
@get:Internal
|
|
||||||
abstract val product: Property<String>
|
|
||||||
|
|
||||||
@get:Inject
|
@get:Inject
|
||||||
abstract val execOperations: ExecOperations
|
abstract val execOperations: ExecOperations
|
||||||
|
|
||||||
@@ -219,11 +206,10 @@ abstract class ExportQuestGameKit : DefaultTask() {
|
|||||||
@TaskAction
|
@TaskAction
|
||||||
fun export() {
|
fun export() {
|
||||||
val app = appDir.get().asFile
|
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()
|
val probe = File(app, "build/intermediates/cxx").walkTopDown()
|
||||||
.filter { it.name == library && it.parentFile.name == "arm64-v8a" }
|
.filter { it.name == "libmkw_quest_kit_probe.so" && it.parentFile.name == "arm64-v8a" }
|
||||||
.maxByOrNull { it.lastModified() }
|
.maxByOrNull { it.lastModified() }
|
||||||
?: throw GradleException("No $library; the native build did not produce the game kit probe")
|
?: throw GradleException("No libmkw_quest_kit_probe.so; the native build did not produce the game kit probe")
|
||||||
val configuration = probe.parentFile.parentFile.parentFile // <BuildType>/<hash>
|
val configuration = probe.parentFile.parentFile.parentFile // <BuildType>/<hash>
|
||||||
val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
|
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")
|
if (!File(binaryDir, "build.ninja").isFile) throw GradleException("No CMake tree at $binaryDir")
|
||||||
@@ -232,7 +218,6 @@ abstract class ExportQuestGameKit : DefaultTask() {
|
|||||||
commandLine(
|
commandLine(
|
||||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
|
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
|
||||||
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
|
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
|
||||||
"-Product", product.get(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,24 +260,19 @@ androidComponents {
|
|||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
|
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
|
||||||
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
||||||
// Both flavours ship a kit instead of the game. AGP packages every library in the CMake
|
// The APK ships kits instead of games. AGP packages every library in the CMake output
|
||||||
// output directory, so a game library left over from an earlier build would otherwise
|
// directory, so a game library left over from an earlier build would otherwise travel in it.
|
||||||
// travel in the APK.
|
|
||||||
variant.packaging.jniLibs.excludes.add("**/libmain*.so")
|
variant.packaging.jniLibs.excludes.add("**/libmain*.so")
|
||||||
val export = tasks.register<ExportQuestGameKit>("export${capitalized}QuestGameKit") {
|
val export = tasks.register<ExportQuestGameKit>("export${capitalized}QuestGameKit") {
|
||||||
dependsOn("merge${capitalized}NativeLibs")
|
dependsOn("merge${capitalized}NativeLibs")
|
||||||
appDir.set(layout.projectDirectory)
|
appDir.set(layout.projectDirectory)
|
||||||
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
|
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
|
||||||
product.set(if (variant.flavorName == "retroRewind") "retro_rewind" else "base")
|
|
||||||
llvmStrip.set(sdkComponents.ndkDirectory.map {
|
llvmStrip.set(sdkComponents.ndkDirectory.map {
|
||||||
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
|
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
variant.sources.assets?.addGeneratedSourceDirectory(export, ExportQuestGameKit::outputDir)
|
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") {
|
val prepareToolchain = tasks.register<PrepareQuestToolchain>("prepare${capitalized}QuestToolchain") {
|
||||||
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
|
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
|
||||||
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
|
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
|
||||||
@@ -303,7 +283,6 @@ androidComponents {
|
|||||||
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
|
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import java.io.File
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The player's compiled game: libmain.so built from their own disc against the game kit this
|
* A player's compiled game: libmain.so built from their own disc against the game kit this APK
|
||||||
* APK carries (assets/game_kit, see android/QuestGameKit.psm1), plus the game.json it came with.
|
* carries (assets/game_kit, see android/QuestGameKit.psm1), plus the game.json it came with. One
|
||||||
|
* per [GameProfile], so the unmodded game and Retro Rewind can both be installed.
|
||||||
*
|
*
|
||||||
* It lives in internal private storage, the one place Android lets an app load native code it
|
* It lives in internal private storage, the one place Android lets an app load native code it
|
||||||
* did not install (execution from shared or external storage is blocked). A library built for
|
* did not install (execution from shared or external storage is blocked). A library built for
|
||||||
@@ -23,11 +24,14 @@ object GameLibrary {
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var cachedKitFingerprint: String? = null
|
private var cachedKitFingerprint: String? = null
|
||||||
|
|
||||||
fun directory(context: Context): File = File(context.filesDir, "game/${BuildConfig.PROFILE}")
|
fun directory(context: Context, profile: GameProfile = GameProfile.selected(context)): File =
|
||||||
|
File(context.filesDir, "game/${profile.id}")
|
||||||
|
|
||||||
fun library(context: Context): File = File(directory(context), LIBRARY_NAME)
|
fun library(context: Context, profile: GameProfile = GameProfile.selected(context)): File =
|
||||||
|
File(directory(context, profile), LIBRARY_NAME)
|
||||||
|
|
||||||
fun manifest(context: Context): JSONObject? = File(directory(context), MANIFEST_NAME)
|
fun manifest(context: Context, profile: GameProfile = GameProfile.selected(context)): JSONObject? =
|
||||||
|
File(directory(context, profile), MANIFEST_NAME)
|
||||||
.takeIf { it.isFile }
|
.takeIf { it.isFile }
|
||||||
?.let { runCatching { JSONObject(it.readText()) }.getOrNull() }
|
?.let { runCatching { JSONObject(it.readText()) }.getOrNull() }
|
||||||
|
|
||||||
@@ -41,12 +45,9 @@ object GameLibrary {
|
|||||||
return fingerprint
|
return fingerprint
|
||||||
}
|
}
|
||||||
|
|
||||||
fun status(context: Context): Status {
|
fun status(context: Context, profile: GameProfile = GameProfile.selected(context)): Status {
|
||||||
if (!BuildConfig.EXTERNAL_GAME) {
|
val manifest = manifest(context, profile)
|
||||||
return Status.Ready
|
if (manifest == null || !library(context, profile).isFile) {
|
||||||
}
|
|
||||||
val manifest = manifest(context)
|
|
||||||
if (manifest == null || !library(context).isFile) {
|
|
||||||
return Status.Missing
|
return Status.Missing
|
||||||
}
|
}
|
||||||
return if (manifest.optString("kitFingerprint") == kitFingerprint(context)) Status.Ready else Status.Stale
|
return if (manifest.optString("kitFingerprint") == kitFingerprint(context)) Status.Ready else Status.Stale
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.wiicompiled.quest
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.File
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The games this app can play, as the PC launcher offers them: the unmodded game and Retro Rewind.
|
||||||
|
*
|
||||||
|
* One APK carries a kit for each (`assets/game_kit`, see android/QuestGameKit.psm1) and the player
|
||||||
|
* keeps a built game per profile in private storage, so switching between them is a toggle rather
|
||||||
|
* than a second app. The choice is a file, not a preference, because the launcher and the game run
|
||||||
|
* in different processes and the game reads it while starting.
|
||||||
|
*/
|
||||||
|
enum class GameProfile(val id: String, val title: Int, val modPack: Boolean) {
|
||||||
|
Base("base", R.string.home_title_base, modPack = false),
|
||||||
|
RetroRewind("retro_rewind", R.string.home_title_retro_rewind, modPack = true);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SELECTION_FILE = "selected-game"
|
||||||
|
|
||||||
|
fun of(id: String?): GameProfile? = entries.firstOrNull { it.id == id }
|
||||||
|
|
||||||
|
/** The profiles this APK's kit can build and load, in launcher order. */
|
||||||
|
fun available(context: Context): List<GameProfile> {
|
||||||
|
val products = runCatching {
|
||||||
|
context.assets.open("game_kit/kit.json").use { input ->
|
||||||
|
JSONObject(input.reader().readText()).getJSONObject("products").keys().asSequence().toSet()
|
||||||
|
}
|
||||||
|
}.getOrDefault(setOf(Base.id))
|
||||||
|
return entries.filter { it.id in products }.ifEmpty { listOf(Base) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The profile the player last chose, or the first available one. */
|
||||||
|
fun selected(context: Context): GameProfile {
|
||||||
|
val available = available(context)
|
||||||
|
val stored = runCatching { File(context.filesDir, SELECTION_FILE).readText().trim() }.getOrNull()
|
||||||
|
return of(stored)?.takeIf { it in available } ?: available.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun select(context: Context, profile: GameProfile) {
|
||||||
|
File(context.filesDir, SELECTION_FILE).writeText(profile.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.wiicompiled.quest
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import org.wiicompiled.quest.launcher.TomlConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where the game's user files live on the headset. The launcher and the game
|
* Where the game's user files live on the headset. The launcher and the game
|
||||||
@@ -30,9 +31,12 @@ object GameStorage {
|
|||||||
|
|
||||||
fun modDirectory(context: Context): File = File(gameRoot(context), MOD_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. */
|
/** The pack's own Code.pul, which is what a modded game and a mod translation both need. */
|
||||||
fun modContentReady(context: Context): Boolean =
|
fun modCodePul(context: Context): File = File(modDirectory(context), "Binaries/Code.pul")
|
||||||
BuildConfig.PROFILE != "retro_rewind" || File(modDirectory(context), "Binaries/Code.pul").isFile
|
|
||||||
|
/** Whether the pack a profile needs is there. Always true for the unmodded game. */
|
||||||
|
fun modContentReady(context: Context, profile: GameProfile): Boolean =
|
||||||
|
!profile.modPack || modCodePul(context).isFile
|
||||||
|
|
||||||
fun configFile(context: Context): File = File(gameRoot(context), "Config.toml")
|
fun configFile(context: Context): File = File(gameRoot(context), "Config.toml")
|
||||||
|
|
||||||
@@ -58,7 +62,8 @@ object GameStorage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the game directory and a first Config.toml with VR on and the
|
* Creates the game directory and a first Config.toml with VR on and the
|
||||||
* disc path filled in. An existing file is left alone: the launcher, the
|
* disc path filled in. An existing file keeps every setting it has, and only
|
||||||
|
* gains the Retro Rewind pack path when it predates it: the launcher, the
|
||||||
* in-headset settings panel and the player all edit it in place.
|
* in-headset settings panel and the player all edit it in place.
|
||||||
*/
|
*/
|
||||||
fun prepare(context: Context): File {
|
fun prepare(context: Context): File {
|
||||||
@@ -75,10 +80,9 @@ object GameStorage {
|
|||||||
"[paths]",
|
"[paths]",
|
||||||
"dvd_root = \"${discDirectory(context).absolutePath}\"",
|
"dvd_root = \"${discDirectory(context).absolutePath}\"",
|
||||||
)
|
)
|
||||||
// The modded product reads its pack from retro_rewind_root, which sits next to DATA here.
|
// The modded product reads its pack from retro_rewind_root, which sits next to DATA
|
||||||
if (BuildConfig.PROFILE == "retro_rewind") {
|
// here. Harmless for the unmodded game, and one config serves both.
|
||||||
lines += "retro_rewind_root = \"${modDirectory(context).absolutePath}\""
|
lines += "retro_rewind_root = \"${modDirectory(context).absolutePath}\""
|
||||||
}
|
|
||||||
lines += listOf(
|
lines += listOf(
|
||||||
"",
|
"",
|
||||||
"[video]",
|
"[video]",
|
||||||
@@ -90,6 +94,14 @@ object GameStorage {
|
|||||||
"render_scale = 1.0",
|
"render_scale = 1.0",
|
||||||
)
|
)
|
||||||
config.writeText(lines.joinToString("\n", postfix = "\n"))
|
config.writeText(lines.joinToString("\n", postfix = "\n"))
|
||||||
|
} else {
|
||||||
|
// A config written before this app offered Retro Rewind names no pack, and the modded
|
||||||
|
// game would find none. Only that one line is added; the rest is the player's.
|
||||||
|
val toml = TomlConfig.parse(config.readText())
|
||||||
|
if (toml.string("paths", "retro_rewind_root").isNullOrBlank()) {
|
||||||
|
toml.setString("paths", "retro_rewind_root", modDirectory(context).absolutePath)
|
||||||
|
config.writeText(toml.text())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return gameRoot
|
return gameRoot
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,27 +40,26 @@ import org.libsdl.app.SDLSurface
|
|||||||
*/
|
*/
|
||||||
class QuestActivity : SDLActivity() {
|
class QuestActivity : SDLActivity() {
|
||||||
|
|
||||||
override fun getLibraries(): Array<String> =
|
/** The game the launcher's toggle last selected; read once, since a session plays one game. */
|
||||||
if (BuildConfig.EXTERNAL_GAME) arrayOf("SDL3") else arrayOf("SDL3", BuildConfig.MAIN_LIBRARY)
|
private val profile: GameProfile by lazy { GameProfile.selected(this) }
|
||||||
|
|
||||||
|
override fun getLibraries(): Array<String> = arrayOf("SDL3")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The game is not part of the APK: it is the libmain.so the player built from their own disc
|
* The game is not part of the APK: it is the libmain.so the player built from their own disc
|
||||||
* ([GameLibrary]), loaded from private storage. Its libSDL3.so, libpng16.so and
|
* ([GameLibrary]) for the selected profile, loaded from private storage. Its libSDL3.so,
|
||||||
* libc++_shared.so dependencies resolve against the ones this APK installed.
|
* libpng16.so and libc++_shared.so dependencies resolve against the ones this APK installed.
|
||||||
*/
|
*/
|
||||||
override fun loadLibraries() {
|
override fun loadLibraries() {
|
||||||
super.loadLibraries()
|
super.loadLibraries()
|
||||||
if (BuildConfig.EXTERNAL_GAME) {
|
|
||||||
// SDLActivity reports a failed load in its own error dialog and never starts the game.
|
// SDLActivity reports a failed load in its own error dialog and never starts the game.
|
||||||
if (GameLibrary.status(this) != GameLibrary.Status.Ready) {
|
if (GameLibrary.status(this, profile) != GameLibrary.Status.Ready) {
|
||||||
throw UnsatisfiedLinkError(getString(R.string.game_not_installed))
|
throw UnsatisfiedLinkError(getString(R.string.game_not_installed))
|
||||||
}
|
}
|
||||||
System.load(GameLibrary.library(this).absolutePath)
|
System.load(GameLibrary.library(this, profile).absolutePath)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMainSharedObject(): String =
|
override fun getMainSharedObject(): String = GameLibrary.library(this, profile).absolutePath
|
||||||
if (BuildConfig.EXTERNAL_GAME) GameLibrary.library(this).absolutePath else super.getMainSharedObject()
|
|
||||||
|
|
||||||
override fun createSDLSurface(context: Context): SDLSurface = QuestSurface(context)
|
override fun createSDLSurface(context: Context): SDLSurface = QuestSurface(context)
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ object BuildRecipe {
|
|||||||
Regex("""\n\s*entry_points:\s*\n\s*-\s*(0x[0-9A-Fa-f]{8})""").find(manifest)?.groupValues?.get(1)
|
Regex("""\n\s*entry_points:\s*\n\s*-\s*(0x[0-9A-Fa-f]{8})""").find(manifest)?.groupValues?.get(1)
|
||||||
?: throw IllegalArgumentException("recomp.yml names no translation entry point")
|
?: throw IllegalArgumentException("recomp.yml names no translation entry point")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where recomp.yml's Retro Rewind profile expects the mod, relative to the workspace. A base
|
||||||
|
* translation only accounts for a mod whose Code.pul is there.
|
||||||
|
*/
|
||||||
|
fun modRoot(manifest: String): String =
|
||||||
|
Regex("""\n\s*mod_root:\s*(\S+)""").find(manifest)?.groupValues?.get(1)
|
||||||
|
?: throw IllegalArgumentException("recomp.yml names no Retro Rewind mod_root")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The blob assembly for ELF, as runtime/cmake/PublicProducts.cmake rewrites a Windows-generated
|
* The blob assembly for ELF, as runtime/cmake/PublicProducts.cmake rewrites a Windows-generated
|
||||||
* one. The headset's translator already writes ELF sections, so this normally changes nothing.
|
* one. The headset's translator already writes ELF sections, so this normally changes nothing.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.json.JSONArray
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.wiicompiled.quest.BuildConfig
|
import org.wiicompiled.quest.BuildConfig
|
||||||
import org.wiicompiled.quest.GameLibrary
|
import org.wiicompiled.quest.GameLibrary
|
||||||
|
import org.wiicompiled.quest.GameProfile
|
||||||
import org.wiicompiled.quest.GameStorage
|
import org.wiicompiled.quest.GameStorage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +54,7 @@ object GameBuild {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val TAG = "WiiCompiledLauncher"
|
private const val TAG = "WiiCompiledLauncher"
|
||||||
private const val KIT_SCHEMA = 2
|
private const val KIT_SCHEMA = 3
|
||||||
private const val TOOLCHAIN_ASSETS = "quest_toolchain"
|
private const val TOOLCHAIN_ASSETS = "quest_toolchain"
|
||||||
private const val KIT_ASSETS = "game_kit"
|
private const val KIT_ASSETS = "game_kit"
|
||||||
private const val MARKER = ".complete"
|
private const val MARKER = ".complete"
|
||||||
@@ -65,8 +66,8 @@ object GameBuild {
|
|||||||
private const val EXPECTED_TRANSLATION_SECONDS = 600.0
|
private const val EXPECTED_TRANSLATION_SECONDS = 600.0
|
||||||
private const val COMPILE_MEMORY_BYTES = 700L * 1024 * 1024
|
private const val COMPILE_MEMORY_BYTES = 700L * 1024 * 1024
|
||||||
|
|
||||||
/** Null on success, otherwise the message to show. */
|
/** Builds [profile]'s game. Null on success, otherwise the message to show. */
|
||||||
fun run(context: Context, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? {
|
fun run(context: Context, profile: GameProfile, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? {
|
||||||
if (!BuildConfig.ON_DEVICE_BUILD) {
|
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."
|
return "This app cannot build its game on the headset. Build it on a computer and use Import from computer."
|
||||||
}
|
}
|
||||||
@@ -74,6 +75,10 @@ object GameBuild {
|
|||||||
if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) {
|
if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) {
|
||||||
return "The game files (DATA) are needed to build the game. Select your disc image first."
|
return "The game files (DATA) are needed to build the game. Select your disc image first."
|
||||||
}
|
}
|
||||||
|
if (!GameStorage.modContentReady(context, profile)) {
|
||||||
|
return "Retro Rewind needs its pack before it can be built. Import a Retro Rewind game file that " +
|
||||||
|
"carries it, or copy the RetroRewind6 folder to ${GameStorage.modDirectory(context).absolutePath}."
|
||||||
|
}
|
||||||
GameFiles.validateData(disc)?.let { return it }
|
GameFiles.validateData(disc)?.let { return it }
|
||||||
val root = File(context.filesDir, "build")
|
val root = File(context.filesDir, "build")
|
||||||
root.mkdirs()
|
root.mkdirs()
|
||||||
@@ -85,7 +90,7 @@ object GameBuild {
|
|||||||
val stamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
val stamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
val log = BuildLog(File(GameStorage.logsDirectory(context), "build_$stamp.log"))
|
val log = BuildLog(File(GameStorage.logsDirectory(context), "build_$stamp.log"))
|
||||||
try {
|
try {
|
||||||
return Build(context, root, disc, reporter, cancelled, finishing, log).run()
|
return Build(context, profile, root, disc, reporter, cancelled, finishing, log).run()
|
||||||
} catch (e: InterruptedIOException) {
|
} catch (e: InterruptedIOException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -99,6 +104,7 @@ object GameBuild {
|
|||||||
|
|
||||||
private class Build(
|
private class Build(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
val profile: GameProfile,
|
||||||
val root: File,
|
val root: File,
|
||||||
val disc: File,
|
val disc: File,
|
||||||
val reporter: Reporter,
|
val reporter: Reporter,
|
||||||
@@ -109,24 +115,26 @@ object GameBuild {
|
|||||||
val toolchain = File(root, "toolchain")
|
val toolchain = File(root, "toolchain")
|
||||||
val ndk = File(root, "ndk")
|
val ndk = File(root, "ndk")
|
||||||
val kit = File(root, "kit")
|
val kit = File(root, "kit")
|
||||||
val work = File(root, GameLibrary.directory(context).name)
|
val work = File(root, profile.id)
|
||||||
val workspace = File(work, "ws")
|
val workspace = File(work, "ws")
|
||||||
val objects = File(work, "obj")
|
val objects = File(work, "obj")
|
||||||
val temporary = File(root, "tmp")
|
val temporary = File(root, "tmp")
|
||||||
val failure = AtomicReference<String?>(null)
|
val failure = AtomicReference<String?>(null)
|
||||||
|
|
||||||
fun run(): String? {
|
fun run(): String? {
|
||||||
log.line("WiiCompiled Quest ${BuildConfig.VERSION_NAME} building ${BuildConfig.PROFILE} on this headset")
|
log.line("WiiCompiled Quest ${BuildConfig.VERSION_NAME} building ${profile.id} on this headset")
|
||||||
report(0, Step.Prepare)
|
report(0, Step.Prepare)
|
||||||
val toolchainManifest = JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/toolchain.json").reader().use { it.readText() })
|
val toolchainManifest = JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/toolchain.json").reader().use { it.readText() })
|
||||||
unpackToolchain(toolchainManifest)
|
unpackToolchain(toolchainManifest)
|
||||||
report(30, Step.Prepare)
|
report(30, Step.Prepare)
|
||||||
val kitFingerprint = GameLibrary.kitFingerprint(context) ?: return "This app carries no game kit."
|
val kitFingerprint = GameLibrary.kitFingerprint(context) ?: return "This app carries no game kit."
|
||||||
unpackKit(kitFingerprint)
|
unpackKit(kitFingerprint)
|
||||||
val recipe = JSONObject(File(kit, "kit.json").readText())
|
val kitJson = JSONObject(File(kit, "kit.json").readText())
|
||||||
if (recipe.optInt("schema") != KIT_SCHEMA) {
|
if (kitJson.optInt("schema") != KIT_SCHEMA) {
|
||||||
return "This app's game kit is version ${recipe.optInt("schema")}, which this builder does not know."
|
return "This app's game kit is version ${kitJson.optInt("schema")}, which this builder does not know."
|
||||||
}
|
}
|
||||||
|
val recipe = kitJson.getJSONObject("products").optJSONObject(profile.id)
|
||||||
|
?: return "This app's game kit cannot build ${profile.id}."
|
||||||
report(40, Step.Prepare)
|
report(40, Step.Prepare)
|
||||||
downloadNdk(JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/${toolchainManifest.getString("ndk")}").reader().use { it.readText() }))
|
downloadNdk(JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/${toolchainManifest.getString("ndk")}").reader().use { it.readText() }))
|
||||||
|
|
||||||
@@ -141,8 +149,9 @@ object GameBuild {
|
|||||||
log,
|
log,
|
||||||
cancelled = { cancelled() || failure.get() != null },
|
cancelled = { cancelled() || failure.get() != null },
|
||||||
)
|
)
|
||||||
translate(tools, "$kitFingerprint ${toolchainManifest.getString("fingerprint")}")?.let { return it }
|
val buildIdentity = "$kitFingerprint ${toolchainManifest.getString("fingerprint")}"
|
||||||
val objectsBySlot = compile(tools, recipe, toolchainManifest.getString("fingerprint"))
|
translate(tools, buildIdentity)?.let { return it }
|
||||||
|
val objectsBySlot = compile(tools, recipe, buildIdentity)
|
||||||
failure.get()?.let { return it }
|
failure.get()?.let { return it }
|
||||||
val library = link(tools, recipe, objectsBySlot) ?: return failure.get() ?: "Linking the game failed."
|
val library = link(tools, recipe, objectsBySlot) ?: return failure.get() ?: "Linking the game failed."
|
||||||
|
|
||||||
@@ -271,7 +280,10 @@ object GameBuild {
|
|||||||
fun translate(tools: ToolProcess, identity: String): String? {
|
fun translate(tools: ToolProcess, identity: String): String? {
|
||||||
val generated = File(workspace, "generated")
|
val generated = File(workspace, "generated")
|
||||||
val provenance = File(generated, "translation-provenance.txt")
|
val provenance = File(generated, "translation-provenance.txt")
|
||||||
val expected = "$identity ${BuildConfig.DISC_DOL_SHA256} ${BuildConfig.DISC_REL_SHA256}"
|
// A modded game needs a base translation that knows this Code.pul, so the pack's own
|
||||||
|
// identity is part of what the stored translation is reused for.
|
||||||
|
val modIdentity = if (profile.modPack) BuildRecipe.hex(sha256(GameStorage.modCodePul(context))) else ""
|
||||||
|
val expected = "$identity ${profile.id} ${BuildConfig.DISC_DOL_SHA256} ${BuildConfig.DISC_REL_SHA256} $modIdentity"
|
||||||
val shards = File(generated, "build_shards/shards.cmake")
|
val shards = File(generated, "build_shards/shards.cmake")
|
||||||
if (provenance.isFile && provenance.readText() == expected && shards.isFile) {
|
if (provenance.isFile && provenance.readText() == expected && shards.isFile) {
|
||||||
log.line("Reusing the translation of an earlier build")
|
log.line("Reusing the translation of an earlier build")
|
||||||
@@ -286,9 +298,17 @@ object GameBuild {
|
|||||||
File(disc, DiscChecks.DOL_PATH).copyTo(File(workspace, "Assets/main.dol"), overwrite = true)
|
File(disc, DiscChecks.DOL_PATH).copyTo(File(workspace, "Assets/main.dol"), overwrite = true)
|
||||||
File(disc, DiscChecks.REL_PATH).copyTo(File(workspace, "Assets/StaticR.rel"), overwrite = true)
|
File(disc, DiscChecks.REL_PATH).copyTo(File(workspace, "Assets/StaticR.rel"), overwrite = true)
|
||||||
val manifest = "projects/mkwii/recomp.yml"
|
val manifest = "projects/mkwii/recomp.yml"
|
||||||
val entryPoint = BuildRecipe.entryPoint(File(workspace, manifest).readText())
|
val manifestText = File(workspace, manifest).readText()
|
||||||
|
val entryPoint = BuildRecipe.entryPoint(manifestText)
|
||||||
|
// The base translation only knows a mod's patches when the profile's Code.pul sits
|
||||||
|
// where recomp.yml's mod_root says, so the pack's copy is staged there first.
|
||||||
|
val steps = if (profile.modPack) 4 else 3
|
||||||
|
if (profile.modPack) {
|
||||||
|
val modRoot = File(workspace, BuildRecipe.modRoot(manifestText))
|
||||||
|
GameStorage.modCodePul(context).copyTo(File(modRoot, "Binaries/Code.pul"), overwrite = true)
|
||||||
|
}
|
||||||
|
|
||||||
report(50, Step.Translate, 1, 3)
|
report(50, Step.Translate, 1, steps)
|
||||||
val timing = Regex("""\(t=([0-9.]+)s\)""")
|
val timing = Regex("""\(t=([0-9.]+)s\)""")
|
||||||
translator(
|
translator(
|
||||||
tools, "translating the game",
|
tools, "translating the game",
|
||||||
@@ -299,22 +319,66 @@ object GameBuild {
|
|||||||
) { line ->
|
) { line ->
|
||||||
timing.find(line)?.groupValues?.get(1)?.toDoubleOrNull()?.let { seconds ->
|
timing.find(line)?.groupValues?.get(1)?.toDoubleOrNull()?.let { seconds ->
|
||||||
val fraction = minOf(seconds / EXPECTED_TRANSLATION_SECONDS, 0.97)
|
val fraction = minOf(seconds / EXPECTED_TRANSLATION_SECONDS, 0.97)
|
||||||
reporter.update(50 + (320 * fraction).toInt(), Step.Translate, 1, 3)
|
reporter.update(50 + (300 * fraction).toInt(), Step.Translate, 1, steps)
|
||||||
}
|
}
|
||||||
}?.let { return it }
|
}?.let { return it }
|
||||||
report(370, Step.Translate, 2, 3)
|
|
||||||
translator(tools, "generating the game data", "generate-data-init", "--project", manifest, "--target-os", "android")?.let { return it }
|
// Retro Rewind's own code: its Code.pul translated against the base translation, as
|
||||||
report(385, Step.Translate, 3, 3)
|
// Launcher/LocalBuild.ps1 does on a PC. Online play needs a payload this cannot fetch.
|
||||||
|
val modOutput = "build/mods/retro_rewind_full_cpp"
|
||||||
|
if (profile.modPack) {
|
||||||
|
report(350, Step.Translate, 2, steps)
|
||||||
translator(
|
translator(
|
||||||
tools, "preparing the build",
|
tools, "creating the base manifest",
|
||||||
|
"emit-base-manifest", "--project", manifest, "--out", "build/base",
|
||||||
|
"--functions-dir", "generated/functions",
|
||||||
|
"--translation-output-metadata", "generated/base_translation_output.json", "--region", "P",
|
||||||
|
)?.let { return it }
|
||||||
|
translator(
|
||||||
|
tools, "translating Retro Rewind",
|
||||||
|
"translate-mod", "--project", manifest, "--profile", "retro-rewind",
|
||||||
|
"--base-manifest", "build/base/mkwii_base_manifest.json",
|
||||||
|
"--base-translation-output-metadata", "generated/base_translation_output.json",
|
||||||
|
"--code-pul", GameStorage.modCodePul(context).absolutePath,
|
||||||
|
"--mod-root", GameStorage.modDirectory(context).absolutePath,
|
||||||
|
"--mod-name", "Retro Rewind", "--region", "P", "--out", modOutput,
|
||||||
|
"--prefer-cached-inputs", "--emit-cpp", "--skip-retro-wfc",
|
||||||
|
"--threads", TRANSLATOR_THREADS.toString(),
|
||||||
|
)?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
report(370, Step.Translate, steps - 1, steps)
|
||||||
|
translator(tools, "generating the game data", "generate-data-init", "--project", manifest, "--target-os", "android")?.let { return it }
|
||||||
|
report(385, Step.Translate, steps, steps)
|
||||||
|
val shardArguments = mutableListOf(
|
||||||
"emit-build-shards", "--project", manifest, "--base-metadata", "generated/base_translation_output.json",
|
"emit-build-shards", "--project", manifest, "--base-metadata", "generated/base_translation_output.json",
|
||||||
"--base-functions-dir", "generated/functions", "--native-source-dir", "runtime/src", "--out", "generated/build_shards",
|
"--base-functions-dir", "generated/functions", "--native-source-dir", "runtime/src", "--out", "generated/build_shards",
|
||||||
)?.let { return it }
|
)
|
||||||
|
if (profile.modPack) {
|
||||||
|
shardArguments += listOf(
|
||||||
|
"--resolved-profile", "$modOutput/resolved_dispatch_profile.json",
|
||||||
|
"--retro-cpp-dir", "$modOutput/cpp",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
translator(tools, "preparing the build", *shardArguments.toTypedArray())?.let { return it }
|
||||||
provenance.writeText(expected)
|
provenance.writeText(expected)
|
||||||
report(400, Step.Translate, 3, 3)
|
report(400, Step.Translate, steps, steps)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sha256(file: File): ByteArray {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
file.inputStream().use { input ->
|
||||||
|
val buffer = ByteArray(1 shl 20)
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read < 0) break
|
||||||
|
digest.update(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return digest.digest()
|
||||||
|
}
|
||||||
|
|
||||||
fun translator(tools: ToolProcess, what: String, vararg arguments: String, onLine: (String) -> Unit = {}): String? {
|
fun translator(tools: ToolProcess, what: String, vararg arguments: String, onLine: (String) -> Unit = {}): String? {
|
||||||
log.line("== translator ${arguments.first()}")
|
log.line("== translator ${arguments.first()}")
|
||||||
val result = tools.run(
|
val result = tools.run(
|
||||||
@@ -332,11 +396,12 @@ object GameBuild {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Compiles every generated source; returns their objects by link slot (runtime, product, translated). */
|
/** Compiles every generated source; returns their objects by link slot (runtime, product, translated). */
|
||||||
fun compile(tools: ToolProcess, recipe: JSONObject, toolchainFingerprint: String): Map<String, List<String>> {
|
// The identity is the kit's fingerprint and the toolchain's: the kit is fingerprinted as a
|
||||||
|
// whole, and each game already compiles in its own work directory.
|
||||||
|
fun compile(tools: ToolProcess, recipe: JSONObject, identity: String): Map<String, List<String>> {
|
||||||
val generated = File(workspace, "generated")
|
val generated = File(workspace, "generated")
|
||||||
val shards = File(generated, "build_shards/shards.cmake").readText()
|
val shards = File(generated, "build_shards/shards.cmake").readText()
|
||||||
val stamp = File(objects, ".stamp")
|
val stamp = File(objects, ".stamp")
|
||||||
val identity = "${recipe.getString("fingerprint")} $toolchainFingerprint"
|
|
||||||
if (stamp.takeIf { it.isFile }?.readText() != identity) {
|
if (stamp.takeIf { it.isFile }?.readText() != identity) {
|
||||||
objects.deleteRecursively()
|
objects.deleteRecursively()
|
||||||
objects.mkdirs()
|
objects.mkdirs()
|
||||||
@@ -481,7 +546,7 @@ object GameBuild {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun install(library: File, kitFingerprint: String): String? {
|
fun install(library: File, kitFingerprint: String): String? {
|
||||||
val destination = GameLibrary.directory(context)
|
val destination = GameLibrary.directory(context, profile)
|
||||||
val staging = File(destination.parentFile, "${destination.name}.building")
|
val staging = File(destination.parentFile, "${destination.name}.building")
|
||||||
staging.deleteRecursively()
|
staging.deleteRecursively()
|
||||||
staging.mkdirs()
|
staging.mkdirs()
|
||||||
@@ -500,7 +565,7 @@ object GameBuild {
|
|||||||
val builtAt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date())
|
val builtAt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date())
|
||||||
val manifest = JSONObject()
|
val manifest = JSONObject()
|
||||||
.put("schema", 1)
|
.put("schema", 1)
|
||||||
.put("profile", BuildConfig.PROFILE)
|
.put("profile", profile.id)
|
||||||
.put("gameId", BuildConfig.DISC_GAME_ID)
|
.put("gameId", BuildConfig.DISC_GAME_ID)
|
||||||
.put("dolSha256", BuildConfig.DISC_DOL_SHA256)
|
.put("dolSha256", BuildConfig.DISC_DOL_SHA256)
|
||||||
.put("relSha256", BuildConfig.DISC_REL_SHA256)
|
.put("relSha256", BuildConfig.DISC_REL_SHA256)
|
||||||
|
|||||||
@@ -15,22 +15,25 @@ import java.util.zip.ZipInputStream
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.wiicompiled.quest.BuildConfig
|
import org.wiicompiled.quest.BuildConfig
|
||||||
import org.wiicompiled.quest.GameLibrary
|
import org.wiicompiled.quest.GameLibrary
|
||||||
|
import org.wiicompiled.quest.GameProfile
|
||||||
import org.wiicompiled.quest.GameStorage
|
import org.wiicompiled.quest.GameStorage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A .wcgame built on a PC (android/Build-QuestGame.ps1, the PC launcher's "Build for Quest")
|
* A .wcgame built on a PC (android/Build-QuestGame.ps1, the PC launcher's "Build for Quest")
|
||||||
* becomes this headset's game: libmain.so and game.json, and DATA too when the package carries
|
* becomes one of this headset's games: libmain.so and game.json, plus DATA when the package
|
||||||
* the extracted disc.
|
* carries the extracted disc and the Retro Rewind pack when it carries that.
|
||||||
*
|
*
|
||||||
* The package is only accepted for the disc and app it was built for: game.json must name the
|
* The package says which game it is (`profile`), so importing one installs that game and leaves
|
||||||
* pinned game and hashes, the kit this APK carries, and a library whose hash matches the bytes
|
* the other alone. It is only accepted for the disc and app it was built for: game.json must name
|
||||||
* in the package. Both parts are staged and checked before either replaces what is installed.
|
* the pinned game and hashes, the kit this APK carries, and a library whose hash matches the bytes
|
||||||
|
* in the package. Every part is staged and checked before any of it replaces what is installed.
|
||||||
*/
|
*/
|
||||||
object GamePackageImport {
|
object GamePackageImport {
|
||||||
|
|
||||||
private const val TAG = "WiiCompiledLauncher"
|
private const val TAG = "WiiCompiledLauncher"
|
||||||
private const val SCHEMA = 1
|
private const val SCHEMA = 1
|
||||||
private const val DATA_PREFIX = "DATA/"
|
private const val DATA_PREFIX = "DATA/"
|
||||||
|
private const val MOD_PREFIX = "MOD/"
|
||||||
private const val FREE_SPACE_MARGIN = 256L * 1024 * 1024
|
private const val FREE_SPACE_MARGIN = 256L * 1024 * 1024
|
||||||
|
|
||||||
/** Null on success, otherwise the message to show. */
|
/** Null on success, otherwise the message to show. */
|
||||||
@@ -42,21 +45,37 @@ object GamePackageImport {
|
|||||||
return "Not enough free space: importing needs ${GameFiles.gigabytes(total + FREE_SPACE_MARGIN)}, and ${GameFiles.gigabytes(available)} is free."
|
return "Not enough free space: importing needs ${GameFiles.gigabytes(total + FREE_SPACE_MARGIN)}, and ${GameFiles.gigabytes(available)} is free."
|
||||||
}
|
}
|
||||||
|
|
||||||
val libraryDir = GameLibrary.directory(context)
|
// The game's destination is only known once game.json has been read, so the library is
|
||||||
val libraryStaging = File(libraryDir.parentFile, "${libraryDir.name}.importing")
|
// staged under a name of its own first.
|
||||||
|
val libraryStaging = File(context.filesDir, "game/.importing")
|
||||||
val dataStaging = File(gameRoot, "DATA.importing")
|
val dataStaging = File(gameRoot, "DATA.importing")
|
||||||
libraryStaging.deleteRecursively()
|
val modStaging = File(gameRoot, "${GameStorage.MOD_DIRECTORY}.importing")
|
||||||
dataStaging.deleteRecursively()
|
for (staging in listOf(libraryStaging, dataStaging, modStaging)) staging.deleteRecursively()
|
||||||
try {
|
try {
|
||||||
libraryStaging.mkdirs()
|
libraryStaging.mkdirs()
|
||||||
var manifestText: String? = null
|
var manifestText: String? = null
|
||||||
var librarySha256: String? = null
|
var librarySha256: String? = null
|
||||||
var hasData = false
|
var hasData = false
|
||||||
|
var hasMod = false
|
||||||
|
|
||||||
val counted = CountingInputStream(FileInputStream(descriptor.fileDescriptor))
|
val counted = CountingInputStream(FileInputStream(descriptor.fileDescriptor))
|
||||||
ZipInputStream(BufferedInputStream(counted, 1 shl 20)).use { zip ->
|
ZipInputStream(BufferedInputStream(counted, 1 shl 20)).use { zip ->
|
||||||
val buffer = ByteArray(1 shl 20)
|
val buffer = ByteArray(1 shl 20)
|
||||||
var lastReport = 0L
|
var lastReport = 0L
|
||||||
|
fun extract(target: File) {
|
||||||
|
target.parentFile?.mkdirs()
|
||||||
|
target.outputStream().use { out ->
|
||||||
|
while (true) {
|
||||||
|
val read = zip.read(buffer)
|
||||||
|
if (read < 0) break
|
||||||
|
out.write(buffer, 0, read)
|
||||||
|
if (counted.count - lastReport >= 8L shl 20) {
|
||||||
|
lastReport = counted.count
|
||||||
|
if (!progress.update(counted.count, total)) throw InterruptedIOException("Import cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
val entry = zip.nextEntry ?: break
|
val entry = zip.nextEntry ?: break
|
||||||
val name = entry.name
|
val name = entry.name
|
||||||
@@ -80,19 +99,11 @@ object GamePackageImport {
|
|||||||
}
|
}
|
||||||
name.startsWith(DATA_PREFIX) -> {
|
name.startsWith(DATA_PREFIX) -> {
|
||||||
hasData = true
|
hasData = true
|
||||||
val target = File(dataStaging, name.removePrefix(DATA_PREFIX))
|
extract(File(dataStaging, name.removePrefix(DATA_PREFIX)))
|
||||||
target.parentFile?.mkdirs()
|
|
||||||
target.outputStream().use { out ->
|
|
||||||
while (true) {
|
|
||||||
val read = zip.read(buffer)
|
|
||||||
if (read < 0) break
|
|
||||||
out.write(buffer, 0, read)
|
|
||||||
if (counted.count - lastReport >= 8L shl 20) {
|
|
||||||
lastReport = counted.count
|
|
||||||
if (!progress.update(counted.count, total)) throw InterruptedIOException("Import cancelled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
name.startsWith(MOD_PREFIX) -> {
|
||||||
|
hasMod = true
|
||||||
|
extract(File(modStaging, name.removePrefix(MOD_PREFIX)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!progress.update(counted.count, total)) throw InterruptedIOException("Import cancelled")
|
if (!progress.update(counted.count, total)) throw InterruptedIOException("Import cancelled")
|
||||||
@@ -102,24 +113,34 @@ object GamePackageImport {
|
|||||||
finishing()
|
finishing()
|
||||||
val manifestJson = manifestText?.let { runCatching { JSONObject(it) }.getOrNull() }
|
val manifestJson = manifestText?.let { runCatching { JSONObject(it) }.getOrNull() }
|
||||||
?: return "This is not a WiiCompiled game file (game.json is missing)."
|
?: return "This is not a WiiCompiled game file (game.json is missing)."
|
||||||
|
val profile = GameProfile.of(manifestJson.optString("profile"))
|
||||||
|
?.takeIf { it in GameProfile.available(context) }
|
||||||
|
?: return "This game file is for ${manifestJson.optString("profile")}, which this app does not carry a kit for."
|
||||||
manifestError(context, manifestJson, librarySha256)?.let { return it }
|
manifestError(context, manifestJson, librarySha256)?.let { return it }
|
||||||
if (hasData) {
|
if (hasData) {
|
||||||
GameFiles.validateData(dataStaging)?.let { return it }
|
GameFiles.validateData(dataStaging)?.let { return it }
|
||||||
} else if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) {
|
} else if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) {
|
||||||
return "This game file has no game files (DATA). Build it again with the disc files included, or select your disc image first."
|
return "This game file has no game files (DATA). Build it again with the disc files included, or select your disc image first."
|
||||||
}
|
}
|
||||||
|
if (hasMod && !File(modStaging, "Binaries/Code.pul").isFile) {
|
||||||
|
return "The Retro Rewind pack in this game file is incomplete (no Binaries/Code.pul)."
|
||||||
|
}
|
||||||
|
|
||||||
File(libraryStaging, GameLibrary.MANIFEST_NAME).writeText(manifestText!!)
|
File(libraryStaging, GameLibrary.MANIFEST_NAME).writeText(manifestText!!)
|
||||||
if (hasData) {
|
if (hasData) {
|
||||||
GameFiles.replace(GameStorage.discDirectory(context), dataStaging)?.let { return it }
|
GameFiles.replace(GameStorage.discDirectory(context), dataStaging)?.let { return it }
|
||||||
}
|
}
|
||||||
|
if (hasMod) {
|
||||||
|
GameFiles.replace(GameStorage.modDirectory(context), modStaging)?.let { return it }
|
||||||
|
}
|
||||||
|
val libraryDir = GameLibrary.directory(context, profile)
|
||||||
libraryDir.parentFile?.mkdirs()
|
libraryDir.parentFile?.mkdirs()
|
||||||
GameFiles.replace(libraryDir, libraryStaging)?.let { return it }
|
GameFiles.replace(libraryDir, libraryStaging)?.let { return it }
|
||||||
Log.i(TAG, "Imported the game built by ${manifestJson.optString("builtBy")} at ${manifestJson.optString("builtAt")}")
|
Log.i(TAG, "Imported ${profile.id} built by ${manifestJson.optString("builtBy")} at ${manifestJson.optString("builtAt")}")
|
||||||
|
GameProfile.select(context, profile)
|
||||||
return null
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
libraryStaging.deleteRecursively()
|
for (staging in listOf(libraryStaging, dataStaging, modStaging)) staging.deleteRecursively()
|
||||||
dataStaging.deleteRecursively()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +148,6 @@ object GamePackageImport {
|
|||||||
if (manifest.optInt("schema") != SCHEMA) {
|
if (manifest.optInt("schema") != SCHEMA) {
|
||||||
return "This game file comes from a newer or older launcher (format ${manifest.optInt("schema")}). Build it again with the current PC launcher."
|
return "This game file comes from a newer or older launcher (format ${manifest.optInt("schema")}). Build it again with the current PC launcher."
|
||||||
}
|
}
|
||||||
if (manifest.optString("profile") != BuildConfig.PROFILE) {
|
|
||||||
return "This game file is for ${manifest.optString("profile")}, but this app is ${BuildConfig.PROFILE}."
|
|
||||||
}
|
|
||||||
if (!manifest.optString("gameId").equals(BuildConfig.DISC_GAME_ID, ignoreCase = true) ||
|
if (!manifest.optString("gameId").equals(BuildConfig.DISC_GAME_ID, ignoreCase = true) ||
|
||||||
!manifest.optString("dolSha256").equals(BuildConfig.DISC_DOL_SHA256, ignoreCase = true) ||
|
!manifest.optString("dolSha256").equals(BuildConfig.DISC_DOL_SHA256, ignoreCase = true) ||
|
||||||
!manifest.optString("relSha256").equals(BuildConfig.DISC_REL_SHA256, ignoreCase = true)
|
!manifest.optString("relSha256").equals(BuildConfig.DISC_REL_SHA256, ignoreCase = true)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.os.Looper
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
|
import org.wiicompiled.quest.GameProfile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The long-running steps that make the game playable from what the player owns, one at a time,
|
* The long-running steps that make the game playable from what the player owns, one at a time,
|
||||||
@@ -87,7 +88,7 @@ object GameSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The whole task, on a worker thread. Always ends in Done, Failed or Cancelled. [uri] is null only for a build. */
|
/** The whole task, on a worker thread. Always ends in Done, Failed or Cancelled. [uri] is null only for a build. */
|
||||||
fun run(context: Context, task: Task, uri: Uri?, deleteSource: Boolean) {
|
fun run(context: Context, task: Task, uri: Uri?, deleteSource: Boolean, profile: GameProfile = GameProfile.selected(context)) {
|
||||||
val progress = Progress { done, total ->
|
val progress = Progress { done, total ->
|
||||||
publish(State.Working(task, done, total))
|
publish(State.Working(task, done, total))
|
||||||
!cancelRequested
|
!cancelRequested
|
||||||
@@ -99,7 +100,7 @@ object GameSetup {
|
|||||||
publish(State.Working(task, permille.toLong(), 1000, step, done, total))
|
publish(State.Working(task, permille.toLong(), 1000, step, done, total))
|
||||||
!cancelRequested
|
!cancelRequested
|
||||||
}
|
}
|
||||||
GameBuild.run(context, reporter, cancelled = { cancelRequested }, finishing)
|
GameBuild.run(context, profile, reporter, cancelled = { cancelRequested }, finishing)
|
||||||
} else {
|
} else {
|
||||||
uri?.let { context.contentResolver.openFileDescriptor(it, "r") }?.use { descriptor ->
|
uri?.let { context.contentResolver.openFileDescriptor(it, "r") }?.use { descriptor ->
|
||||||
when (task) {
|
when (task) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.os.PowerManager
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import org.wiicompiled.quest.GameProfile
|
||||||
import org.wiicompiled.quest.R
|
import org.wiicompiled.quest.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,13 +52,14 @@ class GameSetupService : Service() {
|
|||||||
}
|
}
|
||||||
GameSetup.addListener(listener)
|
GameSetup.addListener(listener)
|
||||||
val deleteSource = intent?.getBooleanExtra(EXTRA_DELETE_SOURCE, false) ?: false
|
val deleteSource = intent?.getBooleanExtra(EXTRA_DELETE_SOURCE, false) ?: false
|
||||||
|
val profile = GameProfile.of(intent?.getStringExtra(EXTRA_PROFILE)) ?: GameProfile.selected(this)
|
||||||
|
|
||||||
val wakeLock = getSystemService(PowerManager::class.java)
|
val wakeLock = getSystemService(PowerManager::class.java)
|
||||||
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WiiCompiled:GameSetup")
|
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WiiCompiled:GameSetup")
|
||||||
wakeLock.acquire(if (task == GameSetup.Task.BuildGame) BUILD_WAKE_LOCK_TIMEOUT_MS else WAKE_LOCK_TIMEOUT_MS)
|
wakeLock.acquire(if (task == GameSetup.Task.BuildGame) BUILD_WAKE_LOCK_TIMEOUT_MS else WAKE_LOCK_TIMEOUT_MS)
|
||||||
thread(name = "GameSetup") {
|
thread(name = "GameSetup") {
|
||||||
try {
|
try {
|
||||||
GameSetup.run(applicationContext, task, uri, deleteSource)
|
GameSetup.run(applicationContext, task, uri, deleteSource, profile)
|
||||||
} finally {
|
} finally {
|
||||||
uri?.let(::releaseReadPermission)
|
uri?.let(::releaseReadPermission)
|
||||||
if (wakeLock.isHeld) wakeLock.release()
|
if (wakeLock.isHeld) wakeLock.release()
|
||||||
@@ -115,21 +117,24 @@ class GameSetupService : Service() {
|
|||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
private const val NOTIFY_INTERVAL_MS = 1000L
|
private const val NOTIFY_INTERVAL_MS = 1000L
|
||||||
private const val EXTRA_TASK = "task"
|
private const val EXTRA_TASK = "task"
|
||||||
|
private const val EXTRA_PROFILE = "profile"
|
||||||
private const val EXTRA_DELETE_SOURCE = "deleteSource"
|
private const val EXTRA_DELETE_SOURCE = "deleteSource"
|
||||||
// Longer than any real task; only a hung run would reach it.
|
// Longer than any real task; only a hung run would reach it.
|
||||||
private const val WAKE_LOCK_TIMEOUT_MS = 60L * 60 * 1000
|
private const val WAKE_LOCK_TIMEOUT_MS = 60L * 60 * 1000
|
||||||
private const val BUILD_WAKE_LOCK_TIMEOUT_MS = 3L * 60 * 60 * 1000
|
private const val BUILD_WAKE_LOCK_TIMEOUT_MS = 3L * 60 * 60 * 1000
|
||||||
|
|
||||||
|
/** Builds [profile]'s game from DATA on this headset ([GameBuild]). */
|
||||||
|
fun startBuild(context: Context, profile: GameProfile) {
|
||||||
|
val intent = Intent(context, GameSetupService::class.java)
|
||||||
|
.putExtra(EXTRA_TASK, GameSetup.Task.BuildGame.name)
|
||||||
|
.putExtra(EXTRA_PROFILE, profile.id)
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts [task] on [uri]: a document the player picked, or a file in the Import folder,
|
* Starts [task] on [uri]: a document the player picked, or a file in the Import folder,
|
||||||
* which [deleteSource] removes once it has been imported.
|
* which [deleteSource] removes once it has been imported.
|
||||||
*/
|
*/
|
||||||
/** Builds the game from DATA on this headset ([GameBuild]). */
|
|
||||||
fun startBuild(context: Context) {
|
|
||||||
val intent = Intent(context, GameSetupService::class.java).putExtra(EXTRA_TASK, GameSetup.Task.BuildGame.name)
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start(context: Context, task: GameSetup.Task, uri: Uri, deleteSource: Boolean = false) {
|
fun start(context: Context, task: GameSetup.Task, uri: Uri, deleteSource: Boolean = false) {
|
||||||
if (uri.scheme == "content") {
|
if (uri.scheme == "content") {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import android.os.Bundle
|
|||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import org.wiicompiled.quest.BuildConfig
|
import org.wiicompiled.quest.BuildConfig
|
||||||
import org.wiicompiled.quest.GameLibrary
|
import org.wiicompiled.quest.GameLibrary
|
||||||
|
import org.wiicompiled.quest.GameProfile
|
||||||
import org.wiicompiled.quest.GameStorage
|
import org.wiicompiled.quest.GameStorage
|
||||||
import org.wiicompiled.quest.QuestActivity
|
import org.wiicompiled.quest.QuestActivity
|
||||||
import org.wiicompiled.quest.R
|
import org.wiicompiled.quest.R
|
||||||
@@ -56,8 +59,14 @@ class LauncherActivity : Activity() {
|
|||||||
private lateinit var dataBanner: View
|
private lateinit var dataBanner: View
|
||||||
private lateinit var dataBannerIcon: ImageView
|
private lateinit var dataBannerIcon: ImageView
|
||||||
private lateinit var dataBannerText: TextView
|
private lateinit var dataBannerText: TextView
|
||||||
|
private lateinit var homeTitle: TextView
|
||||||
|
private lateinit var gameToggle: LinearLayout
|
||||||
private lateinit var settings: SettingsPage
|
private lateinit var settings: SettingsPage
|
||||||
|
|
||||||
|
/** The games this APK carries a kit for, and the one the player picked. */
|
||||||
|
private val profiles: List<GameProfile> by lazy { GameProfile.available(this) }
|
||||||
|
private var profile = GameProfile.Base
|
||||||
|
|
||||||
private var page = Page.Home
|
private var page = Page.Home
|
||||||
private var launching = false
|
private var launching = false
|
||||||
private var trailsAway = true
|
private var trailsAway = true
|
||||||
@@ -99,9 +108,9 @@ class LauncherActivity : Activity() {
|
|||||||
dataBannerIcon = findViewById(R.id.home_data_banner_icon)
|
dataBannerIcon = findViewById(R.id.home_data_banner_icon)
|
||||||
dataBannerText = findViewById(R.id.home_data_banner_text)
|
dataBannerText = findViewById(R.id.home_data_banner_text)
|
||||||
|
|
||||||
findViewById<TextView>(R.id.home_title).setText(
|
homeTitle = findViewById(R.id.home_title)
|
||||||
if (BuildConfig.PROFILE == "retro_rewind") R.string.home_title_retro_rewind else R.string.home_title_base,
|
gameToggle = findViewById(R.id.home_game_toggle)
|
||||||
)
|
buildGameToggle()
|
||||||
findViewById<TextView>(R.id.launcher_version).text = getString(R.string.launcher_version, BuildConfig.VERSION_NAME)
|
findViewById<TextView>(R.id.launcher_version).text = getString(R.string.launcher_version, BuildConfig.VERSION_NAME)
|
||||||
|
|
||||||
settings = SettingsPage(
|
settings = SettingsPage(
|
||||||
@@ -131,13 +140,15 @@ class LauncherActivity : Activity() {
|
|||||||
!GameSetup.isRunning && GameStorage.discStatus(this) == GameStorage.DiscStatus.Ready
|
!GameSetup.isRunning && GameStorage.discStatus(this) == GameStorage.DiscStatus.Ready
|
||||||
) {
|
) {
|
||||||
Log.i(TAG, "Starting a game build requested over adb")
|
Log.i(TAG, "Starting a game build requested over adb")
|
||||||
GameSetupService.startBuild(this)
|
GameSetupService.startBuild(this, GameProfile.selected(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
launching = false
|
launching = false
|
||||||
|
// An import can install the other game and select it, so the toggle follows the file.
|
||||||
|
profile = GameProfile.selected(this)
|
||||||
importDroppedPackage()
|
importDroppedPackage()
|
||||||
setupKind = GameSetup.state.javaClass
|
setupKind = GameSetup.state.javaClass
|
||||||
refresh()
|
refresh()
|
||||||
@@ -198,7 +209,11 @@ class LauncherActivity : Activity() {
|
|||||||
private fun refreshHome() {
|
private fun refreshHome() {
|
||||||
val disc = GameStorage.discDirectory(this).absolutePath
|
val disc = GameStorage.discDirectory(this).absolutePath
|
||||||
val discStatus = GameStorage.discStatus(this)
|
val discStatus = GameStorage.discStatus(this)
|
||||||
val gameStatus = GameLibrary.status(this)
|
val gameStatus = GameLibrary.status(this, profile)
|
||||||
|
homeTitle.setText(profile.title)
|
||||||
|
for ((index, entry) in profiles.withIndex()) {
|
||||||
|
gameToggle.getChildAt(index).isSelected = entry == profile
|
||||||
|
}
|
||||||
val running = isGameRunning()
|
val running = isGameRunning()
|
||||||
val setup = GameSetup.state
|
val setup = GameSetup.state
|
||||||
val settingUp = GameSetup.isRunning
|
val settingUp = GameSetup.isRunning
|
||||||
@@ -211,7 +226,7 @@ class LauncherActivity : Activity() {
|
|||||||
mainAction = when {
|
mainAction = when {
|
||||||
running -> Action.Resume
|
running -> Action.Resume
|
||||||
discStatus != GameStorage.DiscStatus.Ready -> Action.SelectDisc
|
discStatus != GameStorage.DiscStatus.Ready -> Action.SelectDisc
|
||||||
gameStatus != GameLibrary.Status.Ready -> if (BuildConfig.ON_DEVICE_BUILD) Action.BuildGame else Action.ImportGame
|
gameStatus != GameLibrary.Status.Ready -> Action.BuildGame
|
||||||
else -> Action.Play
|
else -> Action.Play
|
||||||
}
|
}
|
||||||
secondaryAction = when {
|
secondaryAction = when {
|
||||||
@@ -280,14 +295,12 @@ 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 && 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)
|
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)
|
discStatus == GameStorage.DiscStatus.Incomplete -> showBanner(getString(R.string.home_data_incomplete, disc), warning = true)
|
||||||
!GameStorage.modContentReady(this) ->
|
!GameStorage.modContentReady(this, profile) ->
|
||||||
showBanner(getString(R.string.home_mod_missing, GameStorage.modDirectory(this).absolutePath), warning = true)
|
showBanner(getString(R.string.home_mod_missing, GameStorage.modDirectory(this).absolutePath), warning = true)
|
||||||
gameStatus == GameLibrary.Status.Stale ->
|
gameStatus == GameLibrary.Status.Stale -> showBanner(getString(R.string.home_game_stale), warning = true)
|
||||||
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 ->
|
gameStatus == GameLibrary.Status.Missing && discStatus == GameStorage.DiscStatus.Missing ->
|
||||||
showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_setup_intro else R.string.home_setup_intro_pc), warning = false)
|
showBanner(getString(R.string.home_setup_intro), warning = false)
|
||||||
gameStatus == GameLibrary.Status.Missing ->
|
gameStatus == GameLibrary.Status.Missing -> showBanner(getString(R.string.home_game_missing), warning = false)
|
||||||
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)
|
discStatus == GameStorage.DiscStatus.Missing -> showBanner(getString(R.string.home_data_missing, disc), warning = false)
|
||||||
else -> showBanner(null)
|
else -> showBanner(null)
|
||||||
}
|
}
|
||||||
@@ -298,6 +311,40 @@ class LauncherActivity : Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Home's game switch, shown only when this APK carries more than one kit. Each game keeps its
|
||||||
|
* own built library, so switching is immediate; only the panel's state changes.
|
||||||
|
*/
|
||||||
|
private fun buildGameToggle() {
|
||||||
|
for (entry in profiles) {
|
||||||
|
val button = TextView(this).apply {
|
||||||
|
setText(entry.title)
|
||||||
|
textSize = 14f
|
||||||
|
setTextColor(getColorStateList(R.color.game_toggle_text))
|
||||||
|
setBackgroundResource(R.drawable.bg_game_toggle)
|
||||||
|
gravity = android.view.Gravity.CENTER
|
||||||
|
// One line: the pill has a fixed height, so a wrapped "Retro Rewind" loses half of itself.
|
||||||
|
isSingleLine = true
|
||||||
|
setPadding(10.dp(), 0, 10.dp(), 0)
|
||||||
|
isClickable = true
|
||||||
|
isFocusable = true
|
||||||
|
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 38.dp())
|
||||||
|
setOnClickListener { selectProfile(entry) }
|
||||||
|
}
|
||||||
|
gameToggle.addView(button)
|
||||||
|
}
|
||||||
|
gameToggle.visibility = if (profiles.size > 1) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.dp(): Int = (this * resources.displayMetrics.density).toInt()
|
||||||
|
|
||||||
|
private fun selectProfile(target: GameProfile) {
|
||||||
|
if (target == profile || GameSetup.isRunning) return
|
||||||
|
profile = target
|
||||||
|
GameProfile.select(this, target)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
private fun label(action: Action): Int = when (action) {
|
private fun label(action: Action): Int = when (action) {
|
||||||
Action.Play -> R.string.home_play
|
Action.Play -> R.string.home_play
|
||||||
Action.Resume -> R.string.home_resume
|
Action.Resume -> R.string.home_resume
|
||||||
@@ -325,12 +372,20 @@ class LauncherActivity : Activity() {
|
|||||||
GameBuild.Step.Prepare, null -> getString(R.string.build_step_prepare)
|
GameBuild.Step.Prepare, null -> getString(R.string.build_step_prepare)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the game on this headset from DATA, after saying what that takes. */
|
/** Builds the selected game on this headset from DATA, after saying what that takes. */
|
||||||
private fun buildGame() {
|
private fun buildGame() {
|
||||||
if (!BuildConfig.ON_DEVICE_BUILD || GameSetup.isRunning || GameStorage.discStatus(this) != GameStorage.DiscStatus.Ready) return
|
if (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
|
if (!GameStorage.modContentReady(this, profile)) {
|
||||||
|
confirm(R.string.home_mod_needed_title, R.string.home_mod_needed_message, R.string.home_import) { importGame() }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val message = if (GameLibrary.status(this, profile) == 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) {
|
confirm(R.string.home_build_title, message, R.string.home_build) {
|
||||||
GameSetupService.startBuild(this)
|
GameSetupService.startBuild(this, profile)
|
||||||
showPage(Page.Home)
|
showPage(Page.Home)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,7 +417,7 @@ class LauncherActivity : Activity() {
|
|||||||
/** Opens the document picker for a .wcgame, asking first when it would replace the game. */
|
/** Opens the document picker for a .wcgame, asking first when it would replace the game. */
|
||||||
private fun importGame() {
|
private fun importGame() {
|
||||||
if (GameSetup.isRunning) return
|
if (GameSetup.isRunning) return
|
||||||
if (GameLibrary.status(this) != GameLibrary.Status.Ready) {
|
if (GameLibrary.status(this, profile) != GameLibrary.Status.Ready) {
|
||||||
openPicker(REQUEST_GAME_PACKAGE)
|
openPicker(REQUEST_GAME_PACKAGE)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.widget.Toast
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import org.wiicompiled.quest.BuildConfig
|
import org.wiicompiled.quest.BuildConfig
|
||||||
import org.wiicompiled.quest.GameLibrary
|
import org.wiicompiled.quest.GameLibrary
|
||||||
|
import org.wiicompiled.quest.GameProfile
|
||||||
import org.wiicompiled.quest.GameStorage
|
import org.wiicompiled.quest.GameStorage
|
||||||
import org.wiicompiled.quest.R
|
import org.wiicompiled.quest.R
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ class SettingsPage(
|
|||||||
|
|
||||||
private fun buildAbout() {
|
private fun buildAbout() {
|
||||||
section(R.string.section_about_app) {
|
section(R.string.section_about_app) {
|
||||||
info(R.string.about_version, activity.getString(R.string.about_version_value, BuildConfig.VERSION_NAME, BuildConfig.PROFILE))
|
info(R.string.about_version, BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
section(R.string.section_about_storage) {
|
section(R.string.section_about_storage) {
|
||||||
val disc = GameStorage.discDirectory(activity).absolutePath
|
val disc = GameStorage.discDirectory(activity).absolutePath
|
||||||
@@ -273,15 +274,18 @@ class SettingsPage(
|
|||||||
action(R.string.about_extract, R.string.about_extract_helper, R.string.home_select_disc, enabled = !GameSetup.isRunning) {
|
action(R.string.about_extract, R.string.about_extract_helper, R.string.home_select_disc, enabled = !GameSetup.isRunning) {
|
||||||
selectDiscImage()
|
selectDiscImage()
|
||||||
}
|
}
|
||||||
val manifest = GameLibrary.manifest(activity)
|
// One row per game this app carries a kit for, so both are visible at once.
|
||||||
val gameText = when (GameLibrary.status(activity)) {
|
for (profile in GameProfile.available(activity)) {
|
||||||
|
val manifest = GameLibrary.manifest(activity, profile)
|
||||||
|
val gameText = when (GameLibrary.status(activity, profile)) {
|
||||||
GameLibrary.Status.Ready -> manifest?.let {
|
GameLibrary.Status.Ready -> manifest?.let {
|
||||||
activity.getString(R.string.about_game_ready, it.optString("builtBy"), it.optString("builtAt"))
|
activity.getString(R.string.about_game_ready, it.optString("builtBy"), it.optString("builtAt"))
|
||||||
} ?: activity.getString(R.string.about_game_missing)
|
} ?: activity.getString(R.string.about_game_missing)
|
||||||
GameLibrary.Status.Stale -> activity.getString(R.string.about_game_stale)
|
GameLibrary.Status.Stale -> activity.getString(R.string.about_game_stale)
|
||||||
GameLibrary.Status.Missing -> activity.getString(R.string.about_game_missing)
|
GameLibrary.Status.Missing -> activity.getString(R.string.about_game_missing)
|
||||||
}
|
}
|
||||||
info(R.string.about_game, gameText, stacked = true)
|
info(profile.title, gameText, stacked = true)
|
||||||
|
}
|
||||||
if (BuildConfig.ON_DEVICE_BUILD) {
|
if (BuildConfig.ON_DEVICE_BUILD) {
|
||||||
action(
|
action(
|
||||||
R.string.home_build, R.string.about_build_helper, R.string.home_build,
|
R.string.home_build, R.string.about_build_helper, R.string.home_build,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/neutral_950" android:state_selected="true" />
|
||||||
|
<item android:color="@color/neutral_100" android:state_hovered="true" />
|
||||||
|
<item android:color="@color/neutral_300" />
|
||||||
|
</selector>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- One side of Home's game toggle: the selected game is filled, as WheelWizard's game switch is. -->
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/primary_400" />
|
||||||
|
<corners android:radius="999dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/neutral_700" />
|
||||||
|
<corners android:radius="999dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:state_hovered="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/neutral_800" />
|
||||||
|
<corners android:radius="999dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:drawable="@android:color/transparent" />
|
||||||
|
</selector>
|
||||||
@@ -90,6 +90,16 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/home_game_toggle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:background="@drawable/bg_pill"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="3dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/home_title"
|
android:id="@+id/home_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -21,12 +21,11 @@
|
|||||||
<string name="home_put_on_headset">The game opens in the headset.</string>
|
<string name="home_put_on_headset">The game opens in the headset.</string>
|
||||||
<string name="home_data_missing">The game is ready, but its game files (DATA) are missing. Select your own PAL (RMCP01) disc image to extract them, or import a game file from your computer that includes them.\n\nThe game files go to:\n%1$s</string>
|
<string name="home_data_missing">The game is ready, but its game files (DATA) are missing. Select your own PAL (RMCP01) disc image to extract them, or import a game file from your computer that includes them.\n\nThe game files go to:\n%1$s</string>
|
||||||
<string name="home_import">Import from computer</string>
|
<string name="home_import">Import from computer</string>
|
||||||
<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_setup_intro">Nothing from Mario Kart Wii comes with this app: each 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. Retro Rewind also needs its pack, which a Retro Rewind game file can carry.</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_missing">The game files are ready. Now build this 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_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_mod_needed_title">Retro Rewind needs its pack</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_mod_needed_message">Building Retro Rewind needs its pack (the RetroRewind6 folder) on this headset. Import a Retro Rewind game file built with the pack included, or copy the folder next to DATA yourself.</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">Build on this Quest</string>
|
||||||
<string name="home_build_preparing">Preparing…</string>
|
<string name="home_build_preparing">Preparing…</string>
|
||||||
<string name="home_building">Building %1$d%%</string>
|
<string name="home_building">Building %1$d%%</string>
|
||||||
@@ -165,7 +164,6 @@
|
|||||||
<!-- About tab -->
|
<!-- About tab -->
|
||||||
<string name="section_about_app">App</string>
|
<string name="section_about_app">App</string>
|
||||||
<string name="about_version">Version</string>
|
<string name="about_version">Version</string>
|
||||||
<string name="about_version_value">%1$s (%2$s)</string>
|
|
||||||
<string name="section_about_storage">Files</string>
|
<string name="section_about_storage">Files</string>
|
||||||
<string name="about_game_data">Game data</string>
|
<string name="about_game_data">Game data</string>
|
||||||
<string name="about_game_data_ready">Found: %1$s</string>
|
<string name="about_game_data_ready">Found: %1$s</string>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import org.junit.Test
|
|||||||
*/
|
*/
|
||||||
class TomlConfigTest {
|
class TomlConfigTest {
|
||||||
|
|
||||||
|
private val dvdRoot =
|
||||||
|
"dvd_root = \"/storage/emulated/0/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/DATA\""
|
||||||
|
|
||||||
private val sample = """
|
private val sample = """
|
||||||
# WiiCompiled Quest configuration.
|
# WiiCompiled Quest configuration.
|
||||||
[paths]
|
[paths]
|
||||||
@@ -50,6 +53,21 @@ class TomlConfigTest {
|
|||||||
assertEquals(expected, config.text())
|
assertEquals(expected, config.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addsTheRetroRewindPackPathToAConfigThatPredatesIt() {
|
||||||
|
// What GameStorage.prepare does to an install made before the app offered Retro Rewind:
|
||||||
|
// the pack path joins [paths], and nothing else moves.
|
||||||
|
val config = TomlConfig.parse(sample)
|
||||||
|
assertNull(config.string("paths", "retro_rewind_root"))
|
||||||
|
val pack = "/storage/emulated/0/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/RetroRewind6"
|
||||||
|
config.setString("paths", "retro_rewind_root", pack)
|
||||||
|
assertEquals(
|
||||||
|
sample.replace("$dvdRoot\n\n[video]", "$dvdRoot\nretro_rewind_root = \"$pack\"\n\n[video]"),
|
||||||
|
config.text(),
|
||||||
|
)
|
||||||
|
assertEquals(pack, TomlConfig.parse(config.text()).string("paths", "retro_rewind_root"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun appendsAMissingSection() {
|
fun appendsAMissingSection() {
|
||||||
val config = TomlConfig.parse(sample)
|
val config = TomlConfig.parse(sample)
|
||||||
|
|||||||
+58
-32
@@ -223,8 +223,8 @@ The kit is exported from CMake's own build graph, so its flags cannot drift from
|
|||||||
- `runtime/cmake/PublicProducts.cmake` defines `mkw_quest_kit_probe` on Android: WiiCompiled
|
- `runtime/cmake/PublicProducts.cmake` defines `mkw_quest_kit_probe` on Android: WiiCompiled
|
||||||
`WITHOUT_GAME` (no base shards, and the runtime objects `$<FILTER>`ed of the two
|
`WITHOUT_GAME` (no base shards, and the runtime objects `$<FILTER>`ed of the two
|
||||||
disc-generated sources, which skip the unity build on Android for that reason), linked with the
|
disc-generated sources, which skip the unity build on Android for that reason), linked with the
|
||||||
game's symbols unresolved. The base flavour builds this probe instead of the product.
|
game's symbols unresolved. The app builds this probe instead of the product.
|
||||||
- `android/QuestGameKit.psm1` (`Export-QuestGameKit`, run by the `exportBase*QuestGameKit`
|
- `android/QuestGameKit.psm1` (`Export-QuestGameKit`, run by the `export*QuestGameKit`
|
||||||
Gradle tasks) turns the probe's ninja link edge into `kit.json`'s ordered link inputs. It adds
|
Gradle tasks) turns the probe's ninja link edge into `kit.json`'s ordered link inputs. It adds
|
||||||
`{game:runtime}`, `{game:product}` and `{game:translated}` markers where WiiCompiled had those
|
`{game:runtime}`, `{game:product}` and `{game:translated}` markers where WiiCompiled had those
|
||||||
objects, and translated shards link inside `--start-lib/--end-lib` with archive semantics as
|
objects, and translated shards link inside `--start-lib/--end-lib` with archive semantics as
|
||||||
@@ -234,40 +234,56 @@ 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
|
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.
|
functions, `NEEDED` list and soname as the CMake-built one, in 2.4 minutes.
|
||||||
|
|
||||||
`Build-Quest.ps1` refuses an APK that contains any `libmain*.so` or lacks the kit, and every
|
`Build-Quest.ps1` refuses an APK that contains any `libmain*.so` or lacks the kit, and packaging
|
||||||
variant excludes `**/libmain*.so` and the probes from packaging, since AGP packages every library
|
excludes `**/libmain*.so` and `**/libmkw_quest_kit_probe*.so`, since AGP packages every library
|
||||||
left in the CMake output directory. The `func_8…` symbols the kit's runtime objects define are
|
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.
|
hand-written HLE overrides (`PPC_NATIVE_OVERRIDE_*` in `hle_stubs.h`), not translated code.
|
||||||
|
|
||||||
**Retro Rewind has its own kit** (`mkw_quest_kit_probe_retro`, RetroRewind without any translated
|
**Retro Rewind rides in the same app, on its own kit** (`mkw_quest_kit_probe_retro`, RetroRewind
|
||||||
code), so that APK ships no game either. It needs one more link slot than the base flavour, because
|
without any translated code), so the APK ships neither game. The mod needs one more link slot than
|
||||||
the modded product links more kinds of translated code: `{game:runtime}` (the disc-generated
|
the base game, because the modded product links more kinds of translated code: `{game:runtime}`
|
||||||
sources), `{game:product}` (the mod's registration and dispatch shards), `{game:mod}` (the mod's own
|
(the disc-generated sources), `{game:product}` (the mod's registration and dispatch shards),
|
||||||
shards, the profile-sensitive base shards it replaces, and the mod's data patches) and
|
`{game:mod}` (the mod's own shards, the profile-sensitive base shards it replaces, and the mod's
|
||||||
`{game:translated}` (the shared base shards, with the archive semantics `libmkw_base_shared.a` has).
|
data patches) and `{game:translated}` (the shared base shards, with the archive semantics
|
||||||
Rather than teach each builder which flavour it is building, `kit.json` now carries a `sources`
|
`libmkw_base_shared.a` has). Rather than teach each builder which game it is building, `kit.json`
|
||||||
map naming the `shards.cmake` list behind each slot, and the builders just follow it (kit schema 2).
|
carries a `sources` map naming the `shards.cmake` list behind each slot, and the builders just
|
||||||
Its game is built on a PC for now: building it on the headset would also need the mod's `Code.pul`
|
follow it.
|
||||||
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
|
One APK, one kit: `kit.json` (schema 3) holds a `products` map with a `base` entry and, when the
|
||||||
first `Config.toml` and Home says so when that pack is missing.
|
translation includes the mod, a `retro_rewind` entry, each with its own compile flags, link line,
|
||||||
|
slots and `fingerprint`; `runtimeIncludeFingerprint` and the kit's own `fingerprint` stay at the
|
||||||
|
top level. Everything that builds a game names the product it wants: `Build-QuestGame.ps1
|
||||||
|
-Product base|retro_rewind`, `Setup --quest-product`, and on the headset `GameProfile`. Each game
|
||||||
|
lives in its own directory under `files/game/<profile>`, so both can be installed at once and the
|
||||||
|
launcher's toggle switches between them; the selected one is remembered in `filesDir/selected-game`
|
||||||
|
(a plain file, because the launcher and the `:game` process do not share preferences).
|
||||||
|
|
||||||
|
Retro Rewind also needs its 2 GB pack on the headset. The app writes `[paths] retro_rewind_root`
|
||||||
|
into `Config.toml`, Home says so when the pack is missing, and a `.wcgame` can carry it (below).
|
||||||
|
Building the mod on the headset additionally needs the mod's `Code.pul`, which arrives with that
|
||||||
|
pack, so **Build on this Quest** offers Retro Rewind only once the pack is installed.
|
||||||
|
|
||||||
### Game packages (.wcgame) and Import from computer
|
### Game packages (.wcgame) and Import from computer
|
||||||
|
|
||||||
A `.wcgame` is a zip holding `game.json`, `libmain.so` and optionally `DATA/…` (the extracted
|
A `.wcgame` is a zip holding `game.json`, `libmain.so` and optionally `DATA/…` (the extracted
|
||||||
disc, written without compression). `game.json` records the profile, game ID, `main.dol` and
|
disc) and `MOD/…` (the RetroRewind6 pack), both written without compression. `game.json` records
|
||||||
`StaticR.rel` pins, the kit fingerprint, the library's SHA-256 and who built it.
|
the profile, game ID, `main.dol` and `StaticR.rel` pins, the kit fingerprint, the library's
|
||||||
`android/Build-QuestGame.ps1` builds one on a PC from the translator's output and the kit the
|
SHA-256, whether the package carries game files and mod content, and who built it.
|
||||||
last APK build exported, and `-Data` includes the game files. `-Install` pushes it into the app's
|
`android/Build-QuestGame.ps1 -Product base|retro_rewind` builds one on a PC from the translator's
|
||||||
`Import` folder. The launcher creates that folder itself so it owns it, and imports the newest
|
output and the kit the last APK build exported; `-Data` includes the game files and `-Mod
|
||||||
package the next time it opens, once per package.
|
<RetroRewind6>` the pack. `-Install` pushes it into the app's `Import` folder. The launcher
|
||||||
|
creates that folder itself so it owns it, and imports the newest package the next time it opens,
|
||||||
|
once per package. An import selects the game it just installed.
|
||||||
|
|
||||||
Players get the same build from WheelWizard VR: Settings → WiiCompiled → Meta Quest → **Build**.
|
Players get the same build from WheelWizard VR: Settings → WiiCompiled → Meta Quest → **Build**.
|
||||||
WheelWizard asks for the Quest app's APK, whether to include the game files and where to save the
|
WheelWizard asks which game, whether to include the game files, for Retro Rewind whether to
|
||||||
package, then runs the installed setup:
|
include its pack, for the Quest app's APK and where to save the package, then runs the installed
|
||||||
|
setup:
|
||||||
|
|
||||||
```
|
```
|
||||||
WiiCompiled-Setup.exe --build-quest --install-dir <install> --quest-apk <app.apk> --output <file.wcgame> [--include-game-files] --progress-json
|
WiiCompiled-Setup.exe --build-quest --install-dir <install> --quest-apk <app.apk> --output <file.wcgame>
|
||||||
|
--quest-product base|retro_rewind [--include-game-files]
|
||||||
|
[--retro-dir <RetroRewind6> --include-mod-content] --progress-json
|
||||||
```
|
```
|
||||||
|
|
||||||
Setup extracts `assets/game_kit` from the APK into `<install>\QuestBuild\kit`, so the game always
|
Setup extracts `assets/game_kit` from the APK into `<install>\QuestBuild\kit`, so the game always
|
||||||
@@ -375,10 +391,11 @@ Android facts this design rests on, all measured on a Quest 3:
|
|||||||
also learned `--target-os windows|macos|linux|android` for
|
also learned `--target-os windows|macos|linux|android` for
|
||||||
`generate-data-init` and `translate-mod`, for pipelines that generate on
|
`generate-data-init` and `translate-mod`, for pipelines that generate on
|
||||||
another host.
|
another host.
|
||||||
- `android/`: the Gradle project. `app/src/main/cpp/CMakeLists.txt` adds the
|
- `android/`: the Gradle project, one app with no flavours.
|
||||||
repository's `runtime/` as a subdirectory with those Android choices;
|
`app/src/main/cpp/CMakeLists.txt` adds the repository's `runtime/` as a
|
||||||
flavours `base` and `retroRewind` each build their own game kit probe, and only `base` carries
|
subdirectory with those Android choices and builds both game kit probes
|
||||||
the on-headset toolchain.
|
(the Retro Rewind one only when the translation includes the mod), which
|
||||||
|
`exportDebugQuestGameKit` turns into the single kit the app carries.
|
||||||
- `android/nod-jni`: Gradle's `buildNodJni` task runs `cargo build --release
|
- `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
|
--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`
|
compiler. `stageNodJni` puts `libnod_jni.so` into the APK's `arm64-v8a`
|
||||||
@@ -389,7 +406,7 @@ Android facts this design rests on, all measured on a Quest 3:
|
|||||||
Prerequisites on the Windows host (all already present on the machine this
|
Prerequisites on the Windows host (all already present on the machine this
|
||||||
was developed on): JDK 17, Android SDK with platform 34+, NDK `29.0.14206865`,
|
was developed on): JDK 17, Android SDK with platform 34+, NDK `29.0.14206865`,
|
||||||
SDK CMake `3.22.1`, `adb`, Rust 1.85+ with `rustup target add aarch64-linux-android`,
|
SDK CMake `3.22.1`, `adb`, Rust 1.85+ with `rustup target add aarch64-linux-android`,
|
||||||
the .NET 10 SDK (for the headset's translator; the base APK build downloads ~70 MB of Termux
|
the .NET 10 SDK (for the headset's translator; the APK build downloads ~70 MB of Termux
|
||||||
packages into `android/.dependencies` the first time); a translated graph for your own disc (the
|
packages into `android/.dependencies` the first time); a translated graph for your own disc (the
|
||||||
installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline).
|
installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline).
|
||||||
|
|
||||||
@@ -397,8 +414,7 @@ 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/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-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-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 app and its kit (needs translate-mod output)
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (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
|
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -468,6 +484,16 @@ What has been verified on the development machine (September 2026):
|
|||||||
the base library does not have. Packaged with the disc files (2.55 GB), imported on the Quest 3 in
|
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
|
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`.
|
menu, with the pack read from `retro_rewind_root`.
|
||||||
|
- **Device, 2026-09-17: one app, both games.** The merged APK (121 MB) carries one kit with a `base`
|
||||||
|
and a `retro_rewind` recipe, no `libmain*.so` and no probe. Both games were built on the PC from
|
||||||
|
that one kit (base in 0.1 min from cached objects, Retro Rewind in 3.3 min) and imported on a
|
||||||
|
Quest 3: the base package in 3 s, the Retro Rewind one — 2.0 GB, carrying the pack — in 93 s,
|
||||||
|
which installed `RetroRewind6` and selected the game it had just installed. Both live side by
|
||||||
|
side in `files/game/<profile>`. Retro Rewind ran first (its title screen), then Home's toggle
|
||||||
|
switched to Mario Kart Wii, which ran from the same app. Two bugs this found: the on-device
|
||||||
|
builder read a per-product `fingerprint` that schema 3 keeps at the kit's top level, and a
|
||||||
|
`Config.toml` written before this app offered Retro Rewind named no pack, so the mod would have
|
||||||
|
found none — `GameStorage.prepare` now adds that one line to an existing config.
|
||||||
|
|
||||||
Bring-up fixes that only a device could reveal:
|
Bring-up fixes that only a device could reveal:
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user