@princeofscale/bloxforge-inspector 2.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/Baseplate.rbxl +0 -0
- package/dist/index.js +19281 -0
- package/package.json +62 -0
- package/studio-plugin/INSTALLATION.md +161 -0
- package/studio-plugin/MCPInspectorPlugin.rbxmx +171699 -0
- package/studio-plugin/MCPPlugin.rbxmx +171699 -0
- package/studio-plugin/default.project.json +19 -0
- package/studio-plugin/dev.project.json +23 -0
- package/studio-plugin/include/LibMP.lua +156378 -0
- package/studio-plugin/inspector-icon.png +0 -0
- package/studio-plugin/package-lock.json +692 -0
- package/studio-plugin/package.json +19 -0
- package/studio-plugin/plugin.json +10 -0
- package/studio-plugin/src/modules/ClientBroker.ts +447 -0
- package/studio-plugin/src/modules/Communication.ts +697 -0
- package/studio-plugin/src/modules/EvalBridges.ts +255 -0
- package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
- package/studio-plugin/src/modules/JobRegistry.ts +77 -0
- package/studio-plugin/src/modules/LuauExec.ts +465 -0
- package/studio-plugin/src/modules/Recording.ts +28 -0
- package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +152 -0
- package/studio-plugin/src/modules/ServerUrlSettings.ts +114 -0
- package/studio-plugin/src/modules/State.ts +101 -0
- package/studio-plugin/src/modules/StopPlayMonitor.ts +276 -0
- package/studio-plugin/src/modules/UI.ts +790 -0
- package/studio-plugin/src/modules/Utils.ts +318 -0
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +203 -0
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +160 -0
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
- package/studio-plugin/src/modules/handlers/JobHandlers.ts +111 -0
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +14 -0
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +354 -0
- package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +1263 -0
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +809 -0
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +535 -0
- package/studio-plugin/src/server/index.server.ts +134 -0
- package/studio-plugin/src/types/index.d.ts +83 -0
- package/studio-plugin/tsconfig.json +21 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Game-VM eval bridges, ported from chrrxs/roblox-mcp-primitives.
|
|
2
|
+
//
|
|
3
|
+
// Our standard `execute_luau target=server/client-N` runs in the plugin VM
|
|
4
|
+
// with a fresh ModuleScript per call. That gives a clean sandbox but means
|
|
5
|
+
// `require(SomeModule)` returns a fresh copy, not the one the running game
|
|
6
|
+
// scripts hold. So runtime-mutated module state is invisible to probes.
|
|
7
|
+
//
|
|
8
|
+
// These bridges fix that by living inside the user's game scripts. Both
|
|
9
|
+
// peers use the same symmetric shape:
|
|
10
|
+
// - Server: a Script in ServerScriptService that creates a BindableFunction.
|
|
11
|
+
// Plugin (server peer) invokes it with a fresh ModuleScript payload;
|
|
12
|
+
// require() runs inside the Script VM so it shares the running server's
|
|
13
|
+
// require cache.
|
|
14
|
+
// - Client: a LocalScript in StarterPlayer.StarterPlayerScripts that
|
|
15
|
+
// creates a BindableFunction. Plugin invokes it with a fresh ModuleScript
|
|
16
|
+
// payload; require() runs inside the LocalScript VM so it shares the
|
|
17
|
+
// game's require cache.
|
|
18
|
+
//
|
|
19
|
+
// Why ModuleScript+require on both sides (no loadstring): require'd modules
|
|
20
|
+
// run with the security level they were created at and don't need
|
|
21
|
+
// ServerScriptService.LoadStringEnabled, so eval_server_runtime works even
|
|
22
|
+
// when LoadStringEnabled=false (the default in fresh places).
|
|
23
|
+
//
|
|
24
|
+
// Lifecycle: bridge scripts are created only in running play DataModels.
|
|
25
|
+
// The server plugin peer creates the Script in runtime ServerScriptService;
|
|
26
|
+
// each client plugin peer creates its LocalScript in that client's
|
|
27
|
+
// PlayerScripts. Nothing is installed into the edit DataModel anymore.
|
|
28
|
+
// Runtime-created scripts disappear naturally when the playtest stops.
|
|
29
|
+
|
|
30
|
+
import { Players, ReplicatedStorage, RunService, ServerScriptService, StarterPlayer } from "@rbxts/services";
|
|
31
|
+
|
|
32
|
+
const ScriptEditorService = game.GetService("ScriptEditorService");
|
|
33
|
+
|
|
34
|
+
function getStarterPlayerScripts(): Instance | undefined {
|
|
35
|
+
return StarterPlayer.FindFirstChild("StarterPlayerScripts");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SERVER_SCRIPT_NAME = "__MCP_ServerEvalBridge";
|
|
39
|
+
const CLIENT_SCRIPT_NAME = "__MCP_ClientEvalBridge";
|
|
40
|
+
|
|
41
|
+
// Public so the eval_*_runtime tool wrappers can reference the same names.
|
|
42
|
+
export const BRIDGE_NAMES = {
|
|
43
|
+
serverScript: SERVER_SCRIPT_NAME,
|
|
44
|
+
clientScript: CLIENT_SCRIPT_NAME,
|
|
45
|
+
serverLocal: "__MCP_ServerEvalLocal",
|
|
46
|
+
clientLocal: "__MCP_ClientEvalBridge",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
// Embedded Luau. The double `${...}` references our exported names so a
|
|
50
|
+
// rename here propagates to both the script source and the tool wrappers.
|
|
51
|
+
const SERVER_BRIDGE_SOURCE = `
|
|
52
|
+
-- Installed by @princeofscale/bloxforge to power the eval_server_runtime MCP
|
|
53
|
+
-- tool (shared-require-cache eval on the server during playtests). Inert
|
|
54
|
+
-- outside Studio (no-ops in live games); safe to leave in place.
|
|
55
|
+
|
|
56
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
57
|
+
local RunService = game:GetService("RunService")
|
|
58
|
+
|
|
59
|
+
if not RunService:IsStudio() then
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
local prevBf = ServerScriptService:FindFirstChild("${BRIDGE_NAMES.serverLocal}")
|
|
64
|
+
if prevBf then prevBf:Destroy() end
|
|
65
|
+
|
|
66
|
+
local bf = Instance.new("BindableFunction")
|
|
67
|
+
bf.Name = "${BRIDGE_NAMES.serverLocal}"
|
|
68
|
+
bf.Archivable = false
|
|
69
|
+
bf.Parent = ServerScriptService
|
|
70
|
+
bf.OnInvoke = function(payload)
|
|
71
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then
|
|
72
|
+
return { ok = false, value = "payload must be a ModuleScript instance" }
|
|
73
|
+
end
|
|
74
|
+
local ok, value = pcall(require, payload)
|
|
75
|
+
return { ok = ok, value = value }
|
|
76
|
+
end
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const CLIENT_BRIDGE_SOURCE = `
|
|
80
|
+
-- Installed by @princeofscale/bloxforge to power the eval_client_runtime MCP
|
|
81
|
+
-- tool (shared-require-cache eval on the client during playtests). Inert
|
|
82
|
+
-- outside Studio (no-ops in live games); safe to leave in place.
|
|
83
|
+
|
|
84
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
85
|
+
local RunService = game:GetService("RunService")
|
|
86
|
+
|
|
87
|
+
if not RunService:IsStudio() then
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
local prevBf = ReplicatedStorage:FindFirstChild("${BRIDGE_NAMES.clientLocal}")
|
|
92
|
+
if prevBf then prevBf:Destroy() end
|
|
93
|
+
|
|
94
|
+
local bf = Instance.new("BindableFunction")
|
|
95
|
+
bf.Name = "${BRIDGE_NAMES.clientLocal}"
|
|
96
|
+
bf.Archivable = false
|
|
97
|
+
bf.Parent = ReplicatedStorage
|
|
98
|
+
bf.OnInvoke = function(payload)
|
|
99
|
+
if typeof(payload) ~= "Instance" or not payload:IsA("ModuleScript") then
|
|
100
|
+
return { ok = false, value = "payload must be a ModuleScript instance" }
|
|
101
|
+
end
|
|
102
|
+
local ok, value = pcall(require, payload)
|
|
103
|
+
return { ok = ok, value = value }
|
|
104
|
+
end
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
// Stamp written onto each installed bridge Script so we can tell whether the
|
|
108
|
+
// runtime bridge currently in the play DM was produced by THIS plugin build.
|
|
109
|
+
// It's a djb2 hash of the actual bridge source plus the plugin version, so ANY
|
|
110
|
+
// change to the source (or a version bump) yields a new stamp and triggers a
|
|
111
|
+
// runtime refresh instead of keeping a stale bridge.
|
|
112
|
+
const STAMP_ATTR = "__MCPBridgeStamp";
|
|
113
|
+
|
|
114
|
+
function computeBridgeStamp(): string {
|
|
115
|
+
const combined = `${SERVER_BRIDGE_SOURCE}|${CLIENT_BRIDGE_SOURCE}`;
|
|
116
|
+
let h = 5381;
|
|
117
|
+
for (let i = 1; i <= combined.size(); i++) {
|
|
118
|
+
h = (h * 33 + string.byte(combined, i)[0]) % 2147483647;
|
|
119
|
+
}
|
|
120
|
+
// "__VERSION__" is replaced with the package version at package time
|
|
121
|
+
// (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
|
|
122
|
+
return `${tostring(h)}-__VERSION__`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const BRIDGE_STAMP = computeBridgeStamp();
|
|
126
|
+
|
|
127
|
+
function setSource(scriptInst: Script | LocalScript, source: string): void {
|
|
128
|
+
// ScriptEditorService is the cleaner API and integrates with Studio's
|
|
129
|
+
// edit history; fall back to direct Source mutation (allowed in plugin
|
|
130
|
+
// context with PluginSecurity) if the edit service rejects the call.
|
|
131
|
+
const [seOk] = pcall(() => {
|
|
132
|
+
ScriptEditorService.UpdateSourceAsync(scriptInst, () => source);
|
|
133
|
+
});
|
|
134
|
+
if (!seOk) {
|
|
135
|
+
(scriptInst as unknown as { Source: string }).Source = source;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface InstallResult {
|
|
140
|
+
installed: boolean;
|
|
141
|
+
error?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function findLegacyEditBridges(): { server?: Instance; client?: Instance } {
|
|
145
|
+
const sps = getStarterPlayerScripts();
|
|
146
|
+
return {
|
|
147
|
+
server: ServerScriptService.FindFirstChild(SERVER_SCRIPT_NAME),
|
|
148
|
+
client: sps ? sps.FindFirstChild(CLIENT_SCRIPT_NAME) : undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function destroyIfPresent(parent: Instance, name: string): void {
|
|
153
|
+
const existing = parent.FindFirstChild(name);
|
|
154
|
+
if (existing) {
|
|
155
|
+
pcall(() => existing.Destroy());
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function cleanupLegacyEditBridges(): void {
|
|
160
|
+
if (RunService.IsRunning()) return;
|
|
161
|
+
const { server, client } = findLegacyEditBridges();
|
|
162
|
+
if (server) {
|
|
163
|
+
pcall(() => server.Destroy());
|
|
164
|
+
}
|
|
165
|
+
if (client) {
|
|
166
|
+
pcall(() => client.Destroy());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function serverRuntimeBridgeReady(): boolean {
|
|
171
|
+
const scriptInst = ServerScriptService.FindFirstChild(SERVER_SCRIPT_NAME);
|
|
172
|
+
const bindable = ServerScriptService.FindFirstChild(BRIDGE_NAMES.serverLocal);
|
|
173
|
+
return scriptInst !== undefined &&
|
|
174
|
+
scriptInst.GetAttribute(STAMP_ATTR) === BRIDGE_STAMP &&
|
|
175
|
+
bindable !== undefined &&
|
|
176
|
+
bindable.IsA("BindableFunction");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getPlayerScripts(): Instance | undefined {
|
|
180
|
+
const localPlayer = Players.LocalPlayer;
|
|
181
|
+
if (!localPlayer) return undefined;
|
|
182
|
+
let playerScripts = localPlayer.FindFirstChild("PlayerScripts");
|
|
183
|
+
if (!playerScripts) {
|
|
184
|
+
playerScripts = localPlayer.WaitForChild("PlayerScripts", 5);
|
|
185
|
+
}
|
|
186
|
+
return playerScripts;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function clientRuntimeBridgeReady(): boolean {
|
|
190
|
+
const playerScripts = getPlayerScripts();
|
|
191
|
+
if (!playerScripts) return false;
|
|
192
|
+
const scriptInst = playerScripts.FindFirstChild(CLIENT_SCRIPT_NAME);
|
|
193
|
+
const bindable = ReplicatedStorage.FindFirstChild(BRIDGE_NAMES.clientLocal);
|
|
194
|
+
return scriptInst !== undefined &&
|
|
195
|
+
scriptInst.GetAttribute(STAMP_ATTR) === BRIDGE_STAMP &&
|
|
196
|
+
bindable !== undefined &&
|
|
197
|
+
bindable.IsA("BindableFunction");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function installServerRuntimeBridge(): InstallResult {
|
|
201
|
+
if (serverRuntimeBridgeReady()) return { installed: true };
|
|
202
|
+
|
|
203
|
+
const [ok, err] = pcall(() => {
|
|
204
|
+
destroyIfPresent(ServerScriptService, SERVER_SCRIPT_NAME);
|
|
205
|
+
destroyIfPresent(ServerScriptService, BRIDGE_NAMES.serverLocal);
|
|
206
|
+
|
|
207
|
+
const serverScript = new Instance("Script");
|
|
208
|
+
serverScript.Name = SERVER_SCRIPT_NAME;
|
|
209
|
+
serverScript.Archivable = false;
|
|
210
|
+
setSource(serverScript, SERVER_BRIDGE_SOURCE);
|
|
211
|
+
serverScript.SetAttribute(STAMP_ATTR, BRIDGE_STAMP);
|
|
212
|
+
serverScript.Parent = ServerScriptService;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!ok) {
|
|
216
|
+
return { installed: false, error: tostring(err) };
|
|
217
|
+
}
|
|
218
|
+
return { installed: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function installClientRuntimeBridge(): InstallResult {
|
|
222
|
+
if (clientRuntimeBridgeReady()) return { installed: true };
|
|
223
|
+
|
|
224
|
+
const playerScripts = getPlayerScripts();
|
|
225
|
+
if (!playerScripts) {
|
|
226
|
+
return { installed: false, error: "Players.LocalPlayer.PlayerScripts not found - cannot install client eval bridge" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const [ok, err] = pcall(() => {
|
|
230
|
+
destroyIfPresent(playerScripts, CLIENT_SCRIPT_NAME);
|
|
231
|
+
destroyIfPresent(ReplicatedStorage, BRIDGE_NAMES.clientLocal);
|
|
232
|
+
|
|
233
|
+
const clientScript = new Instance("LocalScript");
|
|
234
|
+
clientScript.Name = CLIENT_SCRIPT_NAME;
|
|
235
|
+
clientScript.Archivable = false;
|
|
236
|
+
setSource(clientScript, CLIENT_BRIDGE_SOURCE);
|
|
237
|
+
clientScript.SetAttribute(STAMP_ATTR, BRIDGE_STAMP);
|
|
238
|
+
clientScript.Parent = playerScripts;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!ok) {
|
|
242
|
+
return { installed: false, error: tostring(err) };
|
|
243
|
+
}
|
|
244
|
+
return { installed: true };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function ensureRuntimeBridgeInstalled(): InstallResult {
|
|
248
|
+
if (!RunService.IsRunning()) {
|
|
249
|
+
return { installed: false, error: "Eval bridges are installed only in running play DataModels" };
|
|
250
|
+
}
|
|
251
|
+
if (RunService.IsServer()) {
|
|
252
|
+
return installServerRuntimeBridge();
|
|
253
|
+
}
|
|
254
|
+
return installClientRuntimeBridge();
|
|
255
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HttpService } from "@rbxts/services";
|
|
2
|
+
|
|
3
|
+
interface FailureBody {
|
|
4
|
+
error?: string;
|
|
5
|
+
message?: string;
|
|
6
|
+
missingFields?: unknown;
|
|
7
|
+
request?: unknown;
|
|
8
|
+
existing?: unknown;
|
|
9
|
+
details?: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function encodeForLog(value: unknown): string {
|
|
13
|
+
const [ok, encoded] = pcall(() => HttpService.JSONEncode(value));
|
|
14
|
+
return ok ? encoded : tostring(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatBody(body: string): string {
|
|
18
|
+
if (body === "") return "";
|
|
19
|
+
const [ok, decoded] = pcall(() => HttpService.JSONDecode(body));
|
|
20
|
+
if (ok && typeIs(decoded, "table")) {
|
|
21
|
+
const data = decoded as FailureBody;
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
if (typeIs(data.error, "string") && data.error !== "") parts.push(`error=${data.error}`);
|
|
24
|
+
if (typeIs(data.message, "string") && data.message !== "") parts.push(`message=${data.message}`);
|
|
25
|
+
if (data.missingFields !== undefined) parts.push(`missingFields=${encodeForLog(data.missingFields)}`);
|
|
26
|
+
if (data.request !== undefined) parts.push(`request=${encodeForLog(data.request)}`);
|
|
27
|
+
if (data.existing !== undefined) parts.push(`existing=${encodeForLog(data.existing)}`);
|
|
28
|
+
if (data.details !== undefined) parts.push(`details=${encodeForLog(data.details)}`);
|
|
29
|
+
if (parts.size() > 0) return parts.join(" ");
|
|
30
|
+
}
|
|
31
|
+
return `body=${body}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatRequestFailure(url: string, ok: boolean, res: unknown): string {
|
|
35
|
+
if (!ok) {
|
|
36
|
+
return `RequestAsync threw for ${url}: ${tostring(res)}`;
|
|
37
|
+
}
|
|
38
|
+
if (res === undefined) {
|
|
39
|
+
return `RequestAsync returned no response for ${url}`;
|
|
40
|
+
}
|
|
41
|
+
const response = res as RequestAsyncResponse;
|
|
42
|
+
const statusMessage = response.StatusMessage !== "" ? ` ${response.StatusMessage}` : "";
|
|
43
|
+
const body = formatBody(response.Body);
|
|
44
|
+
const suffix = body !== "" ? `: ${body}` : "";
|
|
45
|
+
return `HTTP ${response.StatusCode}${statusMessage} from ${url}${suffix}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export = {
|
|
49
|
+
formatRequestFailure,
|
|
50
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Async-job registry for long-running execute_luau. The poll loop already runs
|
|
2
|
+
// each request in its own task.spawn coroutine, and plugin modules are
|
|
3
|
+
// singletons that persist across poll cycles — so a job started by
|
|
4
|
+
// /api/execute-luau-async can keep running while later /api/get-job-status polls
|
|
5
|
+
// read its state here. This removes the false-timeout class: every individual
|
|
6
|
+
// MCP call returns fast; the heavy work happens between polls.
|
|
7
|
+
|
|
8
|
+
import { Job } from "../types";
|
|
9
|
+
|
|
10
|
+
const jobs = new Map<string, Job>();
|
|
11
|
+
const MAX_JOBS = 50;
|
|
12
|
+
|
|
13
|
+
// Maps a running coroutine to its job id, so the global _G.__mcp helpers can find
|
|
14
|
+
// "which job am I in" via coroutine.running() — concurrency-safe across jobs.
|
|
15
|
+
const threadJobs = new Map<thread, string>();
|
|
16
|
+
|
|
17
|
+
// Keep the registry bounded: once it grows past MAX_JOBS, drop the oldest
|
|
18
|
+
// finished jobs (never running ones).
|
|
19
|
+
function prune(): void {
|
|
20
|
+
if (jobs.size() <= MAX_JOBS) return;
|
|
21
|
+
const finished: Job[] = [];
|
|
22
|
+
for (const [, j] of jobs) {
|
|
23
|
+
if (j.status !== "running") finished.push(j);
|
|
24
|
+
}
|
|
25
|
+
finished.sort((a, b) => (a.finishedAt ?? 0) < (b.finishedAt ?? 0));
|
|
26
|
+
let toRemove = jobs.size() - MAX_JOBS;
|
|
27
|
+
for (const j of finished) {
|
|
28
|
+
if (toRemove <= 0) break;
|
|
29
|
+
jobs.delete(j.id);
|
|
30
|
+
toRemove--;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function create(): Job {
|
|
35
|
+
const job: Job = {
|
|
36
|
+
id: game.GetService("HttpService").GenerateGUID(false),
|
|
37
|
+
status: "running",
|
|
38
|
+
startedAt: tick(),
|
|
39
|
+
};
|
|
40
|
+
jobs.set(job.id, job);
|
|
41
|
+
prune();
|
|
42
|
+
return job;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function get(id: string): Job | undefined {
|
|
46
|
+
return jobs.get(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bindThread(co: thread, jobId: string): void {
|
|
50
|
+
threadJobs.set(co, jobId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unbindThread(co: thread): void {
|
|
54
|
+
threadJobs.delete(co);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Called by _G.__mcp.progress(done, total, msg) from server-generated code.
|
|
58
|
+
function reportProgress(co: thread, done: number, total?: number, message?: string, stage?: string): void {
|
|
59
|
+
const jobId = threadJobs.get(co);
|
|
60
|
+
if (jobId === undefined) return;
|
|
61
|
+
const job = jobs.get(jobId);
|
|
62
|
+
if (!job || job.status !== "running") return;
|
|
63
|
+
job.progress = done;
|
|
64
|
+
if (total !== undefined) job.total = total;
|
|
65
|
+
if (message !== undefined) job.message = message;
|
|
66
|
+
if (stage !== undefined) job.stage = stage;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Called by _G.__mcp.checkCancelled() so long server-generated loops can bail early.
|
|
70
|
+
function isCancelledForThread(co: thread): boolean {
|
|
71
|
+
const jobId = threadJobs.get(co);
|
|
72
|
+
if (jobId === undefined) return false;
|
|
73
|
+
const job = jobs.get(jobId);
|
|
74
|
+
return job?.cancelled === true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export = { create, get, bindThread, unbindThread, reportProgress, isCancelledForThread };
|