@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.
- package/README.md +175 -5
- package/dist/assets/add.d.ts +40 -0
- package/dist/assets/add.js +229 -0
- package/dist/assets/add.js.map +1 -0
- package/dist/assets/entry.d.ts +44 -0
- package/dist/assets/entry.js +71 -0
- package/dist/assets/entry.js.map +1 -0
- package/dist/assets/file-kind.d.ts +61 -0
- package/dist/assets/file-kind.js +195 -0
- package/dist/assets/file-kind.js.map +1 -0
- package/dist/assets/list.d.ts +27 -0
- package/dist/assets/list.js +50 -0
- package/dist/assets/list.js.map +1 -0
- package/dist/assets/remove.d.ts +50 -0
- package/dist/assets/remove.js +91 -0
- package/dist/assets/remove.js.map +1 -0
- package/dist/assets/voxelize-glb.d.ts +37 -0
- package/dist/assets/voxelize-glb.js +65 -0
- package/dist/assets/voxelize-glb.js.map +1 -0
- package/dist/assets/vxl-summary.d.ts +32 -0
- package/dist/assets/vxl-summary.js +118 -0
- package/dist/assets/vxl-summary.js.map +1 -0
- package/dist/bin.js +25 -0
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/assets.d.ts +56 -0
- package/dist/commands/assets.js +328 -0
- package/dist/commands/assets.js.map +1 -0
- package/dist/commands/cover.js +2 -3
- package/dist/commands/cover.js.map +1 -1
- package/dist/commands/forge.d.ts +2 -9
- package/dist/commands/forge.js +2 -13
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.d.ts +41 -9
- package/dist/commands/generate.js +234 -54
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/judge.d.ts +2 -7
- package/dist/commands/judge.js +26 -19
- package/dist/commands/judge.js.map +1 -1
- package/dist/commands/levels.d.ts +33 -0
- package/dist/commands/levels.js +241 -0
- package/dist/commands/levels.js.map +1 -0
- package/dist/commands/number-flag.d.ts +7 -0
- package/dist/commands/number-flag.js +38 -0
- package/dist/commands/number-flag.js.map +1 -0
- package/dist/commands/publish.js +13 -0
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/run-deps.d.ts +27 -0
- package/dist/commands/run-deps.js +43 -0
- package/dist/commands/run-deps.js.map +1 -0
- package/dist/editor/journal.d.ts +48 -1
- package/dist/editor/journal.js +44 -0
- package/dist/editor/journal.js.map +1 -1
- package/dist/editor/previews.d.ts +6 -0
- package/dist/editor/previews.js +16 -0
- package/dist/editor/previews.js.map +1 -1
- package/dist/editor/server.js +228 -53
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +221 -116
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/editor/voxel-save.d.ts +47 -0
- package/dist/editor/voxel-save.js +49 -0
- package/dist/editor/voxel-save.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -1
- package/dist/forge/apply-modifications.d.ts +18 -1
- package/dist/forge/apply-modifications.js +15 -32
- package/dist/forge/apply-modifications.js.map +1 -1
- package/dist/forge/browser-host.js +9 -4
- package/dist/forge/browser-host.js.map +1 -1
- package/dist/forge/voxelize-session.d.ts +61 -0
- package/dist/forge/voxelize-session.js +137 -0
- package/dist/forge/voxelize-session.js.map +1 -0
- package/dist/generate/model.d.ts +36 -0
- package/dist/generate/model.js +90 -0
- package/dist/generate/model.js.map +1 -0
- package/dist/generate/prop.d.ts +52 -4
- package/dist/generate/prop.js +169 -102
- package/dist/generate/prop.js.map +1 -1
- package/dist/generate/stream.d.ts +7 -0
- package/dist/generate/stream.js +2 -1
- package/dist/generate/stream.js.map +1 -1
- package/dist/generate/vehicle.js +5 -85
- package/dist/generate/vehicle.js.map +1 -1
- package/dist/http/signed-upload.d.ts +55 -0
- package/dist/http/signed-upload.js +168 -0
- package/dist/http/signed-upload.js.map +1 -0
- package/dist/judge/facts.js +72 -57
- package/dist/judge/facts.js.map +1 -1
- package/dist/levels/registry.d.ts +88 -0
- package/dist/levels/registry.js +309 -0
- package/dist/levels/registry.js.map +1 -0
- package/dist/messages/check.d.ts +30 -0
- package/dist/messages/check.js +119 -0
- package/dist/messages/check.js.map +1 -0
- package/dist/messages/notice.d.ts +58 -0
- package/dist/messages/notice.js +54 -0
- package/dist/messages/notice.js.map +1 -0
- package/dist/messages/refresh.d.ts +19 -0
- package/dist/messages/refresh.js +42 -0
- package/dist/messages/refresh.js.map +1 -0
- package/dist/project/jobs.d.ts +1 -1
- package/dist/project/jobs.js.map +1 -1
- package/dist/publish/client.d.ts +13 -0
- package/dist/publish/client.js +13 -34
- package/dist/publish/client.js.map +1 -1
- package/dist/publish/platform.d.ts +46 -0
- package/dist/publish/platform.js +92 -0
- package/dist/publish/platform.js.map +1 -0
- package/dist/scaffold/project-files.js +109 -21
- package/dist/scaffold/project-files.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bitmagic levels` — the level registry in `worldProfileData.levels`, without the web Creator.
|
|
3
|
+
*
|
|
4
|
+
* `forge` can CREATE levels but never could manage them: only a game's first forge becomes the
|
|
5
|
+
* start level (or `forge --make-start` at creation), so fixing a wrong start, renaming, or deleting
|
|
6
|
+
* a level meant opening the Creator.
|
|
7
|
+
*
|
|
8
|
+
* ── The invariant this file exists to respect ────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* `worldProfileData` keeps three LEGACY MIRRORS — `voxelUrl`, `spawnPoints` and
|
|
11
|
+
* `playerSpawnPosition` — describing the START level, because frozen genre and work-template code
|
|
12
|
+
* boots from those rather than from the registry (see `WorldLevel` in game/src/types/game.ts).
|
|
13
|
+
*
|
|
14
|
+
* Nothing self-heals on the normal boot path. `applyBootLevelOverride` does recompute them, but
|
|
15
|
+
* only when the main screen's level chooser passes a `bootLevelId`. So a `startLevelId` moved
|
|
16
|
+
* without its mirrors boots the NEW level's objects, lighting and navmesh over the OLD level's
|
|
17
|
+
* terrain, and nothing throws. That is the failure this module exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* The mirrors come from `buildStartLevelMirrors` in `@bitmagic/world-forger`, deliberately rather
|
|
20
|
+
* than reimplemented — the same function the forge uses, carrying a subtlety worth not
|
|
21
|
+
* rediscovering: spawn mirrors are written ONLY when the level owns spawn points, because a level
|
|
22
|
+
* without its own inherits the globals, which are then already correct.
|
|
23
|
+
*
|
|
24
|
+
* Pure: it computes modifications and never applies them.
|
|
25
|
+
*/
|
|
26
|
+
import { type LevelEntry, type WorldJsonModification } from '@bitmagic/world-forger/pipeline/index.js';
|
|
27
|
+
import { type JsonObject } from '../project/world-json.js';
|
|
28
|
+
export interface LevelRow {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
vwldAssetId: string;
|
|
32
|
+
/** The `.vwld` asset is absent from `assets[]`, so this level has nothing to load. */
|
|
33
|
+
missingAsset: boolean;
|
|
34
|
+
start: boolean;
|
|
35
|
+
spawnPoints: number;
|
|
36
|
+
/** Placed objects tagged to this level. Untagged ones are global and belong to none. */
|
|
37
|
+
placed: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function listLevels(world: JsonObject): LevelRow[];
|
|
40
|
+
export interface LevelPlan {
|
|
41
|
+
modifications: WorldJsonModification[];
|
|
42
|
+
/** Said after the write. Never a reason to refuse. */
|
|
43
|
+
notes: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Make `levelId` the level the game boots into — registry AND mirrors, together.
|
|
47
|
+
*
|
|
48
|
+
* Refused when the level's `.vwld` asset is missing or has no url: the mirrors are what actually
|
|
49
|
+
* boot the game, so writing `startLevelId` alone would leave `voxelUrl` describing the previous
|
|
50
|
+
* level, and the registry and the thing that loads would disagree with no error anywhere.
|
|
51
|
+
*/
|
|
52
|
+
export declare function planSetStart(world: JsonObject, levelId: string): LevelPlan;
|
|
53
|
+
export declare function planRename(world: JsonObject, levelId: string, name: string): LevelPlan;
|
|
54
|
+
export interface AddPlan extends LevelPlan {
|
|
55
|
+
level: LevelEntry;
|
|
56
|
+
/** True when this made the game multi-level for the first time. */
|
|
57
|
+
converted: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register an existing `.vwld` asset as a level.
|
|
61
|
+
*
|
|
62
|
+
* Start-level policy matches `forge`'s: a game's FIRST level becomes the start, later ones do not,
|
|
63
|
+
* and `--make-start` overrides. Stealing the start from a working game because someone registered
|
|
64
|
+
* a second level would be the surprising outcome.
|
|
65
|
+
*/
|
|
66
|
+
export declare function planAdd(world: JsonObject, vwldAssetId: string, options: {
|
|
67
|
+
name?: string;
|
|
68
|
+
makeStart: boolean;
|
|
69
|
+
}): AddPlan;
|
|
70
|
+
export interface RemovePlan extends LevelPlan {
|
|
71
|
+
level: LevelEntry;
|
|
72
|
+
/** Placed instances tagged to this level, which go with it. */
|
|
73
|
+
placedRemoved: number;
|
|
74
|
+
/** Doors and key items scoped to it, which also go. */
|
|
75
|
+
scopedRemoved: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Remove a level from the registry.
|
|
79
|
+
*
|
|
80
|
+
* Two refusals, matching the agent's `manage-levels delete` so the two lanes cannot disagree:
|
|
81
|
+
* the last level, and the START level. The second matters most — `startLevelId` naming nothing
|
|
82
|
+
* falls back to `levels[0]` at boot while the mirrors still describe the deleted level, which is
|
|
83
|
+
* the silent stale-mirror failure. Move the start first, deliberately.
|
|
84
|
+
*
|
|
85
|
+
* The `.vwld` asset is left alone: it may be large, nothing here deletes uploaded bytes, and a
|
|
86
|
+
* re-add is then free.
|
|
87
|
+
*/
|
|
88
|
+
export declare function planRemove(world: JsonObject, levelId: string): RemovePlan;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bitmagic levels` — the level registry in `worldProfileData.levels`, without the web Creator.
|
|
3
|
+
*
|
|
4
|
+
* `forge` can CREATE levels but never could manage them: only a game's first forge becomes the
|
|
5
|
+
* start level (or `forge --make-start` at creation), so fixing a wrong start, renaming, or deleting
|
|
6
|
+
* a level meant opening the Creator.
|
|
7
|
+
*
|
|
8
|
+
* ── The invariant this file exists to respect ────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* `worldProfileData` keeps three LEGACY MIRRORS — `voxelUrl`, `spawnPoints` and
|
|
11
|
+
* `playerSpawnPosition` — describing the START level, because frozen genre and work-template code
|
|
12
|
+
* boots from those rather than from the registry (see `WorldLevel` in game/src/types/game.ts).
|
|
13
|
+
*
|
|
14
|
+
* Nothing self-heals on the normal boot path. `applyBootLevelOverride` does recompute them, but
|
|
15
|
+
* only when the main screen's level chooser passes a `bootLevelId`. So a `startLevelId` moved
|
|
16
|
+
* without its mirrors boots the NEW level's objects, lighting and navmesh over the OLD level's
|
|
17
|
+
* terrain, and nothing throws. That is the failure this module exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* The mirrors come from `buildStartLevelMirrors` in `@bitmagic/world-forger`, deliberately rather
|
|
20
|
+
* than reimplemented — the same function the forge uses, carrying a subtlety worth not
|
|
21
|
+
* rediscovering: spawn mirrors are written ONLY when the level owns spawn points, because a level
|
|
22
|
+
* without its own inherits the globals, which are then already correct.
|
|
23
|
+
*
|
|
24
|
+
* Pure: it computes modifications and never applies them.
|
|
25
|
+
*/
|
|
26
|
+
import { buildConversionModifications, buildStartLevelMirrors, getLevels, getStartLevel, mintLevelId, } from '@bitmagic/world-forger/pipeline/index.js';
|
|
27
|
+
import { CliError } from '../errors.js';
|
|
28
|
+
import { isJsonObject } from '../project/world-json.js';
|
|
29
|
+
/** The same document, seen through the shared helpers' narrower view. */
|
|
30
|
+
function asWorldJson(world) {
|
|
31
|
+
return world;
|
|
32
|
+
}
|
|
33
|
+
function assetsOf(world) {
|
|
34
|
+
return (Array.isArray(world.assets) ? world.assets : []).filter(isJsonObject);
|
|
35
|
+
}
|
|
36
|
+
export function listLevels(world) {
|
|
37
|
+
const start = getStartLevel(asWorldJson(world));
|
|
38
|
+
const assets = assetsOf(world);
|
|
39
|
+
const objects = (Array.isArray(world.environmentObjects) ? world.environmentObjects : [])
|
|
40
|
+
.filter(isJsonObject);
|
|
41
|
+
return getLevels(asWorldJson(world)).map((level) => ({
|
|
42
|
+
id: level.id,
|
|
43
|
+
name: level.name,
|
|
44
|
+
vwldAssetId: level.vwldAssetId,
|
|
45
|
+
missingAsset: !assets.some((asset) => asset.id === level.vwldAssetId),
|
|
46
|
+
start: start !== null && start.id === level.id,
|
|
47
|
+
spawnPoints: level.spawnPoints?.length ?? 0,
|
|
48
|
+
placed: objects.filter((object) => object.levelId === level.id).length,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function requireLevel(world, levelId) {
|
|
52
|
+
const levels = getLevels(asWorldJson(world));
|
|
53
|
+
const level = levels.find((entry) => entry.id === levelId);
|
|
54
|
+
if (level === undefined) {
|
|
55
|
+
throw new CliError(`No level with id "${levelId}" in this project. `
|
|
56
|
+
+ (levels.length > 0
|
|
57
|
+
? `This game has: ${levels.map((entry) => `${entry.id} ("${entry.name}")`).join(', ')}. `
|
|
58
|
+
: 'This game has no levels — `bitmagic forge` creates one. ')
|
|
59
|
+
+ 'world.json is unchanged.');
|
|
60
|
+
}
|
|
61
|
+
return level;
|
|
62
|
+
}
|
|
63
|
+
/** The url a level's `.vwld` asset serves, or null when it is missing or not a level bake. */
|
|
64
|
+
function vwldUrlFor(world, level) {
|
|
65
|
+
const asset = assetsOf(world).find((item) => item.id === level.vwldAssetId);
|
|
66
|
+
if (asset === undefined || asset.type !== 'vwld')
|
|
67
|
+
return null;
|
|
68
|
+
return typeof asset.url === 'string' && asset.url !== '' ? asset.url : null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Make `levelId` the level the game boots into — registry AND mirrors, together.
|
|
72
|
+
*
|
|
73
|
+
* Refused when the level's `.vwld` asset is missing or has no url: the mirrors are what actually
|
|
74
|
+
* boot the game, so writing `startLevelId` alone would leave `voxelUrl` describing the previous
|
|
75
|
+
* level, and the registry and the thing that loads would disagree with no error anywhere.
|
|
76
|
+
*/
|
|
77
|
+
export function planSetStart(world, levelId) {
|
|
78
|
+
const level = requireLevel(world, levelId);
|
|
79
|
+
const current = getStartLevel(asWorldJson(world));
|
|
80
|
+
if (current !== null && current.id === levelId) {
|
|
81
|
+
throw new CliError(`"${level.name}" is already the start level. world.json is unchanged.`);
|
|
82
|
+
}
|
|
83
|
+
const vwldUrl = vwldUrlFor(world, level);
|
|
84
|
+
if (vwldUrl === null) {
|
|
85
|
+
throw new CliError(`Level "${level.name}" points at asset "${level.vwldAssetId}", which this project has no `
|
|
86
|
+
+ 'loadable .vwld for — so it has nothing to boot. Fix the level first (`bitmagic assets '
|
|
87
|
+
+ 'list` shows what is there). world.json is unchanged.');
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
modifications: [
|
|
91
|
+
{ type: 'set', path: ['startLevelId'], value: levelId },
|
|
92
|
+
// Without these the game boots the OLD level's terrain. See the file header.
|
|
93
|
+
...buildStartLevelMirrors(level, vwldUrl),
|
|
94
|
+
],
|
|
95
|
+
notes: (level.spawnPoints?.length ?? 0) === 0
|
|
96
|
+
? [`"${level.name}" has no spawn points of its own, so it keeps the game's global ones.`]
|
|
97
|
+
: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function planRename(world, levelId, name) {
|
|
101
|
+
const level = requireLevel(world, levelId);
|
|
102
|
+
const trimmed = name.trim();
|
|
103
|
+
if (trimmed === '')
|
|
104
|
+
throw new CliError('A level name cannot be empty. world.json is unchanged.');
|
|
105
|
+
if (trimmed === level.name) {
|
|
106
|
+
throw new CliError(`"${level.name}" already has that name. world.json is unchanged.`);
|
|
107
|
+
}
|
|
108
|
+
const clash = getLevels(asWorldJson(world)).some((e) => e.id !== levelId && e.name === trimmed);
|
|
109
|
+
return {
|
|
110
|
+
modifications: [{
|
|
111
|
+
type: 'update',
|
|
112
|
+
path: ['levels'],
|
|
113
|
+
predicate: (item) => isJsonObject(item) && item.id === levelId,
|
|
114
|
+
// An updater, not a `{ ...level, name }` snapshot (which is what the agent's tool builds):
|
|
115
|
+
// the snapshot was read before the write and reverts anything another writer changed on this
|
|
116
|
+
// level in between. Same reason editor/save.ts gives for its own level write.
|
|
117
|
+
value: (item) => ({ ...(isJsonObject(item) ? item : {}), name: trimmed }),
|
|
118
|
+
}],
|
|
119
|
+
notes: clash
|
|
120
|
+
? [`Another level is also called "${trimmed}" — names are for humans, ids are what code uses.`]
|
|
121
|
+
: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Register an existing `.vwld` asset as a level.
|
|
126
|
+
*
|
|
127
|
+
* Start-level policy matches `forge`'s: a game's FIRST level becomes the start, later ones do not,
|
|
128
|
+
* and `--make-start` overrides. Stealing the start from a working game because someone registered
|
|
129
|
+
* a second level would be the surprising outcome.
|
|
130
|
+
*/
|
|
131
|
+
export function planAdd(world, vwldAssetId, options) {
|
|
132
|
+
const asset = assetsOf(world).find((item) => item.id === vwldAssetId);
|
|
133
|
+
if (asset === undefined) {
|
|
134
|
+
const available = assetsOf(world).filter((item) => item.type === 'vwld');
|
|
135
|
+
throw new CliError(`No asset with id "${vwldAssetId}" in this project. `
|
|
136
|
+
+ (available.length > 0
|
|
137
|
+
? `Baked levels here: ${available.map((a) => `${String(a.id)} ("${String(a.name)}")`).join(', ')}. `
|
|
138
|
+
: 'This project has no baked levels — `bitmagic forge` makes one. ')
|
|
139
|
+
+ 'world.json is unchanged.');
|
|
140
|
+
}
|
|
141
|
+
if (asset.type !== 'vwld') {
|
|
142
|
+
throw new CliError(`Asset "${vwldAssetId}" is ${typeof asset.type === 'string' ? `a ${asset.type}` : 'an unknown'} `
|
|
143
|
+
+ 'asset, not a baked level (.vwld). Only a level bake can become a level. '
|
|
144
|
+
+ 'world.json is unchanged.');
|
|
145
|
+
}
|
|
146
|
+
const url = typeof asset.url === 'string' ? asset.url : '';
|
|
147
|
+
if (url === '') {
|
|
148
|
+
throw new CliError(`Asset "${vwldAssetId}" has no url, so a level made from it would have nothing to load. `
|
|
149
|
+
+ 'world.json is unchanged.');
|
|
150
|
+
}
|
|
151
|
+
// Two levels on one bake is the same place twice, and the forge's re-bake path resolves a level
|
|
152
|
+
// by `vwldAssetId` — with duplicates it picks the first arbitrarily.
|
|
153
|
+
const sharing = getLevels(asWorldJson(world)).find((entry) => entry.vwldAssetId === vwldAssetId);
|
|
154
|
+
if (sharing !== undefined) {
|
|
155
|
+
throw new CliError(`Level "${sharing.name}" (${sharing.id}) already uses that bake. world.json is unchanged.`);
|
|
156
|
+
}
|
|
157
|
+
const assetName = typeof asset.name === 'string' && asset.name !== '' ? asset.name : vwldAssetId;
|
|
158
|
+
const level = {
|
|
159
|
+
id: mintLevelId(),
|
|
160
|
+
name: (options.name ?? assetName).trim() || assetName,
|
|
161
|
+
vwldAssetId,
|
|
162
|
+
};
|
|
163
|
+
const existing = getLevels(asWorldJson(world));
|
|
164
|
+
if (existing.length > 0) {
|
|
165
|
+
const makeStart = options.makeStart;
|
|
166
|
+
return {
|
|
167
|
+
level,
|
|
168
|
+
converted: false,
|
|
169
|
+
modifications: [
|
|
170
|
+
{ type: 'push', path: ['levels'], value: level },
|
|
171
|
+
...(makeStart
|
|
172
|
+
? [
|
|
173
|
+
{ type: 'set', path: ['startLevelId'], value: level.id },
|
|
174
|
+
...buildStartLevelMirrors(level, url),
|
|
175
|
+
]
|
|
176
|
+
: []),
|
|
177
|
+
],
|
|
178
|
+
notes: makeStart
|
|
179
|
+
? [`"${level.name}" is now the start level.`]
|
|
180
|
+
: ['The start level is unchanged — pass --make-start, or use `bitmagic levels set-start`.'],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// No registry yet. If the game already HAS a world (a `voxelUrl` naming a registered bake), that
|
|
184
|
+
// world must become a level too — otherwise turning on multi-level would strand the place the
|
|
185
|
+
// game currently loads, with no entry naming it.
|
|
186
|
+
const conversion = safeConversion(world);
|
|
187
|
+
if (conversion !== null) {
|
|
188
|
+
return {
|
|
189
|
+
level,
|
|
190
|
+
converted: true,
|
|
191
|
+
modifications: [
|
|
192
|
+
// Everything the conversion does EXCEPT its own `levels` write, which named only level 1.
|
|
193
|
+
...conversion.modifications.filter((mod) => !('path' in mod) || mod.path[0] !== 'levels'),
|
|
194
|
+
{ type: 'set', path: ['levels'], value: [conversion.level1, level] },
|
|
195
|
+
],
|
|
196
|
+
notes: [
|
|
197
|
+
`This game's existing world became level "${conversion.level1.name}" and remains the start `
|
|
198
|
+
+ 'level; every object that was not already tagged now belongs to it.',
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// A procedural game with no world of its own: the new level is the only one, so it must boot.
|
|
203
|
+
return {
|
|
204
|
+
level,
|
|
205
|
+
converted: false,
|
|
206
|
+
modifications: [
|
|
207
|
+
{ type: 'set', path: ['levels'], value: [level] },
|
|
208
|
+
{ type: 'set', path: ['startLevelId'], value: level.id },
|
|
209
|
+
...buildStartLevelMirrors(level, url),
|
|
210
|
+
],
|
|
211
|
+
notes: [`This is the game's first level, so it is the start level.`],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The legacy→levels conversion, or null when there is nothing to convert.
|
|
216
|
+
*
|
|
217
|
+
* Throws inside the shared helper when the current world is not a registered bake (a legacy map or
|
|
218
|
+
* an unregistered one); that is a refusal for the caller, not a crash.
|
|
219
|
+
*/
|
|
220
|
+
function safeConversion(world) {
|
|
221
|
+
try {
|
|
222
|
+
return buildConversionModifications(asWorldJson(world));
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new CliError('This game\'s current world is not a registered level bake, so it cannot be turned into a '
|
|
226
|
+
+ `level alongside the new one: ${error instanceof Error ? error.message : String(error)} `
|
|
227
|
+
+ 'Re-bake it with `bitmagic forge` first. world.json is unchanged.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Remove a level from the registry.
|
|
232
|
+
*
|
|
233
|
+
* Two refusals, matching the agent's `manage-levels delete` so the two lanes cannot disagree:
|
|
234
|
+
* the last level, and the START level. The second matters most — `startLevelId` naming nothing
|
|
235
|
+
* falls back to `levels[0]` at boot while the mirrors still describe the deleted level, which is
|
|
236
|
+
* the silent stale-mirror failure. Move the start first, deliberately.
|
|
237
|
+
*
|
|
238
|
+
* The `.vwld` asset is left alone: it may be large, nothing here deletes uploaded bytes, and a
|
|
239
|
+
* re-add is then free.
|
|
240
|
+
*/
|
|
241
|
+
export function planRemove(world, levelId) {
|
|
242
|
+
const level = requireLevel(world, levelId);
|
|
243
|
+
const levels = getLevels(asWorldJson(world));
|
|
244
|
+
if (levels.length <= 1) {
|
|
245
|
+
throw new CliError('This is the game\'s only level, and a levels-mode game needs one. world.json is unchanged.');
|
|
246
|
+
}
|
|
247
|
+
const start = getStartLevel(asWorldJson(world));
|
|
248
|
+
if (start !== null && start.id === levelId) {
|
|
249
|
+
const other = levels.find((entry) => entry.id !== levelId);
|
|
250
|
+
throw new CliError(`"${level.name}" is the start level. Removing it would leave the game booting one level's `
|
|
251
|
+
+ 'terrain under another\'s objects. Move the start first:\n'
|
|
252
|
+
+ ` bitmagic levels set-start ${other?.id ?? '<levelId>'}\n`
|
|
253
|
+
+ 'world.json is unchanged.');
|
|
254
|
+
}
|
|
255
|
+
const profile = isJsonObject(world.worldProfileData) ? world.worldProfileData : {};
|
|
256
|
+
const objects = (Array.isArray(world.environmentObjects) ? world.environmentObjects : [])
|
|
257
|
+
.filter(isJsonObject);
|
|
258
|
+
const placedRemoved = objects.filter((object) => object.levelId === levelId).length;
|
|
259
|
+
const modifications = [
|
|
260
|
+
{
|
|
261
|
+
type: 'remove',
|
|
262
|
+
path: ['levels'],
|
|
263
|
+
predicate: (item) => isJsonObject(item) && item.id === levelId,
|
|
264
|
+
},
|
|
265
|
+
// Instances belonging to this level go with it. Left behind they would load in no level at
|
|
266
|
+
// all — invisible, and still counted as references that pin their assets against a purge.
|
|
267
|
+
{
|
|
268
|
+
type: 'removeRoot',
|
|
269
|
+
path: ['environmentObjects'],
|
|
270
|
+
predicate: (item) => isJsonObject(item) && item.levelId === levelId,
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
// Doors and key items are level-scoped the same way, and the agent's tool leaves them — which
|
|
274
|
+
// makes them permanent dead weight: they never spawn, yet `persist-level` still counts their
|
|
275
|
+
// assetIds as referenced, so the placeholder assets can never be purged either.
|
|
276
|
+
const scopedRemoved = [];
|
|
277
|
+
for (const key of ['doors', 'keyItems']) {
|
|
278
|
+
const entries = Array.isArray(profile[key]) ? profile[key].filter(isJsonObject) : [];
|
|
279
|
+
const doomed = entries.filter((entry) => entry.levelId === levelId).length;
|
|
280
|
+
if (doomed === 0)
|
|
281
|
+
continue;
|
|
282
|
+
scopedRemoved.push(`${doomed} ${key}`);
|
|
283
|
+
modifications.push({
|
|
284
|
+
type: 'remove',
|
|
285
|
+
path: [key],
|
|
286
|
+
predicate: (item) => isJsonObject(item) && item.levelId === levelId,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const notes = [];
|
|
290
|
+
if (placedRemoved > 0) {
|
|
291
|
+
notes.push(`${placedRemoved} placed object(s) belonged to it and were removed with it.`);
|
|
292
|
+
}
|
|
293
|
+
if (scopedRemoved.length > 0)
|
|
294
|
+
notes.push(`Also removed: ${scopedRemoved.join(', ')}.`);
|
|
295
|
+
// The start-screen chooser filters by id and is hand-authored; it warns and skips unknown ids on
|
|
296
|
+
// its own, so this is reported rather than rewritten.
|
|
297
|
+
if (choosersNaming(profile, levelId) > 0) {
|
|
298
|
+
notes.push('The main screen\'s level chooser still lists this level; it will skip it, but tidy '
|
|
299
|
+
+ '`hud.startScreen.selections[].levelIds` when you get a chance.');
|
|
300
|
+
}
|
|
301
|
+
return { level, modifications, notes, placedRemoved, scopedRemoved };
|
|
302
|
+
}
|
|
303
|
+
function choosersNaming(profile, levelId) {
|
|
304
|
+
const hud = isJsonObject(profile.hud) ? profile.hud : {};
|
|
305
|
+
const startScreen = isJsonObject(hud.startScreen) ? hud.startScreen : {};
|
|
306
|
+
const selections = Array.isArray(startScreen.selections) ? startScreen.selections : [];
|
|
307
|
+
return selections.filter((step) => isJsonObject(step) && Array.isArray(step.levelIds) && step.levelIds.includes(levelId)).length;
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/levels/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EnvironmentName } from '../config/environments.js';
|
|
2
|
+
import { type UnreadMessagesCache } from './notice.js';
|
|
3
|
+
export declare function messagesCachePath(baseDir?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* The last recorded answer, or null if there is none to trust.
|
|
6
|
+
*
|
|
7
|
+
* Missing, unreadable and malformed all collapse to null on purpose: the caller's next move is
|
|
8
|
+
* identical in every case — say nothing now, refresh in the background.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readUnreadMessagesCache(baseDir?: string): UnreadMessagesCache | null;
|
|
11
|
+
export declare function writeUnreadMessagesCache(cache: UnreadMessagesCache, baseDir?: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* The line bin.ts prints before a command runs — read from the cache, never the network.
|
|
14
|
+
*
|
|
15
|
+
* A cache belonging to a DIFFERENT environment answers a different question and is ignored:
|
|
16
|
+
* a dev broadcast must not nudge a prod session, or the other way round. The same rule
|
|
17
|
+
* `pendingUpdateNotice` applies to dist-tags.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pendingMessagesNotice(environment: EnvironmentName | undefined, baseDir?: string, now?: number): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Refresh the cache if it is stale. Fire-and-forget: callers must not await this for its value.
|
|
22
|
+
*
|
|
23
|
+
* A cache about a different environment is refreshed immediately however recent it is —
|
|
24
|
+
* switching environments must not leave the nudge mute for the rest of its interval.
|
|
25
|
+
*
|
|
26
|
+
* On failure the result is still written, with `unread: null`, so an offline machine backs off
|
|
27
|
+
* for the interval instead of asking on every command. That is why the failure marker is null
|
|
28
|
+
* rather than 0: zero would be a claim ("you have nothing unread"), null is an admission.
|
|
29
|
+
*/
|
|
30
|
+
export declare function refreshUnreadMessagesInBackground(environment: EnvironmentName, apiUrl: string, accessToken: string, now?: number, baseDir?: string, fetchImpl?: typeof globalThis.fetch): Promise<void>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { defaultBaseDir } from '../config/credentials.js';
|
|
4
|
+
import { apiServerHeaders } from '../telemetry/request-headers.js';
|
|
5
|
+
import { isMessagesCacheStale, unreadMessagesNotice, } from './notice.js';
|
|
6
|
+
/**
|
|
7
|
+
* Reading and refreshing the unread-messages count.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately the same shape as update/check.ts, which already solved every hard part: the
|
|
10
|
+
* notice is decided from the CACHE ONLY (never the network, so printing costs nothing), the
|
|
11
|
+
* refresh is fire-and-forget and bounded, and every filesystem error is swallowed because this
|
|
12
|
+
* is a hint, not a thing worth failing a command over.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Cap on the background refresh.
|
|
16
|
+
*
|
|
17
|
+
* A pending fetch keeps Node alive, so an unbounded request would hold a finished command open
|
|
18
|
+
* for as long as the network sulks. Matches the update check's allowance.
|
|
19
|
+
*/
|
|
20
|
+
const REFRESH_TIMEOUT_MS = 2000;
|
|
21
|
+
export function messagesCachePath(baseDir = defaultBaseDir()) {
|
|
22
|
+
return path.join(baseDir, 'messages-check.json');
|
|
23
|
+
}
|
|
24
|
+
function isCache(value) {
|
|
25
|
+
if (typeof value !== 'object' || value === null)
|
|
26
|
+
return false;
|
|
27
|
+
const c = value;
|
|
28
|
+
return ((c.environment === 'dev' || c.environment === 'prod' || c.environment === 'local') &&
|
|
29
|
+
(c.unread === null || typeof c.unread === 'number') &&
|
|
30
|
+
(c.inboxUrl === null || typeof c.inboxUrl === 'string') &&
|
|
31
|
+
typeof c.checkedAt === 'number');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The last recorded answer, or null if there is none to trust.
|
|
35
|
+
*
|
|
36
|
+
* Missing, unreadable and malformed all collapse to null on purpose: the caller's next move is
|
|
37
|
+
* identical in every case — say nothing now, refresh in the background.
|
|
38
|
+
*/
|
|
39
|
+
export function readUnreadMessagesCache(baseDir = defaultBaseDir()) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(fs.readFileSync(messagesCachePath(baseDir), 'utf-8'));
|
|
42
|
+
return isCache(parsed) ? parsed : null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function writeUnreadMessagesCache(cache, baseDir = defaultBaseDir()) {
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(messagesCachePath(baseDir), `${JSON.stringify(cache)}\n`);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// A cache we cannot write is a nudge one run late, not an error anyone should see.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The line bin.ts prints before a command runs — read from the cache, never the network.
|
|
59
|
+
*
|
|
60
|
+
* A cache belonging to a DIFFERENT environment answers a different question and is ignored:
|
|
61
|
+
* a dev broadcast must not nudge a prod session, or the other way round. The same rule
|
|
62
|
+
* `pendingUpdateNotice` applies to dist-tags.
|
|
63
|
+
*/
|
|
64
|
+
export function pendingMessagesNotice(environment, baseDir, now = Date.now()) {
|
|
65
|
+
if (environment === undefined)
|
|
66
|
+
return null;
|
|
67
|
+
const cache = readUnreadMessagesCache(baseDir);
|
|
68
|
+
if (cache === null || cache.environment !== environment)
|
|
69
|
+
return null;
|
|
70
|
+
return unreadMessagesNotice(cache, now);
|
|
71
|
+
}
|
|
72
|
+
/** Ask api-server for the caller's count. Null on any failure — this never throws. */
|
|
73
|
+
async function fetchUnread(apiUrl, accessToken, fetchImpl) {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetchImpl(`${apiUrl}/api/cli/v1/messages/unread`, {
|
|
76
|
+
headers: {
|
|
77
|
+
...apiServerHeaders(),
|
|
78
|
+
Authorization: `Bearer ${accessToken}`,
|
|
79
|
+
Accept: 'application/json',
|
|
80
|
+
},
|
|
81
|
+
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
return null;
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
if (typeof data !== 'object' || data === null)
|
|
87
|
+
return null;
|
|
88
|
+
const d = data;
|
|
89
|
+
if (typeof d.unreadCount !== 'number' || typeof d.inboxUrl !== 'string')
|
|
90
|
+
return null;
|
|
91
|
+
return { unreadCount: d.unreadCount, inboxUrl: d.inboxUrl };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Refresh the cache if it is stale. Fire-and-forget: callers must not await this for its value.
|
|
99
|
+
*
|
|
100
|
+
* A cache about a different environment is refreshed immediately however recent it is —
|
|
101
|
+
* switching environments must not leave the nudge mute for the rest of its interval.
|
|
102
|
+
*
|
|
103
|
+
* On failure the result is still written, with `unread: null`, so an offline machine backs off
|
|
104
|
+
* for the interval instead of asking on every command. That is why the failure marker is null
|
|
105
|
+
* rather than 0: zero would be a claim ("you have nothing unread"), null is an admission.
|
|
106
|
+
*/
|
|
107
|
+
export async function refreshUnreadMessagesInBackground(environment, apiUrl, accessToken, now = Date.now(), baseDir, fetchImpl = globalThis.fetch) {
|
|
108
|
+
const cache = readUnreadMessagesCache(baseDir);
|
|
109
|
+
if (cache !== null && cache.environment === environment && !isMessagesCacheStale(cache, now))
|
|
110
|
+
return;
|
|
111
|
+
const result = await fetchUnread(apiUrl, accessToken, fetchImpl);
|
|
112
|
+
writeUnreadMessagesCache({
|
|
113
|
+
environment,
|
|
114
|
+
unread: result === null ? null : result.unreadCount,
|
|
115
|
+
inboxUrl: result === null ? null : result.inboxUrl,
|
|
116
|
+
checkedAt: now,
|
|
117
|
+
}, baseDir);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/messages/check.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { EnvironmentName } from '../config/environments.js';
|
|
2
|
+
/**
|
|
3
|
+
* The unread-messages nudge.
|
|
4
|
+
*
|
|
5
|
+
* Bitmagic broadcasts reach web creators through the portal's notification bell. A CLI-lane
|
|
6
|
+
* creator may never open the portal at all, so a command prints a one-line pointer instead —
|
|
7
|
+
* the same shape as the update nudge next door, and for the same reason: a hint worth giving,
|
|
8
|
+
* never worth failing or slowing a command over.
|
|
9
|
+
*
|
|
10
|
+
* The CLI never CONSUMES a message. Reading is an act, and the read receipt is what tells an
|
|
11
|
+
* admin whether a broadcast actually landed — a terminal glancing at a count is not that.
|
|
12
|
+
*/
|
|
13
|
+
/** What is remembered between runs, so no command pays for the network. */
|
|
14
|
+
export interface UnreadMessagesCache {
|
|
15
|
+
/**
|
|
16
|
+
* Which environment the count is about.
|
|
17
|
+
*
|
|
18
|
+
* Credentials are per environment and a creator can hold a prod and a dev login at once, so
|
|
19
|
+
* an answer about one says nothing about the other. Mirrors `UpdateCheckCache.tag`, including
|
|
20
|
+
* the consequence: a cache for the other environment reads as absent and is refreshed.
|
|
21
|
+
*/
|
|
22
|
+
environment: EnvironmentName;
|
|
23
|
+
/** The count, or null when the last check could not reach the server (a back-off marker). */
|
|
24
|
+
unread: number | null;
|
|
25
|
+
/** Where to read them — the server hands this over; see check.ts. */
|
|
26
|
+
inboxUrl: string | null;
|
|
27
|
+
/** Epoch ms of that check. */
|
|
28
|
+
checkedAt: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* How stale a count may be before it is refreshed.
|
|
32
|
+
*
|
|
33
|
+
* An hour, matching the release line's update check. Broadcasts are rare and never urgent to
|
|
34
|
+
* the minute, and the cost of being wrong is one extra line of stderr.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MESSAGES_CHECK_INTERVAL_MS: number;
|
|
37
|
+
/**
|
|
38
|
+
* How old a cached count may be and still be PRINTED.
|
|
39
|
+
*
|
|
40
|
+
* Separate from the refresh interval, and the thing that stops a laptop that has been offline
|
|
41
|
+
* for a fortnight nagging every single command about a message read in a browser on day one.
|
|
42
|
+
* Past this, the nudge goes quiet until a check succeeds again.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MESSAGES_NOTICE_MAX_AGE_MS: number;
|
|
45
|
+
/** Whether the cached answer should be refreshed in the background. */
|
|
46
|
+
export declare function isMessagesCacheStale(cache: UnreadMessagesCache | null, now: number): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The line to print, or null to stay quiet.
|
|
49
|
+
*
|
|
50
|
+
* Quiet for: no cache at all (a first-ever run pays one silent command, exactly as the update
|
|
51
|
+
* notice does), a failed check (`unread: null` — an unreachable server is not evidence of
|
|
52
|
+
* anything), nothing unread, no inbox URL to point at, and an answer past its max age.
|
|
53
|
+
*
|
|
54
|
+
* The wording is deliberately soft. The count can be up to an interval stale and the CLI cannot
|
|
55
|
+
* know whether the message was read in a browser five minutes ago, so it reports rather than
|
|
56
|
+
* instructs.
|
|
57
|
+
*/
|
|
58
|
+
export declare function unreadMessagesNotice(cache: UnreadMessagesCache | null, now: number): string | null;
|