SprPoP: автономное приложение, выделенное из roomtest

Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 12:12:28 +03:00
parent 4b74478d19
commit 31b82661eb
235 changed files with 51293 additions and 10 deletions
@@ -0,0 +1,529 @@
# Pause menu и Settings для Sprinter PoP
Статус: **MS0, MS2 и MS4MS8 выполнены** (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.