18 KiB
Архитектурные решения 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, названия команд, ответы, идентификаторы операций и семантику ошибок.
После появления документации:
- На Этапе 3 создаётся версия внутреннего документа протокола и типизированный адаптер.
- Команды и события проверяются по документации и примерам реального обмена.
- На Этапе 4 подключается реальный Windows COM-транспорт.
- Любая неподтверждённая возможность контроллера остаётся недоступной, а не имитируется в 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и живёт в namespaceres://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-политикой.
Перед повторным включением точного учёта оператор должен пересчитать физические остатки.