ravensight-playtest 0.5.1 → 0.6.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/README.md +297 -14
- package/addons/ravensight_driver/driver.gd +406 -8
- package/addons/ravensight_simulate/simulate.gd +360 -0
- package/docs/godot-driver.md +70 -8
- package/package.json +4 -3
- package/src/api/README.md +4 -1
- package/src/api/client.js +94 -4
- package/src/api/errors.js +17 -1
- package/src/api/http.js +56 -20
- package/src/assets/formats.js +267 -0
- package/src/assets/index.js +260 -0
- package/src/assets/integration.js +429 -0
- package/src/assets/inventory.js +337 -0
- package/src/assets/measure.js +392 -0
- package/src/cli.js +209 -0
- package/src/commands/assets.js +62 -0
- package/src/commands/build.js +81 -0
- package/src/commands/check.js +4 -1
- package/src/commands/create.js +734 -0
- package/src/commands/fakeCore.js +16 -1
- package/src/commands/feature.js +845 -0
- package/src/commands/fix.js +578 -68
- package/src/commands/hosted.js +39 -0
- package/src/commands/init.js +19 -3
- package/src/commands/profile.js +25 -6
- package/src/commands/replay.js +204 -0
- package/src/commands/resume.js +7 -0
- package/src/commands/run.js +200 -24
- package/src/commands/template.js +70 -0
- package/src/commands/validate.js +414 -0
- package/src/commands/validateHosted.js +292 -0
- package/src/create/scaffold.js +192 -0
- package/src/hosted/archivePreflight.js +240 -0
- package/src/hosted/buildUpload.js +118 -0
- package/src/hosted/recordedRun.js +191 -0
- package/src/run/acceptance.js +212 -0
- package/src/run/args.js +18 -1
- package/src/run/checks.js +705 -0
- package/src/run/dimension.js +95 -0
- package/src/run/drivers/godot-observation.js +59 -11
- package/src/run/drivers/godot-project.js +201 -31
- package/src/run/drivers/godot.js +532 -45
- package/src/run/drivers/web.js +18 -0
- package/src/run/evidence.js +170 -0
- package/src/run/featureLoop.js +288 -0
- package/src/run/fixLoop.js +82 -12
- package/src/run/git.js +110 -20
- package/src/run/json.js +65 -9
- package/src/run/labsOutcome.js +136 -0
- package/src/run/liveWork.js +103 -0
- package/src/run/model.js +105 -5
- package/src/run/openPr.js +107 -0
- package/src/run/paths.js +4 -1
- package/src/run/pendingRegister.js +81 -0
- package/src/run/personaLoop.js +111 -4
- package/src/run/profile.js +68 -7
- package/src/run/replay.js +236 -0
- package/src/run/repoTools.js +227 -27
- package/src/run/report.js +7 -1
- package/src/run/resume.js +19 -1
- package/src/run/secretScan.js +35 -0
- package/src/run/settle.js +41 -0
- package/src/run/tools.js +33 -4
- package/src/run/trace.js +124 -0
- package/src/simulation/godotPack.js +112 -0
- package/src/ui/index.js +61 -9
- package/src/upload/allowlist.js +6 -2
- package/src/upload/index.js +5 -4
- package/templates/godot-2d/README.md +36 -0
- package/templates/godot-2d/addons/ravensight/LICENSE +21 -0
- package/templates/godot-2d/addons/ravensight/Ravensight.gd +1823 -0
- package/templates/godot-2d/export_presets.cfg +27 -0
- package/templates/godot-2d/gitignore +10 -0
- package/templates/godot-2d/project.godot +58 -0
- package/templates/godot-2d/scenes/main.tscn +67 -0
- package/templates/godot-2d/scripts/autoload/game_config.gd +83 -0
- package/templates/godot-2d/scripts/autoload/game_state.gd +44 -0
- package/templates/godot-2d/scripts/autoload/telemetry.gd +37 -0
- package/templates/godot-2d/scripts/main.gd +32 -0
- package/templates/godot-2d/scripts/player.gd +10 -0
- package/templates/godot-2d/scripts/state_hook.gd +16 -0
- package/templates/godot-2d/template.json +14 -0
- package/templates/godot-2d/tools/inject_release_key.sh +19 -0
- package/templates/godot-2d/tools/selftest.gd +70 -0
- package/templates/godot-3d/README.md +49 -0
- package/templates/godot-3d/addons/ravensight/LICENSE +21 -0
- package/templates/godot-3d/addons/ravensight/Ravensight.gd +1823 -0
- package/templates/godot-3d/export_presets.cfg +27 -0
- package/templates/godot-3d/gitignore +10 -0
- package/templates/godot-3d/project.godot +56 -0
- package/templates/godot-3d/scenes/main.tscn +154 -0
- package/templates/godot-3d/scripts/autoload/game_config.gd +83 -0
- package/templates/godot-3d/scripts/autoload/game_state.gd +44 -0
- package/templates/godot-3d/scripts/autoload/telemetry.gd +37 -0
- package/templates/godot-3d/scripts/beacon.gd +20 -0
- package/templates/godot-3d/scripts/main.gd +49 -0
- package/templates/godot-3d/scripts/player.gd +19 -0
- package/templates/godot-3d/scripts/state_hook.gd +25 -0
- package/templates/godot-3d/template.json +16 -0
- package/templates/godot-3d/tools/inject_release_key.sh +19 -0
- package/templates/godot-3d/tools/selftest.gd +77 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import EXIT from '../run/exit.js';
|
|
5
|
+
import { scanUsage } from '../assets/integration.js';
|
|
6
|
+
import { buildInventory, countsLine, DEFAULT_ASSETS_DIR, describeFile, formatBytes, warningKinds } from '../assets/inventory.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `ravensight-playtest assets`: what a project's art and audio folder holds, as
|
|
10
|
+
* measured on this machine. It needs no login, sends nothing anywhere and costs
|
|
11
|
+
* nothing, so it can be run as often as a studio likes before a feature job.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} flags
|
|
14
|
+
* @param {Object} [injected] test seam
|
|
15
|
+
* @returns {Promise<number>} an exit code
|
|
16
|
+
*/
|
|
17
|
+
export async function assetsCommand(flags = {}, injected = {}) {
|
|
18
|
+
const log = injected.log || (line => console.log(line));
|
|
19
|
+
const repoRoot = path.resolve(flags.repoRoot || process.cwd());
|
|
20
|
+
const config = await loadConfig({ repoRoot }).catch(() => null);
|
|
21
|
+
const projectDir = path.resolve(repoRoot, flags.godotProject || config?.godot_project || '.');
|
|
22
|
+
const folder = flags.assetsDir || config?.assets_dir || DEFAULT_ASSETS_DIR;
|
|
23
|
+
const grid = Number.isInteger(flags.grid) && flags.grid > 0 ? flags.grid : null;
|
|
24
|
+
if (flags.grid !== undefined && !grid) {
|
|
25
|
+
log('--grid has to be a whole number of art pixels across, such as 32.');
|
|
26
|
+
return EXIT.ENVIRONMENT;
|
|
27
|
+
}
|
|
28
|
+
let inventory;
|
|
29
|
+
try {
|
|
30
|
+
inventory = await buildInventory({ projectDir, folder, grid });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
log(`The project could not be read: ${String(error && error.message)}`);
|
|
33
|
+
return EXIT.ENVIRONMENT;
|
|
34
|
+
}
|
|
35
|
+
if (!inventory.ok) {
|
|
36
|
+
log(inventory.reason);
|
|
37
|
+
return EXIT.ENVIRONMENT;
|
|
38
|
+
}
|
|
39
|
+
if (!inventory.exists) {
|
|
40
|
+
log(`There is no folder ${folder} in this project. Put your art and audio there, or name the folder with --assets-dir. A feature uses placeholders for anything it cannot find.`);
|
|
41
|
+
return flags.assetsDir ? EXIT.ENVIRONMENT : EXIT.OK;
|
|
42
|
+
}
|
|
43
|
+
const usage = await scanUsage({ projectDir, inventory });
|
|
44
|
+
if (flags.json) {
|
|
45
|
+
(injected.json || (value => console.log(JSON.stringify(value, null, 2))))({ ...inventory, usage: { used: usage.used, unused: usage.unused, built_in_code: usage.built_in_code } });
|
|
46
|
+
return EXIT.OK;
|
|
47
|
+
}
|
|
48
|
+
const used = new Set(usage.used);
|
|
49
|
+
log(`${inventory.folder}: ${countsLine(inventory.counts)}. ${usage.used.length} used by the project, ${usage.unused.length} unused.`);
|
|
50
|
+
for (const file of inventory.files) {
|
|
51
|
+
log(`${file.path} ${[file.format, formatBytes(file.bytes), describeFile(file)].filter(Boolean).join(', ')}${used.has(file.path) ? '' : ' (unused)'}`);
|
|
52
|
+
for (const item of file.warnings) log(` warning: ${item.text}`);
|
|
53
|
+
}
|
|
54
|
+
for (const item of inventory.warnings) log(`warning: ${item.text}`);
|
|
55
|
+
if (inventory.truncated) log(`Only the first ${inventory.counts.files} files are listed.`);
|
|
56
|
+
const kinds = warningKinds(inventory);
|
|
57
|
+
log(kinds.length === 0 ? 'No warnings.' : `${kinds.reduce((sum, item) => sum + item.count, 0)} warning(s).`);
|
|
58
|
+
log('This read your files on this machine and sent nothing.');
|
|
59
|
+
return EXIT.OK;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default assetsCommand;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { createContext } from '../api/index.js';
|
|
3
|
+
import { resolveGameId } from '../config.js';
|
|
4
|
+
import { withAuthHandling } from '../auth/session.js';
|
|
5
|
+
import { preflightBuildArchive, limitsFromProfile } from '../hosted/archivePreflight.js';
|
|
6
|
+
import { uploadHostedBuild } from '../hosted/buildUpload.js';
|
|
7
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
8
|
+
import { ui } from '../ui/index.js';
|
|
9
|
+
|
|
10
|
+
// TTY animation stays live while I/O awaits. Piped logs receive at most one
|
|
11
|
+
// byte update per second, plus phase boundaries and the final byte count.
|
|
12
|
+
export function buildUploadProgress({ json = false, now = Date.now } = {}) {
|
|
13
|
+
let spin, phase, last = 0, finished = false;
|
|
14
|
+
const labels = { capabilities: 'Checking available build profiles', preflight: 'Checking ZIP metadata',
|
|
15
|
+
hash: 'Hashing archive', reservation: 'Requesting the confirmed validation hold',
|
|
16
|
+
upload: 'Streaming archive bytes', completion: 'Submitting the original upload for validation' };
|
|
17
|
+
return {
|
|
18
|
+
update(event) {
|
|
19
|
+
if (json || ui.isQuiet()) return;
|
|
20
|
+
const at = now(), changed = phase !== event.phase;
|
|
21
|
+
const complete = event.totalBytes !== undefined && event.completedBytes === event.totalBytes;
|
|
22
|
+
if (!changed && at - last < 1000 && (!complete || finished)) return;
|
|
23
|
+
const label = labels[event.phase] + (event.totalBytes === undefined ? ''
|
|
24
|
+
: `: ${ui.bytes(event.completedBytes)} / ${ui.bytes(event.totalBytes)}`);
|
|
25
|
+
if (changed) { spin?.stop(); spin = ui.spinner(label); phase = event.phase; }
|
|
26
|
+
else spin.update(label);
|
|
27
|
+
last = at; finished = complete;
|
|
28
|
+
},
|
|
29
|
+
stop() { spin?.stop(); spin = null; phase = null; finished = false; }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function buildPreflight(filename, flags = {}) {
|
|
34
|
+
let profile;
|
|
35
|
+
if (flags.capacity) {
|
|
36
|
+
const info = await stat(flags.capacity);
|
|
37
|
+
if (!info.isFile() || info.size > 65536) throw new CliError('Choose a capacity profile JSON file no larger than 64 KiB.');
|
|
38
|
+
profile = JSON.parse(await readFile(flags.capacity, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
const result = await preflightBuildArchive(filename, { format: profile?.format || flags.format,
|
|
41
|
+
entrypoint: flags.entrypoint, limits: profile ? limitsFromProfile(profile) : undefined,
|
|
42
|
+
requirements: profile?.capacity?.requirements });
|
|
43
|
+
if (flags.json) ui.json(result);
|
|
44
|
+
else {
|
|
45
|
+
ui.ok(`ZIP metadata passed: ${result.entries} entries; ${ui.bytes(result.archiveBytes)} uploaded, ${ui.bytes(result.expandedBytes)} expanded.`);
|
|
46
|
+
ui.info('No wallet charge or upload was requested. Server validation and runtime compatibility checks are still required.');
|
|
47
|
+
}
|
|
48
|
+
return ExitCode.OK;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function buildUpload(filename, flags = {}, deps = {}) {
|
|
52
|
+
const context = deps.context || await createContext({ apiUrl: flags.apiUrl, repoRoot: flags.repoRoot, requireAuth: true });
|
|
53
|
+
return withAuthHandling(context, async () => {
|
|
54
|
+
const progress = buildUploadProgress({ json: Boolean(flags.json) });
|
|
55
|
+
try {
|
|
56
|
+
const result = await uploadHostedBuild({ filename, gameId: resolveGameId(flags, context.config), profileVersion: flags.profile,
|
|
57
|
+
entrypoint: flags.entrypoint, repoRoot: context.repoRoot, api: context.api, apiUrl: context.api.apiUrl,
|
|
58
|
+
journalPath: flags.journal, dryRun: Boolean(flags.dryRun), fetchImpl: deps.fetch,
|
|
59
|
+
confirm: deps.confirm || (question => ui.confirm(question, { yes: Boolean(flags.yes) })),
|
|
60
|
+
onProgress: event => progress.update(event),
|
|
61
|
+
onPrepared: prepared => {
|
|
62
|
+
progress.stop();
|
|
63
|
+
if (flags.json) return;
|
|
64
|
+
ui.info(`ZIP metadata checked: ${prepared.entries} entries; ${ui.bytes(prepared.archiveBytes)} archive, ${ui.bytes(prepared.expandedBytes)} expanded.`);
|
|
65
|
+
const limit = prepared.limits;
|
|
66
|
+
ui.info(`Selected limits: archive ${ui.bytes(limit.archiveBytes)}, expanded ${ui.bytes(limit.expandedBytes)}, file ${ui.bytes(limit.fileBytes)}, ${limit.files} entries, depth ${limit.depth}, ratio ${limit.ratio}:1.`);
|
|
67
|
+
const required = prepared.capacity?.requirements;
|
|
68
|
+
if (required) ui.info(`Server profile: ${required.os}/${required.architecture}, ${required.cpuVcpus} CPU, ${ui.bytes(required.memoryBytes)} RAM. Runtime and license readiness must come from the server.`);
|
|
69
|
+
if (Number.isSafeInteger(prepared.funding?.minimumFundingCents)) ui.info(`Server prepaid funding minimum: $${(prepared.funding.minimumFundingCents / 100).toFixed(2)}; the separate gameplay price is quoted after validation.`);
|
|
70
|
+
ui.info('Static validation hold: $0.49 from prepaid funds, returned after safe cleanup. A gameplay quote is separate. No top-up is performed.');
|
|
71
|
+
ui.info('Passing this local check does not establish GPU, licensing or game runtime compatibility.');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
progress.stop();
|
|
75
|
+
if (flags.json) ui.json(result);
|
|
76
|
+
else if (result.state === 'awaiting_validation') ui.ok(`Uploaded ${result.buildId}; awaiting server validation. Journal: ${result.journal}`);
|
|
77
|
+
else ui.info(result.state === 'declined' ? 'Upload declined; no reservation requested.' : 'Preflight finished; no reservation requested.');
|
|
78
|
+
return result.state === 'declined' ? ExitCode.CANCELED : ExitCode.OK;
|
|
79
|
+
} finally { progress.stop(); }
|
|
80
|
+
});
|
|
81
|
+
}
|
package/src/commands/check.js
CHANGED
|
@@ -425,7 +425,7 @@ export async function runChecks(flags = {}, deps = {}) {
|
|
|
425
425
|
'login',
|
|
426
426
|
games.length > 0 ? 'pass' : 'warn',
|
|
427
427
|
games.length > 0
|
|
428
|
-
? `${who}, valid, scoped to ${plural(games.length, 'game')}: ${games.map(gameLabel).join(', ')}`
|
|
428
|
+
? `${who}, valid, scoped to ${plural(games.length, 'game')}: ${games.slice(0, MAX_GAMES_LISTED).map(gameLabel).join(', ')}${games.length > MAX_GAMES_LISTED ? `, and ${games.length - MAX_GAMES_LISTED} more (check --json lists them all)` : ''}`
|
|
429
429
|
: `${who}, valid, but scoped to no games`,
|
|
430
430
|
games.length > 0 ? null : 'run ravensight-playtest login again and approve the game for this token'
|
|
431
431
|
));
|
|
@@ -742,6 +742,9 @@ export async function firstScreenRow(measure, url, { authed, gameId, priced }) {
|
|
|
742
742
|
* @param {Object} [deps]
|
|
743
743
|
* @returns {Promise<number>} exit code, 3 when anything failed
|
|
744
744
|
*/
|
|
745
|
+
/** Games named on check's login row; the rest are counted, and --json has them all. */
|
|
746
|
+
export const MAX_GAMES_LISTED = 5;
|
|
747
|
+
|
|
745
748
|
export async function check(flags = {}, deps = {}) {
|
|
746
749
|
const progress = flags.json ? {} : { onProgress: line => ui.info(line) };
|
|
747
750
|
const { rows, ok, cli, login, project, brief, build, automation } = await runChecks(flags, { ...progress, ...deps });
|