@bitmagic/cli 0.1.52-dev.0 → 0.1.52-dev.2

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.
Files changed (82) hide show
  1. package/README.md +66 -1
  2. package/dist/assets/add.d.ts +66 -0
  3. package/dist/assets/add.js +274 -0
  4. package/dist/assets/add.js.map +1 -0
  5. package/dist/assets/entry.d.ts +44 -0
  6. package/dist/assets/entry.js +71 -0
  7. package/dist/assets/entry.js.map +1 -0
  8. package/dist/assets/file-kind.d.ts +53 -0
  9. package/dist/assets/file-kind.js +180 -0
  10. package/dist/assets/file-kind.js.map +1 -0
  11. package/dist/assets/list.d.ts +27 -0
  12. package/dist/assets/list.js +50 -0
  13. package/dist/assets/list.js.map +1 -0
  14. package/dist/assets/remove.d.ts +50 -0
  15. package/dist/assets/remove.js +91 -0
  16. package/dist/assets/remove.js.map +1 -0
  17. package/dist/assets/vxl-summary.d.ts +23 -0
  18. package/dist/assets/vxl-summary.js +103 -0
  19. package/dist/assets/vxl-summary.js.map +1 -0
  20. package/dist/browser-gl.d.ts +46 -8
  21. package/dist/browser-gl.js +54 -26
  22. package/dist/browser-gl.js.map +1 -1
  23. package/dist/cli.d.ts +19 -0
  24. package/dist/cli.js +4 -0
  25. package/dist/cli.js.map +1 -1
  26. package/dist/commands/assets.d.ts +62 -0
  27. package/dist/commands/assets.js +359 -0
  28. package/dist/commands/assets.js.map +1 -0
  29. package/dist/commands/forge.d.ts +5 -0
  30. package/dist/commands/forge.js +10 -1
  31. package/dist/commands/forge.js.map +1 -1
  32. package/dist/commands/init.js +19 -12
  33. package/dist/commands/init.js.map +1 -1
  34. package/dist/commands/judge.d.ts +41 -0
  35. package/dist/commands/judge.js +217 -0
  36. package/dist/commands/judge.js.map +1 -0
  37. package/dist/commands/reset-account.js +2 -8
  38. package/dist/commands/reset-account.js.map +1 -1
  39. package/dist/commands/verify.js +3 -3
  40. package/dist/commands/verify.js.map +1 -1
  41. package/dist/editor/previews.d.ts +6 -0
  42. package/dist/editor/previews.js +16 -0
  43. package/dist/editor/previews.js.map +1 -1
  44. package/dist/editor/server.js +2 -2
  45. package/dist/editor/server.js.map +1 -1
  46. package/dist/forge/apply-modifications.d.ts +9 -1
  47. package/dist/forge/apply-modifications.js +3 -14
  48. package/dist/forge/apply-modifications.js.map +1 -1
  49. package/dist/forge/voxelize-session.d.ts +24 -0
  50. package/dist/forge/voxelize-session.js +137 -0
  51. package/dist/forge/voxelize-session.js.map +1 -0
  52. package/dist/generate/prop.js +9 -63
  53. package/dist/generate/prop.js.map +1 -1
  54. package/dist/generate/vehicle.js +5 -85
  55. package/dist/generate/vehicle.js.map +1 -1
  56. package/dist/http/signed-upload.d.ts +55 -0
  57. package/dist/http/signed-upload.js +168 -0
  58. package/dist/http/signed-upload.js.map +1 -0
  59. package/dist/judge/facts.d.ts +18 -0
  60. package/dist/judge/facts.js +180 -0
  61. package/dist/judge/facts.js.map +1 -0
  62. package/dist/judge/record.d.ts +16 -0
  63. package/dist/judge/record.js +55 -0
  64. package/dist/judge/record.js.map +1 -0
  65. package/dist/judge/stream.d.ts +36 -0
  66. package/dist/judge/stream.js +122 -0
  67. package/dist/judge/stream.js.map +1 -0
  68. package/dist/project/jobs.d.ts +1 -1
  69. package/dist/project/jobs.js.map +1 -1
  70. package/dist/publish/client.js +13 -34
  71. package/dist/publish/client.js.map +1 -1
  72. package/dist/scaffold/project-files.d.ts +8 -0
  73. package/dist/scaffold/project-files.js +104 -21
  74. package/dist/scaffold/project-files.js.map +1 -1
  75. package/dist/scaffold/project.js +2 -1
  76. package/dist/scaffold/project.js.map +1 -1
  77. package/dist/verify/browser.d.ts +11 -6
  78. package/dist/verify/browser.js +31 -15
  79. package/dist/verify/browser.js.map +1 -1
  80. package/dist/verify/iterate.js +3 -3
  81. package/dist/verify/iterate.js.map +1 -1
  82. package/package.json +4 -4
package/README.md CHANGED
@@ -191,7 +191,11 @@ browser reloads itself on every change and the assets panel shows each generatio
191
191
  | `bitmagic publish [--visibility public\|private] [--name <name>] [--description <desc>] [--force]` | Verify-gate, build if needed, and upload straight to GCS. **Private unless `--visibility public` is passed.** |
192
192
  | `bitmagic upgrade [--engine <version>] [--force]` | Refresh vendored platform files and the CLI's own skills to the currently published engine. Refuses a dirty git tree unless `--force`. Also records the project's `environment` in `bitmagic.json` if it has none yet — the engine it just vendored came from that api-server — and says so when it does. |
193
193
  | `bitmagic generate <skybox\|sound\|image\|character\|animation\|prop\|vehicle> ...` | Generate one asset directly into `src/work/world.json`. Costs sparks. `prop` upgrades an existing placeholder asset in place, `vehicle` adds a new drivable one, `animation` takes a parameter spec rather than a prompt — see below. |
194
+ | `bitmagic assets add <file> [--name <n>] [--asset-id <id>] [--keep-glb] [--voxel-size <m>] [--max-voxel-size <m>] [--height <m>] [--hollow\|--solid] [--force]` | Upload **your own** file — `.glb`, `.vxl`, `.png/.jpg/.webp`, `.opus/.ogg/.m4a/.mp3`, `.json` — into the game's assets and write its entry into `src/work/world.json`. A `.glb` is voxelized in a headless browser (Chrome + a free port in 3000–3199, like `prop`) unless `--keep-glb`. Free — no sparks. See **Adding your own assets** below. |
195
+ | `bitmagic assets list` | The assets in `world.json`: id, name, type, size, placed instances, and total references. Local, no network. |
196
+ | `bitmagic assets remove <assetId> [--force]` | Remove an asset from `world.json` (the uploaded file stays in storage). Refuses while anything in `world.json` still references it; `--force` removes the asset and its placed instances together. |
194
197
  | `bitmagic cover [--prompt "..."] [--force] [--no-start-screen]` | Generate the game's cover art from `GAME-DESIGN.md`: sets the start screen, saves `.bitmagic/cover.webp`, becomes the publish thumbnail. Costs sparks. |
198
+ | `bitmagic judge [--genre <name>] [--min-score <1-10>]` | Grade the screenshot `bitmagic verify` captured against a quality rubric built from `GAME-DESIGN.md` (plus an optional genre hint) with a vision model, and print a scorecard: four dimension scores and at most three actionable findings, also written to `.bitmagic/judge.json`. Informs rather than gates — exits 0 whatever the score, unless `--min-score` sets a bar (exit 4 below it). Warns when the project changed since the screenshot. Costs sparks (one small model call). |
195
199
  | `bitmagic forge --prompt "..." [--city\|--dungeon\|--platformer\|--freeform] [--name <name>] [--resume <jobId>]` | Design and bake a whole playable level into `world.json`. Costs sparks, and takes many minutes — the bake alone can take twenty. `--resume <jobId>` picks a failed run back up from the last step that finished, rather than redoing the whole thing. |
196
200
 
197
201
  Every command exits non-zero on failure with a single-line message (no stack trace) and, where a
@@ -497,6 +501,66 @@ engine's terrain tools, but saving them needs an asset upload this command does
497
501
  editor warns you as soon as you change a voxel and offers to discard it. Use the web Creator for
498
502
  terrain work.
499
503
 
504
+ ## Adding your own assets
505
+
506
+ `bitmagic assets add` is the Creator's Assets-tab upload for a project your own agent edits: a
507
+ Blender model, a sound library, a texture, a `.vxl` exported from the Creator or a config JSON goes
508
+ into storage and into `src/work/world.json` in one command. Nothing here costs sparks.
509
+
510
+ ```bash
511
+ bitmagic assets add ./models/lantern.glb # voxelized in a headless browser → a `vxl` asset
512
+ bitmagic assets add ./models/lantern.glb --keep-glb # kept as a polygon mesh (`glb`), 2 m tall
513
+ bitmagic assets add ./models/lantern.glb --voxel-size 0.05 --height 1.2 --hollow
514
+ bitmagic assets add ./sfx/door-slam.mp3 --name doorSlam # audio, uploaded as-is
515
+ bitmagic assets add ./textures/rune.png # image; the thumbnail is the image
516
+ bitmagic assets add ./data/waves.json # config data for `loadJsonAsset`
517
+ bitmagic assets list
518
+ bitmagic assets remove asset_1755600000000_k3j9x1
519
+ ```
520
+
521
+ What each file becomes:
522
+
523
+ | File | Asset `type` | Notes |
524
+ |---|---|---|
525
+ | `.glb` | `vxl` | Uploaded first (kept as `sourceGlbUrl`), then the engine voxelizes it: `--voxel-size` (finest, default 0.1 m), `--max-voxel-size` (flat areas, default 0.5 m), `--height` (scale to this height; default the model's own size). Interiors fill solid for prop-scale models and stay hollow above 6 m, matching `generate prop` — a solid-filled building is millions of wasted voxels. `--hollow` and `--solid` override that either way. Needs Chrome and a free port between 3000 and 3199. |
526
+ | `.glb --keep-glb` | `glb` | A polygon mesh, no voxelizing, no browser. `--height` sets `targetHeight` (default 2 m). |
527
+ | `.vxl` | `vxl` | Must be the binary VXL3 format (versions 3-9) the Creator and the engine export. Bounds, voxel size and voxel count are read from the file's own header. |
528
+ | `.png .jpg .jpeg .webp` | `image` | Uploaded as-is. |
529
+ | `.opus .ogg .m4a .mp3` | `audio` | Uploaded as-is. The engine plays all four; the Creator would transcode to `.opus` for a smaller download, and the command prints the `ffmpeg` line that does the same. |
530
+ | `.json` | `json` | The config-data assets `game/agent-docs/json-assets.md` describes. |
531
+ | `.gltf` | refused | Its buffers and textures live in files beside it, and the game loads one URL. Export a self-contained `.glb`. |
532
+ | `.vox .qb .fbx .spz .ply .sog .mp4` | refused | Each needs a conversion the Creator runs server-side (voxel-model import, FBX animation import, splat transcode, video faststart). Upload those through the Creator's Assets tab. |
533
+
534
+ The name defaults to the filename without its extension (`--name` overrides). The id is minted as
535
+ `asset_<timestamp>_<random>` unless `--asset-id` gives one — re-running with the same id **replaces**
536
+ the asset, which is how a `--asset-id` retry stays idempotent. A replacement **merges**: fields the
537
+ upload does not determine (`fitBox`, `vehicleFitment`, `collision`, `light`, …) survive it, and the
538
+ asset's cached thumbnail is dropped so the dev view re-renders. Replacing a still-referenced asset
539
+ with a *different* type is refused unless you pass `--force`, because every reference to it would
540
+ then resolve to something the engine cannot render. Every entry records
541
+ `production: { method: "uploaded", sourceUrl, sourceName }`.
542
+
543
+ Uploads are read with symlinks refused and capped at 512 MB: the destination is a permanent public
544
+ URL that nothing can delete, so the command will not follow a link to a file you did not mean to
545
+ publish.
546
+
547
+ The upload is refused before anything is sent when the project's `gameId` is not one the logged-in
548
+ account owns, and every failure before the write says `world.json is unchanged`. A `.glb` whose bake
549
+ fails after the upload prints where the GLB landed and the exact `--asset-id` to re-run with, so the
550
+ retry replaces that attempt instead of adding a second asset. While it runs, the upload shows in the
551
+ dev view's assets panel like any other job (`.bitmagic/jobs/`).
552
+
553
+ `bitmagic assets remove` edits `world.json` only — the file stays in storage, exactly as the
554
+ Creator's Remove leaves it (published games keep loading; every upload is immutable). It looks first
555
+ for anything in `world.json` still pointing at the asset — any `assetId`, `<thing>AssetId` or
556
+ `<thing>AssetIds` key, so placed objects, a level's `vwldAssetId`, a map, doors, key items and a
557
+ village's `buildingAssetIds` all count — and refuses while there is, listing where; `--force`
558
+ removes the asset and its placed instances in one write and reports the rest. `assets list` shows
559
+ the same total in its `REFS` column, so the two commands never disagree about what is in use. Game
560
+ code that names the id (`engine.playSound(...)`, `spawnFromAsset(...)`) is not checked, and neither
561
+ is a lookup by *name* — `loadJsonAsset` resolves the first asset with a matching name, so `add`
562
+ warns when a name is already taken.
563
+
500
564
  ## Drivable vehicles
501
565
 
502
566
  `bitmagic generate vehicle` designs a vehicle and registers it as a **new** asset — unlike
@@ -581,13 +645,14 @@ refused is a bundle that is not a Bitmagic engine build at all (exit 1).
581
645
 
582
646
  ### Exit codes
583
647
 
584
- `bitmagic publish` uses these to let a script or agent branch without parsing the message:
648
+ `bitmagic publish` and `bitmagic judge` use these to let a script or agent branch without parsing the message:
585
649
 
586
650
  | Code | Meaning |
587
651
  |---|---|
588
652
  | 1 | Build failed, or the uploaded bundle was not a Bitmagic engine build (check `vite.publish.config.js`). Not retryable — republishing the same bytes gets the same refusal. |
589
653
  | 2 | Not logged in |
590
654
  | 3 | Verify missing, stale, or failed |
655
+ | 4 | `bitmagic judge --min-score`: the judge ran, but the overall score is below the bar. The scorecard is in `.bitmagic/judge.json`. |
591
656
  | 5 | Not the game's owner, or the account/game is banned |
592
657
  | 6 | Upload or finalize failed — usually transient (network/storage), but also covers permanent failures such as an unknown game or a rejected request. Retry once; if it fails identically, stop and report it. |
593
658
  | 7 | The game published, but did not render when loaded the way the portal loads it. The game IS live — fix it and publish again rather than retrying the publish. Artifacts in `.bitmagic/smoke/`. |
@@ -0,0 +1,66 @@
1
+ import type { Environment } from '../config/environments.js';
2
+ import type { ProjectContext } from '../project/context.js';
3
+ export interface VoxelizeFlags {
4
+ minVoxelSize: number;
5
+ maxVoxelSize: number;
6
+ /** Metres. Absent = the GLB's own size, as the Creator's dialog leaves it by default. */
7
+ targetHeight?: number;
8
+ fillInterior: boolean;
9
+ }
10
+ /** What the headless engine is asked to bake. */
11
+ export interface VoxelizeGlbRequest {
12
+ context: ProjectContext;
13
+ environment: Environment;
14
+ token: string;
15
+ name: string;
16
+ glbUrl: string;
17
+ assetId: string;
18
+ voxel: VoxelizeFlags;
19
+ retryHint: string;
20
+ log: (message: string) => void;
21
+ }
22
+ /** Runs the bake and returns the engine's asset record. Injected in tests, real everywhere else. */
23
+ export type VoxelizeGlb = (request: VoxelizeGlbRequest) => Promise<Record<string, unknown>>;
24
+ export interface AddAssetOptions {
25
+ context: ProjectContext;
26
+ environment: Environment;
27
+ token: string;
28
+ filePath: string;
29
+ name?: string;
30
+ assetId?: string;
31
+ keepGlb: boolean;
32
+ /** Replace a still-referenced asset even when the type changes. See the gate in addAssetFile. */
33
+ force: boolean;
34
+ voxel: VoxelizeFlags;
35
+ fetchImpl?: typeof globalThis.fetch;
36
+ voxelize?: VoxelizeGlb;
37
+ now?: () => Date;
38
+ log: (message: string) => void;
39
+ }
40
+ export interface AddAssetResult {
41
+ assetId: string;
42
+ name: string;
43
+ type: string;
44
+ url: string;
45
+ size: number;
46
+ sourceGlbUrl?: string;
47
+ voxelCount?: number;
48
+ /** True when an asset with this id already existed and was replaced. */
49
+ replaced: boolean;
50
+ /** Anything the creator should know that is not an error — e.g. that audio went up untranscoded. */
51
+ notes: string[];
52
+ }
53
+ /**
54
+ * Biggest file this lane will upload. Not a server limit — api-server caps nothing — but a local
55
+ * one: every byte is held in memory, gzipped in memory for the compressible types, and the object
56
+ * is immutable once written with no delete endpoint anywhere, so an accidental multi-gigabyte
57
+ * upload cannot be taken back.
58
+ */
59
+ export declare const MAX_ASSET_BYTES: number;
60
+ /**
61
+ * The real bake: the shared headless session, one `CREATE_ASSET_FROM_GLB_URL`, the engine's record
62
+ * back. Same message and options as `generate prop`, minus `fitBox` (there is no placed instance
63
+ * to fit) and with the creator's own resolution flags.
64
+ */
65
+ export declare const voxelizeUploadedGlb: VoxelizeGlb;
66
+ export declare function addAssetFile(options: AddAssetOptions): Promise<AddAssetResult>;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * `bitmagic assets add <file>` — a creator's own file into the game's asset registry.
3
+ *
4
+ * The Creator's Assets tab does this with a file picker; this is the same lane for a project the
5
+ * creator's agent edits: upload the bytes through api-server's signed-URL route, then write the
6
+ * entry into `src/work/world.json`. Nothing here costs sparks — the one slow path (a `.glb`, which
7
+ * the engine has to voxelize in a headless browser) is compute on this machine, not a generation.
8
+ *
9
+ * Order of operations, and why:
10
+ *
11
+ * 1. Classify the file and validate the name BEFORE reading anything, so a format this lane
12
+ * cannot take fails in milliseconds with world.json untouched.
13
+ * 2. Read world.json BEFORE uploading, so a project that is not a Bitmagic project (or whose
14
+ * world.json is unreadable) fails before a file has been pushed to storage for nothing.
15
+ * 3. Upload. From here on the bytes exist in storage whatever happens next — which is fine: every
16
+ * upload is immutable-cached and cheap, and the world.json write below is atomic, so a failure
17
+ * after this point still leaves world.json exactly as it was and says so.
18
+ * 4. For a `.glb` (unless `--keep-glb`): hand the uploaded URL to the engine and let it bake and
19
+ * upload the `.vxl` itself, exactly as `generate prop`/`vehicle` do. The engine returns the
20
+ * complete asset record; only its provenance is corrected to `uploaded`.
21
+ * 5. One `upsertRoot` on `assets[]`, matched by id — so `--asset-id` re-runs replace rather than
22
+ * duplicate — through the same atomic, schema-gated writer the forge uses.
23
+ */
24
+ import * as fs from 'fs';
25
+ import { CliError } from '../errors.js';
26
+ import { applyModificationsToWorld } from '../forge/apply-modifications.js';
27
+ import { projectWorldJsonPath } from '../forge/run-pipeline.js';
28
+ import { VOXELIZE_TIMEOUT_MS, withVoxelizeSession } from '../forge/voxelize-session.js';
29
+ import { uploadProjectFile } from '../http/signed-upload.js';
30
+ import { isJsonObject, readWorldJson } from '../project/world-json.js';
31
+ import { audioAssetEntry, glbAssetEntry, imageAssetEntry, jsonAssetEntry, voxelizedGlbAssetEntry, vxlAssetEntry, } from './entry.js';
32
+ import { audioTranscodeNote, classifyAssetFile, defaultAssetName, mintAssetId, uploadFilename, validateAssetName, } from './file-kind.js';
33
+ import { findAssetReferences } from './remove.js';
34
+ import { summarizeVxl } from './vxl-summary.js';
35
+ import { formatSize } from './list.js';
36
+ function errnoCode(error) {
37
+ return typeof error === 'object' && error !== null && 'code' in error
38
+ ? String(error.code)
39
+ : undefined;
40
+ }
41
+ /** The errno, or the message when there is none — never an unmapped throw. See readFileBytes. */
42
+ function describeErrno(error) {
43
+ return errnoCode(error) ?? (error instanceof Error ? error.message : String(error));
44
+ }
45
+ /**
46
+ * Biggest file this lane will upload. Not a server limit — api-server caps nothing — but a local
47
+ * one: every byte is held in memory, gzipped in memory for the compressible types, and the object
48
+ * is immutable once written with no delete endpoint anywhere, so an accidental multi-gigabyte
49
+ * upload cannot be taken back.
50
+ */
51
+ export const MAX_ASSET_BYTES = 512 * 1024 * 1024;
52
+ function readFileBytes(filePath) {
53
+ // Refuse a symlink rather than following it. This command's documented caller is an AI agent
54
+ // running with a path from a prompt or from a cloned template, and the destination is a public,
55
+ // immutable, undeletable URL recorded in world.json — so a link pointing at a credentials file
56
+ // or an .ssh key is an exfiltration with no undo. `lstat` does not follow, unlike `readFileSync`.
57
+ let stats;
58
+ try {
59
+ stats = fs.lstatSync(filePath);
60
+ }
61
+ catch (error) {
62
+ const code = errnoCode(error);
63
+ if (code === 'ENOENT')
64
+ throw new CliError(`No such file: ${filePath}. world.json is unchanged.`);
65
+ throw new CliError(`Cannot read ${filePath}: ${describeErrno(error)}. world.json is unchanged.`);
66
+ }
67
+ if (stats.isSymbolicLink()) {
68
+ throw new CliError(`${filePath} is a symlink, and this command uploads to a permanent public URL. `
69
+ + 'Pass the real file instead. world.json is unchanged.');
70
+ }
71
+ if (stats.isDirectory()) {
72
+ throw new CliError(`${filePath} is a directory, not a file. world.json is unchanged.`);
73
+ }
74
+ if (stats.size === 0)
75
+ throw new CliError(`${filePath} is empty. world.json is unchanged.`);
76
+ if (stats.size > MAX_ASSET_BYTES) {
77
+ throw new CliError(`${filePath} is ${Math.round(stats.size / (1024 * 1024))} MB; this command uploads at most `
78
+ + `${MAX_ASSET_BYTES / (1024 * 1024)} MB. world.json is unchanged.`);
79
+ }
80
+ let bytes;
81
+ try {
82
+ bytes = fs.readFileSync(filePath);
83
+ }
84
+ catch (error) {
85
+ // Every failure mode is mapped, so none escapes as a raw stack trace that would break this
86
+ // lane's "world.json is unchanged" contract (EACCES, ELOOP, EISDIR, ERR_FS_FILE_TOO_LARGE...).
87
+ throw new CliError(`Cannot read ${filePath}: ${describeErrno(error)}. world.json is unchanged.`);
88
+ }
89
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
90
+ }
91
+ function vxlEntryBuilder(summary) {
92
+ return (entryBase) => vxlAssetEntry(entryBase, summary);
93
+ }
94
+ function requireVxlSummary(bytes, filename) {
95
+ const summary = summarizeVxl(bytes);
96
+ if (summary === null) {
97
+ throw new CliError(`${filename} is not a VXL3 file this engine can load. Export it from the Creator or the engine `
98
+ + '(only the binary VXL3 format, versions 3-9, is supported). world.json is unchanged.');
99
+ }
100
+ return summary;
101
+ }
102
+ /**
103
+ * The real bake: the shared headless session, one `CREATE_ASSET_FROM_GLB_URL`, the engine's record
104
+ * back. Same message and options as `generate prop`, minus `fitBox` (there is no placed instance
105
+ * to fit) and with the creator's own resolution flags.
106
+ */
107
+ export const voxelizeUploadedGlb = async (request) => {
108
+ const { context, environment, token, name, glbUrl, assetId, voxel, retryHint, log } = request;
109
+ return withVoxelizeSession({
110
+ context, environment, token, log, subject: `"${name}"`, retryHint,
111
+ }, async ({ host, port }) => {
112
+ log(`Voxelizing "${name}" at ${voxel.minVoxelSize}m…`);
113
+ const result = await host.transport.sendAndWait({
114
+ type: 'CREATE_ASSET_FROM_GLB_URL',
115
+ requestId: `upload-${assetId}-${port}`,
116
+ name,
117
+ glbUrl,
118
+ assetId,
119
+ options: {
120
+ minVoxelSize: voxel.minVoxelSize,
121
+ maxVoxelSize: voxel.maxVoxelSize,
122
+ ...(voxel.targetHeight !== undefined ? { targetHeight: voxel.targetHeight } : {}),
123
+ fillInterior: voxel.fillInterior,
124
+ placeholder: false,
125
+ },
126
+ }, 'ASSET_FROM_GLB_URL_RESULT', VOXELIZE_TIMEOUT_MS, { maxRetries: 1 });
127
+ if (!result) {
128
+ throw new CliError(`Voxelizing "${name}" timed out. ${retryHint}`);
129
+ }
130
+ if (result.success === false || !isJsonObject(result.asset)) {
131
+ const reason = typeof result.error === 'string' ? result.error : 'unknown error';
132
+ throw new CliError(`Voxelizing "${name}" failed: ${reason}. ${retryHint}`);
133
+ }
134
+ return result.asset;
135
+ });
136
+ };
137
+ export async function addAssetFile(options) {
138
+ const { context, environment, token, filePath, log } = options;
139
+ const now = options.now ?? (() => new Date());
140
+ // 1. Everything that can be refused for free.
141
+ const file = classifyAssetFile(filePath, { keepGlb: options.keepGlb });
142
+ const filename = uploadFilename(filePath);
143
+ const name = validateAssetName(options.name ?? defaultAssetName(filePath));
144
+ const assetId = options.assetId ?? mintAssetId();
145
+ // 2. The world this is going into, before anything leaves the machine.
146
+ const worldPath = projectWorldJsonPath(context.root);
147
+ const world = readWorldJson(worldPath);
148
+ const notes = [];
149
+ const assets = Array.isArray(world.assets) ? world.assets : [];
150
+ const existing = assets.find((item) => isJsonObject(item) && item.id === assetId);
151
+ const replaced = existing !== undefined;
152
+ // Replacing an id is the most destructive thing this command does, and it used to be the only
153
+ // one with no gate at all — `assets remove` refuses while anything still points at the asset,
154
+ // while `add --asset-id` would happily turn a placed prop into an audio clip. Same rule here:
155
+ // a replacement that CHANGES THE TYPE under live references is refused unless forced, because
156
+ // every one of those references resolves to something the engine cannot render.
157
+ if (existing !== undefined && !options.force) {
158
+ const existingType = typeof existing.type === 'string' ? existing.type : '';
159
+ const references = findAssetReferences(world, assetId);
160
+ if (existingType !== file.assetType && references.length > 0) {
161
+ throw new CliError(`Asset "${assetId}" is a ${existingType} referenced ${references.length === 1 ? 'once' : `${references.length} times`} `
162
+ + `in world.json, and ${filename} would replace it with ${file.assetType === 'image' ? 'an' : 'a'} `
163
+ + `${file.assetType}. Those references would resolve to the wrong kind of asset. `
164
+ + 'Use --force to do it anyway, or omit --asset-id to add a new asset. world.json is unchanged.');
165
+ }
166
+ }
167
+ // A name collision is not fatal, but it is silent and it matters: JSON assets are looked up BY
168
+ // NAME (`loadJsonAsset` -> `getAssetUrlByName`, first match wins), so a second asset sharing a
169
+ // name is simply never loaded. Warned rather than refused — duplicate names are legal, and the
170
+ // creator may well want two `rock`s they address by id.
171
+ const nameClash = assets.some((item) => isJsonObject(item) && item.name === name && item.id !== assetId);
172
+ if (nameClash) {
173
+ notes.push(`Another asset is already named "${name}". Assets addressed by name (JSON config assets, `
174
+ + 'vehicles spawned by name) resolve to the FIRST match, so give this one a distinct --name '
175
+ + 'if game code looks it up that way.');
176
+ }
177
+ const bytes = readFileBytes(filePath);
178
+ const size = bytes.byteLength;
179
+ // A `.vxl` is inspected before the upload so a file that is not one is refused for free. The
180
+ // parse is carried to the switch below as a builder rather than re-parsed there — `summarizeVxl`
181
+ // gunzips the whole body, so doing it twice for one add is real work for no answer.
182
+ const buildVxlEntry = file.kind === 'vxl'
183
+ ? vxlEntryBuilder(requireVxlSummary(bytes, filename))
184
+ : null;
185
+ // 3. Upload.
186
+ log(`Uploading ${filename} (${formatSize(size)})…`);
187
+ const upload = await uploadProjectFile(environment, token, { gameId: context.metadata.gameId, filename, contentType: file.contentType, bytes }, { fetch: options.fetchImpl ?? globalThis.fetch });
188
+ const url = upload.url;
189
+ log(`Uploaded: ${url}`);
190
+ const base = { id: assetId, name, url, size, sourceName: filename, at: now().toISOString() };
191
+ // 4. The record — from the engine for a voxelized GLB, built here for everything else.
192
+ let entry;
193
+ switch (file.kind) {
194
+ case 'image':
195
+ entry = imageAssetEntry(base);
196
+ break;
197
+ case 'audio': {
198
+ entry = audioAssetEntry(base);
199
+ const note = audioTranscodeNote(file.extension);
200
+ if (note !== null)
201
+ notes.push(note);
202
+ break;
203
+ }
204
+ case 'json':
205
+ entry = jsonAssetEntry(base);
206
+ break;
207
+ case 'vxl':
208
+ // Non-null for exactly this kind, set above in the same `file.kind` test.
209
+ entry = buildVxlEntry(base);
210
+ break;
211
+ case 'glb-keep':
212
+ entry = glbAssetEntry(base, options.voxel.targetHeight);
213
+ break;
214
+ case 'glb-voxelize': {
215
+ // Names --asset-id explicitly. Unlike `generate prop`/`vehicle` this command has no
216
+ // --glb-url, so "run it again" re-uploads the file and, without a pinned id, MINTS A NEW ONE
217
+ // and leaves the first upload orphaned in storage (uploads are immutable and nothing deletes
218
+ // them). Re-running under the same id is what makes the retry idempotent.
219
+ const retryHint = 'Nothing was charged and world.json is unchanged. Re-run with '
220
+ + `\`--asset-id ${assetId}\` so the retry replaces this attempt instead of adding a second `
221
+ + `asset (the GLB from this run is already uploaded at ${url}).`;
222
+ const voxelize = options.voxelize ?? voxelizeUploadedGlb;
223
+ const engineAsset = await voxelize({
224
+ context, environment, token, name, glbUrl: url, assetId, voxel: options.voxel, retryHint, log,
225
+ });
226
+ // The engine ECHOES the id we asked for — but only since the vendored engine learned to
227
+ // honour a supplied id for an asset that does not exist yet. An older one mints its own,
228
+ // and because `entry` spreads the engine's record while the upsert predicate matches OUR
229
+ // id, the mismatch would silently APPEND under a different id than the one reported: the
230
+ // command prints an id that is in no world.json entry, `assets remove` cannot find it, and
231
+ // a `--asset-id` retry adds a second copy instead of replacing. `generate vehicle` reads
232
+ // the id back for the same reason; this is the same check, made loud.
233
+ const engineId = typeof engineAsset.id === 'string' ? engineAsset.id : '';
234
+ if (engineId !== assetId) {
235
+ throw new CliError(`The engine registered "${name}" under id "${engineId || '(none)'}" instead of "${assetId}", so `
236
+ + 'this CLI cannot write it reliably. The project\'s vendored engine is too old for '
237
+ + '`bitmagic assets add` — run `bitmagic upgrade`, then retry. world.json is unchanged '
238
+ + `(the GLB is already uploaded at ${url}).`);
239
+ }
240
+ entry = voxelizedGlbAssetEntry(engineAsset, base);
241
+ break;
242
+ }
243
+ }
244
+ // 5. One atomic write.
245
+ //
246
+ // MERGED over the existing record, not substituted for it. An asset accumulates authored state
247
+ // the uploader knows nothing about — `fitBox` (without which the dev view's HQ regeneration
248
+ // refuses outright), `vehicleFitment` (without which a vehicle stops driving), `collision`,
249
+ // `light`, `flattenTerrain`, `lodVariants` — and writing a fresh record over the top silently
250
+ // discarded all of it. `editor/server.ts` states the same rule for its own asset write: "An
251
+ // updater, not a replacement". The new record wins field by field, so everything this upload
252
+ // actually determined still lands.
253
+ const value = existing === undefined ? entry : { ...existing, ...entry };
254
+ const modification = {
255
+ type: 'upsertRoot',
256
+ path: ['assets'],
257
+ predicate: (item) => isJsonObject(item) && item.id === assetId,
258
+ value,
259
+ };
260
+ applyModificationsToWorld(worldPath, [modification], { subject: 'The uploaded asset' });
261
+ const finalUrl = typeof entry.url === 'string' ? entry.url : url;
262
+ return {
263
+ assetId,
264
+ name,
265
+ type: typeof entry.type === 'string' ? entry.type : file.assetType,
266
+ url: finalUrl,
267
+ size: typeof entry.size === 'number' ? entry.size : size,
268
+ ...(typeof entry.sourceGlbUrl === 'string' ? { sourceGlbUrl: entry.sourceGlbUrl } : {}),
269
+ ...(typeof entry.voxelCount === 'number' ? { voxelCount: entry.voxelCount } : {}),
270
+ replaced,
271
+ notes,
272
+ };
273
+ }
274
+ //# sourceMappingURL=add.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.js","sourceRoot":"","sources":["../../src/assets/add.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The world.json `assets[]` record for a file a creator uploaded, kind by kind.
3
+ *
4
+ * Kept pure — inputs in, a plain object out — so the shape each kind writes is unit-tested
5
+ * without a project on disk, and so `add.ts` stays about the order of operations (classify, read,
6
+ * upload, bake, write) rather than about which fields go where.
7
+ *
8
+ * Every shape mirrors what the Creator's Assets tab writes for the same file
9
+ * (`useAssetsEditor.ts` `handleFileUpload`, `assetsGlbKeepImport.ts`), including `production`:
10
+ * `uploaded` is the one provenance only the uploading side can know — the engine's voxelize
11
+ * handler stamps `generated` because it sees the same GLB whether a person or a generator made it,
12
+ * and the Creator overwrites that too.
13
+ */
14
+ import type { VxlSummary } from './vxl-summary.js';
15
+ export interface UploadedAssetBase {
16
+ id: string;
17
+ name: string;
18
+ /** Where the uploaded bytes are served from. */
19
+ url: string;
20
+ /** Raw file size in bytes — what the Creator records, not the gzipped transfer size. */
21
+ size: number;
22
+ /** The file's basename, kept as provenance. */
23
+ sourceName: string;
24
+ /** ISO timestamp for `production.at`. */
25
+ at: string;
26
+ }
27
+ /** An image: the thumbnail IS the image, as the Creator and the image generator both record. */
28
+ export declare function imageAssetEntry(input: UploadedAssetBase): Record<string, unknown>;
29
+ export declare function audioAssetEntry(input: UploadedAssetBase): Record<string, unknown>;
30
+ export declare function jsonAssetEntry(input: UploadedAssetBase): Record<string, unknown>;
31
+ /**
32
+ * A `.vxl` uploaded as-is, with the metadata its own header states — the fields the Creator gets
33
+ * back from `/api/analyze-asset` for the same file.
34
+ */
35
+ export declare function vxlAssetEntry(input: UploadedAssetBase, summary: VxlSummary): Record<string, unknown>;
36
+ /** The Creator's keep-as-GLB import: `type: 'glb'`, the mesh as its own source, 2 m tall by default. */
37
+ export declare const DEFAULT_GLB_TARGET_HEIGHT = 2;
38
+ export declare function glbAssetEntry(input: UploadedAssetBase, targetHeight: number | undefined): Record<string, unknown>;
39
+ /**
40
+ * The record the engine returned from `CREATE_ASSET_FROM_GLB_URL`, re-stamped as an upload. The
41
+ * engine's own `production` says `generated` with the GLB as `sourceUrl`; the method is corrected,
42
+ * the source URL kept (it is the uploaded GLB), and the creator's filename added.
43
+ */
44
+ export declare function voxelizedGlbAssetEntry(engineAsset: Record<string, unknown>, input: Pick<UploadedAssetBase, 'url' | 'sourceName' | 'at'>): Record<string, unknown>;
@@ -0,0 +1,71 @@
1
+ function baseEntry(input, type) {
2
+ return {
3
+ id: input.id,
4
+ name: input.name,
5
+ url: input.url,
6
+ type,
7
+ size: input.size,
8
+ production: {
9
+ method: 'uploaded',
10
+ at: input.at,
11
+ sourceUrl: input.url,
12
+ sourceName: input.sourceName,
13
+ },
14
+ };
15
+ }
16
+ /** An image: the thumbnail IS the image, as the Creator and the image generator both record. */
17
+ export function imageAssetEntry(input) {
18
+ return { ...baseEntry(input, 'image'), screenshotUrl: input.url };
19
+ }
20
+ export function audioAssetEntry(input) {
21
+ return baseEntry(input, 'audio');
22
+ }
23
+ export function jsonAssetEntry(input) {
24
+ return baseEntry(input, 'json');
25
+ }
26
+ /**
27
+ * A `.vxl` uploaded as-is, with the metadata its own header states — the fields the Creator gets
28
+ * back from `/api/analyze-asset` for the same file.
29
+ */
30
+ export function vxlAssetEntry(input, summary) {
31
+ return {
32
+ ...baseEntry(input, 'vxl'),
33
+ boundingBox: summary.boundingBox,
34
+ // NOT optional beside `voxelSize`. VXL3 stores world-space bounds, but the engine treats
35
+ // `boundingBox && !boundingBoxInMeters && voxelSize && !sourceGlbUrl` as a pre-1.1 asset whose
36
+ // box is in VOXEL GRID units and multiplies all six components by `voxelSize`
37
+ // (EnvironmentObjectSystem's legacy migration) — a silent 10-20x shrink that then mis-sizes
38
+ // terrain flattening, placement clearance and destruction. Every in-engine writer sets this
39
+ // flag for the same reason; the Creator's upload dodges it only by never recording a voxelSize.
40
+ boundingBoxInMeters: true,
41
+ voxelSize: summary.minVoxelSize,
42
+ fragmentCount: summary.fragmentCount,
43
+ ...(summary.voxelCount !== undefined ? { voxelCount: summary.voxelCount } : {}),
44
+ };
45
+ }
46
+ /** The Creator's keep-as-GLB import: `type: 'glb'`, the mesh as its own source, 2 m tall by default. */
47
+ export const DEFAULT_GLB_TARGET_HEIGHT = 2.0;
48
+ export function glbAssetEntry(input, targetHeight) {
49
+ return {
50
+ ...baseEntry(input, 'glb'),
51
+ sourceGlbUrl: input.url,
52
+ targetHeight: targetHeight ?? DEFAULT_GLB_TARGET_HEIGHT,
53
+ };
54
+ }
55
+ /**
56
+ * The record the engine returned from `CREATE_ASSET_FROM_GLB_URL`, re-stamped as an upload. The
57
+ * engine's own `production` says `generated` with the GLB as `sourceUrl`; the method is corrected,
58
+ * the source URL kept (it is the uploaded GLB), and the creator's filename added.
59
+ */
60
+ export function voxelizedGlbAssetEntry(engineAsset, input) {
61
+ return {
62
+ ...engineAsset,
63
+ production: {
64
+ method: 'uploaded',
65
+ at: input.at,
66
+ sourceUrl: input.url,
67
+ sourceName: input.sourceName,
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/assets/entry.ts"],"names":[],"mappings":"AA4BA,SAAS,SAAS,CAAC,KAAwB,EAAE,IAAY;IACvD,OAAO;QACL,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,IAAI;QACJ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,UAAU,EAAE;YACV,MAAM,EAAE,UAAU;YAClB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,SAAS,EAAE,KAAK,CAAC,GAAG;YACpB,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B;KACF,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,eAAe,CAAC,KAAwB;IACtD,OAAO,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAwB;IACtD,OAAO,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAwB;IACrD,OAAO,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,OAAmB;IACzE,OAAO;QACL,GAAG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;QAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,yFAAyF;QACzF,+FAA+F;QAC/F,8EAA8E;QAC9E,4FAA4F;QAC5F,4FAA4F;QAC5F,gGAAgG;QAChG,mBAAmB,EAAE,IAAI;QACzB,SAAS,EAAE,OAAO,CAAC,YAAY;QAC/B,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChF,CAAC;AACJ,CAAC;AAED,wGAAwG;AACxG,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAE7C,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,YAAgC;IACtF,OAAO;QACL,GAAG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;QAC1B,YAAY,EAAE,KAAK,CAAC,GAAG;QACvB,YAAY,EAAE,YAAY,IAAI,yBAAyB;KACxD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,WAAoC,EACpC,KAA2D;IAE3D,OAAO;QACL,GAAG,WAAW;QACd,UAAU,EAAE;YACV,MAAM,EAAE,UAAU;YAClB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,SAAS,EAAE,KAAK,CAAC,GAAG;YACpB,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,53 @@
1
+ /** How the file is handled, which decides everything downstream. */
2
+ export type AssetFileKind = 'glb-voxelize' | 'glb-keep' | 'vxl' | 'image' | 'audio' | 'json';
3
+ export interface AssetFileClass {
4
+ kind: AssetFileKind;
5
+ /** The `type` recorded on the world.json asset entry. */
6
+ assetType: 'vxl' | 'glb' | 'image' | 'audio' | 'json';
7
+ /** What the upload is signed for; the server decides gzip from this. */
8
+ contentType: string;
9
+ /** Lower-case extension without the dot. */
10
+ extension: string;
11
+ }
12
+ /** Every extension `classifyAssetFile` accepts, for help text and the README. */
13
+ export declare const ACCEPTED_EXTENSIONS: string[];
14
+ export declare function fileExtension(filename: string): string;
15
+ export declare function classifyAssetFile(filename: string, options: {
16
+ keepGlb: boolean;
17
+ }): AssetFileClass;
18
+ /**
19
+ * The asset's name when `--name` is not given: the file's basename without its extension, which
20
+ * is what the Creator pre-fills its name prompt with.
21
+ */
22
+ export declare function defaultAssetName(filename: string): string;
23
+ /**
24
+ * A name the engine can build a filename from. `CREATE_ASSET_FROM_GLB_URL` uploads its bake as
25
+ * `<gameId>-<name>-<timestamp>.vxl`, and api-server refuses a filename carrying a separator or
26
+ * `..` — so a name that would trip that fails HERE, before the file has been uploaded and the
27
+ * browser launched, instead of as an opaque "Failed to upload VXL file" minutes later.
28
+ *
29
+ * The length cap and the URL-unsafe set are part of the same job: both produce a failure that only
30
+ * appears after the upload (or after a ten-minute bake), which is precisely what this is for.
31
+ */
32
+ export declare function validateAssetName(name: string): string;
33
+ /**
34
+ * The upload filename: a bare basename, which is all api-server accepts.
35
+ *
36
+ * Mirrors `sanitizeUploadFilename` in full — separators AND `..`, as three independent checks.
37
+ * `path.basename` strips `/` on POSIX but leaves `\`, so a legal `my\model.glb` used to pass here
38
+ * and then eat a network round-trip to be refused by the server with a message naming nothing the
39
+ * creator could act on. The URL-unsafe and length checks are here for the same reason as on the
40
+ * name above.
41
+ */
42
+ export declare function uploadFilename(filePath: string): string;
43
+ /**
44
+ * `asset_<timestamp>_<random>` — byte-compatible with the engine's `ObjectIdService.generateId`
45
+ * and with what api-server mints for the generators (`cli-assets.ts`), so an id minted here is
46
+ * indistinguishable from one minted anywhere else in the system.
47
+ */
48
+ export declare function mintAssetId(now?: () => number, random?: () => number): string;
49
+ /**
50
+ * Said once per audio upload that the Creator would have transcoded. The engine plays the file
51
+ * as-is; this is about download size, and it names the exact command rather than a vague "consider".
52
+ */
53
+ export declare function audioTranscodeNote(extension: string): string | null;