studio/src/editor/engine/roblox-shim.js
min 9caea93d32
All checks were successful
CI / Lint (pull_request) Successful in 1m6s
CI / Build (pull_request) Successful in 1m58s
CI / Secret scan (pull_request) Successful in 23s
CI / PR size check (pull_request) Successful in 7s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
feat(rbxl-import): single-VM Lua runtime + GUI tree + Touched/click events
ITERATION 6 (single-VM rewrite):
  - RobloxLuaSharedWorker: один wasmoon-state на 742 скрипта (не 742 VM)
  - Pre-populated Workspace + Player.PlayerGui перед addScripts
  - На каждом Part — Touched/TouchEnded сигналы; на каждом TextButton —
    MouseButton1Click/Activated/MouseEnter/Leave; Humanoid с Died/Health
  - Двухфазный init: addScriptsBatch ВСЕ скрипты → kickoff() с PlayerAdded
  - wait()/task.wait/task.spawn/task.delay через scheduler+coroutines
  - guiClick от Rublox-GUI → fireEvent → инстанс.MouseButton1Click.Fire()
  - playerTouch → part.Touched.Fire(HumanoidRootPart) + humanoid.Touched

ITERATION 7 (nullStub compatibility):
  - debug.setmetatable(nil, ...) + debug.setmetatable(function() end, ...)
    с полным набором __index/__newindex/__call/__add/__sub/.../__len/__concat
  - Возврат undefined из FindFirstChild/WaitForChild (вместо JS proxy)
  - Lua-side __null_stub_singleton с Connect/connect/Wait/Fire (lowercase aliases)
  - __rbxl_lookup_part через __rbxl_parts_by_id table (не ipairs на JS array)
  - script.Parent гарантированно не nil (либо реальный Part либо null stub)
  - RbxSignal: Connect+connect, Wait+wait, Fire+fire, Disconnect+disconnect
  - SIGNAL_NAMES whitelist расширен: Tool (Selected/Equipped), Remote (OnInvoke),
    ChatMakeSystemMessage, etc.

Converter:
  - GUI: UDim2 dataclass правильно резолвится (scale*100 + offset/viewport*100)
  - размеры в процентах (как Rublox-GuiOverlay ожидает)
  - ScreenGui.Enabled → пропагируется в детей
  - Эвристика скрыть HD Admin/Chat/CommandBar/TeleportTo модалки
  - rbxasset:// rbxassetid:// фильтруются на пустой URL

Hierarchy:
  - scroll-to-selected раскрывает workspace+rootPrims+folders перед scroll
  - data-sel-id на всех ItemRow с rAF×2 timing

Известные ограничения:
  - Synapse X obfuscated скрипты часто всё равно падают (требуют конкретный Roblox-VM)
  - но debug.setmetatable перехват не даёт скриптам валиться на indexing/arithmetic
  - реальные пользовательские KillBrick (Touched) теперь работают
  - GUI кнопки → MouseButton1Click работают

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 06:18:55 +03:00

716 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* roblox-shim.js — регистрация Roblox API внутри Lua-VM (wasmoon).
*
* Используется из RobloxLuaWorker.js. Регистрирует глобалы:
* - game, workspace, script ← Instance-прокси
* - Vector3, Color3, CFrame, UDim, UDim2 ← конструкторы математических классов
* - Instance.new(class) ← фабрика
* - wait, task, tick, os, print, warn ← стандартные глобалы
* - Enum ← enum-таблица
*
* Архитектура:
* - JS-классы (RbxVector3, RbxCFrame, ...) — обычные дата-объекты с
* перегруженными методами.
* - Instance — прокси-объект который хранит { className, properties, children, parent }.
* Геттеры/сеттеры эмулируются через __index/__newindex (mt в wasmoon).
* - RBXScriptSignal — JS-объект с Connect/Wait/Disconnect.
*
* Sandbox-side: при изменении Part.Position и т.п. отсылаем в main thread
* `partSet` → main применит к Babylon-сцене.
*/
/* ──────── Math classes ──────── */
class RbxVector3 {
constructor(x, y, z) {
this.X = +x || 0;
this.Y = +y || 0;
this.Z = +z || 0;
}
get Magnitude() {
return Math.sqrt(this.X*this.X + this.Y*this.Y + this.Z*this.Z);
}
get Unit() {
const m = this.Magnitude || 1;
return new RbxVector3(this.X / m, this.Y / m, this.Z / m);
}
Dot(o) { return this.X*o.X + this.Y*o.Y + this.Z*o.Z; }
Cross(o) {
return new RbxVector3(
this.Y*o.Z - this.Z*o.Y,
this.Z*o.X - this.X*o.Z,
this.X*o.Y - this.Y*o.X,
);
}
Lerp(o, alpha) {
return new RbxVector3(
this.X + (o.X - this.X) * alpha,
this.Y + (o.Y - this.Y) * alpha,
this.Z + (o.Z - this.Z) * alpha,
);
}
add(o) { return new RbxVector3(this.X + o.X, this.Y + o.Y, this.Z + o.Z); }
sub(o) { return new RbxVector3(this.X - o.X, this.Y - o.Y, this.Z - o.Z); }
mul(scalar) {
if (typeof scalar === 'number') {
return new RbxVector3(this.X * scalar, this.Y * scalar, this.Z * scalar);
}
return new RbxVector3(this.X * scalar.X, this.Y * scalar.Y, this.Z * scalar.Z);
}
toString() { return `${this.X}, ${this.Y}, ${this.Z}`; }
}
class RbxColor3 {
constructor(r, g, b) {
this.R = +r || 0;
this.G = +g || 0;
this.B = +b || 0;
}
static fromRGB(r, g, b) {
return new RbxColor3((r||0)/255, (g||0)/255, (b||0)/255);
}
static fromHex(hex) {
const h = String(hex || '#000000').replace('#','');
return new RbxColor3(
parseInt(h.slice(0,2), 16)/255,
parseInt(h.slice(2,4), 16)/255,
parseInt(h.slice(4,6), 16)/255,
);
}
Lerp(o, alpha) {
return new RbxColor3(
this.R + (o.R - this.R) * alpha,
this.G + (o.G - this.G) * alpha,
this.B + (o.B - this.B) * alpha,
);
}
toHex() {
const h = (n) => Math.max(0, Math.min(255, Math.round(n * 255))).toString(16).padStart(2, '0');
return `#${h(this.R)}${h(this.G)}${h(this.B)}`;
}
toString() { return `${this.R}, ${this.G}, ${this.B}`; }
}
class RbxCFrame {
constructor(x, y, z, r00=1, r01=0, r02=0, r10=0, r11=1, r12=0, r20=0, r21=0, r22=1) {
this.X = +x || 0; this.Y = +y || 0; this.Z = +z || 0;
// Row-major 3x3
this.r00 = r00; this.r01 = r01; this.r02 = r02;
this.r10 = r10; this.r11 = r11; this.r12 = r12;
this.r20 = r20; this.r21 = r21; this.r22 = r22;
}
static new(x, y, z) {
if (x instanceof RbxVector3) return new RbxCFrame(x.X, x.Y, x.Z);
return new RbxCFrame(x || 0, y || 0, z || 0);
}
static Angles(rx, ry, rz) {
// Euler XYZ → 3x3 (intrinsic)
const cx = Math.cos(rx), sx = Math.sin(rx);
const cy = Math.cos(ry), sy = Math.sin(ry);
const cz = Math.cos(rz), sz = Math.sin(rz);
// R = Rx * Ry * Rz
const r00 = cy*cz, r01 = -cy*sz, r02 = sy;
const r10 = sx*sy*cz + cx*sz, r11 = -sx*sy*sz + cx*cz, r12 = -sx*cy;
const r20 = -cx*sy*cz + sx*sz, r21 = cx*sy*sz + sx*cz, r22 = cx*cy;
return new RbxCFrame(0, 0, 0, r00, r01, r02, r10, r11, r12, r20, r21, r22);
}
static fromEulerAnglesXYZ(rx, ry, rz) { return RbxCFrame.Angles(rx, ry, rz); }
get Position() { return new RbxVector3(this.X, this.Y, this.Z); }
get LookVector() { return new RbxVector3(-this.r02, -this.r12, -this.r22); }
get RightVector() { return new RbxVector3(this.r00, this.r10, this.r20); }
get UpVector() { return new RbxVector3(this.r01, this.r11, this.r21); }
Lerp(o, a) {
// Линейная интерполяция (без правильного slerp на матрицах — для MVP сойдёт)
return new RbxCFrame(
this.X + (o.X - this.X) * a,
this.Y + (o.Y - this.Y) * a,
this.Z + (o.Z - this.Z) * a,
this.r00, this.r01, this.r02,
this.r10, this.r11, this.r12,
this.r20, this.r21, this.r22,
);
}
Inverse() {
// Транспонируем 3x3 (для rotation matrix Inverse == Transpose)
return new RbxCFrame(
-this.X, -this.Y, -this.Z,
this.r00, this.r10, this.r20,
this.r01, this.r11, this.r21,
this.r02, this.r12, this.r22,
);
}
toEulerXYZ() {
const rx = Math.atan2(this.r21, this.r22);
const ry = Math.atan2(-this.r20, Math.sqrt(this.r21*this.r21 + this.r22*this.r22));
const rz = Math.atan2(this.r10, this.r00);
return [rx, ry, rz];
}
toString() { return `${this.X}, ${this.Y}, ${this.Z}`; }
}
class RbxUDim {
constructor(scale, offset) { this.Scale = +scale || 0; this.Offset = +offset | 0; }
toString() { return `${this.Scale}, ${this.Offset}`; }
}
class RbxUDim2 {
constructor(xs, xo, ys, yo) {
this.X = new RbxUDim(xs, xo);
this.Y = new RbxUDim(ys, yo);
}
static new(xs, xo, ys, yo) { return new RbxUDim2(xs, xo, ys, yo); }
static fromScale(xs, ys) { return new RbxUDim2(xs, 0, ys, 0); }
static fromOffset(xo, yo) { return new RbxUDim2(0, xo, 0, yo); }
toString() { return `${this.X.Scale}, ${this.X.Offset}, ${this.Y.Scale}, ${this.Y.Offset}`; }
}
/* ──────── RBXScriptSignal ──────── */
let _signalIdCounter = 1000;
class RbxSignal {
constructor(name) {
this.name = name;
this.id = _signalIdCounter++;
this.connections = [];
}
Connect(callback) {
const conn = { callback, connected: true };
this.connections.push(conn);
return {
Disconnect: () => { conn.connected = false; },
disconnect: () => { conn.connected = false; },
Connected: () => conn.connected,
};
}
// Legacy Roblox API — lowercase alias
connect(callback) { return this.Connect(callback); }
Wait() { return null; }
wait() { return null; }
Fire(...args) {
for (const c of this.connections) {
if (!c.connected) continue;
try { c.callback(...args); } catch (e) { /* swallow */ }
}
}
fire(...args) { return this.Fire(...args); }
}
/* ──────── Instance прокси ──────── */
let _instanceCounter = 1;
// Null-stub: возвращается из FindFirstChild/WaitForChild когда объект не найден.
// Имеет все методы Instance как no-op, чтобы Lua-цепочки вроде
// game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("X").Touched:Connect(fn)
// не падали с "attempt to call js_null", когда промежуточный объект не существует.
// Все методы возвращают сам _nullStub чтобы цепочка не разорвалась.
// nullSignal: callable proxy. Lua делает x:Connect(fn) = x.Connect(x, fn),
// но также pattern signal:Connect(fn) сначала достаёт signal.Connect (это функция),
// потом вызывает её. Мы возвращаем функцию которая безусловно возвращает {Disconnect}.
const _nullConn = { Disconnect: () => {}, disconnect: () => {}, Connected: false };
const _nullSignalFn = () => _nullConn;
const _nullSignal = new Proxy(_nullSignalFn, {
get(_, k) {
if (k === 'Connect' || k === 'connect') return _nullSignalFn;
if (k === 'Wait' || k === 'wait') return () => null;
if (k === 'Fire' || k === 'fire') return () => {};
return undefined;
},
});
// Известные имена сигналов (Touched, Changed, MouseButton1Click, ...)
const _SIGNAL_NAMES = new Set([
'Touched','TouchEnded','Changed','Activated',
'MouseButton1Click','MouseButton1Down','MouseButton1Up',
'MouseButton2Click','MouseButton2Down','MouseButton2Up',
'MouseEnter','MouseLeave','InputBegan','InputEnded','InputChanged',
'PlayerAdded','PlayerRemoving','CharacterAdded','CharacterRemoving',
'Heartbeat','Stepped','RenderStepped','Died','HealthChanged',
'FocusLost','Focused','ChildAdded','ChildRemoved',
'AncestryChanged','DescendantAdded','DescendantRemoving',
// Tool сигналы
'Equipped','Unequipped','Selected','Deselected',
// прочие популярные
'OnInvoke','OnServerInvoke','OnClientInvoke',
'OnServerEvent','OnClientEvent','Fired','Triggered',
'ChatMakeSystemMessage','ChatMade',
]);
// _makeDeepStub — рекурсивный proxy которому всё равно сколько раз его
// индексируют. На любом уровне:
// - caps-имя из _SIGNAL_NAMES → возвращает _nullSignal
// - 'Parent' → возвращает _nullStub
// - любое другое имя → callable proxy + рекурсивная глубина
// Это позволяет цепочкам типа `tool.Selected:Connect(fn)` или
// `script.Parent.Parent.Frame.Visible` молча no-op'аться.
// Вместо JS Proxy (который wasmoon оборачивает в js_promise) — используем
// специальный маркер. Реальный stub живёт на Lua-стороне.
const NULL_STUB_MARKER = { __isNullStubMarker: true };
function _makeDeepStub() { return NULL_STUB_MARKER; }
const _nullStubBase = { __isNullStub: true, ClassName: 'Nil', Name: 'Nil', Children: [], Value: 0, Text: '', Visible: false };
// _nullStub оставлен как маркер, но не используется как реальный stub —
// debug.setmetatable(nil) в Lua перехватывает всё это.
const _nullStub = _nullStubBase;
class RbxInstance {
constructor(className, init = {}) {
this.__id = _instanceCounter++;
this.ClassName = className;
this.Name = init.Name || className;
this.Parent = init.Parent || null;
this.Children = [];
this.__props = {}; // raw properties (для Position и т.п.)
// Signals доступны как прямые свойства, плюс дублируются в __signals для serv-кода
this.Touched = new RbxSignal('Touched');
this.TouchEnded = new RbxSignal('TouchEnded');
this.Changed = new RbxSignal('Changed');
this.AncestryChanged = new RbxSignal('AncestryChanged');
this.ChildAdded = new RbxSignal('ChildAdded');
this.ChildRemoved = new RbxSignal('ChildRemoved');
this.__signals = {
Touched: this.Touched,
TouchEnded: this.TouchEnded,
Changed: this.Changed,
AncestryChanged: this.AncestryChanged,
ChildAdded: this.ChildAdded,
ChildRemoved: this.ChildRemoved,
};
this.__sceneState = null;
}
GetChildren() { return [...this.Children]; }
GetDescendants() {
const out = [];
const walk = (n) => {
for (const c of n.Children) { out.push(c); walk(c); }
};
walk(this);
return out;
}
FindFirstChild(name, recursive) {
for (const c of this.Children) {
if (c.Name === name) return c;
if (recursive) {
const found = c.FindFirstChild(name, true);
if (found) return found;
}
}
// Возвращаем undefined — wasmoon отдаст это как nil.
// Lua-side debug.setmetatable(nil) перехватит дальнейшую индексацию.
return undefined;
}
FindFirstChildOfClass(className) {
for (const c of this.Children) {
if (c.ClassName === className) return c;
}
return undefined;
}
FindFirstAncestor(name) {
let p = this.Parent;
while (p) {
if (p.Name === name) return p;
p = p.Parent;
}
return undefined;
}
WaitForChild(name, _timeout) {
// В MVP — синхронный поиск без ожидания. В 4.7 через корутины можно ждать.
return this.FindFirstChild(name);
}
IsA(className) {
if (this.ClassName === className) return true;
// Roblox class hierarchy: Part isA BasePart isA PVInstance isA Instance.
const hierarchy = {
'Part': ['BasePart', 'PVInstance', 'Instance'],
'WedgePart': ['BasePart', 'PVInstance', 'Instance'],
'CornerWedgePart': ['BasePart', 'PVInstance', 'Instance'],
'MeshPart': ['BasePart', 'PVInstance', 'Instance'],
'UnionOperation': ['PartOperation', 'BasePart', 'PVInstance', 'Instance'],
'TrussPart': ['BasePart', 'PVInstance', 'Instance'],
'SpawnLocation': ['Part', 'BasePart', 'PVInstance', 'Instance'],
'Script': ['BaseScript', 'LuaSourceContainer', 'Instance'],
'LocalScript': ['BaseScript', 'LuaSourceContainer', 'Instance'],
'ModuleScript': ['LuaSourceContainer', 'Instance'],
'Folder': ['Instance'],
'Model': ['PVInstance', 'Instance'],
'Sound': ['Instance'],
'PointLight': ['Light', 'Instance'],
'SpotLight': ['Light', 'Instance'],
'Humanoid': ['Instance'],
};
const ancestors = hierarchy[this.ClassName] || [];
return ancestors.includes(className);
}
Destroy() {
if (this.Parent && this.Parent.Children) {
const idx = this.Parent.Children.indexOf(this);
if (idx >= 0) this.Parent.Children.splice(idx, 1);
}
this.Parent = null;
this.__destroyed = true;
}
Clone() {
const cl = new RbxInstance(this.ClassName);
cl.Name = this.Name;
cl.__props = JSON.parse(JSON.stringify(this.__props));
for (const c of this.Children) {
const cc = c.Clone();
cc.Parent = cl;
cl.Children.push(cc);
}
return cl;
}
GetPropertyChangedSignal(propName) {
const sigName = `Changed:${propName}`;
if (!this.__signals[sigName]) this.__signals[sigName] = new RbxSignal(sigName);
return this.__signals[sigName];
}
}
/* ──────── Part — наследник Instance с реальными свойствами сцены ──────── */
class RbxPart extends RbxInstance {
constructor(primId, init = {}) {
super(init.ClassName || 'Part', init);
this.__primId = primId; // id примитива в Rublox-сцене
this.__sendFn = null; // setter из shim init
// Кешированные свойства (mirror'ятся через handleTick)
this._snap = init.snap || {};
}
get Position() {
return new RbxVector3(this._snap.x || 0, this._snap.y || 0, this._snap.z || 0);
}
set Position(v) {
if (v instanceof RbxVector3) {
this._snap.x = v.X; this._snap.y = v.Y; this._snap.z = v.Z;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'position', value: { x: v.X, y: v.Y, z: v.Z } });
}
}
get CFrame() {
return new RbxCFrame(this._snap.x || 0, this._snap.y || 0, this._snap.z || 0);
}
set CFrame(cf) {
if (cf instanceof RbxCFrame) {
this._snap.x = cf.X; this._snap.y = cf.Y; this._snap.z = cf.Z;
const [rx, ry, rz] = cf.toEulerXYZ();
this._snap.rx = rx; this._snap.ry = ry; this._snap.rz = rz;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'cframe', value: { x: cf.X, y: cf.Y, z: cf.Z, rx, ry, rz } });
}
}
get Size() {
return new RbxVector3(this._snap.sx || 1, this._snap.sy || 1, this._snap.sz || 1);
}
set Size(v) {
if (v instanceof RbxVector3) {
this._snap.sx = v.X; this._snap.sy = v.Y; this._snap.sz = v.Z;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'size', value: { sx: v.X, sy: v.Y, sz: v.Z } });
}
}
get Color() { return RbxColor3.fromHex(this._snap.color || '#cccccc'); }
set Color(c) {
if (c instanceof RbxColor3) {
const hex = c.toHex();
this._snap.color = hex;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'color', value: hex });
}
}
get BrickColor() { return { Color: this.Color, Name: 'Medium stone grey' }; }
set BrickColor(b) { if (b && b.Color) this.Color = b.Color; }
get Material() { return this._snap.material || 'glossy'; }
set Material(m) {
this._snap.material = m;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'material', value: m });
}
get Anchored() { return !!this._snap.anchored; }
set Anchored(v) {
this._snap.anchored = !!v;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'anchored', value: !!v });
}
get CanCollide() { return this._snap.canCollide !== false; }
set CanCollide(v) {
this._snap.canCollide = !!v;
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'canCollide', value: !!v });
}
get Transparency() { return 1.0 - (this._snap.opacity ?? 1.0); }
set Transparency(v) {
this._snap.opacity = 1.0 - (+v || 0);
this.__sendFn?.('partSet', { primId: this.__primId, prop: 'opacity', value: this._snap.opacity });
}
get Velocity() { return new RbxVector3(0, 0, 0); }
set Velocity(v) {
if (v instanceof RbxVector3) {
this.__sendFn?.('partVel', { primId: this.__primId, vx: v.X, vy: v.Y, vz: v.Z });
}
}
}
/* ──────── Главный entry-point: регистрация в Lua-VM ──────── */
export function registerRobloxApi(lua, ctx) {
const { getSceneSnap, targetPrimitiveId, send, getGuiTree, scheduler } = ctx;
// 1. Math classes — как глобалы с .new factory
const wrap = (cls) => ({
new: (...args) => new cls(...args),
});
lua.global.set('Vector3', {
new: (x, y, z) => new RbxVector3(x, y, z),
zero: new RbxVector3(0, 0, 0),
one: new RbxVector3(1, 1, 1),
xAxis: new RbxVector3(1, 0, 0),
yAxis: new RbxVector3(0, 1, 0),
zAxis: new RbxVector3(0, 0, 1),
});
lua.global.set('Color3', {
new: (r, g, b) => new RbxColor3(r, g, b),
fromRGB: RbxColor3.fromRGB,
fromHex: RbxColor3.fromHex,
});
lua.global.set('CFrame', {
new: RbxCFrame.new,
Angles: RbxCFrame.Angles,
fromEulerAnglesXYZ: RbxCFrame.fromEulerAnglesXYZ,
});
lua.global.set('UDim', { new: (s, o) => new RbxUDim(s, o) });
lua.global.set('UDim2', {
new: RbxUDim2.new,
fromScale: RbxUDim2.fromScale,
fromOffset: RbxUDim2.fromOffset,
});
// 2. Сцена — собираем JS-структуру из snap'а
// Workspace — корень.
const workspace = new RbxInstance('Workspace', { Name: 'Workspace' });
const part_by_id = new Map();
const snap = getSceneSnap();
if (snap && snap.primitives) {
for (const [id, p] of Object.entries(snap.primitives)) {
const part = new RbxPart(+id, {
ClassName: p.type === 'wedge' ? 'WedgePart' :
p.type === 'cornerwedge' ? 'CornerWedgePart' : 'Part',
Name: p.name || 'Part',
snap: { ...p },
});
part.__sendFn = send;
// Сигналы Part: Touched/TouchEnded существуют на каждом по умолчанию
part.Touched = new RbxSignal('Touched');
part.TouchEnded = new RbxSignal('TouchEnded');
part.Parent = workspace;
workspace.Children.push(part);
part_by_id.set(+id, part);
}
}
// 2b. GUI-tree: предсоздаём ScreenGui + Frame/Button/Label/etc по дереву
// конвертера. Каждый button получает MouseButton1Click/MouseButton1Down/Up
// сигналы которые fire'аются из main через sendGlobalEvent('guiClick').
const gui_by_id = new Map();
// PlayerGui контейнер внутри Players.LocalPlayer
const playerGui = new RbxInstance('PlayerGui', { Name: 'PlayerGui' });
if (getGuiTree) {
const tree = getGuiTree() || [];
// первый проход — создаём instances
for (const el of tree) {
const cls = el.__roblox_class || 'Frame';
const inst = new RbxInstance(cls, { Name: el.name || cls });
inst.__guiId = el.id;
inst.Visible = el.visible !== false;
inst.Text = el.text || '';
// Стандартные сигналы кнопок
if (cls === 'TextButton' || cls === 'ImageButton') {
inst.MouseButton1Click = new RbxSignal('MouseButton1Click');
inst.MouseButton1Down = new RbxSignal('MouseButton1Down');
inst.MouseButton1Up = new RbxSignal('MouseButton1Up');
inst.Activated = new RbxSignal('Activated');
inst.MouseEnter = new RbxSignal('MouseEnter');
inst.MouseLeave = new RbxSignal('MouseLeave');
}
// FocusLost для textboxes
if (cls === 'TextBox') {
inst.FocusLost = new RbxSignal('FocusLost');
inst.Focused = new RbxSignal('Focused');
}
// Changed-сигнал у каждого
inst.Changed = new RbxSignal('Changed');
gui_by_id.set(el.id, inst);
}
// второй проход — parent-связи (parentId → Instance)
for (const el of tree) {
const inst = gui_by_id.get(el.id);
if (!inst) continue;
const parentInst = el.parentId ? gui_by_id.get(el.parentId) : playerGui;
if (parentInst) {
inst.Parent = parentInst;
parentInst.Children.push(inst);
}
}
}
// 3. script — в shared-режиме не глобал, а локально создаётся при addScript.
// Здесь только заглушка чтобы простые non-shared скрипты не падали.
if (targetPrimitiveId != null && part_by_id.has(targetPrimitiveId)) {
const parentPart = part_by_id.get(targetPrimitiveId);
const scriptInst = new RbxInstance('LocalScript', { Name: 'Script' });
scriptInst.Parent = parentPart;
parentPart.Children.push(scriptInst);
lua.global.set('script', scriptInst);
}
// 4. game / game:GetService
const services = new Map();
const game = new RbxInstance('DataModel', { Name: 'Game' });
game.Children.push(workspace);
workspace.Parent = game;
// Builtin services:
const lighting = new RbxInstance('Lighting', { Name: 'Lighting' });
lighting.Parent = game;
game.Children.push(lighting);
services.set('Lighting', lighting);
const replicatedStorage = new RbxInstance('ReplicatedStorage', { Name: 'ReplicatedStorage' });
replicatedStorage.Parent = game;
game.Children.push(replicatedStorage);
services.set('ReplicatedStorage', replicatedStorage);
const runService = new RbxInstance('RunService', { Name: 'RunService' });
runService.Heartbeat = new RbxSignal('Heartbeat');
runService.Stepped = new RbxSignal('Stepped');
runService.RenderStepped = new RbxSignal('RenderStepped');
services.set('RunService', runService);
const playersService = new RbxInstance('Players', { Name: 'Players' });
playersService.PlayerAdded = new RbxSignal('PlayerAdded');
playersService.PlayerRemoving = new RbxSignal('PlayerRemoving');
// LocalPlayer с PlayerGui + Character
const localPlayer = new RbxInstance('Player', { Name: 'Player1' });
localPlayer.UserId = 1;
localPlayer.PlayerGui = playerGui;
playerGui.Parent = localPlayer;
localPlayer.Children.push(playerGui);
// Character заглушка с Humanoid и HumanoidRootPart
const character = new RbxInstance('Model', { Name: 'Character' });
const humanoid = new RbxInstance('Humanoid', { Name: 'Humanoid' });
humanoid.WalkSpeed = 16;
humanoid.JumpPower = 50;
humanoid.Health = 100;
humanoid.MaxHealth = 100;
humanoid.Died = new RbxSignal('Died');
humanoid.HealthChanged = new RbxSignal('HealthChanged');
humanoid.Touched = new RbxSignal('Touched');
humanoid.Parent = character;
character.Children.push(humanoid);
character.Humanoid = humanoid;
const hrp = new RbxPart(-1, { ClassName: 'Part', Name: 'HumanoidRootPart' });
hrp.Touched = new RbxSignal('Touched');
hrp.Parent = character;
character.Children.push(hrp);
character.HumanoidRootPart = hrp;
localPlayer.Character = character;
localPlayer.CharacterAdded = new RbxSignal('CharacterAdded');
localPlayer.CharacterRemoving = new RbxSignal('CharacterRemoving');
playersService.LocalPlayer = localPlayer;
playersService.Children.push(localPlayer);
services.set('Players', playersService);
game.GetService = function(svc) {
if (services.has(svc)) return services.get(svc);
if (svc === 'Workspace') return workspace;
if (svc === 'Workspace') return workspace;
// Неизвестный сервис — создаём заглушку, чтобы не падало
const stub = new RbxInstance(svc, { Name: svc });
services.set(svc, stub);
return stub;
};
game.Workspace = workspace;
game.Lighting = lighting;
game.Players = playersService;
game.ReplicatedStorage = replicatedStorage;
lua.global.set('game', game);
lua.global.set('workspace', workspace);
lua.global.set('Workspace', workspace);
// 5. Instance.new
lua.global.set('Instance', {
new: (className, parent) => {
const inst = new RbxInstance(className);
if (parent && parent instanceof RbxInstance) {
inst.Parent = parent;
parent.Children.push(inst);
}
return inst;
},
});
// 6. wait/task.wait через scheduler. scheduler — main-side, поддерживает
// schedule(sec, fn) что fire'ит fn после задержки в следующих tick'ах.
// spawn/delay/defer запускают функцию через scheduler.spawn (отдельная корутина).
const sched = scheduler || {
schedule: (sec, fn) => { try { fn(); } catch (e) {} },
spawn: (fn) => { try { fn(); } catch (e) {} },
now: () => Date.now() / 1000,
};
lua.global.set('wait', (sec) => {
// В корутине: yield на (sec || 0). Scheduler сам resume'ит.
// Тут мы синхронны (вызов из Lua) — реальный yield делается в lua-wrapper
// через coroutine.yield, который мы оборачиваем в addScript.
// Здесь просто возвращаем длительность для совместимости.
return [sec || 0, 0];
});
lua.global.set('task', {
wait: (sec) => sec || 0,
spawn: (fn, ...args) => { sched.spawn(() => { try { fn(...args); } catch (e) {} }); return null; },
delay: (sec, fn, ...args) => { sched.schedule(sec || 0, () => { try { fn(...args); } catch (e) {} }); return null; },
defer: (fn, ...args) => { sched.spawn(() => { try { fn(...args); } catch (e) {} }); return null; },
});
lua.global.set('spawn', (fn) => { sched.spawn(() => { try { fn(); } catch (e) {} }); });
lua.global.set('delay', (sec, fn) => { sched.schedule(sec || 0, () => { try { fn(); } catch (e) {} }); });
// require(ModuleScript) — возвращаем nil, debug.setmetatable перехватит.
lua.global.set('require', (_arg) => undefined);
lua.global.set('tick', () => Date.now() / 1000);
lua.global.set('time', () => Date.now() / 1000);
lua.global.set('elapsedTime', () => Date.now() / 1000);
// 7. print / warn / error — пробрасываем в main как log
lua.global.set('print', (...args) => {
const text = args.map(a => luaToString(a)).join('\t');
send('log', { level: 'info', text });
});
lua.global.set('warn', (...args) => {
const text = args.map(a => luaToString(a)).join('\t');
send('log', { level: 'warn', text });
});
// 8. Enum — упрощённая заглушка для самых популярных enums
const enumTable = {
Material: { Plastic: { Value: 256, Name: 'Plastic' }, Neon: { Value: 784, Name: 'Neon' },
Metal: { Value: 512, Name: 'Metal' }, Glass: { Value: 1024, Name: 'Glass' },
Wood: { Value: 272, Name: 'Wood' }, SmoothPlastic: { Value: 496, Name: 'SmoothPlastic' } },
PartType: { Ball: { Value: 0, Name: 'Ball' }, Block: { Value: 1, Name: 'Block' },
Cylinder: { Value: 2, Name: 'Cylinder' } },
KeyCode: { Space: { Value: 32, Name: 'Space' }, W: { Value: 87, Name: 'W' },
A: { Value: 65, Name: 'A' }, S: { Value: 83, Name: 'S' }, D: { Value: 68, Name: 'D' } },
EasingStyle: { Linear: { Value: 0, Name: 'Linear' }, Quad: { Value: 1, Name: 'Quad' },
Sine: { Value: 5, Name: 'Sine' } },
EasingDirection: { In: { Value: 0, Name: 'In' }, Out: { Value: 1, Name: 'Out' },
InOut: { Value: 2, Name: 'InOut' } },
};
lua.global.set('Enum', enumTable);
return { workspace, game, part_by_id, services, gui_by_id, localPlayer, character, humanoid };
}
function luaToString(v) {
if (v == null) return 'nil';
if (typeof v === 'string') return v;
if (typeof v === 'number') return String(v);
if (typeof v === 'boolean') return String(v);
if (v.toString) return v.toString();
return '<object>';
}
export { RbxVector3, RbxColor3, RbxCFrame, RbxUDim, RbxUDim2, RbxInstance, RbxPart, RbxSignal };