feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
@ -124,7 +124,11 @@ export class LuaSharedSandbox {
|
|||||||
}
|
}
|
||||||
const safeId = entry.id.replace(/[^a-zA-Z0-9_]/g, '_');
|
const safeId = entry.id.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
const scriptName = entry.name || `Script_${safeId}`;
|
const scriptName = entry.name || `Script_${safeId}`;
|
||||||
// ВАЖНО: chunk_name прокидываем — wasmoon покажет его в traceback.
|
// Скрипт оборачиваем в coroutine — это позволяет task.wait через yield.
|
||||||
|
// Резюмим coroutine из main-loop когда наступило время.
|
||||||
|
// Регистрируем coroutine в __rbxl_coroutines с id для возобновления.
|
||||||
|
// Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает
|
||||||
|
// delay из resume → планируем следующий resume через scheduleResume.
|
||||||
const wrapped = `
|
const wrapped = `
|
||||||
do
|
do
|
||||||
local script = {
|
local script = {
|
||||||
@ -134,11 +138,19 @@ export class LuaSharedSandbox {
|
|||||||
Disabled = false,
|
Disabled = false,
|
||||||
Source = nil,
|
Source = nil,
|
||||||
}
|
}
|
||||||
local ok, err = pcall(function()
|
local co = coroutine.create(function()
|
||||||
${entry.code}
|
${entry.code}
|
||||||
end)
|
end)
|
||||||
|
__rbxl_register_coroutine(${JSON.stringify(entry.id)}, co)
|
||||||
|
local ok, ret = coroutine.resume(co)
|
||||||
if not ok then
|
if not ok then
|
||||||
__rbxl_send_error(${JSON.stringify(entry.id)}, tostring(err))
|
__rbxl_send_error(${JSON.stringify(entry.id)}, tostring(ret))
|
||||||
|
__rbxl_unregister_coroutine(${JSON.stringify(entry.id)})
|
||||||
|
elseif type(ret) == 'number' then
|
||||||
|
-- скрипт yield'нул с delay (через task.wait) — планируем resume
|
||||||
|
__rbxl_schedule_resume(${JSON.stringify(entry.id)}, ret)
|
||||||
|
elseif coroutine.status(co) == 'dead' then
|
||||||
|
__rbxl_unregister_coroutine(${JSON.stringify(entry.id)})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -204,6 +204,10 @@ function makeInstanceMethods() {
|
|||||||
},
|
},
|
||||||
Destroy: function () {
|
Destroy: function () {
|
||||||
this.Destroyed = true;
|
this.Destroyed = true;
|
||||||
|
// Если это Part с примитивом — шлём sceneDelete
|
||||||
|
if (this.__primId != null && this.__sendDestroy) {
|
||||||
|
try { this.__sendDestroy(this.__primId); } catch (_) {}
|
||||||
|
}
|
||||||
if (this.Parent && this.Parent.Children) {
|
if (this.Parent && this.Parent.Children) {
|
||||||
const i = this.Parent.Children.indexOf(this);
|
const i = this.Parent.Children.indexOf(this);
|
||||||
if (i >= 0) this.Parent.Children.splice(i, 1);
|
if (i >= 0) this.Parent.Children.splice(i, 1);
|
||||||
@ -259,18 +263,99 @@ function newInstance(className, name) {
|
|||||||
function newPart(primData, sendFn) {
|
function newPart(primData, sendFn) {
|
||||||
const p = newInstance('Part', primData.name || `Part_${primData.id}`);
|
const p = newInstance('Part', primData.name || `Part_${primData.id}`);
|
||||||
p.__primId = primData.id;
|
p.__primId = primData.id;
|
||||||
p.__sendFn = sendFn;
|
p.__sendDestroy = (id) => sendFn('sceneDelete', { primId: id });
|
||||||
p.Touched = makeSignal();
|
p.Touched = makeSignal();
|
||||||
p.TouchEnded = makeSignal();
|
p.TouchEnded = makeSignal();
|
||||||
p.Position = new RbxVector3(primData.x || 0, primData.y || 0, primData.z || 0);
|
|
||||||
p.Size = new RbxVector3(primData.sx || 1, primData.sy || 1, primData.sz || 1);
|
|
||||||
p.Color = primData.color ? RbxColor3.fromHex(primData.color) : new RbxColor3(0.5, 0.5, 0.5);
|
|
||||||
p.Anchored = !!primData.anchored;
|
|
||||||
p.CanCollide = primData.canCollide !== false;
|
|
||||||
p.Transparency = primData.opacity != null ? (1 - primData.opacity) : 0;
|
|
||||||
p.Material = 'Plastic';
|
p.Material = 'Plastic';
|
||||||
p.BrickColor = { Color: p.Color, Name: 'Custom' };
|
|
||||||
p.CFrame = new RbxCFrame(p.Position.X, p.Position.Y, p.Position.Z);
|
// Внутренний state: реальные значения хранятся здесь, в Lua через getter/setter.
|
||||||
|
p._state = {
|
||||||
|
Position: new RbxVector3(primData.x || 0, primData.y || 0, primData.z || 0),
|
||||||
|
Size: new RbxVector3(primData.sx || 1, primData.sy || 1, primData.sz || 1),
|
||||||
|
Color: primData.color ? RbxColor3.fromHex(primData.color) : new RbxColor3(0.5, 0.5, 0.5),
|
||||||
|
Anchored: !!primData.anchored,
|
||||||
|
CanCollide: primData.canCollide !== false,
|
||||||
|
Transparency: primData.opacity != null ? (1 - primData.opacity) : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setter'ы шлют partSet → BabylonScene.primitiveManager через handleLuaCommand.
|
||||||
|
// Формат payload должен соответствовать rbxl-lua-integration.js#handleLuaCommand.
|
||||||
|
const send = (prop, value) => {
|
||||||
|
try { sendFn('partSet', { primId: p.__primId, prop, value }); } catch (_) {}
|
||||||
|
};
|
||||||
|
Object.defineProperty(p, 'Position', {
|
||||||
|
get() { return p._state.Position; },
|
||||||
|
set(v) {
|
||||||
|
if (!v) return;
|
||||||
|
const nv = new RbxVector3(v.X || 0, v.Y || 0, v.Z || 0);
|
||||||
|
p._state.Position = nv;
|
||||||
|
send('position', { x: nv.X, y: nv.Y, z: nv.Z });
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'Size', {
|
||||||
|
get() { return p._state.Size; },
|
||||||
|
set(v) {
|
||||||
|
if (!v) return;
|
||||||
|
const nv = new RbxVector3(v.X || 1, v.Y || 1, v.Z || 1);
|
||||||
|
p._state.Size = nv;
|
||||||
|
send('size', { sx: nv.X, sy: nv.Y, sz: nv.Z });
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'Color', {
|
||||||
|
get() { return p._state.Color; },
|
||||||
|
set(v) {
|
||||||
|
if (!v) return;
|
||||||
|
const nv = v instanceof RbxColor3 ? v : new RbxColor3(v.R || 0, v.G || 0, v.B || 0);
|
||||||
|
p._state.Color = nv;
|
||||||
|
// handleLuaCommand ожидает строку для color
|
||||||
|
send('color', nv.toHex());
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'BrickColor', {
|
||||||
|
get() { return { Color: p._state.Color, Name: 'Custom' }; },
|
||||||
|
set(v) { if (v && v.Color) p.Color = v.Color; },
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'Anchored', {
|
||||||
|
get() { return p._state.Anchored; },
|
||||||
|
set(v) {
|
||||||
|
p._state.Anchored = !!v;
|
||||||
|
send('anchored', !!v);
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'CanCollide', {
|
||||||
|
get() { return p._state.CanCollide; },
|
||||||
|
set(v) {
|
||||||
|
p._state.CanCollide = !!v;
|
||||||
|
send('canCollide', !!v);
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'Transparency', {
|
||||||
|
get() { return p._state.Transparency; },
|
||||||
|
set(v) {
|
||||||
|
const nv = Math.max(0, Math.min(1, Number(v) || 0));
|
||||||
|
p._state.Transparency = nv;
|
||||||
|
// handleLuaCommand ожидает number для opacity
|
||||||
|
send('opacity', 1 - nv);
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(p, 'CFrame', {
|
||||||
|
get() {
|
||||||
|
const pos = p._state.Position;
|
||||||
|
return new RbxCFrame(pos.X, pos.Y, pos.Z);
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
if (v && v.Position) p.Position = v.Position;
|
||||||
|
else if (v && v.X != null) p.Position = new RbxVector3(v.X, v.Y, v.Z);
|
||||||
|
},
|
||||||
|
enumerable: true, configurable: true,
|
||||||
|
});
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,8 +424,13 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// === task.* + wait ===
|
// === task.* + wait ===
|
||||||
|
// task.wait/wait — реальный yield через coroutines. Юзер пишет:
|
||||||
|
// while true do part.Position = ... ; task.wait(0.1) end
|
||||||
|
// Это работает потому что **скрипт сам запускается как coroutine**
|
||||||
|
// (см. LuaSharedSandbox._startSingleScript → мы оборачиваем код в pcall,
|
||||||
|
// НО для yield нам нужно завернуть в coroutine.create). Делаем это
|
||||||
|
// через Lua-prelude: глобальная функция `_run_in_coroutine(fn)`.
|
||||||
global.set('task', {
|
global.set('task', {
|
||||||
wait: (_) => undefined,
|
|
||||||
spawn: (fn) => {
|
spawn: (fn) => {
|
||||||
try { if (typeof fn === 'function') fn(); } catch (e) {
|
try { if (typeof fn === 'function') fn(); } catch (e) {
|
||||||
send('log', { level: 'error', text: `[task.spawn] ${e?.message || e}` });
|
send('log', { level: 'error', text: `[task.spawn] ${e?.message || e}` });
|
||||||
@ -365,7 +455,7 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
synchronize: () => {},
|
synchronize: () => {},
|
||||||
desynchronize: () => {},
|
desynchronize: () => {},
|
||||||
});
|
});
|
||||||
global.set('wait', (_) => undefined);
|
// task.wait и wait определяются через Lua coroutine.yield в prelude (см. ниже)
|
||||||
|
|
||||||
// === DataModel ===
|
// === DataModel ===
|
||||||
const game = newInstance('DataModel', 'game');
|
const game = newInstance('DataModel', 'game');
|
||||||
@ -452,8 +542,59 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
uis.InputChanged = makeSignal();
|
uis.InputChanged = makeSignal();
|
||||||
uis.InputEnded = makeSignal();
|
uis.InputEnded = makeSignal();
|
||||||
|
|
||||||
|
// TweenService — реальная интерполяция через Heartbeat
|
||||||
const tw = makeService('TweenService');
|
const tw = makeService('TweenService');
|
||||||
tw.Create = function () { return { Play: () => {}, Pause: () => {}, Cancel: () => {} }; };
|
const activeTweens = []; // [{inst, props, duration, startAt, startVals, onDone}]
|
||||||
|
tw.Create = function (inst, info, propGoals) {
|
||||||
|
// info: TweenInfo (duration, EasingStyle, ...) — упрощённо берём только duration
|
||||||
|
const duration = (info && (info.Time || info.duration)) || 1;
|
||||||
|
const tween = {
|
||||||
|
__completed: makeSignal(),
|
||||||
|
Completed: undefined,
|
||||||
|
Play() {
|
||||||
|
if (!inst || !propGoals) return;
|
||||||
|
const startVals = {};
|
||||||
|
for (const k of Object.keys(propGoals)) {
|
||||||
|
try { startVals[k] = inst[k]; } catch (_) {}
|
||||||
|
}
|
||||||
|
activeTweens.push({
|
||||||
|
inst, props: propGoals, duration,
|
||||||
|
startAt: performance.now(),
|
||||||
|
startVals,
|
||||||
|
onDone: () => tween.__completed.Fire(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Pause() {},
|
||||||
|
Cancel() {},
|
||||||
|
};
|
||||||
|
tween.Completed = tween.__completed;
|
||||||
|
return tween;
|
||||||
|
};
|
||||||
|
function _stepTweens(_dt) {
|
||||||
|
if (activeTweens.length === 0) return;
|
||||||
|
const now = performance.now();
|
||||||
|
for (let i = activeTweens.length - 1; i >= 0; i--) {
|
||||||
|
const t = activeTweens[i];
|
||||||
|
const elapsed = (now - t.startAt) / 1000;
|
||||||
|
const k = Math.min(1, elapsed / t.duration);
|
||||||
|
for (const prop of Object.keys(t.props)) {
|
||||||
|
const goal = t.props[prop];
|
||||||
|
const start = t.startVals[prop];
|
||||||
|
if (!start || !goal) continue;
|
||||||
|
if (start instanceof RbxVector3 && goal instanceof RbxVector3) {
|
||||||
|
try { t.inst[prop] = start.Lerp(goal, k); } catch (_) {}
|
||||||
|
} else if (start instanceof RbxColor3 && goal instanceof RbxColor3) {
|
||||||
|
try { t.inst[prop] = start.Lerp(goal, k); } catch (_) {}
|
||||||
|
} else if (typeof start === 'number' && typeof goal === 'number') {
|
||||||
|
try { t.inst[prop] = start + (goal - start) * k; } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (k >= 1) {
|
||||||
|
activeTweens.splice(i, 1);
|
||||||
|
try { t.onDone(); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const http = makeService('HttpService');
|
const http = makeService('HttpService');
|
||||||
http.JSONEncode = function (v) { try { return JSON.stringify(v); } catch (_) { return '{}'; } };
|
http.JSONEncode = function (v) { try { return JSON.stringify(v); } catch (_) { return '{}'; } };
|
||||||
@ -503,21 +644,40 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
global.set('Workspace', workspace);
|
global.set('Workspace', workspace);
|
||||||
|
|
||||||
// === Instance.new ===
|
// === Instance.new ===
|
||||||
|
// Счётчик для новых Part'ов, создаваемых через Instance.new("Part"):
|
||||||
|
// primitiveManager.addInstance даст уникальный id, мы используем временный
|
||||||
|
// отрицательный id пока не пришёл sceneCreate ACK. Но проще: даём свой
|
||||||
|
// негативный id, primitiveManager заменит на свой. Для простоты — даём
|
||||||
|
// высокий positive id (10000+ random) и primitiveManager его использует
|
||||||
|
// если не занят.
|
||||||
|
let _nextNewPartId = 100000 + Math.floor(Math.random() * 10000);
|
||||||
|
|
||||||
global.set('Instance', {
|
global.set('Instance', {
|
||||||
new: (className, parent) => {
|
new: (className, parent) => {
|
||||||
let inst;
|
let inst;
|
||||||
if (className === 'Part' || className === 'WedgePart' || className === 'MeshPart') {
|
if (className === 'Part' || className === 'WedgePart' || className === 'MeshPart') {
|
||||||
inst = newInstance(className, className);
|
// Реальный примитив на сцене: шлём sceneCreate, регистрируем в partById
|
||||||
inst.Touched = makeSignal();
|
const newId = _nextNewPartId++;
|
||||||
inst.TouchEnded = makeSignal();
|
const fakePrim = {
|
||||||
inst.Position = new RbxVector3();
|
id: newId,
|
||||||
inst.Size = new RbxVector3(4, 1, 2);
|
name: `Part_${newId}`,
|
||||||
inst.Color = new RbxColor3(0.5, 0.5, 0.5);
|
x: 0, y: 0, z: 0,
|
||||||
inst.Anchored = false;
|
sx: 4, sy: 1, sz: 2,
|
||||||
inst.CanCollide = true;
|
color: '#A0A0A0',
|
||||||
inst.Transparency = 0;
|
anchored: true,
|
||||||
inst.Material = 'Plastic';
|
canCollide: true,
|
||||||
inst.CFrame = new RbxCFrame();
|
};
|
||||||
|
send('sceneCreate', {
|
||||||
|
primId: newId,
|
||||||
|
type: className === 'WedgePart' ? 'wedge' : 'cube',
|
||||||
|
x: 0, y: 0, z: 0,
|
||||||
|
sx: 4, sy: 1, sz: 2,
|
||||||
|
color: '#A0A0A0',
|
||||||
|
anchored: true,
|
||||||
|
canCollide: true,
|
||||||
|
});
|
||||||
|
inst = newPart(fakePrim, send);
|
||||||
|
partById.set(newId, inst);
|
||||||
} else if (className === 'RemoteEvent') {
|
} else if (className === 'RemoteEvent') {
|
||||||
inst = newInstance('RemoteEvent', 'RemoteEvent');
|
inst = newInstance('RemoteEvent', 'RemoteEvent');
|
||||||
inst.OnServerEvent = makeSignal();
|
inst.OnServerEvent = makeSignal();
|
||||||
@ -555,6 +715,42 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
send('log', { level: 'error', text: `[Lua ${id}] ${errStr || 'unknown error'}` });
|
send('log', { level: 'error', text: `[Lua ${id}] ${errStr || 'unknown error'}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// === Coroutines registry + task.wait через yield ===
|
||||||
|
// Каждый скрипт стартует как coroutine. Когда юзер пишет task.wait(sec),
|
||||||
|
// мы делаем coroutine.yield(sec). Main-loop резюмирует когда время вышло.
|
||||||
|
const coroutines = new Map(); // id → coroutine
|
||||||
|
const waitingCoros = []; // [{coId, wakeAt}]
|
||||||
|
global.set('__rbxl_register_coroutine', (id, co) => {
|
||||||
|
coroutines.set(String(id), co);
|
||||||
|
});
|
||||||
|
global.set('__rbxl_unregister_coroutine', (id) => {
|
||||||
|
coroutines.delete(String(id));
|
||||||
|
});
|
||||||
|
global.set('__rbxl_get_co', (id) => coroutines.get(String(id)) || undefined);
|
||||||
|
global.set('__rbxl_schedule_resume', (coId, delaySec) => {
|
||||||
|
waitingCoros.push({
|
||||||
|
coId: String(coId),
|
||||||
|
wakeAt: SCHEDULER.now() + (Number(delaySec) || 0) * 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Лагалейн Lua-код: определяем task.wait, wait, и обёртку для скрипта.
|
||||||
|
// Этот код выполняется ВНУТРИ Lua VM.
|
||||||
|
lua.doStringSync(`
|
||||||
|
-- task.wait(sec) → coroutine.yield(sec); main-loop вернёт через delay sec
|
||||||
|
local function rbx_wait(sec)
|
||||||
|
sec = sec or 0
|
||||||
|
coroutine.yield(sec)
|
||||||
|
return sec
|
||||||
|
end
|
||||||
|
if type(task) == 'table' then
|
||||||
|
task.wait = rbx_wait
|
||||||
|
else
|
||||||
|
task = { wait = rbx_wait }
|
||||||
|
end
|
||||||
|
wait = rbx_wait
|
||||||
|
`);
|
||||||
|
|
||||||
// === Setter Part-свойств (Position/Size/Color/...) ===
|
// === Setter Part-свойств (Position/Size/Color/...) ===
|
||||||
// Юзер пишет: part.Position = Vector3.new(0, 10, 0)
|
// Юзер пишет: part.Position = Vector3.new(0, 10, 0)
|
||||||
// В Lua-side это просто rawset. Но мы хотим, чтобы JS-сторона узнала и применила.
|
// В Lua-side это просто rawset. Но мы хотим, чтобы JS-сторона узнала и применила.
|
||||||
@ -578,7 +774,7 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
partById.clear();
|
partById.clear();
|
||||||
for (const p of prims) {
|
for (const p of prims) {
|
||||||
if (!p || p.id == null) continue;
|
if (!p || p.id == null) continue;
|
||||||
const part = newPart(p, send);
|
const part = newPart(p, send); // setters внутри шлют через send
|
||||||
part.Parent = workspace;
|
part.Parent = workspace;
|
||||||
workspace.Children.push(part);
|
workspace.Children.push(part);
|
||||||
partById.set(Number(p.id), part);
|
partById.set(Number(p.id), part);
|
||||||
@ -593,16 +789,56 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
onDataSnapshot() {},
|
onDataSnapshot() {},
|
||||||
|
|
||||||
tickScheduler(_dt) {
|
tickScheduler(_dt) {
|
||||||
|
// 0. Tweens
|
||||||
|
_stepTweens(_dt);
|
||||||
const now = SCHEDULER.now();
|
const now = SCHEDULER.now();
|
||||||
if (SCHEDULER.sleeping.length === 0) return;
|
// 1. task.delay / task.defer
|
||||||
const ready = [];
|
if (SCHEDULER.sleeping.length > 0) {
|
||||||
const rest = [];
|
const ready = [];
|
||||||
for (const t of SCHEDULER.sleeping) {
|
const rest = [];
|
||||||
if (t.wakeAt <= now) ready.push(t); else rest.push(t);
|
for (const t of SCHEDULER.sleeping) {
|
||||||
|
if (t.wakeAt <= now) ready.push(t); else rest.push(t);
|
||||||
|
}
|
||||||
|
SCHEDULER.sleeping = rest;
|
||||||
|
for (const t of ready) {
|
||||||
|
try { t.run(); } catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SCHEDULER.sleeping = rest;
|
// 2. Резюм coroutine'ов которые task.wait()
|
||||||
for (const t of ready) {
|
// Скрипт-coroutine при первом запуске yield'ит с delay (от task.wait).
|
||||||
try { t.run(); } catch (_) {}
|
// Мы регистрируем delay через __rbxl_schedule_resume? Нет — проще:
|
||||||
|
// отслеживаем последний yield-результат через coroutine.resume → возвращает
|
||||||
|
// delay. После init скрипта проверим всех coroutines: если status==suspended,
|
||||||
|
// и им пора — резюмируем.
|
||||||
|
for (const [coId, co] of coroutines) {
|
||||||
|
let entry = waitingCoros.find(w => w.coId === coId);
|
||||||
|
if (!entry) {
|
||||||
|
// Первый раз видим этот co — нужно вытащить delay из yield-результата.
|
||||||
|
// Это сделано в _onYield ниже.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.wakeAt > now) continue;
|
||||||
|
// Время резюмить
|
||||||
|
waitingCoros.splice(waitingCoros.indexOf(entry), 1);
|
||||||
|
try {
|
||||||
|
const code = `
|
||||||
|
local co = __rbxl_get_co(${JSON.stringify(coId)})
|
||||||
|
if co and coroutine.status(co) == 'suspended' then
|
||||||
|
local ok, ret = coroutine.resume(co)
|
||||||
|
if not ok then
|
||||||
|
__rbxl_send_error(${JSON.stringify(coId)}, tostring(ret))
|
||||||
|
__rbxl_unregister_coroutine(${JSON.stringify(coId)})
|
||||||
|
elseif type(ret) == 'number' then
|
||||||
|
__rbxl_schedule_resume(${JSON.stringify(coId)}, ret)
|
||||||
|
elseif coroutine.status(co) == 'dead' then
|
||||||
|
__rbxl_unregister_coroutine(${JSON.stringify(coId)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
`;
|
||||||
|
lua.doStringSync(code);
|
||||||
|
} catch (e) {
|
||||||
|
send('log', { level: 'error', text: `[coroutine resume ${coId}] ${e?.message || e}` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fireHeartbeat(dt) {
|
fireHeartbeat(dt) {
|
||||||
|
|||||||
@ -146,6 +146,38 @@ export function handleLuaCommand(_scriptId, cmd, payload, runtime) {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cmd === 'sceneCreate') {
|
||||||
|
// Lua: Instance.new("Part") + part.Parent = workspace → создание примитива.
|
||||||
|
// payload: { primId (предложенный id), type, x, y, z, sx, sy, sz, color, anchored }
|
||||||
|
try {
|
||||||
|
const pm = runtime.scene3d?.primitiveManager;
|
||||||
|
if (!pm || typeof pm.addInstance !== 'function') return;
|
||||||
|
const opts = {
|
||||||
|
id: payload?.primId,
|
||||||
|
x: payload?.x || 0, y: payload?.y || 0, z: payload?.z || 0,
|
||||||
|
sx: payload?.sx || 1, sy: payload?.sy || 1, sz: payload?.sz || 1,
|
||||||
|
color: payload?.color,
|
||||||
|
anchored: payload?.anchored !== false,
|
||||||
|
canCollide: payload?.canCollide !== false,
|
||||||
|
};
|
||||||
|
pm.addInstance(payload?.type || 'cube', opts);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[sceneCreate]', e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cmd === 'sceneDelete') {
|
||||||
|
// Lua: part:Destroy() → удаление примитива.
|
||||||
|
try {
|
||||||
|
const pm = runtime.scene3d?.primitiveManager;
|
||||||
|
if (!pm || typeof pm.removeInstance !== 'function') return;
|
||||||
|
const id = payload?.primId;
|
||||||
|
if (id != null) pm.removeInstance(Number(id));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[sceneDelete]', e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (cmd === 'partVel') {
|
if (cmd === 'partVel') {
|
||||||
try {
|
try {
|
||||||
const pm = runtime.scene3d?.primitiveManager;
|
const pm = runtime.scene3d?.primitiveManager;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user