QuickSave F6/F9 в roomtest; bank_load_file/bank_save_file/gfx_w0_page_prepare; sprinter-cc: авто n_banks
roomtest: - QuickSave/QuickLoad (F6/F9): снапшот 'POPQ' v3 в POP.SAV/POP.BAK на HDD, транзакционная запись (POP.NEW -> rename, откат при ошибке), XOR-контрольная сумма payload'а; сериализация всех игровых переменных через W0-примитивы pop_qs_*; pop_qsave_process() на границе кадра вне Char-окон - pop_qsave_restore_room(): полная перезагрузка комнаты после загрузки (карта/края/швы, сброс bake-кэша, перерисовка обеих страниц, инвалидация кэшей спрайтов и HP) - сериализаторы в pop_map/pop_loose_mob/pop_trob/pop_guard_ai (+ восстановление инвариантов: mobs_live, trob_drawn, redraw) - immortal-чит 2 уровня: уровень 2 поглощает только малый урон Kid'а libc/libbgi: - bank_load_file()/bank_save_file() — резидентное файловое I/O в банк, без правила W3 (путь читается до переключения страницы) - gfx_w0_page_prepare(page) — подготовка W0-окна (IRQ/NMI-стабы) одной функцией; atlas_load.c и roomtest переведены на новые примитивы; ручные ISR-стабы удалены sprinter-cc / сборка: - --bank N=FILE.c: автогенерация n_banks (_n_banks_auto.c), ручные const n_banks удалены из тестов - roomtest/app.mk: ресурсы через stamp-файлы (.resource-stamps/) — один запуск упаковщика на группу вместо N под -B; HDD_PACK_ARGS
This commit is contained in:
@@ -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/).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) — образ пересобран."
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -180,12 +180,14 @@ SDLPoP, сохраняются.
|
||||
|
||||
---
|
||||
|
||||
## 4. Куда писать снимок: файл, а не EMM-страница
|
||||
## 4. Куда писать снимок: HDD-файл, а не EMM-страница
|
||||
|
||||
Рекомендация: **основной путь — файл `QUICKSAVE.SAV`; EMM-страница —
|
||||
необязательный второй слот.**
|
||||
Решение: **один основной слот `POP.SAV` на HDD; предыдущая
|
||||
валидная запись хранится в `POP.BAK`.** EMM-слота нет: программа
|
||||
работает только с HDD, а главный сценарий QuickSave обязан переживать
|
||||
перезапуск игры.
|
||||
|
||||
> Пересмотрено 2026-08-17 по вопросу пользователя «почему EMM, а не файл».
|
||||
> Пересмотрено 2026-08-21 по вопросу пользователя «почему EMM, а не файл».
|
||||
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
|
||||
> скорость и недооценивала главный сценарий использования. Разбор оставлен
|
||||
> целиком, потому что довод переносится и на другие «положить в память
|
||||
@@ -214,10 +216,8 @@ MAME, обязательный после каждой пересборки об
|
||||
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
|
||||
строки.
|
||||
|
||||
Что остаётся за EMM: мгновенный слот для «переиграть» без обращения к диску.
|
||||
Делается тем же сериализатором и добавляется, если понадобится. Поэтому
|
||||
обход состояния писать сразу так, чтобы «куда» было параметром — как у
|
||||
SDLPoP через `process_func`.
|
||||
Обход состояния всё равно писать с абстракцией чтения/записи, как у SDLPoP
|
||||
через `process_func`, но второй EMM-слот в scope не входит.
|
||||
|
||||
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
|
||||
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
|
||||
@@ -234,6 +234,7 @@ roomtest.
|
||||
+5 pop_current_level 1 Б
|
||||
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
||||
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
||||
.. checksum 2 Б (заголовок + payload)
|
||||
```
|
||||
|
||||
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
||||
@@ -282,13 +283,12 @@ static void qs_walk(qs_io_t io) /* io = запись или чтение */
|
||||
|
||||
| шаг | что | критерий готовности |
|
||||
|---|---|---|
|
||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы |
|
||||
| **QS2** | Ядро: `qs_walk` + запись/чтение файла `QUICKSAVE.SAV`, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились |
|
||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||
| **QS2** | Ядро: `qs_walk` + `POP.SAV`, магия/версия/checksum, безопасная замена с предыдущей валидной копией в `POP.BAK` | сохранение и загрузка **в той же комнате, без движения**; порча SAV не портит BAK |
|
||||
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
||||
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
||||
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
||||
| **QS6** | Опционально: второй слот в EMM-странице тем же сериализатором | мгновенное «переиграть» без обращения к диску |
|
||||
|
||||
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
|
||||
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
|
||||
@@ -320,5 +320,7 @@ QS2 нечего проверять, а QS5 обязан идти после QS3
|
||||
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
||||
страницу. Симптом — мерцание через кадр
|
||||
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
|
||||
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы
|
||||
достаточно. Решать после QS3, по факту использования.
|
||||
6. **Транзакция SAV/BAK.** До кодинга проверить на DSS семантику
|
||||
rename/replace. Если атомарная замена не гарантирована, писать через
|
||||
`POP.NEW`, проверять его после close и не удалять единственную валидную копию
|
||||
до завершения новой.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
Уровень 4, комната 22 - страж идет навстречу Киду и попадает сам в челюсти - должен быть инстинк самосохранения
|
||||
|
||||
Перед решеткой Кид делает лишний мелкий шаг.
|
||||
|
||||
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
|
||||
|
||||
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
|
||||
|
||||
Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
|
||||
но двери которые она должна открыть остаются закрытыми
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#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). Звать при смене комнаты/уровня: номера тайлов повторяются, и
|
||||
|
||||
@@ -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, если он
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprite.h> /* 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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#ifndef POP_LEVEL_H
|
||||
#define POP_LEVEL_H
|
||||
|
||||
#include "pop_qsave.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Загрузить уровень из файла в 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`) и выставить
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Сериализация уровня для QuickSave (банк 8).
|
||||
*
|
||||
* Код выполняется только по F6/F9 и вынесен из горячего резидентного
|
||||
* pop_level.c. Данные уровня и снимка используют одно окно W0, поэтому
|
||||
* foretable переносится небольшими блоками через стековый буфер W2.
|
||||
*/
|
||||
#include <sprite.h>
|
||||
#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; i<sizeof(tmp); i++)
|
||||
tmp[i]=((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i];
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
pop_qs_bytes(io,tmp,sizeof(tmp));
|
||||
if (io->reading && io->apply && !io->failed) {
|
||||
gfx_w0_map(pop_lvl_page);
|
||||
for (i=0; i<sizeof(tmp); i++)
|
||||
((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i]=tmp[i];
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3394,3 +3394,16 @@ void pop_check_mirror(void) __banked
|
||||
/* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется,
|
||||
* а следующий тик перезагружает окно из Kid. */
|
||||
}
|
||||
|
||||
/* Закрытое состояние физики, которое влияет на следующие кадры. Таблицы
|
||||
* коллизии и кэши соседей намеренно не входят: их пересчитает вход в комнату. */
|
||||
void pop_map_qsave(pop_qs_io_t *io) __banked
|
||||
{
|
||||
pop_qs_u8(io, &is_screaming);
|
||||
pop_qs_u8(io, &grab_timer);
|
||||
pop_qs_i8(io, &pickup_obj_type);
|
||||
pop_qs_u16(io, &loose_seed.lo);
|
||||
pop_qs_u16(io, &loose_seed.hi);
|
||||
pop_qs_u8(io, &last_loose_snd);
|
||||
if (io->reading && io->apply) pop_coll_invalidate();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#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 / приземление).
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* pop_qsave.c — файловая транзакция и схема игрового снимка.
|
||||
* Живёт в холодном банке: в обычном кадре исполняется только проверка
|
||||
* резидентного флага запроса, вся тяжёлая работа бывает по F6/F9.
|
||||
*/
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h> /* rename */
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprite.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Холодная часть ввода-вывода QuickSave (банк 8).
|
||||
*
|
||||
* Эти функции вызываются только банковым оркестратором pop_qsave.c, поэтому
|
||||
* им не место в дефицитном резидентном _CODE. Здесь только доступ через W0:
|
||||
* переключающие W3 DSS-операции обязаны оставаться в pop_qsave_io.c.
|
||||
*/
|
||||
#include "pop_qsave.h"
|
||||
#include <sprite.h>
|
||||
|
||||
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; i<size; i++) q[i]=data[i];
|
||||
gfx_w0_unmap(); io->pos += 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; i<size; i++) data[i]=q[i];
|
||||
gfx_w0_unmap(); io->pos += 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));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* Резидентная часть QuickSave: часто вызываемые межбанковые примитивы
|
||||
* потока и флаг запроса. Файловый обмен с W3 теперь общий в libc. */
|
||||
#include "pop_qsave.h"
|
||||
#include <sprite.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#ifndef POP_TROB_H
|
||||
#define POP_TROB_H
|
||||
|
||||
#include "pop_qsave.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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 уровня
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и
|
||||
* для запуска, и для рестарта после смерти / выпадения из уровня: в
|
||||
|
||||
@@ -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;
|
||||
|
||||
/* ---- холодная половина ----------------------------------------------- */
|
||||
|
||||
|
||||
@@ -27,6 +27,17 @@
|
||||
#include <sprite.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
#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); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
+37
-10
@@ -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#*=}"
|
||||
|
||||
+18
-20
@@ -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,
|
||||
|
||||
@@ -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-банка.
|
||||
|
||||
## <sprinter_exit.h>
|
||||
|
||||
|
||||
+11
-35
@@ -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 <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprite.h>
|
||||
#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;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* gfx_w0_page_prepare — сделать произвольную EMM-страницу безопасной в W0.
|
||||
*
|
||||
* При первом вызове запоминает штатную страницу DSS, затем ставит в целевой
|
||||
* странице переход на общий IRQ-восстановитель и RETN для NMI. Функция
|
||||
* резидентна: она сама временно переключает W3.
|
||||
*/
|
||||
#include <sprinter.h>
|
||||
#include <sprite.h>
|
||||
#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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
*
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* bank_load_file — прочитать файл непосредственно в физическую EMM-страницу.
|
||||
*
|
||||
* ESTEX принимает файловый буфер только в W1..W3; используем W3, сохраняя и
|
||||
* восстанавливая его раскладку. Функция резидентна, поэтому её безопасно
|
||||
* вызывать из банкового кода huge: после open() указатель path больше не
|
||||
* читается, пока W3 временно занят целевой страницей.
|
||||
*
|
||||
* Возврат: фактически прочитанный размер либо -1 + errno.
|
||||
*/
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* bank_save_file — записать диапазон физической EMM-страницы в файл.
|
||||
*
|
||||
* Файл создаётся заново (O_CREAT|O_TRUNC), данные передаются одним WRITE.
|
||||
* W3 восстанавливается до close(), поэтому функцию можно вызывать из
|
||||
* банкового кода huge. Возврат: записанный размер либо -1 + errno.
|
||||
*/
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
### Диагностика
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
/* 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;
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
/* 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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
/* 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;
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
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[];
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
/* 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);
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
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-резидент, прямой вызов */
|
||||
|
||||
Reference in New Issue
Block a user