fix(import): YXZ Euler + watchdog для tight-loop защиты

1. CFrame.to_euler_xyz переписан под Babylon YXZ convention:
   rx = asin(-r12), ry = atan2(r02, r22), rz = atan2(r10, r11).
   Раньше извлекал XYZ-Euler → Babylon применял как YXZ → клины,
   мостики, наклонные постройки рендерились повёрнутые
   (примеры из ROBLOX Battle: мостик торчал в стену).
   Учтён gimbal-lock на X=±90°.

2. Lua watchdog в _startSingleScript и __rbxl_drain_handler:
   debug.sethook(yield_50ms, '', 50000) — каждые 50k Lua-инструкций
   принудительно yield 1 кадр. Защищает от:
     while not workspace:FindFirstChild('X') do
       workspace.ChildAdded:wait()
     end
   где наш stub :wait() возвращает -1 мгновенно — раньше скрипт
   подвешивал вкладку (50k+ итераций в секунду). Сейчас yield'ит,
   tickScheduler возобновляет.

3. Signal.Wait возвращает -1 как 'no-arg yield marker'. Сейчас
   не используется в Lua, но если позже сделаем wrapper — будет.

ROBLOX Battle карта (arch1_ROBLOX_Battle_v2.rbxl, 1677 примитивов,
66 скриптов) — теперь не должна подвешивать.

Деплой rbxl_types.py на VM 130.
This commit is contained in:
min 2026-06-08 19:16:39 +03:00
parent 71d6396d8b
commit dbfd214f42
4 changed files with 41 additions and 9 deletions

View File

@ -113,18 +113,28 @@ class CFrame:
matrix: tuple # (r00, r01, r02, r10, r11, r12, r20, r21, r22)
def to_euler_xyz(self) -> tuple:
"""Конверт 3x3 rotation matrix в Euler XYZ (radians).
"""Конверт 3x3 rotation matrix в Euler YXZ (Babylon convention).
Использует стандартную intrinsic XYZ rotation extraction:
Rx = atan2(r21, r22)
Ry = atan2(-r20, sqrt(r21² + r22²))
Rz = atan2(r10, r00)
Babylon mesh.rotation = Vector3(rx, ry, rz) применяется в порядке YXZ
(rotate Y first, then X, then Z). Чтобы извлечь Euler из матрицы под
этот convention, используем формулу YXZ-extraction:
Rx = asin(-r12)
Ry = atan2(r02, r22)
Rz = atan2(r10, r11)
(имя метода to_euler_xyz сохраняем для совместимости вызовов.)
"""
import math
r00, r01, r02, r10, r11, r12, r20, r21, r22 = self.matrix
rx = math.atan2(r21, r22)
ry = math.atan2(-r20, math.sqrt(r21*r21 + r22*r22))
rz = math.atan2(r10, r00)
# Edge case: r12 близко к ±1 (gimbal lock на X = ±90°)
clamped = max(-1.0, min(1.0, -r12))
rx = math.asin(clamped)
if abs(clamped) > 0.99999:
# Gimbal lock — z = 0, y = atan2(-r20, r00)
ry = math.atan2(-r20, r00)
rz = 0.0
else:
ry = math.atan2(r02, r22)
rz = math.atan2(r10, r11)
return (rx, ry, rz)

View File

@ -183,6 +183,15 @@ export class LuaSharedSandbox {
Source = nil,
}
local co = coroutine.create(function()
-- WATCHDOG: каждые 50000 инструкций yield 1 кадр.
-- Защищает от tight-loop типа:
-- while not parent:FindFirstChild(name) do
-- parent.ChildAdded:wait()
-- end
-- где наш stub :wait() возвращает сразу.
debug.sethook(function()
coroutine.yield(0.016)
end, "", 50000)
-- pcall защищает от runtime-ошибок которые иначе крашат
-- coroutine и могут повредить WASM-стейт. Возвраты
-- handler'а намеренно поглощаются.

View File

@ -58,7 +58,10 @@ function makeSignal() {
}
};
sig.fire = sig.Fire;
sig.Wait = () => undefined;
// Wait() возвращает -1 как маркер "yield 1 кадр" — наш Lua-prelude
// оборачивает все Signal:Wait через __rbxl_signal_wait который при
// получении -1 делает rbx_wait(0.05) (yield в coroutine).
sig.Wait = () => -1;
sig.wait = sig.Wait;
return sig;
}
@ -1385,6 +1388,12 @@ export function registerRobloxShim(lua, opts) {
local ret = coroutine.yield(sec)
return ret or sec
end
-- Глобальный безопасный yield для любых stub-сигналов / любых
-- "ждунов". Используется в Lua-обёртках вокруг WaitForChild и т.п.
function __rbxl_yield_frame()
coroutine.yield(0.05)
end
if type(task) == 'table' then
task.wait = rbx_wait
else
@ -1413,6 +1422,10 @@ export function registerRobloxShim(lua, opts) {
-- что приводит к wasmoon promise-detection crash). pcall возвращает
-- (ok, ret1, ret2, ...) мы их не используем.
local co = coroutine.create(function()
-- Тот же watchdog что и в _startSingleScript
debug.sethook(function()
coroutine.yield(0.016)
end, "", 50000)
pcall(fn, a1, a2, a3, a4)
end)
__rbxl_register_coroutine(handlerId, co)