windows-development-sandbox 1.23.2 → 1.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.example.json +5 -3
- package/dist/app/features/data/data.def.d.ts +4 -4
- package/dist/app/features/data/data.run.d.ts +1 -1
- package/dist/app/features/defs.d.ts +161 -6
- package/dist/app/features/defs.js +7 -0
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/unreal/index.d.ts +2 -0
- package/dist/app/features/unreal/index.js +3 -0
- package/dist/app/features/unreal/unreal.def.d.ts +236 -0
- package/dist/app/features/unreal/unreal.def.js +379 -0
- package/dist/app/features/unreal/unreal.run.d.ts +96 -0
- package/dist/app/features/unreal/unreal.run.js +217 -0
- package/dist/app/registry.js +5 -0
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-7YPEDAJD.js → chunk-3GAXLATK.js} +58 -5
- package/dist/bin/{chunk-USTRSHIE.js → chunk-F4PVBN23.js} +2615 -1515
- package/dist/bin/{chunk-QFSJX2KW.js → chunk-G3TG4ZT7.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +5 -2
- package/dist/cli/commands/registry.js +3 -0
- package/dist/cli/manifest.d.ts +18 -0
- package/dist/cli/manifest.js +26 -0
- package/dist/cli/menu/coverage-exempt.js +1 -0
- package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
- package/dist/cli/menu/pages/sandbox-page.js +34 -4
- package/dist/cli/menu/views.js +2 -0
- package/dist/core/config/defaults.js +18 -5
- package/dist/core/detect/toolchains.js +0 -6
- package/dist/core/schema/config.d.ts +10 -9
- package/dist/core/schema/pc-installs.d.ts +3 -1
- package/dist/core/schema/pc-installs.js +10 -1
- package/dist/core/schema/toolchains.d.ts +1 -2
- package/dist/core/schema/toolchains.js +4 -7
- package/dist/guest/accounts/kinds/epic.js +18 -13
- package/dist/guest/agents/agent-files.d.ts +11 -7
- package/dist/guest/agents/agent-files.js +37 -11
- package/dist/guest/agents/local-state.d.ts +2 -1
- package/dist/guest/agents/local-state.js +2 -1
- package/dist/guest/apps/after-install.d.ts +1 -1
- package/dist/guest/apps/after-install.js +6 -1
- package/dist/guest/apps/app-installer.d.ts +1 -0
- package/dist/guest/apps/app-installer.js +13 -3
- package/dist/guest/apps/comet-unpack.js +3 -2
- package/dist/guest/apps/resolvers-gamedev.js +55 -1
- package/dist/guest/boot/boot-console.d.ts +28 -0
- package/dist/guest/boot/boot-console.js +98 -0
- package/dist/guest/boot/boot.d.ts +8 -1
- package/dist/guest/boot/boot.js +80 -43
- package/dist/guest/boot/graph.d.ts +9 -5
- package/dist/guest/boot/graph.js +8 -2
- package/dist/guest/boot/guide.js +24 -0
- package/dist/guest/boot/progress-view.d.ts +116 -0
- package/dist/guest/boot/progress-view.js +456 -0
- package/dist/guest/boot/runner.d.ts +20 -10
- package/dist/guest/boot/runner.js +39 -47
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/runtime/log-line.d.ts +7 -0
- package/dist/guest/runtime/log-line.js +18 -0
- package/dist/guest/runtime/log.d.ts +17 -4
- package/dist/guest/runtime/log.js +40 -17
- package/dist/guest/runtime/process.d.ts +9 -0
- package/dist/guest/runtime/process.js +69 -8
- package/dist/guest/setup/addon-mcp.d.ts +7 -0
- package/dist/guest/setup/addon-mcp.js +19 -4
- package/dist/guest/setup/browser-gpu.js +14 -11
- package/dist/guest/setup/environment.js +9 -4
- package/dist/guest/setup/headroom.js +4 -0
- package/dist/guest/setup/hooks.d.ts +2 -0
- package/dist/guest/setup/hooks.js +4 -0
- package/dist/guest/setup/path-plan.d.ts +12 -0
- package/dist/guest/setup/path-plan.js +18 -0
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
- package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
- package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
- package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
- package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
- package/dist/guest/toolchains/engines/unreal-status.js +41 -0
- package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
- package/dist/guest/toolchains/native/debuggers.js +57 -0
- package/dist/guest/toolchains/native/mesa.js +15 -8
- package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
- package/dist/guest/toolchains/native/rust-extras.js +23 -1
- package/dist/guest/toolchains/native/unreal.d.ts +2 -2
- package/dist/guest/toolchains/native/unreal.js +96 -20
- package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
- package/dist/guest/toolchains/native/visual-studio.js +145 -174
- package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
- package/dist/guest/toolchains/native/vs-persist.js +256 -0
- package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
- package/dist/guest/toolchains/native/vs-request.js +16 -50
- package/dist/guest/toolchains/plan.js +23 -15
- package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
- package/dist/host/accounts/kinds/epic.js +11 -5
- package/dist/host/accounts/source.d.ts +2 -0
- package/dist/host/accounts/source.js +1 -0
- package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
- package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
- package/dist/host/migrate/registry.js +7 -1
- package/dist/host/pc-installs/discover.d.ts +2 -0
- package/dist/host/pc-installs/discover.js +25 -5
- package/dist/host/pc-installs/epic.d.ts +3 -0
- package/dist/host/pc-installs/epic.js +33 -2
- package/dist/host/pc-installs/source.d.ts +1 -1
- package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
- package/dist/host/pc-installs/unreal-vault.js +78 -0
- package/dist/host/pc-installs/unreal.d.ts +36 -0
- package/dist/host/pc-installs/unreal.js +145 -2
- package/dist/host/profile/claude-profile.d.ts +8 -0
- package/dist/host/profile/claude-profile.js +43 -4
- package/dist/host/profile/index.d.ts +1 -1
- package/dist/host/profile/index.js +1 -1
- package/dist/runtime/agent.js +148 -33
- package/dist/runtime/daemon.js +3146 -1546
- package/dist/runtime/db.js +148 -33
- package/dist/runtime/main.js +3982 -2049
- package/dist/runtime/netproxy.js +84 -13
- package/dist/runtime/tool.js +125 -31
- package/dist/runtime/view.js +85 -14
- package/dist/selftest/agent-paths.test.js +18 -1
- package/dist/selftest/agent-settings.test.js +36 -5
- package/dist/selftest/agents-web.test.js +12 -0
- package/dist/selftest/boot-console.test.d.ts +2 -0
- package/dist/selftest/boot-console.test.js +291 -0
- package/dist/selftest/boot-runner.test.js +10 -12
- package/dist/selftest/engines.test.js +602 -3
- package/dist/selftest/guest-boot.test.js +7 -1
- package/dist/selftest/heavy-load.test.js +3 -1
- package/dist/selftest/hostile-input.test.js +11 -1
- package/dist/selftest/index.js +4 -0
- package/dist/selftest/layout.test.js +4 -0
- package/dist/selftest/menu-screens.js +3 -1
- package/dist/selftest/migrate.test.js +21 -0
- package/dist/selftest/mixamo.test.d.ts +2 -0
- package/dist/selftest/mixamo.test.js +60 -0
- package/dist/selftest/persisted-installs.test.js +169 -33
- package/dist/selftest/secrets.test.js +36 -1
- package/dist/selftest/support/engine-golden.js +5 -0
- package/dist/selftest/support/fixtures.d.ts +4 -3
- package/dist/selftest/support/unreal-sample.d.ts +2 -0
- package/dist/selftest/support/unreal-sample.js +83 -0
- package/dist/selftest/toolchains.test.js +32 -2
- package/dist/shared/accounts/index.d.ts +1 -1
- package/dist/shared/accounts/index.js +1 -1
- package/dist/shared/accounts/kinds/epic.d.ts +6 -1
- package/dist/shared/accounts/kinds/epic.js +11 -1
- package/dist/shared/apps/associations.d.ts +7 -0
- package/dist/shared/apps/associations.js +26 -0
- package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
- package/dist/shared/apps/catalogue-agents-web.js +5 -2
- package/dist/shared/apps/catalogue-gamedev.js +71 -1
- package/dist/shared/apps/index.d.ts +2 -0
- package/dist/shared/apps/index.js +2 -0
- package/dist/shared/apps/preview.d.ts +2 -0
- package/dist/shared/apps/preview.js +10 -0
- package/dist/shared/browser-policy.d.ts +14 -0
- package/dist/shared/browser-policy.js +22 -0
- package/dist/shared/channel/schema.d.ts +3 -6
- package/dist/shared/io/index.d.ts +1 -0
- package/dist/shared/io/index.js +1 -0
- package/dist/shared/io/rotate-log.d.ts +2 -0
- package/dist/shared/io/rotate-log.js +16 -0
- package/dist/shared/memory/policy.d.ts +5 -0
- package/dist/shared/memory/policy.js +7 -0
- package/dist/shared/paths/pc-installs.d.ts +13 -2
- package/dist/shared/paths/pc-installs.js +1 -0
- package/dist/shared/pty/terminal.js +2 -1
- package/dist/shared/readiness.d.ts +0 -1
- package/dist/shared/readiness.js +0 -1
- package/dist/shared/signer.d.ts +4 -0
- package/dist/shared/signer.js +2 -0
- package/dist/shared/unreal/engines.d.ts +161 -0
- package/dist/shared/unreal/engines.js +308 -0
- package/dist/shared/unreal/guide.d.ts +19 -0
- package/dist/shared/unreal/guide.js +105 -0
- package/dist/shared/unreal/index.d.ts +7 -0
- package/dist/shared/unreal/index.js +8 -0
- package/dist/shared/unreal/launcher-files.d.ts +88 -0
- package/dist/shared/unreal/launcher-files.js +292 -0
- package/dist/shared/unreal/mixamo.d.ts +5 -0
- package/dist/shared/unreal/mixamo.js +16 -0
- package/dist/shared/unreal/room.d.ts +17 -0
- package/dist/shared/unreal/room.js +34 -0
- package/dist/shared/unreal/status.d.ts +54 -0
- package/dist/shared/unreal/status.js +57 -0
- package/dist/shared/unreal/vault.d.ts +21 -0
- package/dist/shared/unreal/vault.js +67 -0
- package/dist/shared/unreal/vs-check.d.ts +39 -0
- package/dist/shared/unreal/vs-check.js +79 -0
- package/dist/sync/watcher.js +5 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/csharp/VsSetup.cs +210 -0
- package/sandbox/scripts/get-vs-setup.ps1 +93 -0
- package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
- package/sandbox/scripts/save-visual-studio.ps1 +106 -0
- package/sandbox/scripts/set-registry-values.ps1 +35 -0
- package/schemas/config.schema.json +38 -9
- package/schemas/sandbox-config.schema.json +11 -8
- package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
- package/dist/guest/toolchains/native/autosdk.js +0 -50
|
@@ -1,7 +1,606 @@
|
|
|
1
|
+
// Unreal engines and the game-development apps: what an engine folder is, where the sandbox finds engines (this PC
|
|
2
|
+
// read-only, the launcher's folder on the data disk, source builds), which one a project builds with, the registry and
|
|
3
|
+
// file-association plan, PATH and variables, the launcher's files (LauncherInstalled.dat merge with foreign entries
|
|
4
|
+
// kept, .item manifests, GameUserSettings.ini in UTF-16 and UTF-8), the room check, the guide lines, UnrealBuildTool's
|
|
5
|
+
// Visual Studio verdict, the vault cache, this PC's discovery (fixtures), the apps catalogue entries and the Unreal
|
|
6
|
+
// page. Temp folders and fixtures only: never this machine's registry or launcher.
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { DEFAULTS } from '../core/config/index.js';
|
|
11
|
+
import { isWin } from '../core/util/index.js';
|
|
12
|
+
import { ConfigSchema } from '../core/schema/index.js';
|
|
13
|
+
import { decodeText, encodeText, readBoundedFile } from '../shared/io/index.js';
|
|
14
|
+
import { SYSTEM } from '../shared/system.js';
|
|
15
|
+
import { chooseEngine, ddcVars, enginePlaceProblem, ENGINE_NEED_BYTES, engineAt, engineExtraDirs, engineToolDirs, engineVars, entriesUnder, fileAssociationPlan, findEngines, findUproject, hasMcpPlugin, incompleteDirs, isBuildId, installLocationOf, itemSizes, launcherEntriesFor, launcherEntry, launcherInstallations, mergeLauncherInstalled, parseBuildVersion, parseEngineAssociation, preferredInstalled, registryPlan, restoreItems, saveItems, setLauncherInstallLocation, stableBuildId, UNREAL_STATUS_FILE, unrealGuideLines, unrealSummaryLine, uprojectAssociation, vaultDirsFromIni, vaultPacks, vsVerdict, VsSetupReportSchema, withInstallLocation, } from '../shared/unreal/index.js';
|
|
16
|
+
import { discoverPcInstalls } from '../host/pc-installs/index.js';
|
|
17
|
+
import { findEngineFolders, findUnrealInstalls, pcSourceBuilds, regValues } from '../host/pc-installs/unreal.js';
|
|
18
|
+
import { findEpicGames } from '../host/pc-installs/epic.js';
|
|
19
|
+
import { vaultInstalls } from '../host/pc-installs/unreal-vault.js';
|
|
20
|
+
import { GAMEDEV_APPS } from '../shared/apps/catalogue-gamedev.js';
|
|
21
|
+
import { APPS } from '../shared/apps/index.js';
|
|
22
|
+
import { pageActions } from '../app/kernel/index.js';
|
|
23
|
+
import { engineFolderProblem, unrealPage, unrealView } from '../app/features/unreal/index.js';
|
|
24
|
+
import { readStatus } from '../app/features/unreal/unreal.run.js';
|
|
25
|
+
import { pageLines } from '../cli/output/render-view.js';
|
|
26
|
+
import { UNREAL_SAMPLE } from './support/unreal-sample.js';
|
|
27
|
+
const BUILD_VERSION = (major, minor, patch) => JSON.stringify({
|
|
28
|
+
MajorVersion: major,
|
|
29
|
+
MinorVersion: minor,
|
|
30
|
+
PatchVersion: patch,
|
|
31
|
+
Changelist: 35576357,
|
|
32
|
+
CompatibleChangelist: 35576357,
|
|
33
|
+
IsLicenseeVersion: 0,
|
|
34
|
+
IsPromotedBuild: 1,
|
|
35
|
+
BranchName: '++UE5+Release-' + major + '.' + minor,
|
|
36
|
+
});
|
|
37
|
+
/** The real disk under the test's temp folder. */
|
|
38
|
+
const DISK = {
|
|
39
|
+
exists: (f) => fs.existsSync(f),
|
|
40
|
+
read: (f, max) => readBoundedFile(f, max),
|
|
41
|
+
list: (d) => {
|
|
42
|
+
try {
|
|
43
|
+
return fs.readdirSync(d);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const VAULT_DISK = {
|
|
51
|
+
exists: (f) => fs.existsSync(f),
|
|
52
|
+
list: DISK.list,
|
|
53
|
+
size: (f) => {
|
|
54
|
+
const s = fs.lstatSync(f, { throwIfNoEntry: false });
|
|
55
|
+
return s?.isFile() ? s.size : null;
|
|
56
|
+
},
|
|
57
|
+
isDir: (d) => fs.lstatSync(d, { throwIfNoEntry: false })?.isDirectory() ?? false,
|
|
58
|
+
};
|
|
1
59
|
export const enginesSuite = {
|
|
2
60
|
name: 'engines',
|
|
3
|
-
title: 'Unreal engines,
|
|
4
|
-
run: (t) => {
|
|
5
|
-
|
|
61
|
+
title: 'Unreal engines (every route), the launcher files, the guide, the Unreal page and the game-development apps',
|
|
62
|
+
run: async (t) => {
|
|
63
|
+
/** A fake engine folder: Build.version, InstalledBuild.txt for an installed build, the tools agents run. */
|
|
64
|
+
const makeEngine = (dir, v, installed, mcp = false) => {
|
|
65
|
+
t.write(path.join(dir, 'Engine', 'Build', 'Build.version'), BUILD_VERSION(...v));
|
|
66
|
+
if (installed)
|
|
67
|
+
t.write(path.join(dir, 'Engine', 'Build', 'InstalledBuild.txt'), '');
|
|
68
|
+
t.write(path.join(dir, 'Engine', 'Build', 'BatchFiles', 'Build.bat'), '@echo off\r\n');
|
|
69
|
+
t.write(path.join(dir, 'Engine', 'Binaries', 'Win64', 'UnrealEditor-Cmd.exe'), '');
|
|
70
|
+
if (mcp)
|
|
71
|
+
t.write(path.join(dir, 'Engine', 'Plugins', 'Experimental', 'ModelContextProtocol', 'ModelContextProtocol.uplugin'), '{}');
|
|
72
|
+
return dir;
|
|
73
|
+
};
|
|
74
|
+
t.section('an engine folder');
|
|
75
|
+
t.equal(parseBuildVersion('\uFEFF' + BUILD_VERSION(5, 4, 4)), { major: 5, minor: 4, patch: 4, changelist: 35576357, branch: '++UE5+Release-5.4' }, 'Build.version is read (with a byte-order mark too)');
|
|
76
|
+
t.equal(parseBuildVersion('{"MajorVersion":"5"}'), null, 'a file that is not a Build.version is no engine');
|
|
77
|
+
const pcEngine = makeEngine(t.dir('PcInstalls', 'unreal', 'UE_5.4'), [5, 4, 4], true);
|
|
78
|
+
const enginesRoot = t.dir('tools', 'unreal', 'engines');
|
|
79
|
+
const launcherEngine = makeEngine(path.join(enginesRoot, 'UE_5.4'), [5, 4, 2], true);
|
|
80
|
+
const dataSource = makeEngine(path.join(enginesRoot, 'UE5-src'), [5, 6, 0], false, true);
|
|
81
|
+
const projectsRoot = t.dir('Projects', 'Work');
|
|
82
|
+
const projectSource = makeEngine(path.join(projectsRoot, 'UnrealEngine'), [5, 5, 1], false);
|
|
83
|
+
t.dir('Projects', 'Work', 'MyGame');
|
|
84
|
+
const installed = engineAt(pcEngine, 'pc', false, DISK);
|
|
85
|
+
t.equal([installed.id, installed.build, installed.writable], ['UE_5.4', 'installed', false], 'an installed build (InstalledBuild.txt) is named UE_<major.minor>');
|
|
86
|
+
const source = engineAt(dataSource, 'data', true, DISK);
|
|
87
|
+
t.check(source.build === 'source' && source.id === stableBuildId(dataSource) && /^\{[0-9A-F-]{36}\}$/.test(source.id), 'a source build without a registered name gets a GUID made from its folder (the same every boot)');
|
|
88
|
+
t.equal(engineAt(pcEngine, 'pc', false, DISK, '{AAAA}')?.id, 'UE_5.4', "an installed build's name is its version");
|
|
89
|
+
t.equal(engineAt(projectSource, 'pc', false, DISK, 'MyBuild')?.id, 'MyBuild', "a source build from this PC keeps its registered name (the project's association still matches)");
|
|
90
|
+
t.equal(engineAt(t.dir('not-an-engine'), 'data', true, DISK), null, 'a folder without Build.version is no engine');
|
|
91
|
+
t.section('every route: this PC, the launcher, the data disk, a project root');
|
|
92
|
+
const engines = findEngines({
|
|
93
|
+
mapped: [{ guest: pcEngine, id: 'UE_5.4' }],
|
|
94
|
+
enginesRoot,
|
|
95
|
+
launcherDirs: [launcherEngine.toLowerCase()],
|
|
96
|
+
projectRoots: [projectsRoot],
|
|
97
|
+
}, DISK);
|
|
98
|
+
t.equal(engines.map((e) => [e.id.startsWith('{') ? 'guid' : e.id, e.origin, e.writable]), [
|
|
99
|
+
['UE_5.4', 'pc', false],
|
|
100
|
+
['guid', 'data', true],
|
|
101
|
+
['UE_5.4', 'launcher', true],
|
|
102
|
+
['guid', 'project', true],
|
|
103
|
+
], "this PC's engine (read-only), the launcher's and a source build on the data disk, a source build in a project root");
|
|
104
|
+
t.equal(findEngines({
|
|
105
|
+
mapped: [
|
|
106
|
+
{ guest: pcEngine, id: 'UE_5.4' },
|
|
107
|
+
{ guest: pcEngine, id: 'UE_5.4' },
|
|
108
|
+
],
|
|
109
|
+
enginesRoot: t.at('none'),
|
|
110
|
+
launcherDirs: [],
|
|
111
|
+
projectRoots: [],
|
|
112
|
+
}, DISK).length, 1, 'each folder once');
|
|
113
|
+
t.section("a project's engine");
|
|
114
|
+
t.equal(parseEngineAssociation('5.4'), { kind: 'version', version: '5.4' }, '"5.4" names an installed build');
|
|
115
|
+
t.equal(parseEngineAssociation('5.4.4'), { kind: 'version', version: '5.4' }, '"5.4.4" too');
|
|
116
|
+
t.equal(parseEngineAssociation('{2D9FCA5D-4E7B-4C12-8B6E-1A2B3C4D5E6F}'), { kind: 'build', id: '{2D9FCA5D-4E7B-4C12-8B6E-1A2B3C4D5E6F}' }, 'a {GUID} names a registered source build');
|
|
117
|
+
t.equal(parseEngineAssociation(''), { kind: 'none' }, 'an empty association names none');
|
|
118
|
+
t.equal(parseEngineAssociation('../x'), { kind: 'none' }, 'a path is no association');
|
|
119
|
+
t.equal(uprojectAssociation('{"FileVersion":3,"EngineAssociation":"5.6"}'), { kind: 'version', version: '5.6' }, 'read from a .uproject');
|
|
120
|
+
t.equal(uprojectAssociation('not json'), null, 'a .uproject that is not JSON says nothing');
|
|
121
|
+
const pick = (a) => chooseEngine(engines, a).engine;
|
|
122
|
+
t.equal(pick({ kind: 'version', version: '5.4' })?.dir, launcherEngine, 'a version: the writable copy before the read-only one from this PC');
|
|
123
|
+
t.equal(chooseEngine([engines[0]], { kind: 'version', version: '5.4' }).engine?.origin, 'pc', "with only this PC's copy, that one");
|
|
124
|
+
t.equal(pick({ kind: 'build', id: engines.find((e) => e.origin === 'data').id.toLowerCase() })?.dir, dataSource, 'a build id (any case) picks that build');
|
|
125
|
+
t.equal(pick({ kind: 'none' })?.dir, dataSource, 'no association: the newest engine');
|
|
126
|
+
const missing = chooseEngine(engines, { kind: 'version', version: '5.8' });
|
|
127
|
+
t.check(missing.engine === null && missing.reason.includes('5.8'), 'a version that is not here: none, and the reason says which');
|
|
128
|
+
t.equal(chooseEngine([], { kind: 'none' }).reason, 'no engine is installed yet', 'no engines: says so');
|
|
129
|
+
const game = t.dir('Projects', 'Work', 'MyGame');
|
|
130
|
+
t.write(path.join(game, 'MyGame.uproject'), '{}');
|
|
131
|
+
t.equal(findUproject(game, DISK), path.join(game, 'MyGame.uproject'), 'the .uproject in the project folder');
|
|
132
|
+
const nested = t.dir('repo');
|
|
133
|
+
t.write(path.join(nested, 'Game', 'Game.uproject'), '{}');
|
|
134
|
+
t.equal(findUproject(nested, DISK), path.join(nested, 'Game', 'Game.uproject'), 'or one folder down');
|
|
135
|
+
t.section('registration');
|
|
136
|
+
const plan = registryPlan(engines);
|
|
137
|
+
t.equal(plan.filter((v) => v.hive === 'HKLM'), [
|
|
138
|
+
{
|
|
139
|
+
hive: 'HKLM',
|
|
140
|
+
key: 'SOFTWARE\\EpicGames\\Unreal Engine\\5.4',
|
|
141
|
+
name: 'InstalledDirectory',
|
|
142
|
+
value: launcherEngine,
|
|
143
|
+
},
|
|
144
|
+
], 'installed builds: one HKLM ...\\Unreal Engine\\<5.4> InstalledDirectory per version, the writable one');
|
|
145
|
+
t.equal(plan.filter((v) => v.hive === 'HKCU').map((v) => [v.key, v.value]), [
|
|
146
|
+
['Software\\Epic Games\\Unreal Engine\\Builds', dataSource],
|
|
147
|
+
['Software\\Epic Games\\Unreal Engine\\Builds', projectSource],
|
|
148
|
+
], 'source builds: a value under HKCU\\Software\\Epic Games\\Unreal Engine\\Builds each');
|
|
149
|
+
const assoc = fileAssociationPlan('C:\\E\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe');
|
|
150
|
+
t.check(assoc.some((v) => v.key === 'Software\\Classes\\.uproject' && v.value === 'Unreal.ProjectFile') &&
|
|
151
|
+
assoc.some((v) => v.key.endsWith('\\shell\\rungenproj\\command') &&
|
|
152
|
+
v.value === '"C:\\E\\Engine\\Binaries\\Win64\\UnrealVersionSelector.exe" /projectfiles "%1"'), '.uproject opens with UnrealVersionSelector, with "Generate Visual Studio project files"');
|
|
153
|
+
await t.throws(() => fileAssociationPlan('C:\\a"b.exe'), 'not a program path', 'a path with a quote is refused');
|
|
154
|
+
t.section('PATH, variables, the derived data cache, MCP');
|
|
155
|
+
t.equal([...engineToolDirs(pcEngine, DISK), ...engineExtraDirs(pcEngine, DISK)], [path.join(pcEngine, 'Engine', 'Build', 'BatchFiles'), path.join(pcEngine, 'Engine', 'Binaries', 'Win64')], 'the engine folders that exist go on PATH (Build.bat and RunUAT.bat first, the DLL folders last)');
|
|
156
|
+
t.equal(engineVars(installed), { UE_ROOT: pcEngine, UE_ENGINE_DIR: path.join(pcEngine, 'Engine') }, 'UE_ROOT is the engine folder');
|
|
157
|
+
t.equal(ddcVars('persisted', 'C:\\AgentData\\tools\\unreal\\ddc'), { 'UE-LocalDataCachePath': 'C:\\AgentData\\tools\\unreal\\ddc' }, 'persisted: UE-LocalDataCachePath on the data disk');
|
|
158
|
+
t.equal(ddcVars('local', 'x'), {}, "local: Unreal's own place");
|
|
159
|
+
t.check(!hasMcpPlugin(source, DISK) && !hasMcpPlugin(installed, DISK), 'before 5.8: no editor MCP server');
|
|
160
|
+
const ue58 = engineAt(makeEngine(t.dir('ue58'), [5, 8, 0], true, true), 'pc', false, DISK);
|
|
161
|
+
t.check(hasMcpPlugin(ue58, DISK), 'Unreal 5.8 with ModelContextProtocol: the editor MCP server is offered');
|
|
162
|
+
t.section('LauncherInstalled.dat');
|
|
163
|
+
const foreign = {
|
|
164
|
+
InstallLocation: 'D:\\Games\\Fortnite',
|
|
165
|
+
NamespaceId: 'fn',
|
|
166
|
+
ItemId: '4fe75bbc5a674f4f9b356b5c90567da5',
|
|
167
|
+
ArtifactId: 'Fortnite',
|
|
168
|
+
AppVersion: '++Fortnite+Release-30.00-CL-1',
|
|
169
|
+
AppName: 'Fortnite',
|
|
170
|
+
ExtraField: { kept: true },
|
|
171
|
+
};
|
|
172
|
+
const live = {
|
|
173
|
+
...foreign,
|
|
174
|
+
InstallLocation: launcherEngine,
|
|
175
|
+
NamespaceId: 'ue',
|
|
176
|
+
ItemId: 'e1a',
|
|
177
|
+
ArtifactId: 'UE_5.4',
|
|
178
|
+
AppVersion: '5.4.2-1+++UE5+Release-5.4',
|
|
179
|
+
AppName: 'UE_5.4',
|
|
180
|
+
};
|
|
181
|
+
const existing = '\uFEFF' + JSON.stringify({ InstallationList: [foreign, live], Version: 3 });
|
|
182
|
+
const merged = JSON.parse(mergeLauncherInstalled(existing, [launcherEntry(installed)]));
|
|
183
|
+
t.check(merged.Version === 3 &&
|
|
184
|
+
merged.InstallationList[0].ExtraField?.kept === true &&
|
|
185
|
+
merged.InstallationList.some((e) => e.InstallLocation === pcEngine), "other apps' entries and the file's own fields stay exactly as they were; this PC's engine is added");
|
|
186
|
+
const again = JSON.parse(mergeLauncherInstalled(JSON.stringify(merged), [launcherEntry(installed)]));
|
|
187
|
+
t.equal(again.InstallationList.length, 3, 'merging twice adds nothing (the same folder is replaced)');
|
|
188
|
+
await t.throws(() => mergeLauncherInstalled('{"something":"else"}', []), /InstallationList|expected/, 'a file that is not a LauncherInstalled.dat is never overwritten');
|
|
189
|
+
t.equal(JSON.parse(mergeLauncherInstalled(null, [])).InstallationList, [], 'no file: a new list');
|
|
190
|
+
t.equal(entriesUnder(existing, enginesRoot).map((e) => e.ItemId), ['e1a'], "the entries of the data disk's engines are the ones saved");
|
|
191
|
+
t.equal(launcherEntry(installed, { namespaceId: 'ue', itemId: 'real', artifactId: 'UE_5.4', appVersion: 'v' }).ItemId, 'real', "this PC's own ids are used when known");
|
|
192
|
+
t.equal(launcherInstallations('[]'), [], 'a broken file lists nothing');
|
|
193
|
+
t.section('.item manifests');
|
|
194
|
+
const manifests = t.dir('ProgramData', 'Manifests');
|
|
195
|
+
const saved = t.dir('saved', 'Manifests');
|
|
196
|
+
t.write(path.join(manifests, 'AAA.item'), JSON.stringify({ AppName: 'UE_5.4', InstallLocation: launcherEngine, InstallSize: 1234 }));
|
|
197
|
+
t.write(path.join(manifests, 'BBB.item'), JSON.stringify({ AppName: 'Fortnite', InstallLocation: 'D:\\Games\\Fortnite' }));
|
|
198
|
+
t.write(path.join(manifests, 'evil..item'), '{}');
|
|
199
|
+
t.equal(saveItems(manifests, saved, enginesRoot), ['AAA.item'], 'only the manifests of engines on the data disk are saved');
|
|
200
|
+
t.equal(saveItems(manifests, saved, enginesRoot), [], 'an unchanged manifest is not saved again');
|
|
201
|
+
t.equal(itemSizes(saved).get(launcherEngine.toLowerCase()), 1234, 'the manifest gives the engine its size');
|
|
202
|
+
const fresh = t.dir('ProgramData2', 'Manifests');
|
|
203
|
+
t.write(path.join(fresh, 'BBB.item'), "the launcher's own");
|
|
204
|
+
t.equal(restoreItems(saved, fresh, enginesRoot, fs.existsSync), [launcherEngine], 'a new sandbox gets the saved manifests back');
|
|
205
|
+
t.check(fs.existsSync(path.join(fresh, 'AAA.item')) &&
|
|
206
|
+
fs.readFileSync(path.join(fresh, 'BBB.item'), 'utf8') === "the launcher's own", 'what the launcher has is never replaced');
|
|
207
|
+
t.write(path.join(saved, 'GONE.item'), JSON.stringify({ InstallLocation: path.join(enginesRoot, 'UE_9.9') }));
|
|
208
|
+
t.equal(restoreItems(saved, t.dir('ProgramData3'), enginesRoot, fs.existsSync).length, 1, 'a manifest whose engine folder is gone is not put back');
|
|
209
|
+
t.section('GameUserSettings.ini');
|
|
210
|
+
t.equal(withInstallLocation('', 'D:\\E'), '[Launcher]\r\nDefaultAppInstallLocation=D:\\E\r\n', 'an empty file gets the section');
|
|
211
|
+
const ini = ';METADATA=(Diff=true, UseCommands=true)\r\n[Launcher]\r\nLastActiveVersion=20.3.7\r\n\r\n[Portal.OSS]\r\nbShouldPrimeEms=False\r\n';
|
|
212
|
+
const set = withInstallLocation(ini, 'C:\\AgentData\\tools\\unreal\\engines');
|
|
213
|
+
t.check(set.startsWith(';METADATA') &&
|
|
214
|
+
set.includes('[Launcher]\r\nDefaultAppInstallLocation=C:\\AgentData\\tools\\unreal\\engines\r\nLastActiveVersion') &&
|
|
215
|
+
set.includes('[Portal.OSS]\r\nbShouldPrimeEms=False'), 'the value goes into [Launcher]; every other line stays (launcher 20 file)');
|
|
216
|
+
t.equal(withInstallLocation(set, 'C:\\AgentData\\tools\\unreal\\engines'), set, 'setting it again changes nothing');
|
|
217
|
+
t.includes(withInstallLocation('[Launcher]\nDefaultAppInstallLocation=C:\\Old\n', 'D:\\New'), 'DefaultAppInstallLocation=D:\\New\n', 'an old value is replaced, LF endings kept');
|
|
218
|
+
t.equal(installLocationOf(set), 'C:\\AgentData\\tools\\unreal\\engines', 'and read back');
|
|
219
|
+
const utf16 = t.write(path.join(t.dir('ini'), 'a.ini'), '');
|
|
220
|
+
fs.writeFileSync(utf16, encodeText(ini, 'utf16le'));
|
|
221
|
+
t.check(setLauncherInstallLocation(utf16, 'C:\\X'), 'a UTF-16 file is changed');
|
|
222
|
+
const back = decodeText(fs.readFileSync(utf16));
|
|
223
|
+
t.check(back.encoding === 'utf16le' && installLocationOf(back.text) === 'C:\\X', 'and stays UTF-16 with its mark');
|
|
224
|
+
const bom = t.at('ini', 'b.ini');
|
|
225
|
+
fs.writeFileSync(bom, encodeText(ini, 'utf8bom'));
|
|
226
|
+
setLauncherInstallLocation(bom, 'C:\\Y');
|
|
227
|
+
t.equal(decodeText(fs.readFileSync(bom)).encoding, 'utf8bom', 'a UTF-8 file with a mark keeps its mark');
|
|
228
|
+
t.check(setLauncherInstallLocation(t.at('ini', 'new', 'GameUserSettings.ini'), 'C:\\Z'), 'a missing file is created');
|
|
229
|
+
t.section('room for an engine (never the sandbox disk)');
|
|
230
|
+
const statfs = (gb) => () => ({ bavail: gb * 1024 ** 3, bsize: 1 });
|
|
231
|
+
t.equal(enginePlaceProblem({ set: true }, enginesRoot, statfs(200)).problem, null, '200 GB free on the data disk: fine');
|
|
232
|
+
const tight = enginePlaceProblem({ set: true }, enginesRoot, statfs(50));
|
|
233
|
+
t.check(tight.problem !== null &&
|
|
234
|
+
tight.problem.includes('50 GB') &&
|
|
235
|
+
tight.problem.includes('60 GB') &&
|
|
236
|
+
tight.freeBytes === 50 * 1024 ** 3, 'under 60 GB: refused, saying how much is free and needed');
|
|
237
|
+
t.equal(ENGINE_NEED_BYTES, 60 * 1024 ** 3, 'an engine needs 60 GB');
|
|
238
|
+
const unset = enginePlaceProblem({ set: false, error: 'could not write the settings' }, enginesRoot, statfs(500));
|
|
239
|
+
t.check(unset.problem?.startsWith('ENGINES WOULD GO ON THE SANDBOX DISK') === true &&
|
|
240
|
+
unset.problem.includes('could not write'), "the launcher's folder not set: said loudly");
|
|
241
|
+
t.section('the guide');
|
|
242
|
+
const status = {
|
|
243
|
+
updatedAt: 'now',
|
|
244
|
+
engines: [
|
|
245
|
+
{
|
|
246
|
+
id: 'UE_5.4',
|
|
247
|
+
dir: pcEngine,
|
|
248
|
+
version: '5.4.4',
|
|
249
|
+
build: 'installed',
|
|
250
|
+
origin: 'pc',
|
|
251
|
+
writable: false,
|
|
252
|
+
sizeBytes: null,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
projects: [{ workspace: 'C:\\Projects\\Work\\MyGame', engine: pcEngine, reason: 'the project asks for 5.4' }],
|
|
256
|
+
launcher: {
|
|
257
|
+
installed: true,
|
|
258
|
+
installLocation: enginesRoot,
|
|
259
|
+
problem: null,
|
|
260
|
+
freeBytes: null,
|
|
261
|
+
needBytes: ENGINE_NEED_BYTES,
|
|
262
|
+
},
|
|
263
|
+
visualStudio: { version: '17.14', path: 'C:\\VS', how: 'kept on the data disk', check: 'ok', ok: true },
|
|
264
|
+
debugger: 'C:\\Kits\\Debuggers\\x64\\cdb.exe',
|
|
265
|
+
ddc: { mode: 'persisted', dir: 'C:\\AgentData\\tools\\unreal\\ddc' },
|
|
266
|
+
vaults: ['C:\\PcInstalls\\unreal\\Vault'],
|
|
267
|
+
mcpUrl: 'http://127.0.0.1:8000/mcp',
|
|
268
|
+
};
|
|
269
|
+
const guide = unrealGuideLines(status, [
|
|
270
|
+
{ workspace: 'C:\\Projects\\Work\\MyGame', uproject: 'C:\\Projects\\Work\\MyGame\\MyGame.uproject' },
|
|
271
|
+
]).join('\n');
|
|
272
|
+
for (const needle of [
|
|
273
|
+
'engine UE_5.4 5.4.4',
|
|
274
|
+
'`UE_ROOT` is set',
|
|
275
|
+
'Read-only',
|
|
276
|
+
'Build.bat MyGameEditor Win64 Development -Project="C:\\Projects\\Work\\MyGame\\MyGame.uproject" -WaitMutex -NoHotReload',
|
|
277
|
+
'Build.bat -projectfiles -project=',
|
|
278
|
+
'-ExecCmds="Automation RunTests <filter>;Quit"',
|
|
279
|
+
'-unattended -nullrhi',
|
|
280
|
+
'RunUAT.bat BuildCookRun -project=',
|
|
281
|
+
'-platform=Win64 -clientconfig=Development -build -cook -stage -pak -archive -archivedirectory=',
|
|
282
|
+
'Saved\\Logs',
|
|
283
|
+
'cdb -z "<UEMinidump.dmp>" -c "!analyze -v;q"',
|
|
284
|
+
'C:\\AgentData\\tools\\unreal\\ddc',
|
|
285
|
+
'C:\\PcInstalls\\unreal\\Vault',
|
|
286
|
+
'<Pack>\\data\\Content',
|
|
287
|
+
'ModelContextProtocol.StartServer',
|
|
288
|
+
'sbx-unreal-editor',
|
|
289
|
+
])
|
|
290
|
+
t.includes(guide, needle, 'the guide says: ' + needle);
|
|
291
|
+
t.excludes(guide, 'sbx-db', 'no wrapper commands: the plain engine tools');
|
|
292
|
+
const none = unrealGuideLines({
|
|
293
|
+
...status,
|
|
294
|
+
engines: [],
|
|
295
|
+
projects: [{ workspace: 'C:\\P', engine: null, reason: 'no engine is installed yet' }],
|
|
296
|
+
vaults: [],
|
|
297
|
+
mcpUrl: null,
|
|
298
|
+
}, [{ workspace: 'C:\\P', uproject: null }]).join('\n');
|
|
299
|
+
t.includes(none, 'no engine yet', 'no engine: the guide says how to get one');
|
|
300
|
+
t.includes(none, 'sign in once', 'the launcher sign-in route');
|
|
301
|
+
t.excludes(none, 'Build.bat', 'and gives no build commands');
|
|
302
|
+
t.equal(unrealSummaryLine(status, 'C:\\Projects\\Work\\MyGame'), 'unreal: UE_5.4 5.4.4 (this PC, read-only)', 'the tools line names the engine');
|
|
303
|
+
t.section("UnrealBuildTool's view of Visual Studio");
|
|
304
|
+
// What sandbox\scripts\get-vs-setup.ps1 reported for Visual Studio kept on the data disk (2026-11-04).
|
|
305
|
+
const report = {
|
|
306
|
+
setupError: '',
|
|
307
|
+
instances: [
|
|
308
|
+
{
|
|
309
|
+
instanceId: 'a190d188',
|
|
310
|
+
installationPath: 'C:\\AgentData\\tools\\visualstudio\\persist\\Community',
|
|
311
|
+
installationVersion: '17.14.37827.9',
|
|
312
|
+
productId: 'Microsoft.VisualStudio.Product.Community',
|
|
313
|
+
state: 4294967295,
|
|
314
|
+
isComplete: true,
|
|
315
|
+
isLaunchable: true,
|
|
316
|
+
packages: ['Microsoft.VisualStudio.Workload.NativeGame', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64'],
|
|
317
|
+
toolchains: [{ version: '14.44.35207', cl: true, link: true }],
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
kitsRoots: [{ where: 'HKLM (64-bit)', path: 'C:\\AgentData\\tools\\visualstudio\\persist\\WindowsKits10\\' }],
|
|
321
|
+
sdks: [
|
|
322
|
+
{
|
|
323
|
+
version: '10.0.22621.0',
|
|
324
|
+
root: 'C:\\AgentData\\tools\\visualstudio\\persist\\WindowsKits10\\',
|
|
325
|
+
windowsH: true,
|
|
326
|
+
kernel32: true,
|
|
327
|
+
ucrt: true,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
const verdict = vsVerdict(report);
|
|
332
|
+
t.check(verdict.ok && verdict.text.includes('MSVC 14.44.35207') && verdict.text.includes('Windows SDK 10.0.22621.0'), 'the restored copy: found, with MSVC and the Windows SDK');
|
|
333
|
+
t.check(!vsVerdict({ ...report, instances: [] }).ok, 'no instance: not found');
|
|
334
|
+
t.check(!vsVerdict({ ...report, instances: [{ ...report.instances[0], packages: [] }] }).ok, 'no C++ x64 tools: not found');
|
|
335
|
+
t.check(!vsVerdict({ ...report, sdks: [] }).ok, 'no Windows SDK: not found');
|
|
336
|
+
t.check(!vsVerdict({ ...report, instances: [{ ...report.instances[0], state: 0 }] }).ok, 'an instance that is not local (state 0): not usable, as UnrealBuildTool sees it');
|
|
337
|
+
const old = vsVerdict({
|
|
338
|
+
...report,
|
|
339
|
+
instances: [{ ...report.instances[0], toolchains: [{ version: '14.36.1', cl: true, link: true }] }],
|
|
340
|
+
});
|
|
341
|
+
t.check(!old.ok && old.text.includes('too old'), 'MSVC older than 14.38: too old for Unreal 5.4+');
|
|
342
|
+
t.section("this PC's vault cache (assets)");
|
|
343
|
+
t.equal(vaultDirsFromIni('[Launcher]\r\nVaultCacheDirectories=("D:/EpicVault/VaultCache/")\r\n+VaultCacheDirectories=E:\\More\\VaultCache\\\r\n'), ['D:\\EpicVault\\VaultCache', 'E:\\More\\VaultCache'], 'the folders the launcher settings name (array and plain lines, either slash)');
|
|
344
|
+
t.equal(vaultDirsFromIni('[Launcher]\r\nDefaultAppInstallLocation=D:\\E\r\n'), [], 'none named: the default is used');
|
|
345
|
+
const vault = t.dir('VaultCache');
|
|
346
|
+
t.write(path.join(vault, 'CityPark', 'data', 'Content', 'Maps', 'Park.umap'), 'x'.repeat(2048));
|
|
347
|
+
t.write(path.join(vault, 'Toolkit', 'data', 'Toolkit.uplugin'), '{}');
|
|
348
|
+
t.write(path.join(vault, 'Toolkit', 'data', 'Source', 'a.cpp'), 'int a;');
|
|
349
|
+
t.dir('VaultCache', '.tmp');
|
|
350
|
+
t.equal(vaultPacks(vault, VAULT_DISK).map((p) => [p.name, p.kind, p.partial]), [
|
|
351
|
+
['CityPark', 'content', false],
|
|
352
|
+
['Toolkit', 'plugin', false],
|
|
353
|
+
], 'each pack, content or a code plugin');
|
|
354
|
+
t.equal(vaultPacks(vault, VAULT_DISK)[0].sizeBytes, 2048, 'with its size');
|
|
355
|
+
t.equal(vaultInstalls(['C:\\ProgramData\\Epic\\EpicGamesLauncher\\VaultCache', 'D:\\V']).map((v) => [v.kind, v.id]), [
|
|
356
|
+
['unreal-vault', 'Vault'],
|
|
357
|
+
['unreal-vault', 'Vault2'],
|
|
358
|
+
], 'mapped at C:\\PcInstalls\\unreal\\Vault (Vault2 ...)');
|
|
359
|
+
t.section("this PC's engines (fixtures)");
|
|
360
|
+
const pcRoot = t.dir('pc');
|
|
361
|
+
const pcLauncher = makeEngine(path.join(pcRoot, 'Epic Games', 'UE_5.4'), [5, 4, 4], true);
|
|
362
|
+
const pcDefault = makeEngine(path.join(pcRoot, 'Epic Games', 'UE_5.6'), [5, 6, 1], true);
|
|
363
|
+
const pcSource = makeEngine(path.join(pcRoot, 'src', 'UnrealEngine'), [5, 7, 0], false);
|
|
364
|
+
const dat = JSON.stringify({
|
|
365
|
+
InstallationList: [
|
|
366
|
+
foreign,
|
|
367
|
+
{
|
|
368
|
+
InstallLocation: pcLauncher,
|
|
369
|
+
NamespaceId: 'ue',
|
|
370
|
+
ItemId: 'item54',
|
|
371
|
+
ArtifactId: 'UE_5.4',
|
|
372
|
+
AppVersion: '5.4.4-1+++UE5+Release-5.4',
|
|
373
|
+
AppName: 'UE_5.4',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
InstallLocation: path.join(pcRoot, 'gone'),
|
|
377
|
+
NamespaceId: 'ue',
|
|
378
|
+
ItemId: 'x',
|
|
379
|
+
ArtifactId: 'UE_5.3',
|
|
380
|
+
AppVersion: 'v',
|
|
381
|
+
AppName: 'UE_5.3',
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
});
|
|
385
|
+
const reg = '\r\nHKEY_CURRENT_USER\\Software\\Epic Games\\Unreal Engine\\Builds\r\n {2D9FCA5D-4E7B-4C12-8B6E-1A2B3C4D5E6F} REG_SZ ' +
|
|
386
|
+
pcSource.replace(/\\/g, '/') +
|
|
387
|
+
'\r\n Broken REG_SZ not-a-path\r\n';
|
|
388
|
+
t.equal(regValues(reg).map((v) => v.name), ['{2D9FCA5D-4E7B-4C12-8B6E-1A2B3C4D5E6F}', 'Broken'], 'reg query output is read');
|
|
389
|
+
const files = {
|
|
390
|
+
launcherInstalled: dat,
|
|
391
|
+
builds: reg,
|
|
392
|
+
defaultRoot: path.join(pcRoot, 'Epic Games'),
|
|
393
|
+
list: DISK.list,
|
|
394
|
+
read: DISK.read,
|
|
395
|
+
};
|
|
396
|
+
const pcFound = findUnrealInstalls(files, true);
|
|
397
|
+
t.equal(pcFound.map((i) => [i.id, i.version, i.launcher?.itemId ?? null, i.buildId ?? null]), [
|
|
398
|
+
['UE_5.4', '5.4.4', 'item54', null],
|
|
399
|
+
['UE_5.6', '5.6.1', null, null],
|
|
400
|
+
], "the launcher's engines (with its ids) and the default folders: a source build is not mapped by itself");
|
|
401
|
+
t.check(pcFound.every((i) => i.host !== path.join(pcRoot, 'gone')), 'an entry whose folder is gone is left out');
|
|
402
|
+
t.equal(findUnrealInstalls(files, ['5.6']).map((i) => i.host), [pcDefault], 'a list maps only those versions');
|
|
403
|
+
t.equal(findUnrealInstalls({ ...files, launcherInstalled: null, builds: '' }, true).map((i) => i.id), ['UE_5.4', 'UE_5.6'], 'no LauncherInstalled.dat: the default folders');
|
|
404
|
+
const folders = findEngineFolders({ read: DISK.read, builds: reg }, [
|
|
405
|
+
pcSource,
|
|
406
|
+
path.join(pcRoot, 'nothing'),
|
|
407
|
+
'relative',
|
|
408
|
+
]);
|
|
409
|
+
t.equal(folders.map((f) => [f.id, f.writable]), [['dir-UnrealEngine', true]], 'a registered folder is mapped writable; one without an engine is left out');
|
|
410
|
+
t.equal(findEpicGames(dat, true).map((g) => g.id), ['Fortnite'], "the launcher's games are the epic-game installs (engines are not)");
|
|
411
|
+
t.equal(findEpicGames(dat, ['nothing']), [], 'a list maps only those games');
|
|
412
|
+
t.check(engineFolderProblem(pcSource, DISK.read) === null, 'Register an engine folder accepts an engine folder');
|
|
413
|
+
t.includes(engineFolderProblem(pcRoot, DISK.read) ?? '', 'not an Unreal Engine folder', 'and refuses one without Build.version');
|
|
414
|
+
t.section('"auto": the engines come along when the sandbox is set up for Unreal');
|
|
415
|
+
const one = (kind, setting, host, extra = {}) => ({
|
|
416
|
+
kind,
|
|
417
|
+
setting,
|
|
418
|
+
find: () => [{ kind, id: kind === 'unreal' ? 'UE_5.4' : 'Vault', name: 'x', host, guest: '', ...extra }],
|
|
419
|
+
});
|
|
420
|
+
const sources = [
|
|
421
|
+
one('unreal', 'unrealEngineFolders', pcSource, { id: 'dir-UnrealEngine', writable: true }),
|
|
422
|
+
one('unreal', 'unrealEngines', pcLauncher),
|
|
423
|
+
one('unreal-vault', 'unrealAssets', vault),
|
|
424
|
+
];
|
|
425
|
+
const o = { packageRoot: t.packageRoot, storageRoot: t.dir('storage'), sources };
|
|
426
|
+
const PC = { localAppData: '', appData: '', programData: '' };
|
|
427
|
+
t.equal(DEFAULTS.pcInstalls.unrealEngines, 'auto', 'pcInstalls.unrealEngines is "auto" by default');
|
|
428
|
+
t.equal(DEFAULTS.pcInstalls.unrealAssets, 'auto', 'pcInstalls.unrealAssets is "auto" by default');
|
|
429
|
+
const off = discoverPcInstalls({ ...DEFAULTS.pcInstalls, unrealEngineFolders: [pcSource] }, { ...o, unrealOn: false }, PC);
|
|
430
|
+
t.equal(off.mappings.map((m) => m.guest), ['C:\\PcInstalls\\unreal\\dir-UnrealEngine'], 'not set up for Unreal: only the folders you registered');
|
|
431
|
+
const on = discoverPcInstalls({ ...DEFAULTS.pcInstalls, unrealEngineFolders: [pcSource] }, { ...o, unrealOn: true }, PC);
|
|
432
|
+
t.equal(on.mappings.map((m) => [m.guest, m.readOnly]), [
|
|
433
|
+
['C:\\PcInstalls\\unreal\\dir-UnrealEngine', false],
|
|
434
|
+
['C:\\PcInstalls\\unreal\\UE_5.4', true],
|
|
435
|
+
['C:\\PcInstalls\\unreal\\Vault', true],
|
|
436
|
+
], "set up for Unreal: this PC's engines and vault read-only, your registered folder writable");
|
|
437
|
+
t.check(on.manifest.items.every((i) => !('host' in i)), 'session.json gets no path of this PC');
|
|
438
|
+
t.check(ConfigSchema.shape.pcInstalls.safeParse({ ...DEFAULTS.pcInstalls, unrealEngines: 'auto' }).success, 'the schema takes "auto"');
|
|
439
|
+
t.section('the game-development apps');
|
|
440
|
+
const ids = APPS.map((a) => a.id);
|
|
441
|
+
t.check(new Set(ids).size === ids.length, 'every app id is used once');
|
|
442
|
+
t.equal(GAMEDEV_APPS.map((a) => a.id), ['krita', 'inkscape', 'audacity', 'renderdoc', 'f3d', 'imagemagick', 'nomacs'], 'Krita, Inkscape, Audacity, RenderDoc, then F3D, ImageMagick, nomacs for previews (Blender was there)');
|
|
443
|
+
t.check(ids.includes('blender'), 'Blender is in the catalogue');
|
|
444
|
+
t.check(GAMEDEV_APPS.every((a) => a.category === 'gamedev' && a.exe && a.about.length > 20), 'each is a game-development app with its program');
|
|
445
|
+
const audacity = new RegExp(GAMEDEV_APPS.find((a) => a.id === 'audacity').source.asset);
|
|
446
|
+
t.check(audacity.test('audacity-win-4.0.2-x86_64.7z') &&
|
|
447
|
+
audacity.test('audacity-win-3.7.7-64bit.zip') &&
|
|
448
|
+
!audacity.test('audacity-win-4.0.2-arm64.7z'), "Audacity's GitHub asset: the x64 build of 3.x and 4.x");
|
|
449
|
+
t.section('the Unreal page');
|
|
450
|
+
const page = unrealPage(UNREAL_SAMPLE);
|
|
451
|
+
t.equal(page.sections.map((s) => s.id), ['engines', 'pc', 'vault', 'tools', 'settings'], 'engines, this PC, assets, build tools, settings');
|
|
452
|
+
const ops = pageActions(page).flatMap((a) => (a.kind === 'operation' ? [a.op] : []));
|
|
453
|
+
t.check(['unreal.registerFolder', 'unreal.forgetFolder', 'unreal.openLauncher'].every((op) => ops.includes(op)), 'register a folder, forget one, open the launcher');
|
|
454
|
+
t.check(unrealView.operations.every((op) => ops.includes(op.id)), 'every operation of the view is on the page');
|
|
455
|
+
const text = pageLines(page).join('\n');
|
|
456
|
+
for (const needle of [
|
|
457
|
+
'UE_5.6',
|
|
458
|
+
'this PC, read-only',
|
|
459
|
+
'C:\\AgentData\\tools\\unreal\\engines\\UE_5.4',
|
|
460
|
+
'CityParkEnvironment',
|
|
461
|
+
'MSVC 14.44.35207',
|
|
462
|
+
'toolchains.unreal.persistEngines',
|
|
463
|
+
'pcInstalls.unrealAssets',
|
|
464
|
+
])
|
|
465
|
+
t.includes(text, needle, 'the page shows ' + needle);
|
|
466
|
+
t.excludes(text, 'sandbox ', 'no page text tells you to run a command');
|
|
467
|
+
const problem = unrealPage({
|
|
468
|
+
...UNREAL_SAMPLE,
|
|
469
|
+
launcher: { ...UNREAL_SAMPLE.launcher, problem: 'ENGINES WOULD GO ON THE SANDBOX DISK: x' },
|
|
470
|
+
});
|
|
471
|
+
t.check(problem.notice?.tone === 'danger' && problem.notice.text.startsWith('ENGINES WOULD GO'), 'engines that would go on C: are said loudly at the top');
|
|
472
|
+
const empty = pageLines(unrealPage({ ...UNREAL_SAMPLE, engines: [], projects: [] })).join('\n');
|
|
473
|
+
t.includes(empty, 'Install Unreal Engine on this PC with the Epic Games Launcher', 'no engine: the page says how to get one');
|
|
474
|
+
t.section('every route on real disk: the engines, the registry, the launcher files, this PC');
|
|
475
|
+
const R = (...p) => path.join(t.dir('real'), ...p);
|
|
476
|
+
const rPc = makeEngine(R('PcInstalls', 'unreal', 'UE_5.4'), [5, 4, 4], true);
|
|
477
|
+
const rCopy = makeEngine(R('PcInstalls', 'unreal', 'dir-UE54Copy'), [5, 4, 1], true);
|
|
478
|
+
const rEngines = t.dir('real', 'tools', 'unreal', 'engines');
|
|
479
|
+
const rLauncher = makeEngine(path.join(rEngines, 'UE_5.5'), [5, 5, 0], true);
|
|
480
|
+
const rUnfinished = makeEngine(path.join(rEngines, 'UE_5.6'), [5, 6, 0], true);
|
|
481
|
+
const rSource = makeEngine(R('Projects', 'Work', 'UnrealEngine'), [5, 5, 1], false);
|
|
482
|
+
const rManifests = t.dir('real', 'Manifests');
|
|
483
|
+
t.write(path.join(rManifests, 'A1.item'), JSON.stringify({ InstallLocation: rLauncher, bIsIncompleteInstall: false, InstallSize: 1 }));
|
|
484
|
+
t.write(path.join(rManifests, 'B2.item'), JSON.stringify({ InstallLocation: rUnfinished, bIsIncompleteInstall: true, InstallSize: 1 }));
|
|
485
|
+
const guid = '{11111111-2222-3333-4444-555555555555}';
|
|
486
|
+
t.write(R('Projects', 'Work', 'MyGame', 'MyGame.uproject'), JSON.stringify({ EngineAssociation: guid }));
|
|
487
|
+
t.write(R('Projects', 'Work', 'OldGame', 'OldGame.uproject'), JSON.stringify({ EngineAssociation: '5.4' }));
|
|
488
|
+
const unfinished = incompleteDirs(rManifests);
|
|
489
|
+
t.equal([...unfinished], [rUnfinished.toLowerCase()], 'a launcher install that is not finished is listed as such');
|
|
490
|
+
const realEngines = findEngines({
|
|
491
|
+
mapped: [
|
|
492
|
+
{ guest: rPc, id: 'UE_5.4' },
|
|
493
|
+
{ guest: rCopy, id: 'dir-UE54Copy', writable: true },
|
|
494
|
+
],
|
|
495
|
+
enginesRoot: rEngines,
|
|
496
|
+
launcherDirs: [rLauncher.toLowerCase(), rUnfinished.toLowerCase()],
|
|
497
|
+
projectRoots: [R('Projects', 'Work')],
|
|
498
|
+
}, DISK).filter((e) => !unfinished.has(e.dir.toLowerCase()));
|
|
499
|
+
t.check(!realEngines.some((e) => e.dir === rUnfinished) && realEngines.some((e) => e.dir === rLauncher), 'the unfinished install (UE_5.6) is not an engine; the finished one (UE_5.5) is');
|
|
500
|
+
const projectChoice = (name) => {
|
|
501
|
+
const uproject = findUproject(R('Projects', 'Work', name), DISK);
|
|
502
|
+
return chooseEngine(realEngines, uprojectAssociation(fs.readFileSync(uproject, 'utf8')));
|
|
503
|
+
};
|
|
504
|
+
t.equal(projectChoice('OldGame').engine?.dir, rCopy, 'a project asking for 5.4 gets the writable copy, not the PC engine');
|
|
505
|
+
const mine = projectChoice('MyGame');
|
|
506
|
+
t.check(mine.engine?.dir === rSource && mine.alias === guid, "a project naming this PC's source build GUID gets the source build here, with the GUID as its alias");
|
|
507
|
+
const rPlan = registryPlan(realEngines, [{ id: guid, dir: rSource }]);
|
|
508
|
+
t.check(rPlan.some((v) => v.hive === 'HKCU' && v.name === guid && v.value === rSource), 'the registry plan registers the GUID for the source build (HKCU Builds)');
|
|
509
|
+
t.equal(rPlan.filter((v) => v.name === 'InstalledDirectory' && v.key.endsWith('\\5.4')).map((v) => v.value), [rCopy], 'HKLM 5.4 InstalledDirectory is the writable copy (one value per version)');
|
|
510
|
+
const entries = launcherEntriesFor(realEngines, [], []);
|
|
511
|
+
t.equal(entries.filter((e) => e.AppName === 'UE_5.4').map((e) => e.InstallLocation), [rCopy], 'LauncherInstalled.dat gets exactly one UE_5.4 entry (the preferred engine)');
|
|
512
|
+
t.equal(preferredInstalled(realEngines).map((e) => e.dir), [rLauncher, rCopy], 'one installed engine per version, newest first');
|
|
513
|
+
const fortnite = JSON.stringify({
|
|
514
|
+
InstallationList: [{ InstallLocation: 'C:\\Games\\Fortnite', AppName: 'Fortnite', AppVersion: '1' }],
|
|
515
|
+
});
|
|
516
|
+
const rMerged = launcherInstallations(mergeLauncherInstalled(fortnite, entries));
|
|
517
|
+
t.check(rMerged.some((i) => i.appName === 'Fortnite') && rMerged.some((i) => i.appName === 'UE_5.4'), 'merging into a file with a Fortnite entry keeps Fortnite');
|
|
518
|
+
const savedItems = R('state', 'Manifests');
|
|
519
|
+
t.equal(saveItems(rManifests, savedItems, rEngines).sort(), ['A1.item', 'B2.item'], 'the .item rManifests are saved');
|
|
520
|
+
const rFresh = t.dir('real', 'fresh', 'Manifests');
|
|
521
|
+
const rBack = restoreItems(savedItems, rFresh, rEngines, (d) => fs.existsSync(d));
|
|
522
|
+
t.check(fs.existsSync(path.join(rFresh, 'A1.item')) &&
|
|
523
|
+
fs.readFileSync(path.join(rFresh, 'A1.item'), 'utf8') ===
|
|
524
|
+
fs.readFileSync(path.join(rManifests, 'A1.item'), 'utf8') &&
|
|
525
|
+
rBack.includes(rLauncher), 'saved manifests come back byte for byte into a fresh Manifests folder');
|
|
526
|
+
const rIni = R('state', 'GameUserSettings.ini');
|
|
527
|
+
fs.mkdirSync(path.dirname(rIni), { recursive: true });
|
|
528
|
+
fs.writeFileSync(rIni, Buffer.concat([
|
|
529
|
+
Buffer.from([0xff, 0xfe]),
|
|
530
|
+
Buffer.from('[Other]\r\nKeep=1\r\n[Launcher]\r\nDefaultAppInstallLocation=C:\\Old\r\n', 'utf16le'),
|
|
531
|
+
]));
|
|
532
|
+
setLauncherInstallLocation(rIni, rEngines);
|
|
533
|
+
const after = fs.readFileSync(rIni);
|
|
534
|
+
const afterText = after.subarray(2).toString('utf16le');
|
|
535
|
+
t.check(after[0] === 0xff && after[1] === 0xfe && afterText.includes('Keep=1') && afterText.includes(rEngines), 'the install location in a UTF-16 GameUserSettings.ini keeps the byte-order mark and the other sections');
|
|
536
|
+
t.check(isBuildId('UE5 Main (CL1)') && isBuildId(guid), 'a registered name may hold spaces and brackets');
|
|
537
|
+
t.check(!isBuildId(' x') && !isBuildId('a"b') && !isBuildId('a\\b') && !isBuildId(''), 'but not quotes, slashes or edge spaces');
|
|
538
|
+
t.check(engineToolDirs(rPc, DISK).length === 1 && engineExtraDirs(rPc, DISK).every((d) => d.includes('Binaries')), 'only BatchFiles goes first on PATH; the DLL folders go last');
|
|
539
|
+
t.section("every route on real disk: this PC's files");
|
|
540
|
+
const pcRoot2 = t.dir('real', 'PC');
|
|
541
|
+
const pcLauncher2 = makeEngine(path.join(pcRoot2, 'Epic Games', 'UE_5.4'), [5, 4, 4], true);
|
|
542
|
+
makeEngine(path.join(pcRoot2, 'Epic Games', 'UE_5.3'), [5, 3, 2], true);
|
|
543
|
+
const pcSrc = makeEngine(path.join(pcRoot2, 'src', 'UnrealEngine'), [5, 7, 0], false);
|
|
544
|
+
const pcFiles = {
|
|
545
|
+
launcherInstalled: JSON.stringify({
|
|
546
|
+
InstallationList: [
|
|
547
|
+
{
|
|
548
|
+
InstallLocation: pcLauncher2,
|
|
549
|
+
NamespaceId: 'ue',
|
|
550
|
+
ItemId: 'i',
|
|
551
|
+
ArtifactId: 'UE_5.4',
|
|
552
|
+
AppVersion: 'v',
|
|
553
|
+
AppName: 'UE_5.4',
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
}),
|
|
557
|
+
builds: '\r\nHKEY_CURRENT_USER\\Software\\Epic Games\\Unreal Engine\\Builds\r\n ' +
|
|
558
|
+
guid +
|
|
559
|
+
' REG_SZ ' +
|
|
560
|
+
pcSrc +
|
|
561
|
+
'\r\n',
|
|
562
|
+
defaultRoot: path.join(pcRoot2, 'Epic Games'),
|
|
563
|
+
list: DISK.list,
|
|
564
|
+
read: DISK.read,
|
|
565
|
+
};
|
|
566
|
+
t.equal(findUnrealInstalls(pcFiles, true).map((i) => i.id), ['UE_5.4', 'UE_5.3'], "the PC's launcher engine and the default folder's UE_5.3 are found, and no source build");
|
|
567
|
+
t.equal(pcSourceBuilds(pcFiles).map((b) => [b.buildId, b.host]), [[guid, pcSrc]], "the PC's source build is listed on its own");
|
|
568
|
+
const registered = findEngineFolders(pcFiles, [pcSrc]);
|
|
569
|
+
t.check(registered.length === 1 && registered[0].writable === true && registered[0].buildId === guid, "a registered source build is mapped writable and keeps the PC's build id");
|
|
570
|
+
const linkData = t.dir('real', 'data');
|
|
571
|
+
const target = t.dir('real', 'elsewhere');
|
|
572
|
+
t.write(path.join(target, UNREAL_STATUS_FILE), JSON.stringify(status));
|
|
573
|
+
t.write(path.join(linkData, 'plain', 'tools', 'unreal', UNREAL_STATUS_FILE), JSON.stringify(status));
|
|
574
|
+
t.check(readStatus(path.join(linkData, 'plain')) !== null, 'a status file in the data folder is read');
|
|
575
|
+
fs.mkdirSync(path.join(linkData, 'linked', 'tools'), { recursive: true });
|
|
576
|
+
fs.symlinkSync(target, path.join(linkData, 'linked', 'tools', 'unreal'), 'junction');
|
|
577
|
+
t.check(readStatus(path.join(linkData, 'linked')) === null, 'a junction at tools\\unreal is never followed');
|
|
578
|
+
t.check(!vsVerdict({
|
|
579
|
+
...report,
|
|
580
|
+
instances: [{ ...report.instances[0], state: 0 }],
|
|
581
|
+
}).ok, 'Visual Studio with state 0 is not usable');
|
|
582
|
+
if (!isWin)
|
|
583
|
+
return;
|
|
584
|
+
t.section('the Visual Studio probe: the helper compiles and the script reports');
|
|
585
|
+
const vsDir = t.dir('real', 'vs');
|
|
586
|
+
const vsCsc = spawnSync(SYSTEM.csc(), [
|
|
587
|
+
'/nologo',
|
|
588
|
+
'/target:library',
|
|
589
|
+
'/out:' + path.join(vsDir, 'VsSetup.dll'),
|
|
590
|
+
path.join(t.packageRoot, 'sandbox', 'csharp', 'VsSetup.cs'),
|
|
591
|
+
], { encoding: 'utf8', windowsHide: true });
|
|
592
|
+
t.check(vsCsc.status === 0, 'VsSetup.cs compiles with the C# 5 compiler', vsCsc.stdout);
|
|
593
|
+
const vsJson = path.join(vsDir, 'vs.json');
|
|
594
|
+
const vsRun = spawnSync(SYSTEM.powershell(), [
|
|
595
|
+
'-NoProfile',
|
|
596
|
+
'-ExecutionPolicy',
|
|
597
|
+
'Bypass',
|
|
598
|
+
'-File',
|
|
599
|
+
path.join(t.packageRoot, 'sandbox', 'scripts', 'get-vs-setup.ps1'),
|
|
600
|
+
'-ResultFile',
|
|
601
|
+
vsJson,
|
|
602
|
+
], { encoding: 'utf8', windowsHide: true, timeout: 120_000 });
|
|
603
|
+
t.check(vsRun.status === 0, 'get-vs-setup.ps1 runs', vsRun.stderr);
|
|
604
|
+
t.check(fs.existsSync(vsJson) && VsSetupReportSchema.safeParse(JSON.parse(fs.readFileSync(vsJson, 'utf8'))).success, 'and its report fits the schema');
|
|
6
605
|
},
|
|
7
606
|
};
|
|
@@ -34,7 +34,13 @@ export const guestBootSuite = {
|
|
|
34
34
|
];
|
|
35
35
|
const headers = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => boot.out.indexOf('[' + n + '/9] '));
|
|
36
36
|
t.check(headers.every((at, i) => at >= 0 && (i === 0 || at > headers[i - 1])), 'the boot prints its nine stages, [1/9] to [9/9], in order', headers.join(', '));
|
|
37
|
-
t.check(boot.out
|
|
37
|
+
t.check(/^Ready in \d+\.\ds/m.test(boot.out) && /^\[3\/9\] Developer tools +\d+\.\ds$/m.test(boot.out), 'the boot says how long it took and how long each stage took', boot.out);
|
|
38
|
+
const tools = boot.out.indexOf('\nTools\n');
|
|
39
|
+
t.check(!boot.out.includes(': started') &&
|
|
40
|
+
!/^\s*[A-Z][\w -]+ \| /m.test(boot.out) &&
|
|
41
|
+
!boot.out.includes('\x1b[') &&
|
|
42
|
+
tools > headers[8] &&
|
|
43
|
+
/\n {2}gh {2,}off \(not listed in toolchains\.cli\)\n/.test(boot.out.slice(tools)), 'the dry run prints plain lines: no per-task lines, no prefixes, no colours; the tool list once, after the stages', boot.out);
|
|
38
44
|
t.equal(steps.filter((s) => !boot.out.includes(s)), [], 'the guest provisions every enabled toolchain');
|
|
39
45
|
t.check(boot.out.includes('AgentSandbox-Block-Private') &&
|
|
40
46
|
boot.out.includes('remoteport=1-52,54-65535') &&
|