fix(lua): universal callable-stub для unknown свойств Instance

Импортированные скрипты делают obj:Method(arg) где obj — stub. Раньше
stub был просто Folder, его как функцию вызвать нельзя → self2 is not
a function массово.

Фикс: makeCallableStub() — Proxy на function(){} который:
- вызывается как функция (apply trap) → возвращает себя;
- имеет .Connect/.Disconnect/.Wait/.Fire → ведёт себя как сигнал;
- любое .UnknownField → возвращает другой callable-stub;
- .then/.catch/.constructor → undefined (wasmoon не путается).

Этим закрывается основная масса остаточных падений в туториал-скриптах
с цепочками вроде Tool.Handle:WaitForChild('X'):Connect(...) где Handle
у нас отсутствует.
This commit is contained in:
min 2026-06-08 13:10:26 +03:00
parent dc7420a61d
commit 8fe52dbe68

View File

@ -274,6 +274,42 @@ function isProbablySignalName(prop) {
|| /(Changed|Added|Removed|Began|Ended|Clicked|Activated|Touched|Died|Loaded|Hover|Connect|Event|Signal|Reached)$/.test(prop); || /(Changed|Added|Removed|Began|Ended|Clicked|Activated|Touched|Died|Loaded|Hover|Connect|Event|Signal|Reached)$/.test(prop);
} }
// Callable stub: функция которая ничего не делает + поля Connect/Wait/Fire,
// чтобы выглядеть и как метод (`obj:Method()`), и как сигнал (`sig:Connect()`),
// и как объект (`obj.Property`).
function makeCallableStub(name) {
// Используем function чтобы Proxy мог иметь apply trap
const fnTarget = function () { return fnTarget; };
fnTarget.__stubName = name || 'stub';
fnTarget.Connect = function () { return { Disconnect: () => {}, disconnect: () => {}, Connected: false }; };
fnTarget.connect = fnTarget.Connect;
fnTarget.Wait = function () { return undefined; };
fnTarget.wait = fnTarget.Wait;
fnTarget.Fire = function () { return undefined; };
fnTarget.fire = fnTarget.Fire;
fnTarget.Disconnect = function () {};
fnTarget.disconnect = fnTarget.Disconnect;
// Proxy чтобы любое доступ к unknown полю/индексу возвращал тот же stub
return new Proxy(fnTarget, {
get(t, prop) {
if (Object.prototype.hasOwnProperty.call(t, prop) || prop in t) {
return t[prop];
}
if (typeof prop !== 'string') return undefined;
if (prop === 'then' || prop === 'catch' || prop === 'finally' ||
prop === 'toJSON' || prop === 'constructor' || prop === 'prototype' ||
prop.startsWith('__') || prop.startsWith('Symbol')) {
return undefined;
}
const child = makeCallableStub(prop);
t[prop] = child;
return child;
},
set(t, prop, value) { t[prop] = value; return true; },
apply() { return fnTarget; }, // obj() возвращает себя
});
}
function newInstance(className, name) { function newInstance(className, name) {
const m = makeInstanceMethods(); const m = makeInstanceMethods();
const target = { const target = {
@ -318,15 +354,10 @@ function newInstance(className, name) {
prop.startsWith('__') || prop.startsWith('Symbol')) { prop.startsWith('__') || prop.startsWith('Symbol')) {
return undefined; return undefined;
} }
// Эвристика: имена сигналов → stub-сигнал. Иначе — stub-Folder. // Callable-stub: можно вызвать как функцию, как сигнал (:Connect),
// Stub-Folder сам по себе callable (на случай если скрипт его вызовет // как объект (.Property), как child Instance (.WaitForChild). Не падает
// как функцию: `foo()` вместо `foo:Connect()` — оба не падают). // на любом обращении со стороны импортированных скриптов.
let stub; const stub = makeCallableStub(prop);
if (isProbablySignalName(prop)) {
stub = makeStubSignal();
} else {
stub = newInstance('Folder', prop);
}
t[prop] = stub; t[prop] = stub;
return stub; return stub;
}, },