# Pause menu и Settings для Sprinter PoP Статус: **MS0, MS2 и MS4–MS8 выполнены** (2026-08-23). Pause menu, CFG, Settings, диалоги, Controls и build screen находятся в bank 9. Решение о рендеринге и затемнении — §10. Связанные документы: - [`full_game_plan.md`](full_game_plan.md) — автомат состояний, title, demo, cutscenes и ending; - [`quicksave_plan.md`](quicksave_plan.md) — состав и восстановление снимка. ## 1. Решения - Программа работает только с HDD; настройки, QuickSave и Hall of Fame всегда могут быть постоянными файлами. - Основной pause menu обязательно содержит QuickSave и QuickLoad. - QuickSave имеет один слот `POP.SAV`; предыдущая корректная запись хранится как `POP.BAK`. - Первая версия имеет один профиль `VANILLA`. Под этим именем пока понимается **текущее поведение SprPoP**, включая уже встроенные исправления. - Дизайн файла и API предусматривает будущий `ENHANCED`, но аудит и переключение fixes сейчас не выполняются. - Уровень 15/copy protection отсутствует. - Моды, levelsets и меню Mods отложены. ## 2. Что есть в SDLPoP `src/menu.c` содержит: - Resume, QuickSave, QuickLoad, Restart Level, Settings, Restart Game, Quit; - General, Gameplay, Visuals, Mods, Controls; - toggle/number/key controls, пояснения, scroll и confirmation dialogs; - большой список fixes/enhancements и custom level options. На Sprinter не переносятся SDL-специфичные параметры: fullscreen, hardware acceleration, scaling, aspect ratio, rumble. UI берёт структуру SDLPoP, но набор настроек соответствует платформе. ## 3. Pause menu первой версии ```text RESUME QUICKSAVE (F6) QUICKLOAD (F9) RESTART LEVEL SETTINGS RESTART GAME QUIT GAME ``` Поведение: - `Esc` в `PLAYING` открывает меню; повторный Esc или Resume возвращает игру; - игра, логический таймер и звуковой насос ставятся на паузу согласованно; - QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на безопасной границе кадра; - QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK; - перед QuickLoad из меню лёгкий probe проверяет заголовок и checksum обоих файлов: валидный `POP.BAK` при отсутствующем/битом `POP.SAV` требует отдельного `LOAD BACKUP?`, а не загружается молча; - Restart Level и Restart Game выполняются сразу, БЕЗ подтверждения (2026-08-22): Restart Level перечитывает уровень, Restart Game завершает gameplay и возвращает к первому экрану title/intro; новая игра создаётся общим LEVEL_LOAD только после skip/attract; - Quit требует подтверждения и закрывает файлы/каналы штатным путём; - меню недоступно в demo, cutscene, time-expired и ending; - отдельная debug-комбинация немедленного выхода может остаться только в отладочной сборке. ## 4. Settings первой версии ```text GENERAL Sound ON / OFF Show Sprinter screen ON / OFF Restore defaults... GAMEPLAY Speed NORMAL / FAST / FASTEST Gameplay profile VANILLA Cheats ON / OFF CONTROLS Show key bindings BACK ``` `Gameplay profile: VANILLA` показывается read-only: место в модели уже есть, но пользователь не может выбрать ещё не реализованный ENHANCED. Изменения применяются немедленно к скорости, читам и звуку, но `POP.CFG` записывается один раз при Back/Esc. На экране есть итог `SETTINGS SAVED` или `SAVE ERROR`; во втором случае runtime-значения остаются рабочими. Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение double buffering не являются пользовательскими Settings. Они остаются compile-time/debug функциями и скрываются из release UI. ## 5. Модель настроек Игровой код не должен читать UI-структуры. Единственный runtime-контракт: ```c typedef enum { POP_PROFILE_VANILLA = 0, POP_PROFILE_ENHANCED = 1 } pop_gameplay_profile_t; typedef struct { uint8_t sound_enabled; uint8_t speed_mode; uint8_t gameplay_profile; uint8_t cheats_enabled; uint8_t show_build_info; uint16_t enhancement_flags; } pop_settings_t; ``` В первой версии загрузчик принимает только `POP_PROFILE_VANILLA`. Значение ENHANCED из более нового/ручного файла заменяется на VANILLA с диагностикой, а не включает частично реализованный режим. Будущий профиль задаёт маску возможностей централизованно: ```text VANILLA -> текущий согласованный набор ENHANCED -> будущий рекомендуемый набор fixes CUSTOM -> только если позже действительно понадобится ``` До отдельного аудита существующие `fix_exit_door`, feather guard behavior, jump grab и sound priorities не переключаются и считаются частью текущего VANILLA. ## 6. Файл POP.CFG Бинарный, компактный, версионированный формат: ```text +0 "PCFG" magic, 4 Б +4 format_version 1 Б +5 payload_size 2 Б +7 payload фиксированные поля little-endian .. checksum 2 Б ``` Требования: - путь рядом с exe/в выделенном каталоге игры на HDD; - неизвестная версия, неверная длина или checksum -> defaults; - неизвестные будущие хвостовые поля можно пропустить по `payload_size`; - запись только после Apply/выхода из Settings, не на каждый шаг курсора; - ошибка записи не завершает игру: показать сообщение и оставить runtime значения; - Restore defaults меняет RAM только после подтверждения и затем сохраняет. CFG не содержит состояние уровня, QuickSave или Hall of Fame. ## 7. QuickSave / QuickLoad в меню Детальный состав снимка и порядок восстановления — в [`quicksave_plan.md`](quicksave_plan.md). Здесь фиксируется UI и файловая транзакция. ### Один слот и backup Файлы: ```text POP.SAV текущий слот POP.BAK предыдущий валидный слот POP.NEW временный файл во время записи ``` Безопасная запись: 1. записать полный снимок в `POP.NEW`; 2. закрыть файл; 3. повторно открыть/прочитать заголовок и checksum; 4. старый валидный `POP.SAV` перенести/скопировать в `POP.BAK`; 5. `POP.NEW` сделать новым `POP.SAV`; 6. при любой ошибке сохранить прежний `POP.SAV`. Точную последовательность rename/copy выбрать после характеризации DSS. Если атомарный rename не гарантирован, использовать copy + fsync/close и никогда не удалять единственную валидную копию до проверки новой. ### Загрузка 1. проверить `POP.SAV`; 2. если он отсутствует/повреждён/несовместим — проверить `POP.BAK`; 3. при валидном BAK показать `LOAD BACKUP?`; 4. несовместимая версия — `INCOMPATIBLE SAVE`, без частичной загрузки; 5. после успеха закрыть menu, перерисовать обе страницы, перезапустить звук. ### Сообщения Минимальный набор: ```text QUICKSAVED QUICKLOADED NO QUICKLOAD SAVE ERROR INCOMPATIBLE SAVE LOAD BACKUP? ``` Сообщение показывается UI-слоем, но операция завершается до возврата в игровой кадр. ## 8. Restart Level / Restart Game Restart Level: - использует существующий штатный reset текущего уровня; - не перечитывает CFG; - не меняет `POP.SAV`; - сбрасывает состояние, которое сбрасывает текущая реализация SprPoP. Restart Game: - выполняется сразу, без подтверждения; - завершить текущий gameplay session и вернуть автомат в TITLE; - начать title/intro с самого первого экрана; - создать новую игру с `FIRST_LEVEL` и новым глобальным таймером только после пользовательского skip либо ввода в attract-demo; - настройки оставить; - QuickSave не удалять. ## 9. Controls Первая версия только показывает активную раскладку. Переназначение клавиш откладывается: raw PS/2 канал имеет особенности Shift и расширенных кодов, поэтому generic key-binding UI требует отдельного проекта. Экран должен перечислить минимум: - движение и Shift/action; - Esc/menu; - F6/F9 QuickSave/QuickLoad; - Ctrl+S sound; - P speed; - доступные cheats, только если они включены: K/Kill Guard, I/Immortal, Shift+L/Next Level, U/Flip Screen и F7/F8/Time −/+ на отдельных понятных строках. Нижней подсказки `Esc or Enter: Back` нет. ## 10. UI renderer и ввод ### 10.1. Выбор способа отрисовки: текст против спрайт-атласов Ограничение платформы: стандартный текстовый вывод libbgi (`outtextxy`) не годится — он тянет системный знакогенератор в `_gfx_font_buf` (2 КБ статики в W2) плюс жирный резидентный код, а W1/W2 забиты игрой (тот же вывод зафиксирован комментарием в `sprpop_cold.c`, где отладочный борд рисуется палочками именно поэтому). Значит, любой вариант требует СВОЕЙ реализации вывода меню, живущей в отдельном банке (память на банк есть; скорость не критична — меню работает на паузе). Рассматривались два подхода. **Вариант A — текстовые строки + собственный растровый рендерер.** Плюсы: - минимальные данные: шрифт 2–4 КБ + таблицы строк по сотни байт на язык; - весь динамический текст бесплатно: значения опций (ON/OFF, NORMAL/FAST/FASTEST), сообщения (`QUICKSAVED`, `INCOMPATIBLE SAVE`), диалоги (`LOAD BACKUP?`), экран Controls, будущий ввод инициалов Hall of Fame — без текстового движка HoF вообще не сделать; - правка формулировки = правка C-строки, мгновенные итерации; - локализация = вторая таблица строк (+ вторая половина глифов); - **решающий аргумент: так сделано в самом SDLPoP** — см. §10.2. Минусы: - надо написать рендерер (блиттер глифа + строка + центрирование + подсветка) — небольшой, но свой; - вид определяется качеством шрифта-ассета. **Вариант B — готовые спрайт-атласы** (атлас главного меню с активными/ неактивными пунктами, атлас вложенного меню, атлас каждой опции On/Off и т.д.). Плюсы: - аутентичный вид: любая типографика/декор запекаются при упаковке; - вывод = существующий блит атласов, текстовый движок не нужен; - язык = другой файл атласа с диска, ноль логики. Минусы: - комбинаторика ассетов: 7 пунктов × состояния + вложенные меню + значения всех опций + все сообщения + все диалоги ≈ десятки КБ raw на язык до RLE; второй язык удваивает; - любая правка текста = перегенерация ассетов + перекладка ресурсов; - динамический текст (HoF initials) всё равно потребует шрифтового движка — получили бы ОБЕ системы сразу. **Решение (2026-08-22): Вариант A**, шрифт — ассет. Спрайты остаются только для нетекстового декора (рамка/фон меню, маркер выделения — как arrowheads в SDLPoP). Титульный экран — полноэкранная картинка, тема `full_game_plan.md`. ### 10.2. Референс: как устроено меню в SDLPoP `SDLPoP/src/menu.c` + текстовый движок `seg009` — источник структуры: - **Текстовые строки + встроенный пропорциональный bitmap-шрифт** `hc_small_font_data[]` (menu.c:2488): символы 32..126, каждый глиф — монохромное изображение переменной ширины; `font_type` {first_char, last_char, space_between_chars, height_above_baseline, chtab}. Никаких per-item атласов, хотя SDL_ttf доступен. - Вывод — портированный движок оригинального DOS PoP (seg009): `draw_text_character` → `method_3_blit_mono(image, x, y, textblit, textcolor)`; `get_line_width` для центрирования; перенос по словам. Тем же движком рисуются in-game тексты и copy protection. - Пункты меню — data-driven C-структуры `{id, previous, next, required, char text[32]}` + таблицы `pause_menu_items[]` / `settings_menu_items[]`; `required` — указатель на флаг disabled, такие пункты пропускаются при навигации (prev/next пересчитываются). - Выделенный пункт = смена цвета текста (bright-white против обычного) + рамка-контур `draw_rect_contours(selection_box, lightgray)`; НЕ отдельный спрайт «активного пункта». - Фон меню — затемнение замороженного игрового кадра: `draw_rect_with_alpha(black, alpha=120)`, внизу просвечивает «GAME PAUSED». - Settings — декларативная таблица `setting_type` со стилями TOGGLE / NUMBER / TEXT_ONLY / KEY, геттером/сеттером/increase/decrease значения, строкой- explanation внизу экрана, скроллом длинных списков и фокусом «левая половина (список) / правая половина (значения)». - Диалоги — один общий `draw_confirmation_dialog(text)` + обработчик результата; диалог возвращает решение автомату меню. - Мини-спрайты только для декора значений (arrowheads up/down/left/right). - Навигация озвучена (menu tick), ввод клавиатура+мышь, hover по прямоугольникам. ### 10.3. Наша реализация - Банк 9: код рендерера, шрифт, таблицы строк, автомат меню. Резидентно — только request-flag и вызов процесса на границе кадра (паттерн pop_qsave_io). - Рендерер повторяет минимальный контракт seg009: пропорциональные глифы, baseline, `draw_string` и центрирование по сумме advance. Блит идёт через W0-атлас, в `GFX_BANK_SPRITE`: `0xFF` в атласе пропускается, а UI временный и не портит теневую копию игрового фона. Перед каждым кадром UI `gfx_copy_page` переносит чистый shadow видимой страницы в скрытую, затем готовый кадр показывается только на следующем фронте. При выходе чистый фон тем же способом возвращается на обе страницы и восстанавливается исходная visible-страница. Поэтому перемещение выделения не показывает поэтапную перерисовку и не оставляет следов на back buffer. - Шрифт — АССЕТ из **оригинальных** `hc_small_font_data[]` и `hc_font_data[]` SDLPoP, не системный ZG и не TTF. Паковщик `toolchain/pop_extract_font.py` делает `FONT\\font.atl`: 95 ASCII-глифов малого и 95 крупного шрифта (7667 Б). Номер ленты вычисляется из ASCII, поэтому это один текстовый движок, а не атлас готовых надписей. - Двуязычность (eng/rus): строки храним в CP866 — латиница и кириллица одним байтовым порядком, одна кодировка на оба алфавита. Локаль = пара (указатель на таблицу строк, файл шрифта); переключатель — одна настройка. Русские строки длиннее английских ~10–15% — раскладку экранов и ширину колонок закладывать по русской. Второй язык можно добавить позже без переделки: сначала eng. - Визуальная композиция MS4 следует SDLPoP: замороженная сцена остаётся открытой, поверх неё компактный центрированный список без чёрной карточки, выбранная строка обведена тонким светло-серым контуром, а крупное `GAME PAUSED` лежит в нижнем борту. Цвета текста и контура берутся из стабильного диапазона палитры 0x37..0x3F. - Фон открытого меню: снимок текущей палитры, затемнение всех слотов кроме UI 0x37..0x3F и точное восстановление при выходе. Снимок хранится в свободном хвосте EMM-страницы шрифта, не в W2. - Навигация MS4: вверх/вниз, Enter/Esc, edge-triggered поверх `kbd_raw`. Left/right и menu tick добавляются вместе с настройками на MS5. Первый UI может быть визуально простым. Критично отсутствие потери клавиш, предсказуемая пауза и отсутствие повреждения игрового back buffer. ### 10.4. Затенение экрана под меню — решение MS4 Режим меню виден сразу: bank 9 делает динамический снимок palette 0, затемняет RGB-каналы вдвое и пишет одинаковый результат в обе экранные палитры. Девять стабильных UI-слотов 0x37..0x3F не гасятся. При Resume/Enter палитра восстанавливается из EMM-снимка. Это выбранный вариант Б ниже; ступенчатый fade для роликов пока не нужен и остаётся отдельной будущей задачей, а не причиной раздувать MS4. **Как сделано в SDLPoP** (`seg009.c`): - Меню: `draw_rect_with_alpha(&screen_rect, color_0_black, pause_menu_alpha)` (menu.c:1364) — альфа-заливка чёрным поверх замороженного кадра средствами SDL; нижняя полоса рисуется с alpha=0, чтобы сквозь неё просвечивало «GAME PAUSED». Прямого аналога на Sprinter НЕТ (альфа-блендинг в железе отсутствует) — это SDL-специфика, переносить нечего. - Ролики/переходы: `fade_in_2/fade_out_2(rows)` (seg009.c:3947+, вызовы из seg000.c) — ПОШАГОВОЕ затухание ПАЛИТРЫ к чёрному и обратно: палитра копируется, каждая строка по 16 цветов гасится за несколько кадров (`which_rows` маской выбирает, какие строки участвуют: 0x800/0x1000/...). Вот этот механизм на Sprinter воспроизводим один в один. Отсюда рабочая гипотеза: наш примитив = «снимок текущей палитры → ступенчатое приближение к затемнённой копии (кроме резервного блока для UI)», статично для меню и анимированно для роликов/переходов. Варианты: **Вариант А — единая основная палитра (глобальный рефакторинг палитры).** 1. Собрать ВСЕ палитры игры (уровневые наборы `pal_env*`, kid.pal, палитра Тени и пр.) в одну общую 256-цветную; использовать её целиком всегда. Сейчас переиспользования цветов НЕТ — каждая загрузка ассетов перезаписывает слоты (см. pop_boot: kid.pal затирает тайловые цвета, приходится восстанавливать `pop_bg_pal_apply`/`pop_shadow_pal_apply`). 2. Для затенения — затемнённая копия основной палитры, КРОМЕ зарезервированного блока из 16 цветов для самого меню (кандидат — стандартные 16 цветов VGA). 3. Выход из меню — возврат к полной основной палитре. Плюс: решает попутно существующую боль с перезаписью палитр при загрузках. Минус: большой разовый рефакторинг упаковщиков и всех загрузчиков атласов; нужен аудит, что все цвета всех уровней влезают в 256. **Против говорит план перевода камней подземелья на цвета VGA-версии PoP: там ряд уровней несёт ДРУГУЮ палитру, отличную от SDLPoP (VDUNGEON/VPALACE каскад, levels_plan.md), — единая палитра этому прямо противоречит.** **Вариант Б — динамический снимок текущей палитры (сейчас выглядит предпочтительным).** 1. При открытии меню прочитать всю текущую палитру, сохранить. 2. Записать затемнённую копию (кроме зарезервированного блока для меню). 3. При выходе — восстановить сохранённую. Плюс: локальная правка внутри меню, ничего в пайплайне ассетов не меняется; работает при любой текущей палитре автоматически — включая будущие уровне-специфичные палитры VGA-камней; тот же примитив ступенями даёт fade-out/fade-in для роликов и переходов между уровнями (как fade_*_2 в SDLPoP). Минус: чтение/запись 256 записей палитры при входе/выходе (раз на открытие — дёшево); затемнение «на глаз» может по-разному выглядеть на разных уровнях. Резервный блок 16 цветов нужен в ОБОИХ вариантах; текущий диапазон 0x37..0x3F (стабильный, проверен) даёт 9 цветов — этого может не хватить на текст+подсветку+рамку, тогда резервировать отдельный блок. **Следствие для архитектуры:** работа с цветом/палитрой должна собраться в ОДИН модуль (сейчас она разбросана: gfx_pal_* вызовы в boot, pop_bg_pal_apply, pop_shadow_pal_apply, вспышки урона в sprpop.c и т.д.). Модуль палитры — единственный владелец записи в палитру и предоставляет примитивы, которые понадобятся и меню, и роликам: ```text pal_snapshot()/pal_restore() — снимок/восстановление всей палитры pal_dim(step) / pal_undim(step) — ступени затемнения (кроме резервного блока) pal_fade_out(rows)/pal_fade_in(rows) — анимированное затухание по строкам (порт fade_out_2/fade_in_2, seg009) ``` Меню уже использует snapshot+dim локально в bank 9. Когда появятся ролики, выделить из него общий palette/fade-модуль; вспышка урона сможет переехать туда же после отдельного аудита. ## 11. Диалоги Общий диалог подтверждения: ```text QUIT GAME? RESTORE DEFAULTS? LOAD BACKUP? YES / NO ``` Диалог не выполняет действие напрямую: он возвращает решение автомату меню, который формирует команду приложению. Так UI не зависит от gameplay-модулей. По умолчанию выбран `NO`; Up/Down/Left/Right меняют ответ, Enter подтверждает, Esc отменяет. Реализованы все три вопроса: Quit, Restore defaults и backup QuickLoad. В Quit-dialog вопрос и `YES / [NO]` заключены в общую рамку; отдельная строка `Enter: Select Esc: Cancel` не выводится. ## 12. Будущий ENHANCED Не реализуется сейчас, но дизайн обязан позволять: - добавить второй профиль без смены всего UI; - хранить `enhancement_flags` в CFG; - отличать технические исправления порта (всегда включены) от изменений оригинальной механики; - провести аудит уже встроенных исправлений; - покрыть каждый переключаемый fix host/MAME тестом; - при необходимости добавить Advanced screen, не раздувая основной menu. До этого момента нельзя рассыпать проверки `if (enhanced)` по горячему коду. Сначала составляется реестр и выбирается минимальная битовая модель. ## 13. Этапы реализации | этап | результат | критерий приёмки | |---|---|---| | **MS0** ✓ | определить команды app/menu и структуру settings | UI возвращает команду главному циклу; прямых gameplay-вызовов нет | | **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save | | **MS2** ✓ | `POP.CFG`: defaults, load, validate, save | v1 codec, будущий хвост, checksum; повреждённый CFG даёт defaults | | **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` | | **MS4** ✓ | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | SDLPoP fonts в одном W0-atlas, центрирование, dim/restore palette и tear-free page flip; все семь пунктов видимы, навигация и Resume/QuickSave работают в MAME | | **MS5** ✓ | General/Gameplay Settings | значения применяются сразу и после Back/Esc записываются в POP.CFG | | **MS6** ✓ | dialogs + backup recovery | подтверждения default-NO; QuickLoad спрашивает перед валидным POP.BAK | | **MS7** ✓ | Controls help | показаны движение, action, menu, save/load, звук, speed и conditional cheats; MAME проверил отдельные K/I и Shift+L/U и возврат Esc ровно на один уровень | | **MS8** ✓ | build info | CFG читается до первого показа; включаемый build screen получает ID и дату из Make/git | QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала F6/F9 и сообщения, затем подключить те же команды к пунктам UI. ## 14. Тесты - Host: CFG round-trip, defaults, bad magic/version/size/checksum. - Host: меню navigation, disabled items, confirmations, команды приложению. - Host: рендерер строк — вывод глифов обеих локалей, центрирование, ширина строки для малого и крупного шрифта. - Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD. - MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9. - MAME: прервать запись/испортить SAV — BAK остаётся загружаемым. - MAME: pause на бое/падении, Resume не меняет состояние и таймер; смена выбранного пункта не показывает промежуточный кадр и после закрытия не оставляет меню на второй странице. - MAME: Settings сохраняются после полного выхода и запуска с HDD. - MAME: включить Show Sprinter screen, перезапустить `SprPoP`, увидеть build ID/date до первого игрового кадра и пропустить экран Esc/Enter/Space. - Проверка лимита 8 DSS handles на каждом error path. - `make size-check`; menu/text строки не должны съесть резидентный бюджет. ## 15. Не входит в план - Mods и выбор levelset; - уровень 15/copy protection; - несколько save slots; - replay/recording; - key rebinding; - SDL visual/controller options; - фактическая реализация ENHANCED и individual fix switches.