# C++ gameplay и метаданные: от объявления поля до безопасного изменения игры Дата: 17 сентября 2026. Это углубление исследований [Godot UX](08-godot-ux-source-study.md), [Unity extensions](09-unity-ux-extensibility-study.md), [Blender RNA/operators](10-blender-editor-patterns.md) и [ECS](11-ecs-and-ergonomics.md). Здесь исследуется нижележащий контракт: как описание C++-типа становится доступным инструментам, как вызывается код, что переживает пересборку и кто отвечает за время жизни данных. **Принятые решения 18.09.2026:** C++ core/gameplay первым, затем Lua отдельным модулем (необязательным для конкретной игры); EnTT; собственная явная C++ metadata registration; статическая gameplay-библиотека в отдельном Player dev/release; schema export отдельным служебным процессом; custom inspectors в Editor DLL/SO под точный SDK с restart. **MCP только в Editor/headless editor services, без runtime inspection/mutation и без MCP в Player, игре или SchemaExporter.** Канон — [архитектура](../ARCHITECTURE.md), этапы — [PLAN.md](../../PLAN.md). Исследование ниже объясняет решения, но не является реализацией или отчётом о тестах движка. Исходники сверены с [manifest](source-manifest.json): UE **5.8.2**, `16d75d84714512edfb744e1fd0a59e9c74d57873`; Godot **4.8.0 dev**, `9c776068d6ed23acd0c78bfe534272d1d2a3a619`. Оба checkout остались чистыми. Изучены тела функций и генератора; движки и тестовый gameplay-модуль не собирались. Дополнительно прочитаны официальные документы UE 5.8, Lua 5.5 и текущий Godot docs source, commit `e1129cb5c3f27e0e3a12e816ae405ab5ce032e3f`. Последний нужен потому, что разделы GDExtension перемещены относительно прежних URL; это документация разработки, а не гарантия поведения любого стабильного Godot. Главный вывод: **небольшая явная схема экспортируемого API важнее универсальной reflection**. Нужны отдельно идентичность данных, способ доступа к памяти текущего бинарного модуля и правила исполнения операций. Inspector, MCP и будущий Lua используют выбранную часть этой схемы; обычный C++ сохраняет типизированные вызовы и не обязан превращать каждое присваивание в динамический вызов. ## 1. UE: генерация дескриптора связывает C++ layout с выбранным набором возможностей **Путь исполнения.** UBT запускает UHT до обычного C++-компилятора: UHT разбирает размеченные заголовки и генерирует код UObject-системы, затем компилятор собирает результат. Это отдельная стадия сборки, а не обнаружение произвольных типов уже работающей программы. Поддерживаемый механизм стороннего расширения UHT — exporters; возможность добавить свой exporter не означает возможность без ограничений расширить весь язык описаний UE. [Официальный UHT](https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-header-tool-for-unreal-engine). У `UhtProperty.Validate` есть проверки допустимости описания: например, статические массивы контейнеров отклоняются. `ValidateMember` проверяет согласованность edit-флагов и требует Category для определённых экспортируемых полей engine-модулей. Это **валидация объявления**, а не проверка каждого будущего значения `Health`. [UhtProperty.cs:2367](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Programs/Shared/EpicGames.UHT/Types/UhtProperty.cs#L2367), [ValidateMember:2547](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Programs/Shared/EpicGames.UHT/Types/UhtProperty.cs#L2547). Далее `AppendParamsDefStart` выводит имя поля, property flags, указатели на доступные getter/setter wrappers, размер массива и `STRUCT_OFFSET`. В Params-пути `ConstructUClassHelper` строит зависимости, регистрирует класс, связывает функции, вызывает `ConstructFProperties`, а затем `StaticLink`. `ConstructFProperty` выбирает специализированный тип свойства и рекурсивно создаёт дочерние описания контейнеров. `NewFProperty` выбирает вариант с accessor-функциями, если они заданы; конструктор `FProperty` сохраняет offset текущего бинарного layout. [Генерация параметров](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Programs/Shared/EpicGames.UHT/Types/UhtProperty.cs#L1643), [конструирование класса](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectConstructInternal.h#L213), [создание свойств](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectGlobals.cpp#L6175), [сохранение offset](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Property.cpp#L774). В этом checkout нельзя описывать Params как единственный актуальный механизм. `AppendPropertiesDecl/Defs` отдельно поддерживают **ConstInit** и **Params**, с условием `UE_WITH_CONSTINIT_UOBJECT`. ConstInit-генерация тоже содержит имя, флаги, связи и `STRUCT_OFFSET`, но создаёт непосредственно инициализируемые описания. Здесь проверены обе ветки генератора; активная конфигурация конкретной сборки не устанавливалась. [Выбор формата](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Programs/Shared/EpicGames.UHT/Exporters/CodeGen/UhtHeaderCodeGeneratorCppFile.cs#L1758), [ConstInit-параметры](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Programs/Shared/EpicGames.UHT/Types/UhtProperty.cs#L1887). Методам тоже требуется glue code. `AppendFunctionThunk` генерирует получение аргументов, завершение разбора параметров, вызов C++-реализации и запись результата. Дополнительная проверка `_Validate` здесь включается для `NetValidate`, а не для каждого экспортированного метода. Следовательно, наличие thunk само по себе не даёт доменную валидацию, транзакцию или право вызывать метод из редактора. [Генерация thunk](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Programs/Shared/EpicGames.UHT/Exporters/CodeGen/UhtHeaderCodeGeneratorCppFile.cs#L3483). **Что перенять.** Для Faset принята явная регистрация выбранных компонентов, полей и методов в C++ gameplay-модуле. Шаблонные helpers могут принимать типизированные member pointers/accessors и проверять поддерживаемые сигнатуры компилятором. Не начинать с собственного парсера всего C++; codegen поверх ограниченных аннотаций имеет смысл позже, когда повторяющиеся регистрации станут измеримой проблемой. Сгенерированные файлы должны зависеть от исходной схемы, версии генератора и конфигурации, попадать в build diagnostics и обновляться только при изменении содержимого. Отдельно хранить UI-подсказки и обязательные runtime-правила. UE прямо предупреждает, что metadata предназначена для editor; просмотренный codegen ограждает её `WITH_METADATA`, тогда как runtime property flags и типизированные описания остаются отдельными сущностями. Нельзя строить правило «отрицательная масса запрещена» только на строке для slider. [Metadata specifiers](https://dev.epicgames.com/documentation/en-us/unreal-engine/metadata-specifiers-in-unreal-engine), [условное добавление metadata](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectGlobals.cpp#L6189). ## 2. UE: загрузка свойства — сопоставление схем, а не копирование старого layout **Путь исполнения.** `SerializeTaggedProperties` выбирает versioned либо unversioned-представление архива. Ниже исследован именно **versioned tagged path**, поэтому его особенности нельзя приписывать всем cooked-данным UE. [Развилка сериализации](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L1476). При загрузке `SerializeVersionedTaggedProperties` читает `FPropertyTag`, сначала сопоставляет его ожидаемому свойству, затем при несовпадении ищет по имени. При разрешённых условиях применяются property redirects. GUID-ветка также условная: код отдельно отмечает доступность property GUID для поддерживающих их классов, в частности Blueprint-generated classes. **Это не доказательство наличия постоянного GUID у каждого native UPROPERTY.** [Чтение и сопоставление](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L1620), [redirect имени](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L1685). После сопоставления проверяются editor-only policy, индекс массива и `ShouldSerializeValue`. Затем вызывается `ConvertFromType`: результат может означать выполненное преобразование, собственную десериализацию, обычный `SerializeItem` либо невозможность преобразования. Только у подходящего свойства вычисляется **нынешний** адрес через `ContainerPtrToValuePtr`, после чего читается значение. При несовпадении типа есть диагностический путь; обработка неизвестных свойств имеет дополнительные editor-only условия и не равна обещанию автоматически сохранить произвольные потерянные поля. [Проверки и преобразование](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L1793), [адрес текущего поля](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L1843), [unknown-property tracking](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L1867). **Что перенять.** В authoring-формате Faset определить `TypeId`, `FieldId` и версию схемы независимо от C++-имени, `typeid`, offset и порядка деклараций. Читаемая подпись и путь к исходнику — изменяемые атрибуты. Fingerprint текущего layout полезен для проверки совместимости бинарного модуля, но не должен становиться постоянной идентичностью сохраняемого поля. Переименование `open_speed` в `angular_speed` при неизменном смысле сохраняет `FieldId`; смена единиц с градусов на радианы требует явной миграции значения. Разделение одного поля на несколько требует миграции с доступом к документу. Новые поля получают defaults по правилам версии, удалённые — явную политику сохранения/удаления неизвестных данных. Один UUID этого не решает. Миграцию следует выполнять над копией authoring-документа, валидировать результат и публиковать целиком, с диагностикой и возможностью отката. Отсутствующий gameplay-пакет должен оставлять читаемые непрозрачные записи компонентов и их ссылки, а не превращать открытие и сохранение сцены в потерю данных. Запуск сцены может быть запрещён до восстановления обязательного типа. Это предлагаемая политика Faset, а не универсальное поведение изученных UE-архивов. Cooked-представление можно уплотнять после миграции и проверки; оно привязывается к конкретному schema/build fingerprint и при несовместимости пересобирается. ## 3. Reflection и владение объектами — связанные, но разные механизмы **Путь исполнения.** UE `FObjectProperty::EmitReferenceInfo` добавляет в GC schema позиции объектных ссылок, учитывая offset и элементы массива. Базовая реализация `FProperty::EmitReferenceInfo` пустая: понимание того, что поле содержит объектную ссылку, обеспечивается специализированным descriptor. Это полезный пример того, что тип свойства участвует не только в Inspector. [GC reference schema](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/GarbageCollection.cpp#L6833). Но слабая ссылка решает другую задачу. `FWeakObjectPtr` при присваивании сохраняет index/serial; при разрешении проверяет соответствие текущему элементу object array, а `Internal_Get` дополнительно проверяет пригодность объекта. В checkout есть отдельная конфигурация remote-object handles; приведённое объяснение index/serial относится к обычному локальному пути. [Создание weak handle](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Private/UObject/WeakObjectPtr.cpp#L29), [сопоставление serial](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Public/UObject/WeakObjectPtr.h#L428), [разрешение указателя](https://github.com/EpicGames/UnrealEngine/blob/16d75d84714512edfb744e1fd0a59e9c74d57873/Engine/Source/Runtime/CoreUObject/Public/UObject/WeakObjectPtr.h#L560). Godot даёт независимую проверку идеи: `ObjectDB::get_instance` извлекает slot и validator из ObjectID, возвращает null при несовпадении и освобождает spinlock перед возвратом указателя. Это проверка идентичности, а не автоматически удерживаемая блокировка на всё дальнейшее использование объекта. [ObjectDB lookup](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/object.h#L914). **Что перенять.** Не нужно копировать UObject GC, чтобы получить удобный C++ gameplay. Нужны явные категории: значение компонента, ссылка на entity, удерживаемый ресурс, наблюдаемая слабая ссылка и временный доступ к памяти. При принятом EnTT descriptor поля не вправе хранить постоянный pointer на компонент: structural change может переместить данные, что подробно разобрано в [исследовании ECS](11-ecs-and-ergonomics.md). Принятый runtime handle включает world/session и generation; постоянная ссылка authoring-документа использует другую идентичность. Runtime handles не открываются для inspection/mutation через MCP. Доступ к компоненту разрешается на время фазы/заимствования, а дальнейшее использование требует повторного resolve. Проверка handle не заменяет scheduler и владение при многопоточности. Подписки, отложенные callbacks и Lua userdata должны хранить проверяемую ссылку либо явно владеть ресурсом; завершение сессии инвалидирует её даже при повторном использовании локального index. ## 4. Godot: поле регистрируется через методы, а ошибка вызова не заменяет проверку смысла **Путь исполнения.** У `Light3D::_bind_methods` зарегистрирован `set_param`; несколько полей света используют `ADD_PROPERTYI` с одним setter/getter и разными индексами параметра. Например, `light_energy` описан как float с range hint. Это конкретный способ не писать отдельный универсальный getter/setter для каждой записи схемы. [Регистрация метода](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/3d/light_3d.cpp#L362), [описания полей](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/3d/light_3d.cpp#L413). В данном dev-снимке `ClassDB::add_property` передаёт описание в **GDType**, поэтому сводить реализацию только к старым таблицам ClassDB было бы неточно. `GDType::add_property` разрешает регистрацию в mutable-фазе и на потоке владельца, отклоняет дубликаты, находит MethodBind getter/setter, проверяет число аргументов с учётом индекса и сохраняет property record. Отдельный ordered list нужен перечислению. [ClassDB bridge](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/class_db.cpp#L1339), [GDType registration](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/gdtype.cpp#L192). `Object::set` сначала предлагает изменение script-instance и extension callbacks, затем вызывает native setter. `set_native` ищет свойство по имени, формирует один аргумент либо пару «индекс + значение» и вызывает сохранённый MethodBind. `r_valid` определяется через `Callable::CallError`. У `Light3D::set_param` проверяется индекс, затем значение сохраняется, передаётся RenderingServer, при нужных параметрах обновляются gizmos/warnings. Здесь range hint не читается и не превращается автоматически в clamp входного числа. [Общий set](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/object.cpp#L198), [native dispatch](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/object.cpp#L349), [setter света](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/3d/light_3d.cpp#L40). Ещё одна важная граница: `Object::validate_property(PropertyInfo&)` вызывает native/extension/script callbacks, которые могут изменить **тип, hint, имя и usage описания**. Эта функция не получает новое значение свойства. `ClassDB::get_property_list` использует её при перечислении для конкретного объекта. Название `_validate_property` поэтому нельзя понимать как единый валидатор входных данных. [Изменение PropertyInfo](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/object.cpp#L623), [валидация при перечислении](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/object/class_db.cpp#L1376). **Что перенять.** Разделить минимум три контракта: `describe(context)` для представления и доступности поля; чистый `validate(proposed_state)` для типа, диапазона и связанных инвариантов; `commit(changes)` для публикации и уведомлений. Если setter меняет второе поле, посылает renderer-команду или событие, простой вызов нескольких setters не даёт атомарный MCP batch и надёжный Undo. В Faset UI и MCP должны валидировать один proposed authoring-state до commit, а дорогую invalidation собирать после него. Gameplay hot loop при этом вправе работать напрямую с разрешёнными компонентами: не нужно прогонять каждое обновление Position через editor transaction. Доменные операции вроде «применить импульс» и «перестроить collider» отличаются от редактирования сохраняемого параметра; публичный API обязан выражать это явно. Официальный C++-пример Godot показывает ручную регистрацию methods, properties и signals и появление полей в Inspector после компиляции. Это подтверждает практичность явного экспортируемого подмножества C++, но не автоматическую поддержку любого класса или шаблона. [Текущий пример в Godot docs](https://github.com/godotengine/godot-docs/blob/e1129cb5c3f27e0e3a12e816ae405ab5ce032e3f/tutorials/scripting/cpp/gdextension_cpp_example.rst). ## 5. GDExtension: ABI требует версий и правил восстановления, даже когда функции уже описаны **Путь исполнения.** `GDExtensionLibraryLoader::initialize` получает entry symbol из динамической библиотеки и вызывает функцию инициализации с `get_proc_address`, library pointer и структурой результата. Официальная документация разделяет C interface, экспортируемый API и `.gdextension`-описание загрузки. Это организованная граница между движком и библиотекой, не загрузка произвольного C++-объекта по его имени. [Инициализация библиотеки](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension_library_loader.cpp#L221), [устройство GDExtension](https://github.com/godotengine/godot-docs/blob/e1129cb5c3f27e0e3a12e816ae405ab5ce032e3f/engine_details/engine_api/gdextension/what_is_gdextension.rst). `_register_extension_class_method` создаёт GDExtensionMethodBind и передаёт его в ClassDB. `update` копирует callback pointers, argument/return descriptions и defaults. `call` передаёт аргументы extension-функции и преобразует её error record обратно в `Callable::CallError`; `validated_call`/`ptrcall` образуют отдельные пути для уже подготовленных данных. Эти пути не означают одинаковую стоимость и одинаковый набор проверок. При получении engine method bind API учитывает hash сигнатуры и compatibility fallback; несовместимый метод не просто вызывается по совпавшему имени. [Регистрация](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L574), [описание и dispatch](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L100), [копирование method info](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L194), [signature lookup](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension_interface.cpp#L1633). Reload особенно показателен. `try_update` проверяет static/vararg, наличие и тип результата, число и типы аргументов. При несовместимости старая привязка помечается invalid и создаётся новая; вызов invalid bind в tools-сборке имеет отдельную ошибку. `prepare_reload` сохраняет выбранные `PROPERTY_USAGE_STORAGE` значения, пропуская часть defaults/nulls. `finish_reload` восстанавливает extension-части и затем вызывает `set` для сохранённых свойств. Смена parent type при регистрации отдельно отклоняется с требованием restart. Это **восстановление через описанные свойства и специальные lifecycle hooks**, не универсальное сохранение всей native памяти. [Совместимость метода](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L161), [состояние до reload](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L916), [восстановление](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L999), [ограничение parent type](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/extension/gdextension.cpp#L473). Документация ориентирует расширения на совместимость с более поздними minor-версиями, указывает исключение Godot 4.0 и требует согласованной floating-point precision; для custom engine предлагается генерировать свой API description. Это поддерживаемая политика с условиями, а не обещание вечной совместимости любого бинарного файла. [Godot-cpp compatibility](https://github.com/godotengine/godot-docs/blob/e1129cb5c3f27e0e3a12e816ae405ab5ce032e3f/tutorials/scripting/cpp/about_godot_cpp.rst). **Принято для Faset.** Gameplay — отдельная **статическая библиотека, связанная с Player в development и release**. Linux использует Clang, Windows — clang-cl с согласованными SDK/CRT. Editor plugins поставляются DLL/SO под конкретный Editor SDK/build и загружаются при запуске; обновление требует restart Editor. Manifest задаёт module ID/version, зависимости, назначение runtime/editor и build fingerprint. Entry function согласует API version, ownership и ошибки; произвольные compiler/stdlib combinations не получают обещания стабильного ABI. EnTT registry не экспортируется как универсальный native plugin ABI. Принятый Play: **stop → build → restart**. Сохранение существующих vtables, component pointers, closures, задач и layout не обещается. Native editor plugin способен уронить Editor; отдельный Player изолирует игровой код, который действительно остаётся вне редактора. **Schema export после сборки — отдельная обязательная стадия.** Служебная target `SchemaExporter` связывается с теми же C++ registration units, выполняет регистрацию без мира, renderer и lifecycle callbacks и пишет декларативный manifest: TypeId/FieldId, версии, value types, явные defaults, constraints, units, hints и source locations. Editor запускает процесс, проверяет exit status/manifest/build fingerprint и лишь затем публикует новую схему Inspector. Сбой оставляет последнюю корректную схему и diagnostics. C++ регистрация всё же исполняется, поэтому отдельный процесс не называется sandbox; произвольные static initializers нельзя считать безопасными только из-за имени режима. **MCP в этом процессе отсутствует.** Custom Inspector реализуется отдельным Editor module над authoring command API. Декларативные constraints проверяет AuthoringService; исполняемые дополнительные валидаторы/миграции требуют явного editor/helper-контракта. Inspector не вызывает runtime accessor по адресу из manifest и не загружает gameplay DLL ради перечисления полей. Сборка отбрасывает зависимости runtime от editor/MCP; JSON схемы не содержит pointers или сериализованных C++ layouts. ## Принятый контракт Faset: C++ первым, Lua следующим этапом **Компонент.** `TypeId`, version, module owner, зависимости, правила создания/удаления и список полей. Отдельно — runtime size/alignment, callbacks construction/destruction/move и registration generation. Выбран EnTT, но descriptor отделён от его runtime storage и не сериализует registry. Для полиморфных или нестандартных layout использовать типизированные accessors; не публиковать вычисленные offsets через MCP. **Поле.** `FieldId`, переносимый value type, default, единицы, hard constraints, UI hints, read/write policy и категории invalidation. AssetRef и SceneEntityRef — явные типы, не произвольные строки. Коллекциям нужны правила адресации элементов и конфликтов после вставки/удаления. В JSON сохраняются смысловые значения и ID; memory adapters принадлежат конкретной сборке. **Метод.** `MethodId`, именованные аргументы/результат, error schema, домен исполнения, требуемая фаза, side effects и lifetime результата. Обычный native метод не получает автоматически права editor-команды. `OpenDoor` — runtime-действие gameplay/Lua, `SetDoorDefaults` — authoring-транзакция, `InspectDoorAuthoring` — запрос к документу. MCP может вызывать только editor authoring-контракты; runtime method exposure не предоставляется. Общая metadata не объединяет права и домены исполнения. **Событие.** `EventId`, payload schema, фаза доставки и ownership подписки. Начать с очереди копируемых payload и scope, освобождаемого при удалении поведения/мира. Listener не удерживает случайный pointer на переехавший компонент; callback другого языка не вызывается из произвольного потока. Metadata описывает payload, но сама не выбирает порядок, рекурсию и гарантию доставки. **Ручное программирование.** Отдельный `Gameplay` модуль, понятный typed API и обычный C++ debugger; минимальная регистрация нужна только инструментально доступной части. Для двери автор пишет реакцию на взаимодействие и целевой угол, для тысячи объектов — batch system. Метаданные дают Inspector и `schema.describe` один источник имён, типов и документации, но MCP вызывает только authoring/build/import/PlayStop/editor-log операции из [контракта автоматизации](12-mcp-and-blender-integration.md), без произвольного native address и без runtime world inspection/mutation. **Lifecycle.** C++-поведения используют `OnStart`, `FixedUpdate`, `Update`, `LateUpdate`, `OnDestroy`; Lua повторит те же фазы. Первый scheduler последовательный. Spawn/despawn/add/remove записываются в очередь и применяются в начале следующего fixed tick после завершения прежних задач. `OnStart` идёт после создания, `OnDestroy` — до освобождения допустимых данных; подписки имеют ограниченный lifetime. Fixed tick 60 Гц по умолчанию, bounded catch-up и render interpolation описаны в [архитектуре](../ARCHITECTURE.md). **Lua после C++.** Реализация Lua-модуля принята как следующий этап после C++ gameplay; конкретный срок определяется отдельно. Необязательным остаётся использование Lua отдельной игрой. На первом этапе проектируем пригодную границу для его подключения. Модуль добавит VM, conversion wrappers, binding registry, сообщения об ошибках и scope подписок. Экспортируются явно разрешённые компоненты/методы; templates, arbitrary pointers и все C++ overloads не становятся скриптовым API автоматически. Генерация справки/IDE declarations полезна, но не заменяет runtime-проверки. Lua full userdata предоставляет память для wrapper, а `luaL_checkudata` проверяет его metatable type; это не проверка существования entity. Поэтому wrapper хранит handle и повторно разрешает его при обращении. `lua_pcall` возвращает ошибку защищённого вызова, а message handler позволяет собрать traceback. Lua использует `longjmp` либо C++ exceptions в зависимости от сборки: bindings должны явно учитывать эту границу и не рассчитывать, что RAII cleanup автоматически сработает через любой Lua error. [Lua 5.5: userdata](https://www.lua.org/manual/5.5/manual.html#lua_newuserdatauv), [проверка wrapper](https://www.lua.org/manual/5.5/manual.html#luaL_checkudata), [ошибки](https://www.lua.org/manual/5.5/manual.html#4.4), [protected call](https://www.lua.org/manual/5.5/manual.html#lua_pcall). Это адаптация для Faset, не доказательство безопасности будущего binding. Lua GC освобождает wrapper по его правилам, а игровая entity живёт по правилам мира; владение ресурсом следует указывать отдельно. Базовая C++-игра должна собираться без Lua headers/runtime и без динамической ветки на каждом компонентном доступе. ## Первый прототип и критерии принятия Объём первого сквозного прототипа по [плану](../../PLAN.md): один gameplay-модуль **Door**, поля угла/скорости и asset reference, метод взаимодействия, событие открытия; одна 2D- и одна 3D-сцена используют тот же контракт описания, сохраняя разные spatial/physics-типы. Здесь не требуется реализовать полный animation graph, native hot reload или универсальный binding generator. 1. **Регистрация и discovery.** Дубликат TypeId/FieldId, несовместимый accessor и неподдерживаемый тип дают ошибку со ссылкой на исходник. Inspector и MCP описывают одинаковые поля, units и read-only правила. Новый layout меняет build fingerprint, но не IDs сохранённых данных. 2. **Одинаковая правка.** Ручной multi-edit и MCP batch дают эквивалентный документ; недопустимая скорость и конфликт revision не применяют половину изменений. Побочная invalidation выполняется после commit, Undo восстанавливает связанные поля. Прямая runtime-правка не записывает authoring-сцену. 3. **Эволюция данных.** Переименовать C++-поле, изменить порядок членов, добавить поле с default, затем выполнить отдельную миграцию единиц. Сохранённые overrides остаются привязаны к правильным полям. Неудачная миграция и временное отсутствие пакета не уничтожают исходные записи. 4. **Время жизни.** Сохранить handle, удалить entity, переиспользовать slot, перезапустить Play. Старый handle и поздний callback отвергаются; ссылка на компонент не переживает structural change. Если позже подключён Lua, тот же сценарий выполняется с userdata и сборкой мусора. 5. **Сборка и границы.** Редактирование gameplay `.cpp` пересобирает реальные зависимости и статически связанный Player. SchemaExporter выдаёт manifest без OnStart/Update и без загрузки gameplay в Editor; сбой не заменяет рабочую схему. GUI/editor MCP получают compiler/schema diagnostics и build/schema IDs. Crash Player не закрывает Editor; изменение layout требует restart. Несовместимый editor plugin manifest отклоняется до загрузки. Player и exporter не содержат MCP, запросы runtime inspection/mutation через Editor отклоняются. 6. **Опциональность.** C++-вариант собирается и запускается без Lua. В отдельном будущем эксперименте Lua-реакция вызывает ту же доменную операцию и видит те же компоненты; ошибки аргументов, traceback и cleanup подписок проверяются явно. Это проверка запланированного Lua-модуля и его необязательности для проектов только на C++. Измерять edit→diagnostic, edit→Play, стоимость schema export и загрузки сцены, количество allocations на вызов, время массового typed-path относительно динамического вызова и число действий/ошибок при создании двери. Не подменять удобство краткостью регистрации: если автор не понимает, почему setter изменил другое поле или почему ссылка устарела, reflection ещё не решила задачу API.