QuickSave F6/F9 в roomtest; bank_load_file/bank_save_file/gfx_w0_page_prepare; sprinter-cc: авто n_banks

roomtest:
- QuickSave/QuickLoad (F6/F9): снапшот 'POPQ' v3 в POP.SAV/POP.BAK на HDD,
  транзакционная запись (POP.NEW -> rename, откат при ошибке), XOR-контрольная
  сумма payload'а; сериализация всех игровых переменных через W0-примитивы
  pop_qs_*; pop_qsave_process() на границе кадра вне Char-окон
- pop_qsave_restore_room(): полная перезагрузка комнаты после загрузки
  (карта/края/швы, сброс bake-кэша, перерисовка обеих страниц, инвалидация
  кэшей спрайтов и HP)
- сериализаторы в pop_map/pop_loose_mob/pop_trob/pop_guard_ai
  (+ восстановление инвариантов: mobs_live, trob_drawn, redraw)
- immortal-чит 2 уровня: уровень 2 поглощает только малый урон Kid'а

libc/libbgi:
- bank_load_file()/bank_save_file() — резидентное файловое I/O в банк,
  без правила W3 (путь читается до переключения страницы)
- gfx_w0_page_prepare(page) — подготовка W0-окна (IRQ/NMI-стабы) одной
  функцией; atlas_load.c и roomtest переведены на новые примитивы;
  ручные ISR-стабы удалены

sprinter-cc / сборка:
- --bank N=FILE.c: автогенерация n_banks (_n_banks_auto.c), ручные
  const n_banks удалены из тестов
- roomtest/app.mk: ресурсы через stamp-файлы (.resource-stamps/) — один
  запуск упаковщика на группу вместо N под -B; HDD_PACK_ARGS
This commit is contained in:
2026-08-22 11:53:10 +03:00
parent b6699b3aef
commit f4b4852d51
57 changed files with 1610 additions and 301 deletions
+2
View File
@@ -14,6 +14,8 @@
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 |
| [`full_game_plan.md`](full_game_plan.md) | **Полноценная игра**: app state machine, title/intro, demo level 0, таймер, cutscenes, уровни 1..14, ending и Hall of Fame. Уровень 15 исключён. 2026-08-21 |
| [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED. 2026-08-21 |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
+349
View File
@@ -0,0 +1,349 @@
# От roomtest к полноценной игре — сценарий и оболочка
Статус: **согласованный план, код не начат** (2026-08-21).
Этот документ описывает превращение текущего игрового цикла
`roomtest` в законченную игру: заставка, интро, демонстрационный уровень,
сцены между уровнями, таймер, финал и Hall of Fame. План меню и постоянных
настроек вынесен в [`menu_settings_plan.md`](menu_settings_plan.md),
детальный план QuickSave — в [`quicksave_plan.md`](quicksave_plan.md).
## 1. Зафиксированный scope
- Целевая последовательность — оригинальная SDLPoP/DOS PoP с уровнями
**1..14**. Уровень 14 — скрытая финальная часть после Джаффара: его номер
игроку не показывается, победа наступает в комнате 5.
- Уровень **15 удаляется полностью**: не пакуется на HDD, не загружается,
отсутствует в переходах, читах и UI; специальная логика potions/copy
protection level удаляется.
- Уровень **0** остаётся только демонстрационным (attract mode), а не частью
новой игры.
- Title sequence повторяет SDLPoP. Перед ней допускается отдельный
пропускаемый экран с информацией о Sprinter-сборке.
- Первая версия использует текущий профиль поведения `VANILLA`. Сейчас это
означает **существующую реализацию roomtest**, включая уже встроенные
исправления. Аудит и разведение `VANILLA/ENHANCED` — будущая задача.
- Программа работает **только с HDD**. Варианты без сохранения для floppy не
проектируются.
- Моды и выбор levelset в этот план не входят.
## 2. Что делает SDLPoP
Источники истины в локальном SDLPoP:
- `src/seg000.c`: `start_game()`, `show_title()`, demo mode, общий кадр,
проверка финала;
- `src/seg003.c`: `init_game()`, `play_level()`, `play_level_2()`;
- `src/seg001.c`: cutscene engine, `pv_scene()`, сцены 2/4/6/8/9/12,
`time_expired()`, `end_sequence()` и Hall of Fame;
- `src/data.h`: `tbl_cutscenes`, параметры уровня 0, win level/room;
- `data/TITLE`, `data/PV`, `data/LEVELS/res2000.bin`: ресурсы оболочки.
Штатный маршрут:
```text
boot
-> title / story screens
-> Princess + Jaffar intro
-> credits / Hall of Fame
-> demo level 0
-> title или новая игра
-> levels 1..14
before 2 -> princess cutscene
before 4 -> princess cutscene
before 6 -> princess cutscene
before 8 -> princess + mouse
before 9 -> princess + mouse
before 12 -> scene selected by remaining time
-> level 14, room 5
-> embrace + mouse
-> ending text/music
-> Hall of Fame
-> title
```
Кроме уровней, здесь есть глобальный 60-минутный таймер, сцена истечения
времени, пропуск сцен клавишей, fade/flash, ожидание музыки и возврат в
attract loop после демо или финала.
## 3. Текущее состояние roomtest
Уже реализованы игровой кадр, комнаты, уровни, тайлсеты, Kid/Guard/Shadow,
Джаффар, специальные события, checkpoint, переходы уровней, бесшовный
выход 12-го уровня, перенос максимального HP и звуковые эффекты.
Отсутствуют:
- верхнеуровневая оболочка приложения;
- title/story sequence и текстовые экраны;
- уровень 0 и записанное управление демо;
- общий cutscene engine и PV-ресурсы;
- сцены между уровнями;
- глобальный таймер и time-expired sequence;
- корректный переход из уровня 14 в ending;
- финальная сцена и Hall of Fame;
- возврат к title без перезапуска программы.
## 4. Архитектура: автомат состояний приложения
Нельзя наращивать все режимы условиями внутри кадрового цикла. Текущий
цикл должен стать реализацией одного состояния `PLAYING`:
```text
BOOT -> BUILD_INFO -> TITLE -> INTRO -> DEMO
| |
+---- NEW_GAME <-+
NEW_GAME -> LEVEL_LOAD -> PLAYING <-> PAUSE_MENU
|
+-> CUTSCENE -> LEVEL_LOAD
+-> TIME_EXPIRED -> TITLE
+-> ENDING -> HALL_OF_FAME -> TITLE
```
Минимальный контекст оболочки:
```c
typedef enum {
POP_APP_BOOT,
POP_APP_BUILD_INFO,
POP_APP_TITLE,
POP_APP_INTRO,
POP_APP_DEMO,
POP_APP_LEVEL_LOAD,
POP_APP_PLAYING,
POP_APP_PAUSE_MENU,
POP_APP_CUTSCENE,
POP_APP_TIME_EXPIRED,
POP_APP_ENDING,
POP_APP_HALL_OF_FAME,
POP_APP_QUIT
} pop_app_state_t;
```
Переходы задаются результатом состояния, а не прямыми рекурсивными
вызовами наподобие SDLPoP `start_game()`/`longjmp()`. На Z80 это проще для
стека и позволяет освобождать ресурсы каждого режима в одном месте.
## 5. Ресурсная модель
Title и cutscene-ресурсы нельзя постоянно держать рядом с игровыми
атласами. Для каждого состояния нужен явный lifecycle:
```text
enter: pause sound -> unload incompatible set -> load set -> apply palette
run: process input/timer/animation
leave: stop sound -> release EMM pages -> clear transient state
```
Новые группы HDD:
```text
TITLE\ title/story images, palette, optional build-screen assets
PV\ princess room, Princess/Jaffar/mouse frames, palettes
MUSIC\ intro, cutscene and ending tracks/samples
LEVELS\ res2000..res2014.bin
```
Конкретный формат атласов выбирает упаковщик. Runtime не должен разбирать
PNG/DAT: как и игровые спрайты, он получает подготовленные `.atl`/`.bin`.
## 6. Экран Sprinter build
Отдельное состояние перед оригинальной заставкой:
```text
PRINCE OF PERSIA
SPRINTER SP2000 BUILD
version / date / build id
```
Требования:
- пропускается любой клавишей;
- выключается в Settings;
- не запускает музыку оригинального title и не меняет её тайминги;
- данные версии генерируются сборкой, а не правятся вручную в C;
- отсутствие экрана приводит прямо к `TITLE`.
## 7. Title и текстовая подсистема
Порядок переносится из `show_title()`:
1. основной титульный экран;
2. Presents;
3. название игры и Jordan Mechner;
4. story frame / “In the absence…”;
5. intro Princess + Jaffar;
6. story “Marry Jaffar…”;
7. credits;
8. Hall of Fame, если таблица непуста;
9. demo level 0.
Нужны общие примитивы: загрузить full-screen image, вывести строку,
показать экран заданное время, transition left-to-right, fade in/out,
прервать ожидание клавишей. Текст и меню должны использовать один renderer.
Критерий: последовательность и музыкальные точки совпадают с SDLPoP;
Sprinter build screen не сдвигает оригинальный soundtrack.
## 8. Demo level 0
- Добавить на HDD `res2000.bin`.
- Загружать уровень обычным loader, но выставлять demo HP и demo mode.
- Воспроизводить `demo_moves` как синтетический источник `control_*`.
- Пользовательский ввод прерывает демо и начинает новую игру.
- Достижение demo end room (у SDLPoP — 24), смерть или конец скрипта
возвращают в `TITLE`.
- Pause menu, QuickSave и cheats в demo недоступны.
- RNG демо и начальное состояние должны быть детерминированы.
Критерий: без ввода attract loop не требует перезапуска процесса;
title -> demo -> title повторяется неограниченно.
## 9. Глобальный таймер
Состояние: минуты, тики и флаг показа. Таймер создаётся при New Game,
переносится между уровнями и входит в QuickSave.
Правила `VANILLA`:
- на pause menu, загрузке HDD, QuickSave/QuickLoad время не идёт;
- игровые тики следуют темпу логического кадра, а не частоте render loop;
- поведение во время level-end sound и cutscenes сверяется буквально с
SDLPoP;
- после Джаффара/на финальном уровне время не должно вызвать поражение;
- ноль времени переводит приложение в `TIME_EXPIRED`.
Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени,
что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики.
## 10. Cutscene engine
Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора
крупных C-функций нужен компактный интерпретатор:
```text
SET_ACTOR actor
SET_POS x,y,dir
START_SEQ seq
WAIT_FRAMES n
PLAY_SOUND id
WAIT_SOUND
SET_HOURGLASS frame
SET_SAND state
FLASH color,frames
FADE_IN / FADE_OUT
CLEAR_ACTOR actor
END
```
Скрипты — `const` в холодном банке или подготовленный бинарный ресурс.
Interpreter обязан:
- исполнять один шаг/кадр без блокирующих длинных циклов;
- поддерживать пропуск сцены;
- при пропуске выполнять cleanup и выходить в заранее заданное состояние;
- освобождать PV-ресурсы перед загрузкой игрового тайлсета;
- не разрешать pause menu/QuickSave внутри сцены.
Порядок переноса: intro, 2/6, 4, 8, 9, 12, time expired, ending. Сцена 12
выбирает короткий или обычный вариант по остатку времени.
## 11. Переходы между уровнями
Таблица сценария должна быть отдельна от таблиц механики уровня:
```c
typedef struct {
uint8_t level;
uint8_t pre_cutscene;
uint8_t show_level_number;
uint8_t ending_rule;
} pop_level_flow_t;
```
Особые правила:
- New Game начинает уровень 1;
- перед 2/4/6/8/9/12 запускается сцена;
- 12 -> 13 остаётся бесшовным;
- после победы над Джаффаром переход идёт в 14;
- номер 14 не показывается;
- вход в комнату 5 уровня 14 переводит в `ENDING`;
- значения больше 14 недопустимы и дают диагностическую ошибку, а не
попытку открыть файл.
## 12. Ending и Hall of Fame
Ending:
1. загрузить PV-набор;
2. встреча Kid и Princess;
3. объятие;
4. появление мыши;
5. ending music;
6. финальные story/title экраны;
7. переход в Hall of Fame.
Hall of Fame хранится на HDD в отдельном версионированном `POP.HOF`.
Сохраняются имя и результат; ввод имени использует тот же текстовый/UI слой.
Повреждённый или неизвестный формат означает пустую таблицу, но не мешает
запуску игры. После показа — возврат в `TITLE`.
## 13. Удаление уровня 15
Отдельный ранний этап, чтобы новый flow не наследовал лишний маршрут:
- убрать `res2015.bin` из `LVL_NUMS` и HDD image;
- заменить последний игровой уровень на 14;
- остановить Shift+L и прочую навигацию на 14;
- удалить `POP_POTIONS_LEVEL` и специальный половинный урон синих зелий;
- исключить copy protection из конфигурации и меню;
- добавить тест: после уровня 14 приложение входит в ending и никогда не
запрашивает `res2015.bin`.
## 14. Этапы реализации
| этап | результат | критерий приёмки |
|---|---|---|
| **FG0** | удалить уровень 15, формализовать 1..14 | HDD не содержит res2015; переход выше 14 невозможен |
| **FG1** | автомат состояний, текущая игра = PLAYING | старт/рестарт/выход проходят без рекурсии и утечки EMM |
| **FG2** | QuickSave/QuickLoad | критерии `quicksave_plan.md`, включая POP.BAK |
| **FG3** | минимальный pause menu | Resume/Save/Load/Restart/Settings/Quit работают |
| **FG4** | глобальный таймер | совпадение с SDLPoP и корректный save/load |
| **FG5** | text/full-screen/fade primitives | тестовые экраны и переходы на Sprinter |
| **FG6** | build info + оригинальный title | точный порядок, корректный пропуск |
| **FG7** | demo level 0 | бесконечный attract loop, ввод начинает игру |
| **FG8** | cutscene interpreter + intro | интро проходит и пропускается без утечек |
| **FG9** | сцены 2/4/6/8/9/12 | правильный вызов один раз перед уровнем |
| **FG10** | time expired | отдельная сцена и возврат на title |
| **FG11** | level 14 -> ending | полный маршрут после Джаффара |
| **FG12** | Hall of Fame | запись HDD, ввод имени, возврат к attract loop |
QuickSave допускается реализовать до остальных частей оболочки: FG2 — одна
из ближайших самостоятельных задач.
## 15. Проверки
- Host-тест автомата: все допустимые переходы и отсутствие уровня 15.
- Host-тест cutscene interpreter на синтетическом скрипте и skip в каждой
ожидающей команде.
- Host-тест demo input: одинаковый seed даёт одинаковый поток управления.
- MAME: cold boot -> build info -> title -> demo -> title.
- MAME: новая игра -> принудительный переход по всем pre-level scenes.
- MAME: time expired и пропуск сцены.
- MAME: 13 -> 14 -> room 5 -> ending -> HOF -> title.
- Проверка EMM/FD до и после каждого состояния: число страниц и открытых
файлов возвращается к базовому.
- `make size-check`; крупный cold-код размещать в банках и отдельно следить
за лимитом 16 КБ каждого банка.
## 16. Не входит в план
- уровень 15 и copy protection;
- моды и выбор levelset;
- replay/recording;
- точная эмуляция SDL video/controller options;
- профиль ENHANCED и индивидуальные switches fixes.
+319
View File
@@ -0,0 +1,319 @@
# Pause menu и Settings для Sprinter PoP
Статус: **согласованный план, код не начат** (2026-08-21).
Связанные документы:
- [`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`. Под этим именем пока понимается
**текущее поведение roomtest**, включая уже встроенные исправления.
- Дизайн файла и 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
QUICKLOAD
RESTART LEVEL
SETTINGS
RESTART GAME
QUIT
```
Поведение:
- `Esc` в `PLAYING` открывает меню; повторный Esc или Resume возвращает игру;
- игра, логический таймер и звуковой насос ставятся на паузу согласованно;
- QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на
безопасной границе кадра;
- QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK;
- Restart Level и Restart Game требуют подтверждения;
- 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.
Отладочные параметры `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`;
- сбрасывает состояние, которое сбрасывает текущая реализация roomtest.
Restart Game:
- подтверждение;
- завершить текущий gameplay session;
- освободить level/atlas/temporary EMM;
- создать новую игру с уровня 1 и новым глобальным таймером;
- настройки оставить;
- 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, только если они включены.
## 10. UI renderer и ввод
Меню использует общую с title текстовую подсистему:
- фиксированный bitmap font без `_gfx_font_buf` на 2 КБ в W2;
- фон/рамка и выделенная строка;
- вертикальная навигация, left/right для значения, Enter, Esc;
- edge-triggered клавиши поверх существующего `kbd_raw`;
- двойная буферизация либо один заранее сохранённый фон menu;
- строки и холодный код — в отдельном банке, постоянное состояние — в W2.
Первый UI может быть визуально простым. Критично отсутствие потери клавиш,
предсказуемая пауза и отсутствие повреждения игрового back buffer.
## 11. Диалоги
Общий диалог подтверждения:
```text
RESTART LEVEL?
RESTART GAME?
QUIT GAME?
RESTORE DEFAULTS?
LOAD BACKUP?
YES / NO
```
Диалог не выполняет действие напрямую: он возвращает решение автомату меню,
который формирует команду приложению. Так UI не зависит от gameplay-модулей.
## 12. Будущий ENHANCED
Не реализуется сейчас, но дизайн обязан позволять:
- добавить второй профиль без смены всего UI;
- хранить `enhancement_flags` в CFG;
- отличать технические исправления порта (всегда включены) от изменений
оригинальной механики;
- провести аудит уже встроенных исправлений;
- покрыть каждый переключаемый fix host/MAME тестом;
- при необходимости добавить Advanced screen, не раздувая основной menu.
До этого момента нельзя рассыпать проверки `if (enhanced)` по горячему коду.
Сначала составляется реестр и выбирается минимальная битовая модель.
## 13. Этапы реализации
| этап | результат | критерий приёмки |
|---|---|---|
| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую |
| **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save |
| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults |
| **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` |
| **MS4** | минимальный pause menu | все семь пунктов доступны и корректно паузят игру |
| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт |
| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK |
| **MS7** | Controls help | полная актуальная раскладка на экране |
| **MS8** | интеграция с title/build info | CFG применяется до первого экрана |
QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала
F6/F9 и сообщения, затем подключить те же команды к пунктам UI.
## 14. Тесты
- Host: CFG round-trip, defaults, bad magic/version/size/checksum.
- Host: menu navigation, disabled items, confirmations, команды приложению.
- Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD.
- MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9.
- MAME: прервать запись/испортить SAV — BAK остаётся загружаемым.
- MAME: pause на бое/падении, Resume не меняет состояние и таймер.
- MAME: Settings сохраняются после полного выхода и запуска с HDD.
- Проверка лимита 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.
+15 -13
View File
@@ -180,12 +180,14 @@ SDLPoP, сохраняются.
---
## 4. Куда писать снимок: файл, а не EMM-страница
## 4. Куда писать снимок: HDD-файл, а не EMM-страница
Рекомендация: **основной путь — файл `QUICKSAVE.SAV`; EMM-страница —
необязательный второй слот.**
Решение: **один основной слот `POP.SAV` на HDD; предыдущая
валидная запись хранится в `POP.BAK`.** EMM-слота нет: программа
работает только с HDD, а главный сценарий QuickSave обязан переживать
перезапуск игры.
> Пересмотрено 2026-08-17 по вопросу пользователя «почему EMM, а не файл».
> Пересмотрено 2026-08-21 по вопросу пользователя «почему EMM, а не файл».
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
> скорость и недооценивала главный сценарий использования. Разбор оставлен
> целиком, потому что довод переносится и на другие «положить в память
@@ -214,10 +216,8 @@ MAME, обязательный после каждой пересборки об
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
строки.
Что остаётся за EMM: мгновенный слот для «переиграть» без обращения к диску.
Делается тем же сериализатором и добавляется, если понадобится. Поэтому
обход состояния писать сразу так, чтобы «куда» было параметром — как у
SDLPoP через `process_func`.
Обход состояния всё равно писать с абстракцией чтения/записи, как у SDLPoP
через `process_func`, но второй EMM-слот в scope не входит.
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
@@ -234,6 +234,7 @@ roomtest.
+5 pop_current_level 1 Б
+6 длина полезной части 2 Б (контроль, что обход совпал)
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
.. checksum 2 Б (заголовок + payload)
```
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
@@ -282,13 +283,12 @@ static void qs_walk(qs_io_t io) /* io = запись или чтение */
| шаг | что | критерий готовности |
|---|---|---|
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы |
| **QS2** | Ядро: `qs_walk` + запись/чтение файла `QUICKSAVE.SAV`, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились |
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
| **QS2** | Ядро: `qs_walk` + `POP.SAV`, магия/версия/checksum, безопасная замена с предыдущей валидной копией в `POP.BAK` | сохранение и загрузка **в той же комнате, без движения**; порча SAV не портит BAK |
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
| **QS6** | Опционально: второй слот в EMM-странице тем же сериализатором | мгновенное «переиграть» без обращения к диску |
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
@@ -320,5 +320,7 @@ QS2 нечего проверять, а QS5 обязан идти после QS3
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
страницу. Симптом — мерцание через кадр
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы
достаточно. Решать после QS3, по факту использования.
6. **Транзакция SAV/BAK.** До кодинга проверить на DSS семантику
rename/replace. Если атомарная замена не гарантирована, писать через
`POP.NEW`, проверять его после close и не удалять единственную валидную копию
до завершения новой.