@el4cteo/rbx-studio-mcp 0.8.3 → 0.8.6

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 (68) hide show
  1. package/README.md +24 -6
  2. package/dist/bridge/rpc.js +1 -1
  3. package/dist/bridge/rpc.js.map +1 -1
  4. package/dist/index.js +7 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/apidump.js +75 -0
  7. package/dist/lib/apidump.js.map +1 -1
  8. package/dist/lib/errors.js +5 -4
  9. package/dist/lib/errors.js.map +1 -1
  10. package/dist/lib/format.js +92 -25
  11. package/dist/lib/format.js.map +1 -1
  12. package/dist/lib/notices.js +29 -0
  13. package/dist/lib/notices.js.map +1 -0
  14. package/dist/lib/protocol.js.map +1 -1
  15. package/dist/lib/sync.js +1141 -0
  16. package/dist/lib/sync.js.map +1 -0
  17. package/dist/lib/syncplan.js +338 -0
  18. package/dist/lib/syncplan.js.map +1 -0
  19. package/dist/lib/tool.js +9 -2
  20. package/dist/lib/tool.js.map +1 -1
  21. package/dist/tools/discover.js +6 -1
  22. package/dist/tools/discover.js.map +1 -1
  23. package/dist/tools/exec.js +5 -0
  24. package/dist/tools/exec.js.map +1 -1
  25. package/dist/tools/instances.js +2 -2
  26. package/dist/tools/instances.js.map +1 -1
  27. package/dist/tools/perf.js +28 -3
  28. package/dist/tools/perf.js.map +1 -1
  29. package/dist/tools/screenshot.js +7 -3
  30. package/dist/tools/screenshot.js.map +1 -1
  31. package/dist/tools/scripts.js +110 -28
  32. package/dist/tools/scripts.js.map +1 -1
  33. package/dist/tools/sync.js +169 -0
  34. package/dist/tools/sync.js.map +1 -0
  35. package/package.json +4 -4
  36. package/plugin/src/Commands.luau +72 -5
  37. package/plugin/src/Config.luau +65 -65
  38. package/plugin/src/Console.luau +5 -0
  39. package/plugin/src/Dispatch.luau +131 -90
  40. package/plugin/src/ExecRuntime.luau +190 -168
  41. package/plugin/src/LogBuffer.luau +38 -9
  42. package/plugin/src/Paths.luau +42 -0
  43. package/plugin/src/Phrase.luau +41 -0
  44. package/plugin/src/Prompt.luau +23 -3
  45. package/plugin/src/ScriptEdit.luau +94 -8
  46. package/plugin/src/Serialize.luau +16 -1
  47. package/plugin/src/Transport.luau +5 -2
  48. package/plugin/src/Undo.luau +74 -10
  49. package/plugin/src/handlers/Capture.luau +818 -809
  50. package/plugin/src/handlers/Debug.luau +19 -16
  51. package/plugin/src/handlers/Discover.luau +31 -1
  52. package/plugin/src/handlers/Perf.luau +100 -21
  53. package/plugin/src/handlers/Scripts.luau +274 -90
  54. package/plugin/src/handlers/Sync.luau +968 -0
  55. package/plugin/src/init.server.luau +47 -2
  56. package/scripts/build.mjs +14 -0
  57. package/scripts/sync-fake.mjs +191 -0
  58. package/scripts/test-live-sync-scale.mjs +150 -0
  59. package/scripts/test-live-sync.mjs +232 -0
  60. package/scripts/test-live-tools.mjs +6 -1
  61. package/scripts/test-plugin.mjs +17 -0
  62. package/scripts/test-results.mjs +41 -0
  63. package/scripts/test-sync-more.mjs +228 -0
  64. package/scripts/test-sync.mjs +245 -0
  65. package/dist/tools/spatial.js +0 -135
  66. package/dist/tools/spatial.js.map +0 -1
  67. package/dist/tools/upload.js +0 -294
  68. package/dist/tools/upload.js.map +0 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * `sync`, offline: the planner on its own, then the whole engine against a
3
+ * real temp folder and an in-memory Studio that behaves like the plugin's
4
+ * sync handler (revisions, conditional writes, creates, moves, deletes).
5
+ *
6
+ * Usage: node scripts/test-sync.mjs (after `npm run build`)
7
+ */
8
+ import assert from "node:assert/strict";
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import path from "node:path";
12
+ import {
13
+ classFromFile,
14
+ decodeName,
15
+ emptyManifest,
16
+ encodeName,
17
+ layout,
18
+ placementOf,
19
+ plan,
20
+ } from "../dist/lib/syncplan.js";
21
+ import { hashOf, runSync, stopAllWatches } from "../dist/lib/sync.js";
22
+ import { fakeStudio, scanOf } from "./sync-fake.mjs";
23
+
24
+ // Names -----------------------------------------------------------------------
25
+
26
+ for (const name of ["Main", "Dr. Who", "a/b\\c", "what?", "100%", "CON", "trailing.", "trailing ", "..", "", "init", "Config.server", "日本"]) {
27
+ for (const kind of ["dir", "script"]) {
28
+ const encoded = encodeName(name, kind);
29
+ assert.ok(!/[<>:"/\\|?*]/.test(encoded), `${JSON.stringify(name)} -> ${encoded} is file-safe`);
30
+ assert.ok(!/[. ]$/.test(encoded) && !/^(con|prn|aux|nul)$/i.test(encoded), `${encoded} is Windows-safe`);
31
+ assert.equal(decodeName(encoded), name, `${JSON.stringify(name)} round-trips as ${kind}`);
32
+ }
33
+ }
34
+ assert.equal(classFromFile(`${encodeName("Config.server", "script")}.luau`).className, "ModuleScript", "a dotted name keeps its class");
35
+ assert.equal(classFromFile(`${encodeName("init", "script")}.luau`).init, false, "a script named init is not an init file");
36
+ assert.deepEqual(classFromFile("Main.server.luau"), { stem: "Main", className: "Script", init: false });
37
+ assert.deepEqual(classFromFile("init.client.lua"), { stem: "init", className: "LocalScript", init: true });
38
+ assert.equal(classFromFile("notes.txt"), null);
39
+ console.log("sync names: encode/decode round trips, reserved and ambiguous names");
40
+
41
+ // Layout ----------------------------------------------------------------------
42
+
43
+ {
44
+ const shape = layout(
45
+ scanOf({
46
+ "ServerScriptService.Main": { className: "Script", revision: "r1" },
47
+ "ServerScriptService.Main.Helper": { className: "ModuleScript", revision: "r2" },
48
+ "ReplicatedStorage.Shared.Util": { className: "ModuleScript", revision: "r3" },
49
+ "ReplicatedStorage.Shared.util": { className: "LocalScript", revision: "r4" },
50
+ "ReplicatedStorage.Shared.UTIL": { className: "ModuleScript", revision: "r5" },
51
+ "StarterGui.Bound": { className: "LocalScript", revision: "r6", fileSynced: true },
52
+ }),
53
+ );
54
+ assert.equal(shape.fileOf.get("ServerScriptService.Main"), "ServerScriptService/Main/init.server.luau");
55
+ assert.equal(shape.fileOf.get("ServerScriptService.Main.Helper"), "ServerScriptService/Main/Helper.luau");
56
+ assert.equal(shape.fileOf.get("ReplicatedStorage.Shared.UTIL"), "ReplicatedStorage/Shared/UTIL.luau");
57
+ assert.equal(shape.fileOf.get("ReplicatedStorage.Shared.Util"), "ReplicatedStorage/Shared/Util~2.luau", "case collision suffixed");
58
+ assert.equal(shape.fileOf.get("ReplicatedStorage.Shared.util"), "ReplicatedStorage/Shared/util.client.luau", "different extension, no collision");
59
+ assert.deepEqual(shape.fileSynced, ["StarterGui.Bound"]);
60
+ assert.equal(shape.pathOfDir.get("ReplicatedStorage/Shared"), "ReplicatedStorage.Shared");
61
+
62
+ const place = placementOf("ReplicatedStorage/Shared/New/Thing.luau", shape, () => undefined);
63
+ assert.deepEqual(place, { parentPath: "ReplicatedStorage.Shared", parents: [{ name: "New", className: "Folder" }], name: "Thing", className: "ModuleScript" });
64
+ const init = placementOf("ServerScriptService/Svc/init.server.luau", shape, () => "Script");
65
+ assert.deepEqual(init, { parentPath: "ServerScriptService", parents: [], name: "Svc", className: "Script" });
66
+ assert.ok("error" in placementOf("Loose.luau", shape, () => undefined), "files must sit inside a service folder");
67
+ console.log("sync layout: init folders, case collisions, Script Sync exclusions, placement");
68
+ }
69
+
70
+ // Plan ------------------------------------------------------------------------
71
+
72
+ {
73
+ const scripts = { "ServerScriptService.A": { className: "Script", revision: "a1" }, "ServerScriptService.B": { className: "ModuleScript", revision: "b1" } };
74
+ const studio = layout(scanOf(scripts));
75
+ const inScope = () => true;
76
+ const manifest = emptyManifest();
77
+ const first = plan({ manifest, studio, disk: new Map(), inScope, direction: "both" });
78
+ assert.deepEqual(first.actions.map((action) => action.kind).sort(), ["create-disk", "create-disk"], "first sync pulls everything, deletes nothing");
79
+
80
+ manifest.files["ServerScriptService/A.server.luau"] = { path: "ServerScriptService.A", className: "Script", revision: "a1", hash: "ha" };
81
+ manifest.files["ServerScriptService/B.luau"] = { path: "ServerScriptService.B", className: "ModuleScript", revision: "b1", hash: "hb" };
82
+ const kinds = (input) => plan({ manifest, inScope, direction: "both", ...input }).actions.map((action) => `${action.kind} ${action.file}`).sort();
83
+
84
+ const same = new Map([["ServerScriptService/A.server.luau", "ha"], ["ServerScriptService/B.luau", "hb"]]);
85
+ assert.deepEqual(kinds({ studio, disk: same }), [], "in step: nothing to do");
86
+ assert.deepEqual(kinds({ studio, disk: new Map([...same, ["ServerScriptService/A.server.luau", "ha2"]]) }), ["push ServerScriptService/A.server.luau"]);
87
+ const edited = layout(scanOf({ ...scripts, "ServerScriptService.A": { className: "Script", revision: "a2" } }));
88
+ assert.deepEqual(kinds({ studio: edited, disk: same }), ["pull ServerScriptService/A.server.luau"]);
89
+ assert.deepEqual(kinds({ studio: edited, disk: new Map([...same, ["ServerScriptService/A.server.luau", "ha2"]]) }), ["merge ServerScriptService/A.server.luau"]);
90
+
91
+ // Renamed on disk: same hash under a new name moves the script.
92
+ const renamedOnDisk = new Map([["ServerScriptService/Alpha.server.luau", "ha"], ["ServerScriptService/B.luau", "hb"]]);
93
+ assert.deepEqual(kinds({ studio, disk: renamedOnDisk }), ["move-studio ServerScriptService/Alpha.server.luau"]);
94
+ // Renamed in Studio: same revision at a new path moves the file.
95
+ const renamedInStudio = layout(scanOf({ "ServerScriptService.Alpha": { className: "Script", revision: "a1" }, "ServerScriptService.B": scripts["ServerScriptService.B"] }));
96
+ assert.deepEqual(kinds({ studio: renamedInStudio, disk: same }), ["move-disk ServerScriptService/Alpha.server.luau"]);
97
+
98
+ // Deleted on one side, untouched on the other: delete on the other.
99
+ assert.deepEqual(kinds({ studio, disk: new Map([["ServerScriptService/B.luau", "hb"]]) }), ["delete-studio ServerScriptService/A.server.luau"]);
100
+ const withoutA = layout(scanOf({ "ServerScriptService.B": scripts["ServerScriptService.B"] }));
101
+ assert.deepEqual(kinds({ studio: withoutA, disk: same }), ["delete-disk ServerScriptService/A.server.luau"]);
102
+ // Deleted on one side, edited on the other: conflict, unless told who wins.
103
+ assert.deepEqual(kinds({ studio: withoutA, disk: new Map([...same, ["ServerScriptService/A.server.luau", "ha2"]]) }), ["conflict ServerScriptService/A.server.luau"]);
104
+ assert.deepEqual(
105
+ plan({ manifest, inScope, direction: "both", prefer: "disk", studio: withoutA, disk: new Map([...same, ["ServerScriptService/A.server.luau", "ha2"]]) }).actions.map((a) => a.kind),
106
+ ["create-studio"],
107
+ );
108
+ // Gone from both: forget.
109
+ assert.deepEqual(kinds({ studio: withoutA, disk: new Map([["ServerScriptService/B.luau", "hb"]]) }), ["forget ServerScriptService/A.server.luau"]);
110
+ // Direction holds the other side's changes back rather than dropping them.
111
+ const pushOnly = plan({ manifest, inScope, direction: "push", studio: edited, disk: same });
112
+ assert.equal(pushOnly.actions.length, 0);
113
+ assert.deepEqual(pushOnly.held.map((action) => action.kind), ["pull"]);
114
+ // Out of scope entries are ignored, never deleted.
115
+ assert.deepEqual(plan({ manifest, inScope: () => false, direction: "both", studio: layout(scanOf({})), disk: new Map() }).actions, []);
116
+ console.log("sync plan: first sync, push, pull, merge, renames both ways, deletes, conflicts, direction, scope");
117
+ }
118
+
119
+ // Engine ----------------------------------------------------------------------
120
+
121
+ const dir = mkdtempSync(path.join(tmpdir(), "rbx-sync-test-"));
122
+ const file = (relative) => path.join(dir, ...relative.split("/"));
123
+ const read = (relative) => readFileSync(file(relative), "utf8");
124
+ const studio = fakeStudio();
125
+ studio.scripts.set("ServerScriptService.Main", { className: "Script", source: 'print("hi")\n' });
126
+ studio.scripts.set("ServerScriptService.Main.Helper", { className: "ModuleScript", source: "return {}\n" });
127
+ studio.scripts.set("StarterGui.Shop.Client", { className: "LocalScript", source: "-- client\n" });
128
+ const sync = (extra = {}) => runSync(studio, { dir, direction: "both", ...extra });
129
+
130
+ try {
131
+ // First pull writes everything, the Rojo way.
132
+ let report = await sync();
133
+ assert.equal(report.counts["create-disk"], 3);
134
+ assert.equal(read("ServerScriptService/Main/init.server.luau"), 'print("hi")\n');
135
+ assert.equal(read("ServerScriptService/Main/Helper.luau"), "return {}\n");
136
+ assert.equal(read("StarterGui/Shop/Client.client.luau"), "-- client\n");
137
+ assert.ok(existsSync(file(".rbx-sync/manifest.json")) && read(".rbx-sync/.gitignore") === "*\n");
138
+ assert.equal((await sync()).lines.length, 0, "a second run is a no-op");
139
+
140
+ // Edit a file (with CRLF, as some editors save): it reaches Studio as LF.
141
+ writeFileSync(file("ServerScriptService/Main/Helper.luau"), "return { answer = 42 }\r\n");
142
+ report = await sync();
143
+ assert.equal(report.counts.push, 1);
144
+ assert.equal(studio.scripts.get("ServerScriptService.Main.Helper").source, "return { answer = 42 }\n");
145
+
146
+ // Edit in Studio: it reaches the file.
147
+ studio.scripts.get("StarterGui.Shop.Client").source = "-- client v2\n";
148
+ report = await sync();
149
+ assert.equal(report.counts.pull, 1);
150
+ assert.equal(read("StarterGui/Shop/Client.client.luau"), "-- client v2\n");
151
+
152
+ // New file on disk, in a new folder: created in Studio.
153
+ mkdirSync(file("ReplicatedStorage/Shared"), { recursive: true });
154
+ writeFileSync(file("ReplicatedStorage/Shared/Util.luau"), "return 1\n");
155
+ report = await sync();
156
+ assert.equal(report.counts["create-studio"], 1);
157
+ assert.equal(studio.scripts.get("ReplicatedStorage.Shared.Util").source, "return 1\n");
158
+
159
+ // Both sides edited: a conflict, and neither side is touched.
160
+ writeFileSync(file("ReplicatedStorage/Shared/Util.luau"), "return 2\n");
161
+ studio.scripts.get("ReplicatedStorage.Shared.Util").source = "return 3\n";
162
+ report = await sync();
163
+ assert.equal(report.conflicts.length, 1);
164
+ assert.equal(read("ReplicatedStorage/Shared/Util.luau"), "return 2\n");
165
+ assert.equal(studio.scripts.get("ReplicatedStorage.Shared.Util").source, "return 3\n");
166
+ // ...and the same text on both sides settles it without asking.
167
+ writeFileSync(file("ReplicatedStorage/Shared/Util.luau"), "return 3\n");
168
+ report = await sync();
169
+ assert.equal(report.conflicts.length, 0);
170
+ assert.equal((await sync()).lines.length, 0);
171
+ // prefer settles a real conflict in one direction.
172
+ writeFileSync(file("ReplicatedStorage/Shared/Util.luau"), "return 4\n");
173
+ studio.scripts.get("ReplicatedStorage.Shared.Util").source = "return 5\n";
174
+ report = await sync({ prefer: "disk" });
175
+ assert.equal(studio.scripts.get("ReplicatedStorage.Shared.Util").source, "return 4\n");
176
+
177
+ // Rename on disk: the script moves, it is not recreated.
178
+ const moved = studio.scripts.get("ReplicatedStorage.Shared.Util");
179
+ renameSync(file("ReplicatedStorage/Shared/Util.luau"), file("ReplicatedStorage/Shared/Tools.luau"));
180
+ report = await sync();
181
+ assert.equal(report.counts["move-studio"], 1);
182
+ assert.equal(studio.scripts.get("ReplicatedStorage.Shared.Tools"), moved, "same instance, new name");
183
+ assert.ok(!studio.scripts.has("ReplicatedStorage.Shared.Util"));
184
+
185
+ // Rename in Studio: the file moves.
186
+ studio.scripts.set("StarterGui.Shop.ShopClient", studio.scripts.get("StarterGui.Shop.Client"));
187
+ studio.scripts.delete("StarterGui.Shop.Client");
188
+ report = await sync();
189
+ assert.equal(report.counts["move-disk"], 1);
190
+ assert.ok(existsSync(file("StarterGui/Shop/ShopClient.client.luau")) && !existsSync(file("StarterGui/Shop/Client.client.luau")));
191
+
192
+ // Deleted in Studio: the file goes to the trash, not away.
193
+ studio.scripts.delete("StarterGui.Shop.ShopClient");
194
+ report = await sync();
195
+ assert.equal(report.counts["delete-disk"], 1);
196
+ assert.ok(!existsSync(file("StarterGui/Shop/ShopClient.client.luau")));
197
+ assert.ok(!existsSync(file("StarterGui/Shop")), "emptied folders are pruned");
198
+ const trashRuns = readdirSync(file(".rbx-sync/trash"));
199
+ assert.equal(read(`.rbx-sync/trash/${trashRuns[0]}/StarterGui/Shop/ShopClient.client.luau`), "-- client v2\n");
200
+
201
+ // Deleted on disk: the script is deleted in Studio.
202
+ unlinkSync(file("ReplicatedStorage/Shared/Tools.luau"));
203
+ report = await sync();
204
+ assert.equal(report.counts["delete-studio"], 1);
205
+ assert.ok(!studio.scripts.has("ReplicatedStorage.Shared.Tools"));
206
+
207
+ // Push holds Studio's changes back and says so.
208
+ studio.scripts.get("ServerScriptService.Main").source = 'print("studio")\n';
209
+ report = await sync({ direction: "push" });
210
+ assert.equal(report.held.length, 1);
211
+ assert.equal(read("ServerScriptService/Main/init.server.luau"), 'print("hi")\n');
212
+ await sync();
213
+ assert.equal(read("ServerScriptService/Main/init.server.luau"), 'print("studio")\n');
214
+
215
+ // Dry run changes nothing.
216
+ writeFileSync(file("ServerScriptService/Main/Helper.luau"), "return 'dry'\n");
217
+ report = await sync({ dryRun: true });
218
+ assert.equal(report.counts.push, 1);
219
+ assert.notEqual(studio.scripts.get("ServerScriptService.Main.Helper").source, "return 'dry'\n");
220
+ await sync();
221
+
222
+ // Another place is refused outright.
223
+ const other = fakeStudio(2);
224
+ await assert.rejects(runSync(other, { dir, direction: "both" }), /PLACE_MISMATCH|synced with place/);
225
+
226
+ // Deleting most of what is tracked stops and asks.
227
+ const big = mkdtempSync(path.join(tmpdir(), "rbx-sync-big-"));
228
+ const bigStudio = fakeStudio(3);
229
+ for (let index = 0; index < 20; index += 1) bigStudio.scripts.set(`ServerScriptService.S${index}`, { className: "ModuleScript", source: `return ${index}\n` });
230
+ await runSync(bigStudio, { dir: big, direction: "both" });
231
+ rmSync(path.join(big, "ServerScriptService"), { recursive: true });
232
+ await assert.rejects(runSync(bigStudio, { dir: big, direction: "both" }), /would delete 20/);
233
+ assert.equal(bigStudio.scripts.size, 20, "nothing was deleted");
234
+ const confirmed = await runSync(bigStudio, { dir: big, direction: "both", confirmDeletes: true });
235
+ assert.equal(confirmed.counts["delete-studio"], 20);
236
+ rmSync(big, { recursive: true, force: true });
237
+
238
+ assert.ok(studio.logged.some((line) => line.message.startsWith("sync to files")), "disk-side work is reported in the panel");
239
+ console.log("sync engine: pull, push (CRLF), create, conflicts, prefer, renames, trash, prune, direction, dry run, place and mass-delete guards");
240
+ } finally {
241
+ stopAllWatches();
242
+ rmSync(dir, { recursive: true, force: true });
243
+ }
244
+
245
+ assert.equal(hashOf("a\r\nb"), hashOf("a\nb"));
@@ -1,135 +0,0 @@
1
- import { z } from "zod";
2
- import { json } from "../lib/format.js";
3
- import { defineTool } from "../lib/tool.js";
4
- /** A position, written the way the Properties panel shows one. */
5
- const position = z
6
- .string()
7
- .describe('A position, written as the Properties panel shows it: "12, 0, 5".');
8
- export function registerSpatialTools(context) {
9
- const { bridge } = context;
10
- defineTool(context, {
11
- name: "spatial",
12
- title: "Ask what is actually there",
13
- description: "Casts a ray, block or sphere through the world, or lists every part " +
14
- "inside a volume.\n\n" +
15
- "This answers the one question the Explorer cannot: what is physically " +
16
- "THERE. A path tells you an instance exists and where its pivot sits. " +
17
- "It does not tell you the door frame is clipping into the wall, that " +
18
- "the spawn is buried a stud inside the floor, or that nothing stands " +
19
- "between the turret and the player. Every one of those is a query " +
20
- "against the world, and geometry that is wrong in exactly that way " +
21
- "looks completely correct in `inspect`.\n\n" +
22
- "`op=\"cast\"` fires a shape and reports the first thing it meets: the " +
23
- "part, where on it, the surface normal, the material and the distance. " +
24
- "Aim it with `to` (a point to look at) or with `direction` plus " +
25
- "`distance`. A miss comes back as `hit: false`, which is a real answer " +
26
- "— that is how you confirm a sightline is clear or that there is no " +
27
- "ground below.\n\n" +
28
- "`op=\"overlap\"` lists what sits inside a volume: a box, a radius, or " +
29
- "`region=\"part\"`, which takes an existing part and reports what " +
30
- "overlaps it. That last one is the fastest way to find things clipping " +
31
- "through each other.\n\n" +
32
- "Narrow with `only` or `ignore` — a query over a whole place returns a " +
33
- "transcript, not an answer. Set `collisionGroup` when the place uses " +
34
- "them: a cast run in the wrong group reports a clear path through a " +
35
- "wall the player cannot walk through, and a wrong answer of that shape " +
36
- "is indistinguishable from a right one.\n\n" +
37
- "`worldModel` points the query at a WorldModel inside a ViewportFrame " +
38
- "instead of the Workspace. Those are separate worlds with their own " +
39
- "parts and their own collision groups.",
40
- readOnly: true,
41
- inputSchema: {
42
- op: z
43
- .enum(["cast", "overlap"])
44
- .default("cast")
45
- .describe("'cast' fires a ray/block/sphere and reports the first hit; " +
46
- "'overlap' lists everything inside a volume."),
47
- shape: z
48
- .enum(["ray", "block", "sphere"])
49
- .optional()
50
- .describe("cast only: 'ray' is a line and the usual choice. 'block' and " +
51
- "'sphere' sweep a volume along the same path — use them when the " +
52
- "thing moving has width, e.g. checking whether a character fits " +
53
- "through a gap rather than whether a point does."),
54
- from: position.optional().describe("cast only: where the cast starts."),
55
- to: position
56
- .optional()
57
- .describe("cast only: a point to aim at. Use this for sightlines — it saves " +
58
- "working out a direction vector, which is where sign errors live."),
59
- direction: position
60
- .optional()
61
- .describe('cast only: which way to go, e.g. "0, -1, 0" for straight down. ' +
62
- "Used with `distance`. Ignored when `to` is given."),
63
- distance: z
64
- .number()
65
- .optional()
66
- .describe("cast only: how far to travel along `direction`. Defaults to 100."),
67
- size: position
68
- .optional()
69
- .describe('cast shape="block" or overlap region="box": the volume\'s size.'),
70
- radius: z
71
- .number()
72
- .optional()
73
- .describe('cast shape="sphere" or overlap region="radius": the radius.'),
74
- region: z
75
- .enum(["box", "radius", "part"])
76
- .optional()
77
- .describe("overlap only: 'box' and 'radius' need `at`; 'part' takes `path` " +
78
- "and reports what overlaps that part. Defaults to 'box'."),
79
- at: position.optional().describe('overlap only: the centre, for region "box" or "radius".'),
80
- path: z
81
- .string()
82
- .optional()
83
- .describe('overlap region="part" only: the part to test against.'),
84
- only: z
85
- .array(z.string())
86
- .optional()
87
- .describe("Consider ONLY these instances and their descendants. Use it to " +
88
- 'ask a narrow question — "does anything tagged Door block this".'),
89
- ignore: z
90
- .array(z.string())
91
- .optional()
92
- .describe("Skip these instances and their descendants. The usual case: " +
93
- "ignore the character doing the looking, which otherwise blocks " +
94
- "its own cast at zero distance."),
95
- collisionGroup: z
96
- .string()
97
- .optional()
98
- .describe("Run the query as if it came from a part in this collision group. " +
99
- "Required for a truthful answer in any place that uses groups."),
100
- respectCanCollide: z
101
- .boolean()
102
- .optional()
103
- .describe("Skip parts with CanCollide off. Off by default, matching the " +
104
- "engine — leave it off to ask what is there, turn it on to ask " +
105
- "what would stop a player."),
106
- ignoreWater: z
107
- .boolean()
108
- .optional()
109
- .describe("cast only: pass through terrain water instead of hitting it."),
110
- limit: z
111
- .number()
112
- .int()
113
- .min(1)
114
- .max(500)
115
- .optional()
116
- .describe("overlap only: how many parts to list. Defaults to 50."),
117
- worldModel: z
118
- .string()
119
- .optional()
120
- .describe("Query this WorldModel instead of the Workspace. Omit for the " +
121
- "normal case."),
122
- studioId: z.string().optional().describe("Target Studio; omit for the active one."),
123
- },
124
- }, async (args) => {
125
- const { op, studioId, ...rest } = args;
126
- const response = await bridge.call(op === "overlap" ? "spatial.overlap" : "spatial.cast", rest, { studioId, timeoutMs: 30_000 });
127
- if (op === "cast" && response["hit"] === false) {
128
- return json(response, "Nothing was hit. If that is a surprise, the usual causes are a " +
129
- "`direction` pointing the wrong way, a `distance` shorter than the " +
130
- "gap, or an `only` filter that excludes what you meant to find.");
131
- }
132
- return json(response);
133
- });
134
- }
135
- //# sourceMappingURL=spatial.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"spatial.js","sourceRoot":"","sources":["../../src/tools/spatial.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAoB,MAAM,gBAAgB,CAAC;AAE9D,kEAAkE;AAClE,MAAM,QAAQ,GAAG,CAAC;KACf,MAAM,EAAE;KACR,QAAQ,CAAC,mEAAmE,CAAC,CAAC;AAEjF,MAAM,UAAU,oBAAoB,CAAC,OAAoB;IACvD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAE3B,UAAU,CACR,OAAO,EACP;QACE,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,sEAAsE;YACtE,sBAAsB;YACtB,wEAAwE;YACxE,uEAAuE;YACvE,sEAAsE;YACtE,sEAAsE;YACtE,mEAAmE;YACnE,oEAAoE;YACpE,4CAA4C;YAC5C,wEAAwE;YACxE,wEAAwE;YACxE,iEAAiE;YACjE,wEAAwE;YACxE,qEAAqE;YACrE,mBAAmB;YACnB,wEAAwE;YACxE,mEAAmE;YACnE,wEAAwE;YACxE,yBAAyB;YACzB,wEAAwE;YACxE,sEAAsE;YACtE,qEAAqE;YACrE,wEAAwE;YACxE,4CAA4C;YAC5C,uEAAuE;YACvE,qEAAqE;YACrE,uCAAuC;QACzC,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE;YACX,EAAE,EAAE,CAAC;iBACF,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;iBACzB,OAAO,CAAC,MAAM,CAAC;iBACf,QAAQ,CACP,6DAA6D;gBAC3D,6CAA6C,CAChD;YACH,KAAK,EAAE,CAAC;iBACL,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;iBAChC,QAAQ,EAAE;iBACV,QAAQ,CACP,+DAA+D;gBAC7D,kEAAkE;gBAClE,iEAAiE;gBACjE,iDAAiD,CACpD;YACH,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;YACvE,EAAE,EAAE,QAAQ;iBACT,QAAQ,EAAE;iBACV,QAAQ,CACP,mEAAmE;gBACjE,kEAAkE,CACrE;YACH,SAAS,EAAE,QAAQ;iBAChB,QAAQ,EAAE;iBACV,QAAQ,CACP,iEAAiE;gBAC/D,mDAAmD,CACtD;YACH,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,kEAAkE,CAAC;YAC/E,IAAI,EAAE,QAAQ;iBACX,QAAQ,EAAE;iBACV,QAAQ,CAAC,iEAAiE,CAAC;YAC9E,MAAM,EAAE,CAAC;iBACN,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,6DAA6D,CAAC;YAC1E,MAAM,EAAE,CAAC;iBACN,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;iBAC/B,QAAQ,EAAE;iBACV,QAAQ,CACP,kEAAkE;gBAChE,yDAAyD,CAC5D;YACH,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yDAAyD,CAAC;YAC3F,IAAI,EAAE,CAAC;iBACJ,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,uDAAuD,CAAC;YACpE,IAAI,EAAE,CAAC;iBACJ,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,QAAQ,CACP,iEAAiE;gBAC/D,iEAAiE,CACpE;YACH,MAAM,EAAE,CAAC;iBACN,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,QAAQ,CACP,8DAA8D;gBAC5D,iEAAiE;gBACjE,gCAAgC,CACnC;YACH,cAAc,EAAE,CAAC;iBACd,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,mEAAmE;gBACjE,+DAA+D,CAClE;YACH,iBAAiB,EAAE,CAAC;iBACjB,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,QAAQ,CACP,+DAA+D;gBAC7D,gEAAgE;gBAChE,2BAA2B,CAC9B;YACH,WAAW,EAAE,CAAC;iBACX,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,QAAQ,CAAC,8DAA8D,CAAC;YAC3E,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,GAAG,CAAC;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,uDAAuD,CAAC;YACpE,UAAU,EAAE,CAAC;iBACV,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,+DAA+D;gBAC7D,cAAc,CACjB;YACH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;SACpF;KACF,EACD,KAAK,EAAE,IAAI,EAAuB,EAAE;QAClC,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;QACvC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAChC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,cAAc,EACrD,IAAI,EACJ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,CAChC,CAAC;QAEF,IAAI,EAAE,KAAK,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC;YAC/C,OAAO,IAAI,CACT,QAAQ,EACR,iEAAiE;gBAC/D,oEAAoE;gBACpE,gEAAgE,CACnE,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxB,CAAC,CACF,CAAC;AACJ,CAAC"}