@princeofscale/bloxforge 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 +19391 -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,697 @@
|
|
|
1
|
+
import { HttpService, RunService, ServerStorage } from "@rbxts/services";
|
|
2
|
+
import State from "./State";
|
|
3
|
+
import Utils from "./Utils";
|
|
4
|
+
import UI from "./UI";
|
|
5
|
+
import { cleanupLegacyEditBridges } from "./EvalBridges";
|
|
6
|
+
import QueryHandlers from "./handlers/QueryHandlers";
|
|
7
|
+
import PropertyHandlers from "./handlers/PropertyHandlers";
|
|
8
|
+
import InstanceHandlers from "./handlers/InstanceHandlers";
|
|
9
|
+
import ScriptHandlers from "./handlers/ScriptHandlers";
|
|
10
|
+
import MetadataHandlers from "./handlers/MetadataHandlers";
|
|
11
|
+
import TestHandlers from "./handlers/TestHandlers";
|
|
12
|
+
import BuildHandlers from "./handlers/BuildHandlers";
|
|
13
|
+
import AssetHandlers from "./handlers/AssetHandlers";
|
|
14
|
+
import CaptureHandlers from "./handlers/CaptureHandlers";
|
|
15
|
+
import InputHandlers from "./handlers/InputHandlers";
|
|
16
|
+
import LogHandlers from "./handlers/LogHandlers";
|
|
17
|
+
import SerializationHandlers from "./handlers/SerializationHandlers";
|
|
18
|
+
import MemoryHandlers from "./handlers/MemoryHandlers";
|
|
19
|
+
import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
|
|
20
|
+
import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
|
|
21
|
+
import BreakpointHandlers from "./handlers/BreakpointHandlers";
|
|
22
|
+
import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
|
|
23
|
+
import MicroProfilerHandlers from "./handlers/MicroProfilerHandlers";
|
|
24
|
+
import JobHandlers from "./handlers/JobHandlers";
|
|
25
|
+
import ClientBroker from "./ClientBroker";
|
|
26
|
+
import ServerUrlSettings from "./ServerUrlSettings";
|
|
27
|
+
import HttpDiagnostics from "./HttpDiagnostics";
|
|
28
|
+
import { Connection, RequestPayload, PollResponse, ReadyResponse } from "../types";
|
|
29
|
+
|
|
30
|
+
// Per-plugin-load random GUID. Used as the /poll URL param so the server
|
|
31
|
+
// can tell our polls apart from any other plugin's polls. Not user-facing —
|
|
32
|
+
// MCP tools and the LLM operate on instanceId (the place identifier).
|
|
33
|
+
const pluginSessionId = HttpService.GenerateGUID(false);
|
|
34
|
+
|
|
35
|
+
// Place-level identifier shared by every plugin running in DataModels of
|
|
36
|
+
// the same place file (edit DM + playtest server DM + playtest clients).
|
|
37
|
+
// Format: "place:<PlaceId>" when published, "anon:<UUID>" for unpublished
|
|
38
|
+
// places where the UUID lives on ServerStorage's __MCPPlaceId attribute
|
|
39
|
+
// and travels with the .rbxl.
|
|
40
|
+
const MCP_PLACE_ID_ATTRIBUTE = "__MCPPlaceId";
|
|
41
|
+
|
|
42
|
+
function computeInstanceId(): string {
|
|
43
|
+
if (game.PlaceId !== 0) {
|
|
44
|
+
return `place:${tostring(game.PlaceId)}`;
|
|
45
|
+
}
|
|
46
|
+
const existing = ServerStorage.GetAttribute(MCP_PLACE_ID_ATTRIBUTE);
|
|
47
|
+
if (typeIs(existing, "string") && existing !== "") {
|
|
48
|
+
return `anon:${existing as string}`;
|
|
49
|
+
}
|
|
50
|
+
const fresh = HttpService.GenerateGUID(false);
|
|
51
|
+
pcall(() => ServerStorage.SetAttribute(MCP_PLACE_ID_ATTRIBUTE, fresh));
|
|
52
|
+
return `anon:${fresh}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let assignedRole: string | undefined;
|
|
56
|
+
let duplicateInstanceRole = false;
|
|
57
|
+
let hasVersionMismatch = false;
|
|
58
|
+
let lastVersionMismatchWarningKey: string | undefined;
|
|
59
|
+
let lastReadyInstanceId: string | undefined;
|
|
60
|
+
const readyFailureLogKeys = new Set<string>();
|
|
61
|
+
|
|
62
|
+
// Cache the published place name from MarketplaceService:GetProductInfo so
|
|
63
|
+
// /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
|
|
64
|
+
// from game.Name (the DataModel name, often "Place1" in edit). We only fetch
|
|
65
|
+
// once per plugin load; the published name doesn't change mid-session.
|
|
66
|
+
let cachedPlaceName: string | undefined;
|
|
67
|
+
let cachedPlaceNamePlaceId: number | undefined;
|
|
68
|
+
|
|
69
|
+
function resolvePlaceName(): string {
|
|
70
|
+
if (cachedPlaceName !== undefined && cachedPlaceNamePlaceId === game.PlaceId) return cachedPlaceName;
|
|
71
|
+
cachedPlaceName = undefined;
|
|
72
|
+
cachedPlaceNamePlaceId = game.PlaceId;
|
|
73
|
+
if (game.PlaceId === 0) {
|
|
74
|
+
cachedPlaceName = game.Name;
|
|
75
|
+
return cachedPlaceName;
|
|
76
|
+
}
|
|
77
|
+
const MarketplaceService = game.GetService("MarketplaceService");
|
|
78
|
+
const [ok, info] = pcall(() => MarketplaceService.GetProductInfo(game.PlaceId));
|
|
79
|
+
if (ok && info !== undefined) {
|
|
80
|
+
const name = (info as { Name?: string }).Name;
|
|
81
|
+
if (typeIs(name, "string") && name !== "") {
|
|
82
|
+
cachedPlaceName = name;
|
|
83
|
+
return cachedPlaceName;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Don't cache failures — could be transient (offline, rate-limited).
|
|
87
|
+
// Next /ready will retry. Return game.Name as fallback.
|
|
88
|
+
return game.Name;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function detectRole(): string {
|
|
92
|
+
if (!RunService.IsRunning()) return "edit";
|
|
93
|
+
if (RunService.IsServer()) return "server";
|
|
94
|
+
return "client";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const initialRole = detectRole();
|
|
98
|
+
|
|
99
|
+
type Handler = (data: Record<string, unknown>) => unknown;
|
|
100
|
+
|
|
101
|
+
const routeMap: Record<string, Handler> = {
|
|
102
|
+
|
|
103
|
+
"/api/file-tree": QueryHandlers.getFileTree,
|
|
104
|
+
"/api/search-files": QueryHandlers.searchFiles,
|
|
105
|
+
"/api/place-info": QueryHandlers.getPlaceInfo,
|
|
106
|
+
"/api/services": QueryHandlers.getServices,
|
|
107
|
+
"/api/search-objects": QueryHandlers.searchObjects,
|
|
108
|
+
"/api/instance-properties": QueryHandlers.getInstanceProperties,
|
|
109
|
+
"/api/instance-children": QueryHandlers.getInstanceChildren,
|
|
110
|
+
"/api/search-by-property": QueryHandlers.searchByProperty,
|
|
111
|
+
"/api/class-info": QueryHandlers.getClassInfo,
|
|
112
|
+
"/api/project-structure": QueryHandlers.getProjectStructure,
|
|
113
|
+
"/api/grep-scripts": QueryHandlers.grepScripts,
|
|
114
|
+
"/api/get-descendants": QueryHandlers.getDescendants,
|
|
115
|
+
"/api/compare-instances": QueryHandlers.compareInstances,
|
|
116
|
+
|
|
117
|
+
"/api/set-property": PropertyHandlers.setProperty,
|
|
118
|
+
"/api/set-properties": PropertyHandlers.setProperties,
|
|
119
|
+
"/api/mass-set-property": PropertyHandlers.massSetProperty,
|
|
120
|
+
"/api/mass-get-property": PropertyHandlers.massGetProperty,
|
|
121
|
+
"/api/create-object": InstanceHandlers.createObject,
|
|
122
|
+
"/api/mass-create-objects": InstanceHandlers.massCreateObjects,
|
|
123
|
+
// Back-compat alias: pre-2.7.0 servers split this endpoint when properties were present.
|
|
124
|
+
"/api/mass-create-objects-with-properties": InstanceHandlers.massCreateObjects,
|
|
125
|
+
"/api/delete-object": InstanceHandlers.deleteObject,
|
|
126
|
+
"/api/smart-duplicate": InstanceHandlers.smartDuplicate,
|
|
127
|
+
"/api/mass-duplicate": InstanceHandlers.massDuplicate,
|
|
128
|
+
"/api/clone-object": InstanceHandlers.cloneObject,
|
|
129
|
+
|
|
130
|
+
"/api/get-script-source": ScriptHandlers.getScriptSource,
|
|
131
|
+
"/api/set-script-source": ScriptHandlers.setScriptSource,
|
|
132
|
+
"/api/edit-script-lines": ScriptHandlers.editScriptLines,
|
|
133
|
+
"/api/insert-script-lines": ScriptHandlers.insertScriptLines,
|
|
134
|
+
"/api/delete-script-lines": ScriptHandlers.deleteScriptLines,
|
|
135
|
+
|
|
136
|
+
"/api/set-attribute": MetadataHandlers.setAttribute,
|
|
137
|
+
"/api/get-attributes": MetadataHandlers.getAttributes,
|
|
138
|
+
"/api/delete-attribute": MetadataHandlers.deleteAttribute,
|
|
139
|
+
"/api/get-tags": MetadataHandlers.getTags,
|
|
140
|
+
"/api/add-tag": MetadataHandlers.addTag,
|
|
141
|
+
"/api/remove-tag": MetadataHandlers.removeTag,
|
|
142
|
+
"/api/get-tagged": MetadataHandlers.getTagged,
|
|
143
|
+
"/api/get-selection": MetadataHandlers.getSelection,
|
|
144
|
+
"/api/execute-luau": MetadataHandlers.executeLuau,
|
|
145
|
+
"/api/execute-luau-async": JobHandlers.executeLuauAsync,
|
|
146
|
+
"/api/get-job-status": JobHandlers.getJobStatus,
|
|
147
|
+
"/api/get-job-result": JobHandlers.getJobResult,
|
|
148
|
+
"/api/cancel-job": JobHandlers.cancelJob,
|
|
149
|
+
"/api/eval-runtime": EvalRuntimeHandlers.evalRuntime,
|
|
150
|
+
"/api/undo": MetadataHandlers.undo,
|
|
151
|
+
"/api/redo": MetadataHandlers.redo,
|
|
152
|
+
"/api/bulk-set-attributes": MetadataHandlers.bulkSetAttributes,
|
|
153
|
+
|
|
154
|
+
"/api/start-playtest": TestHandlers.startPlaytest,
|
|
155
|
+
"/api/stop-playtest": TestHandlers.stopPlaytest,
|
|
156
|
+
"/api/multiplayer-test-start": TestHandlers.multiplayerTestStart,
|
|
157
|
+
"/api/multiplayer-test-state": TestHandlers.multiplayerTestState,
|
|
158
|
+
"/api/multiplayer-test-add-players": TestHandlers.multiplayerTestAddPlayers,
|
|
159
|
+
"/api/multiplayer-test-leave-client": TestHandlers.multiplayerTestLeaveClient,
|
|
160
|
+
"/api/multiplayer-test-end": TestHandlers.multiplayerTestEnd,
|
|
161
|
+
"/api/character-navigation": TestHandlers.characterNavigation,
|
|
162
|
+
|
|
163
|
+
"/api/export-build": BuildHandlers.exportBuild,
|
|
164
|
+
"/api/import-build": BuildHandlers.importBuild,
|
|
165
|
+
"/api/import-scene": BuildHandlers.importScene,
|
|
166
|
+
"/api/search-materials": BuildHandlers.searchMaterials,
|
|
167
|
+
|
|
168
|
+
"/api/insert-asset": AssetHandlers.insertAsset,
|
|
169
|
+
"/api/preview-asset": AssetHandlers.previewAsset,
|
|
170
|
+
|
|
171
|
+
"/api/capture-screenshot": CaptureHandlers.captureScreenshot,
|
|
172
|
+
"/api/capture-begin": CaptureHandlers.captureBegin,
|
|
173
|
+
"/api/capture-read": CaptureHandlers.captureRead,
|
|
174
|
+
"/api/simulate-mouse-input": InputHandlers.simulateMouseInput,
|
|
175
|
+
"/api/simulate-keyboard-input": InputHandlers.simulateKeyboardInput,
|
|
176
|
+
|
|
177
|
+
"/api/find-and-replace-in-scripts": ScriptHandlers.findAndReplaceInScripts,
|
|
178
|
+
|
|
179
|
+
"/api/get-runtime-logs": LogHandlers.getRuntimeLogs,
|
|
180
|
+
"/api/breakpoints": BreakpointHandlers.breakpoints,
|
|
181
|
+
"/api/capture-script-profiler": ScriptProfilerHandlers.captureScriptProfiler,
|
|
182
|
+
"/api/capture-micro-profiler": MicroProfilerHandlers.captureMicroProfiler,
|
|
183
|
+
|
|
184
|
+
"/api/export-rbxm": SerializationHandlers.exportRbxm,
|
|
185
|
+
"/api/import-rbxm": SerializationHandlers.importRbxm,
|
|
186
|
+
|
|
187
|
+
"/api/get-memory-breakdown": MemoryHandlers.getMemoryBreakdown,
|
|
188
|
+
"/api/get-scene-analysis": SceneAnalysisHandlers.getSceneAnalysis,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function processRequest(request: RequestPayload): unknown {
|
|
192
|
+
const endpoint = request.endpoint;
|
|
193
|
+
const data = request.data ?? {};
|
|
194
|
+
|
|
195
|
+
const handler = routeMap[endpoint];
|
|
196
|
+
if (handler) {
|
|
197
|
+
return handler(data as Record<string, unknown>);
|
|
198
|
+
} else {
|
|
199
|
+
return { error: `Unknown endpoint: ${endpoint}` };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function sendResponse(conn: Connection, requestId: string, responseData: unknown) {
|
|
204
|
+
pcall(() => {
|
|
205
|
+
HttpService.RequestAsync({
|
|
206
|
+
Url: `${conn.serverUrl}/response`,
|
|
207
|
+
Method: "POST",
|
|
208
|
+
Headers: { "Content-Type": "application/json" },
|
|
209
|
+
Body: HttpService.JSONEncode({ requestId, response: responseData }),
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getConnectionStatus(connIndex: number): string {
|
|
215
|
+
const conn = State.getConnection(connIndex);
|
|
216
|
+
if (!conn || !conn.isActive) return "disconnected";
|
|
217
|
+
if (conn.consecutiveFailures >= conn.maxFailuresBeforeError) return "error";
|
|
218
|
+
if (conn.lastHttpOk) return "connected";
|
|
219
|
+
return "connecting";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Throttle for re-issuing /ready after the server reports knownInstance=false.
|
|
223
|
+
// Without this, every poll during the brief window where the server has just
|
|
224
|
+
// restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
|
|
225
|
+
let lastReadyPostAt = 0;
|
|
226
|
+
|
|
227
|
+
// game.Name and game.PlaceId can both settle after plugin load. PlaceId also
|
|
228
|
+
// changes when an unpublished file is published while MCP is already active.
|
|
229
|
+
// Re-fire /ready so the bridge can migrate anon:<uuid> to place:<PlaceId>.
|
|
230
|
+
let nameChangeConn: RBXScriptConnection | undefined;
|
|
231
|
+
let placeIdChangeConn: RBXScriptConnection | undefined;
|
|
232
|
+
function ensureIdentityWatcher(conn: Connection): void {
|
|
233
|
+
if (!nameChangeConn) {
|
|
234
|
+
const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("Name"));
|
|
235
|
+
if (okSig && signal) {
|
|
236
|
+
nameChangeConn = signal.Connect(() => {
|
|
237
|
+
// sendReady has its own 2s throttle, so rapid burst changes coalesce.
|
|
238
|
+
sendReady(conn);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!placeIdChangeConn) {
|
|
243
|
+
const [okSig, signal] = pcall(() => game.GetPropertyChangedSignal("PlaceId"));
|
|
244
|
+
if (okSig && signal) {
|
|
245
|
+
placeIdChangeConn = signal.Connect(() => {
|
|
246
|
+
cachedPlaceName = undefined;
|
|
247
|
+
cachedPlaceNamePlaceId = undefined;
|
|
248
|
+
sendReady(conn);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sendReady(conn: Connection): void {
|
|
255
|
+
if (duplicateInstanceRole) return; // stop retrying once the server has rejected us
|
|
256
|
+
const now = tick();
|
|
257
|
+
if (now - lastReadyPostAt < 2) return; // throttle to ≤1 /ready every 2s
|
|
258
|
+
lastReadyPostAt = now;
|
|
259
|
+
const instanceId = computeInstanceId();
|
|
260
|
+
task.spawn(() => {
|
|
261
|
+
const [readyOk, readyResult] = pcall(() => {
|
|
262
|
+
return HttpService.RequestAsync({
|
|
263
|
+
Url: `${conn.serverUrl}/ready`,
|
|
264
|
+
Method: "POST",
|
|
265
|
+
Headers: { "Content-Type": "application/json" },
|
|
266
|
+
Body: HttpService.JSONEncode({
|
|
267
|
+
pluginSessionId,
|
|
268
|
+
instanceId,
|
|
269
|
+
role: detectRole(),
|
|
270
|
+
placeId: game.PlaceId,
|
|
271
|
+
placeName: resolvePlaceName(),
|
|
272
|
+
dataModelName: game.Name,
|
|
273
|
+
isRunning: RunService.IsRunning(),
|
|
274
|
+
pluginVersion: State.CURRENT_VERSION,
|
|
275
|
+
pluginVariant: State.PLUGIN_VARIANT,
|
|
276
|
+
protocolVersion: State.PROTOCOL_VERSION,
|
|
277
|
+
pluginReady: true,
|
|
278
|
+
timestamp: tick(),
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
const readyUrl = `${conn.serverUrl}/ready`;
|
|
283
|
+
const readyRole = detectRole();
|
|
284
|
+
const readyLogKey = `${conn.serverUrl}|${instanceId}|${readyRole}`;
|
|
285
|
+
if (!readyOk) {
|
|
286
|
+
readyFailureLogKeys.add(readyLogKey);
|
|
287
|
+
warn(`[BloxForge] /ready failed for ${instanceId}/${readyRole}: ${HttpDiagnostics.formatRequestFailure(readyUrl, readyOk, readyResult)}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!readyResult.Success) {
|
|
291
|
+
const reason = HttpDiagnostics.formatRequestFailure(readyUrl, true, readyResult);
|
|
292
|
+
readyFailureLogKeys.add(readyLogKey);
|
|
293
|
+
// 409 = duplicate_instance_role. Surface in UI and stop polling.
|
|
294
|
+
if (readyResult.StatusCode === 409) {
|
|
295
|
+
duplicateInstanceRole = true;
|
|
296
|
+
conn.isActive = false;
|
|
297
|
+
const ui = UI.getElements();
|
|
298
|
+
if (State.getActiveTabIndex() === 0) {
|
|
299
|
+
ui.statusLabel.Text = "Duplicate instance";
|
|
300
|
+
ui.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68);
|
|
301
|
+
ui.detailStatusLabel.Text = reason;
|
|
302
|
+
ui.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68);
|
|
303
|
+
}
|
|
304
|
+
warn(`[BloxForge] /ready rejected for ${instanceId}/${readyRole}: ${reason}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
warn(`[BloxForge] /ready rejected for ${instanceId}/${readyRole}: ${reason}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const [parseOk, readyData] = pcall(
|
|
311
|
+
() => HttpService.JSONDecode(readyResult.Body) as ReadyResponse,
|
|
312
|
+
);
|
|
313
|
+
if (parseOk && readyData.assignedRole) {
|
|
314
|
+
assignedRole = readyData.assignedRole;
|
|
315
|
+
}
|
|
316
|
+
lastReadyInstanceId = parseOk && typeIs(readyData.instanceId, "string") && readyData.instanceId !== ""
|
|
317
|
+
? readyData.instanceId
|
|
318
|
+
: instanceId;
|
|
319
|
+
ServerUrlSettings.rememberServerUrl(conn.serverUrl);
|
|
320
|
+
const connectedRole = assignedRole ?? detectRole();
|
|
321
|
+
if (readyFailureLogKeys.has(readyLogKey)) {
|
|
322
|
+
readyFailureLogKeys.delete(readyLogKey);
|
|
323
|
+
print(`[BloxForge] /ready connected for ${instanceId}/${connectedRole} via ${conn.serverUrl}`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function streamUrl(serverUrl: string): string {
|
|
329
|
+
const [websocketUrl] = string.gsub(serverUrl, "^http", "ws");
|
|
330
|
+
return `${websocketUrl}/stream?pluginSessionId=${pluginSessionId}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function sendStreamResponse(conn: Connection, requestId: string, response: unknown) {
|
|
334
|
+
if (!conn.streamOpen || !conn.streamClient) return;
|
|
335
|
+
pcall(() => conn.streamClient!.Send(HttpService.JSONEncode({ type: "response", requestId, response })));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function startRequestStream(conn: Connection) {
|
|
339
|
+
if (!conn.isActive || conn.streamClient) return;
|
|
340
|
+
const [ok, stream] = pcall(() => HttpService.CreateWebStreamClient(
|
|
341
|
+
Enum.WebStreamClientType.WebSocket,
|
|
342
|
+
{ Url: streamUrl(conn.serverUrl), Method: "GET", Headers: { "Content-Type": "application/json" } },
|
|
343
|
+
));
|
|
344
|
+
if (!ok || !stream) return;
|
|
345
|
+
|
|
346
|
+
conn.streamClient = stream;
|
|
347
|
+
stream.Opened.Connect(() => {
|
|
348
|
+
if (conn.streamClient !== stream) return;
|
|
349
|
+
conn.streamOpen = true;
|
|
350
|
+
conn.consecutiveFailures = 0;
|
|
351
|
+
conn.currentRetryDelay = 0.5;
|
|
352
|
+
});
|
|
353
|
+
stream.MessageReceived.Connect((message) => {
|
|
354
|
+
if (conn.streamClient !== stream) return;
|
|
355
|
+
const [parsed, frame] = pcall(() => HttpService.JSONDecode(message) as { type?: string; requestId?: string; request?: RequestPayload });
|
|
356
|
+
if (!parsed || frame.type !== "request" || !frame.requestId || !frame.request) return;
|
|
357
|
+
task.spawn(() => {
|
|
358
|
+
const [handled, response] = pcall(() => processRequest(frame.request!));
|
|
359
|
+
sendStreamResponse(conn, frame.requestId!, handled ? response : { error: tostring(response) });
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
const close = () => {
|
|
363
|
+
if (conn.streamClient !== stream) return;
|
|
364
|
+
conn.streamClient = undefined;
|
|
365
|
+
conn.streamOpen = false;
|
|
366
|
+
conn.nextStreamRetryAt = tick() + 5;
|
|
367
|
+
};
|
|
368
|
+
stream.Closed.Connect(close);
|
|
369
|
+
stream.Error.Connect(close);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function pollForRequests(connIndex: number) {
|
|
373
|
+
const conn = State.getConnection(connIndex);
|
|
374
|
+
if (!conn || !conn.isActive) return;
|
|
375
|
+
if (conn.isPolling) return;
|
|
376
|
+
|
|
377
|
+
conn.isPolling = true;
|
|
378
|
+
|
|
379
|
+
const [success, result] = pcall(() => {
|
|
380
|
+
return HttpService.RequestAsync({
|
|
381
|
+
Url: `${conn.serverUrl}/poll?pluginSessionId=${pluginSessionId}`,
|
|
382
|
+
Method: "GET",
|
|
383
|
+
Headers: { "Content-Type": "application/json" },
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
conn.isPolling = false;
|
|
388
|
+
|
|
389
|
+
const ui = UI.getElements();
|
|
390
|
+
UI.updateTabDot(connIndex);
|
|
391
|
+
if (connIndex === State.getActiveTabIndex()) UI.updateToolbarIcon();
|
|
392
|
+
|
|
393
|
+
if (success && (result.Success || result.StatusCode === 503)) {
|
|
394
|
+
// Recovery acceleration: detect the failing->ok transition. A transient
|
|
395
|
+
// throttling gap (Studio backgrounded/minimized throttles
|
|
396
|
+
// HttpService.RequestAsync) can make the server reap us as stale, so on
|
|
397
|
+
// the first successful poll after failures we re-fire /ready immediately
|
|
398
|
+
// to restore routing instead of waiting for knownInstance=false on a
|
|
399
|
+
// later poll. Throttled by lastReadyPostAt inside sendReady.
|
|
400
|
+
const wasFailing = conn.consecutiveFailures > 0;
|
|
401
|
+
conn.consecutiveFailures = 0;
|
|
402
|
+
conn.currentRetryDelay = 0.5;
|
|
403
|
+
conn.lastSuccessfulConnection = tick();
|
|
404
|
+
|
|
405
|
+
const data = HttpService.JSONDecode(result.Body) as PollResponse;
|
|
406
|
+
const mcpConnected = data.mcpConnected === true;
|
|
407
|
+
conn.lastHttpOk = true;
|
|
408
|
+
conn.lastMcpOk = mcpConnected;
|
|
409
|
+
const serverVersion = data.serverVersion ?? "unknown";
|
|
410
|
+
if (data.versionMismatch === true) {
|
|
411
|
+
hasVersionMismatch = true;
|
|
412
|
+
const warningKey = `${State.CURRENT_VERSION}:${serverVersion}`;
|
|
413
|
+
if (lastVersionMismatchWarningKey !== warningKey) {
|
|
414
|
+
lastVersionMismatchWarningKey = warningKey;
|
|
415
|
+
warn(`[BloxForge] Version mismatch: Studio plugin v${State.CURRENT_VERSION} / MCP v${serverVersion}. Run npx -y @princeofscale/bloxforge@latest --auto-install-plugin and restart Studio.`);
|
|
416
|
+
}
|
|
417
|
+
UI.showBanner("version-mismatch", `Plugin v${State.CURRENT_VERSION} / MCP v${serverVersion} mismatch`);
|
|
418
|
+
} else if (hasVersionMismatch) {
|
|
419
|
+
hasVersionMismatch = false;
|
|
420
|
+
UI.hideBanner("version-mismatch");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Server tells us when its in-memory instances map doesn't have us
|
|
424
|
+
// (e.g. after an MCP process restart, or after it reaped us during a
|
|
425
|
+
// throttling gap). Re-issue /ready immediately so target=server/client-N
|
|
426
|
+
// start routing again. The throttle inside sendReady() prevents
|
|
427
|
+
// duplicate registrations while the server catches up. We also re-fire
|
|
428
|
+
// on the failing->ok transition (wasFailing) even when knownInstance is
|
|
429
|
+
// already true, because the server may still be mid-reap during the
|
|
430
|
+
// poll that returns to health.
|
|
431
|
+
if (data.knownInstance === false || wasFailing) {
|
|
432
|
+
task.spawn(() => sendReady(conn));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (connIndex === State.getActiveTabIndex()) {
|
|
436
|
+
const el = ui;
|
|
437
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
|
|
438
|
+
el.step1Label.Text = "HTTP server (OK)";
|
|
439
|
+
|
|
440
|
+
if (mcpConnected && !el.statusLabel.Text.find("Connected")[0]) {
|
|
441
|
+
el.statusLabel.Text = "Connected";
|
|
442
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94);
|
|
443
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
|
|
444
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
|
|
445
|
+
el.statusText.Text = "ONLINE";
|
|
446
|
+
el.detailStatusLabel.Text = "HTTP: OK MCP: OK";
|
|
447
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94);
|
|
448
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
|
|
449
|
+
el.step2Label.Text = "MCP bridge (OK)";
|
|
450
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94);
|
|
451
|
+
el.step3Label.Text = "Commands (OK)";
|
|
452
|
+
conn.mcpWaitStartTime = undefined;
|
|
453
|
+
el.troubleshootLabel.Visible = false;
|
|
454
|
+
UI.stopPulseAnimation();
|
|
455
|
+
} else if (!mcpConnected) {
|
|
456
|
+
el.statusLabel.Text = "Waiting for MCP server";
|
|
457
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
|
|
458
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
459
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
460
|
+
el.statusText.Text = "WAITING";
|
|
461
|
+
el.detailStatusLabel.Text = "HTTP: OK MCP: ...";
|
|
462
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
|
|
463
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
464
|
+
el.step2Label.Text = "MCP bridge (waiting...)";
|
|
465
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
466
|
+
el.step3Label.Text = "Commands (waiting...)";
|
|
467
|
+
if (conn.mcpWaitStartTime === undefined) {
|
|
468
|
+
conn.mcpWaitStartTime = tick();
|
|
469
|
+
}
|
|
470
|
+
const elapsed = tick() - (conn.mcpWaitStartTime ?? tick());
|
|
471
|
+
el.troubleshootLabel.Visible = elapsed > 8;
|
|
472
|
+
UI.startPulseAnimation();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (data.request && mcpConnected) {
|
|
477
|
+
task.spawn(() => {
|
|
478
|
+
const [ok, response] = pcall(() => processRequest(data.request!));
|
|
479
|
+
if (ok) {
|
|
480
|
+
sendResponse(conn, data.requestId!, response);
|
|
481
|
+
} else {
|
|
482
|
+
sendResponse(conn, data.requestId!, { error: tostring(response) });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
} else if (conn.isActive) {
|
|
487
|
+
conn.consecutiveFailures++;
|
|
488
|
+
|
|
489
|
+
if (conn.consecutiveFailures > 1) {
|
|
490
|
+
conn.currentRetryDelay = math.min(
|
|
491
|
+
conn.currentRetryDelay * conn.retryBackoffMultiplier,
|
|
492
|
+
conn.maxRetryDelay,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (connIndex === State.getActiveTabIndex()) {
|
|
497
|
+
const el = ui;
|
|
498
|
+
if (conn.consecutiveFailures >= conn.maxFailuresBeforeError) {
|
|
499
|
+
el.statusLabel.Text = "Server unavailable";
|
|
500
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68);
|
|
501
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
|
|
502
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
|
|
503
|
+
el.statusText.Text = "ERROR";
|
|
504
|
+
el.detailStatusLabel.Text = "HTTP: X MCP: X";
|
|
505
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68);
|
|
506
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
|
|
507
|
+
el.step1Label.Text = "HTTP server (error)";
|
|
508
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
|
|
509
|
+
el.step2Label.Text = "MCP bridge (error)";
|
|
510
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68);
|
|
511
|
+
el.step3Label.Text = "Commands (error)";
|
|
512
|
+
conn.mcpWaitStartTime = undefined;
|
|
513
|
+
el.troubleshootLabel.Visible = false;
|
|
514
|
+
UI.stopPulseAnimation();
|
|
515
|
+
} else if (conn.consecutiveFailures > 5) {
|
|
516
|
+
const waitTime = math.ceil(conn.currentRetryDelay);
|
|
517
|
+
el.statusLabel.Text = `Retrying (${waitTime}s)`;
|
|
518
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
|
|
519
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
520
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
521
|
+
el.statusText.Text = "RETRY";
|
|
522
|
+
el.detailStatusLabel.Text = "HTTP: ... MCP: ...";
|
|
523
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
|
|
524
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
525
|
+
el.step1Label.Text = "HTTP server (retrying...)";
|
|
526
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
527
|
+
el.step2Label.Text = "MCP bridge (retrying...)";
|
|
528
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
529
|
+
el.step3Label.Text = "Commands (retrying...)";
|
|
530
|
+
conn.mcpWaitStartTime = undefined;
|
|
531
|
+
el.troubleshootLabel.Visible = false;
|
|
532
|
+
UI.startPulseAnimation();
|
|
533
|
+
} else if (conn.consecutiveFailures > 1) {
|
|
534
|
+
el.statusLabel.Text = `Connecting (attempt ${conn.consecutiveFailures})`;
|
|
535
|
+
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
|
|
536
|
+
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
537
|
+
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
538
|
+
el.statusText.Text = "CONNECTING";
|
|
539
|
+
el.detailStatusLabel.Text = "HTTP: ... MCP: ...";
|
|
540
|
+
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11);
|
|
541
|
+
el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
542
|
+
el.step1Label.Text = "HTTP server (connecting...)";
|
|
543
|
+
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
544
|
+
el.step2Label.Text = "MCP bridge (connecting...)";
|
|
545
|
+
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11);
|
|
546
|
+
el.step3Label.Text = "Commands (connecting...)";
|
|
547
|
+
conn.mcpWaitStartTime = undefined;
|
|
548
|
+
el.troubleshootLabel.Visible = false;
|
|
549
|
+
UI.startPulseAnimation();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
function activatePlugin(connIndex?: number) {
|
|
557
|
+
const idx = connIndex ?? State.getActiveTabIndex();
|
|
558
|
+
const conn = State.getConnection(idx);
|
|
559
|
+
if (!conn) return;
|
|
560
|
+
|
|
561
|
+
const ui = UI.getElements();
|
|
562
|
+
|
|
563
|
+
conn.isActive = true;
|
|
564
|
+
conn.consecutiveFailures = 0;
|
|
565
|
+
conn.currentRetryDelay = 0.5;
|
|
566
|
+
|
|
567
|
+
if (idx === State.getActiveTabIndex()) {
|
|
568
|
+
const normalizedUrl = ServerUrlSettings.normalizeServerUrl(ui.urlInput.Text);
|
|
569
|
+
conn.serverUrl = normalizedUrl !== "" ? normalizedUrl : conn.serverUrl;
|
|
570
|
+
if (conn.serverUrl === "") conn.serverUrl = ClientBroker.DEFAULT_MCP_URL;
|
|
571
|
+
ui.urlInput.Text = conn.serverUrl;
|
|
572
|
+
const port = ServerUrlSettings.extractPort(conn.serverUrl);
|
|
573
|
+
if (port !== undefined) conn.port = port;
|
|
574
|
+
UI.updateTabLabel(idx);
|
|
575
|
+
UI.updateUIState();
|
|
576
|
+
}
|
|
577
|
+
UI.updateTabDot(idx);
|
|
578
|
+
|
|
579
|
+
if (!conn.heartbeatConnection) {
|
|
580
|
+
conn.heartbeatConnection = RunService.Heartbeat.Connect(() => {
|
|
581
|
+
const now = tick();
|
|
582
|
+
if (initialRole === "server" && !RunService.IsRunning()) {
|
|
583
|
+
ClientBroker.disconnectAllProxies();
|
|
584
|
+
deactivatePlugin(idx);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const currentInstanceId = computeInstanceId();
|
|
588
|
+
if (lastReadyInstanceId !== undefined && currentInstanceId !== lastReadyInstanceId) {
|
|
589
|
+
cachedPlaceName = undefined;
|
|
590
|
+
cachedPlaceNamePlaceId = undefined;
|
|
591
|
+
sendReady(conn);
|
|
592
|
+
}
|
|
593
|
+
if (!conn.streamClient && now >= conn.nextStreamRetryAt) {
|
|
594
|
+
conn.nextStreamRetryAt = now + 5;
|
|
595
|
+
startRequestStream(conn);
|
|
596
|
+
}
|
|
597
|
+
if (conn.streamOpen && conn.streamClient && now - conn.lastStreamHeartbeat > 15) {
|
|
598
|
+
conn.lastStreamHeartbeat = now;
|
|
599
|
+
pcall(() => conn.streamClient!.Send(HttpService.JSONEncode({ type: "heartbeat" })));
|
|
600
|
+
}
|
|
601
|
+
if (!conn.streamOpen) {
|
|
602
|
+
const currentInterval = conn.consecutiveFailures > 5 ? conn.currentRetryDelay : conn.pollInterval;
|
|
603
|
+
if (now - conn.lastPoll > currentInterval) {
|
|
604
|
+
conn.lastPoll = now;
|
|
605
|
+
pollForRequests(idx);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Initial /ready; pollForRequests will also re-fire ready if the server
|
|
612
|
+
// later reports knownInstance=false (process restart, etc).
|
|
613
|
+
sendReady(conn);
|
|
614
|
+
|
|
615
|
+
// Remove legacy edit-mode eval bridge scripts from older plugin builds.
|
|
616
|
+
// Current bridges are created only in running play DataModels.
|
|
617
|
+
if (!RunService.IsRunning()) {
|
|
618
|
+
task.spawn(cleanupLegacyEditBridges);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Watch identity fields so stale name or anon instance ids are refreshed.
|
|
622
|
+
ensureIdentityWatcher(conn);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function deactivatePlugin(connIndex?: number) {
|
|
626
|
+
const idx = connIndex ?? State.getActiveTabIndex();
|
|
627
|
+
const conn = State.getConnection(idx);
|
|
628
|
+
if (!conn) return;
|
|
629
|
+
|
|
630
|
+
conn.isActive = false;
|
|
631
|
+
conn.lastMcpOk = false;
|
|
632
|
+
|
|
633
|
+
if (idx === State.getActiveTabIndex()) UI.updateUIState();
|
|
634
|
+
UI.updateTabDot(idx);
|
|
635
|
+
|
|
636
|
+
pcall(() => {
|
|
637
|
+
HttpService.RequestAsync({
|
|
638
|
+
Url: `${conn.serverUrl}/disconnect`,
|
|
639
|
+
Method: "POST",
|
|
640
|
+
Headers: { "Content-Type": "application/json" },
|
|
641
|
+
Body: HttpService.JSONEncode({ pluginSessionId, timestamp: tick() }),
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (conn.heartbeatConnection) {
|
|
646
|
+
conn.heartbeatConnection.Disconnect();
|
|
647
|
+
conn.heartbeatConnection = undefined;
|
|
648
|
+
}
|
|
649
|
+
if (conn.streamClient) {
|
|
650
|
+
conn.streamClient.Close();
|
|
651
|
+
conn.streamClient = undefined;
|
|
652
|
+
}
|
|
653
|
+
conn.streamOpen = false;
|
|
654
|
+
|
|
655
|
+
conn.consecutiveFailures = 0;
|
|
656
|
+
conn.currentRetryDelay = 0.5;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function deactivateAll() {
|
|
660
|
+
for (let i = 0; i < State.getConnections().size(); i++) {
|
|
661
|
+
if (State.getConnections()[i].isActive) {
|
|
662
|
+
deactivatePlugin(i);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function checkForUpdates() {
|
|
668
|
+
task.spawn(() => {
|
|
669
|
+
const [success, result] = pcall(() => {
|
|
670
|
+
return HttpService.RequestAsync({
|
|
671
|
+
Url: "https://registry.npmjs.org/@princeofscale/bloxforge/latest",
|
|
672
|
+
Method: "GET",
|
|
673
|
+
Headers: { Accept: "application/json" },
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
if (success && result.Success) {
|
|
678
|
+
const [ok, data] = pcall(() => HttpService.JSONDecode(result.Body) as { version?: string });
|
|
679
|
+
if (ok && data?.version) {
|
|
680
|
+
const latestVersion = data.version;
|
|
681
|
+
if (Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0) {
|
|
682
|
+
if (!hasVersionMismatch) {
|
|
683
|
+
UI.showBanner("update", `v${latestVersion} available - github.com/princeofscale/bloxforge`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export = {
|
|
692
|
+
getConnectionStatus,
|
|
693
|
+
activatePlugin,
|
|
694
|
+
deactivatePlugin,
|
|
695
|
+
deactivateAll,
|
|
696
|
+
checkForUpdates,
|
|
697
|
+
};
|