fix(lua): Proxy для Instance — unknown свойства возвращают stub вместо nil

Импортированные Roblox-скрипты массово падали на доступе к свойствам которых
у нас нет (.Selected, .Equipped, .MouseEnter и т.д.). В Roblox это сигналы
которые скрипты подключают через :Connect().

Фикс: оборачиваю newInstance() в Proxy:
- isProbablySignalName(prop) → возвращает makeStubSignal() (наш Signal с Connect/Fire)
- иначе → возвращает stub-Folder (тоже Instance с потенциальными children)
- системные ключи (then, __*, Symbol) → undefined чтобы wasmoon не путался

Эвристика покрывает основные Roblox-паттерны:
- *.Changed, *.Added, *.Removed, *.Began, *.Ended, *.Touched, *.Died
- Mouse*, Touch*, Input*, Render*, Step*, Heart*, On*, Char*, Player*
- Selected, Deselected, Equipped, Unequipped, Activated, Reached, Loaded

Это позволяет проходить инициализацию массе туториал-скриптов вместо
падения на первой же строке.
This commit is contained in:
min 2026-06-08 13:04:38 +03:00
parent 3e20107125
commit 59d0d86811

View File

@ -241,9 +241,27 @@ function makeInstanceMethods() {
return _instanceMethods;
}
// Создаёт stub-signal который ничего не делает — для unknown свойств Instance
// которые скрипты пытаются использовать как сигнал (script.Parent.Selected:Connect).
function makeStubSignal() {
const sig = makeSignal();
// Помечаем чтобы знать что это stub (для возможной отладки)
sig.__stub = true;
return sig;
}
// Эвристика: какие имена свойств вероятно сигналы?
// В Roblox сигналы заканчиваются на: Changed, Added, Removed, Began, Ended,
// Clicked, Activated, Touched, Selected, Deselected, Equipped, Unequipped, и т.д.
function isProbablySignalName(prop) {
if (typeof prop !== 'string') return false;
return /^(Mouse|Touch|Input|Render|Step|Heart|Render|On|Char|Player|Selected|Deselect|Equipped|Unequipped|Activated|Click|Changed|Added|Removed|Began|Ended|Died|Spawned|Reached|Loaded|Hover)/.test(prop)
|| /(Changed|Added|Removed|Began|Ended|Clicked|Activated|Touched|Died|Loaded|Hover|Connect|Event|Signal|Reached)$/.test(prop);
}
function newInstance(className, name) {
const m = makeInstanceMethods();
return {
const target = {
ClassName: className || 'Instance',
Name: name || className || 'Instance',
Parent: undefined,
@ -269,6 +287,35 @@ function newInstance(className, name) {
SetAttribute: m.SetAttribute,
GetPropertyChangedSignal: m.GetPropertyChangedSignal,
};
// Proxy: для unknown свойств возвращаем stub чтобы скрипты не падали.
return new Proxy(target, {
get(t, prop) {
if (prop in t) return t[prop];
if (typeof prop !== 'string') return undefined;
// Системные/wasmoon-внутренние ключи — undefined чтобы wasmoon не путался
if (prop === 'then' || prop === 'catch' || prop === 'toJSON' ||
prop === Symbol.toPrimitive || prop.startsWith('__')) {
return undefined;
}
if (isProbablySignalName(prop)) {
const stub = makeStubSignal();
t[prop] = stub;
return stub;
}
// Иначе — child stub (Folder) который тоже выживет на чтении свойств
// и может иметь свои дочерние stub'ы. Cache в target чтобы тот же ссылочно.
const childStub = newInstance('Folder', prop);
t[prop] = childStub;
return childStub;
},
set(t, prop, value) {
t[prop] = value;
return true;
},
has(t, prop) {
return prop in t || (typeof prop === 'string' && !prop.startsWith('__'));
},
});
}
/**