@bitmagic/cli 0.1.52-dev.1 → 0.1.52-dev.10

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 (115) hide show
  1. package/README.md +175 -5
  2. package/dist/assets/add.d.ts +40 -0
  3. package/dist/assets/add.js +229 -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 +61 -0
  9. package/dist/assets/file-kind.js +195 -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/voxelize-glb.d.ts +37 -0
  18. package/dist/assets/voxelize-glb.js +65 -0
  19. package/dist/assets/voxelize-glb.js.map +1 -0
  20. package/dist/assets/vxl-summary.d.ts +32 -0
  21. package/dist/assets/vxl-summary.js +118 -0
  22. package/dist/assets/vxl-summary.js.map +1 -0
  23. package/dist/bin.js +25 -0
  24. package/dist/bin.js.map +1 -1
  25. package/dist/cli.d.ts +4 -0
  26. package/dist/cli.js +15 -1
  27. package/dist/cli.js.map +1 -1
  28. package/dist/commands/assets.d.ts +56 -0
  29. package/dist/commands/assets.js +328 -0
  30. package/dist/commands/assets.js.map +1 -0
  31. package/dist/commands/cover.js +2 -3
  32. package/dist/commands/cover.js.map +1 -1
  33. package/dist/commands/forge.d.ts +2 -9
  34. package/dist/commands/forge.js +2 -13
  35. package/dist/commands/forge.js.map +1 -1
  36. package/dist/commands/generate.d.ts +41 -9
  37. package/dist/commands/generate.js +234 -54
  38. package/dist/commands/generate.js.map +1 -1
  39. package/dist/commands/judge.d.ts +2 -7
  40. package/dist/commands/judge.js +26 -19
  41. package/dist/commands/judge.js.map +1 -1
  42. package/dist/commands/levels.d.ts +33 -0
  43. package/dist/commands/levels.js +241 -0
  44. package/dist/commands/levels.js.map +1 -0
  45. package/dist/commands/number-flag.d.ts +7 -0
  46. package/dist/commands/number-flag.js +38 -0
  47. package/dist/commands/number-flag.js.map +1 -0
  48. package/dist/commands/publish.js +13 -0
  49. package/dist/commands/publish.js.map +1 -1
  50. package/dist/commands/run-deps.d.ts +27 -0
  51. package/dist/commands/run-deps.js +43 -0
  52. package/dist/commands/run-deps.js.map +1 -0
  53. package/dist/editor/journal.d.ts +48 -1
  54. package/dist/editor/journal.js +44 -0
  55. package/dist/editor/journal.js.map +1 -1
  56. package/dist/editor/previews.d.ts +6 -0
  57. package/dist/editor/previews.js +16 -0
  58. package/dist/editor/previews.js.map +1 -1
  59. package/dist/editor/server.js +228 -53
  60. package/dist/editor/server.js.map +1 -1
  61. package/dist/editor/shell-page.js +221 -116
  62. package/dist/editor/shell-page.js.map +1 -1
  63. package/dist/editor/voxel-save.d.ts +47 -0
  64. package/dist/editor/voxel-save.js +49 -0
  65. package/dist/editor/voxel-save.js.map +1 -0
  66. package/dist/errors.d.ts +10 -0
  67. package/dist/errors.js +16 -0
  68. package/dist/errors.js.map +1 -1
  69. package/dist/forge/apply-modifications.d.ts +18 -1
  70. package/dist/forge/apply-modifications.js +15 -32
  71. package/dist/forge/apply-modifications.js.map +1 -1
  72. package/dist/forge/browser-host.js +9 -4
  73. package/dist/forge/browser-host.js.map +1 -1
  74. package/dist/forge/voxelize-session.d.ts +61 -0
  75. package/dist/forge/voxelize-session.js +137 -0
  76. package/dist/forge/voxelize-session.js.map +1 -0
  77. package/dist/generate/model.d.ts +36 -0
  78. package/dist/generate/model.js +90 -0
  79. package/dist/generate/model.js.map +1 -0
  80. package/dist/generate/prop.d.ts +52 -4
  81. package/dist/generate/prop.js +169 -102
  82. package/dist/generate/prop.js.map +1 -1
  83. package/dist/generate/stream.d.ts +7 -0
  84. package/dist/generate/stream.js +2 -1
  85. package/dist/generate/stream.js.map +1 -1
  86. package/dist/generate/vehicle.js +5 -85
  87. package/dist/generate/vehicle.js.map +1 -1
  88. package/dist/http/signed-upload.d.ts +55 -0
  89. package/dist/http/signed-upload.js +168 -0
  90. package/dist/http/signed-upload.js.map +1 -0
  91. package/dist/judge/facts.js +72 -57
  92. package/dist/judge/facts.js.map +1 -1
  93. package/dist/levels/registry.d.ts +88 -0
  94. package/dist/levels/registry.js +309 -0
  95. package/dist/levels/registry.js.map +1 -0
  96. package/dist/messages/check.d.ts +30 -0
  97. package/dist/messages/check.js +119 -0
  98. package/dist/messages/check.js.map +1 -0
  99. package/dist/messages/notice.d.ts +58 -0
  100. package/dist/messages/notice.js +54 -0
  101. package/dist/messages/notice.js.map +1 -0
  102. package/dist/messages/refresh.d.ts +19 -0
  103. package/dist/messages/refresh.js +42 -0
  104. package/dist/messages/refresh.js.map +1 -0
  105. package/dist/project/jobs.d.ts +1 -1
  106. package/dist/project/jobs.js.map +1 -1
  107. package/dist/publish/client.d.ts +13 -0
  108. package/dist/publish/client.js +13 -34
  109. package/dist/publish/client.js.map +1 -1
  110. package/dist/publish/platform.d.ts +46 -0
  111. package/dist/publish/platform.js +92 -0
  112. package/dist/publish/platform.js.map +1 -0
  113. package/dist/scaffold/project-files.js +109 -21
  114. package/dist/scaffold/project-files.js.map +1 -1
  115. package/package.json +4 -4
package/README.md CHANGED
@@ -190,10 +190,14 @@ browser reloads itself on every change and the assets panel shows each generatio
190
190
  | `bitmagic build` | Bundle the game into one self-contained `.bitmagic/build/index.html`. Rarely needs to be run by hand — `publish` builds automatically when the project changed. |
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
- | `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. |
193
+ | `bitmagic generate <skybox\|background\|block-type\|sound\|image\|character\|animation\|model\|prop\|vehicle> ...` | Generate one asset directly into `src/work/world.json`. Costs sparks. `model` mints a **new** 3D object from a prompt, `prop` upgrades an existing placeholder **in place**, `vehicle` adds a new drivable one, `animation` takes a parameter spec rather than a prompt, `block-type` registers a custom voxel block — see below. `skybox` and `background` write the **same** field: pick one. |
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. |
195
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). |
196
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. |
200
+ | `bitmagic levels <list\|add\|rename\|set-start\|remove> ...` | Manage the levels `forge` creates: which one boots, their names, registering a bake you already have, deleting one. Local, no network, no sparks. See **Managing levels** below. |
197
201
 
198
202
  Every command exits non-zero on failure with a single-line message (no stack trace) and, where a
199
203
  distinction is useful to an automated caller, a specific exit code — see **Exit codes** below.
@@ -467,9 +471,15 @@ did without inferring it from a diff:
467
471
 
468
472
  ```
469
473
  {"at":"…","event":"object.moved","objectId":"inst_…","position":{"from":{…},"to":{…}}}
474
+ {"at":"…","event":"terrain.saved","voxelUrl":"https://…/game-terrain-….vxl"}
470
475
  {"at":"…","event":"hq.requested","assetId":"asset_…","prompt":"a mossy stone arch","command":"bitmagic generate prop …"}
471
476
  ```
472
477
 
478
+ Most lines are worked out by comparing `world.json` before and after a save. The sculpting ones are
479
+ not: a terrain save changes one URL that says nothing about what was carved, and a save that
480
+ **failed** changes no file at all — so the editor reports those itself rather than leaving your
481
+ agent to infer a thing that left no trace.
482
+
473
483
  ### Regenerating a placeholder
474
484
 
475
485
  Forged levels are built from placeholder assets — box-model stand-ins. Select one and the object
@@ -493,10 +503,111 @@ carries the URL: retry with `--glb-url <url>` and you are not charged again.
493
503
  Both ports must fall between 3000 and 3199. That is the window the asset CDN allows as an origin;
494
504
  outside it the game loads with no terrain and no error.
495
505
 
496
- What it does **not** do yet: terrain and voxel sculpting. Clicking the ground still opens the
497
- engine's terrain tools, but saving them needs an asset upload this command does not perform, so the
498
- editor warns you as soon as you change a voxel and offers to discard it. Use the web Creator for
499
- terrain work.
506
+ ### Sculpting the ground
507
+
508
+ Click the ground and you are in the engine's terrain tools; **Save & Exit** in their toolbar keeps
509
+ the work. The sculpted volume is uploaded and `src/work/world.json` gets the one line that points at
510
+ it, the same surgical write every other edit here makes.
511
+
512
+ That upload is the only part of `bitmagic dev` that needs an account — everything else runs offline.
513
+ Logged out, the save fails and **says so in the toolbar, leaving your edits where they are**: run
514
+ `bitmagic login`, then press Save & Exit again. Nothing is thrown away without you saying so, and
515
+ `terrain.saveFailed` lands in the journal either way, so your agent knows what happened.
516
+
517
+ Per-object voxel sculpting works the same way — select a voxel object, edit it, Save & Exit.
518
+
519
+ ## Adding your own assets
520
+
521
+ `bitmagic assets add` is the Creator's Assets-tab upload for a project your own agent edits: a
522
+ Blender model, a sound library, a texture, a `.vxl` exported from the Creator or a config JSON goes
523
+ into storage and into `src/work/world.json` in one command. Nothing here costs sparks.
524
+
525
+ ```bash
526
+ bitmagic assets add ./models/lantern.glb # voxelized in a headless browser → a `vxl` asset
527
+ bitmagic assets add ./models/lantern.glb --keep-glb # kept as a polygon mesh (`glb`), 2 m tall
528
+ bitmagic assets add ./models/lantern.glb --voxel-size 0.05 --height 1.2 --hollow
529
+ bitmagic assets add ./sfx/door-slam.mp3 --name doorSlam # audio, uploaded as-is
530
+ bitmagic assets add ./textures/rune.png # image; the thumbnail is the image
531
+ bitmagic assets add ./data/waves.json # config data for `loadJsonAsset`
532
+ bitmagic assets list
533
+ bitmagic assets remove asset_1755600000000_k3j9x1
534
+ ```
535
+
536
+ What each file becomes:
537
+
538
+ | File | Asset `type` | Notes |
539
+ |---|---|---|
540
+ | `.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. |
541
+ | `.glb --keep-glb` | `glb` | A polygon mesh, no voxelizing, no browser. `--height` sets `targetHeight` (default 2 m). |
542
+ | `.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. |
543
+ | `.png .jpg .jpeg .webp` | `image` | Uploaded as-is. |
544
+ | `.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. |
545
+ | `.json` | `json` | The config-data assets `game/agent-docs/json-assets.md` describes. |
546
+ | `.gltf` | refused | Its buffers and textures live in files beside it, and the game loads one URL. Export a self-contained `.glb`. |
547
+ | `.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. |
548
+
549
+ The name defaults to the filename without its extension (`--name` overrides). The id is minted as
550
+ `asset_<timestamp>_<random>` unless `--asset-id` gives one — re-running with the same id **replaces**
551
+ the asset, which is how a `--asset-id` retry stays idempotent. A replacement **merges**: fields the
552
+ upload does not determine (`fitBox`, `vehicleFitment`, `collision`, `light`, …) survive it, and the
553
+ asset's cached thumbnail is dropped so the dev view re-renders. Replacing a still-referenced asset
554
+ with a *different* type is refused unless you pass `--force`, because every reference to it would
555
+ then resolve to something the engine cannot render. Every entry records
556
+ `production: { method: "uploaded", sourceUrl, sourceName }`.
557
+
558
+ Uploads are read with symlinks refused and capped at 512 MB: the destination is a permanent public
559
+ URL that nothing can delete, so the command will not follow a link to a file you did not mean to
560
+ publish.
561
+
562
+ The upload is refused before anything is sent when the project's `gameId` is not one the logged-in
563
+ account owns, and every failure before the write says `world.json is unchanged`. A `.glb` whose bake
564
+ fails after the upload prints where the GLB landed and the exact `--asset-id` to re-run with, so the
565
+ retry replaces that attempt instead of adding a second asset. While it runs, the upload shows in the
566
+ dev view's assets panel like any other job (`.bitmagic/jobs/`).
567
+
568
+ `bitmagic assets remove` edits `world.json` only — the file stays in storage, exactly as the
569
+ Creator's Remove leaves it (published games keep loading; every upload is immutable). It looks first
570
+ for anything in `world.json` still pointing at the asset — any `assetId`, `<thing>AssetId` or
571
+ `<thing>AssetIds` key, so placed objects, a level's `vwldAssetId`, a map, doors, key items and a
572
+ village's `buildingAssetIds` all count — and refuses while there is, listing where; `--force`
573
+ removes the asset and its placed instances in one write and reports the rest. `assets list` shows
574
+ the same total in its `REFS` column, so the two commands never disagree about what is in use. Game
575
+ code that names the id (`engine.playSound(...)`, `spawnFromAsset(...)`) is not checked, and neither
576
+ is a lookup by *name* — `loadJsonAsset` resolves the first asset with a matching name, so `add`
577
+ warns when a name is already taken.
578
+
579
+ ## Generating a 3D model
580
+
581
+ `bitmagic generate model` turns a sentence into a new placeable asset: the Asset Forger builds a
582
+ mesh, the engine voxelizes it, and the result lands in `assets[]` under a fresh id.
583
+
584
+ ```bash
585
+ bitmagic generate model --prompt "a mossy stone arch"
586
+ bitmagic generate model --prompt "a market stall" --name marketStall --height 3
587
+ bitmagic generate model --prompt "a cathedral" --height 30 # hollow above 6 m, automatically
588
+ ```
589
+
590
+ Place it from game code by name, or drag it in from the dev view's Add-object palette.
591
+
592
+ **Which of the three 3D lanes you want** matters, because picking wrong wastes a generation:
593
+
594
+ | Command | Makes | Existing instances |
595
+ |---|---|---|
596
+ | `generate model --prompt "..."` | a **new** asset from a prompt | none yet — nothing is placed |
597
+ | `generate prop --asset <id> --prompt "..."` | replaces the mesh of an **existing** placeholder | **all of them upgrade at once** |
598
+ | `assets add <file>` | a new asset from a file you already have | none yet |
599
+
600
+ Like `prop` and `vehicle` it needs Chrome and a free port between 3000 and 3199 — only the engine
601
+ can voxelize, so the CLI drives its own headless browser. `--voxel-size` (default 0.1 m) and
602
+ `--max-voxel-size` (default 0.5 m) set the bake resolution; `--height` scales the model, and
603
+ interiors fill solid at or below 6 m and stay hollow above it (`--hollow` / `--solid` override),
604
+ because a solid-filled building is millions of wasted voxels.
605
+
606
+ It costs the same sparks as generating the same object in the web editor — both bill the Asset
607
+ Forger call under one price, so the lane you choose never changes what a creator pays.
608
+
609
+ If the bake fails after the mesh exists, the error prints the exact
610
+ `--glb-url … --asset-id …` command to retry the browser step **without paying again**.
500
611
 
501
612
  ## Drivable vehicles
502
613
 
@@ -527,6 +638,45 @@ exact `--glb-url … --voxel-size …` command to retry the browser step without
527
638
  If the finished asset reports that it will not drive, the project's vendored engine predates
528
639
  vehicle-fitment derivation: run `bitmagic upgrade`, then regenerate.
529
640
 
641
+ ## Managing levels
642
+
643
+ `bitmagic forge` bakes levels but could not manage them: only a game's *first* forge became the
644
+ level it boots into, so fixing a wrong start, renaming, or deleting one meant opening the web
645
+ Creator. These do it from here. They edit `src/work/world.json` directly — no network, no sparks,
646
+ no login — and journal each change to `.bitmagic/edit/events.jsonl` so your agent sees it.
647
+
648
+ ```bash
649
+ bitmagic levels list
650
+ bitmagic levels set-start <levelId>
651
+ bitmagic levels rename <levelId> "Old Town"
652
+ bitmagic levels add <vwldAssetId> [--name "Cavern"] [--make-start]
653
+ bitmagic levels remove <levelId>
654
+ ```
655
+
656
+ `list` shows each level's id, name, which one is the **START**, how many spawn points it owns and
657
+ how many placed objects belong to it — plus a warning when its baked `.vwld` asset has gone missing.
658
+
659
+ **`set-start` is the one that matters most.** `worldProfileData` keeps three *mirror* fields —
660
+ `voxelUrl`, `spawnPoints` and `playerSpawnPosition` — describing the start level, because published
661
+ games boot from those rather than from the level registry. This moves them together. Doing it by
662
+ hand and forgetting the mirrors gives you a game that renders the new level's objects and lighting
663
+ over the **old** level's terrain, with matching collision from a third place and no error anywhere.
664
+ It refuses when the level's `.vwld` asset is missing, since there would be nothing to boot.
665
+
666
+ **`add`** registers a `.vwld` that is already in the project (`bitmagic assets list` shows them) —
667
+ from a forge, or from `bitmagic assets add level.vwld`. It follows `forge`'s rule: a game's first
668
+ level becomes the start, later ones do not unless you pass `--make-start`. On a game that has a
669
+ world but no level registry yet, the existing world is registered as level 1 first, so it is not
670
+ stranded by the one you are adding.
671
+
672
+ **`remove`** takes the level's placed objects, doors and key items with it, and leaves the `.vwld`
673
+ asset alone — it may be large, and nothing here deletes uploaded bytes. It refuses two cases: the
674
+ game's only level, and the **start** level (move the start first, so you never end up with a game
675
+ booting one level's terrain under another's objects).
676
+
677
+ Runtime level switching is engine API — `engine.loadLevel(levelId)` from game code. These commands
678
+ manage the registry, not gameplay.
679
+
530
680
  ## Publishing
531
681
 
532
682
  `bitmagic publish` is two gated steps in one command:
@@ -560,6 +710,26 @@ before `--force` has anything to work with.
560
710
  stick — and note that writing them into `game.json` yourself would change the fingerprint and
561
711
  invalidate the verify you just checked.
562
712
 
713
+ ### Which platform the game is catalogued as
714
+
715
+ Publish syncs two indicators from your project files, so bitmagic.ai lists the game as what it
716
+ actually is:
717
+
718
+ | Indicator | Where you set it | Values |
719
+ |---|---|---|
720
+ | `primaryPlatform` | top level of `src/work/game.json` | `desktop` (templates ship this) or `mobile` |
721
+ | `mobileOrientation` | `worldProfileData` in `src/work/world.json` | `portrait` or `landscape`; absent means no lock |
722
+
723
+ Those files are the **source of truth**: each publish re-sends what they say, so a value changed
724
+ elsewhere (an admin edit, say) is replaced by your next publish, and removing `mobileOrientation`
725
+ from `world.json` clears the stored lock. A project whose `game.json` declares no
726
+ `primaryPlatform` at all leaves the stored value alone rather than forcing it to `desktop`.
727
+
728
+ Only the catalogue entry is at stake here — the game itself is playable from a phone either way.
729
+ Making it *good* on a phone is code, and the scaffolded `AGENTS.md` tells your agent how: bind every
730
+ action with `registerCustomAction`/`declareMobileActions` so each desktop key gets a touch button,
731
+ and never with a raw `keydown` listener.
732
+
563
733
  ### The source archive
564
734
 
565
735
  Every publish also uploads a **source archive** — `.bitmagic/publish/source.zip` — alongside the
@@ -0,0 +1,40 @@
1
+ import type { Environment } from '../config/environments.js';
2
+ import type { ProjectContext } from '../project/context.js';
3
+ import { type VoxelizeFlags, type VoxelizeGlb } from './voxelize-glb.js';
4
+ export interface AddAssetOptions {
5
+ context: ProjectContext;
6
+ environment: Environment;
7
+ token: string;
8
+ filePath: string;
9
+ name?: string;
10
+ assetId?: string;
11
+ keepGlb: boolean;
12
+ /** Replace a still-referenced asset even when the type changes. See the gate in addAssetFile. */
13
+ force: boolean;
14
+ voxel: VoxelizeFlags;
15
+ fetchImpl?: typeof globalThis.fetch;
16
+ voxelize?: VoxelizeGlb;
17
+ now?: () => Date;
18
+ log: (message: string) => void;
19
+ }
20
+ export interface AddAssetResult {
21
+ assetId: string;
22
+ name: string;
23
+ type: string;
24
+ url: string;
25
+ size: number;
26
+ sourceGlbUrl?: string;
27
+ voxelCount?: number;
28
+ /** True when an asset with this id already existed and was replaced. */
29
+ replaced: boolean;
30
+ /** Anything the creator should know that is not an error — e.g. that audio went up untranscoded. */
31
+ notes: string[];
32
+ }
33
+ /**
34
+ * Biggest file this lane will upload. Not a server limit — api-server caps nothing — but a local
35
+ * one: every byte is held in memory, gzipped in memory for the compressible types, and the object
36
+ * is immutable once written with no delete endpoint anywhere, so an accidental multi-gigabyte
37
+ * upload cannot be taken back.
38
+ */
39
+ export declare const MAX_ASSET_BYTES: number;
40
+ export declare function addAssetFile(options: AddAssetOptions): Promise<AddAssetResult>;
@@ -0,0 +1,229 @@
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, describeErrno, errnoCode } from '../errors.js';
26
+ import { applyModificationsToWorld } from '../forge/apply-modifications.js';
27
+ import { projectWorldJsonPath } from '../forge/run-pipeline.js';
28
+ import { uploadProjectFile } from '../http/signed-upload.js';
29
+ import { isJsonObject, readWorldJson } from '../project/world-json.js';
30
+ import { audioAssetEntry, glbAssetEntry, imageAssetEntry, jsonAssetEntry, voxelizedGlbAssetEntry, vxlAssetEntry, } from './entry.js';
31
+ import { audioTranscodeNote, classifyAssetFile, defaultAssetName, mintAssetId, uploadFilename, validateAssetName, } from './file-kind.js';
32
+ import { findAssetReferences } from './remove.js';
33
+ import { assertEngineHonouredId, voxelizeGlbIntoAsset, } from './voxelize-glb.js';
34
+ import { isGlb, summarizeVxl } from './vxl-summary.js';
35
+ import { formatSize } from './list.js';
36
+ /**
37
+ * Biggest file this lane will upload. Not a server limit — api-server caps nothing — but a local
38
+ * one: every byte is held in memory, gzipped in memory for the compressible types, and the object
39
+ * is immutable once written with no delete endpoint anywhere, so an accidental multi-gigabyte
40
+ * upload cannot be taken back.
41
+ */
42
+ export const MAX_ASSET_BYTES = 512 * 1024 * 1024;
43
+ function readFileBytes(filePath) {
44
+ // Refuse a symlink rather than following it. This command's documented caller is an AI agent
45
+ // running with a path from a prompt or from a cloned template, and the destination is a public,
46
+ // immutable, undeletable URL recorded in world.json — so a link pointing at a credentials file
47
+ // or an .ssh key is an exfiltration with no undo. `lstat` does not follow, unlike `readFileSync`.
48
+ let stats;
49
+ try {
50
+ stats = fs.lstatSync(filePath);
51
+ }
52
+ catch (error) {
53
+ const code = errnoCode(error);
54
+ if (code === 'ENOENT')
55
+ throw new CliError(`No such file: ${filePath}. world.json is unchanged.`);
56
+ throw new CliError(`Cannot read ${filePath}: ${describeErrno(error)}. world.json is unchanged.`);
57
+ }
58
+ if (stats.isSymbolicLink()) {
59
+ throw new CliError(`${filePath} is a symlink, and this command uploads to a permanent public URL. `
60
+ + 'Pass the real file instead. world.json is unchanged.');
61
+ }
62
+ if (stats.isDirectory()) {
63
+ throw new CliError(`${filePath} is a directory, not a file. world.json is unchanged.`);
64
+ }
65
+ if (stats.size === 0)
66
+ throw new CliError(`${filePath} is empty. world.json is unchanged.`);
67
+ if (stats.size > MAX_ASSET_BYTES) {
68
+ throw new CliError(`${filePath} is ${Math.round(stats.size / (1024 * 1024))} MB; this command uploads at most `
69
+ + `${MAX_ASSET_BYTES / (1024 * 1024)} MB. world.json is unchanged.`);
70
+ }
71
+ let bytes;
72
+ try {
73
+ bytes = fs.readFileSync(filePath);
74
+ }
75
+ catch (error) {
76
+ // Every failure mode is mapped, so none escapes as a raw stack trace that would break this
77
+ // lane's "world.json is unchanged" contract (EACCES, ELOOP, EISDIR, ERR_FS_FILE_TOO_LARGE...).
78
+ throw new CliError(`Cannot read ${filePath}: ${describeErrno(error)}. world.json is unchanged.`);
79
+ }
80
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
81
+ }
82
+ function vxlEntryBuilder(summary) {
83
+ return (entryBase) => vxlAssetEntry(entryBase, summary);
84
+ }
85
+ function requireVxlSummary(bytes, filename) {
86
+ const summary = summarizeVxl(bytes);
87
+ if (summary === null) {
88
+ throw new CliError(`${filename} is not a VXL3 file this engine can load. Export it from the Creator or the engine `
89
+ + '(only the binary VXL3 format, versions 3-9, is supported). world.json is unchanged.');
90
+ }
91
+ return summary;
92
+ }
93
+ export async function addAssetFile(options) {
94
+ const { context, environment, token, filePath, log } = options;
95
+ const now = options.now ?? (() => new Date());
96
+ // 1. Everything that can be refused for free.
97
+ const file = classifyAssetFile(filePath, { keepGlb: options.keepGlb });
98
+ const filename = uploadFilename(filePath);
99
+ const name = validateAssetName(options.name ?? defaultAssetName(filePath));
100
+ const assetId = options.assetId ?? mintAssetId();
101
+ // 2. The world this is going into, before anything leaves the machine.
102
+ const worldPath = projectWorldJsonPath(context.root);
103
+ const world = readWorldJson(worldPath);
104
+ const notes = [];
105
+ const assets = Array.isArray(world.assets) ? world.assets : [];
106
+ const existing = assets.find((item) => isJsonObject(item) && item.id === assetId);
107
+ const replaced = existing !== undefined;
108
+ // Replacing an id is the most destructive thing this command does, and it used to be the only
109
+ // one with no gate at all — `assets remove` refuses while anything still points at the asset,
110
+ // while `add --asset-id` would happily turn a placed prop into an audio clip. Same rule here:
111
+ // a replacement that CHANGES THE TYPE under live references is refused unless forced, because
112
+ // every one of those references resolves to something the engine cannot render.
113
+ if (existing !== undefined && !options.force) {
114
+ const existingType = typeof existing.type === 'string' ? existing.type : '';
115
+ const references = findAssetReferences(world, assetId);
116
+ if (existingType !== file.assetType && references.length > 0) {
117
+ throw new CliError(`Asset "${assetId}" is a ${existingType} referenced ${references.length === 1 ? 'once' : `${references.length} times`} `
118
+ + `in world.json, and ${filename} would replace it with ${file.assetType === 'image' ? 'an' : 'a'} `
119
+ + `${file.assetType}. Those references would resolve to the wrong kind of asset. `
120
+ + 'Use --force to do it anyway, or omit --asset-id to add a new asset. world.json is unchanged.');
121
+ }
122
+ }
123
+ // A name collision is not fatal, but it is silent and it matters: JSON assets are looked up BY
124
+ // NAME (`loadJsonAsset` -> `getAssetUrlByName`, first match wins), so a second asset sharing a
125
+ // name is simply never loaded. Warned rather than refused — duplicate names are legal, and the
126
+ // creator may well want two `rock`s they address by id.
127
+ const nameClash = assets.some((item) => isJsonObject(item) && item.name === name && item.id !== assetId);
128
+ if (nameClash) {
129
+ notes.push(`Another asset is already named "${name}". Assets addressed by name (JSON config assets, `
130
+ + 'vehicles spawned by name) resolve to the FIRST match, so give this one a distinct --name '
131
+ + 'if game code looks it up that way.');
132
+ }
133
+ // `let`, and nulled after the upload: a 300 MB Blender export otherwise stays reachable from this
134
+ // frame for the whole bake — ten minutes of a headless Chrome doing the voxelization on the same
135
+ // machine — for bytes that were consumed minutes earlier.
136
+ let bytes = readFileBytes(filePath);
137
+ const size = bytes.byteLength;
138
+ // A `.vxl` is inspected before the upload so a file that is not one is refused for free. The
139
+ // parse is carried to the switch below as a builder rather than re-parsed there — `summarizeVxl`
140
+ // gunzips the whole body, so doing it twice for one add is real work for no answer.
141
+ // Same idea as the .vxl parse below: prove the bytes are what the extension claims BEFORE the
142
+ // upload, so a mislabelled file costs nothing instead of an upload plus a browser launch.
143
+ if ((file.kind === 'glb-voxelize' || file.kind === 'glb-keep') && !isGlb(bytes)) {
144
+ throw new CliError(`${filename} is named .glb but does not start with the glTF magic — it is not a binary GLB. `
145
+ + 'A .gltf renamed, a truncated download and a Git-LFS pointer all look like this. '
146
+ + 'world.json is unchanged.');
147
+ }
148
+ const buildVxlEntry = file.kind === 'vxl'
149
+ ? vxlEntryBuilder(requireVxlSummary(bytes, filename))
150
+ : null;
151
+ // 3. Upload.
152
+ log(`Uploading ${filename} (${formatSize(size)})…`);
153
+ const upload = await uploadProjectFile(environment, token, { gameId: context.metadata.gameId, filename, contentType: file.contentType, bytes }, { fetch: options.fetchImpl ?? globalThis.fetch });
154
+ bytes = null;
155
+ const url = upload.url;
156
+ log(`Uploaded: ${url}`);
157
+ const base = { id: assetId, name, url, size, sourceName: filename, at: now().toISOString() };
158
+ // 4. The record — from the engine for a voxelized GLB, built here for everything else.
159
+ let entry;
160
+ switch (file.kind) {
161
+ case 'image':
162
+ entry = imageAssetEntry(base);
163
+ break;
164
+ case 'audio': {
165
+ entry = audioAssetEntry(base);
166
+ const note = audioTranscodeNote(file.extension);
167
+ if (note !== null)
168
+ notes.push(note);
169
+ break;
170
+ }
171
+ case 'json':
172
+ entry = jsonAssetEntry(base);
173
+ break;
174
+ case 'vxl':
175
+ // Non-null for exactly this kind, set above in the same `file.kind` test.
176
+ entry = buildVxlEntry(base);
177
+ break;
178
+ case 'glb-keep':
179
+ entry = glbAssetEntry(base, options.voxel.targetHeight);
180
+ break;
181
+ case 'glb-voxelize': {
182
+ // Names --asset-id explicitly. Unlike `generate prop`/`vehicle` this command has no
183
+ // --glb-url, so "run it again" re-uploads the file and, without a pinned id, MINTS A NEW ONE
184
+ // and leaves the first upload orphaned in storage (uploads are immutable and nothing deletes
185
+ // them). Re-running under the same id is what makes the retry idempotent.
186
+ const retryHint = 'Nothing was charged and world.json is unchanged. Re-run with '
187
+ + `\`--asset-id ${assetId}\` so the retry replaces this attempt instead of adding a second `
188
+ + `asset (the GLB from this run is already uploaded at ${url}).`;
189
+ const voxelize = options.voxelize ?? voxelizeGlbIntoAsset;
190
+ const engineAsset = await voxelize({
191
+ context, environment, token, name, glbUrl: url, assetId, voxel: options.voxel, retryHint,
192
+ requestPrefix: 'upload', log,
193
+ });
194
+ assertEngineHonouredId(engineAsset, assetId, name, `the GLB is already uploaded at ${url}`);
195
+ entry = voxelizedGlbAssetEntry(engineAsset, base);
196
+ break;
197
+ }
198
+ }
199
+ // 5. One atomic write.
200
+ //
201
+ // MERGED over the existing record, not substituted for it. An asset accumulates authored state
202
+ // the uploader knows nothing about — `fitBox` (without which the dev view's HQ regeneration
203
+ // refuses outright), `vehicleFitment` (without which a vehicle stops driving), `collision`,
204
+ // `light`, `flattenTerrain`, `lodVariants` — and writing a fresh record over the top silently
205
+ // discarded all of it. `editor/server.ts` states the same rule for its own asset write: "An
206
+ // updater, not a replacement". The new record wins field by field, so everything this upload
207
+ // actually determined still lands.
208
+ const value = existing === undefined ? entry : { ...existing, ...entry };
209
+ const modification = {
210
+ type: 'upsertRoot',
211
+ path: ['assets'],
212
+ predicate: (item) => isJsonObject(item) && item.id === assetId,
213
+ value,
214
+ };
215
+ applyModificationsToWorld(worldPath, [modification], { subject: 'The uploaded asset' });
216
+ const finalUrl = typeof entry.url === 'string' ? entry.url : url;
217
+ return {
218
+ assetId,
219
+ name,
220
+ type: typeof entry.type === 'string' ? entry.type : file.assetType,
221
+ url: finalUrl,
222
+ size: typeof entry.size === 'number' ? entry.size : size,
223
+ ...(typeof entry.sourceGlbUrl === 'string' ? { sourceGlbUrl: entry.sourceGlbUrl } : {}),
224
+ ...(typeof entry.voxelCount === 'number' ? { voxelCount: entry.voxelCount } : {}),
225
+ replaced,
226
+ notes,
227
+ };
228
+ }
229
+ //# 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"}