All checks were successful
- UDim2: scale теперь умножается на viewport reference (1280×720), раньше игнорировался — фреймы получали 0×0 и фейлились на дефолт 100×30 или наоборот заполняли всё окно - _udim2_pair(): пара (x,y) через _udim2_to_rublox(axis='x'|'y') - Фильтр rbxasset:// rbxassetid:// rbxhttp:// rbxthumb:// URL'ов на пустую строку — браузер их не загружает, спам в console исчезает Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
156 lines
6.7 KiB
JavaScript
156 lines
6.7 KiB
JavaScript
/**
|
||
* rbxl-lua-integration.js — single-VM интеграция Roblox-Lua скриптов.
|
||
*
|
||
* Архитектура (single-VM):
|
||
* - Один shared Worker для ВСЕХ Roblox-Lua скриптов проекта
|
||
* - Один wasmoon Lua-state
|
||
* - Скрипты добавляются через addScript(id, target, luaSource)
|
||
*
|
||
* Это снимает WASM OOM (1 wasmoon ~ 16 MB, не 742 × 16 MB).
|
||
*/
|
||
import RobloxLuaSharedWorker from './RobloxLuaSharedWorker.js?worker';
|
||
import { RobloxLuaSharedSandbox } from './RobloxLuaSharedSandbox.js';
|
||
|
||
/**
|
||
* Распаковывает Lua-исходник из поля code упакованного rbxl-importer'ом.
|
||
* Формат: "// @roblox-lua\\n// {meta json}\\n/[*] lua_source:\\n<код>\\n[*]/"
|
||
*/
|
||
export function unpackRobloxLuaCode(code) {
|
||
const openTag = '/* lua_source:\n';
|
||
const i = code.indexOf(openTag);
|
||
if (i < 0) return null;
|
||
const start = i + openTag.length;
|
||
const closeIdx = code.lastIndexOf('\n*/');
|
||
if (closeIdx < start) return null;
|
||
return code.slice(start, closeIdx);
|
||
}
|
||
|
||
/**
|
||
* Snap сцены для Lua-shim (workspace:GetChildren).
|
||
*/
|
||
export function buildLuaSceneSnap(primitives) {
|
||
const out = { primitives: {} };
|
||
if (!Array.isArray(primitives)) return out;
|
||
for (const p of primitives) {
|
||
out.primitives[p.id] = {
|
||
id: p.id, type: p.type, name: p.name,
|
||
x: p.x, y: p.y, z: p.z,
|
||
sx: p.sx, sy: p.sy, sz: p.sz,
|
||
color: p.color, material: p.material,
|
||
anchored: !!p.anchored, canCollide: p.canCollide !== false,
|
||
opacity: typeof p.opacity === 'number' ? p.opacity : 1,
|
||
};
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Создаёт shared sandbox менеджер, добавляет все валидные скрипты и
|
||
* возвращает его. GameRuntime пушит результат в this.sandboxes ОДИН раз.
|
||
*
|
||
* @param {Array} scripts — entries из state.scripts (с маркером // @roblox-lua)
|
||
* @param {Object} ctx — { primitives, onCommand(cmd, payload) }
|
||
* @returns {{ sandbox: RobloxLuaSharedSandbox, count: number, filtered: number } | null}
|
||
*/
|
||
export function startRobloxLuaShared(scripts, ctx) {
|
||
try {
|
||
const luaScripts = [];
|
||
let filtered = 0;
|
||
for (const s of scripts) {
|
||
if (!s || typeof s.code !== 'string') continue;
|
||
if (!s.code.startsWith('// @roblox-lua')) continue;
|
||
const luaSource = unpackRobloxLuaCode(s.code);
|
||
if (!luaSource) { filtered++; continue; }
|
||
// Фильтр: скрипты декомпилированные из Synapse X / HD Admin — обычно
|
||
// длинные и сервисные. Оставим только короткие с target.
|
||
// Но в single-VM режиме лимита на количество нет — пробуем все.
|
||
luaScripts.push({ id: s.id, target: s.target, luaSource });
|
||
}
|
||
if (luaScripts.length === 0) return { sandbox: null, count: 0, filtered };
|
||
|
||
const worker = new RobloxLuaSharedWorker();
|
||
const sceneSnap = buildLuaSceneSnap(ctx.primitives);
|
||
const mgr = new RobloxLuaSharedSandbox();
|
||
mgr.setOnCommand(ctx.onCommand);
|
||
mgr.start(sceneSnap, worker);
|
||
for (const ls of luaScripts) {
|
||
mgr.addScript(ls.id, ls.target, ls.luaSource);
|
||
}
|
||
return { sandbox: mgr, count: luaScripts.length, filtered };
|
||
} catch (e) {
|
||
// eslint-disable-next-line no-console
|
||
console.warn('[rbxl-lua-shared] start failed:', e?.message || e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Маппинг IPC команд от shared sandbox на действия в Babylon-сцене.
|
||
*
|
||
* @param {string} _scriptId — не используется (команды от shared VM не привязаны к одному id)
|
||
* @param {string} cmd
|
||
* @param {object} payload
|
||
* @param {object} runtime — { scene3d, game }
|
||
*/
|
||
export function handleLuaCommand(_scriptId, cmd, payload, runtime) {
|
||
if (cmd === 'log') {
|
||
const fn = payload?.level === 'error' ? console.error
|
||
: payload?.level === 'warn' ? console.warn : console.log;
|
||
fn('[rbxl-lua]', payload?.text || '');
|
||
return;
|
||
}
|
||
if (cmd === 'partSet') {
|
||
try {
|
||
const pm = runtime.scene3d?.primitiveManager;
|
||
if (!pm) return;
|
||
const primId = payload?.primId;
|
||
const prop = payload?.prop;
|
||
const value = payload?.value;
|
||
const patch = {};
|
||
if (prop === 'position' && value) {
|
||
patch.x = value.x; patch.y = value.y; patch.z = value.z;
|
||
} else if (prop === 'cframe' && value) {
|
||
patch.x = value.x; patch.y = value.y; patch.z = value.z;
|
||
patch.rotationX = value.rx; patch.rotationY = value.ry; patch.rotationZ = value.rz;
|
||
} else if (prop === 'size' && value) {
|
||
patch.sx = value.sx; patch.sy = value.sy; patch.sz = value.sz;
|
||
} else if (prop === 'color') patch.color = value;
|
||
else if (prop === 'material') patch.material = value;
|
||
else if (prop === 'anchored') patch.anchored = value;
|
||
else if (prop === 'canCollide') patch.canCollide = value;
|
||
else if (prop === 'opacity') patch.opacity = value;
|
||
else if (prop === 'rotation' && value) {
|
||
patch.rotationX = value.rx; patch.rotationY = value.ry; patch.rotationZ = value.rz;
|
||
}
|
||
if (typeof pm.applyPatch === 'function') pm.applyPatch(primId, patch);
|
||
else if (typeof pm.update === 'function') pm.update(primId, patch);
|
||
} catch (e) { /* swallow */ }
|
||
return;
|
||
}
|
||
if (cmd === 'partVel') {
|
||
try {
|
||
const pm = runtime.scene3d?.primitiveManager;
|
||
if (pm && typeof pm.setVelocity === 'function') {
|
||
pm.setVelocity(payload.primId, payload.vx, payload.vy, payload.vz);
|
||
}
|
||
} catch (e) {}
|
||
return;
|
||
}
|
||
if (cmd === 'playerCmd') {
|
||
try {
|
||
const p = runtime.game?.player;
|
||
if (!p) return;
|
||
const method = payload?.method;
|
||
const args = payload?.args || [];
|
||
if (method === 'teleport') p.teleport && p.teleport(args[0], args[1], args[2]);
|
||
else if (method === 'setWalkSpeed') p.setWalkSpeed && p.setWalkSpeed(args[0]);
|
||
else if (method === 'setJumpPower') p.setJumpPower && p.setJumpPower(args[0]);
|
||
else if (method === 'setHealth') p.setHealth && p.setHealth(args[0]);
|
||
else if (method === 'die') p.die && p.die();
|
||
else if (method === 'damage' || method === 'takeDamage') p.damage && p.damage(args[0]);
|
||
} catch (e) {}
|
||
return;
|
||
}
|
||
}
|
||
|