diff --git a/.gitignore b/.gitignore index 400d45c..078f2bd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ # sprinter-cc per-example intermediate directory .sprinter-cc-*/ +.resource-stamps/ # Per-program final/intermediate outputs landing alongside the source # (real apps under examples/ and libc feature tests under tests/). diff --git a/README.md b/README.md index 595a8aa..76dafa0 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,8 @@ sprinter-cc -o foo.exe foo.c [more.c ...] [options] --memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3) --stack-size N bytes reserved for the stack (default ~1278) --crt0=TYPE default | minimal | banked | small - --bank N=FILE.c compile FILE.c into bank N (repeatable, max 15) + --bank N=FILE.c compile FILE.c into bank N (repeatable, consecutive 1..15; + crt0 bank count is generated automatically) --debug enable runtime diagnostics (defines DEBUG_RT) -I PATH extra include path -L 0xADDR / -E / -S override load / entry / stack addresses diff --git a/app.mk b/app.mk index 063beea..6a52c9f 100644 --- a/app.mk +++ b/app.mk @@ -44,6 +44,9 @@ RUN_MAME := $(MAME_DIR)/run_mame.sh # Optional knobs — see top of file. MEMORY ?= tiny SOURCES := $(EXAMPLE).c $(EXTRA_SRCS) +# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со +# своей раскладкой каталогов может переопределить переменную до include. +HDD_PACK_ARGS ?= $(EXAMPLE).exe $(EXTRA_DATA) CC_FLAGS := --memory $(MEMORY) ifneq ($(STACK_SIZE),) @@ -92,7 +95,7 @@ run: floppy # используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа # MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory). hdd: $(EXAMPLE).exe - $(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA) + $(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS) @echo @echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))" @echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран." diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index 1c432c1..fc80e1f 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -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 | diff --git a/applications/PoP/docs/full_game_plan.md b/applications/PoP/docs/full_game_plan.md new file mode 100644 index 0000000..33c2b39 --- /dev/null +++ b/applications/PoP/docs/full_game_plan.md @@ -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. + diff --git a/applications/PoP/docs/menu_settings_plan.md b/applications/PoP/docs/menu_settings_plan.md new file mode 100644 index 0000000..813e89e --- /dev/null +++ b/applications/PoP/docs/menu_settings_plan.md @@ -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. + diff --git a/applications/PoP/docs/quicksave_plan.md b/applications/PoP/docs/quicksave_plan.md index c459d5c..e1f4a79 100644 --- a/applications/PoP/docs/quicksave_plan.md +++ b/applications/PoP/docs/quicksave_plan.md @@ -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`) через ``, флаги `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 и не удалять единственную валидную копию + до завершения новой. diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 8944583..b4e5c03 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -35,7 +35,32 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 5=pop_tile_cold.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS) +# Единственный список банковых исходников: из него строятся И ключи +# sprinter-cc, И зависимости roomtest.exe. Раньше эти списки жили отдельно, +# поэтому добавленные pop_shadow.c/pop_sfx_cold.c компилировались, но make не +# замечал их последующие изменения и оставлял устаревший exe. +BANK1_SRCS := guards.c +BANK2_SRCS := pop_bg.c +BANK3_SRCS := pop_map.c +BANK4_SRCS := pop_cdraw.c pop_kdraw.c +BANK5_SRCS := pop_ctrl.c pop_tile_cold.c +BANK6_SRCS := pop_trob.c +BANK7_SRCS := pop_room.c pop_redraw.c +BANK8_SRCS := roomtest_cold.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \ + pop_shadow.c pop_sfx_cold.c pop_qsave.c pop_qsave_coldio.c \ + pop_level_qsave.c +BANKED_SRCS := $(BANK1_SRCS) $(BANK2_SRCS) $(BANK3_SRCS) $(BANK4_SRCS) \ + $(BANK5_SRCS) $(BANK6_SRCS) $(BANK7_SRCS) $(BANK8_SRCS) +BANK_FLAGS := $(foreach f,$(BANK1_SRCS),--bank 1=$(f)) \ + $(foreach f,$(BANK2_SRCS),--bank 2=$(f)) \ + $(foreach f,$(BANK3_SRCS),--bank 3=$(f)) \ + $(foreach f,$(BANK4_SRCS),--bank 4=$(f)) \ + $(foreach f,$(BANK5_SRCS),--bank 5=$(f)) \ + $(foreach f,$(BANK6_SRCS),--bank 6=$(f)) \ + $(foreach f,$(BANK7_SRCS),--bank 7=$(f)) \ + $(foreach f,$(BANK8_SRCS),--bank 8=$(f)) +EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \ + $(BANK_FLAGS) $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте @@ -57,7 +82,7 @@ POS ?= 2 ifneq ($(strip $(ROOM)),) PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS) endif -EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c +EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c pop_qsave_io.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid @@ -127,47 +152,121 @@ SND_DATA := $(foreach n,0 1 2 3 4 5 6 7,$(SND_DIR)/s$(n).bin) EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ $(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA) -include $(PROJ_ROOT)/app.mk - # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), # чтобы корень диска не зарастал десятками .atl. # BG\ — фон (env/wall/fore/pot) # KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации) # GUARD\ — стражи (появятся здесь же) # LEVELS\ — уровни -hdd: $(EXAMPLE).exe - $(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \ - $(EXAMPLE).exe \ - $(foreach f,$(BG_DATA),BG:$(f)) \ - $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ - $(foreach f,$(GUARD_DATA),GUARD:$(f)) \ - $(foreach f,$(SKEL_DATA),SKEL:$(f)) \ - $(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \ - $(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \ - $(foreach f,$(SND_DATA),SND:$(f)) \ - $(foreach f,$(LVL_DATA),LEVELS:$(f)) +HDD_PACK_ARGS := $(EXAMPLE).exe \ + $(foreach f,$(BG_DATA),BG:$(f)) \ + $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ + $(foreach f,$(GUARD_DATA),GUARD:$(f)) \ + $(foreach f,$(SKEL_DATA),SKEL:$(f)) \ + $(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \ + $(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \ + $(foreach f,$(SND_DATA),SND:$(f)) \ + $(foreach f,$(LVL_DATA),LEVELS:$(f)) + +include $(PROJ_ROOT)/app.mk TC := $(PROJ_ROOT)/applications/PoP/toolchain -$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py + +# Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило +# заставляло GNU make при `-B` запускать один и тот же упаковщик отдельно для +# каждого .atl/.bin. Stamp-файл представляет один запуск генератора; все +# созданные им файлы зависят от этого stamp без собственных рецептов. +RESOURCE_STAMP_DIR := .resource-stamps +BG_STAMP := $(RESOURCE_STAMP_DIR)/bg +GUARD_STAMP := $(RESOURCE_STAMP_DIR)/guard +SKEL_STAMP := $(RESOURCE_STAMP_DIR)/skel +VIZIER_STAMP := $(RESOURCE_STAMP_DIR)/vizier +SHADOW_STAMP := $(RESOURCE_STAMP_DIR)/shadow +SOUND_STAMP := $(RESOURCE_STAMP_DIR)/sound +KID_STAMP := $(RESOURCE_STAMP_DIR)/kid +KID_BIN_STAMP := $(RESOURCE_STAMP_DIR)/kid-bin + +$(RESOURCE_STAMP_DIR): + mkdir -p $@ + +$(BG_STAMP): $(TC)/pop_pack_bg.py $(TC)/render_room.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace -$(GUARD_DATA): $(TC)/pop_pack_guard.py + touch $@ +$(BG_DATA): $(BG_STAMP) + @test -f $@ || { $(MAKE) -B $(BG_STAMP); test -f $@; } + +$(GUARD_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_guard.py + touch $@ +$(GUARD_DATA): $(GUARD_STAMP) + @test -f $@ || { $(MAKE) -B $(GUARD_STAMP); test -f $@; } -$(SKEL_DATA): $(TC)/pop_pack_guard.py +$(SKEL_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_guard.py SKEL + touch $@ +$(SKEL_DATA): $(SKEL_STAMP) + @test -f $@ || { $(MAKE) -B $(SKEL_STAMP); test -f $@; } -$(VIZIER_DATA): $(TC)/pop_pack_guard.py +$(VIZIER_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_guard.py VIZIER + touch $@ +$(VIZIER_DATA): $(VIZIER_STAMP) + @test -f $@ || { $(MAKE) -B $(VIZIER_STAMP); test -f $@; } -$(SHADOW_DATA) pop_shadow_atlas.h: $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py +$(SHADOW_STAMP): $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_shadow.py + touch $@ +$(SHADOW_DATA) pop_shadow_atlas.h: $(SHADOW_STAMP) + @test -f $@ || { $(MAKE) -B $(SHADOW_STAMP); test -f $@; } -$(SND_DATA) pop_sound_tbl.h: $(TC)/pop_pack_sound.py +$(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_sound.py + touch $@ +$(SND_DATA) pop_sound_tbl.h: $(SOUND_STAMP) + @test -f $@ || { $(MAKE) -B $(SOUND_STAMP); test -f $@; } -$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py +$(KID_STAMP): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py | $(RESOURCE_STAMP_DIR) cd $(TC) && python3 pop_pack_kid.py -kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py - cd $(TC) && python3 pop_extract_kid_data.py + touch $@ +$(KID_DATA): $(KID_STAMP) + @test -f $@ || { $(MAKE) -B $(KID_STAMP); test -f $@; } -$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_vflip.c pop_vflip.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) +$(KID_BIN_STAMP): $(TC)/pop_extract_kid_data.py | $(RESOURCE_STAMP_DIR) + cd $(TC) && python3 pop_extract_kid_data.py + touch $@ +kid_data.h $(KID_BIN): $(KID_BIN_STAMP) + @test -f $@ || { $(MAKE) -B $(KID_BIN_STAMP); test -f $@; } + +# Явные цели позволяют собирать ресурсы без `make -B`, который по смыслу +# принудительно пересобирает также libc, libbgi и само приложение. +resources-bg: $(BG_DATA) +resources-kid: $(KID_DATA) $(KID_BIN) +resources-actors: $(GUARD_DATA) $(SKEL_DATA) $(VIZIER_DATA) $(SHADOW_DATA) +resources-sound: $(SND_DATA) +resources-levels: $(LVL_DATA) +resources: resources-bg resources-kid resources-actors resources-sound resources-levels +resources-rebuild: + $(MAKE) -B resources + +# Принудительная перелинковка только roomtest, без глобального `-B` и без +# принудительной пересборки библиотек/ресурсов. +relink: resources + $(MAKE) -W Makefile $(EXAMPLE).exe + +# HDD_PACK_ARGS содержит все эти файлы; make обязан подготовить их до запуска +# упаковщика и при чистой сборке, а не полагаться на старое содержимое дерева. +hdd: resources + +# sprinter-cc собирает все модули одним вызовом и depfile пока не выдаёт. +# Банковые .c берём из того же списка, что формирует --bank; любой локальный +# заголовок также обязан перелинковать приложение. wildcard намеренно +# широкий: цена лишней полной сборки ниже риска получить stale-бинарник. +LOCAL_HEADERS := $(wildcard *.h) +$(EXAMPLE).exe: Makefile $(PROJ_ROOT)/app.mk $(BANKED_SRCS) $(LOCAL_HEADERS) \ + $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) + +clean-resources: + rm -rf $(RESOURCE_STAMP_DIR) + +.PHONY: resources resources-rebuild resources-bg resources-kid resources-actors relink \ + resources-sound resources-levels clean-resources diff --git a/applications/PoP/roomtest/_pop_kid.h b/applications/PoP/roomtest/_pop_kid.h index 4fbfcfd..eb88700 100644 --- a/applications/PoP/roomtest/_pop_kid.h +++ b/applications/PoP/roomtest/_pop_kid.h @@ -20,10 +20,7 @@ extern uint8_t kdat_blk; /* EMM-блок страницы kid_data.bin */ extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */ extern uint8_t kdat_ok; /* данные загружены */ -/* ПРАВИЛО ОКНА W3: банковый модуль исполняется из W3 и переключать это окно - * не может (следующая инструкция придёт уже из чужой страницы). Поэтому - * ЧТЕНИЕ kid_data.bin — pop_kid_data_load — осталось в резиденте, а в банк - * уехало только то, что читает через W0 или вообще не трогает окна. - * То же правило и в _pop_level.h. */ +/* Файловый обмен с W3 делает общий резидентный bank_load_file(), поэтому + * оркестратор загрузки kid_data.bin целиком живёт в холодном банке 8. */ #endif /* _POP_KID_H */ diff --git a/applications/PoP/roomtest/_pop_level.h b/applications/PoP/roomtest/_pop_level.h index e527a85..8bfabae 100644 --- a/applications/PoP/roomtest/_pop_level.h +++ b/applications/PoP/roomtest/_pop_level.h @@ -69,13 +69,9 @@ static const uint8_t *room_bg_ptr(uint8_t r) return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES; } -/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3, поэтому сам он - * менять это окно НЕ МОЖЕТ: после sprinter_page_w3(другая_страница) - * следующая инструкция читается уже из чужой страницы. Всё чтение файлов - * (оно грузит данные по 0xC000) обязано жить в резиденте — отсюда - * pop_level_read_file здесь, а не в pop_level_cold.c. Через W0 - * (gfx_w0_map) банкам работать можно: это другое окно. */ -int pop_level_read_file(const char *path); +/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3 и сам менять окно + * не может. Файлы в EMM читает резидентный bank_load_file(), после его + * возврата холодный код продолжает подготовку страницы через W0. */ /* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */ void pop_gstate_init(void) __banked; diff --git a/applications/PoP/roomtest/_pop_sfx.h b/applications/PoP/roomtest/_pop_sfx.h index 3e9ccfe..b765214 100644 --- a/applications/PoP/roomtest/_pop_sfx.h +++ b/applications/PoP/roomtest/_pop_sfx.h @@ -19,8 +19,4 @@ extern uint8_t pop_snd_ok; * cbl_open() холодной половиной. */ int pop_sfx_fill(uint16_t n); -/* Прочитать файл целиком в ФИЗИЧЕСКУЮ страницу (через W3). Резидентный: - * банковый код, переключив W3, вырезал бы сам себя. */ -int pop_page_read(const char *path, uint8_t phys, uint16_t want); - #endif diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index c8f4c73..9ce9891 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -265,6 +265,12 @@ void pop_check_mouse(void) __banked static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */ static uint8_t demo_index; /* текущая запись */ +void pop_guard_ai_qsave(pop_qs_io_t *io) __banked +{ + pop_qs_u8(io, &demo_time); + pop_qs_u8(io, &demo_index); +} + #define SHADOW_SKILL 3 #define SHADOW_HP 4 #define SEQ_2_STAND 2 diff --git a/applications/PoP/roomtest/new bugs b/applications/PoP/roomtest/new bugs new file mode 100644 index 0000000..092e7f4 --- /dev/null +++ b/applications/PoP/roomtest/new bugs @@ -0,0 +1,10 @@ +Уровень 4, комната 22 - страж идет навстречу Киду и попадает сам в челюсти - должен быть инстинк самосохранения + +Перед решеткой Кид делает лишний мелкий шаг. + +Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения) + +Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0) + +Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень) +но двери которые она должна открыть остаются закрытыми diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index a1c864b..c35bb14 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -13,6 +13,7 @@ #include #include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */ +#include "pop_qsave.h" /* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- * * Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на @@ -178,6 +179,7 @@ void pop_loose_mob_spawn(int row, int col) __banked; void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked; void pop_loose_mob_tick(void) __banked; void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */ +void pop_loose_mob_qsave(pop_qs_io_t *io) __banked; /* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в * pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index c4fa54f..dd86519 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -21,6 +21,7 @@ #include "kid_data.h" /* kframe */ #include "pop_char.h" #include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */ +#include "pop_qsave.h" /* seqids (types.h:1052+) — последовательности боя и стойки. */ #define SEQ_55_DRAW_SWORD 55 @@ -303,6 +304,7 @@ void pop_check_sword_hurt(void) __banked; /* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2, * читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */ extern kframe pop_gframe; +void pop_guard_ai_qsave(pop_qs_io_t *io) __banked; void pop_guard_load_frame(void); /* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он diff --git a/applications/PoP/roomtest/pop_kboot.c b/applications/PoP/roomtest/pop_kboot.c index e878cd7..4646145 100644 --- a/applications/PoP/roomtest/pop_kboot.c +++ b/applications/PoP/roomtest/pop_kboot.c @@ -17,11 +17,6 @@ #include "pop_vflip.h" #include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */ -/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с - * замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на - * _gfx_w0_isr (восстановит окно). */ -extern void _gfx_w0_isr(void); - #define KID_MAXPAGES 28 int pop_kid_load(uint8_t npages) __banked @@ -50,9 +45,21 @@ void pop_kid_free(void) __banked kid_npages = 0; } -/* ЗАГРУЗКА kid_data.bin (pop_kid_data_load) осталась в РЕЗИДЕНТЕ: она - * маппит страницу в W3 на время чтения, а этот модуль сам в W3 — см. - * то же правило в _pop_kid.h. */ +int pop_kid_data_load(const char *path) __banked +{ + uint8_t blk, page; + int n; + blk=mem_alloc_pages(1); + if (!blk) return -1; + page=mem_get_page(blk,0); + n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF); + if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { + mem_free_block(blk); return -1; + } + gfx_w0_page_prepare(page); + kdat_blk=blk; kdat_page=page; kdat_ok=1; + return 0; +} void pop_kid_data_free(void) __banked { diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index c0a6ea1..73ba773 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -61,8 +61,6 @@ static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT; * Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame, * и коллизия/отрисовка работают с ним, а не с таблицей. * Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */ -extern void _gfx_w0_isr(void); - /* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН * раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает * НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом @@ -449,35 +447,3 @@ void kid_tick(void) * W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM- * странице, которая маппится в W0 РОВНО на время play_seq (раз за тик). * Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */ -/* ЖИВЁТ В РЕЗИДЕНТЕ, хотя зовётся раз за игру: маппит страницу в W3 на - * время чтения, а банковый код сам лежит в W3 (см. _pop_kid.h). */ -int pop_kid_data_load(const char *path) -{ - uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */ - uint8_t saved_w3, blk; - uint16_t stub; - int fd, n; - - fd = open(path, O_RDONLY); - if (fd < 0) return -1; - blk = mem_alloc_pages(1); - if (!blk) { close(fd); return -1; } - kdat_blk = blk; - kdat_page = mem_get_page(blk, 0); - - saved_w3 = _io_page_w3; - sprinter_page_w3(kdat_page); - n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF); - if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { - stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */ - pg[0x38] = 0xC3; - pg[0x39] = (uint8_t)(stub & 0xFF); - pg[0x3A] = (uint8_t)(stub >> 8); - pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */ - } - sprinter_page_w3(saved_w3); - close(fd); - if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; } - kdat_ok = 1; - return 0; -} diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 9ba6fba..5980e37 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -35,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked; * (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО * первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт). * Страница маппится в W0 внутри play_seq, раз за тик. */ -int pop_kid_data_load(const char *path); +int pop_kid_data_load(const char *path) __banked; /* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF * (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard). diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index e320766..4edd4f4 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -17,10 +17,6 @@ #include #include /* gfx_w0_map / gfx_w0_unmap */ -/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с - * замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на - * _gfx_w0_isr (восстановит окно). */ -extern void _gfx_w0_isr(void); #include "pop_level.h" #include "_pop_level.h" @@ -138,53 +134,3 @@ uint8_t pop_guard_state_x(uint8_t room) if (room < 1 || room > 24) return 0; return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X]; } - -/* Чтение файла уровня в свежую EMM-страницу. ЖИВЁТ В РЕЗИДЕНТЕ, хотя - * зовётся раз на уровень: функция маппит страницу в W3 на время read(), а - * весь холодный код лежит В ЭТОМ ЖЕ окне — переключив его оттуда, банк - * вырезает из-под себя собственные инструкции (первый же вызов улетал в - * halt по мусору, поймано в MAME 2026-08-12). Правило — в _pop_level.h. - * Наружу не публичная в pop_level.h: уровень грузят по НОМЕРУ. */ -int pop_level_read_file(const char *path) -{ - uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */ - uint8_t saved_w3, blk; - uint16_t stub; - int fd, n; - - fd = open(path, O_RDONLY); - if (fd < 0) return -1; - - blk = mem_alloc_pages(1); - if (!blk) { close(fd); return -1; } - pop_lvl_blk = blk; - pop_lvl_page = mem_get_page(blk, 0); - - saved_w3 = _io_page_w3; - sprinter_page_w3(pop_lvl_page); - /* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */ - n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF); - if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */ - uint16_t i; - stub = (uint16_t)&_gfx_w0_isr; - pg[0x38] = 0xC3; /* JP _gfx_w0_isr */ - pg[0x39] = (uint8_t)(stub & 0xFF); - pg[0x3A] = (uint8_t)(stub >> 8); - pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */ - /* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */ - for (i = 0; i < 256; i++) { - pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i]; - pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i]; - } - /* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */ - for (i = 0; i < 24u * ROOM_TILES; i++) - pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i]; - } - sprinter_page_w3(saved_w3); - close(fd); - - if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; } - pop_lvl_ok = 1; - pop_gstate_init(); /* живая копия состояния стражей — из уровня */ - return 0; -} diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index a58faa7..d2f1bf2 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -18,6 +18,8 @@ #ifndef POP_LEVEL_H #define POP_LEVEL_H +#include "pop_qsave.h" + #include /* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка @@ -32,6 +34,7 @@ void pop_level_free(void) __banked; * level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают * потабличные функции ниже, стражи (HP/тип) и стартовая логика. */ extern uint8_t pop_current_level; +void pop_level_qsave(pop_qs_io_t *io); /* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять * `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить diff --git a/applications/PoP/roomtest/pop_level_cold.c b/applications/PoP/roomtest/pop_level_cold.c index 5a2794b..30f145e 100644 --- a/applications/PoP/roomtest/pop_level_cold.c +++ b/applications/PoP/roomtest/pop_level_cold.c @@ -18,16 +18,8 @@ #include "_pop_level.h" #include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */ -/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с - * замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на - * _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но - * стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */ -extern void _gfx_w0_isr(void); - -/* ЧТЕНИЕ ФАЙЛА уровня живёт в РЕЗИДЕНТЕ (pop_level.c, pop_level_read_file): - * оно маппит страницу в W3 на время read(), а этот модуль сам лежит в W3 — - * переключив окно, он вырезал бы из-под себя собственный код (проверено: - * первый же вызов улетал в halt по мусору). Правило записано в _pop_level.h. */ +/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file(); + * остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */ void pop_level_free(void) __banked { @@ -58,6 +50,31 @@ int8_t pop_level_guard_type(uint8_t n) __banked { return n < 16 ? tbl_guard_typ uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; } uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; } +static int level_read_file(const char *path) +{ + uint8_t blk, page; + uint16_t i; + int n; + blk=mem_alloc_pages(1); + if (!blk) return -1; + page=mem_get_page(blk,0); + n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF); + if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; } + gfx_w0_page_prepare(page); + gfx_w0_map(page); + for (i=0; i<256; i++) { + pop_dl1[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKLOC+i]; + pop_dl2[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKMAP+i]; + } + for (i=0; i<24u*ROOM_TILES; i++) + ((uint8_t *)LVL_PRISTINE_OFF)[i]= + ((const uint8_t *)(LVL_DATA_OFF+BP_FG))[i]; + gfx_w0_unmap(); + pop_lvl_blk=blk; pop_lvl_page=page; pop_lvl_ok=1; + pop_gstate_init(); + return 0; +} + int pop_level_load_num(uint8_t n) __banked { /* Имена патчим на месте, а не через sprintf: printf-семейство тянет в @@ -71,10 +88,10 @@ int pop_level_load_num(uint8_t n) __banked pri[13] = alt[9] = (char)('0' + n % 10); /* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой: - * pop_level_read_file сам аллоцирует страницу, и если файла нет — остаёмся + * level_read_file сам аллоцирует страницу, и если файла нет — остаёмся * на текущем уровне вместо падения в пустой уровень. */ pop_lvl_ok = 0; /* чтобы level_load_path не мешался */ - if (pop_level_read_file(pri) != 0 && pop_level_read_file(alt) != 0) { + if (level_read_file(pri) != 0 && level_read_file(alt) != 0) { pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok; return -1; } diff --git a/applications/PoP/roomtest/pop_level_qsave.c b/applications/PoP/roomtest/pop_level_qsave.c new file mode 100644 index 0000000..02c5e6b --- /dev/null +++ b/applications/PoP/roomtest/pop_level_qsave.c @@ -0,0 +1,38 @@ +/* + * Сериализация уровня для QuickSave (банк 8). + * + * Код выполняется только по F6/F9 и вынесен из горячего резидентного + * pop_level.c. Данные уровня и снимка используют одно окно W0, поэтому + * foretable переносится небольшими блоками через стековый буфер W2. + */ +#include +#include "pop_level.h" +#include "_pop_level.h" + +void pop_level_qsave(pop_qs_io_t *io) +{ + uint8_t tmp[30]; + uint16_t off; + pop_qs_bytes(io, pop_dl1, sizeof(pop_dl1)); + pop_qs_bytes(io, pop_dl2, sizeof(pop_dl2)); + pop_qs_bytes(io, pop_gstate, sizeof(pop_gstate)); + if (!pop_lvl_ok) { io->failed = 1; return; } + /* W0 одновременно не может держать страницу уровня и страницу снимка. */ + for (off = 0; off < 24u * ROOM_TILES; off += sizeof(tmp)) { + uint8_t i; + if (!io->reading || io->apply) { + gfx_w0_map(pop_lvl_page); + if (!io->reading) + for (i=0; ireading && io->apply && !io->failed) { + gfx_w0_map(pop_lvl_page); + for (i=0; ireading && io->apply) pop_coll_invalidate(); +} diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 3b03845..e34c3d9 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -13,6 +13,7 @@ #include #include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */ +#include "pop_qsave.h" /* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10). * col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив @@ -197,6 +198,7 @@ void pop_check_knock(void) __banked; * links.down — сменить комнату (pop_room_load+перерисовка) и продолжить; * иначе — выпал из уровня (респавн). Снять флаг после обработки. */ extern uint8_t pop_fell_out; +void pop_map_qsave(pop_qs_io_t *io) __banked; /* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + * determine_col + check_action (триггер падения / do_fall / приземление). diff --git a/applications/PoP/roomtest/pop_qsave.c b/applications/PoP/roomtest/pop_qsave.c new file mode 100644 index 0000000..41f6f0f --- /dev/null +++ b/applications/PoP/roomtest/pop_qsave.c @@ -0,0 +1,207 @@ +/* + * pop_qsave.c — файловая транзакция и схема игрового снимка. + * Живёт в холодном банке: в обычном кадре исполняется только проверка + * резидентного флага запроса, вся тяжёлая работа бывает по F6/F9. + */ +#include +#include +#include /* rename */ +#include +#include +#include "pop_qsave.h" +#include "pop_kid.h" +#include "pop_guard.h" +#include "pop_map.h" +#include "pop_level.h" +#include "pop_trob.h" +#include "pop_bg.h" +#include "pop_state.h" +#include "pop_tile.h" +#include "pop_sfx.h" +#include "pop_pace.h" +#include "roomtest_cold.h" + +#define QS_VERSION 3 +#define QS_SAVE 1 +#define QS_LOAD 2 +#define QS_LIMIT 4096 +#define QS_BASE 0x100 + +extern uint8_t pop_qsave_request; + +static void qs_char(pop_qs_io_t *io, pop_char_t *c) +{ + pop_qs_u8(io, &c->frame); pop_qs_u8(io, &c->x); pop_qs_u8(io, &c->y); + pop_qs_i8(io, &c->direction); pop_qs_i8(io, &c->curr_col); + pop_qs_i8(io, &c->curr_row); pop_qs_u8(io, &c->action); + pop_qs_i8(io, &c->fall_x); pop_qs_i8(io, &c->fall_y); + pop_qs_u8(io, &c->room); pop_qs_u8(io, &c->repeat); + pop_qs_u8(io, &c->charid); pop_qs_u8(io, &c->sword); + pop_qs_i8(io, &c->alive); pop_qs_u16(io, &c->curr_seq); +} + +static void qs_payload(pop_qs_io_t *io) +{ + uint8_t speed=pop_speed_mode, immortal=pop_immortal, sound=pop_snd_want; + + /* Пользовательские параметры входят в снимок, а не остаются свойством + * текущего запуска. Читаем сначала во временные байты: повреждённые + * значения не должны частично применить остальное игровое состояние. */ + pop_qs_u8(io, &speed); pop_qs_u8(io, &immortal); pop_qs_u8(io, &sound); + if (io->reading && io->apply) { + if (speed >= POP_SPEED_MODES || immortal > 2 || sound > 1) { + io->failed=1; return; + } + pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound; + } + + qs_char(io, &Kid); qs_char(io, &Guard); + pop_qs_u8(io, &cur_room); pop_qs_u8(io, &kid_room); + pop_qs_u8(io, &pop_checkpoint); pop_qs_u8(io, &pop_next_level); + pop_qs_i8(io, &knock); + + pop_qs_u8(io, &hitp_curr); pop_qs_u8(io, &hitp_max); + pop_qs_u8(io, &hitp_beg_lev); pop_qs_i8(io, &hitp_delta); + pop_qs_u8(io, &guardhp_curr); pop_qs_u8(io, &guardhp_max); + pop_qs_i8(io, &guardhp_delta); pop_qs_u8(io, &guard_skill); + pop_qs_i8(io, &guard_refrac); pop_qs_i8(io, &justblocked); + pop_qs_i8(io, &kid_sword_strike); pop_qs_i8(io, &offguard); + pop_qs_i8(io, &holding_sword); pop_qs_u8(io, &pop_guard_hurt); + pop_qs_i8(io, &can_guard_see_kid); pop_qs_i8(io, &is_guard_notice); + pop_qs_i8(io, &pop_united_shadow); pop_qs_u8(io, &pop_shadow_init); + pop_qs_i8(io, &pop_guard_notice_timer); + pop_qs_u16(io, &pop_fight_seed.lo); pop_qs_u16(io, &pop_fight_seed.hi); + + pop_qs_u8(io, &pop_kid_dead); pop_qs_u8(io, &pop_kid_hurt); + pop_qs_u8(io, &pop_have_sword); pop_qs_u8(io, &pop_item_taken); + pop_qs_u8(io, &pop_flash_time); pop_qs_u8(io, &pop_flash_color); + pop_qs_u8(io, &pop_feather); pop_qs_u8(io, &pop_upside); + pop_qs_u8(io, &pop_upside_want); pop_qs_u8(io, &pop_upside_dirty); + pop_qs_u8(io, &pop_leave_dir); pop_qs_u8(io, &pop_leave_timer); + pop_qs_u8(io, &pop_loose_fell); pop_qs_u8(io, &pop_ceil_fell); + pop_qs_u8(io, &pop_debris_at); pop_qs_u8(io, &pop_fell_out); + pop_qs_bytes(io, pop_loose_modif, sizeof(pop_loose_modif)); + pop_qs_bytes(io, pop_ceil_modif, sizeof(pop_ceil_modif)); + + pop_qs_u8(io, &pop_loose_landed); pop_qs_u8(io, &pop_loose_exit); + pop_qs_u8(io, &pop_loose_exit_room); pop_qs_u8(io, &pop_droppedout); + pop_qs_u16(io, &pop_leveldoor_open); pop_qs_i8(io, &pop_jumped_mirror); + pop_qs_u8(io, &pop_seamless); pop_qs_u8(io, &pop_nav_hold); + + pop_level_qsave(io); + pop_trob_qsave(io); + pop_loose_mob_qsave(io); + pop_map_qsave(io); + pop_guard_ai_qsave(io); +} + +static void qs_init(pop_qs_io_t *io, uint8_t page, uint16_t limit, + uint8_t reading, uint8_t apply) +{ + io->page=page; io->pos=QS_BASE; io->limit=(uint16_t)(QS_BASE+limit); + io->checksum=0; + io->count=0; io->reading=reading; io->apply=apply; io->failed=0; +} + +static uint8_t qs_header_read(pop_qs_io_t *io, uint8_t *level) +{ + uint8_t h[7]; + if (!pop_qs_raw_get(io,h,sizeof(h))) return 0; + if (h[0]!='P' || h[1]!='O' || h[2]!='P' || h[3]!='Q' || + h[4] != QS_VERSION || h[5] < 1 || h[5] > 15 || h[6] != 0) return 0; + *level = h[5]; return 1; +} + +static uint8_t qs_validate_page(uint8_t page, uint16_t size, + uint8_t *level, uint16_t *payload_size) +{ + pop_qs_io_t io; + uint8_t tail[4]; + uint16_t n, sum; + if (size < 11) return 0; + qs_init(&io,page,size,1,0); + if (!qs_header_read(&io,level)) return 0; + io.pos=(uint16_t)(QS_BASE+size-4); + if (!pop_qs_raw_get(&io,tail,4)) return 0; + n = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8)); + sum = (uint16_t)(tail[2] | ((uint16_t)tail[3] << 8)); + if (n != (uint16_t)(size-11) || + sum != pop_qs_checksum_page(page,QS_BASE+7,n)) return 0; + *payload_size=n; return 1; +} + +static uint8_t qs_apply(uint8_t page, uint16_t size, uint16_t payload_size, + uint8_t *level) +{ + pop_qs_io_t io; + qs_init(&io,page,size,1,1); + if (!qs_header_read(&io,level)) return 0; + qs_payload(&io); + return (uint8_t)(!io.failed && io.count == payload_size && + io.pos == (uint16_t)(QS_BASE+7+payload_size)); +} + +static int8_t qs_save(uint8_t page) +{ + pop_qs_io_t io; + uint8_t tail[4]; + const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0}; + uint16_t size; + qs_init(&io,page,QS_LIMIT,0,0); + pop_qs_raw_put(&io,h,sizeof(h)); + qs_payload(&io); + tail[0]=(uint8_t)io.count; tail[1]=(uint8_t)(io.count >> 8); + io.checksum=pop_qs_checksum_page(page,QS_BASE+7,io.count); + tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8); + pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE); + if (io.failed) return -1; + unlink("POP.NEW"); + if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) { + unlink("POP.NEW"); return -1; + } + unlink("POP.BAK"); + (void)rename("POP.SAV", "POP.BAK"); + if (rename("POP.NEW", "POP.SAV") != 0) { + (void)rename("POP.BAK", "POP.SAV"); return -1; + } + return 1; +} + +static int8_t qs_load_one(const char *name, uint8_t page) +{ + uint8_t level; + uint16_t payload_size; + int size=bank_load_file(page,QS_BASE,name,QS_LIMIT); + if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level, + &payload_size)) return -1; + if (level != pop_current_level) { + pop_next_level = level; + if (pop_level_switch() != 0 || pop_current_level != level) return -1; + } + if (!qs_apply(page,(uint16_t)size,payload_size,&level)) return -1; + Char = Kid; Opp = Guard; + pop_qsave_restore_room(cur_room); + return 1; +} + +int8_t pop_qsave_process(void) __banked +{ + uint8_t request = pop_qsave_request; + uint8_t blk,page; + int8_t rc=-1; + pop_qsave_request = 0; + if (!request) return 0; + blk=mem_alloc_pages(1); + if (!blk) return -1; + page=mem_get_page(blk,0); + gfx_w0_page_prepare(page); + pop_sfx_pause(); + if (request == QS_SAVE) rc = qs_save(page); + else { + rc = qs_load_one("POP.SAV",page); + if (rc < 0) rc = qs_load_one("POP.BAK",page); + } + pop_sfx_start(); + mem_free_block(blk); + return rc; +} diff --git a/applications/PoP/roomtest/pop_qsave.h b/applications/PoP/roomtest/pop_qsave.h new file mode 100644 index 0000000..37ba1f1 --- /dev/null +++ b/applications/PoP/roomtest/pop_qsave.h @@ -0,0 +1,40 @@ +/* + * pop_qsave.h — потоковый QuickSave/QuickLoad одного игрового слота. + * + * На диске используются POP.NEW, POP.SAV и POP.BAK. Формат не содержит + * указателей, файловых дескрипторов и графических кэшей; многобайтные поля + * записываются явно little-endian. Внутренние функции pop_qs_* нужны + * модулям-владельцам закрытого состояния. + */ +#ifndef POP_QSAVE_H +#define POP_QSAVE_H + +#include + +typedef struct { + uint8_t page; + uint16_t pos; + uint16_t limit; + uint16_t checksum; + uint16_t count; + uint8_t reading; + uint8_t apply; + uint8_t failed; +} pop_qs_io_t; + +void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size); +void pop_qs_u8(pop_qs_io_t *io, uint8_t *value); +void pop_qs_i8(pop_qs_io_t *io, int8_t *value); +void pop_qs_u16(pop_qs_io_t *io, uint16_t *value); +void pop_qs_i16(pop_qs_io_t *io, int *value); +void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size); +uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size); +uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size); + +/* Вызывается в безопасной границе кадра. Возвращает: 0 — запроса нет, + * 1 — сохранено/загружено, -1 — ошибка. */ +int8_t pop_qsave_process(void) __banked; +void pop_qsave_request_save(void); +void pop_qsave_request_load(void); + +#endif diff --git a/applications/PoP/roomtest/pop_qsave_coldio.c b/applications/PoP/roomtest/pop_qsave_coldio.c new file mode 100644 index 0000000..511db08 --- /dev/null +++ b/applications/PoP/roomtest/pop_qsave_coldio.c @@ -0,0 +1,38 @@ +/* + * Холодная часть ввода-вывода QuickSave (банк 8). + * + * Эти функции вызываются только банковым оркестратором pop_qsave.c, поэтому + * им не место в дефицитном резидентном _CODE. Здесь только доступ через W0: + * переключающие W3 DSS-операции обязаны оставаться в pop_qsave_io.c. + */ +#include "pop_qsave.h" +#include + +void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size) +{ + uint8_t *q, i; + if (io->pos + size > io->limit) { io->failed=1; return; } + gfx_w0_map(io->page); q=(uint8_t *)io->pos; + for (i=0; ipos += size; +} + +uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size) +{ + uint8_t *q, i; + if (io->pos + size > io->limit) return 0; + gfx_w0_map(io->page); q=(uint8_t *)io->pos; + for (i=0; ipos += size; return 1; +} + +/* XOR8 достаточно для обнаружения случайной порчи транзакционного файла; + * размер payload проверяется отдельно, старший байт содержит инверсию XOR. */ +uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size) +{ + uint8_t *p, sum=0; + gfx_w0_map(page); p=(uint8_t *)pos; + while(size--) sum ^= *p++; + gfx_w0_unmap(); + return (uint16_t)(sum | ((uint16_t)(uint8_t)~sum << 8)); +} diff --git a/applications/PoP/roomtest/pop_qsave_io.c b/applications/PoP/roomtest/pop_qsave_io.c new file mode 100644 index 0000000..86b7310 --- /dev/null +++ b/applications/PoP/roomtest/pop_qsave_io.c @@ -0,0 +1,33 @@ +/* Резидентная часть QuickSave: часто вызываемые межбанковые примитивы + * потока и флаг запроса. Файловый обмен с W3 теперь общий в libc. */ +#include "pop_qsave.h" +#include +#include + +uint8_t pop_qsave_request; +void pop_qsave_request_save(void) { pop_qsave_request=1; } +void pop_qsave_request_load(void) { pop_qsave_request=2; } + +void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size) +{ + uint8_t *p=(uint8_t *)data, *q; + if (io->failed || io->pos + size > io->limit) { io->failed=1; return; } + gfx_w0_map(io->page); q=(uint8_t *)io->pos; + if (!io->reading) memcpy(q,p,size); + else if (io->apply) memcpy(p,q,size); + gfx_w0_unmap(); io->pos += size; io->count += size; +} +void pop_qs_u8(pop_qs_io_t *io, uint8_t *v) { pop_qs_bytes(io,v,1); } +void pop_qs_i8(pop_qs_io_t *io, int8_t *v) { pop_qs_bytes(io,v,1); } +void pop_qs_u16(pop_qs_io_t *io, uint16_t *value) +{ + uint8_t b[2]; b[0]=(uint8_t)*value; b[1]=(uint8_t)(*value >> 8); + pop_qs_bytes(io,b,2); + if (io->reading && io->apply && !io->failed) + *value=(uint16_t)(b[0] | ((uint16_t)b[1] << 8)); +} +void pop_qs_i16(pop_qs_io_t *io, int *value) +{ + uint16_t v=(uint16_t)*value; pop_qs_u16(io,&v); + if (io->reading && io->apply && !io->failed) *value=(int)v; +} diff --git a/applications/PoP/roomtest/pop_room.c b/applications/PoP/roomtest/pop_room.c index 0616e6c..eb29305 100644 --- a/applications/PoP/roomtest/pop_room.c +++ b/applications/PoP/roomtest/pop_room.c @@ -1350,6 +1350,28 @@ void pop_loose_mob_reset(void) __banked * мапит и размапливает W0. Инвалидируется сменой комнаты. */ static uint8_t mob_link_room, mob_link_up, mob_link_dn; +void pop_loose_mob_qsave(pop_qs_io_t *io) __banked +{ + uint8_t i, live = 0; + for (i = 0; i < MOB_MAX; i++) { + mob_t *m = &mobs[i]; + pop_qs_u8(io, &m->active); + pop_qs_i16(io, &m->x); pop_qs_i16(io, &m->y); + pop_qs_i8(io, &m->speed); pop_qs_i8(io, &m->row); + pop_qs_i8(io, &m->col); pop_qs_u8(io, &m->room); + if (io->reading && io->apply) { + m->clean = m->defer = 0; + m->draw_y = m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; + if (m->active) live++; + } + } + if (io->reading && io->apply) { + mobs_live = live; + pop_mob_busy = (uint8_t)(live != 0); + mob_link_room = 0; + } +} + static void mob_links(void) { if (mob_link_room == pop_t_room) return; @@ -1955,4 +1977,4 @@ void pop_room_draw(uint8_t room_num, pop_t_clip_top = 0; pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); -} \ No newline at end of file +} diff --git a/applications/PoP/roomtest/pop_sfx.c b/applications/PoP/roomtest/pop_sfx.c index a8ddfd0..e0ebbb7 100644 --- a/applications/PoP/roomtest/pop_sfx.c +++ b/applications/PoP/roomtest/pop_sfx.c @@ -9,9 +9,7 @@ * ставит базовую страницу W1, значит хендлер обязан быть в базе; * - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг * Кида, физика, боёвка) — трамплин там был бы чистой потерей; - * - pop_page_read маппит W3 на время чтения, а банковый код сам живёт - * в W3 и вырезал бы себя из-под ног (та же причина, по которой - * резидентен pop_level_read_file, см. _pop_level.h). + * Файловое чтение страниц выполняет общий резидентный bank_load_file(). * Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c. */ @@ -198,21 +196,3 @@ void pop_sfx_stop(void) sfx_left = 0; IRQ_ENABLE(); } - -/* ---- чтение файла в ФИЗИЧЕСКУЮ страницу ------------------------------ * - * Резидентно по причине из шапки: маппим W3, читаем, возвращаем окно. - * Ровно то же делает atlas_load, поэтому вызов из банка безопасен. - * 0 — прочитано ожидаемое число байт, -1 — нет. */ -int pop_page_read(const char *path, uint8_t phys, uint16_t want) -{ - uint8_t saved_w3; - int fd, n; - fd = open(path, O_RDONLY); - if (fd < 0) return -1; - saved_w3 = _io_page_w3; - sprinter_page_w3(phys); - n = read(fd, (uint8_t *)0xC000, want); - _io_page_w3 = saved_w3; - close(fd); - return (n == (int)want) ? 0 : -1; -} diff --git a/applications/PoP/roomtest/pop_sfx_cold.c b/applications/PoP/roomtest/pop_sfx_cold.c index 8420bf5..1ef10ef 100644 --- a/applications/PoP/roomtest/pop_sfx_cold.c +++ b/applications/PoP/roomtest/pop_sfx_cold.c @@ -26,7 +26,7 @@ int pop_sfx_init(void) __banked for (i = 0; i < POP_SND_PAGES; i++) { pop_snd_page[i] = mem_get_page(blk, i); if (!pop_snd_page[i] || - pop_page_read(pop_snd_files[i], pop_snd_page[i], 16384) != 0) { + bank_load_file(pop_snd_page[i],0,pop_snd_files[i],16384) != 16384) { mem_free_block(blk); return -1; } diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 48dc19d..948a2d2 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -651,13 +651,34 @@ uint8_t pop_trob_room_active(uint8_t room) __banked * анимацию или нет). */ #define TORCH_ANIM_DIV 1 -#if TORCH_ANIM_DIV > 1 static uint8_t torch_tick; +#if TORCH_ANIM_DIV > 1 #define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0) #else #define TORCH_ANIM_NOW 1 #endif +void pop_trob_qsave(pop_qs_io_t *io) __banked +{ + uint8_t i; + pop_qs_bytes(io, room_modif, sizeof(room_modif)); + pop_qs_bytes(io, room_seen, sizeof(room_seen)); + pop_qs_u8(io, &trobs_count); + for (i = 0; i < TROBS_MAX; i++) { + pop_qs_u8(io, &trobs[i].room); + pop_qs_u8(io, &trobs[i].tilepos); + pop_qs_i8(io, &trobs[i].type); + } + pop_qs_u16(io, &trob_seed.lo); + pop_qs_u16(io, &trob_seed.hi); + pop_qs_u8(io, &torch_tick); + if (io->reading && io->apply) { + if (trobs_count > TROBS_MAX) { io->failed = 1; return; } + for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0; + pop_redraw_reset(); + } +} + void pop_process_trobs(uint8_t cur_room) __banked { uint8_t i, w = 0; diff --git a/applications/PoP/roomtest/pop_trob.h b/applications/PoP/roomtest/pop_trob.h index 9a67028..eb7d998 100644 --- a/applications/PoP/roomtest/pop_trob.h +++ b/applications/PoP/roomtest/pop_trob.h @@ -18,6 +18,8 @@ #ifndef POP_TROB_H #define POP_TROB_H +#include "pop_qsave.h" + #include #define POP_NROOMS 24 @@ -26,6 +28,7 @@ /* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и * очистить список trob. */ void pop_trob_reset(void) __banked; +void pop_trob_qsave(pop_qs_io_t *io) __banked; /* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт. * При ПЕРВОМ обращении лениво инициализируется из backtable уровня diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 1891940..522fa87 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -26,6 +26,7 @@ #include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */ #include "pop_pace.h" /* фиксированный логический кадр по лучу */ #include "roomtest_cold.h" /* холодная половина цикла — в банке */ +#include "pop_qsave.h" __sfr __at (0xFE) io_border; @@ -62,8 +63,8 @@ __sfr __at (0xFE) io_border; #define PROF(n) ((void)0) #endif -/* Сколько банков грузит crt0_banked (читает его стартовый код). - * 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход, +/* Раскладка банков; их число crt0_banked получает автоматически из + * --bank флагов: 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход, * оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw * (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления), * 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина @@ -72,14 +73,7 @@ __sfr __at (0xFE) io_border; * отладочная навигация — см. roomtest_cold.h). * Отрисовка персонажей и холодная половина фона вынесены из банка 2 * отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть - * куда — тайлы поздних уровней, чомперы, зеркало. - * - * ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть — это не ошибка - * сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и - * останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку - * runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра - * доходила до конца загрузки ресурсов и вставала намертво в DSS. */ -const uint8_t n_banks = 8; + * куда — тайлы поздних уровней, чомперы, зеркало. */ /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats; @@ -115,6 +109,8 @@ static void kbd_idle(void) #define SEQ_STAND 2 #define ROOM_1 1 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ +#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */ +#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */ #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ @@ -250,6 +246,8 @@ int main(void) * железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */ pop_pace_arm(); + { + uint8_t qs_save_prev = 0, qs_load_prev = 0; while (!kbd_raw_down(KBD_ESC)) { /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один * переключ). Выключение возвращает показ на страницу 0. */ @@ -286,6 +284,18 @@ int main(void) /* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места * в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */ pop_cheat_tick(); + /* F6/F9 только ставят запрос; файловая операция выполняется здесь, + * на границе кадров — вне окон Char и до heal/tick/draw. */ + { + uint8_t s = kbd_raw_down(KBD_QUICKSAVE); + uint8_t l = kbd_raw_down(KBD_QUICKLOAD); + int8_t qrc; + if (s && !qs_save_prev) pop_qsave_request_save(); + if (l && !qs_load_prev) pop_qsave_request_load(); + qs_save_prev = s; qs_load_prev = l; + qrc = pop_qsave_process(); + if (qrc > 0) dead_frames = 0; + } if (frozen) { PROF(0); /* чёрный: ждём vsync */ /* Заморозка — тоже полноценный логический кадр: иначе якорь @@ -733,6 +743,7 @@ int main(void) front = back; } } + } pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */ return 0; diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index 4777264..2abfec1 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -602,6 +602,69 @@ void enter_room_side(uint8_t room, uint8_t side) __banked * Страж такой смены не «видит» — он остаётся в своей комнате. */ void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); } +void pop_qsave_restore_room(uint8_t room) __banked +{ + uint8_t i, pg, *room_mod; + gfx_rect_t full; + uint8_t lft, rgt, up; + + pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, + rcol_fg, rcol_bg, below_fg); + pop_map_set(room_fg); + lft = pop_room_link(room, 0); rgt = pop_room_link(room, 1); + up = pop_room_link(room, 2); + pop_map_set_edges(lft, rgt, up, pop_room_link(room, 3)); + pop_room_set_below(below_fg); + if (up) { + uint8_t *um = pop_trob_modif(up); + pop_level_access_begin(); + for (i = 0; i < 10; i++) + above_fg[i] = pop_level_tile_raw(up, (uint8_t)(20 + i)); + pop_level_access_end(); + for (i = 0; i < 10; i++) above_mod[i] = um[20 + i]; + pop_room_set_above(above_fg, above_mod); + pop_map_set_above(above_fg); + } else { + pop_room_set_above(0, 0); pop_map_set_above(0); + } + pop_map_set_room(room); + pop_loose_reset(); + pop_trob_room_changed(); + pop_bake_forget(); + pop_loose_mob_room_changed(); + room_mod = pop_trob_modif(room); + if (lft) { + uint8_t *m = pop_trob_modif(lft); + for (i = 0; i < 3; i++) { + lcol_bg[i] = m[9 + i * 10]; + seam_sig[i] = seam_row_sig(m, lcol_fg, i); + } + } + if (rgt) { + uint8_t *m = pop_trob_modif(rgt); + rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; + } + seam_redraw = seam_rows = 0; + pg = (uint8_t)(gfx_get_visible_page() ^ 1); + gfx_set_draw_page(pg); + setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255); + pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg); + gfx_set_visible_page(pg); + gfx_set_draw_page((uint8_t)(pg ^ 1)); + full.x = 0; full.y = 0; full.w = 320; full.h = 256; + gfx_copy_page(&full, GFX_COPY_DIRECT); + cur_room = room; kid_room = Kid.room; + /* Обе страницы теперь содержат только свежий фон. Старые габариты + * персонажей относятся к кадру до QuickLoad и не должны запускать heal + * поверх восстановленной комнаты. */ + pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0; + pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0; + pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0; + pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0; + pop_cd_init(); + pop_hp_invalidate(); +} + /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и * для запуска, и для рестарта после смерти / выпадения из уровня: в diff --git a/applications/PoP/roomtest/roomtest_cold.h b/applications/PoP/roomtest/roomtest_cold.h index 7076ef2..ed1ebfb 100644 --- a/applications/PoP/roomtest/roomtest_cold.h +++ b/applications/PoP/roomtest/roomtest_cold.h @@ -47,6 +47,9 @@ extern uint8_t pop_checkpoint; /* checkpoint (seg000:270) — ставит у * Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */ void enter_room_side(uint8_t room, uint8_t side) __banked; void enter_room(uint8_t room) __banked; +/* Пересобрать только производные кэши и обе страницы после QuickLoad. + * Игровые таймеры, trobs, mobs и персонажей не изменяет. */ +void pop_qsave_restore_room(uint8_t room) __banked; /* ---- холодная половина ----------------------------------------------- */ diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index ea76f95..eea8121 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -27,6 +27,17 @@ #include #include #include +#include "../pop_qsave.h" + +/* Сериализаторы приватного состояния входят в те же крупные модули, что и + * физика/ИИ. Сами QuickSave-тесты работают на target с EMM; обычным + * ucsim-сценам нужны только символы, и вызывать их они не должны. */ +void pop_qs_bytes(pop_qs_io_t *io, void *p, uint16_t n) +{ (void)io; (void)p; (void)n; } +void pop_qs_u8(pop_qs_io_t *io, uint8_t *p) { (void)io; (void)p; } +void pop_qs_i8(pop_qs_io_t *io, int8_t *p) { (void)io; (void)p; } +void pop_qs_u16(pop_qs_io_t *io, uint16_t *p) { (void)io; (void)p; } +void pop_qs_i16(pop_qs_io_t *io, int *p) { (void)io; (void)p; } #include "pop_kid.h" #include "pop_guard.h" @@ -35,6 +46,8 @@ #include "pop_trob.h" #include "pop_level.h" #include "pop_redraw.h" +#include "pop_pace.h" +#include "pop_sfx.h" #include "_pop_draw.h" #include "pop_cdraw.h" #include "stubs.h" @@ -49,6 +62,7 @@ void _gfx_w0_isr(void) { } void gfx_w0_map(uint8_t page) { (void)page; } void gfx_w0_unmap(void) { } +void gfx_w0_page_prepare(uint8_t page) { (void)page; } void gfx_blit_cols(int x, int y, const void *img, uint8_t flip) { (void)x; (void)y; (void)img; (void)flip; } @@ -95,6 +109,12 @@ int read(int fd, void *b, int n) int close(int fd) { (void)fd; return 0; } +int bank_load_file(uint8_t page, uint16_t off, const char *path, uint16_t max) +{ + (void)page; (void)off; (void)path; + return kid_blob_size <= max ? (int)kid_blob_size : (int)max; +} + /* ---- клавиатура: управляемая тестом --------------------------------- */ uint16_t tk_keys[TK_KEYS_MAX]; @@ -124,6 +144,13 @@ void tk_key_down(uint16_t code) if (tk_keys[i] == 0) { tk_keys[i] = code; return; } } +/* Темп кадра и звук относятся к окружению реальной машины. Физика и + * управление только оставляют выборки луча и заявки эффектов; host-тесты + * проверяют их игровую логику, поэтому здесь достаточно no-op контрактов. */ +void pop_beam_sample(void) __preserves_regs(b, c, d, e) { } +void pop_sfx_play(uint8_t id) { (void)id; } +void pop_sfx_stop(void) { } + /* ---- 2. соседние модули движка -------------------------------------- */ /* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота), @@ -167,8 +194,9 @@ void pop_start_chompers(uint8_t room, int8_t row) __banked { (void)room; (void)row; } void pop_trigger_button(uint8_t room, uint8_t tilepos, - uint8_t button_type, uint8_t modifier) __banked -{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); } + uint8_t button_type, uint8_t modifier, + uint8_t snd) __banked +{ (void)modifier; (void)snd; log_put(TK_BUTTON, room, tilepos, button_type); } void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked { log_put(TK_REDRAW, tilepos, kind, pages); } diff --git a/applications/PoP/roomtest/tests-host/t_char.c b/applications/PoP/roomtest/tests-host/t_char.c index 0a6c710..f213c60 100644 --- a/applications/PoP/roomtest/tests-host/t_char.c +++ b/applications/PoP/roomtest/tests-host/t_char.c @@ -84,19 +84,21 @@ TC_TEST(char_take_hp_goes_to_active) pop_saveshad(); } -/* Чит бессмертия — только Киду: страж обязан гибнуть и с ним (иначе - * отладочный режим ломает боёвку). */ -TC_TEST(char_immortal_covers_kid_only) +/* Уровень 2 чита гасит только мелкий урон Киду. Мгновенная смерть + * (count=100) проходит всегда, а на стража чит не действует вовсе. */ +TC_TEST(char_immortal_level2_covers_kid_only) { sc_room(room_flat, 1); sc_kid_at(3, 1, -1); sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); - pop_immortal = 1; + pop_immortal = 2; pop_loadkid(); hitp_delta = 0; - tc_check(pop_take_hp(100), 0, "kid: бессмертен", __LINE__); + tc_check(pop_take_hp(1), 0, "kid: мелкий урон погашен", __LINE__); tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__); + tc_check(pop_take_hp(100), 1, "kid: мгновенная смерть проходит", __LINE__); + tc_check(hitp_delta, -3, "kid: смертельная дельта", __LINE__); pop_savekid(); pop_loadshad(); @@ -497,7 +499,7 @@ void main(void) { sc_init(); TC_RUN(char_take_hp_goes_to_active); - TC_RUN(char_immortal_covers_kid_only); + TC_RUN(char_immortal_level2_covers_kid_only); TC_RUN(char_delta_hp_applies_per_character); TC_RUN(char_kid_survives_two_row_fall); TC_RUN(char_guard_dies_from_two_row_fall); diff --git a/bin/sprinter-cc b/bin/sprinter-cc index b3b84f7..cab852c 100755 --- a/bin/sprinter-cc +++ b/bin/sprinter-cc @@ -163,6 +163,34 @@ done [[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; } [[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; } +# ------- Validate bank layout and derive n_banks ---------------------------- +# crt0_banked loads 16 KB images consecutively and fills _bank_pages[1..N], +# therefore N is the highest bank number and the numbering must have no gaps. +# Several source modules may share one bank, so counting --bank options would +# be incorrect. +BANK_MAX=0 +BANK_SEEN=() +for spec in "${BANK_SPECS[@]}"; do + if [[ ! "$spec" =~ ^([0-9]+)=(.+)$ ]]; then + echo "sprinter-cc: bad --bank specification: '$spec' (expected N=FILE.c)" >&2 + exit 1 + fi + bank_n="${BASH_REMATCH[1]}" + bank_n=$((10#$bank_n)) + if (( bank_n < 1 || bank_n > 15 )); then + echo "sprinter-cc: bank number must be in range 1..15: '$spec'" >&2 + exit 1 + fi + BANK_SEEN[$bank_n]=1 + (( bank_n > BANK_MAX )) && BANK_MAX=$bank_n +done +for ((bank_n = 1; bank_n <= BANK_MAX; bank_n++)); do + if [[ -z "${BANK_SEEN[$bank_n]:-}" ]]; then + echo "sprinter-cc: bank $bank_n is missing; banks must be consecutive from 1 to $BANK_MAX" >&2 + exit 1 + fi +done + # ------- --w3: резидентный код окна W3 (подход A) ---------------------------- # W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без # трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2), @@ -399,8 +427,8 @@ fi # 3. bank infrastructure — required whenever crt0_banked is in play. # Always assemble bank.s for _bank_pages + the bcall/bjump trampolines. -# If the user did not pass any --bank, generate a tiny stub providing -# n_banks = 0 (crt0_banked.s imports this symbol unconditionally). +# Generate n_banks from the validated --bank layout. crt0_banked.s imports +# this symbol unconditionally, including banked memory modes with zero banks. # In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default # mode they live in W3 at low16 = 0xC000. BANK_RELS=() @@ -416,14 +444,13 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL" BANK_RELS+=("$BANK_TRAMP_REL") - if [[ ${#BANK_SPECS[@]} -eq 0 ]]; then - # User has no banks — supply n_banks=0 so crt0_banked links. - STUB_C="$WORK/_n_banks_stub.c" - STUB_REL="$WORK/_n_banks_stub.rel" - echo "const unsigned char n_banks = 0;" > "$STUB_C" - run "$SDCC" "${CC_FLAGS[@]}" -c -o "$STUB_REL" "$STUB_C" - BANK_RELS+=("$STUB_REL") - else + BANK_COUNT_C="$WORK/_n_banks_auto.c" + BANK_COUNT_REL="$WORK/_n_banks_auto.rel" + echo "const unsigned char n_banks = $BANK_MAX;" > "$BANK_COUNT_C" + run "$SDCC" "${CC_FLAGS[@]}" -c -o "$BANK_COUNT_REL" "$BANK_COUNT_C" + BANK_RELS+=("$BANK_COUNT_REL") + + if [[ ${#BANK_SPECS[@]} -gt 0 ]]; then for spec in "${BANK_SPECS[@]}"; do bank_n="${spec%%=*}" bank_src="${spec#*=}" diff --git a/docs/TODO.md b/docs/TODO.md index c07f000..9c77389 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,6 +5,17 @@ ## Ближайшее +- [ ] **Перевести новые EMM/W0-функции libc/libbgi на Z80 asm:** + `bank_load_file()`, `bank_save_file()` и `gfx_w0_page_prepare()`. + Сейчас универсальная C-обвязка (четыре аргумента, IX-frame, проверки + диапазона и сохранение временных значений) съедает часть выигрыша от + унификации загрузчиков PoP. Сделать компактные `__naked`-реализации + по образцу остальных низкоуровневых функций библиотек, строго сохранив + `__sdcccall(1)`, IX, W3 до возврата и семантику `-1 + errno`; fast- + вариант может вырезать проверки диапазона, safe обязан их оставить. + После правки: `make -C libc`, `make -C libbgi`, `make size-check` и + MAME-проверка Atlas/Kid/Level/Sound + QuickSave/QuickLoad. + - [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план: `docs/host-tests-plan.md`. Обвязка уже готова и обкатана (`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но @@ -61,12 +72,10 @@ ## Auto-banking (memory/banking_roadmap.md) -- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.** - Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…` - у `sprinter-cc` и константой `const uint8_t n_banks = N;` в - приложении (её читает `crt0_banked` при старте, чтобы выделить - EMM-страницы и загрузить в них образы банков). Рассогласование не - даёт ни ошибки компиляции, ни ошибки линковки — только зависание в +- [x] **Генерировать `n_banks` из реальной раскладки банков.** + `sprinter-cc` вычисляет максимальный N из `--bank N=…`, проверяет диапазон + 1..15 и отсутствие дыр, затем линкует сгенерированный `_n_banks`. + Раньше число задавалось ещё и вручную в приложении; рассогласование давало зависание в рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом уже есть в шапке `runtime/bank.s`). @@ -75,20 +84,9 @@ загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз занял заметно больше, чем сама правка: симптом (зависание в чужом коде) никак не указывает на причину. - Сверять надо не количество, а **максимальный индекс** банка: `--bank` - нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked` - заполняет `_bank_pages[1..n_banks]` подряд. - Варианты, от простого к правильному: - 1. **Проверка после линковки** — вытащить значение `_n_banks` из - `.ihx`/`.map` (это `const` в `_CODE`, адрес известен из карты) и - сравнить с `max(N)` по флагам; не совпало — `sprinter-cc` падает - с внятным сообщением. Ничего не ломает в существующих - приложениях, ставится сразу. - 2. **Генерировать самим `sprinter-cc`** — он уже так делает, когда - банков нет (`const unsigned char n_banks = 0;`). Правильнее по - сути (единственный источник истины), но требует убрать константу - из всех банковых приложений, иначе дубль символа: `tests/banked`, - `banktest`, `banklocl`, `bankedbg`, `applications/PoP/roomtest`. + Берётся не число опций (в одном банке может быть несколько модулей), а + **максимальный индекс**. Дыры запрещены, поскольку `crt0_banked` и `mkexe` + обрабатывают `_bank_pages[1..n_banks]` подряд. Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map, diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 2d35eb0..33660fe 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -267,6 +267,9 @@ tests/bgi_img. сменить Port_Y, промежуточный RAM-буфер не нужен). Копия = скролл + heal цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в *dirty (NULL = не нужно). Пример: examples/scroll. +Для произвольных EMM-данных, которые затем маппятся в W0, +`gfx_w0_page_prepare(page)` устанавливает IRQ/NMI-стабы в `0x38/0x66` и +запоминает штатную DSS-страницу. `atlas_load()` вызывает её автоматически. Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings` (SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/ drawpoly. @@ -476,7 +479,10 @@ inline `sprinter_page_w0..w3(page)`; ENV: `getenv putenv sysenv`. mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам: `bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 — -вариант через окно W1). +вариант через окно W1). `bank_load_file(page,off,path,max_size)` читает +файл непосредственно в диапазон EMM-страницы, `bank_save_file` записывает +диапазон одним WRITE; обе функции возвращают фактический размер либо -1. +Они восстанавливают W3 до возврата и безопасны для вызова из huge-банка. ## diff --git a/libbgi/common/atlas_load.c b/libbgi/common/atlas_load.c index 9565ebe..90fdc25 100644 --- a/libbgi/common/atlas_load.c +++ b/libbgi/common/atlas_load.c @@ -2,58 +2,37 @@ * atlas_load — загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в * дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес). * - * Порядок: выделить страницу (mem_alloc_pages) → замапить в W3 → - * read() файла ЦЕЛИКОМ в 0xC000 (ESTEX READ пишет в W3 — проверено - * tests/w0page P1) → проверить магию 'SPA1' → пропатчить ISR-стаб - * (0x38: JP _gfx_w0_isr; 0x66: RETN — место зарезервировано в - * заголовке файла) → вернуть W3. Никакого пересчёта офсетов: каталог - * и ленты уже лежат по своим W0-адресам. + * Порядок: выделить страницу → bank_load_file() → проверить магию 'SPA1' + * → gfx_w0_page_prepare(). Никакого пересчёта офсетов: каталог и ленты + * уже лежат по своим W0-адресам. * * Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа). * Ошибки: -1 + errno (open/read/alloc; EINVAL — не .atl). */ -#include -#include #include -#include #include #include #include "../_bgi.h" int atlas_load(atlas_t *a, const char *path) { - uint8_t *pg = (uint8_t *)0xC000; - uint8_t saved_w3, blk, ok, count; - uint16_t stub; - int fd, n; - - fd = open(path, O_RDONLY); - if (fd < 0) return -1; + uint8_t blk, ok, count, h[5]; + int n; blk = mem_alloc_pages(1); - if (!blk) { close(fd); return -1; } - - if (!_gfx_w0_dss) - _gfx_w0_dss = _io_page_w0; /* снять страницу ядра DSS */ + if (!blk) return -1; a->blk = blk; a->page = mem_get_page(blk, 0); - saved_w3 = _io_page_w3; - sprinter_page_w3(a->page); - - n = read(fd, pg, 16384); + n = bank_load_file(a->page, 0, path, 16384); + if (n >= 5) bank_read(a->page, 0, h, sizeof(h)); ok = (n >= (int)_ATL_DATA_OFF && - pg[0] == 'S' && pg[1] == 'P' && pg[2] == 'A' && pg[3] == '1'); + h[0] == 'S' && h[1] == 'P' && h[2] == 'A' && h[3] == '1'); count = 0; if (ok) { - count = pg[4]; - stub = (uint16_t)&_gfx_w0_isr; - pg[0x38] = 0xC3; /* JP _gfx_w0_isr */ - pg[0x39] = (uint8_t)(stub & 0xFF); - pg[0x3A] = (uint8_t)(stub >> 8); - pg[0x66] = 0xED; /* RETN (NMI) */ - pg[0x67] = 0x45; + count = h[4]; + gfx_w0_page_prepare(a->page); } /* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской @@ -63,9 +42,6 @@ int atlas_load(atlas_t *a, const char *path) * a->count оставался мусором, room_draw читал пиксели не с того * оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не * касалось, только a->count. */ - sprinter_page_w3(saved_w3); - close(fd); - if (!ok) { mem_free_block(blk); a->page = 0; diff --git a/libbgi/common/gfx_w0_page_prepare.c b/libbgi/common/gfx_w0_page_prepare.c new file mode 100644 index 0000000..5806fc4 --- /dev/null +++ b/libbgi/common/gfx_w0_page_prepare.c @@ -0,0 +1,21 @@ +/* + * gfx_w0_page_prepare — сделать произвольную EMM-страницу безопасной в W0. + * + * При первом вызове запоминает штатную страницу DSS, затем ставит в целевой + * странице переход на общий IRQ-восстановитель и RETN для NMI. Функция + * резидентна: она сама временно переключает W3. + */ +#include +#include +#include "../_bgi.h" + +void gfx_w0_page_prepare(uint8_t page) +{ + uint8_t saved=_io_page_w3, *p=(uint8_t *)0xC000; + uint16_t stub=(uint16_t)&_gfx_w0_isr; + if (!_gfx_w0_dss) _gfx_w0_dss=_io_page_w0; + sprinter_page_w3(page); + p[0x38]=0xC3; p[0x39]=(uint8_t)stub; p[0x3A]=(uint8_t)(stub >> 8); + p[0x66]=0xED; p[0x67]=0x45; + sprinter_page_w3(saved); +} diff --git a/libbgi/include/sprite.h b/libbgi/include/sprite.h index c5a2dbc..036707b 100644 --- a/libbgi/include/sprite.h +++ b/libbgi/include/sprite.h @@ -290,6 +290,10 @@ typedef struct { * 0 — OK; -1 + errno (файл/память/формат). */ int atlas_load(atlas_t *a, const char *path); +/* Подготовить уже выделенную EMM-страницу для безопасного маппинга в W0: + * установить IRQ/NMI-стабы и запомнить штатную страницу DSS. */ +void gfx_w0_page_prepare(uint8_t page); + /* Освободить EMM-страницу атласа (все sprite_t с его лентами должны * быть скрыты/переинициализированы ДО вызова). */ void atlas_free(atlas_t *a); diff --git a/libc/include/sprinter_mem.h b/libc/include/sprinter_mem.h index 825bae8..ef0cece 100644 --- a/libc/include/sprinter_mem.h +++ b/libc/include/sprinter_mem.h @@ -110,6 +110,15 @@ void bank_read(uint8_t phys_page, uint16_t off, void *dst, uint16_t n); /* Copy `n` bytes from near buffer `src` into phys_page[off..off+n-1]. */ void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n); +/* Файловый обмен непосредственно с диапазоном EMM-страницы через W3. + * В отличие от bank_read/bank_write безопасен для вызова из huge-банка: + * path используется до переключения W3, а окно восстановлено до возврата. + * Возвращает фактический размер либо -1 + errno. */ +int bank_load_file(uint8_t phys_page, uint16_t off, const char *path, + uint16_t max_size); +int bank_save_file(uint8_t phys_page, uint16_t off, const char *path, + uint16_t size); + /* =================================================================== * Far-page accessors via window 1 (base 0x4000, port 0xA2) * diff --git a/libc/mem/bank_load_file.c b/libc/mem/bank_load_file.c new file mode 100644 index 0000000..e4c93cb --- /dev/null +++ b/libc/mem/bank_load_file.c @@ -0,0 +1,33 @@ +/* + * bank_load_file — прочитать файл непосредственно в физическую EMM-страницу. + * + * ESTEX принимает файловый буфер только в W1..W3; используем W3, сохраняя и + * восстанавливая его раскладку. Функция резидентна, поэтому её безопасно + * вызывать из банкового кода huge: после open() указатель path больше не + * читается, пока W3 временно занят целевой страницей. + * + * Возврат: фактически прочитанный размер либо -1 + errno. + */ +#include +#include +#include +#include +#include + +int bank_load_file(uint8_t phys_page, uint16_t off, const char *path, + uint16_t max_size) +{ + uint8_t saved; + int fd, n; + if (off > 0x3FFFu || max_size > (uint16_t)(0x4000u - off)) { + errno=EINVAL; return -1; + } + fd = open(path, O_RDONLY); + if (fd < 0) return -1; + saved = _io_page_w3; + sprinter_page_w3(phys_page); + n = read(fd, (void *)(0xC000u + off), max_size); + sprinter_page_w3(saved); + close(fd); + return n; +} diff --git a/libc/mem/bank_save_file.c b/libc/mem/bank_save_file.c new file mode 100644 index 0000000..fedc829 --- /dev/null +++ b/libc/mem/bank_save_file.c @@ -0,0 +1,30 @@ +/* + * bank_save_file — записать диапазон физической EMM-страницы в файл. + * + * Файл создаётся заново (O_CREAT|O_TRUNC), данные передаются одним WRITE. + * W3 восстанавливается до close(), поэтому функцию можно вызывать из + * банкового кода huge. Возврат: записанный размер либо -1 + errno. + */ +#include +#include +#include +#include +#include + +int bank_save_file(uint8_t phys_page, uint16_t off, const char *path, + uint16_t size) +{ + uint8_t saved; + int fd, n; + if (off > 0x3FFFu || size > (uint16_t)(0x4000u - off)) { + errno=EINVAL; return -1; + } + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC); + if (fd < 0) return -1; + saved = _io_page_w3; + sprinter_page_w3(phys_page); + n = write(fd, (const void *)(0xC000u + off), size); + sprinter_page_w3(saved); + if (close(fd) != 0) return -1; + return n; +} diff --git a/release_docs/en/sprinter_cc.md b/release_docs/en/sprinter_cc.md index 51e8261..4adcfe9 100644 --- a/release_docs/en/sprinter_cc.md +++ b/release_docs/en/sprinter_cc.md @@ -23,7 +23,7 @@ sprinter-cc -o OUT.exe SRC.c [more.c ...] [options] | Flag | Description | |---|---| -| `--bank N=FILE.c` | Compile FILE.c into bank N (1..15). Repeatable. Functions in banked files need the `__banked` qualifier. | +| `--bank N=FILE.c` | Compile FILE.c into bank N (1..15). Numbers must be consecutive; crt0's bank count is generated automatically. Repeatable; functions need `__banked`. | | `--crt0=TYPE` | Override startup file: `default` / `minimal` / `banked` / `small`. Normally chosen automatically by the memory mode. | ### Diagnostics diff --git a/release_docs/ru/sprinter_cc.md b/release_docs/ru/sprinter_cc.md index 82ada9e..f2a2fb4 100644 --- a/release_docs/ru/sprinter_cc.md +++ b/release_docs/ru/sprinter_cc.md @@ -23,7 +23,7 @@ sprinter-cc -o OUT.exe SRC.c [more.c ...] [options] | Флаг | Описание | |---|---| -| `--bank N=FILE.c` | Компилировать FILE.c в банк N (1..15). Повторяемый. Функции в banked-файлах нужны с квалификатором `__banked`. | +| `--bank N=FILE.c` | Компилировать FILE.c в банк N (1..15). Номера без пропусков; число банков crt0 получает автоматически. Повторяемый; функции нужны с `__banked`. | | `--crt0=TYPE` | Переопределение startup-файла: `default` / `minimal` / `banked` / `small`. Обычно выбирается автоматически по memory mode. | ### Диагностика diff --git a/runtime/crt0_banked.s b/runtime/crt0_banked.s index 6d19522..8f9364c 100644 --- a/runtime/crt0_banked.s +++ b/runtime/crt0_banked.s @@ -20,11 +20,9 @@ ;; 6. CLOSE file ;; 7. Standard gsinit + call _main + ESTEX EXIT ;; -;; _n_banks MUST be defined as `const uint8_t n_banks = N;` — we read it -;; before gsinit, so a plain initialized `uint8_t` would still hold 0 -;; (the initializer hasn't been copied yet) and bank loading would be -;; silently skipped, leaving window 3 mapped to a garbage page. -;; For zero banks, link crt0.s instead. +;; _n_banks is generated by sprinter-cc from the highest --bank number. +;; It is emitted as const because we read it before gsinit; a normal +;; initialized variable would still contain 0 at this point. ;; ---------------------------------------------------------------------- .module crt0_banked diff --git a/tests/banked/banked.c b/tests/banked/banked.c index 7f912c4..d10627f 100644 --- a/tests/banked/banked.c +++ b/tests/banked/banked.c @@ -2,9 +2,6 @@ #include #include -/* crt0_banked.s reads this constant to know how many banks to load. */ -const uint8_t n_banks = 2; - void bank1_func(int x) __banked; void bank2_func(int x) __banked; diff --git a/tests/bankedbg/bankedbg.c b/tests/bankedbg/bankedbg.c index c69d5ad..7f531c8 100644 --- a/tests/bankedbg/bankedbg.c +++ b/tests/bankedbg/bankedbg.c @@ -13,9 +13,6 @@ #include #include -/* crt0_banked.s reads this constant to know how many banks to load. */ -const uint8_t n_banks = 2; - void bank1_func(int x) __banked; void bank2_func(int x) __banked; diff --git a/tests/banklocl/banklocl.c b/tests/banklocl/banklocl.c index f595d1e..cc1fc25 100644 --- a/tests/banklocl/banklocl.c +++ b/tests/banklocl/banklocl.c @@ -18,11 +18,6 @@ extern void bank1_map_print(void) __banked; extern uint8_t *bank1_alloc_and_fill(int n, uint8_t pattern) __banked; extern void bank1_free (void *p) __banked; -/* MUST be `const`: crt0_banked reads _n_banks BEFORE gsinit, so - * non-const initialized variables haven't been copied yet and would - * read 0 → bank loading is skipped → garbage in window 3. */ -const uint8_t n_banks = 1; - uint8_t local0_buf[16]; diff --git a/tests/banktest/banktest.c b/tests/banktest/banktest.c index 58188ad..cc1ff3f 100644 --- a/tests/banktest/banktest.c +++ b/tests/banktest/banktest.c @@ -2,9 +2,6 @@ #include #include -/* crt0_banked.s reads this constant to know how many banks to load. */ -const uint8_t n_banks = 2; - void bank1_func(void) __banked; void bank2_func(void) __banked; void bank1_func2(uint16_t) __banked; diff --git a/tests/w3bankgfx/w3bgfx.c b/tests/w3bankgfx/w3bgfx.c index fb1b87f..93e4563 100644 --- a/tests/w3bankgfx/w3bgfx.c +++ b/tests/w3bankgfx/w3bgfx.c @@ -15,8 +15,6 @@ #include #include -const uint8_t n_banks = 1; /* читает crt0_banked */ - extern uint8_t res_page_seen; uint8_t bank_page_before, bank_page_after, bank_page_after_home, bank_alive; extern uint8_t bank_pages[]; diff --git a/tests/w3probe/w3big.c b/tests/w3probe/w3big.c index 587358e..689414f 100644 --- a/tests/w3probe/w3big.c +++ b/tests/w3probe/w3big.c @@ -7,9 +7,6 @@ #include #include -/* crt0_banked читает это ДО gsinit — только `const`. */ -const uint8_t n_banks = 1; - void bigbank_func(void) __banked; /* W1-банк, трамплин */ extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */ extern void w3_show(void); diff --git a/tests/w3probe/w3huge.c b/tests/w3probe/w3huge.c index 86ae070..3e8742e 100644 --- a/tests/w3probe/w3huge.c +++ b/tests/w3probe/w3huge.c @@ -12,8 +12,6 @@ #include #include -const uint8_t n_banks = 1; /* один свапаемый W3-банк */ - void hugebank_func(void) __banked; /* W3-банк, трамплин */ extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */ extern void w3_show(void); /* W3-резидент, прямой вызов */