@bitmagic/cli 0.1.71-dev.0 → 0.1.71-dev.1

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 (36) hide show
  1. package/assets/roblox-port/README.md +19 -0
  2. package/assets/roblox-port/SKILL.md +154 -0
  3. package/assets/roblox-port/build-studio-update.mjs +41 -0
  4. package/assets/roblox-port/export-packed-mesh.mjs +166 -0
  5. package/assets/roblox-port/lib.mjs +193 -0
  6. package/assets/roblox-port/port-state.mjs +129 -0
  7. package/assets/roblox-port/references/assets.md +100 -0
  8. package/assets/roblox-port/references/source-and-runtime.md +68 -0
  9. package/assets/roblox-port/references/state-contracts.md +129 -0
  10. package/assets/roblox-port/references/studio-and-updates.md +119 -0
  11. package/assets/roblox-port/references/verification.md +63 -0
  12. package/assets/roblox-port/studio-runtime.luau +347 -0
  13. package/assets/roblox-port/tests/mesh.test.mjs +81 -0
  14. package/assets/roblox-port/tests/state.test.mjs +148 -0
  15. package/assets/roblox-port/tests/studio-mock.luau +109 -0
  16. package/assets/roblox-port/tests/studio-runtime.test.mjs +309 -0
  17. package/dist/commands/generate.js +12 -0
  18. package/dist/commands/generate.js.map +1 -1
  19. package/dist/generate/model.d.ts +2 -0
  20. package/dist/generate/model.js +1 -0
  21. package/dist/generate/model.js.map +1 -1
  22. package/dist/generate/prop.d.ts +7 -0
  23. package/dist/generate/prop.js +2 -0
  24. package/dist/generate/prop.js.map +1 -1
  25. package/dist/scaffold/project-files.js +1 -1
  26. package/dist/scaffold/project-files.js.map +1 -1
  27. package/dist/scaffold/project.js +2 -0
  28. package/dist/scaffold/project.js.map +1 -1
  29. package/dist/scaffold/roblox-skill-files.d.ts +2 -0
  30. package/dist/scaffold/roblox-skill-files.js +16 -0
  31. package/dist/scaffold/roblox-skill-files.js.map +1 -0
  32. package/dist/scaffold/skill-files.js +2 -1
  33. package/dist/scaffold/skill-files.js.map +1 -1
  34. package/dist/tools/bundles.js +6 -0
  35. package/dist/tools/bundles.js.map +1 -1
  36. package/package.json +4 -4
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { parseArgs } from 'node:util';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { VERSION, assert, hash, equal, readJson, writeJson, loadProject, readBase, materialize, assertTarget, reconcile, verifyReadback, validatePlan, withLock } from './lib.mjs';
8
+
9
+ export function init(sourceRoot, portRoot, environment) {
10
+ const source = fs.realpathSync(sourceRoot);
11
+ const parent = fs.realpathSync(path.dirname(path.resolve(portRoot)));
12
+ const root = path.join(parent, path.basename(portRoot));
13
+ assert(root !== source && !root.startsWith(`${source}${path.sep}`), 'Port workspace must be outside the source project');
14
+ assert(!fs.existsSync(root), 'Output already exists; do not overwrite an existing project');
15
+ const metadata = readJson(path.join(source, 'bitmagic.json'));
16
+ assert(typeof metadata.gameId === 'string' && typeof metadata.engineVersion === 'string', 'Missing Bitmagic source identity');
17
+ const env = environment ?? metadata.environment;
18
+ assert(typeof env === 'string' && env.length > 0, 'Explicit source environment required');
19
+ assert(!metadata.environment || env === metadata.environment, 'Source environment mismatch');
20
+ const project = { schemaVersion: VERSION, projectId: randomUUID(), targetId: randomUUID(), source: { root: source, gameId: metadata.gameId, engineVersion: metadata.engineVersion, environment: env }, target: { placeId: null, universeId: null }, runtimeProfile: 'private-single-player' };
21
+ fs.mkdirSync(root);
22
+ for (const dir of ['generated/shared', 'generated/client', 'generated/server', 'overrides', 'tests', 'state', '.bitmagic/roblox/runs']) fs.mkdirSync(path.join(root, dir), { recursive: true });
23
+ writeJson(path.join(root, 'project.json'), project, true);
24
+ writeJson(path.join(root, 'incoming.json'), { schemaVersion: VERSION, sourceFiles: ['bitmagic.json'], scripts: [] }, true);
25
+ writeJson(path.join(root, 'port-contract.json'), { schemaVersion: VERSION, features: [], deviations: [] }, true);
26
+ writeJson(path.join(root, 'assets.lock.json'), { schemaVersion: VERSION, assets: [] }, true);
27
+ fs.writeFileSync(path.join(root, '.gitignore'), '.bitmagic/\nstate/lock\n', { flag: 'wx' });
28
+ return { schemaVersion: VERSION, root, projectId: project.projectId, targetId: project.targetId };
29
+ }
30
+ export function prepare(root, live, run, { bind = false, adopt = false } = {}) {
31
+ return withLock(root, () => {
32
+ const project = loadProject(root);
33
+ assertTarget(project, live, bind);
34
+ assert(live.pendingRun === null, 'PARTIAL_APPLY: resume or roll back the existing run first');
35
+ const incoming = materialize(root, project), base = readBase(root);
36
+ assert(!base || live.buildId === base.planHash, 'BUILD_MISMATCH: recover the accepted baseline before planning');
37
+ const result = reconcile(project, incoming.scripts, base, live, adopt);
38
+ const body = { schemaVersion: VERSION, runId: randomUUID(), project, baseHash: hash(base), baselineBefore: base, sourceHash: incoming.sourceHash, sourceFiles: incoming.sourceFiles, packageHash: incoming.packageHash, targetBefore: live.target, buildBefore: live.buildId, snapshot: live, generated: incoming.scripts, ...result };
39
+ const plan = { ...body, planHash: hash(body) };
40
+ assert(!fs.existsSync(run), 'Run directory already exists; use its plan or choose a new run');
41
+ fs.mkdirSync(run, { recursive: true, mode: 0o700 });
42
+ writeJson(path.join(run, 'plan.json'), plan, true);
43
+ writeJson(path.join(run, 'before.json'), live, true);
44
+ writeJson(path.join(run, 'journal.json'), { schemaVersion: VERSION, stage: result.conflicts.length ? 'conflict' : 'prepared', planHash: plan.planHash }, true);
45
+ return plan;
46
+ });
47
+ }
48
+ export function checkFresh(root, plan) {
49
+ validatePlan(plan);
50
+ const project = loadProject(root), incoming = materialize(root, project);
51
+ assert(equal(project, plan.project), 'Project binding changed; prepare again');
52
+ assert(incoming.sourceHash === plan.sourceHash && incoming.packageHash === plan.packageHash, 'SOURCE_CHANGED: prepare a new plan');
53
+ const base = readBase(root);
54
+ assert(hash(base) === plan.baseHash || base?.planHash === plan.planHash, 'Baseline advanced since planning');
55
+ }
56
+ export function accept(root, run, live) {
57
+ return withLock(root, () => {
58
+ const plan = readJson(path.join(run, 'plan.json'));
59
+ checkFresh(root, plan); verifyReadback(plan, live);
60
+ writeJson(path.join(run, 'after.json'), live);
61
+ const base = { schemaVersion: VERSION, planHash: plan.planHash, sourceHash: plan.sourceHash, packageHash: plan.packageHash, generated: plan.generated, applied: plan.desired };
62
+ // The accepted GENERATED values remain the three-way base. Kept live overrides are recorded
63
+ // separately; making them the generated base would erase them on the next unchanged export.
64
+ writeJson(path.join(root, 'state/base.json'), base);
65
+ writeJson(path.join(run, 'journal.json'), { schemaVersion: VERSION, stage: 'applied', planHash: plan.planHash });
66
+ return { schemaVersion: VERSION, stage: 'applied', buildId: plan.planHash, verified: false, saveStatus: 'pending' };
67
+ });
68
+ }
69
+ export function recordSave(root, receipt) {
70
+ return withLock(root, () => {
71
+ const base = readBase(root); assert(base, 'No accepted Studio build');
72
+ assert(receipt.buildId === base.planHash, 'Save receipt is for a different build');
73
+ assert(['pending', 'saved', 'unsupported', 'failed', 'unconfirmed'].includes(receipt.status), 'Invalid save status');
74
+ if (receipt.status === 'saved') {
75
+ assert(typeof receipt.destination === 'string' && receipt.destination.length > 0, 'Saved destination required');
76
+ assert(typeof receipt.evidence === 'string' && receipt.evidence.length > 0, 'Save evidence required');
77
+ assert(receipt.published === false, 'Receipt must identify a non-publishing save');
78
+ }
79
+ writeJson(path.join(root, 'state/save.json'), { schemaVersion: VERSION, ...receipt });
80
+ return receipt;
81
+ });
82
+ }
83
+ export function acceptRollback(root, run, receipt) {
84
+ return withLock(root, () => {
85
+ const plan = validatePlan(readJson(path.join(run, 'plan.json')));
86
+ assert(equal(loadProject(root), plan.project), 'Project binding changed');
87
+ assert(receipt.stage === 'rolled-back' && receipt.planHash === plan.planHash && receipt.failures.length === 0, 'Rollback incomplete or for another plan');
88
+ const restored = receipt.snapshot;
89
+ // Sorting affects order only, not what was preserved. Compare exact editor source and metadata.
90
+ const sorted = snapshot => ({ ...snapshot, scripts: [...snapshot.scripts].sort((a, b) => JSON.stringify(a.path).localeCompare(JSON.stringify(b.path))) });
91
+ assert(equal(sorted(restored), sorted(plan.snapshot)), 'Rollback readback differs; reconcile before changing the local baseline');
92
+ const base = readBase(root);
93
+ assert(hash(base) === plan.baseHash || base?.planHash === plan.planHash, 'A later baseline must not be rolled back');
94
+ if (plan.baselineBefore === null) {
95
+ const file = path.join(root, 'state/base.json');
96
+ if (fs.existsSync(file)) fs.unlinkSync(file);
97
+ } else writeJson(path.join(root, 'state/base.json'), plan.baselineBefore);
98
+ writeJson(path.join(run, 'rollback.json'), receipt);
99
+ writeJson(path.join(run, 'journal.json'), { schemaVersion: VERSION, stage: 'rolled-back', planHash: plan.planHash });
100
+ return { schemaVersion: VERSION, stage: 'rolled-back', buildId: plan.buildBefore };
101
+ });
102
+ }
103
+ export function status(root) {
104
+ const project = loadProject(root), base = readBase(root);
105
+ const saveFile = path.join(root, 'state/save.json'), save = fs.existsSync(saveFile) ? readJson(saveFile) : null;
106
+ const current = base ? materialize(root, project) : null;
107
+ const stale = base ? current.sourceHash !== base.sourceHash || current.packageHash !== base.packageHash : false;
108
+ return { schemaVersion: VERSION, projectId: project.projectId, targetId: project.targetId, stage: base ? 'applied' : 'prepared', buildId: base?.planHash ?? null, localChanges: stale, saveStatus: save && save.buildId === base?.planHash && !stale ? save.status : 'pending', studioReadbackRequired: true };
109
+ }
110
+ function main() {
111
+ const { values: a, positionals } = parseArgs({ allowPositionals: true, options: { source: { type: 'string' }, project: { type: 'string' }, environment: { type: 'string' }, live: { type: 'string' }, run: { type: 'string' }, receipt: { type: 'string' }, 'bind-target': { type: 'boolean' }, adopt: { type: 'boolean' }, help: { type: 'boolean' } } });
112
+ if (a.help) { console.log('port-state.mjs init --source <source> --project <new-port> [--environment dev]\nprepare --project <port> --live <snapshot.json> --run <new-run-dir> [--bind-target] [--adopt]\naccept --project <port> --run <run-dir> --live <readback.json>\naccept-rollback --project <port> --run <run-dir> --receipt <rollback.json>\nstatus --project <port>\nrecord-save --project <port> --receipt <save.json>'); return; }
113
+ assert(a.project, '--project required');
114
+ let result;
115
+ switch (positionals[0]) {
116
+ case 'init': assert(a.source, '--source required'); result = init(a.source, a.project, a.environment); break;
117
+ case 'prepare': assert(a.live && a.run, '--live and --run required'); result = prepare(a.project, readJson(a.live), a.run, { bind: a['bind-target'], adopt: a.adopt }); break;
118
+ case 'accept': assert(a.live && a.run, '--live and --run required'); result = accept(a.project, a.run, readJson(a.live)); break;
119
+ case 'accept-rollback': assert(a.receipt && a.run, '--receipt and --run required'); result = acceptRollback(a.project, a.run, readJson(a.receipt)); break;
120
+ case 'status': result = status(a.project); break;
121
+ case 'record-save': assert(a.receipt, '--receipt required'); result = recordSave(a.project, readJson(a.receipt)); break;
122
+ default: throw Error('Unknown command; use --help');
123
+ }
124
+ console.log(JSON.stringify(result));
125
+ if (result.conflicts?.length) process.exitCode = 2;
126
+ }
127
+ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) {
128
+ try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; }
129
+ }
@@ -0,0 +1,100 @@
1
+ # Original assets and packed geometry
2
+
3
+ For each reachable asset try: permission-valid cached original → supported direct import →
4
+ deterministic conversion → faithful native approximation → licensed/generated replacement.
5
+ Record a valid attempted route or concrete preflight blocker, not blind repeated retries. Missing
6
+ gameplay-critical content blocks completion unless the creator explicitly changes the contract.
7
+
8
+ Use the source bytes, not screenshots, whenever possible. Packed mesh JSON, articulated components,
9
+ procedural buildings and audio-bank URLs count as original content. Retain source bytes, decoder
10
+ version, conversion recipe and hash, target owner/type/ID/version, upload operation, and evidence.
11
+ Reuse by bytes + recipe + importer version + owner/permission context, not filename.
12
+
13
+ Studio's importer and Open Cloud asset endpoints have distinct format/settings capabilities.
14
+ Discover the current route. GLB/glTF Model creation is documented; a raw Mesh upload is not a
15
+ general geometry uploader, and content updates are more restricted than asset creation. Tool
16
+ `insert_asset` inserts an existing asset; `store_image` produces a tool reference, not a permanent
17
+ game image. Neither transfers all local models/audio automatically.
18
+
19
+ ## Offline packed-mesh helper
20
+
21
+ `export-packed-mesh.mjs` supports these explicit, little-endian layouts only:
22
+
23
+ | Layout | Buffers | Other semantics |
24
+ | --- | --- | --- |
25
+ | `bitmagic-packed-v1`, kit `version: 1` | Unindexed float32 XYZ positions/normals/RGB colors | Optional pivot, height and muzzleHeight. |
26
+ | `bitmagic-packed-v2`, kit `version: 2` | int16 positions × quantum; normalized int8 normals; uint8 colors; uint16 indices | Ordered `[start,count,materialIndex]` groups and articulated pivots. |
27
+
28
+ Unknown fields/layouts fail. Read the actual source decoder before selecting one. The helper does
29
+ not extract arbitrary procedural code, skin/retarget skeletal animation, bake textures, import
30
+ into Studio, upload assets, or prove visual parity. Those remain explicit conversion tasks.
31
+
32
+ Save a recipe in the port workspace. This calibration example preserves source axes and scale;
33
+ choose measured values for the actual game:
34
+
35
+ ```json
36
+ {
37
+ "schemaVersion": 1,
38
+ "format": "bitmagic-packed-v1",
39
+ "meshIds": ["townhall"],
40
+ "basis": [1,0,0, 0,1,0, 0,0,1],
41
+ "scale": 1,
42
+ "materials": [{
43
+ "name": "source-painted",
44
+ "doubleSided": false,
45
+ "pbrMetallicRoughness": {
46
+ "baseColorFactor": [1,1,1,1],
47
+ "metallicFactor": 0,
48
+ "roughnessFactor": 0.8
49
+ }
50
+ }]
51
+ }
52
+ ```
53
+
54
+ ```text
55
+ node "<bundle>/export-packed-mesh.mjs" --input <original-kit.json> --recipe <recipe.json> --out <new-model.glb>
56
+ ```
57
+
58
+ It emits GLB plus `<new-model.glb>.json` with hashes, mesh IDs, triangles, vertices and original
59
+ pivot metadata. It preserves pivot translations, vertex colors and material groups, transforms
60
+ normals, and reverses winding for a reflected basis. Select only reachable mesh IDs; articulated
61
+ components must share the intended hierarchy after native import. Preserve muzzle/footprint
62
+ metadata in native code even if the importer ignores GLB extras.
63
+
64
+ Verify silhouette, dimensions, pivot articulation, colors/PBR, grounding, sockets, collision roles
65
+ and animation timing after import. Vertex colors surviving a GLB export do not prove the chosen
66
+ Roblox import preserves their appearance. If necessary bake an atlas and record that conversion.
67
+ Gray replacement materials are not an acceptable silent success. Use inexpensive dedicated
68
+ collision geometry, and disable collision/touch/query for decorative content where appropriate.
69
+
70
+ Zero-area triangles are rejected by default. If inspection confirms that these are degenerate
71
+ source faces, set `"degenerateTriangles": "drop"` in the recipe. The receipt records original,
72
+ retained and dropped triangle counts; the converter removes unused vertices too. It never
73
+ invents normals for retained surfaces. Include the cleanup in the accepted conversion evidence.
74
+
75
+ ## Uploads, permissions and native reconstruction
76
+
77
+ Use only the selected creator/group owner and declared dependencies. Asset upload, moderation,
78
+ experience permissions, and working playback/rendering are separate states. Private ownership
79
+ does not prove a target experience can play audio. If asset permissions need an existing place,
80
+ ask the creator to establish that in Studio; do not create/publish an experience yourself.
81
+
82
+ Track operations before polling; after ambiguous timeouts query their actual outcome before
83
+ creating duplicates. Bound retries/concurrency and follow throttling. Preserve previous asset
84
+ mappings when changed content requires a new ID. Avoid auto-updating live package dependencies.
85
+ Never upload secrets, unrelated local files, recovery archives, or signed URL credentials.
86
+
87
+ Keep original voices, cue timing, subtitles, cooldowns, overlap caps and ducking. Generic growls
88
+ are a replacement, not proof the original voice pack transferred. Source cosmetics/content changes
89
+ are intentional overrides only when requested; do not infer them from platform stereotypes.
90
+
91
+ Store native construction recipes and per-object identities/property snapshots in the port
92
+ workspace. Recipes must support scoped updates; do not run a full rebuild that erases unrelated
93
+ models. Inspect imported marketplace models for embedded scripts before executing them.
94
+
95
+ When an image-upload tool needs temporary HTTP hosting, expose only the intended export directory,
96
+ bind narrowly, record the owned PID/port, and stop it and its children after the transfer.
97
+
98
+ - [Studio importer](https://create.roblox.com/docs/studio/importer).
99
+ - [Assets API guide](https://create.roblox.com/docs/cloud/guides/usage-assets).
100
+ - [Audio assets](https://create.roblox.com/docs/audio/assets).
@@ -0,0 +1,68 @@
1
+ # Source contracts and native runtime
2
+
3
+ Inventory reachable code and data from `Game.ts`, `game.json`, `world.json`, imported JSON,
4
+ asset registries, audio banks, procedural geometry and engine calls. Classify unused experiments
5
+ separately; a cinematic mesh is not automatically the ordinary crowd model. Do not execute
6
+ arbitrary source to discover assets. If runtime extraction is necessary, use an explicitly scoped
7
+ capture with controlled networking/side effects and retain the extraction recipe.
8
+
9
+ Each feature contract records:
10
+
11
+ - Stable ID; source files and enclosing symbols; source input hash.
12
+ - Enter/exit state transitions, input gates, pause/cleanup, camera and audio owners.
13
+ - Numeric rules: costs, unlocks, cooldowns, targeting, damage shapes, waves, rewards, loss conditions.
14
+ - Presentation: model variants, animation events, UI behavior, voice/subtitle cues, VFX.
15
+ - Native strategy and dependencies; fidelity category and intentional deviations.
16
+ - A test with explicit inputs and expected outputs/events; independent verification evidence.
17
+
18
+ Preserve all chapters and tutorial stages, not just an initial playable slice. A result flow that
19
+ returns to the map, selects the next chapter, and waits for the player differs from automatically
20
+ starting the next chapter. Session carry-over differs from account progress after rejoining.
21
+
22
+ ## Translation
23
+
24
+ Extract tables mechanically. Native Luau is a semantic rewrite for browser/Bitmagic APIs, not
25
+ syntax substitution. Explicitly account for zero/one-based indexes, JavaScript truthiness,
26
+ `undefined`/`null`, Maps/Sets, class initialization, promises, timers and event lifetimes.
27
+ `roblox-ts` cannot provide Three.js, WebGL, DOM/CSS, browser audio or Bitmagic runtime bindings.
28
+ Unknown engine calls become conversion work, not empty stubs or swallowed exceptions.
29
+
30
+ Use modules with distinct rules, presentation and platform integrations. Stable local events
31
+ such as phase changes, volleys, splash/piercing hits, resource changes and tutorials can connect
32
+ them without becoming network remotes. Preserve event order and single consumption. Keep deliberate
33
+ Roblox extensions in `overrides/` or explicit hooks, with source mappings to the original contract.
34
+
35
+ ## Authority
36
+
37
+ For a private single-player simulation, local input, placement preview, camera, rules and visuals
38
+ can remain local. Shared world state and consequential multiplayer outcomes need server authority.
39
+ Purchases, ownership and trusted currencies/rewards always need their appropriate server checks.
40
+ Do not grant trusted rewards from client-reported victories. Choose and document a persistence
41
+ trust policy; serializing a client payload does not establish integrity.
42
+
43
+ Existing target commerce stays intact. Do not add commerce, ads, ownership claims or receipts just
44
+ because a source game is being ported. Preserve coordinated receipt handling and deduplication.
45
+
46
+ ## Coordinates, UI and performance
47
+
48
+ Use an asymmetric calibration object, floor, forward arrow and known footprint to establish a
49
+ source-to-target basis and scale. Transform normals, winding, negative scales, pivots, animation,
50
+ projectile sockets, ranges, movement and camera distances consistently. Test gameplay facing
51
+ separately from camera facing. One source unit is not automatically one stud.
52
+
53
+ Map UI intent into native GUI controls, including portraits/icons, safe areas, scrolling,
54
+ controller focus where needed, readable text and touch confirmation. Check the world space left
55
+ visible for placement. Combat should hide construction UI when the contract requires it.
56
+ Use one camera owner per phase and hand over from the rendered pose without snapping/resetting.
57
+
58
+ Pool transient visuals/audio, bound overlap and allocations, and separate simulation scheduling
59
+ from rendering. Avoid per-enemy scripts, physics bodies and pathfinding for crowds unless required.
60
+ Keep render-step work narrow. Measure before reducing visible density; sampling a small stable
61
+ subset is a disclosed fidelity compromise, not a proven platform limit. Record dropped simulation
62
+ time separately from rendering FPS. Never ship the case study's numeric budgets as universal rules.
63
+
64
+ Official references, consulted as needed:
65
+
66
+ - [Client/server runtime](https://create.roblox.com/docs/projects/client-server).
67
+ - [Task scheduler](https://create.roblox.com/docs/performance-optimization/microprofiler/task-scheduler).
68
+ - [Bitmagic coordinate conventions](../../../../engine/agent-docs/coordinate-system.md) when available in the source project; otherwise read its installed engine documentation directly.
@@ -0,0 +1,129 @@
1
+ # Port workspace and helper contracts
2
+
3
+ The helper bundle is found with `bitmagic tools path roblox-port --json`. Use its returned `path`
4
+ as `<bundle>` below. The normal route executes it in place; `tools install roblox-port` is optional
5
+ and copies helpers into `tools/roblox-port`, never into generated game modules. All helper commands
6
+ are local-only. Inputs/outputs are explicit files; helpers never choose a Studio session.
7
+
8
+ ## Initialize
9
+
10
+ ```text
11
+ node "<bundle>/port-state.mjs" init --source <absolute-bitmagic-project> --project <new-sibling-port>
12
+ ```
13
+
14
+ The source must have a game ID and engine version in `bitmagic.json`. If it lacks an environment,
15
+ provide `--environment` with the confirmed source environment. The output must not exist or nest
16
+ inside the source. Initialization creates stable project/target UUIDs, empty inventories and
17
+ generated/override/test/state directories. Fill the inventories before using them; initialization
18
+ is not a generated or verified game. Existing ports need explicit mapping/adoption, not `init`
19
+ over their files. An empty sibling workspace can hold an adopted port's copied local sources.
20
+
21
+ Commit `project.json`, `incoming.json`, `port-contract.json`, `assets.lock.json`, generated source,
22
+ overrides, tests and `state/base.json` according to the project's VCS policy. Keep journals and
23
+ recovery snapshots under `.bitmagic/roblox/runs/`; they are ignored for sharing, but retain them
24
+ until recovery is no longer needed. Only conversion cache is disposable. Never store credentials
25
+ in these files, asset recipes, Lua source, tool outputs, or archives.
26
+
27
+ `project.json` contains schema version 1, immutable managed project/target IDs, source identity,
28
+ runtime profile, and nullable target `placeId`/`universeId` strings. Fill known target IDs before
29
+ applying. A null binding may be used for an explicitly selected unsaved template. Do not store a
30
+ transient Studio session ID as the durable identity. Reconcile later owner-created cloud IDs while
31
+ retaining the managed UUIDs.
32
+
33
+ ## Generated sources
34
+
35
+ Prepare `incoming.json`:
36
+
37
+ ```json
38
+ {
39
+ "schemaVersion": 1,
40
+ "sourceFiles": ["bitmagic.json", "src/work/Game.ts", "src/work/game.json", "src/work/world.json"],
41
+ "scripts": [
42
+ {
43
+ "id": "rules",
44
+ "path": ["ReplicatedStorage", "BitmagicGame", "Rules"],
45
+ "className": "ModuleScript",
46
+ "file": "generated/shared/Rules.luau"
47
+ },
48
+ {
49
+ "id": "client-entry",
50
+ "path": ["StarterPlayer", "StarterPlayerScripts", "BitmagicGame", "Main"],
51
+ "className": "LocalScript",
52
+ "file": "generated/client/Main.client.luau",
53
+ "enabled": true
54
+ }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ Declare **every source file used by conversion**, including imported rules, packed art and audio
60
+ registries, plus relevant engine dependencies. `sourceFiles` is an explicit inventory, not an
61
+ automatic dependency scanner; undeclared changes cannot be detected by the helper. Paths are
62
+ relative to the source root, generated files relative to the port root; escaping symlinks and
63
+ traversal are rejected. Allowed root services are ReplicatedStorage, ServerScriptService,
64
+ ServerStorage, StarterPlayer, StarterGui and Workspace. Use simple generated path segments;
65
+ live creator names, including spaces and Unicode, are preserved by stable ID. Existing
66
+ Folder/Model/StarterPlayerScripts/StarterCharacterScripts containers can hold scripts; missing
67
+ intermediate containers become Folders. Explicitly create special native containers beforehand.
68
+
69
+ Every Script/LocalScript needs an explicit `enabled`; ModuleScripts omit it. Stable IDs do not
70
+ change when names/paths change. The source helper supports up to 256 scripts, each at most 1 MiB.
71
+ It does not remove omitted scripts, change classes, or reconcile native models/properties.
72
+ Retirement and class migration require a separate backed-up, explicit operation and baseline
73
+ migration. Resolve an unsupported operation instead of hand-editing a prepared plan.
74
+
75
+ ## Feature and asset ledgers
76
+
77
+ `port-contract.json` has schema version 1 with `features` and `deviations` arrays. Each feature
78
+ needs stable `id`, source files/symbols, `required`, target strategy, fidelity, dependencies,
79
+ test fixture and verification evidence. Accepted differences record the creator's decision and
80
+ affected feature IDs. This is the agent's semantic contract; the source helper does not certify it.
81
+
82
+ `assets.lock.json` has schema version 1 and `assets`. For each asset record `id`, source path/hash,
83
+ provenance, recipe/version/hash, target owner/type/ID/version, operation ID, moderation, permissions,
84
+ and validation evidence. Store service IDs as strings. Preserve earlier mappings for recovery.
85
+
86
+ Track native objects/settings outside the script helper in a reviewable scene inventory and
87
+ recipes with expected-value updates and durable before-snapshots. A full game includes this state.
88
+
89
+ ## Plan, apply, accept
90
+
91
+ See [studio-and-updates.md](studio-and-updates.md) for the exact commands. A run directory contains
92
+ immutable `plan.json` and `before.json` written before Studio mutation, followed by journal and
93
+ readback. The plan binds source/package hashes, target identity, expected live values and baseline.
94
+ Do not edit its hash or values to force a mismatch through. Prepare again after resolving the cause.
95
+
96
+ `accept` means exact managed source readback passed, **not** gameplay, visuals or performance passed.
97
+ It stores generated values and applied values separately in `state/base.json`; keep both. An
98
+ interrupted forward apply can be replayed from the same plan after inspecting target state, or
99
+ conditionally rolled back. New plans are refused while Studio reports an incomplete run.
100
+
101
+ Local helper operations use `state/lock`. A stale lock after a killed local process needs inspection:
102
+ verify the owning PID no longer exists and no other task uses the port before removing it. Never
103
+ remove a lock merely because a command is slow. The lock does not prevent human/Team Create edits;
104
+ Studio expected-value checks still apply.
105
+
106
+ ## Save evidence and completion
107
+
108
+ Record actual tool/manual evidence in a save receipt, never infer it from local source output:
109
+
110
+ ```json
111
+ {
112
+ "buildId": "the-accepted-plan-hash",
113
+ "status": "saved",
114
+ "destination": "confirmed place file or non-published draft destination",
115
+ "evidence": "actual save operation receipt or verified file evidence",
116
+ "published": false
117
+ }
118
+ ```
119
+
120
+ ```text
121
+ node "<bundle>/port-state.mjs" record-save --project <port> --receipt <save.json>
122
+ node "<bundle>/port-state.mjs" status --project <port>
123
+ ```
124
+
125
+ Statuses are `pending`, `saved`, `unsupported`, `failed`, and `unconfirmed`. The helper validates
126
+ receipt shape/build binding; the agent must obtain genuine evidence. Local `status` reports source
127
+ freshness and recorded save state; it cannot know whether Studio changed since the last readback.
128
+ Reinspect before claiming the current Studio project is unchanged or saved. A new local package
129
+ or accepted build invalidates applicability of older save evidence.
@@ -0,0 +1,119 @@
1
+ # Studio connection, source updates, recovery and saving
2
+
3
+ Discover the connected tools rather than pinning an observed ABI. Typical operations are
4
+ `list_roblox_studios`, `get_studio_state`, hierarchy/source inspection, `multi_edit`, `execute_luau`,
5
+ Play controls, console/input, and captures. Every call targets the selected `studio_id`.
6
+ Studio MCP and Open Cloud authorization are independent. No helper starts its own MCP client.
7
+
8
+ Use the connected server's schemas and editor-aware reads. Do not assume Script.Source reflects
9
+ an unsaved open buffer. The generated updater requires `ScriptEditorService:GetEditorSource` and
10
+ `UpdateSourceAsync`; unavailable permissions stop it, not trigger a stale-Source fallback.
11
+ Do not paste an entire game into many improvised edits when a reviewed local package is available.
12
+
13
+ ## Inspect and plan
14
+
15
+ After preparing the local source package:
16
+
17
+ ```text
18
+ node "<bundle>/build-studio-update.mjs" inspect --project <port> --out <new-inspect.luau>
19
+ ```
20
+
21
+ Read that generated code, then execute it through the connected MCP executor in the explicitly
22
+ selected **Edit** context. Save the returned JSON string's contents as `<live.json>`; unwrap the
23
+ tool envelope without editing values. Inspection reads the candidate paths plus managed scripts,
24
+ identity, build and pending-run markers. Duplicate names/IDs and occupied non-script paths fail.
25
+
26
+ ```text
27
+ node "<bundle>/port-state.mjs" prepare --project <port> --live <live.json> --run <new-run-dir>
28
+ ```
29
+
30
+ For the first explicitly selected unbound target add `--bind-target`. This installs the port's
31
+ managed UUIDs on apply, not a cloud identity. It never clears the template. For existing unowned
32
+ scripts, `--adopt` is allowed only after an explicit ownership decision and exact source/class/path/
33
+ enabled matches. First copy their actual editor content into the local package; do not use adoption
34
+ to overwrite a different game. Keep source contract/deviations separate from this ownership baseline.
35
+
36
+ Review `plan.json`: target/owner, source/package hashes, conflicts, desired values, changed scripts,
37
+ and preserved customizations. No uploads occur. A conflict result exits 2 and produces no applicable
38
+ update. Existing baseline omissions, missing managed objects, foreign path occupants, and divergent
39
+ edits are actionable conflicts. Locate a renamed object by stable ID rather than recreating it.
40
+
41
+ The plan stores complete affected script text locally. Protect it appropriately. Before altering
42
+ native models, terrain, assets or global settings, prepare separate complete backups and scoped
43
+ expected-value operations; the source helper deliberately does not pretend to cover that content.
44
+
45
+ ## Apply and read back
46
+
47
+ ```text
48
+ node "<bundle>/build-studio-update.mjs" apply --project <port> --run <run-dir> --out <new-apply.luau>
49
+ ```
50
+
51
+ Review and execute that generated Luau in the same selected Edit target. It rechecks target,
52
+ source and hierarchy expectations, stages Script/LocalScript activation, checks editor buffers
53
+ inside update callbacks, and commits the build marker only after all managed sources match.
54
+ Save the returned snapshot as `<after.json>`:
55
+
56
+ ```text
57
+ node "<bundle>/port-state.mjs" accept --project <port> --run <run-dir> --live <after.json>
58
+ ```
59
+
60
+ This advances only the source baseline. It does not run gameplay tests or save the place. Native
61
+ assets/settings must already satisfy the separate scene/asset plan before calling the whole port
62
+ ready. Whole-place atomicity is not provided by Studio MCP or a per-script update callback.
63
+
64
+ ## Interrupted operations
65
+
66
+ Keep the same run after a timeout. Generate a fresh inspection with `inspect --project <port>
67
+ --run <run-dir> --out <new-inspect.luau>` so recovery reads the durable plan even when local
68
+ conversion inputs are missing or invalid. Inspect its pending marker and
69
+ actual source state, and either replay the existing apply or compensate. A replay accepts only
70
+ expected pre-state or this run's identifiable intermediate writes. An unrelated edit stops it.
71
+ Do not prepare a new baseline from a half-applied game.
72
+
73
+ Rollback records `rollback:<run-id>` in `BitmagicPendingRun` before restoring anything. While that
74
+ marker remains, new plans and forward replay are blocked; continue the same rollback after resolving
75
+ its conflicts. The marker clears only after successful recovery. Per-script recovery markers let
76
+ that rollback resume a source write interrupted before its ownership/path bookkeeping finished.
77
+
78
+ ```text
79
+ node "<bundle>/build-studio-update.mjs" rollback --project <port> --run <run-dir> --out <new-rollback.luau>
80
+ ```
81
+
82
+ Review and execute the generated recovery only for this run. It restores source/ownership/path/
83
+ enabled state conditionally; later human edits are preserved and reported. New scripts are removed
84
+ only if still attributable to this run, with expected content and no new children. Additional
85
+ attributes, tags, or changes to Archivable, RunContext or LinkedSource preserve the script and
86
+ report a conflict. Empty Folders created by the run also require their recorded original path,
87
+ unchanged attributes/tags and default Archivable state before removal. Missing folder creation
88
+ metadata or unreadable properties require manual recovery. It never deletes imported cloud assets.
89
+ Keep the returned recovery snapshot and resolve any `rollback-incomplete` entries before continuing.
90
+
91
+ After a successful rollback, restore the matching local accepted baseline from the run's durable
92
+ backup using the returned receipt:
93
+
94
+ ```text
95
+ node "<bundle>/port-state.mjs" accept-rollback --project <port> --run <run-dir> --receipt <rollback.json>
96
+ ```
97
+
98
+ Never leave the local baseline naming the
99
+ reverted build. Cloud versions, purchases and account-data migrations are outside this recovery.
100
+
101
+ ## Saving
102
+
103
+ After the complete verified game is in Edit state, discover whether a supported MCP operation can
104
+ save the complete place. Confirm its semantics: local file or non-published draft, selected
105
+ destination, no experience publication. Use it when available within the authorized port workflow.
106
+ Reuse an established save destination; ask when one is missing or it would overwrite unrelated work.
107
+ Confirm the result against the installed build and record actual evidence with `record-save`.
108
+
109
+ Do not infer save privilege from `execute_luau`, invent a save tool, request publishing credentials,
110
+ or call undocumented privileged APIs. If no supported save exists, finish with “ready in Studio;
111
+ save required.” On failure/ambiguous response preserve the project, inspect before retrying, and
112
+ report failed/unconfirmed. Source-only Rojo output and script backups are not complete-place saves.
113
+
114
+ Leave publishing, visibility and release management to the creator in Studio in every case.
115
+
116
+ - [Studio MCP](https://create.roblox.com/docs/studio/mcp).
117
+ - [ScriptEditorService](https://create.roblox.com/docs/reference/engine/classes/ScriptEditorService).
118
+ - [Instance attributes, tags and Archivable](https://create.roblox.com/docs/reference/engine/classes/Instance).
119
+ - [BaseScript properties](https://create.roblox.com/docs/reference/engine/classes/BaseScript).
@@ -0,0 +1,63 @@
1
+ # Verify the installed Roblox port
2
+
3
+ Bind evidence to source/package hashes, live overrides, asset versions, native settings, tool
4
+ versions, and the installed `BitmagicBuildId` attribute. Inspect Edit state before each relevant
5
+ test, then use a fresh Play context. A cached Edit-mode `require` can return old code.
6
+
7
+ Maintain independent results for:
8
+
9
+ | Check | Evidence |
10
+ | --- | --- |
11
+ | Source/data | Luau syntax/type checks and complete tables/configurations. |
12
+ | Rules | Fixed inputs and expected outputs/event sequences; defined numeric tolerances. |
13
+ | Native integration | Fresh correct Client/Server contexts, build marker, clean console, working dependencies. |
14
+ | Visual/input | Valid observed views/UI interactions at recorded desktop/touch viewports. |
15
+ | Performance | Named hardware, quality, resolution, foreground state, duration and representative load. |
16
+ | Saved | Confirmed complete-project save receipt/file evidence for this build, if supported. |
17
+
18
+ An earlier row does not imply later rows passed. Test stubs are not Roblox rendering, input,
19
+ replication, asset moderation or audio. Do not enable unsafe runtime `loadstring` to run tests.
20
+ Explicit owner inspection can provide manual evidence. Missing checks remain unverified, not green.
21
+
22
+ ## Minimum player-journey contract
23
+
24
+ Derive concrete cases from the source inventory, including later progression:
25
+
26
+ - Title, briefing, map/level selection, build/play, combat, results, return, defeat/retry and replay.
27
+ - Every tutorial stage, legal/illegal actions, affordability/recovery, unlock and start gating.
28
+ - Rule tables, specialized enemies/abilities, damage boundaries, rewards and single payouts.
29
+ - Repeated full cycles: no duplicate input handlers, stale UI/audio/camera ownership or accumulating instances.
30
+ - Persistence across sessions/rejoining when required; session carry-over alone is not persistence.
31
+ - Required icons, mesh identities/materials, correct scale/grounding, pivots/sockets and animation timing.
32
+ - Feedback that communicates mechanics: splash radius, piercing corridor, attacks and meaningful impact.
33
+ - Desktop, portrait phone and short landscape: safe areas, text, scrolling, touch confirmation,
34
+ placement visibility, selected controls, pause/music and controller focus where supported.
35
+ - Existing target-only commerce/customization stays intact; inspect cancellation/unknown ownership,
36
+ receipt deduplication and rejoin behavior if those integrations are part of the port.
37
+
38
+ Use public UI for a short smoke journey. Test hooks may accelerate long scenarios, but label them
39
+ and exclude temporary cheats/fixtures from the final Edit project. Poll a bounded ready/build marker
40
+ instead of arbitrary sleeps. Stop bounded retries and preserve the last error when evidence fails.
41
+
42
+ Do not assume JavaScript PRNG streams match Roblox Random. Specify shared deterministic decisions
43
+ or compare exported fixtures. Counts of simulated actors differ from the number actually rendered.
44
+ Report sampling and visible role representation; reducing density requires a disclosed decision.
45
+
46
+ Profile cold load, mass spawn, collision/contact, peak VFX/audio, boss cases, and repeated returns.
47
+ Record frame-time distributions, simulation and presentation CPU costs, memory/instance/connection
48
+ growth, asset stalls and relevant traffic separately. A headless rules benchmark is not FPS.
49
+ Background-throttled Studio measurements cannot certify foreground or mobile performance.
50
+
51
+ ## Captures and handoff
52
+
53
+ Check that a screenshot is the intended build/view and includes the GUI under test. Reject blank,
54
+ magenta, locked-screen, wrong-session or missing-overlay evidence. When MCP capture cannot observe
55
+ the UI, give the creator a precise view/action to inspect rather than claiming it passed.
56
+
57
+ After tests, return task-started Play to Edit and ensure all required content lives there. Remove
58
+ temporary fixtures, disable test hooks, and stop task-owned servers/children/ports. Leave unrelated
59
+ processes and Studio open. Save through a supported MCP operation if available and report its status.
60
+
61
+ Completion is a complete game ready in Studio with required checks/deviations accounted for.
62
+ Published-client testing, cloud release and public availability are the creator's subsequent
63
+ workflow. Never turn “not published yet” into a reason to add publishing automation.