# Архитектурные решения Exchanger Дата первичной фиксации: **2026-08-20**. Документ хранит решения, которые определяют границы подсистем и не должны заново пересматриваться без явной причины. Неизвестные параметры будущего оборудования не заполняются предположениями: они оформляются как входные данные соответствующего этапа интеграции. ## DEC-001. Универсальность до появления целевого оборудования Статус: **принято**. Целевой ПК, GPU, тач-контроллер и точная версия Windows пока отсутствуют. До получения реального аппарата приложение разрабатывается без привязки к конкретной модели оборудования: - базовый рендерер — Godot Mobile (Vulkan): это требуется runtime-расширением Native Video для MP4/MOV/M4V; UI сохраняет логический пиксельный viewport и nearest-фильтрацию; - UI использует логический портретный viewport 720×1280 — тот же, что и theme-preview; на панели другого размера применяется `canvas_items/expand`; - размеры, таймауты, тарифы, аппаратный транспорт и контент задаются конфигурацией; - ввод поддерживает тач и мышь без функций, доступных только по hover; - код не полагается на конкретный DPI, частоту обновления, GPU или имя COM-порта; - тяжёлые необязательные элементы должны отключаться конфигурацией; - фактическое железо позже добавляется как профиль проверки, а не как основание для переработки UI и бизнес-логики. Следствие: отсутствие целевого оборудования не блокирует интерфейс, mock-сценарии и основную архитектуру. Финальная проверка производительности и эксплуатации остаётся обязательной после появления устройства. ## DEC-002. Граница аппаратной интеграции Статус: **принято**. Приложение взаимодействует с автоматом только через абстракцию `IHardwarePort` и типизированный слой протокола. UI и `SessionController` не открывают COM-порт, не знают wire-формат сообщений и не обращаются напрямую к купюроприёмнику, монетоприёмнику, банковскому терминалу или диспенсеру. До появления документации контроллера используется `MockHardwarePort`. Реальный COM-транспорт подключается позднее отдельным адаптером без изменения экранов. ## DEC-003. Протокол контроллера не проектируется заранее Статус: **принято**. Список команд и документация контроллера будут добавлены в `references/` на более позднем этапе. До этого запрещено придумывать окончания строк, кодировку, heartbeat, названия команд, ответы, идентификаторы операций и семантику ошибок. После появления документации: 1. На Этапе 3 создаётся версия внутреннего документа протокола и типизированный адаптер. 2. Команды и события проверяются по документации и примерам реального обмена. 3. На Этапе 4 подключается реальный Windows COM-транспорт. 4. Любая неподтверждённая возможность контроллера остаётся недоступной, а не имитируется в production. Это входная зависимость Этапов 3–4, но не незавершённая задача Этапа 0. ## DEC-004. Контроллер — единственный источник аппаратных событий Статус: **принято**. Контроллер сообщает ПК все сведения об аппаратуре по протоколу из будущей документации. В их число по потребности сценария входят: - связь и готовность контроллера; - запас жетонов; - принятая наличная сумма и состояние приёмников; - результат карточной оплаты; - начало, прогресс и результат выдачи; - отмена, таймауты и аппаратные ошибки. Конкретный набор сообщений и их wire-формат определяются только документацией контроллера. На стороне приложения они преобразуются в типизированные события, с которыми работает `SessionController`. ## DEC-005. Статический фон и отдельные анимированные спрайты Статус: **заменено DEC-009**. Фоновая композиция интерфейса статична. Тема предоставляет fallback-цвет и при необходимости неподвижную текстуру. НЛО, маскот и другие движущиеся элементы подключаются отдельной пассивной `PackedScene` с анимированными спрайтами. Фоновые шейдеры, частицы и видео не используются. Промо-ролик является отдельным UI-блоком `HomeScreen`. ## DEC-006. Проектные инструкции агентов версионируются Статус: **принято**. Файл `.agents/AGENTS.md` является частью проекта и должен храниться в Git. Это позволяет новым агентам восстановить правила работы, требования к документации, использованию Godot MCP и `agentmemory`. Каталог `.agents/` не исключается через `.gitignore`. ## DEC-007. Состояние репозитория Статус: **принято**. Локальный Git-репозиторий инициализирован 2026-08-20. Удалённый репозиторий, стратегия веток и первый коммит на этом этапе не назначаются без отдельного решения владельца проекта. ## DEC-008. Полные визуальные сцены принадлежат теме, бизнес-логика — приложению Статус: **дополнено DEC-009**. Новая DLC-тема формата v2 предоставляет полные визуальные сцены всех девяти экранов. Она может свободно менять композицию и вложенность `Control`, но не содержит исполняемых скриптов и не обращается к сессии, оплате, настройкам или оборудованию. Приложение сохраняет app-owned C#-контроллер каждого экрана. До `_Ready` встроенный визуальный корень контроллера заменяется сценой DLC, после чего кнопки, поля и динамические области находятся по metadata `exchanger_binding_id`. Имена файлов, scene binding-id и root-имена зафиксированы `ThemeSceneContract`; фактические NodePath внутри темы частью runtime-контракта не являются. Следствия: - встроенные сцены остаются единственным безопасным fallback без внешнего пакета; - manifest v1 продолжает работать как стилевой DLC без замены сцен; - manifest v2 активируется только полным набором экранов, а не смесью разных тем; - новые сцены могут быть самодостаточными: root содержит собственный `Theme`, прямой статический `ThemeBackground` и прямой `BackgroundDecoration`; все девять сцен обязаны использовать один режим, а глобальные background/decoration пути manifest в нём остаются пустыми; - host не меняет визуальные свойства самодостаточной сцены после загрузки: запрещены повторное назначение Theme, type variations, геометрии и theme overrides; контроллеры только находят готовые узлы по binding-id, подключают события и обновляют функциональные значения; - визуальные узлы запаса, связи и промо принадлежат сцене темы; приложение находит их по биндингам, передаёт только состояние и медиаресурсы и не добавляет дочерние Control-узлы; - production-сцены темы не содержат GDScript/C#; допустимы только данные, стандартные Godot-узлы, `AnimationPlayer` и `AnimatedSprite2D`; - точный каталог содержит 481 обязательный биндинг и входит в PCK по каноническому пути; приложение закрепляет его SHA-256, поэтому DLC не может объявить себе ослабленный контракт; - небольшой набор supplemental runtime-биндингов проверяется приложением поверх каталога: состояния полоски запаса, приблизительный остаток на Home и сервисные действия двух хопперов; - `ReducedEffects` и эксплуатационный бюджет применяются как к прежней глобальной decoration, так и к `BackgroundDecoration` внутри самодостаточной сцены; - проект создания тем находится отдельно в `../ExchangerThemeDlcBuilder/themes/`, а единый каталог — в `../ExchangerThemeDlcBuilder/contracts/screen_bindings.v2.json`. ## DEC-009. Внешние темы являются доверенным исполняемым кодом Статус: **принято**. Владелец приложения самостоятельно разрабатывает и устанавливает все PCK-темы; стороннее распространение пакетов не предполагается. Поэтому Exchanger не ограничивает тему пассивной графикой и считает всё содержимое `res://exchanger_theme_dlc/` доверенным. Следствия: - production-сцены, вложенные компоненты, legacy decoration и visual slots могут содержать GDScript и любые стандартные runtime-узлы Godot; - разрешены сигналы, method tracks, таймеры, аудио, видео, частицы, шейдеры, процедурная графика, сетевые и файловые API; - удалён эксплуатационный decoration budget и ограничение размера PCK; - manifest v2, девять канонических сцен, 481 binding-id, их типы и закреплённый SHA-256 контракта остаются обязательными для подключения app-owned C#-логики; - пакет по-прежнему монтируется с `replace_files=false` и живёт в namespace `res://exchanger_theme_dlc/`, чтобы его ресурсы имели стабильные пути; - старое требование статического фона и запрет исполняемых скриптов больше не действуют. Скрипт темы исполняется при инстанцировании PackedScene. Контрактная проверка также временно инстанцирует сцену, поэтому код `_init()` может выполниться до фактического показа экрана; логику, которой нужен SceneTree, следует размещать в `_ready()`. ## DEC-010. Первый хоппер имеет строгий приоритет при обычной выдаче Статус: **принято**. В профиле `DualHopperAuto` приложение строит план по точному программному остатку, а не выбирает стартовый хоппер по бинарному датчику уровня или по сравнению запасов. Для запроса `N`: - из hopper 1 планируется `min(N, остаток hopper 1)`; - из hopper 2 планируется только разница `N - hopper 1`; - команда hopper 2 отправляется после подтверждения всех запланированных жетонов hopper 1; - объём одного пакета ограничен 99 жетонами; при большем объёме пакеты одного хоппера идут последовательно, и следующий отправляется только после всех `OK` предыдущего (`APPEND=N` подтверждает лишь постановку в очередь); - если суммарного учётного остатка меньше `N`, выдача отклоняется до отправки `309N/409N`. Бинарные датчики `Low/Enough` продолжают описывать уровень, но не меняют порядок маршрутизации. Если уже активный хоппер возвращает ошибку или связь теряется, автоматического перехода на другой хоппер нет: сохраняется at-most-once-политика, учитывается только подтверждённая выдача и операция завершается безопасной ошибкой. ## DEC-011. Точный учёт жетонов является отключаемой бизнес-политикой Статус: **принято**. `AppSettings.TokenInventory.Enabled` отделяет сохранённый точный остаток от бинарных аппаратных датчиков. `TokenInventory.PurchaseLimitEnabled` независимо определяет, должен ли этот остаток ограничивать покупку. Оба флага включены по умолчанию ради совместимости. При активном лимите полное количество к выдаче, включая бонус, проверяется до карточного запроса оплаты и повторно перед выдачей; наличная ветка проверяется до команды хопперу. При включённом учёте и выключенном лимите остаток продолжает отображаться и уменьшаться по подтверждённой выдаче, но не блокирует оплату. Доступность Home определяется датчиками. В `DualHopperAuto` записанный остаток hopper 1 используется как первая часть плана, а вся разница направляется в hopper 2 независимо от его записанного остатка. При выключенном учёте `user://data/token_inventory.json` сохраняется, но не читается как лимит и не уменьшается. Сервисные действия пересчёта/пополнения и числовая индикация скрываются, тогда как качественное состояние `Low/Enough/Unknown` продолжает определяться датчиками. Для `DualHopperAuto` запрос направляется в hopper 1 последовательными пакетами до 99 жетонов. Автоматический переход на другой хоппер после ошибки запрещён прежней at-most-once-политикой. Перед повторным включением точного учёта оператор должен пересчитать физические остатки.