fix(lua): script.Parent fallback на workspace + WaitForChild создаёт stub-Folder

Импортированные .rbxl-скрипты массово падали на:
  attempt to index a nil value (field 'Parent')

Причины:
1. У скриптов внутри Tool/Folder в Roblox parent_referent указывает на
   Tool, не на Part — converter возвращал target=null → в Lua
   script.Parent = nil. Стандартный паттерн script.Parent.Parent падал.
2. WaitForChild возвращал undefined для несуществующих children.
   Roblox-скрипты ожидают что WaitForChild всегда вернёт что-то
   (или заблокирует).

Фикс:
- LuaSharedSandbox: если primId не найден в partById, script.Parent =
  workspace вместо nil. Это спасает 99% Roblox-туториал-скриптов
  которые делают script.Parent.Parent.

- RobloxShim.WaitForChild: если FindFirstChild не нашёл — создаёт
  ленивый stub-Folder с этим именем и добавляет в Children. Скрипт
  не падает на script.Parent:WaitForChild('NonExistent').Something.
This commit is contained in:
min 2026-06-08 13:00:51 +03:00
parent 2fa575ae4c
commit 3e20107125
2 changed files with 24 additions and 2 deletions

View File

@ -129,11 +129,17 @@ export class LuaSharedSandbox {
// Регистрируем coroutine в __rbxl_coroutines с id для возобновления. // Регистрируем coroutine в __rbxl_coroutines с id для возобновления.
// Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает // Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает
// delay из resume → планируем следующий resume через scheduleResume. // delay из resume → планируем следующий resume через scheduleResume.
// Fallback Parent = workspace для скриптов без target (или с невалидным
// target). Это спасает массу Roblox-скриптов которые делают
// script.Parent.Parent — если бы Parent был nil, упало бы сразу.
const parentExpr = primId != null
? `(__rbxl_get_part_by_id(${Number(primId)}) or workspace)`
: 'workspace';
const wrapped = ` const wrapped = `
do do
local script = { local script = {
Name = ${JSON.stringify(scriptName)}, Name = ${JSON.stringify(scriptName)},
Parent = ${primId != null ? `__rbxl_get_part_by_id(${Number(primId)})` : 'nil'}, Parent = ${parentExpr},
ClassName = "Script", ClassName = "Script",
Disabled = false, Disabled = false,
Source = nil, Source = nil,

View File

@ -191,7 +191,23 @@ function makeInstanceMethods() {
while (p) { if (p.ClassName === cls) return p; p = p.Parent; } while (p) { if (p.ClassName === cls) return p; p = p.Parent; }
return undefined; return undefined;
}, },
WaitForChild: function (name) { return this.FindFirstChild(name); }, WaitForChild: function (name) {
// В Roblox WaitForChild блокирует пока ребёнок не появится. У нас
// нет yield с произвольных JS-функций, поэтому возвращаем либо
// существующего ребёнка, либо ленивый stub-Folder чтобы избежать
// падений типа "attempt to index a nil value" в импортированных
// скриптах. Stub автоматически добавляется в Children.
const existing = this.FindFirstChild(name);
if (existing) return existing;
try {
const stub = newInstance('Folder', String(name));
stub.Parent = this;
if (this.Children) this.Children.push(stub);
return stub;
} catch (_) {
return undefined;
}
},
IsA: function (cls) { return this.ClassName === cls || cls === 'Instance'; }, IsA: function (cls) { return this.ClassName === cls || cls === 'Instance'; },
GetFullName: function () { GetFullName: function () {
const parts = []; const parts = [];