All checks were successful
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>
716 lines
31 KiB
JavaScript
716 lines
31 KiB
JavaScript
/**
|
||
* 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 };
|