@chrrxs/robloxstudio-mcp 2.23.1 → 3.0.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.
- package/dist/index.js +1688 -4547
- package/package.json +7 -5
- package/studio-plugin/MCPPlugin.rbxmx +489 -1965
- package/studio-plugin/.Carbon.rbxm.lock +0 -0
- package/studio-plugin/Carbon.rbxm +0 -0
- package/studio-plugin/INSTALLATION.md +0 -170
- package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -171527
- package/studio-plugin/default.project.json +0 -19
- package/studio-plugin/dev.project.json +0 -23
- package/studio-plugin/include/LibMP.lua +0 -156378
- package/studio-plugin/inspector-icon.png +0 -0
- package/studio-plugin/package-lock.json +0 -706
- package/studio-plugin/package.json +0 -19
- package/studio-plugin/plugin.json +0 -10
- package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
- package/studio-plugin/src/modules/ClientBroker.ts +0 -450
- package/studio-plugin/src/modules/Communication.ts +0 -632
- package/studio-plugin/src/modules/EvalBridges.ts +0 -255
- package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
- package/studio-plugin/src/modules/LuauExec.ts +0 -403
- package/studio-plugin/src/modules/Recording.ts +0 -28
- package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
- package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
- package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
- package/studio-plugin/src/modules/State.ts +0 -39
- package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
- package/studio-plugin/src/modules/UI.ts +0 -597
- package/studio-plugin/src/modules/Utils.ts +0 -469
- package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
- package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
- package/studio-plugin/src/modules/handlers/BuildHandlers.ts +0 -481
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
- package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
- package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
- package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
- package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +0 -380
- package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
- package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
- package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -354
- package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
- package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -191
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -874
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -530
- package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
- package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
- package/studio-plugin/src/server/index.server.ts +0 -135
- package/studio-plugin/src/types/index.d.ts +0 -57
- package/studio-plugin/tsconfig.json +0 -20
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { LogService, ReplicatedStorage, RunService, ServerScriptService } from "@rbxts/services";
|
|
2
|
-
import { BRIDGE_NAMES, ensureRuntimeBridgeInstalled } from "../EvalBridges";
|
|
3
|
-
import LuauExec from "../LuauExec";
|
|
4
|
-
|
|
5
|
-
const PAYLOAD_INSTANCE_NAME = "__MCPEvalPayload";
|
|
6
|
-
|
|
7
|
-
interface BridgeInvokeResult {
|
|
8
|
-
ok?: boolean;
|
|
9
|
-
value?: unknown;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface WrapperResult {
|
|
13
|
-
ok?: boolean;
|
|
14
|
-
value?: unknown;
|
|
15
|
-
output?: unknown;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function findBridge(config: { service: Instance; bridgeName: string }): BindableFunction | undefined {
|
|
19
|
-
const bridge = config.service.FindFirstChild(config.bridgeName);
|
|
20
|
-
return bridge && bridge.IsA("BindableFunction") ? bridge : undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function waitForBridge(config: { service: Instance; bridgeName: string }, timeoutSec = 2): BindableFunction | undefined {
|
|
24
|
-
const deadline = tick() + timeoutSec;
|
|
25
|
-
let bridge = findBridge(config);
|
|
26
|
-
while (!bridge && tick() < deadline) {
|
|
27
|
-
task.wait(0.05);
|
|
28
|
-
bridge = findBridge(config);
|
|
29
|
-
}
|
|
30
|
-
return bridge;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getBridgeConfig() {
|
|
34
|
-
if (!RunService.IsRunning()) {
|
|
35
|
-
return {
|
|
36
|
-
error: "eval_*_runtime requires a running playtest.",
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
if (RunService.IsServer()) {
|
|
40
|
-
return {
|
|
41
|
-
service: ServerScriptService,
|
|
42
|
-
bridgeName: BRIDGE_NAMES.serverLocal,
|
|
43
|
-
missingError: "ServerEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime server peer, including for manually-started playtests.",
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
service: ReplicatedStorage,
|
|
48
|
-
bridgeName: BRIDGE_NAMES.clientLocal,
|
|
49
|
-
missingError: "ClientEvalBridge not found. The bridge runs inside the play DM, so a playtest must be running. The bridge installs automatically in the runtime client peer, including for manually-started playtests.",
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function evalRuntime(requestData: Record<string, unknown>) {
|
|
54
|
-
const code = requestData.code as string;
|
|
55
|
-
if (!code || code === "") return { error: "Code is required" };
|
|
56
|
-
|
|
57
|
-
const config = getBridgeConfig();
|
|
58
|
-
if (config.error !== undefined) {
|
|
59
|
-
return { bridge: "missing", error: config.error };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let bridge = findBridge(config);
|
|
63
|
-
if (!bridge) {
|
|
64
|
-
const install = ensureRuntimeBridgeInstalled();
|
|
65
|
-
if (!install.installed) {
|
|
66
|
-
return {
|
|
67
|
-
bridge: "missing",
|
|
68
|
-
error: `${config.missingError} Runtime bridge install failed: ${install.error}`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
bridge = waitForBridge(config);
|
|
72
|
-
}
|
|
73
|
-
if (!bridge) {
|
|
74
|
-
return {
|
|
75
|
-
bridge: "missing",
|
|
76
|
-
error: `${config.missingError} Runtime bridge was installed but did not become ready.`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const m = new Instance("ModuleScript");
|
|
81
|
-
m.Name = PAYLOAD_INSTANCE_NAME;
|
|
82
|
-
const userLines = LuauExec.countLines(code);
|
|
83
|
-
const wrapped = LuauExec.buildWrapper(code, PAYLOAD_INSTANCE_NAME);
|
|
84
|
-
|
|
85
|
-
const [okSet, setErr] = pcall(() => {
|
|
86
|
-
(m as unknown as { Source: string }).Source = wrapped;
|
|
87
|
-
});
|
|
88
|
-
if (!okSet) {
|
|
89
|
-
m.Destroy();
|
|
90
|
-
return {
|
|
91
|
-
bridge: "ok",
|
|
92
|
-
ok: false,
|
|
93
|
-
error: `ModuleScript Source set failed: ${tostring(setErr)}`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
m.Parent = game.GetService("Workspace");
|
|
98
|
-
const historyStart = LogService.GetLogHistory().size();
|
|
99
|
-
const [invokeOk, invokeResult] = pcall(() => bridge.Invoke(m) as BridgeInvokeResult);
|
|
100
|
-
m.Destroy();
|
|
101
|
-
|
|
102
|
-
if (!invokeOk) {
|
|
103
|
-
return {
|
|
104
|
-
bridge: "ok",
|
|
105
|
-
ok: false,
|
|
106
|
-
error: tostring(invokeResult),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!typeIs(invokeResult, "table")) {
|
|
111
|
-
return {
|
|
112
|
-
bridge: "ok",
|
|
113
|
-
ok: false,
|
|
114
|
-
error: `Eval bridge returned invalid result: ${tostring(invokeResult)}`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const bridgeResult = invokeResult as BridgeInvokeResult;
|
|
119
|
-
if (bridgeResult.ok !== true) {
|
|
120
|
-
return {
|
|
121
|
-
bridge: "ok",
|
|
122
|
-
ok: false,
|
|
123
|
-
error: LuauExec.recoverPayloadRequireError(bridgeResult.value, userLines, PAYLOAD_INSTANCE_NAME, historyStart),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const inner = bridgeResult.value;
|
|
128
|
-
if (!typeIs(inner, "table")) {
|
|
129
|
-
return {
|
|
130
|
-
bridge: "ok",
|
|
131
|
-
ok: true,
|
|
132
|
-
result: inner === undefined ? undefined : LuauExec.formatReturnValue(inner),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const r = inner as WrapperResult;
|
|
137
|
-
const ok = r.ok === true;
|
|
138
|
-
return {
|
|
139
|
-
bridge: "ok",
|
|
140
|
-
ok,
|
|
141
|
-
result: ok && r.value !== undefined ? LuauExec.formatReturnValue(r.value) : undefined,
|
|
142
|
-
error: !ok ? tostring(r.value) : undefined,
|
|
143
|
-
output: r.output ?? [],
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export = {
|
|
148
|
-
evalRuntime,
|
|
149
|
-
};
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import Utils from "../Utils";
|
|
2
|
-
import Recording from "../Recording";
|
|
3
|
-
|
|
4
|
-
const GenerationService = game.GetService("GenerationService");
|
|
5
|
-
const HttpService = game.GetService("HttpService");
|
|
6
|
-
const ServerStorage = game.GetService("ServerStorage");
|
|
7
|
-
const Selection = game.GetService("Selection");
|
|
8
|
-
|
|
9
|
-
const { getInstancePath } = Utils;
|
|
10
|
-
const { beginRecording, finishRecording } = Recording;
|
|
11
|
-
|
|
12
|
-
const OUTPUT_FOLDER_NAME = "__MCPGeneratedModels";
|
|
13
|
-
const GENERATE_MODEL_MODERATION_RETRIES = 2;
|
|
14
|
-
|
|
15
|
-
type AssetImageInput = {
|
|
16
|
-
kind: "asset";
|
|
17
|
-
asset_id: number;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function fail(message: string) {
|
|
21
|
-
return { success: false, error: message };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function contentFromAssetId(assetId: number): Content {
|
|
25
|
-
return Content.fromUri(`rbxassetid://${assetId}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function imageToContent(image: unknown): Content {
|
|
29
|
-
const payload = image as Partial<AssetImageInput>;
|
|
30
|
-
if (payload.kind !== "asset" || !typeIs(payload.asset_id, "number") || payload.asset_id <= 0) {
|
|
31
|
-
error("generate_model image input must be an asset-backed Roblox image ID", 0);
|
|
32
|
-
}
|
|
33
|
-
return contentFromAssetId(math.floor(payload.asset_id));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function outputFolder(): Folder | string {
|
|
37
|
-
const existing = ServerStorage.FindFirstChild(OUTPUT_FOLDER_NAME);
|
|
38
|
-
if (existing !== undefined) {
|
|
39
|
-
if (existing.IsA("Folder")) return existing;
|
|
40
|
-
return `game.ServerStorage.${OUTPUT_FOLDER_NAME} already exists and is not a Folder`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const folder = new Instance("Folder");
|
|
44
|
-
folder.Name = OUTPUT_FOLDER_NAME;
|
|
45
|
-
folder.Parent = ServerStorage;
|
|
46
|
-
return folder;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function sanitizeName(value: unknown): string {
|
|
50
|
-
const raw = typeIs(value, "string") && value !== "" ? value : "GeneratedModel";
|
|
51
|
-
let name = raw.gsub("[%c]", " ")[0];
|
|
52
|
-
name = name.gsub("^%s+", "")[0].gsub("%s+$", "")[0];
|
|
53
|
-
if (name === "") name = "GeneratedModel";
|
|
54
|
-
if (name.size() > 80) name = name.sub(1, 80);
|
|
55
|
-
return name;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function uniqueName(parent: Instance, baseName: string): string {
|
|
59
|
-
if (parent.FindFirstChild(baseName) === undefined) return baseName;
|
|
60
|
-
for (let i = 2; i <= 999; i++) {
|
|
61
|
-
const candidate = `${baseName}_${i}`;
|
|
62
|
-
if (parent.FindFirstChild(candidate) === undefined) return candidate;
|
|
63
|
-
}
|
|
64
|
-
return `${baseName}_${HttpService.GenerateGUID(false)}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function buildInputs(requestData: Record<string, unknown>): Record<string, unknown> {
|
|
68
|
-
const inputs: Record<string, unknown> = {};
|
|
69
|
-
const prompt = requestData.prompt;
|
|
70
|
-
if (typeIs(prompt, "string") && prompt !== "") {
|
|
71
|
-
inputs.TextPrompt = prompt;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const image = requestData.image;
|
|
75
|
-
if (image !== undefined) {
|
|
76
|
-
inputs.Image = imageToContent(image);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const size = requestData.size as { x?: number; y?: number; z?: number } | undefined;
|
|
80
|
-
if (size !== undefined) {
|
|
81
|
-
inputs.Size = new Vector3(size.x ?? 1, size.y ?? 1, size.z ?? 1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const maxTriangles = requestData.max_triangles;
|
|
85
|
-
if (typeIs(maxTriangles, "number")) {
|
|
86
|
-
inputs.MaxTriangles = math.floor(maxTriangles);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const generateTextures = requestData.generate_textures;
|
|
90
|
-
if (typeIs(generateTextures, "boolean")) {
|
|
91
|
-
inputs.GenerateTextures = generateTextures;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return inputs;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function buildSchema(requestData: Record<string, unknown>): Record<string, unknown> {
|
|
98
|
-
const schemaGroups = requestData.schema_groups as string[] | undefined;
|
|
99
|
-
if (schemaGroups !== undefined) {
|
|
100
|
-
return { SchemaDefinition: { Groups: schemaGroups } };
|
|
101
|
-
}
|
|
102
|
-
const schema = typeIs(requestData.schema, "string") && requestData.schema !== ""
|
|
103
|
-
? requestData.schema
|
|
104
|
-
: "Body1";
|
|
105
|
-
return { PredefinedSchema: schema };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isModerationFailure(value: unknown): boolean {
|
|
109
|
-
return tostring(value).find("Moderation failed", 1, true)[0] !== undefined;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function generateModel(requestData: Record<string, unknown>) {
|
|
113
|
-
const [inputOk, inputsOrError] = pcall(() => buildInputs(requestData));
|
|
114
|
-
if (!inputOk) return fail(`Failed to prepare model input: ${tostring(inputsOrError)}`);
|
|
115
|
-
|
|
116
|
-
const schema = buildSchema(requestData);
|
|
117
|
-
const recordingId = beginRecording("Generate model");
|
|
118
|
-
let createdModel: Model | undefined;
|
|
119
|
-
let generateOk = false;
|
|
120
|
-
let generateResult: unknown;
|
|
121
|
-
|
|
122
|
-
for (let attempt = 0; attempt <= GENERATE_MODEL_MODERATION_RETRIES; attempt++) {
|
|
123
|
-
const [ok, result] = pcall(() => {
|
|
124
|
-
return GenerationService.GenerateModelAsync(inputsOrError as Record<string, unknown>, schema, {});
|
|
125
|
-
});
|
|
126
|
-
generateOk = ok;
|
|
127
|
-
generateResult = result;
|
|
128
|
-
if (ok || !isModerationFailure(result) || attempt === GENERATE_MODEL_MODERATION_RETRIES) {
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
task.wait(0.25);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!generateOk) {
|
|
135
|
-
finishRecording(recordingId, false);
|
|
136
|
-
if (isModerationFailure(generateResult)) {
|
|
137
|
-
return fail("Moderation failed after 3 attempts.");
|
|
138
|
-
}
|
|
139
|
-
return fail(tostring(generateResult));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!typeIs(generateResult, "Instance") || !generateResult.IsA("Model")) {
|
|
143
|
-
finishRecording(recordingId, false);
|
|
144
|
-
return fail("GenerationService did not return a Model.");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
createdModel = generateResult as Model;
|
|
148
|
-
const folder = outputFolder();
|
|
149
|
-
if (typeIs(folder, "string")) {
|
|
150
|
-
createdModel.Destroy();
|
|
151
|
-
finishRecording(recordingId, false);
|
|
152
|
-
return fail(folder);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
createdModel.Name = uniqueName(folder, sanitizeName(requestData.name));
|
|
156
|
-
createdModel.Parent = folder;
|
|
157
|
-
pcall(() => Selection.Set([createdModel as Instance]));
|
|
158
|
-
finishRecording(recordingId, true);
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
success: true,
|
|
162
|
-
modelPath: getInstancePath(createdModel),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export = {
|
|
167
|
-
generateModel,
|
|
168
|
-
};
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// Virtual input via UserInputService:CreateVirtualInput().
|
|
2
|
-
//
|
|
3
|
-
// We deliberately do NOT use VirtualInputManager:Send*Event — those methods
|
|
4
|
-
// are gated behind RobloxScriptSecurity ("lacking capability RobloxScript")
|
|
5
|
-
// in every context a plugin can reach (edit DM, play server/client DMs), so
|
|
6
|
-
// they silently never worked. CreateVirtualInput() is callable without that
|
|
7
|
-
// capability and drives the REAL input pipeline: SendKey feeds
|
|
8
|
-
// UserInputService.InputBegan/Ended and the control modules (so WASD walks the
|
|
9
|
-
// character at full WalkSpeed with controls intact, no Humanoid hijack),
|
|
10
|
-
// SendMouseButton feeds UIS and activates GUI buttons (and hit-tests against
|
|
11
|
-
// CoreGui), and SendTextInput types into the focused TextBox.
|
|
12
|
-
//
|
|
13
|
-
// Method set on the VirtualInput object (verified live):
|
|
14
|
-
// SendKey(isDown: boolean, keyCode: Enum.KeyCode)
|
|
15
|
-
// SendMouseButton(position: Vector2, inputType: Enum.UserInputType, isDown: boolean)
|
|
16
|
-
// SendTextInput(text: string)
|
|
17
|
-
// There is NO SendMouseMove / SendMouseWheel / SendKeyEvent — so "move" and
|
|
18
|
-
// "scroll" mouse actions are not supported.
|
|
19
|
-
//
|
|
20
|
-
// Coordinate space: SendMouseButton coordinates are viewport pixels matching
|
|
21
|
-
// what capture_screenshot returns (window space, origin at the top-left of the
|
|
22
|
-
// rendered viewport). Pass screenshot pixel coordinates straight through. Note
|
|
23
|
-
// that UserInputService reports input positions in GUI space, which is offset
|
|
24
|
-
// from this by GuiService:GetGuiInset() (~58px on the Y axis) — irrelevant for
|
|
25
|
-
// callers who pick coordinates off a screenshot, which is why we do not
|
|
26
|
-
// translate here.
|
|
27
|
-
|
|
28
|
-
import * as RenderMonitor from "../RenderMonitor";
|
|
29
|
-
|
|
30
|
-
const UserInputService = game.GetService("UserInputService");
|
|
31
|
-
|
|
32
|
-
interface VirtualInput {
|
|
33
|
-
SendKey(isDown: boolean, keyCode: Enum.KeyCode): void;
|
|
34
|
-
SendMouseButton(position: Vector2, inputType: Enum.UserInputType, isDown: boolean): void;
|
|
35
|
-
SendTextInput(text: string): void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// One VirtualInput per plugin VM, reused across calls so that a key held down
|
|
39
|
-
// in one call (action="press") and released in a later call (action="release")
|
|
40
|
-
// share the same input source.
|
|
41
|
-
let cachedVI: VirtualInput | undefined;
|
|
42
|
-
|
|
43
|
-
function getVI(): VirtualInput | undefined {
|
|
44
|
-
if (cachedVI) return cachedVI;
|
|
45
|
-
const [ok, vi] = pcall(() => {
|
|
46
|
-
return (UserInputService as unknown as { CreateVirtualInput(): unknown }).CreateVirtualInput();
|
|
47
|
-
});
|
|
48
|
-
if (ok && vi !== undefined) {
|
|
49
|
-
cachedVI = vi as VirtualInput;
|
|
50
|
-
return cachedVI;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const MOUSE_TYPE_MAP: Record<string, Enum.UserInputType> = {
|
|
56
|
-
Left: Enum.UserInputType.MouseButton1,
|
|
57
|
-
Right: Enum.UserInputType.MouseButton2,
|
|
58
|
-
Middle: Enum.UserInputType.MouseButton3,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
function simulateMouseInput(requestData: Record<string, unknown>) {
|
|
62
|
-
const action = requestData.action as string;
|
|
63
|
-
const x = requestData.x as number | undefined;
|
|
64
|
-
const y = requestData.y as number | undefined;
|
|
65
|
-
const button = (requestData.button as string) ?? "Left";
|
|
66
|
-
|
|
67
|
-
if (!action) return { error: "action is required" };
|
|
68
|
-
if (x === undefined || y === undefined) {
|
|
69
|
-
return { error: "x and y are required" };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Input is silently dropped by the engine when the window isn't rendering
|
|
73
|
-
// (e.g. minimized). Surface that instead of returning a false success.
|
|
74
|
-
const notRendering = RenderMonitor.notRenderingReason();
|
|
75
|
-
if (notRendering !== undefined) return { error: notRendering };
|
|
76
|
-
|
|
77
|
-
const vi = getVI();
|
|
78
|
-
if (!vi) {
|
|
79
|
-
return { error: "UserInputService:CreateVirtualInput() is not available in this context" };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const inputType = MOUSE_TYPE_MAP[button] ?? Enum.UserInputType.MouseButton1;
|
|
83
|
-
const pos = new Vector2(x, y);
|
|
84
|
-
|
|
85
|
-
const [success, err] = pcall(() => {
|
|
86
|
-
if (action === "click") {
|
|
87
|
-
vi.SendMouseButton(pos, inputType, true);
|
|
88
|
-
task.wait(0.05);
|
|
89
|
-
vi.SendMouseButton(pos, inputType, false);
|
|
90
|
-
} else if (action === "mouseDown") {
|
|
91
|
-
vi.SendMouseButton(pos, inputType, true);
|
|
92
|
-
} else if (action === "mouseUp") {
|
|
93
|
-
vi.SendMouseButton(pos, inputType, false);
|
|
94
|
-
} else {
|
|
95
|
-
error(
|
|
96
|
-
`Unsupported action "${action}". CreateVirtualInput supports click, mouseDown, mouseUp ` +
|
|
97
|
-
`(no move/scroll — those methods don't exist on VirtualInput).`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (success) {
|
|
103
|
-
return { success: true, action, x, y, button };
|
|
104
|
-
}
|
|
105
|
-
return { error: `Failed to simulate mouse input: ${err}` };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function simulateKeyboardInput(requestData: Record<string, unknown>) {
|
|
109
|
-
const notRendering = RenderMonitor.notRenderingReason();
|
|
110
|
-
if (notRendering !== undefined) return { error: notRendering };
|
|
111
|
-
|
|
112
|
-
const vi = getVI();
|
|
113
|
-
if (!vi) {
|
|
114
|
-
return { error: "UserInputService:CreateVirtualInput() is not available in this context" };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Text mode: type a string into the focused TextBox.
|
|
118
|
-
const text = requestData.text as string | undefined;
|
|
119
|
-
if (text !== undefined) {
|
|
120
|
-
const [ok, err] = pcall(() => vi.SendTextInput(text));
|
|
121
|
-
if (ok) return { success: true, text };
|
|
122
|
-
return { error: `Failed to send text input: ${err}` };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const keyCodeName = requestData.keyCode as string;
|
|
126
|
-
if (!keyCodeName) return { error: "keyCode (or text) is required" };
|
|
127
|
-
|
|
128
|
-
const action = (requestData.action as string) ?? "tap";
|
|
129
|
-
const duration = (requestData.duration as number) ?? 0.1;
|
|
130
|
-
|
|
131
|
-
const [enumOk, keyCode] = pcall(() => {
|
|
132
|
-
return (Enum.KeyCode as unknown as Record<string, Enum.KeyCode>)[keyCodeName];
|
|
133
|
-
});
|
|
134
|
-
if (!enumOk || !keyCode) {
|
|
135
|
-
return {
|
|
136
|
-
error: `Unknown keyCode: ${keyCodeName}. Use Enum.KeyCode names like "W", "Space", "E", "LeftShift", etc.`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const [success, err] = pcall(() => {
|
|
141
|
-
if (action === "press") {
|
|
142
|
-
vi.SendKey(true, keyCode);
|
|
143
|
-
} else if (action === "release") {
|
|
144
|
-
vi.SendKey(false, keyCode);
|
|
145
|
-
} else if (action === "tap") {
|
|
146
|
-
vi.SendKey(true, keyCode);
|
|
147
|
-
task.wait(duration);
|
|
148
|
-
vi.SendKey(false, keyCode);
|
|
149
|
-
} else {
|
|
150
|
-
error(`Unknown action: ${action}`);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (success) {
|
|
155
|
-
return { success: true, keyCode: keyCodeName, action };
|
|
156
|
-
}
|
|
157
|
-
return { error: `Failed to simulate keyboard input: ${err}` };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export = {
|
|
161
|
-
simulateMouseInput,
|
|
162
|
-
simulateKeyboardInput,
|
|
163
|
-
};
|