exchanger/docs/THEME_DLC.md

485 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Внешние DLC-темы Exchanger
## Архитектура
Основной проект Exchanger не содержит редактируемых ресурсов тем. В нём остаются только:
- `BuiltInFallbackTheme` — единственная минимальная тема, создаваемая кодом;
- `VendingThemeDefinition` — runtime-представление уже загруженной темы;
- `ThemeDlcLoader` и валидатор manifest;
- `ThemeManager`, выбирающий fallback либо один внешний пакет при запуске.
- `ThemeBindableScreen` и `ThemeBindingResolver`, связывающие app-owned логику с визуальными
сценами DLC по стабильным binding-id.
Все исходники визуальных тем находятся в соседнем проекте `../ExchangerThemeDlcBuilder/`. В Exchanger отсутствуют `themes/`, исходные `.tres`-темы, фоновые картинки и шаблоны их разработки.
## Путь из пользовательских файлов
Монтажный способ выбора темы задаётся в `user://config/technical_settings.toml`:
```toml
[theme]
enabled = true
directory = "user://theme_dlc"
file_name = "active_theme"
```
Путь собирается как `<directory>/<file_name>.pck`; расширение в `file_name` не указывается.
Разрешены `user://` и абсолютный локальный каталог, включая Linux-путь вроде
`/opt/exchanger/themes`. Если optional раздел `[theme]` отсутствует в старом TOML версии 1,
используется прежняя настройка из `appsettings.json`:
Для Windows разрешены `directory = 'D:\Exchanger\themes'` (literal TOML-строка без
экранирования), `directory = "D:/Exchanger/themes"` либо двойные обратные слеши внутри обычной
двойной строки.
Настройка хранится в `%APPDATA%\Godot\app_userdata\Exchanger\config\appsettings.json`. Значение по умолчанию:
```json
{
"Themes": {
"ReducedEffects": false,
"ExternalDlcEnabled": true,
"ExternalDlcPath": "user://theme_dlc/active_theme.pck"
}
}
```
На Windows этот `user://` соответствует `%APPDATA%\Godot\app_userdata\Exchanger\theme_dlc\active_theme.pck`. Также разрешён полный абсолютный путь. Относительные пути и `res://` отклоняются. Технический TOML имеет приоритет над JSON, а разовые `--theme-dlc=D:\ExchangerThemes\active_theme.pck` и `--no-theme-dlc` — над TOML.
В debug-сборке эти параметры доступны во вкладке `F10 → Тема`. Корректный путь разрешено
сохранить до появления пакета: при следующем запуске отсутствующий файл безопасно оставит
встроенную тему. Включение DLC и изменение пути применяются только после перезапуска.
`ReducedEffects` имеет немедленный предварительный просмотр; кнопка «Отменить» возвращает
сохранённое значение.
## Формат пакета
DLC является доверенным Godot resource pack `.pck`:
```text
res://exchanger_theme_dlc/
├── manifest.json
├── ui_theme.tres
├── background.png|webp|jpg|svg # необязательно, только legacy-композиция
├── background_decoration.tscn # необязательно, только legacy-композиция
├── components/ # необязательные theme-owned runtime-компоненты
│ └── interactive_pet.tscn # рекомендуемое имя сцены питомца
├── visuals/ # необязательные runtime-сцены экранов
├── scripts/ # GDScript темы
├── contracts/ # обязательно для manifest v2
│ └── screen_bindings.v2.json
└── scenes/ # обязательно для manifest v2
├── screen_home.tscn
├── screen_cash_payment.tscn
├── screen_card_amount.tscn
├── screen_card_custom_amount.tscn
├── screen_card_terminal.tscn
├── screen_success.tscn
├── screen_error.tscn
├── screen_service_access.tscn
└── screen_settings.tscn
```
Manifest версии 1:
```json
{
"Format": "ExchangerThemeDlc",
"FormatVersion": 1,
"ThemeId": "sample-theme",
"DisplayName": "Название темы",
"UiThemePath": "res://exchanger_theme_dlc/ui_theme.tres",
"BackgroundTexturePath": "res://exchanger_theme_dlc/background.svg",
"BackgroundDecorationPath": "res://exchanger_theme_dlc/background_decoration.tscn",
"BackgroundDecorationScale": 1.0,
"InteractivePetScene": "res://exchanger_theme_dlc/components/interactive_pet.tscn",
"VisualSlots": {
"home.top": "res://exchanger_theme_dlc/visuals/home_top.tscn",
"card.terminal": "res://exchanger_theme_dlc/visuals/card_terminal.tscn"
},
"FallbackColor": "#061229",
"UseNearestTextureFilter": false
}
```
Версия 1 остаётся совместимой и меняет стили, фон и visual slots встроенных экранов. Для новой
темы следует использовать manifest версии 2: он передаёт теме всё видимое дерево каждого экрана.
```json
{
"Format": "ExchangerThemeDlc",
"FormatVersion": 2,
"ThemeId": "sample-theme",
"DisplayName": "Название темы",
"UiThemePath": "res://exchanger_theme_dlc/ui_theme.tres",
"InteractivePetScene": "res://exchanger_theme_dlc/components/interactive_pet.tscn",
"ScreenScenes": {
"screen.home": "res://exchanger_theme_dlc/scenes/screen_home.tscn",
"screen.cash_payment": "res://exchanger_theme_dlc/scenes/screen_cash_payment.tscn",
"screen.card_amount": "res://exchanger_theme_dlc/scenes/screen_card_amount.tscn",
"screen.card_custom_amount": "res://exchanger_theme_dlc/scenes/screen_card_custom_amount.tscn",
"screen.card_terminal": "res://exchanger_theme_dlc/scenes/screen_card_terminal.tscn",
"screen.success": "res://exchanger_theme_dlc/scenes/screen_success.tscn",
"screen.error": "res://exchanger_theme_dlc/scenes/screen_error.tscn",
"screen.service_access": "res://exchanger_theme_dlc/scenes/screen_service_access.tscn",
"screen.settings": "res://exchanger_theme_dlc/scenes/screen_settings.tscn"
},
"VisualSlots": {},
"FallbackColor": "#061229",
"UseNearestTextureFilter": true
}
```
`UiThemePath` обязателен. Фон и декорация могут быть пустыми строками.
`UseNearestTextureFilter` необязателен и по умолчанию равен `false`. Его следует включать
только для pixel-art тем; тогда приложение применяет nearest-фильтрацию к фону и Control-узлам,
не меняя глобальные настройки рендера для других тем.
`BackgroundDecorationScale` необязателен, по умолчанию равен `1.0` и допускает значения 0.1–4.0.
Он нужен только для decoration, подготовленной не в координатах host 720×1280.
`InteractivePetScene` необязателен. Он указывает на `PackedScene` внутри
`res://exchanger_theme_dlc/`; рекомендуемый путь — `components/interactive_pet.tscn`, но manifest
может объявить любое другое имя сцены внутри package. Exchanger безопасно проверяет namespace,
расширение и наличие ресурса, загружает сцену в `VendingThemeDefinition.InteractivePetScene`, но
показывает её на всех экранах, кроме `screen.settings`, для которых тема предоставила корректный
навигационный граф. Актуальный Builder требует такой граф в каждой из восьми non-settings сцен
темы с питомцем; host сохраняет совместимость со старыми пакетами и скрывает питомца только на
конкретной сцене без графа.
Обычная навигация и ввод остаются app-owned, а сцена питомца, анимации, размеры и геометрия
доступных поверхностей — theme-owned. Для уникальной многошаговой реакции тема может временно
управлять экранной опорной точкой через scoped runtime, не получая ссылок на внутренние узлы host.
Корень компонента питомца — `Node2D`. Нейтральный API состоит из методов
`get_pet_animation_names`, `get_pet_animation_frame_count`, `has_pet_animation`,
`play_pet_animation`, `pause_pet_animation`, `stop_pet_animation`,
`get_current_pet_animation`, `get_current_pet_frame` и сигналов `pet_animation_started` /
`pet_animation_completed`. Имена и количество анимаций определяет тема. Сцена напрямую ссылается
на свои ресурсы, поэтому каталог текстур может называться как угодно и не входит в контракт.
Для поведения тема дополнительно может реализовать методы `get_pet_action_animation`,
`get_pet_runtime_scale`, `get_pet_walk_speed`, `get_pet_climb_speed`, `get_pet_hit_size` и
`get_pet_ground_offset`. Первый метод сопоставляет нейтральные действия приложения с
фактическими именами клипов конкретной темы; в том числе `sit` для посадки после приземления.
Это сохраняет совместимость с уже выпущенными DLC.
Во время обычного падения host запрашивает действие `drop`, после контакта с объявленной
поверхностью — один полный цикл `landing`, а затем включает `idle`. Даже если при смене экрана
anchor уже совпадает с новой поверхностью, состояние `landing` не пропускается. Если старая тема
не содержит mapping или клип `landing`, host выдерживает безопасную фазу `Landing` в 0,5 секунды
и пишет предупреждение вместо немедленного перехода к `idle`. `celebrate` запускается при переходе
к клиентской выдаче жетонов: host последовательно проигрывает четыре полных цикла. Если тема не
содержит его, для обратной совместимости используется `interact`. Длительность цикла определяется
обязательным `get_pet_animation_frame_count` и optional `get_pet_animation_frames_per_second`; при
отсутствии корректной скорости действует безопасный fallback. Запрошенное во время `falling` или `landing`
празднование ждёт завершения приземления и только затем начинает первый цикл.
После завершения обычного маршрута host выбирает паузу независимо от темы: с вероятностью 35%
он показывает `idle` на 3–5 секунд, в остальных случаях — спокойный `sitting` на 1,5–3,5 секунды.
Темы без `sitting` остаются совместимыми: в этой ветке используется `idle`.
Необязательный составной контракт позволяет теме самой выполнить действие из нескольких клипов,
визуальных преобразований и перемещений. Он принимается только целиком: методы
`has_pet_composite_action`, `start_pet_composite_action`, `cancel_pet_composite_action` и сигналы
`pet_composite_action_started` / `pet_composite_action_finished`. При отсутствии или неполной
реализации Exchanger использует одиночную анимацию с тем же каноническим именем.
`start_pet_composite_action(action, runtime, parameters)` возвращает run id переданного runtime.
Для `interact` параметры содержат `action_repetitions` (1–16) и `reduced_effects`. Scoped runtime
даёт методы `GetRunId`, `IsActive`, `GetViewportRect`, `GetAnchor`, `GetHighestSupport`,
`SetMotionPolicy`, `SetAnchor`, `MoveAnchor` и `LandOnSupport`. Политика отдельно управляет обычной
навигацией (`suspended`/`theme_driven`), контактами с опорами (`ignore`, `solid`,
`after_vertical_wrap`), границами (`clamp`, `allow`, `wrap_vertical_once`) и пользовательским
вводом. Для посадки тема указывает `landing_surface_id`; host проверяет, что такая поверхность
существует и сейчас доступна.
Каждый runtime привязан к одному запуску. Смена экрана/темы, начало drag и конфликтующее действие
инвалидируют его; последующие команды старого runtime отклоняются. После сигнала успешного
завершения host возвращает обычное состояние Sitting. Поэтому GDScript темы обязан проверять
`IsActive()` после каждого `await`, корректно обрабатывать `cancel_pet_composite_action` и не
сохранять runtime для будущих запусков.
Маршруты задаются отдельными невидимыми metadata-маркерами внутри каждой готовой сцены, кроме
`screen.settings`:
- один root с `exchanger_pet_navigation_root = true`, версией, `Rect2` границами и id
поверхности по умолчанию;
- поверхности с уникальным id, диапазоном X, координатой опорной точки Y и необязательными
точками посадки;
- двунаправленные рёбра подъёма с id исходной/целевой поверхности, X края и двумя Y.
Поверхность может дополнительно объявить строковую metadata
`exchanger_pet_surface_visibility_binding_id`. Она должна ссылаться на `CanvasItem` этой же
экранной сцены по `exchanger_binding_id`. Если готовый визуальный элемент скрыт пользовательской
настройкой, host исключает такую поверхность и все связанные с ней рёбра из активного графа.
Все координаты описывают экранную опорную точку питомца в системе 720×1280. Host не вычисляет
поверхности из имён или геометрии `Control` и не меняет визуалы темы. Он ходит по объявленным
поверхностям, строит путь по рёбрам, поднимается и спускается у заданных краёв, а после
перетаскивания первым пальцем или зажатой ЛКМ сажает питомца на ближайшую нижнюю поверхность.
На каждом ненулевом участке подъёма или спуска host выбирает случайную точку между 20% и 80%
пути, приостанавливает в ней перемещение и последовательно запрашивает действия
`climbing_action_start`, `climbing_action` и `climbing_action_stop`. Средняя анимация выполняется
один полный цикл, после чего питомец продолжает тот же маршрут и направление. Последовательность
включается только при наличии всех трёх действий и клипов; тема без любого из них сохраняет
обычное непрерывное движение для обратной совместимости.
При переключении экрана host выбирает граф новой сцены, сохраняет текущую экранную позицию
питомца и запускает `falling` до её поверхности по умолчанию; после посадки доступен весь новый граф.
На `screen.settings` питомец скрыт, а его сохранённая позиция используется при выходе из настроек.
`hanging` действует только пока пользователь удерживает питомца, а `falling` — во время его падения
к выбранной поверхности после отпускания.
Режим reduced-effects отключает автономное блуждание, сохраняя явное взаимодействие и drag.
Новые темы используют самодостаточную композицию экранов. В этом режиме каждый root из
`ScreenScenes` имеет metadata `exchanger_self_contained_visuals = true`, назначенный `Theme` и
два прямых дочерних узла: `ThemeBackground` типа `TextureRect` с текстурой и
`BackgroundDecoration` типа `CanvasItem`. Все девять сцен пакета обязаны выбрать одинаковый
режим. `BackgroundTexturePath` и `BackgroundDecorationPath` при этом оставляют пустыми, иначе
приложение отклонит DLC как дублирующую визуальную композицию. Смешивать самодостаточные и
legacy-сцены в одном пакете также нельзя.
После инстанцирования host сохраняет присутствующее визуальное дерево самодостаточной сцены в том
виде, в каком оно собрано в Theme Builder. Он не назначает другой `Theme`, `ThemeTypeVariation`,
размеры, отступы или локальные theme overrides. Разрешены функциональные операции через
binding-id: подключение сигналов, обновление данных, видимости, доступности и состояния. Если
обязательного binding-id нет, host сначала принимает совместимый узел старой темы по закреплённому
`FallbackPath`, а при отсутствии самого узла переносит на его место готовый элемент встроенной
fallback-сцены с её оформлением. Внешние элементы, у которых binding-id присутствует, не меняются.
Для управления видимостью Home приложение использует готовые bindings
`home.scroll.content.stock_status`, `home.scroll.content.advertisement_panel` и необязательный
`home.decoration.custom_text_block`. Последний объединяет theme-owned фон пользовательской
речевой плашки и персонажа справа; для старых тем без этой группы сохранён fallback
`home.decoration.right_character`. Экран Settings предоставляет готовую
панель `settings.scroll.content.menu_visibility_panel` и три `CheckButton` с окончаниями
`show_stock_status`, legacy `show_right_character` (пользовательский текст и персонаж) и
`show_promotion_block`. Host только читает состояния
переключателей и меняет `Visible` соответствующих готовых элементов.
Фиксированный футер Settings задаётся supplemental bindings `settings.footer` и
`settings.footer.save_and_exit_button`; он является корневым соседом `SafeMargin`, а не потомком
`settings.scroll`. В конец прокручиваемого `Content` добавляется
`settings.scroll.content.footer_clearance`, чтобы нижние поля не перекрывались футером. Все три
узла обязательны для новых тем; в уже выпущенной теме без этих ID host переносит готовый футер и
зазор встроенной темы, не переоформляя остальные элементы DLC. Старый полноэкранный Scroll при
этом может проходить под непрозрачным футером; `footer_clearance` позволяет прокрутить последние
поля выше него без изменения геометрии выпущенной сцены.
Сервисный переключатель учёта использует supplemental binding
`settings.scroll.content.service_inventory_panel.margin.content.inventory_enabled` типа
`CheckButton`; независимый переключатель ограничения покупки —
`settings.scroll.content.service_inventory_panel.margin.content.purchase_limit_enabled` того же
типа. Изменяемое пояснение рядом с ними —
`settings.scroll.content.service_inventory_panel.margin.content.description` типа `Label`.
При выключении host меняет только функциональные `Visible`, `Disabled`, `ButtonPressed` и текст
готовых элементов этой панели. Старые PCK без metadata получают совместимые узлы по каноническим
fallback-путям; закреплённый JSON-каталог остаётся на 481 binding.
На всех пользовательских экранах, кроме Home и Settings, предусмотрена готовая кнопка
`home_navigation_button` с изображением дома. Его основа — `KeypadButton`; в `space-pixel` этот
вариант использует готовый `assets/ui/button_square.png`. На CashPayment, CardAmount, CardCustomAmount и
CardTerminal она расположена рядом с `BackButton`; на Success, Error и ServiceAccess занимает
обычную верхнюю левую позицию. Host только подписывается на `pressed`: незавершённую оплату он
безопасно отменяет, а на Success до завершения фактической выдачи кнопка недоступна. Новые темы
обязаны объявить семь соответствующих supplemental ID; старые PCK получают готовые fallback-узлы
из встроенных сцен.
Тексты нижних анимированных плашек с НЛО на CashPayment, CardAmount, CardCustomAmount и
CardTerminal настраиваются в разделе «Общие» через готовую панель темы
`settings.scroll.content.animated_banner_texts_panel`. Текст CashPayment выводится в
`cash_payment.content.banner_text`; для таймера CardTerminal в шаблоне нужно оставить
`{seconds}`, которое host заменяет количеством оставшихся секунд. Эти два новых supplemental ID
не изменяют immutable JSON-каталог; старой DLC-теме host подставляет встроенные узлы.
`VisualSlots` — необязательный словарь именованных `PackedScene`. Поддерживаемые host-слоты:
| Slot | Область preview |
|---|---|
| `home.top` | НЛО и реплика на главном экране |
| `home.advertisement` | тематическая рамка промо-блока |
| `cash.instructions` | купюра и монета |
| `cash.tip` | маскот и предупреждение о сдаче |
| `card.terminal` | анимация карты у терминала |
| `result.success` | анимация успешной выдачи |
| `result.error` | анимация ошибки |
Сцены слотов располагаются в нативной системе координат 720×1280 либо в локальном размере
соответствующего контейнера. Они не заменяют интерактивные узлы приложения: суммы, состояния,
кнопки и переходы всегда принадлежат host.
## Полносценовый контракт v2
Приложение всегда инстанцирует собственный C#-контроллер экрана, содержащий бизнес-логику,
навигацию и обращения к оборудованию. До входа контроллера в SceneTree его встроенный визуальный
корень `SafeMargin` заменяется сценой из DLC. Поэтому тема полностью определяет композицию и
вложенность `Control`, но не получает доступ к оплате, COM-порту или состоянию сессии.
Имена файлов, ключи `ScreenScenes` и имена корневых узлов фиксированы:
| Scene binding | Файл | Имя root |
|---|---|---|
| `screen.home` | `screen_home.tscn` | `ThemeScreen_Home` |
| `screen.cash_payment` | `screen_cash_payment.tscn` | `ThemeScreen_CashPayment` |
| `screen.card_amount` | `screen_card_amount.tscn` | `ThemeScreen_CardAmount` |
| `screen.card_custom_amount` | `screen_card_custom_amount.tscn` | `ThemeScreen_CardCustomAmount` |
| `screen.card_terminal` | `screen_card_terminal.tscn` | `ThemeScreen_CardTerminal` |
| `screen.success` | `screen_success.tscn` | `ThemeScreen_Success` |
| `screen.error` | `screen_error.tscn` | `ThemeScreen_Error` |
| `screen.service_access` | `screen_service_access.tscn` | `ThemeScreen_ServiceAccess` |
| `screen.settings` | `screen_settings.tscn` | `ThemeScreen_Settings` |
Root каждой сцены — `Control` с metadata `exchanger_screen_binding_id`, равной ключу из таблицы.
Интерактивные и динамические узлы получают уникальную metadata `exchanger_binding_id`.
Пример кнопки наличной оплаты:
```text
exchanger_binding_id = "home.scroll.content.payment_panel.margin.content.buttons.cash_button"
```
Binding-id вычисляется из стабильного fallback NodePath: префикс `SafeMargin/` удаляется,
каждый оставшийся сегмент переводится из PascalCase в snake_case, перед ним добавляется ключ
экрана. Фактическое положение узла в тематической сцене на binding-id не влияет. Точный
машиночитаемый каталог находится в `../ExchangerThemeDlcBuilder/contracts/screen_bindings.v2.json`,
а готовый шаблон — в `../ExchangerThemeDlcBuilder/themes/example/`. Preview-overlay является
инструментом Builder и не входит в production-сцены.
Manifest v2 обязан включать точную копию каталога в
`res://exchanger_theme_dlc/contracts/screen_bindings.v2.json`. Приложение закрепляет SHA-256
этого файла и проверяет девять экранов. Для присутствующих биндингов проверяются ID и Godot-тип;
неизвестные ID, дубли и неверные типы отклоняются. Отсутствующий обязательный ID не отключает весь
DLC: он разрешается через канонический `FallbackPath` из закреплённого каталога. Если точечная
подстановка невозможна, встроенная сцена используется только для проблемного экрана. Корневой
контракт сцены, self-contained композиция и подлинность каталога остаются строгими.
Все присутствующие визуальные узлы принадлежат теме. Приложение не переделывает их стили или
компоновку: оно находит узлы по binding-id, подключает сигналы и обновляет функциональные значения.
Единственное исключение — отсутствующий обязательный узел: для обратной совместимости на его
каноническое место переносится готовый элемент встроенной темы-заглушки.
Для итогов наличной оплаты предусмотрены три независимых обязательных `Label`:
- `cash_payment.content.summary.margin.values.balance` получает только внесённую сумму в рублях;
- `cash_payment.content.summary.margin.values.tokens` получает базовое количество жетонов;
- `cash_payment.content.summary.margin.values.bonus` получает бонус в формате
`+ N ЖЕТОНОВ\nВ ПОДАРОК` либо `БЕЗ БОНУСА`.
Контроллер вычисляет жетоны через общую `PricingPolicy`, заполняет все три узла и не объединяет
значения в одну строку. Геометрия, перенос строк, выравнивание и оформление остаются частью темы.
Панель «К ВЫДАЧЕ» на экране готовых карточных сумм использует существующий обязательный Label
`card_amount.content.reward_panel.content.value` только для базового количества жетонов. Бонус
выводится отдельно через supplemental runtime binding
`card_amount.content.reward_panel.content.bonus` типа `Label` с каноническим fallback-путём
`SafeMargin/Content/RewardPanel/Content/Bonus`. Формат совпадает с наличной оплатой:
`+ N ЖЕТОНОВ\nВ ПОДАРОК` либо `БЕЗ БОНУСА`. Supplemental ID не входит в закреплённый JSON-каталог
481 binding; старая тема получает готовый встроенный Label побиндинговым fallback.
На экране ручной карточной суммы введённое число выводится отдельно через обязательный Label
`card_custom_amount.content.keypad_panel.content.input_value`. Верхние Label
`...amount_panel.margin.values.amount` и `...tokens` получают соответственно базовые и бонусные
жетоны; второй Label использует тот же двухстрочный формат либо текст `БЕЗ БОНУСА`. Таким образом
верхняя панель не содержит рублей, а поле под «ВВЕДИТЕ СУММУ» всегда отражает фактически набранное
значение.
Изменяемые списки представлены заранее созданными фиксированными слотами: 16 кнопок сумм,
16 правил дополнительных жетонов, 16 сумм оплаты и 12 промо-роликов. Неиспользуемые слоты
скрыты. Home аналогично содержит готовые узлы состояния запаса, связи, постера, видео и
резервного текста. Благодаря этому внешний вид полной актуальной темы совпадает с предпросмотром;
дерево дополняется только при отсутствии обязательного binding в старом пакете.
Строка запаса Home состоит из готовых узлов темы: текстового состояния слева, полоски по центру
и Label приблизительного общего остатка справа. Для последнего используется supplemental
runtime binding home.scroll.content.stock_status.approximate_count; приложение меняет только
его Text и скрывает Label при отключённом учёте. В Settings, напротив, сервисная панель получает
точное учётное количество, а при отключённом учёте скрывает остатки, пересчёт и пополнение.
Экран Settings дополнительно содержит две готовые панели `KeyboardOverlay`: текстовую и цифровую.
Они дают 57 обязательных биндингов: 33 символьные клавиши, 10 цифр, две кнопки скрытия и
12 служебных действий. Панели по умолчанию скрыты, все их кнопки имеют `focus_mode = None`,
чтобы нажатие не отнимало каретку у редактируемого поля. Host подключает `Pressed` и меняет только
`Visible`, `Text`, `Disabled` и `ButtonPressed`; создание, перестановка и стилизация узлов runtime
запрещены. В preview Builder предусмотрены состояния `KEYBOARD: OFF/TEXT/NUM`.
## Семантический UI-контракт
Runtime применяет один `Theme` ко всем девяти экранам, включая `ServiceAccess` и `Settings`.
Тема может оформлять следующие стабильные type variations:
| Variation | Базовый Godot-тип | Назначение |
|---|---|---|
| `ScreenTitle` | `Label` | главный заголовок экрана |
| `ScreenDescription` | `Label` | пояснение под заголовком |
| `PanelTitle` | `Label` | заголовок панели |
| `PanelText` | `Label` | крупное содержимое пользовательской панели |
| `FieldLabel` | `Label` | подпись или обычный текст поля настроек |
| `SupportText` | `Label` | подсказка и вторичный текст |
| `StatusText` | `Label` | текущее состояние операции или оборудования |
| `WarningText` | `Label` | предупреждение |
| `ErrorText` | `Label` | ошибка валидации или операции |
| `PrimaryButton` | `Button` | главное действие |
| `CompactButton` | `Button` | цифровая клавиша или компактное действие |
| `BackButton` | `Button` | компактный возврат с пользовательского экрана |
| `SectionTabButton` | `Button` | вкладка сервисных настроек |
| `SecondaryButton` | `Button` | отмена, закрытие или сброс |
| `DangerButton` | `Button` | удаление/необратимое действие |
| `WhitePanel` | `PanelContainer` | основная смысловая панель |
| `AdvertisementPanel` | `PanelContainer` | контейнер постера, текста или промо-ролика OGV/MP4/MOV/M4V |
| `FooterPanel` | `PanelContainer` | техподдержка и служебный footer Home |
| `StatusPanel` | `PanelContainer` | компактный индикатор связи/состояния |
Host всегда регистрирует базовый тип каждой variation. Поэтому старый корректный DLC v1,
в котором новый семантический тип ещё не оформлен, наследует безопасный стандартный стиль и
остаётся совместимым.
Экран настроек дополнительно использует базовые классы `LineEdit`, `SpinBox`, `OptionButton`,
`CheckButton`, `HSlider`, `VScrollBar` и `HScrollBar`. Полноценная тема должна явно задавать их
normal/focus/pressed/disabled-состояния и сохранять читаемость внутри `WhitePanel`.
## Доверенное выполнение и fallback
- Пакет монтируется с `replace_files=false` и не может заменить ресурсы Exchanger.
- Все пути manifest обязаны находиться внутри `res://exchanger_theme_dlc/`; `..`, обратные слеши и ссылки на сцены приложения запрещены.
- Decoration, visual slots и полносценовые view могут содержать GDScript и любые стандартные
runtime-узлы Godot. Код начинает исполняться при инстанцировании сцены.
- Проверяются формат, версия, ThemeId, цвет, расширения, существование и тип каждого ресурса.
- Для manifest v2 проверяются закреплённый каталог биндингов, namespace, дубли и типы присутствующих
узлов. Неизвестный либо неверно типизированный биндинг отключает DLC; отсутствующий обязательный
биндинг восстанавливается по `FallbackPath` встроенным элементом.
- Самодостаточная сцена дополнительно проверяется на собственный `Theme`, текстурированный
`ThemeBackground` и `BackgroundDecoration`; смешанный режим девяти сцен и одновременное
объявление глобального фона или decoration запрещены.
- При отсутствии, повреждении или несовместимости DLC приложение продолжает запуск со встроенной fallback-темой.
- В журнал не записывается полный пользовательский путь к DLC.
- Одновременно загружается не более одного пакета. После замены `.pck` приложение нужно перезапустить.
## Runtime-возможности темы
В legacy-композиции `BackgroundTexturePath` задаёт общий фон, а `BackgroundDecorationPath` может
ссылаться на произвольную доверенную `PackedScene`. В самодостаточной композиции те же роли
выполняют `ThemeBackground` и `BackgroundDecoration` внутри каждой сцены.
Разрешены GDScript, сигналы, `Timer`, `Tween`, `AnimatedSprite2D`, `AnimationPlayer`, method tracks,
аудио, видео, частицы, шейдеры, процедурная графика, сетевые и файловые API Godot. Host не
применяет лимиты количества узлов, кадров, FPS или скорости анимации и не ограничивает размер PCK.
При `ReducedEffects=true` глобальная decoration не создаётся, а узел `BackgroundDecoration`
самодостаточного экрана скрывается и перестаёт обрабатываться. Остальная скриптовая логика сцены
продолжает работать; тема при желании может самостоятельно учитывать эту настройку.
## Разработка и сборка
Откройте соседний `ExchangerThemeDlcBuilder/project.godot` в Godot 4.7.1. После редактирования выполните:
```powershell
powershell -ExecutionPolicy Bypass -File .\build-theme.ps1 -ThemeId space-pixel
```
Пакет появится в `build/space-pixel.pck`; совместимая копия будет записана в
`build/active_theme.pck`. Для одновременной установки в стандартный пользовательский путь:
```powershell
powershell -ExecutionPolicy Bypass -File .\build-theme.ps1 -ThemeId space-pixel -InstallToDefaultUserPath
```
Сборщик проверяет, что запущен настоящий Godot 4.7, а не MCP-сервер `godot-ai`. Сборка DLC не требует C#-кода или ссылки на проект Exchanger.