@myvillage/cli 2.2.0 → 2.3.0
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/package.json +1 -1
- package/src/commands/deploy.js +25 -2
- package/src/utils/preflight.js +95 -9
- package/src/utils/templates.js +70 -4
- package/tools/preflight/README.md +1 -1
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -463,6 +463,13 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
|
|
|
463
463
|
buildDir,
|
|
464
464
|
sdkVersion: mvConfig.sdkVersion,
|
|
465
465
|
platform,
|
|
466
|
+
onStatus: (text) => {
|
|
467
|
+
preflightSpinner.text = `[${platform}] ${text}`;
|
|
468
|
+
// Studio streams our stdout into its publish log and isn't a TTY, so
|
|
469
|
+
// the spinner's text changes never reach the developer there. Print a
|
|
470
|
+
// plain line too, otherwise the one-time UnityPy install looks like a hang.
|
|
471
|
+
if (!process.stderr.isTTY) console.log(brand.teal(` [${platform}] ${text}`));
|
|
472
|
+
},
|
|
466
473
|
});
|
|
467
474
|
if (preflightResult.skipped) {
|
|
468
475
|
if (preflightResult.fatal) {
|
|
@@ -471,7 +478,18 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
|
|
|
471
478
|
}
|
|
472
479
|
preflightSpinner.warn(`[${platform}] Preflight skipped: ${preflightResult.reason}`);
|
|
473
480
|
} else if (!preflightResult.ok) {
|
|
474
|
-
|
|
481
|
+
// "preflight_error" / "preflight_crash" mean the scanner itself failed
|
|
482
|
+
// (couldn't parse the bundle, script crashed). That's on us, not on the
|
|
483
|
+
// developer's scripts — so don't tell them to rewrite their game.
|
|
484
|
+
const toolingFailures = preflightResult.violations.filter(
|
|
485
|
+
(v) => v.kind === 'preflight_error' || v.kind === 'preflight_crash',
|
|
486
|
+
);
|
|
487
|
+
const codeViolations = preflightResult.violations.filter((v) => !toolingFailures.includes(v));
|
|
488
|
+
if (codeViolations.length > 0) {
|
|
489
|
+
preflightSpinner.fail(`[${platform}] Preflight found ${codeViolations.length} violation(s).`);
|
|
490
|
+
} else {
|
|
491
|
+
preflightSpinner.fail(`[${platform}] Preflight could not scan the bundle.`);
|
|
492
|
+
}
|
|
475
493
|
console.log();
|
|
476
494
|
for (const v of preflightResult.violations) {
|
|
477
495
|
console.log(chalk.red(` \u2717 ${v.kind}`));
|
|
@@ -481,7 +499,12 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
|
|
|
481
499
|
if (v.reason) console.log(` ${v.reason}`);
|
|
482
500
|
console.log();
|
|
483
501
|
}
|
|
484
|
-
|
|
502
|
+
if (codeViolations.length > 0) {
|
|
503
|
+
console.log(brand.teal(' Fix these scripts to extend MissionBase / use SDK templates, rebuild, and retry.'));
|
|
504
|
+
} else {
|
|
505
|
+
console.log(brand.teal(' This is a problem with the bundle scanner, not with your game.'));
|
|
506
|
+
console.log(brand.teal(' Rebuild the bundle in Unity and publish again. If it keeps happening, send the message above to the MyVillage team.'));
|
|
507
|
+
}
|
|
485
508
|
console.log();
|
|
486
509
|
return { ok: false };
|
|
487
510
|
} else {
|
package/src/utils/preflight.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { existsSync, mkdtempSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir, tmpdir } from 'os';
|
|
4
4
|
import { dirname, join, resolve } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { getGameKitAllowlist } from './api.js';
|
|
@@ -14,6 +14,91 @@ const PREFLIGHT_SCRIPT = resolve(__dirname, '../../tools/preflight/preflight.py'
|
|
|
14
14
|
|
|
15
15
|
const PYTHON_CANDIDATES = ['python3', 'python'];
|
|
16
16
|
|
|
17
|
+
// The bundle scanner needs UnityPy. Rather than asking every game developer to
|
|
18
|
+
// pip-install it by hand (the #1 reason deploys failed with "preflight_error"),
|
|
19
|
+
// we install it once into a private venv under ~/.myvillage and reuse it.
|
|
20
|
+
const UNITYPY_SPEC = 'UnityPy==1.25.0';
|
|
21
|
+
const PREFLIGHT_VENV_DIR = join(homedir(), '.myvillage', 'tools', 'preflight-venv');
|
|
22
|
+
|
|
23
|
+
function venvPython(dir) {
|
|
24
|
+
return process.platform === 'win32' ? join(dir, 'Scripts', 'python.exe') : join(dir, 'bin', 'python');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Run a command, capture output, never throw. */
|
|
28
|
+
function runCapture(cmd, args) {
|
|
29
|
+
return new Promise((resolveRun) => {
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
let proc;
|
|
33
|
+
try {
|
|
34
|
+
proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
resolveRun({ code: -1, stdout, stderr: err.message });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
proc.stdout.on('data', (b) => { stdout += b.toString(); });
|
|
40
|
+
proc.stderr.on('data', (b) => { stderr += b.toString(); });
|
|
41
|
+
proc.on('error', (err) => resolveRun({ code: -1, stdout, stderr: err.message }));
|
|
42
|
+
proc.on('exit', (code) => resolveRun({ code, stdout, stderr }));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function hasUnityPy(python) {
|
|
47
|
+
const r = await runCapture(python, ['-c', 'import UnityPy']);
|
|
48
|
+
return r.code === 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lastLine(text) {
|
|
52
|
+
const lines = (text || '').trim().split(/\r?\n/).filter(Boolean);
|
|
53
|
+
return lines.length ? lines[lines.length - 1] : '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find a Python that can import UnityPy, installing it if needed.
|
|
58
|
+
*
|
|
59
|
+
* Order: our managed venv → a system Python that already has UnityPy →
|
|
60
|
+
* create the managed venv with the system Python and pip-install UnityPy.
|
|
61
|
+
* `onStatus(text)` is called with progress copy for the caller's spinner.
|
|
62
|
+
*
|
|
63
|
+
* Returns { python } on success, or { python: null, reason } when there's no
|
|
64
|
+
* usable interpreter — the caller decides whether that skips or blocks.
|
|
65
|
+
*/
|
|
66
|
+
export async function ensurePreflightPython({ onStatus } = {}) {
|
|
67
|
+
const managed = venvPython(PREFLIGHT_VENV_DIR);
|
|
68
|
+
if (existsSync(managed) && await hasUnityPy(managed)) return { python: managed };
|
|
69
|
+
|
|
70
|
+
const system = await findPython();
|
|
71
|
+
if (!system) {
|
|
72
|
+
return {
|
|
73
|
+
python: null,
|
|
74
|
+
reason: 'Python 3 was not found on your PATH. Install Python 3.9+ from python.org (on Windows, tick "Add to PATH"), then publish again.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (await hasUnityPy(system)) return { python: system };
|
|
78
|
+
|
|
79
|
+
const manualHint = `${system} -m pip install "${UNITYPY_SPEC}"`;
|
|
80
|
+
|
|
81
|
+
onStatus?.('First publish from this computer: setting up the bundle scanner (one-time, about a minute)...');
|
|
82
|
+
mkdirSync(PREFLIGHT_VENV_DIR, { recursive: true });
|
|
83
|
+
const venv = await runCapture(system, ['-m', 'venv', PREFLIGHT_VENV_DIR]);
|
|
84
|
+
if (venv.code !== 0 || !existsSync(managed)) {
|
|
85
|
+
return {
|
|
86
|
+
python: null,
|
|
87
|
+
reason: `Could not create a Python environment at ${PREFLIGHT_VENV_DIR} (${lastLine(venv.stderr) || `exit ${venv.code}`}). Repair your Python install or run: ${manualHint}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onStatus?.('Installing the bundle scanner (UnityPy)...');
|
|
92
|
+
const pip = await runCapture(managed, ['-m', 'pip', 'install', '--disable-pip-version-check', '--quiet', UNITYPY_SPEC]);
|
|
93
|
+
if (pip.code !== 0 || !(await hasUnityPy(managed))) {
|
|
94
|
+
return {
|
|
95
|
+
python: null,
|
|
96
|
+
reason: `Could not install ${UNITYPY_SPEC} (${lastLine(pip.stderr) || `exit ${pip.code}`}). Check your network connection and retry, or run: ${manualHint}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { python: managed };
|
|
100
|
+
}
|
|
101
|
+
|
|
17
102
|
/**
|
|
18
103
|
* Find the first usable Python interpreter on PATH.
|
|
19
104
|
* Returns the command name (e.g. "python3") or null if none works.
|
|
@@ -94,11 +179,11 @@ async function runPreflight(python, bundlePath, allowlistPath) {
|
|
|
94
179
|
* Run preflight against all AssetBundles in a build directory.
|
|
95
180
|
*
|
|
96
181
|
* Returns:
|
|
97
|
-
* { skipped: true, reason } — bundles couldn't be checked (no SDK, no Python, etc.)
|
|
182
|
+
* { skipped: true, reason } — bundles couldn't be checked (no SDK, no usable Python, etc.)
|
|
98
183
|
* { ok: true, bundlesChecked } — all bundles passed
|
|
99
184
|
* { ok: false, violations, bundle } — at least one violation found
|
|
100
185
|
*/
|
|
101
|
-
export async function preflightBundles({ buildDir, sdkVersion, platform }) {
|
|
186
|
+
export async function preflightBundles({ buildDir, sdkVersion, platform, onStatus }) {
|
|
102
187
|
if (!sdkVersion) {
|
|
103
188
|
return { skipped: true, reason: 'No sdkVersion in myvillage.json — preflight only runs for SDK-based games.' };
|
|
104
189
|
}
|
|
@@ -109,12 +194,13 @@ export async function preflightBundles({ buildDir, sdkVersion, platform }) {
|
|
|
109
194
|
return { skipped: true, reason: `preflight.py not found at ${PREFLIGHT_SCRIPT} — CLI install may be incomplete.` };
|
|
110
195
|
}
|
|
111
196
|
|
|
112
|
-
|
|
197
|
+
// No usable Python (or UnityPy couldn't be installed) is a *tooling* gap on
|
|
198
|
+
// the developer's machine, never a problem with their game. Skip with a clear
|
|
199
|
+
// warning instead of blocking — the same outcome for "no Python at all" and
|
|
200
|
+
// "Python but no UnityPy", which used to diverge (one skipped, one failed).
|
|
201
|
+
const { python, reason: pythonReason } = await ensurePreflightPython({ onStatus });
|
|
113
202
|
if (!python) {
|
|
114
|
-
return {
|
|
115
|
-
skipped: true,
|
|
116
|
-
reason: 'Python 3 not found on PATH. Install Python 3.9+ and then run: pip install UnityPy==1.25.0',
|
|
117
|
-
};
|
|
203
|
+
return { skipped: true, reason: `${pythonReason} (Bundle scan skipped; the platform re-checks on upload.)` };
|
|
118
204
|
}
|
|
119
205
|
|
|
120
206
|
let allowlistResponse;
|
package/src/utils/templates.js
CHANGED
|
@@ -12,7 +12,7 @@ const GAME_ART_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets
|
|
|
12
12
|
|
|
13
13
|
// GameKit SDK version that scaffolded Unity projects pin to.
|
|
14
14
|
// Bump alongside CLI minor releases as the SDK contract evolves.
|
|
15
|
-
const GAMEKIT_SDK_VERSION = '1.1.0-alpha.
|
|
15
|
+
const GAMEKIT_SDK_VERSION = '1.1.0-alpha.5';
|
|
16
16
|
const GAMEKIT_SDK_URL =
|
|
17
17
|
`https://github.com/MyVillage-Project-Technologies/myvillage-gamekit.git#v${GAMEKIT_SDK_VERSION}`;
|
|
18
18
|
|
|
@@ -1302,7 +1302,7 @@ export async function createUnityGameProject(targetDir, options) {
|
|
|
1302
1302
|
// Unity project files. When the SDK is embedded it must NOT also be listed as
|
|
1303
1303
|
// a manifest dependency (Unity would try to re-resolve it); when it isn't, the
|
|
1304
1304
|
// git-URL entry is the fallback resolution path.
|
|
1305
|
-
writeFileSync(join(targetDir, 'Packages/manifest.json'), generateUnityPackagesManifest(sdkEmbedded));
|
|
1305
|
+
writeFileSync(join(targetDir, 'Packages/manifest.json'), generateUnityPackagesManifest(sdkEmbedded, unityVersion));
|
|
1306
1306
|
writeFileSync(join(targetDir, 'ProjectSettings/ProjectVersion.txt'), generateUnityProjectVersion(unityVersion));
|
|
1307
1307
|
|
|
1308
1308
|
// Starter mission. New devs subclass MissionBase here.
|
|
@@ -1367,7 +1367,59 @@ public static class MyVillageOrientation
|
|
|
1367
1367
|
|
|
1368
1368
|
// ── Unity (GameKit SDK) ─────────────────────────────────────────────
|
|
1369
1369
|
|
|
1370
|
-
|
|
1370
|
+
// Unity's built-in engine modules. A project created through Unity Hub lists
|
|
1371
|
+
// every one of these in Packages/manifest.json; a manifest that omits them
|
|
1372
|
+
// DISABLES them. An earlier version of this scaffold wrote only the registry
|
|
1373
|
+
// packages, so fresh projects had no Particle System, Terrain, Animation,
|
|
1374
|
+
// Tilemap, Physics2D, Video, ... and developers had to hand-restore them.
|
|
1375
|
+
const UNITY_BUILTIN_MODULES = [
|
|
1376
|
+
'accessibility',
|
|
1377
|
+
'ai',
|
|
1378
|
+
'androidjni',
|
|
1379
|
+
'animation',
|
|
1380
|
+
'assetbundle',
|
|
1381
|
+
'audio',
|
|
1382
|
+
'cloth',
|
|
1383
|
+
'director',
|
|
1384
|
+
'imageconversion',
|
|
1385
|
+
'imgui',
|
|
1386
|
+
'jsonserialize',
|
|
1387
|
+
'particlesystem',
|
|
1388
|
+
'physics',
|
|
1389
|
+
'physics2d',
|
|
1390
|
+
'screencapture',
|
|
1391
|
+
'terrain',
|
|
1392
|
+
'terrainphysics',
|
|
1393
|
+
'tilemap',
|
|
1394
|
+
'ui',
|
|
1395
|
+
'uielements',
|
|
1396
|
+
'umbra',
|
|
1397
|
+
'unityanalytics',
|
|
1398
|
+
'unitywebrequest',
|
|
1399
|
+
'unitywebrequestassetbundle',
|
|
1400
|
+
'unitywebrequestaudio',
|
|
1401
|
+
'unitywebrequesttexture',
|
|
1402
|
+
'unitywebrequestwww',
|
|
1403
|
+
'vehicles',
|
|
1404
|
+
'video',
|
|
1405
|
+
'vr',
|
|
1406
|
+
'wind',
|
|
1407
|
+
'xr',
|
|
1408
|
+
];
|
|
1409
|
+
|
|
1410
|
+
// Visual Scripting version pinned in the scaffold manifest. Keep in sync with
|
|
1411
|
+
// the `com.unity.visualscripting` dependency in the GameKit's package.json —
|
|
1412
|
+
// GameKit's MyVillage.GameKit.VisualScripting assembly references it, and an
|
|
1413
|
+
// embedded package's own dependency isn't enough on its own for Unity to keep
|
|
1414
|
+
// it resolved, so the manifest lists it explicitly.
|
|
1415
|
+
const UNITY_VISUAL_SCRIPTING_VERSION = '1.9.4';
|
|
1416
|
+
|
|
1417
|
+
function unityMajor(unityVersion) {
|
|
1418
|
+
const m = /^(\d+)\./.exec(unityVersion || '');
|
|
1419
|
+
return m ? Number(m[1]) : 6000;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function generateUnityPackagesManifest(sdkEmbedded = true, unityVersion = UNITY_EDITOR_VERSION_FALLBACK) {
|
|
1371
1423
|
const dependencies = {};
|
|
1372
1424
|
// Only declare the SDK as a git dependency when it could NOT be embedded
|
|
1373
1425
|
// locally. When embedded under Packages/com.myvillage.gamekit/, Unity
|
|
@@ -1378,11 +1430,25 @@ function generateUnityPackagesManifest(sdkEmbedded = true) {
|
|
|
1378
1430
|
Object.assign(dependencies, {
|
|
1379
1431
|
'com.unity.feature.development': '1.0.2',
|
|
1380
1432
|
'com.unity.render-pipelines.universal': '17.2.0',
|
|
1381
|
-
'com.unity.textmeshpro': '3.0.6',
|
|
1382
1433
|
'com.unity.inputsystem': '1.14.2',
|
|
1434
|
+
// GameKit ships Visual Scripting units (CompleteMission, ReadConfig, ...).
|
|
1435
|
+
// Without this package the SDK's VisualScripting assembly can't compile.
|
|
1436
|
+
'com.unity.visualscripting': UNITY_VISUAL_SCRIPTING_VERSION,
|
|
1383
1437
|
'com.unity.ide.rider': '3.0.38',
|
|
1384
1438
|
'com.unity.ide.visualstudio': '2.0.22',
|
|
1385
1439
|
});
|
|
1440
|
+
// Unity 6 folded TextMesh Pro into com.unity.ugui 2.x; declaring the old
|
|
1441
|
+
// standalone com.unity.textmeshpro there triggers a deprecation conflict.
|
|
1442
|
+
// Older editors still need the standalone package.
|
|
1443
|
+
if (unityMajor(unityVersion) >= 6000) {
|
|
1444
|
+
dependencies['com.unity.ugui'] = '2.0.0';
|
|
1445
|
+
} else {
|
|
1446
|
+
dependencies['com.unity.ugui'] = '1.0.0';
|
|
1447
|
+
dependencies['com.unity.textmeshpro'] = '3.0.6';
|
|
1448
|
+
}
|
|
1449
|
+
for (const mod of UNITY_BUILTIN_MODULES) {
|
|
1450
|
+
dependencies[`com.unity.modules.${mod}`] = '1.0.0';
|
|
1451
|
+
}
|
|
1386
1452
|
return JSON.stringify({ dependencies }, null, 2) + '\n';
|
|
1387
1453
|
}
|
|
1388
1454
|
|
|
@@ -16,7 +16,7 @@ Requires **Python 3.9+** and **UnityPy 1.25.0** (specifically — newer versions
|
|
|
16
16
|
pip install -r requirements.txt
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The CLI
|
|
19
|
+
The CLI finds `python3`/`python` on PATH and, if UnityPy isn't importable, installs it once into a private venv at `~/.myvillage/tools/preflight-venv/` (see `ensurePreflightPython` in `src/utils/preflight.js`). If Python is missing or the install fails, preflight is skipped with a warning and the upload proceeds; the ed-platform re-checks on upload.
|
|
20
20
|
|
|
21
21
|
## Run directly (for debugging)
|
|
22
22
|
|