Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 | |||
| 7377bf6a2c | |||
| b34997073e | |||
| 63bfd997a9 | |||
| decbec79de | |||
| b0e7130d0b | |||
| 4e12aa50d1 | |||
| c218e8b983 | |||
| a3aaa30e53 | |||
| 90e304f071 | |||
| b55d4d11e3 | |||
| ca67895667 | |||
| a5252b1c60 | |||
| 3a0e847353 | |||
| df5071a967 | |||
| 2349481b86 | |||
| 589894c50d | |||
| ea8efdb0fd | |||
| 623199337e | |||
| 602c3a20fa | |||
| 4ac3584bf9 | |||
| 3bf28da8ee | |||
| 770b946a36 | |||
| 89e9663753 | |||
| ea07a8d7b0 | |||
| d23126983e | |||
| 50570881f5 | |||
| f25ed37d85 | |||
| 3449f6f8c9 | |||
| 6dabe9b4b1 | |||
| a05970cd36 | |||
| 2176c12cc5 | |||
| 4e43890fce | |||
| 25b2db8b0b | |||
| f76914849f | |||
| 6cc8611d03 | |||
| 659071838d | |||
| f8e96c0495 | |||
| 76f02e76db | |||
| c71981fdf9 | |||
| 808c2a5349 | |||
| 31b82661eb | |||
| 4b74478d19 | |||
| 747783c422 | |||
| a3c5c600da | |||
| faec9a7d3a | |||
| 5d912f0f3f |
@@ -9,3 +9,21 @@ args = [
|
||||
"mcp<2",
|
||||
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
|
||||
]
|
||||
|
||||
[mcp_servers.mame-z80.tools.clear_breakpoint]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.list_breakpoints]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.press_key]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.step_out]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.debugger_command]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.pause]
|
||||
approval_mode = "approve"
|
||||
|
||||
@@ -131,3 +131,10 @@ applications/PoP/PoP1_DOS_music/
|
||||
# R1 — рабочая копия roomtest для экспериментов пользователя, в репозиторий
|
||||
# не идёт (сама roomtest и есть версируемая ветка разработки).
|
||||
applications/PoP/R1/
|
||||
|
||||
# SprPoP — автономное приложение, и правила игнора у него СВОИ:
|
||||
# applications/SprPoP/.gitignore. Он написан так, чтобы стать корневым
|
||||
# .gitignore, когда SprPoP выделят в отдельный репозиторий, — поэтому
|
||||
# здесь его содержимое НЕ дублируется (иначе разъедется). Коротко: в
|
||||
# репозиторий не идут assets/orig/ (чужие данные — их выкачивает
|
||||
# `make fetch`) и assets/packed/LEVELS/ (уровни из оригинала как есть).
|
||||
|
||||
@@ -80,7 +80,9 @@ floppy: tests examples tests/seek/big.txt
|
||||
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
|
||||
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
|
||||
# ucsim идёт в комплекте нашего SDCC.
|
||||
HOST_TEST_DIRS := testkit applications/PoP/roomtest/tests-host
|
||||
# applications/PoP/roomtest заморожена (её ветка развития — SprPoP), поэтому
|
||||
# её набор здесь больше не гоняется.
|
||||
HOST_TEST_DIRS := testkit applications/SprPoP/tests/host
|
||||
|
||||
host-tests:
|
||||
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||
# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd`
|
||||
#
|
||||
# include $(PROJ_ROOT)/app.mk
|
||||
#
|
||||
@@ -36,17 +37,40 @@ LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
||||
|
||||
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
|
||||
# ?= — приложение со своим каталогом выхода (applications/SprPoP) держит
|
||||
# образ у себя и связывает его с MAME символьной ссылкой.
|
||||
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||
|
||||
# Optional knobs — see top of file.
|
||||
MEMORY ?= tiny
|
||||
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
|
||||
|
||||
# SRC_DIR / BUILD_DIR — раскладка приложения, которое НЕ держит исходники и
|
||||
# выхлоп в одной папке с Makefile (applications/SprPoP: src/ и build/). По
|
||||
# умолчанию обе пусты, то есть всё как было: ./$(EXAMPLE).c → ./$(EXAMPLE).exe.
|
||||
# Пустое значение обрабатывается отдельной веткой намеренно: "./prog.exe" и
|
||||
# "prog.exe" — разные имена целей, и склеивать префикс безусловно нельзя.
|
||||
SRC_DIR ?=
|
||||
BUILD_DIR ?=
|
||||
ifeq ($(strip $(SRC_DIR)),)
|
||||
MAIN_SRC := $(EXAMPLE).c
|
||||
else
|
||||
MAIN_SRC := $(SRC_DIR)/$(EXAMPLE).c
|
||||
endif
|
||||
ifeq ($(strip $(BUILD_DIR)),)
|
||||
EXE := $(EXAMPLE).exe
|
||||
else
|
||||
EXE := $(BUILD_DIR)/$(EXAMPLE).exe
|
||||
endif
|
||||
SOURCES := $(MAIN_SRC) $(EXTRA_SRCS)
|
||||
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||
# своей раскладкой каталогов может переопределить переменную до include.
|
||||
HDD_PACK_ARGS ?= $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
||||
# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю
|
||||
# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него.
|
||||
HDD_DEST_DIR ?=
|
||||
|
||||
CC_FLAGS := --memory $(MEMORY)
|
||||
ifneq ($(STACK_SIZE),)
|
||||
@@ -54,15 +78,25 @@ CC_FLAGS += --stack-size $(STACK_SIZE)
|
||||
endif
|
||||
CC_FLAGS += $(EXTRA_FLAGS)
|
||||
|
||||
all: $(EXAMPLE).exe
|
||||
all: $(EXE)
|
||||
|
||||
# runtime/*.s (crt0-семейство, bank.s, heap.s) собираются per-build
|
||||
# внутри sprinter-cc — без этой зависимости их правка не перелинкует
|
||||
# уже собранный exe (кусало: фикс bank.s не подхватился).
|
||||
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
||||
|
||||
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
||||
# ПРОВЕРКА БАНКОВЫХ ВЫЗОВОВ — сразу после линковки, пока артефакты свежие.
|
||||
# Ловит прямой `call` в чужой банк: он собирается МОЛЧА и стреляет диким
|
||||
# переходом в пустой хвост банка (разбор — в шапке скрипта). Запускается
|
||||
# только если банки вообще есть, то есть по наличию каталога сборки с
|
||||
# bankN_*.asm; обычным небанковым программам ничего не стоит.
|
||||
BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py
|
||||
|
||||
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
||||
$(if $(strip $(BUILD_DIR)),@mkdir -p $(dir $@))
|
||||
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
||||
@d=$(dir $@).sprinter-cc-$(EXAMPLE); \
|
||||
if ls $$d/bank*_*.asm >/dev/null 2>&1; then python3 $(BANK_CHECK) $$d; fi
|
||||
|
||||
$(MKEXE):
|
||||
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
||||
@@ -76,13 +110,13 @@ $(LIB):
|
||||
$(MAKE) -C $(PROJ_ROOT)/libbgi
|
||||
|
||||
clean:
|
||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe
|
||||
rm -rf $(if $(strip $(BUILD_DIR)),$(BUILD_DIR),.sprinter-cc-* $(EXE))
|
||||
|
||||
# `make floppy` packs ONLY this program (+ optional EXTRA_DATA files) into
|
||||
# the MAME floppy image, replacing whatever was there. Handy for trying a
|
||||
# single program without rebuilding everything.
|
||||
floppy: $(EXAMPLE).exe
|
||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
floppy: $(EXE)
|
||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXE) $(EXTRA_DATA)
|
||||
@echo
|
||||
@echo "Floppy ready: $(FLOPPY_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA))) "
|
||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||
@@ -94,8 +128,8 @@ run: floppy
|
||||
# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD —
|
||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||
hdd: $(EXAMPLE).exe
|
||||
$(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
|
||||
hdd: $(EXE)
|
||||
$(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") $(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) — образ пересобран."
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
* Left: turn or run left
|
||||
* Right: turn or run right
|
||||
* Up: jump or climb up
|
||||
* Down: crouch or climb down
|
||||
* Down+Left/Right: hop
|
||||
* Shift: pick up things
|
||||
* Shift+Left/Right: careful step
|
||||
* Home or Up+Left: jump left
|
||||
* Page Up or Up+Right: jump right
|
||||
* Up while running: running jump
|
||||
* Shift while falling: grab onto ledge
|
||||
|
||||
* Left/Right: walk (advance or retreat)
|
||||
* Shift: strike (attack)
|
||||
* Up: block (defend)
|
||||
* Down: put sword away; press Shift to draw your sword again.
|
||||
|
||||
===
|
||||
|
||||
* Esc: Pause game.
|
||||
* Space: Show how much time is left.
|
||||
* Ctrl+A: Restart level.
|
||||
|
||||
* Ctrl+R: Return to intro.
|
||||
* Ctrl+S: Sound on/off.
|
||||
* Ctrl+M: Music on/off.
|
||||
* Ctrl+V: Show version of SprPoP.
|
||||
* Ctrl+Q: Quit game.
|
||||
|
||||
* F6: Quicksave: Save the exact state of the game.
|
||||
* F9: Quickload: Load what the last quicksave saved.
|
||||
* F12: Save a screenshot to the screenshots folder.
|
||||
* Backspace: Display the in-game menu. (Esc will also display the menu by default, but you can turn that off.)
|
||||
|
||||
* Shift+L: Go to next level.
|
||||
* -: Decrease remaining time by one minute.
|
||||
* +: Increase remaining time by one minute.
|
||||
* R: Resurrect kid.
|
||||
* K: Kill guard.
|
||||
* Shift+I: Flip the screen upside down.
|
||||
* Shift+W: Slow falling.
|
||||
* Shift+S: Restore a lost hit-point. (Like a small red potion.)
|
||||
* Shift+T: Give more hit-points. (Like a big red potion.)
|
||||
|
||||
===
|
||||
|
||||
* H: Look at the room to the left.
|
||||
* J: Look at the room to the right.
|
||||
* U: Look at the room above.
|
||||
* N: Look at the room below.
|
||||
* Ctrl+B: Go back to the room where the prince is. (Undo H,J,U,N.)
|
||||
|
||||
===
|
||||
|
||||
* [: Shift kid 1 pixel to the left.
|
||||
* ]: Shift kid 1 pixel to the right.
|
||||
* T: Toggle display of timer (remaining minutes:seconds:ticks). Also shows the total elapsed ticks during playback.
|
||||
|
||||
@@ -930,8 +930,34 @@ load_intro у оригинала и так начинается с ожидан
|
||||
«ступень на каждый кадр» звал пересчёт сто раз и растягивал fade до
|
||||
десяти с лишним секунд.
|
||||
|
||||
**Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM —
|
||||
в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен
|
||||
КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те,
|
||||
что насос уже прошёл. Это отдельная задача — курсор насоса придётся
|
||||
научить заворачиваться, а загрузчик — держать дистанцию от него.
|
||||
## 8. ПОТОКОВЫЙ ТРЕК: ФИНАЛЬНАЯ ТЕМА (2026-08-26)
|
||||
|
||||
`won` (56) — 115 с, 1,2 МБ, 78 страниц EMM. В память он не влезает ни при
|
||||
каком бюджете, поэтому играется КОЛЬЦОМ из шести страниц (96 КБ = 9 с): насос
|
||||
идёт по кругу, а `pop_music_service` дочитывает файл в те страницы, которые
|
||||
насос уже прошёл.
|
||||
|
||||
**Кто кого догоняет.** Страница звучит 1,5 с, а читается 33 мс — запас
|
||||
сорокакратный. Дистанция считается без отдельных счётчиков: страница ровно
|
||||
128 блоков насоса, поэтому проигранных страниц = (всего блоков − осталось)
|
||||
/ 128. Пока прочитано меньше, чем проиграно плюс размер кольца, в кольце
|
||||
есть свободный слот. Файл читается ПОСЛЕДОВАТЕЛЬНО, без `lseek`.
|
||||
|
||||
**Что пришлось учесть.**
|
||||
|
||||
* Насос заворачивает страницу только при `pop_mus_ring != 0`; конец трека
|
||||
по-прежнему определяет `left`. Обычный трек этой ветки не касается.
|
||||
* Кольцо обязано сниматься при любом обычном запуске (`pop_music_play`,
|
||||
`_stream`, `_load_begin`): иначе следующий трек играет по кругу первых
|
||||
шести страниц — поймано на титрах сразу после победы.
|
||||
* Живые сцены комнаты принцессы открывают CBL сами (`cut_begin`):
|
||||
`pop_ending_show` глушит звук первым действием, и «arrived to princess»
|
||||
(26) не звучал вовсе.
|
||||
* Тема дослушивается до конца (прерывается клавишей), как `while
|
||||
(check_sound_playing() && !key_test_quit())` в seg001:637. У оригинала
|
||||
между титрами и этим ожиданием стоит ввод имени в таблицу рекордов —
|
||||
когда он появится у нас, ожидание переедет за него (задача HOF-ENTRY).
|
||||
|
||||
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
||||
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
||||
за размер кольца — то есть подкачка успевает.
|
||||
|
||||
@@ -3571,3 +3571,174 @@ FIRST_LEVEL`. Рестарт ТОГО ЖЕ уровня (смерть, чекп
|
||||
|
||||
**Проверено в MAME:** после провала на 6-м `pop_current_level` = 7, Kid.room
|
||||
= 16, то есть спецсобытие отработало (старт в 17, переход вниз).
|
||||
|
||||
---
|
||||
|
||||
<a id="cutscene-ltr"></a>
|
||||
## CUTSCENE-LTR. Переходы story должны проявляться слева направо — **ЗАКРЫТ 2026-08-26**
|
||||
|
||||
**Было.** Полосовой переход (`transition_ltr` оригинала) стоял только на
|
||||
«In the absence…»; «свадьба» → титры возникали мгновенно, а промежуточный
|
||||
титульный экран отсутствовал вовсе.
|
||||
|
||||
**Стало.** Переход выделен в общий `pop_screen_present_ltr()` (pop_ui.c) —
|
||||
им пользуются story-переход интро, титры, таблица рекордов и титульная
|
||||
картинка финала. Восстановлен и пропущенный шаг `show_title()`
|
||||
(seg000:2045): между «свадьбой» и титрами показывается композиция с
|
||||
логотипом и Jordan Mechner, тоже полосами, и лишь через 0x78 тиков идут
|
||||
титры.
|
||||
|
||||
**Грабля, которая стоила отладки.** Переход копирует страницу
|
||||
акселератором, а тот читает ОЗУ-копию. `GFX_BANK_SPRITE` (NOSHADOW +
|
||||
TRANSPARENT) в неё не пишет, поэтому переезжал один фон без текста (так вышла
|
||||
пустая таблица рекордов, см. [HOF-ENTRY](TASKS_CLOSED.md#hof-entry)). Всё,
|
||||
что должно проявляться ВМЕСТЕ с фоном, рисовать банком
|
||||
`GFX_BANK_TRANSPARENT` — прозрачность та же, но копия обновляется.
|
||||
|
||||
---
|
||||
|
||||
<a id="pv-music-stall"></a>
|
||||
## PV-MUSIC-STALL. В сцене с принцессой звучала только первая реплика — **ЗАКРЫТ 2026-08-26**
|
||||
|
||||
**Симптом (пользователь).** «Музыка после открытия двери для входа Джафара
|
||||
пропала» — дальше сцена шла в тишине до конца: не звучали ни реплика входа
|
||||
Джафара (m53), ни его ухода (m52).
|
||||
|
||||
**Корень.** Регрессия коммита `4086dde` («убрана промотка кадров»). До него
|
||||
шаг постраничной подкачки стоял БЕЗУСЛОВНО в теле кадра сцены (плюс там же
|
||||
вызывался `pop_music_service`). Тот коммит перенёс шаг «в паузу кадра» —
|
||||
внутрь цикла ожидания тика насоса — и одновременно ввёл правильную защиту от
|
||||
промотки (`if (snd_done > snd_want) snd_want = snd_done`). Вместе это и убило
|
||||
подкачку: пауза в этой сцене почти не выпадает, цикл ожидания выходит сразу,
|
||||
и шаг не вызывается.
|
||||
|
||||
**Замер, который это показал** (адреса статиков `pop_music.c` вычислены от
|
||||
public `_pop_mus_page` по смещениям из `.sym`):
|
||||
|
||||
| | до фикса | после фикса |
|
||||
|---|---|---|
|
||||
| `ld_next` (прочитано страниц m53) | **0** за 20 с | **11 из 11** |
|
||||
| `mus_ready` | 0 | 1 |
|
||||
| что звучит | только m50 | m50 → m53 → m52 |
|
||||
|
||||
**Фикс.** Один безусловный `pop_music_load_step()` в кадре сцены (шаг в паузе
|
||||
оставлен: он бесплатный, когда время есть). Страница стоит 33 мс против
|
||||
кадра сцены в 100-133 мс, а шкала работает по факту прошедшего времени,
|
||||
поэтому сцена от этого не разъезжается.
|
||||
|
||||
**Ловушка отладки, стоившая ложного диагноза.** Пока машина стоит на
|
||||
брейкпоинте, значения в памяти застывают — картина «`busy=0` при `next=3` из
|
||||
20» выглядела как обрыв чтения с ошибкой, хотя это был просто стоп-кадр.
|
||||
Прежде чем толковать значения, проверять `status`: `state=run` или `stop`.
|
||||
|
||||
---
|
||||
|
||||
<a id="final-bankcall"></a>
|
||||
## FINAL-BANKCALL. Финал убивал программу: прямой вызов в чужой банк
|
||||
|
||||
**Симптом (пользователь, 2026-08-27).** Пройденная игра доходила до
|
||||
таблицы рекордов и умирала: программа исчезала, а машина следом вставала
|
||||
намертво (`di; halt` по адресу 0x0000) либо уходила в reset. Из Flex
|
||||
Navigator и из голого DSS — одинаково.
|
||||
|
||||
**Что оказалось не при чём** (проверено, чтобы не искать заново): Flex
|
||||
Navigator, звук и CBL (с выключенным звуком падало так же), потоковое
|
||||
чтение победной темы, второй `open` поверх открытого файла, фейды с
|
||||
клавиатурным диспетчером, чтение архива PV, межбанковый вызов
|
||||
банк 10 → банк 11, переполнение банка (самый полный — BANK8, 15376 из
|
||||
16384), утечка манипуляторов (`_fd_open_count` = 1) и утечка ссылок на
|
||||
IM2-таблицу (`_irq_refs` = 2, как задумано).
|
||||
|
||||
**Как искали.** Бисекция ключом `HOF=0/2/4/5` (пропустить таблицу целиком
|
||||
/ только `hof_load` / без фона / без фейдов) сузила место до
|
||||
`hof_draw_rows`, а точную инструкцию дала ТРАССИРОВКА MAME на узком
|
||||
участке: `trace` включалась брейкпоинтом на входе в `pop_hof_show`
|
||||
(0x44AB) и выключалась на процедуре завершения процесса DSS (0x1E56).
|
||||
`history` для этого не годится — её 250 записей забиваются обработчиком
|
||||
прерываний, пока процессор ползёт по мусору.
|
||||
|
||||
**Корень.**
|
||||
|
||||
```
|
||||
CD42: ld hl,$B8AB ← _gfx_bank
|
||||
CD45: ld (hl),$58 ← gfx_set_bank(GFX_BANK_TRANSPARENT)
|
||||
CD47: call $E503 ← ПРЯМОЙ вызов; по карте 0009E503 = _pop_text_map, банк 9
|
||||
E503: rst $38 ← а в W3 стоит банк 10, там пустой хвост (0xFF)
|
||||
```
|
||||
|
||||
`pop_ui.h` объявлял группу `pop_text_*_mapped` БЕЗ `__banked`. Пока
|
||||
`pop_hof.c` лежал в банке 9 рядом с `pop_ui.c`, прямой `call` был верен.
|
||||
Когда `pop_hof`/`pop_config`/`pop_pal` перенесли в банк 10 ради разгрузки
|
||||
банка 9, тот же `call` стал уходить в пустоту. Процессор полз по 0xFF до
|
||||
0x0000, где ловушка DSS ставит B=0x27 и сворачивает процесс: закрывает его
|
||||
файлы, возвращает контекст (байт 0x1E48: 2 = шелл, 3 = наша программа) и
|
||||
восстанавливает страницы родителя. Именно это выглядело как «W2 подменили
|
||||
у нас под ногами» — на самом деле подмена была уже уборкой трупа.
|
||||
|
||||
**Фикс.** Группа помечена `__banked`; появилась
|
||||
`toolchain/check_bank_calls.py`, встроенная в `app.mk` и валящая сборку
|
||||
(проверено намеренной поломкой). Правило записано в `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
<a id="final-hof-garbage"></a>
|
||||
## FINAL-HOF-GARBAGE. Таблица рекордов заливалась знаками вопроса
|
||||
|
||||
**Симптом.** После фикса FINAL-BANKCALL экран HOF рисовался правильно, но
|
||||
с началом ввода имени строку заливало `?` во всю ширину.
|
||||
|
||||
**Корень — вторая мина того же класса, созданная первым фиксом.** Курсор
|
||||
рисовался литералом:
|
||||
|
||||
```c
|
||||
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_"); /* ___str_3 в _BANK10 */
|
||||
```
|
||||
|
||||
Пока `pop_text_draw_mapped` был в том же банке, литерал был виден. После
|
||||
пометки `__banked` трамплин на время вызова переключает W3 на банк 9 —
|
||||
указатель показывает в чужой банк, функция читает оттуда байты до первого
|
||||
нуля и рисует их знаками вопроса. Сходится и с наблюдением пользователя
|
||||
«сначала рисуется правильно, потом мусор»: строка времени берётся из
|
||||
локального массива (стек, W2 — виден всем), а мусор начинается с мигания
|
||||
курсора.
|
||||
|
||||
**Фикс.** Курсор строится на стеке. Вторая проверка
|
||||
`check_bank_calls.py` предупреждает о таких случаях (проверено: на сборке
|
||||
с возвращённым литералом даёт ровно одно срабатывание).
|
||||
|
||||
---
|
||||
|
||||
<a id="kbd-arrow-phantom"></a>
|
||||
## KBD-ARROW-PHANTOM. Залипшая стрелка вешала ожидания насмерть
|
||||
|
||||
**Симптом.** После игры (особенно 14-й уровень — он почти весь проходится
|
||||
удержанием Left) текстовые экраны переставали прерываться, а «новая игра»
|
||||
вставала намертво: `pop_new_game_load` начинается с
|
||||
`while (kbd_raw_any_down())`. При этом ввод имени в HOF работал —
|
||||
он опрашивает конкретные коды, а не всю карту.
|
||||
|
||||
**Корень (подтверждён дважды: 0x72 24.08, 0x6B 27.08).** Стрелка идёт по
|
||||
проводу как `E0 6B`. При переполнении 3-байтового FIFO SIO теряется
|
||||
префикс, make садится в PLAIN-половину карты как код нумпада (0x6B = KP4),
|
||||
а break приходит уже с префиксом и снимает бит в EXT-половине. PLAIN-бит
|
||||
остаётся зажатым навсегда, `kbd_raw_any_down()` отвечает «да» вечно.
|
||||
Доказано вмешательством: запись 0 в этот байт отладчиком мгновенно
|
||||
перевела состояние 5 (LEVEL_LOAD) → 6 (PLAYING).
|
||||
|
||||
**Фикс.** `kbd_raw_keypad_as_ext()` в libc: `kbd_raw_sync()` переносит
|
||||
биты голых кодов нумпада в EXT-половину и гасит в PLAIN — make и break
|
||||
начинают работать с одним битом. В PS/2 это и есть одни и те же
|
||||
физические клавиши, поэтому нумпад заодно стал управлением (7/8/9, 4/6,
|
||||
2 и 5 — вниз). НЕ лечит обратный случай (префикс потерян у break) — там
|
||||
бит стоит уже в EXT и выглядит как реально зажатая клавиша.
|
||||
|
||||
---
|
||||
|
||||
<a id="f10-gameplay"></a>
|
||||
## F10-GAMEPLAY. Выход по F10 не работал в игре
|
||||
|
||||
F10 проверяется в idle-хуке, а хук висит на `gfx_wait_vsync`. Когда
|
||||
игровой цикл перевели на пейсинг по лучу, он стал ждать через
|
||||
`pop_wait_edge`, который звал `kbd_raw_poll` напрямую — хук не вызывался,
|
||||
и F10 в геймплее умер (в заставках и меню работал). Фикс: `pop_wait_edge`
|
||||
зовёт тот же `pop_idle`.
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
||||
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
|
||||
| [PAL-DUNGEON-STALE](#pal-dungeon-stale) | переход 3→4 (подземелье→дворец): остаётся палитра подземелья | палитра/fade | открыт: КОРЕНЬ ЯСЕН — fade_in восстанавливает из протухшего снимка |
|
||||
| [CUTSCENE-LTR](#cutscene-ltr) | `Marry Jaffar` → title → credits должны проявляться полосами слева направо | заставка | открыт: зафиксировано 2026-08-24, взять после текущего цикла анимации PV |
|
||||
| [BUG-CORPSE-AIR](#bug-corpse-air) | труп Кида висит в воздухе, когда убившая его плита улетела вниз | физика | открыт |
|
||||
| [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) |
|
||||
| [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** |
|
||||
@@ -162,17 +161,16 @@ Rx-overrun не срабатывала. Гашение бита отладчи
|
||||
---
|
||||
|
||||
<a id="cutscene-ltr"></a>
|
||||
## CUTSCENE-LTR. Переходы story через экран TITLE должны проявляться слева направо
|
||||
## CUTSCENE-LTR. Переходы story через экран TITLE — ЗАКРЫТ 2026-08-26
|
||||
|
||||
**Наблюдение пользователя (2026-08-24).** В SDLPoP переходы `Marry Jaffar` →
|
||||
экран с надписью *Prince of Persia* → credits выполнены тем же приёмом, что и
|
||||
`In the absence…`: следующая композиция появляется вертикальными полосами
|
||||
слева направо. Сейчас это последовательность статичных сцен без этого
|
||||
перехода.
|
||||
Разбор и что именно добавилось — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#cutscene-ltr).
|
||||
|
||||
**Очередь.** Не прерывать ради него текущую работу над палитрой и анимацией
|
||||
Princess/Jaffar. Когда будет взят, выделить общий helper полосового перехода
|
||||
и проверить все три story-маршрута, включая пропуск клавишей.
|
||||
---
|
||||
|
||||
<a id="pv-music-stall"></a>
|
||||
## PV-MUSIC-STALL. В сцене с принцессой звучала только первая реплика — ЗАКРЫТ 2026-08-26
|
||||
|
||||
Разбор — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#pv-music-stall).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
||||
EXAMPLE := roomtest
|
||||
HDD_IMG := $(CURDIR)/build/hdd/roomtest.chd
|
||||
|
||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||
@@ -88,7 +90,7 @@ EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
# MAME = время фазы в растровых строках.
|
||||
PROF ?= 1
|
||||
PROF ?= 0
|
||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
|
||||
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1.
|
||||
@@ -214,7 +216,7 @@ SND_DATA := $(SND_ARC)
|
||||
# игра 24 25 28 29 30 32 33 37 39 41 43
|
||||
# Нет только 56 (won, 115 с / 1,2 МБ) — финалу нужен стриминг с диска.
|
||||
MUS_DIR := $(CURDIR)/../poc/res/music
|
||||
MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55
|
||||
MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55 56
|
||||
MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin)
|
||||
|
||||
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
|
||||
@@ -235,7 +237,7 @@ TITLE_DATA := $(TITLE_DIR)/title.pal $(TITLE_ARC)
|
||||
# intro есть ещё чистый фон `b0_*` и шесть <=16K actor-атласов: они остаются
|
||||
# в EMM на время сцены, а не перечитывают полный 320x200 кадр с HDD.
|
||||
PV_DIR := $(CURDIR)/../poc/res/pv
|
||||
PV_STORY := 0 1 2 3 4
|
||||
PV_STORY := 0 1 2 3 4 5
|
||||
PV_SCENES := 0 1 2 3 4 5 6 7
|
||||
PV_PARTS := 0 1 2 3 4
|
||||
PV_ANIM := $(foreach p,$(PV_PARTS),$(PV_DIR)/b0_$(p).atl) \
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
# Точка входа для следующей сессии (обновлено 2026-08-12, ночь)
|
||||
|
||||
Файл для старта с чистого контекста: где всё стоит, что делать первым, какие
|
||||
грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам:
|
||||
`TASKS_OPEN.md` (доска), `BUGS_OPEN.md` (открытые баги), `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. СЛЕДУЮЩАЯ ЗАДАЧА: smoke-прогон уровня 9 и регресс 1-8
|
||||
|
||||
Переворот (зелье инверсии) **доведён и принят пользователем 2026-08-12**:
|
||||
фон, персонажи, клинок, полоса HP, факелы, окклюзия передних граней и
|
||||
переходы между комнатами работают зеркально; чит `U` и зелёное зелье идут
|
||||
одним путём. Осталось пройти сам уровень 9 живьём и убедиться, что на
|
||||
уровнях 1-8 ничего не поехало (весь слой фона теперь спрашивает `pop_upside`).
|
||||
|
||||
Что смотреть на прогоне в первую очередь:
|
||||
|
||||
- **вис на кромке и спуск в перевёрнутом виде** — это новый срез `clip_char`
|
||||
(он режет кадр СНИЗУ вместо верха); тело должно уходить за кладку, а не
|
||||
ложиться поверх неё;
|
||||
- **бой со стражем перевёрнутым** — у соперника свой зеркальный набор
|
||||
(`g*_v.atl`), а тень рисуется атласами Кида, отдельных файлов ей не надо;
|
||||
- **зелье в другой комнате** — переворот перерисовывает не комнату, а чистит
|
||||
факелы и отражает страницу; проверить, что после смены комнаты и возврата
|
||||
фон остаётся чистым.
|
||||
|
||||
Открытый риск (пока не проявился, но проверить осознанно): имена зеркальных
|
||||
атласов `kid10_v.atl`…`kid27_v.atl` — **9 символов до точки, а DSS работает
|
||||
по 8.3**. Если часть кадров Кида в перевёрнутом виде рисуется обычными
|
||||
(неперевёрнутыми) — это оно; лечится переименованием в `pop_pack_kid.py`
|
||||
(например `k10v.atl`) плюс те же имена в `pop_kboot.c`.
|
||||
|
||||
Дальше по плану — уровни 10/11 (по анализу нового в них нет) и
|
||||
[MEM-COLD2](TASKS_OPEN.md#mem-cold2) п.3, если снова понадобится место.
|
||||
|
||||
Ощущенческий хвост прежний — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing),
|
||||
разбирается после готовности всех уровней (решение пользователя).
|
||||
|
||||
## 2. Состояние репозитория
|
||||
|
||||
Всё закоммичено, `main` чист. `tests-host`: **6 наборов** проходят
|
||||
(`[char] 65`, `[geom] 3144`, `[grab] 55`, `[mouse] 17`, `[phys] 1727`,
|
||||
`[wall] 1`).
|
||||
|
||||
Коммиты 2026-08-12 (сверху — свежие):
|
||||
|
||||
| хеш | что |
|
||||
|---|---|
|
||||
| `7f5c99b` | чистка факелов при перевороте — в СТАРОЙ системе координат |
|
||||
| `c9aa6d9` | переворот: не перерисовывать комнату, стирать только запечённое пламя |
|
||||
| `948d8f0` | переворот: перерисовка вместо отражения, переключение на границе кадра, `clip_char` |
|
||||
| `5b6664f` | зеркальные кадры персонажей — готовыми файлами `*_v.atl` |
|
||||
| `c4dd2c7` | отсев тайлов по fore-окну — в ЛОГИЧЕСКИХ координатах |
|
||||
| `7551906` | L9-INVERT II.4: персонажи рисуются перевёрнутыми |
|
||||
| `055d6d7` | чтение файлов нельзя выносить в банк (код банка сам живёт в W3) |
|
||||
| `147cb18` | резидент: `pop_kid`/`pop_guard`/`main` расколоты по частоте вызова |
|
||||
| `5d31ed0` | резидент: `pop_redraw` + холодная половина `pop_level` в банки |
|
||||
| `f541c0a` | клип поля при перевороте (мусор в бортах) |
|
||||
|
||||
Уровни 1-8 проходятся, уровень 9 ждёт прогона.
|
||||
|
||||
## 3. Раскладка кода после разгрузки резидента
|
||||
|
||||
Куча выросла **902 → 5795 Б** (`_CODE` 25 628 → 20 569). Кандидатов выбирали
|
||||
не по размеру, а по тому, КТО зовёт: если единственный вызывающий уже в
|
||||
банке, код едет к нему и трамплин не появляется вовсе.
|
||||
|
||||
| банк | модули |
|
||||
|---|---|
|
||||
| 1 | `guards.c` |
|
||||
| 2 | `pop_bg.c` |
|
||||
| 3 | `pop_map.c` |
|
||||
| 4 | `pop_cdraw.c` + **`pop_kdraw.c`** (клинок, блит по id — прямые вызовы) |
|
||||
| 5 | `pop_ctrl.c` |
|
||||
| 6 | `pop_trob.c` |
|
||||
| 7 | `pop_room.c` + **`pop_redraw.c`** |
|
||||
| 8 | `roomtest_cold.c` + **`pop_level_cold.c`** + **`pop_kboot.c`** + **`pop_guard_cold.c`** |
|
||||
|
||||
Внутренние контракты: `_pop_level.h`, `_pop_kid.h`, `_pop_kdraw.h`. В них же
|
||||
записаны два правила, которые линкер НЕ проверяет:
|
||||
|
||||
1. **Банковый модуль не маппит W3** — он в этом окне и исполняется (через W0
|
||||
можно). Поэтому `pop_level_read_file` и `pop_kid_data_load` остались в
|
||||
резиденте: первый же вызов из банка улетал в halt по мусору.
|
||||
2. **Прямые вызовы между модулями одного банка** корректны только пока они
|
||||
действительно в одном банке; меняешь раскладку в Makefile — проверь пары.
|
||||
|
||||
## 4. Переворот (L9-INVERT) — как он устроен
|
||||
|
||||
- **Флаги:** `pop_upside_want` (пишут зелье, смерть Кида, чит `U`) и
|
||||
`pop_upside` (читают все слои). Переключение — ОДНО место, начало кадра,
|
||||
вместе с `pop_flip_screen`. Оригиналу этого не нужно (он рисует в offscreen
|
||||
неперевёрнутым и зеркалит на выводе) — расхождение записано в
|
||||
`../docs/impl_diff.md`.
|
||||
- **Сам переворот:** чистка запечённого пламени (`pop_torch_wipe`, 0..4
|
||||
тайла) + отражение страницы акселератором + прямая копия во вторую.
|
||||
**904 247 тактов = 2.1 кадра.** Вариант с полной перерисовкой комнаты
|
||||
(как `redraw_screen` в оригинале) стоил 6 245 477 = 14.5 кадра — отвергнут.
|
||||
- **Спрайты персонажей:** column-major, вертикальное зеркало для них не
|
||||
бесплатно, поэтому второй набор файлов делает упаковщик (`vflip_cols` в
|
||||
`pop_pack_kid.py`). Загрузка одной страницы с диска — 970 003 такта,
|
||||
разворот той же страницы в рантайме — 12 645 582; поэтому файлы.
|
||||
`pop_vflip_load_all()` идемпотентна и зовётся откуда угодно (сейчас — старт
|
||||
и смена уровня, если это `POP_UPSIDE_LEVEL`; задел под загрузку в интро).
|
||||
- **Координаты:** у окна fore-клипа ДВЕ пары границ — экранная
|
||||
(`pop_t_fclip_y0/y1`, по ней режется блит) и логическая
|
||||
(`pop_t_fclip_ly0/ly1`, по ней идут ВСЕ отсевы тайлов). Смешать их — и
|
||||
передние грани перестают перекрывать персонажа.
|
||||
|
||||
## 5. Состояние окружения
|
||||
|
||||
- **MAME запущена с мостом**, на HDD — roomtest с `LEVEL=9`. Стартовый
|
||||
уровень теперь **параметр сборки**: `make LEVEL=9` (дефолт 9; для «настоящей»
|
||||
игры — `LEVEL=1`), константа объявлена в `pop_tune.h`.
|
||||
- **ПРАВИЛО ПОЛЬЗОВАТЕЛЯ: перед сборкой образа — `make clean`, потом `make`,
|
||||
потом `make hdd`** (memory `mame_build_clean_rule`). После пересборки
|
||||
образа MAME **обязана** пройти полный рестарт (memory
|
||||
`mame_hdd_rebuild_restart`): остановка через `exit` в дебаггере, затем
|
||||
`run_bridge.sh`.
|
||||
- **Тайминги моста** (правило пользователя, длиннее не ждать; они же в
|
||||
`docs/mame-autotest.md` §10): старт `run_bridge.sh` → **6 с** → `go` →
|
||||
**8 с** → `keyseq d:{ENTER}` + `keyseq roomtest{ENTER}` → **5 с** →
|
||||
программа работает. Весь цикл перезапуска ~20 секунд.
|
||||
- **MCP-инструменты `mame-z80` работают** (в отличие от прошлой сессии):
|
||||
`press_key`, `screenshot`, `read_logical_memory`, `debugger_command`.
|
||||
Профилирование — `bpset <addr>` + `print totalcycles`, затем `out` и снова
|
||||
`print totalcycles`; кадр = 430 000 тактов.
|
||||
- Адреса состояния смещаются после КАЖДОЙ пересборки — брать из
|
||||
`.sprinter-cc-roomtest/roomtest.map` заново. Банковые функции лежат по
|
||||
`0x0008XXXX`; для брейкпоинта брать младшие 16 бит (`0xC912` и т.п.).
|
||||
- **SDLPoP собран с отладочной информацией** (`-O0 -g3`).
|
||||
- **НЕ ПРИБРАНО:** в `SDLPoP/src/seg008.c` диагностический `fprintf` с меткой
|
||||
`DBGMIRROR` в начале `add_objtable`.
|
||||
|
||||
## 6. Остальное открытое
|
||||
|
||||
- **[FORE-DUP](BUGS_OPEN.md#fore-dup)** — передний слой тайла рисуется дважды
|
||||
при перекрытии объектов. Картинку не портит, тратит такты. Сначала
|
||||
замерить, потом чинить.
|
||||
- **TORCH-ANIM-RIGHT** — под запечённым пламенем могут застыть пики/меч/зелье
|
||||
справа от факела.
|
||||
- **[GATE-FORE-KID](BUGS_OPEN.md#gate-fore-kid)** — Кид в проёме ворот виден
|
||||
поверх решётки; портирован только шовный случай.
|
||||
- Долгие: `BUG-SPIKE-1`, `BUG-CHOMP-JUMP-1`, `L3-COLOR`, `L1-SPEED`, `TUNE-1`.
|
||||
- **Вид тени (OR+XOR) отложен** до готовности всех уровней —
|
||||
[`../docs/shadow_render.md`](../docs/shadow_render.md).
|
||||
|
||||
## 7. Грабли, собранные 2026-08-12 (вечер/ночь)
|
||||
|
||||
- **`return 1;` из ветки у SDCC 4.5 может не положить единицу в A** — функция
|
||||
возвращает мусор. Годами маскировался (в A случайно оставался ненулевой
|
||||
байт) и вылез ровно на `__banked`. Репро — `docs/bugs/sdcc-z80-ret-const-lost/`,
|
||||
обход — один выход через переменную. Аудит всех `.asm` roomtest: других
|
||||
мест нет.
|
||||
- **Банковый модуль не может маппить W3** (см. §3). Симптом — halt по мусору
|
||||
в 0xC4xx на первом же вызове.
|
||||
- **Логические и экранные координаты при перевороте — разные системы.** Три
|
||||
бага подряд из одного корня: отсев тайлов по fore-окну, `clip_char`, чистка
|
||||
факелов. Правило: всё, что считается ИЗ РЯДА комнаты (`63*row + …`), —
|
||||
логическое; всё, что режет блит, — экранное.
|
||||
- **Пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию** (heal'а у него нет, следующий кадр
|
||||
накрывает предыдущий непрозрачно). Любое отражение/копирование страницы
|
||||
тащит его с собой — перед такими операциями звать `pop_torch_wipe`.
|
||||
- **`pop_upside` переключать в середине кадра нельзя**: остаток кадра
|
||||
рисуется зеркально поверх ещё неперевёрнутой картинки, и запечённые
|
||||
транзиенты остаются в комнате навсегда.
|
||||
- **Перенос константы между файлами — потерянное значение.** При выносе
|
||||
`FIRST_LEVEL` в `pop_tune.h` записал дефолт 1, и игра молча стартовала с
|
||||
первого уровня вместо девятого; заметил пользователь. Теперь это параметр
|
||||
сборки (`make LEVEL=N`).
|
||||
- **`make -C tests-host` не пересобирает по изменению заголовков** — при
|
||||
правке прототипов (особенно `__banked`) обязателен `make -C tests-host
|
||||
clean`, иначе стаб и вызывающий разъезжаются по ABI и тест врёт.
|
||||
|
||||
## Грабли 2026-08-10/11
|
||||
|
||||
- **Не оценивать железо по своей же memory-заметке.** Дважды поправлял
|
||||
пользователь: accel умеет не только копирование (есть AND/OR/XOR) и умеет
|
||||
работать с обычной RAM, а не только с видео (`docs/part2/accelerator_doc.txt`:
|
||||
«supports data blocks transfer of RAM and Video-RAM», не поддерживает только
|
||||
ROM и FastRAM). Проверять по драйверу MAME и докам, а не по памяти.
|
||||
- **lldb через FIFO — плохая идея.** Работает надёжно: `fprintf(stderr, …)`
|
||||
прямо в SDLPoP, пересборка (секунды) и чтение stdout.
|
||||
- **Доска отставала от кода на три задачи.** Перед планированием — сверять
|
||||
`TASKS_OPEN.md` с кодом грепом, а не верить записи.
|
||||
- Диапазон `obj_x` = **−416..695** (посчитан из `kid_data.bin`).
|
||||
@@ -1320,3 +1320,58 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o
|
||||
**Остаток на следующую сессию:** влетая в комнату, Кид должен уметь
|
||||
зацепиться за край пола, мимо которого пролетает — этой механики у нас,
|
||||
похоже, нет (см. `NEXT_SESSION.md`, п. 1).
|
||||
|
||||
---
|
||||
|
||||
<a id="hof-entry"></a>
|
||||
## HOF-ENTRY. Таблица рекордов — **СДЕЛАНО И ПРОВЕРЕНО В MAME 2026-08-26**
|
||||
|
||||
Порт двух показов оригинала. `pop_hof.c` был написан раньше, но никуда не
|
||||
вызывался: после финала автомат уходил мимо него в title, а на титрах экран
|
||||
получался ЧЁРНЫМ.
|
||||
|
||||
**Где это в оригинале.** `show_title()` (seg000:2060) показывает
|
||||
сохранённую таблицу между credits и attract-demo — 240 тиков, переходом
|
||||
слева направо, без fade. `end_sequence()` (seg001:5F1) после HAIL проявляет
|
||||
титульную картинку (240 тиков), при подходящем результате гасит её, показывает
|
||||
таблицу с золотой полосой ввода, принимает имя, ждёт 120 тиков, снова
|
||||
проявляет титульную картинку и лишь потом дослушивает победную тему
|
||||
(`while (check_sound_playing() && !key_test_quit())`, seg001:637).
|
||||
|
||||
**Два корня, из-за которых экран был пуст.**
|
||||
|
||||
1. `story.pal` — 256 записей, и запись 0x3F (цвет глифов шрифта,
|
||||
`POP_FONT_COLOR`) в ней ЧЁРНАЯ. Текст рисовался, но был не виден.
|
||||
Теперь генератор кладёт в 0x3F золотой 0xB7 оригинала.
|
||||
2. Полосовой переход копирует страницу АКСЕЛЕРАТОРОМ, а тот читает ОЗУ-копию
|
||||
(`gfx_copy_page`: «копируется чистый фон без спрайтов»). `GFX_BANK_SPRITE`
|
||||
— это NOSHADOW + TRANSPARENT, то есть в копию он не пишет, и на титрах
|
||||
переезжал один фон без строк. Лечится банком `GFX_BANK_TRANSPARENT`
|
||||
(0x58): та же прозрачность 0xFF, но с записью в копию — страница
|
||||
собирается ЦЕЛИКОМ до показа, как offscreen оригинала.
|
||||
|
||||
**Что появилось попутно.**
|
||||
|
||||
- `s5` в архиве PV — фон таблицы: рамка story + логотип PRINCE OF PERSIA на
|
||||
y=24 (`HOF_POP` оригинала — тот же спрайт res54, что и в титрах).
|
||||
- Отдельный индекс палитры под фон текстовой рамки (`POP_PAL_STORY_BG`, 16).
|
||||
`load_title_images(bgcolor)` красит его в #100060 на титрах и в #800000 в
|
||||
финале; раньше фон был ремаплен в индекс 9, а тот занят самой титульной
|
||||
картинкой (8265 пикселей), и подменить его было нельзя.
|
||||
- Второй набор глифов в `font.atl` цветом `POP_FONT_DARK_COLOR` (0x3E).
|
||||
У SDLPoP шрифт — маска, и `show_hof_text` рисует текст дважды разным
|
||||
цветом; у нас цвет запечён в пиксели, поэтому «другой цвет» = другой набор.
|
||||
Каталог SPA1 держит счётчик картинок в одном байте, поэтому тёмный набор
|
||||
обрезан по '_' (32..95): 190 + 64 = 254 из 255 возможных.
|
||||
- Общий `pop_screen_present_ltr()` в `pop_ui.c` — им теперь пользуются и
|
||||
story-переход интро, и таблица, и титульная картинка финала
|
||||
([CUTSCENE-LTR](BUGS_CLOSED.md#cutscene-ltr)).
|
||||
|
||||
**Формат `POP.HOF`** — 6 записей (как `MAX_HOF_COUNT`), имя до 15 символов,
|
||||
XOR+инверсия в хвосте; версия 2. Проверено на образе: после ввода имени файл
|
||||
128 Б, `PHOF`, запись читается обратно и показывается на титрах.
|
||||
|
||||
**Что проверено в MAME** (сборка `LEVEL=14`, вход в комнату 5 читом обхода
|
||||
комнат): цепочка HAIL → титульная картинка → таблица с полосой ввода и
|
||||
временем справа → ввод имени → титульная картинка; на следующем запуске
|
||||
таблица показывается между credits и demo.
|
||||
|
||||
@@ -78,7 +78,10 @@
|
||||
это максимум из встречавшихся; 12/22/24 — skill 2/3/3).
|
||||
|
||||
<a id="l9-invert"></a>
|
||||
### L9-INVERT. Зелье ПЕРЕВОРОТА (уровень 9, комнаты 7 и 10)
|
||||
### L9-INVERT. Зелье ПЕРЕВОРОТА — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Подробный план реализации — [`../docs/l9_invert_plan.md`](../docs/l9_invert_plan.md)**
|
||||
> (задачи libbgi, задачи приложения, порядок работ, риски, открытые вопросы).
|
||||
@@ -379,7 +382,10 @@ x = 106; разбор — [`../docs/perf_l11_room15.md`](../docs/perf_l11_room15
|
||||
(6 126 тактов фиксированной накладной на любой блит, независимо от размера).
|
||||
|
||||
|
||||
### <a id="l12-shadow"></a>L12-SHADOW. Уровень 12: встреча с тенью, бой, слияние
|
||||
### <a id="l12-shadow"></a>L12-SHADOW. Уровень 12: тень, бой, слияние — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Статус 2026-08-13: код написан, хост-тесты зелёные (45 проверок,
|
||||
> `tests-host/t_shadow.c`), ЖИВОЙ ПРОВЕРКИ В MAME ЕЩЁ НЕ БЫЛО.** Для неё
|
||||
@@ -497,7 +503,10 @@ POP_RD_LOOSE, 1)` — полную перерисовку тайла, десят
|
||||
берётся вместе с остальной шлифовкой переходов.
|
||||
|
||||
<a id="l13-jaffar"></a>
|
||||
### L13-JAFFAR. Уровень 13: Джафар, победа, выход, падающая гряда
|
||||
### L13-JAFFAR. Уровень 13: Джафар, победа, выход — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Статус 2026-08-13: код написан, хост-тесты зелёные (44 проверки,
|
||||
> `tests-host/t_jaffar.c`), ЖИВОЙ ПРОВЕРКИ В MAME ЕЩЁ НЕ БЫЛО.**
|
||||
@@ -550,7 +559,10 @@ POP_RD_LOOSE, 1)` — полную перерисовку тайла, десят
|
||||
6. комната 23 — уровень молча становится 13-м, HP НЕ сбрасывается.
|
||||
|
||||
|
||||
### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1)
|
||||
### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все
|
||||
> шесть шагов ниже плюс синие пузырьки зелья «−HP» (тип 5) — они всплыли по
|
||||
@@ -1188,7 +1200,15 @@ N навык M» — отдельная надобность, если пона
|
||||
|
||||
**Оговорка по памяти** — та же, что в TUNE-1: парсер холодный, в банк.
|
||||
|
||||
### <a id="snd"></a>SND. Звук — эффекты через CBL, музыка на AY
|
||||
### <a id="snd"></a>SND. Звук — эффекты через CBL, музыка на AY — ЗАКРЫТА ИНАЧЕ 2026-08-26
|
||||
|
||||
> **Решение пересмотрено.** Музыка пошла НЕ на AY, а тем же PCM через CBL:
|
||||
> записи DOS-версии укладываются в формат железа без единого преобразования
|
||||
> в рантайме (`../docs/sound_plan.md` §4). Озвучены заставка, сцены между
|
||||
> уровнями и игровые джинглы — 21 трек. Единственный неозвученный кусок —
|
||||
> финал `won`, см. [MUS-WON](#mus-won).
|
||||
|
||||
<details><summary>исходная постановка (путь A на AY)</summary>
|
||||
|
||||
Полный разбор с замерами — [`../docs/sound_plan.md`](../docs/sound_plan.md).
|
||||
Коротко, почему это оказалось дешевле, чем выглядело:
|
||||
@@ -1206,6 +1226,8 @@ N навык M» — отдельная надобность, если пона
|
||||
|
||||
Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке.
|
||||
|
||||
</details>
|
||||
|
||||
### <a id="mob-spr-emm"></a>MOB-SPR-EMM. Композит падающего куска — в EMM (1284 Б W2)
|
||||
|
||||
`mob_spr` (`pop_room.c`) — самый крупный буфер в W2: 4 + 64*20 байт под
|
||||
@@ -1270,7 +1292,23 @@ N навык M» — отдельная надобность, если пона
|
||||
нужны в разное время: `kid.pal` — каждому уровню, заставочные — только
|
||||
заставке.
|
||||
|
||||
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг с диска
|
||||
### <a id="hof-entry"></a>HOF-ENTRY. Таблица рекордов — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> **Сделано.** Оба показа оригинала: между credits и attract-demo и хвостом
|
||||
> `end_sequence` (с вводом имени, паузой 120 тиков и возвратом на титульную
|
||||
> картинку). Экран — рамка story с логотипом, время в формате `59:43`,
|
||||
> строки с тенью, мигающий курсор. Ожидание конца победной темы переехало
|
||||
> за таблицу, как в оригинале. Протокол и грабли —
|
||||
> [`TASKS_CLOSED.md`](TASKS_CLOSED.md#hof-entry).
|
||||
|
||||
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> **Сделано.** Кольцо из шести страниц (96 КБ = 9 с), насос заворачивается
|
||||
> по `pop_mus_ring`, подкачка в `pop_music_service` по дистанции
|
||||
> «проиграно / прочитано». Разбор и грабли — `../docs/sound_plan.md` §8.
|
||||
> Проверено в MAME на сборке `LEVEL=14`.
|
||||
|
||||
<details><summary>исходная постановка</summary>
|
||||
|
||||
Трек 56 (`won`, 115 с) — это 1,2 МБ, 78 страниц EMM. Ни в `POP_MUS_PAGES`
|
||||
(20), ни в разумный бюджет памяти он не влезает, поэтому единственный
|
||||
@@ -1286,7 +1324,24 @@ N навык M» — отдельная надобность, если пона
|
||||
**Заодно.** Тот же механизм снимает ограничение `POP_MUS_PAGES` со всех
|
||||
треков: сейчас каждый должен помещаться в память целиком.
|
||||
|
||||
### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1, как звук и музыка
|
||||
</details>
|
||||
|
||||
### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1 — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> **Сделано.** Восемь архивов: kid (58 атласов), pv (78), shadow (32), bg
|
||||
> (25), title (20), guard (10), vizier (5), skel (4). На образе было 232
|
||||
> файла, стало 8 архивов плюс шесть палитр, `kid.ani` и таблицы уровней.
|
||||
> Индексы печатает упаковщик (`ARC_<ГРУППА>_<ФАЙЛ>`), серии код проверяет
|
||||
> статически. `atlas_load` разделён: `atlas_attach` принимает уже
|
||||
> прочитанную страницу, поэтому чтение из архива не дублирует проверку
|
||||
> магии. Грабля, стоившая отладки: имя архива обязано лежать в том же
|
||||
> банке, что и код, который его читает, — строковый литерал из чужого банка
|
||||
> после переключения W3 показывает на другие данные; поэтому наружу отдаётся
|
||||
> НОМЕР архива (`pop_arc_open_id`).
|
||||
>
|
||||
> Остаток на будущее — палитры, см. [PAL-ARC](#pal-arc).
|
||||
|
||||
<details><summary>исходная постановка</summary>
|
||||
|
||||
**Что сделано.** Формат PBA1 и читатель готовы и работают на звуке:
|
||||
`toolchain/pop_pack_arc.py` собирает архив (заголовок 512 Б, записи по 4
|
||||
@@ -1308,6 +1363,8 @@ N навык M» — отдельная надобность, если пона
|
||||
меряется по MAME), число файлов на образе, и что каждая сцена по-прежнему
|
||||
собирается при неполном диске (пути ошибок в `atlas_load`).
|
||||
|
||||
</details>
|
||||
|
||||
### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2
|
||||
|
||||
`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча
|
||||
|
||||
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 413 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
@@ -1,6 +0,0 @@
|
||||
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
|
||||
|
||||
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
|
||||
|
||||
/// Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
|
||||
/// но двери которые она должна открыть остаются закрытыми
|
||||
@@ -107,13 +107,10 @@ uint8_t pop_app_dispatch(pop_app_event_t event) __banked
|
||||
|
||||
case POP_APP_ENDING:
|
||||
if (event == POP_APP_EV_ENDING_FINISHED) {
|
||||
/* ВРЕМЕННО в обход Hall of Fame: экран HOF написан (pop_hof.c),
|
||||
* но после финала показывает чёрное вместо таблицы, и упереться
|
||||
* в него сразу после победы хуже, чем не показать вовсе.
|
||||
* Решение пользователя 2026-08-24: пока уходим прямо в начало,
|
||||
* HOF чинится отдельно (BUGS_OPEN.md#hof-after-ending).
|
||||
* Из title таблица по-прежнему доступна (pop_hof_show_title). */
|
||||
next = POP_APP_TITLE; accepted = 1;
|
||||
/* Таблица рекордов — ХВОСТ end_sequence, а не отдельный экран
|
||||
* по желанию: она же дослушивает победную тему и гасит экран
|
||||
* перед возвратом в начало (seg001:5F1). */
|
||||
next = POP_APP_HALL_OF_FAME; accepted = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_FONT_COLOR 0x3F
|
||||
#define POP_FONT_DARK_COLOR 0x3E
|
||||
#define POP_FONT_FIRST 32
|
||||
#define POP_FONT_LAST 126
|
||||
#define POP_FONT_SMALL_BASE 0
|
||||
@@ -17,7 +18,12 @@
|
||||
#define POP_FONT_BIG_BASE 95
|
||||
#define POP_FONT_BIG_ASCENT 7
|
||||
#define POP_FONT_BIG_SPACE 1
|
||||
/* Тот же КРУПНЫЙ шрифт цветом POP_FONT_DARK_COLOR: тень и текст на светлом.
|
||||
* Набор обрезан по '_' — им пишется только таблица рекордов. */
|
||||
#define POP_FONT_DARK_BASE 190
|
||||
#define POP_FONT_DARK_FIRST 32
|
||||
#define POP_FONT_DARK_LAST 95
|
||||
#define POP_FONT_SNAPSHOT_OFF 0x3C00
|
||||
#define POP_FONT_ATLAS_SIZE 7667
|
||||
#define POP_FONT_ATLAS_SIZE 10841
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
/*
|
||||
* pop_hof.c — persistent Hall of Fame, банк 9 (FG12).
|
||||
*
|
||||
* Порт двух показов оригинала: между credits и attract-demo (seg000:2060)
|
||||
* и после победы, с вводом имени (end_sequence, seg001:5F1). Экран тот
|
||||
* же самый — рамка story с логотипом PRINCE OF PERSIA (STORY_FRAME +
|
||||
* HOF_POP), поэтому фон приходит готовой композицией из архива PV, а
|
||||
* палитру выбирает вызывающая сцена: обе фазы идут на story.pal, и
|
||||
* отличаются только цветом фона рамки (см. pop_pal_story_load).
|
||||
*
|
||||
* POP.HOF — небольшой самодостаточный формат: magic, version, fixed records
|
||||
* и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры:
|
||||
* decode просто начинает с пустой таблицы. Ввод — raw-клавиатура, как весь
|
||||
@@ -19,18 +26,44 @@
|
||||
#include "pop_timer.h"
|
||||
#include "pop_pal.h"
|
||||
#include "pop_ui.h"
|
||||
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
|
||||
#include "pop_intro.h" /* фон экрана: рамка story + логотип */
|
||||
#include "pop_title.h" /* титульная картинка после ввода имени */
|
||||
#include "pop_music.h" /* подкачка потоковой темы победы */
|
||||
#include "pop_hof.h"
|
||||
|
||||
#define HOF_COUNT 5
|
||||
#define HOF_NAME_SIZE 10 /* девять букв + '\0' */
|
||||
#define HOF_REC_SIZE 14
|
||||
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
||||
#define HOF_COUNT 6
|
||||
#define HOF_NAME_SIZE 16 /* пятнадцать букв + '\0' */
|
||||
#define HOF_REC_SIZE 20
|
||||
#define HOF_HEADER_SIZE 6
|
||||
#define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2)
|
||||
#define HOF_BASE 0x100
|
||||
#define HOF_VERSION 1
|
||||
#define HOF_FADE 4
|
||||
#define HOF_VERSION 2
|
||||
#define HOF_FADE POP_FADE_FRAMES
|
||||
#define HOF_NO_ROW 0xFF
|
||||
|
||||
/* Геометрия hof_rects[] (seg001:0D92) в координатах 320x200: строка i
|
||||
* занимает y = 84 + 14*i .. +12, имя от x=72, время по правому краю 248.
|
||||
* Полоса ввода — та же строка, сдвинутая offset4_rect_add(-4,-1,-40,-1). */
|
||||
#define HOF_ROW_TOP 84
|
||||
#define HOF_ROW_STEP 14
|
||||
#define HOF_ROW_H 12
|
||||
#define HOF_LEFT 72
|
||||
#define HOF_RIGHT 248
|
||||
#define HOF_EDIT_LEFT (HOF_LEFT - 4)
|
||||
#define HOF_EDIT_RIGHT (HOF_RIGHT - 40)
|
||||
/* Крупный шрифт: глиф 7 строк, строка 12 — текст центрируется по вертикали.
|
||||
* baseline = верх строки + 2 + POP_FONT_BIG_ASCENT. */
|
||||
#define HOF_BASELINE(i) (POP_YOFF + HOF_ROW_TOP + (int)(i) * HOF_ROW_STEP + 9)
|
||||
/* Золотой цвет полосы ввода: 0xB7 оригинала = седьмой цвет палитры title,
|
||||
* а у нас story.pal сделана из неё же. */
|
||||
#define HOF_GOLD 7
|
||||
/* Паузы оригинала в тиках 60 Гц: показ таблицы на титрах и пауза после
|
||||
* ввода имени (seg000:2065, seg001:628). */
|
||||
#define HOF_TITLE_TICKS POP_T60(0xF0)
|
||||
#define HOF_AFTER_TICKS POP_T60(120)
|
||||
|
||||
typedef struct {
|
||||
char name[HOF_NAME_SIZE];
|
||||
uint8_t minutes;
|
||||
@@ -135,7 +168,7 @@ static void hof_load(void)
|
||||
for (i = 0; i < HOF_SIZE; i++)
|
||||
hof_buf[i] = ((const uint8_t *)HOF_BASE)[i];
|
||||
gfx_w0_unmap();
|
||||
if (!hof_decode(hof_buf, HOF_SIZE)) hof_clear();
|
||||
if (!hof_decode(hof_buf, (uint8_t)HOF_SIZE)) hof_clear();
|
||||
}
|
||||
mem_free_block(blk);
|
||||
}
|
||||
@@ -208,62 +241,161 @@ static void hof_dec(uint8_t value, char *out)
|
||||
*out = 0;
|
||||
}
|
||||
|
||||
/* «59:43» — те же min-1 : tick/12, что печатает show_hof (seg001:0E94). */
|
||||
static void hof_result_text(const hof_entry_t *entry, char *out)
|
||||
{
|
||||
uint8_t seconds;
|
||||
|
||||
if (!entry->limited) {
|
||||
strcpy(out, "UNLIMITED");
|
||||
return;
|
||||
}
|
||||
hof_dec(entry->minutes, out);
|
||||
/* min == 0 у оригинала означает истёкшее время и печатается как 0:00. */
|
||||
hof_dec(entry->minutes ? (uint8_t)(entry->minutes - 1) : 0, out);
|
||||
while (*out) out++;
|
||||
*out++ = ' ';
|
||||
*out++ = 'M';
|
||||
*out++ = 'I';
|
||||
*out++ = 'N';
|
||||
seconds = entry->minutes ? (uint8_t)(entry->ticks / 12) : 0;
|
||||
*out++ = ':';
|
||||
*out++ = (char)('0' + seconds / 10);
|
||||
*out++ = (char)('0' + seconds % 10);
|
||||
*out = 0;
|
||||
}
|
||||
|
||||
static void hof_draw(uint8_t edit_row)
|
||||
/* show_hof_text(): сначала тень со сдвигом (1,1), затем сам текст. У нас
|
||||
* цвет запечён в глифы, поэтому «другой цвет» — это второй набор
|
||||
* (POP_TEXT_BIG_DARK), а не параметр рисования. */
|
||||
static void hof_text_left(int x, int baseline, const char *s)
|
||||
{
|
||||
uint8_t page, i;
|
||||
|
||||
page = pop_screen_begin();
|
||||
pop_screen_fill(BLACK);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_text_map();
|
||||
pop_text_center_mapped(POP_TEXT_BIG, 160, 58, "HALL OF FAME");
|
||||
pop_text_draw_mapped(POP_TEXT_SMALL, 64, 84, "NAME");
|
||||
pop_text_right_mapped(POP_TEXT_SMALL, 255, 84, "TIME");
|
||||
for (i = 0; i < HOF_COUNT; i++) {
|
||||
char rank[3], value[12];
|
||||
const hof_entry_t *entry = &hof_table[i];
|
||||
int y = 106 + (int)i * 20;
|
||||
|
||||
rank[0] = (char)('1' + i); rank[1] = '.'; rank[2] = 0;
|
||||
pop_text_draw_mapped(POP_TEXT_SMALL, 46, y, rank);
|
||||
if (entry->name[0])
|
||||
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, entry->name);
|
||||
else if (i == edit_row)
|
||||
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "_");
|
||||
else
|
||||
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "---");
|
||||
if (entry->name[0]) {
|
||||
hof_result_text(entry, value);
|
||||
pop_text_right_mapped(POP_TEXT_SMALL, 255, y, value);
|
||||
}
|
||||
}
|
||||
if (edit_row != HOF_NO_ROW)
|
||||
pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "TYPE NAME - ENTER");
|
||||
else
|
||||
pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "PRESS ANY KEY");
|
||||
pop_text_unmap();
|
||||
pop_screen_present(page);
|
||||
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x + 1, baseline + 1, s);
|
||||
pop_text_draw_mapped(POP_TEXT_BIG, x, baseline, s);
|
||||
}
|
||||
|
||||
static void hof_wait_release(void)
|
||||
static void hof_text_right(int right, int baseline, const char *s)
|
||||
{
|
||||
while (kbd_raw_any_down()) {
|
||||
kbd_raw_sync();
|
||||
pop_text_right_mapped(POP_TEXT_BIG_DARK, right + 1, baseline + 1, s);
|
||||
pop_text_right_mapped(POP_TEXT_BIG, right, baseline, s);
|
||||
}
|
||||
|
||||
/* Золотая полоса под ввод — draw_rect(&rect, bgcolor) оригинала. */
|
||||
static void hof_edit_bar(uint8_t row)
|
||||
{
|
||||
int top = HOF_BASELINE(row) - 10;
|
||||
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
setfillstyle(SOLID_FILL, HOF_GOLD);
|
||||
bar(HOF_EDIT_LEFT, top, HOF_EDIT_RIGHT, top + HOF_ROW_H - 1);
|
||||
}
|
||||
|
||||
/* Строки таблицы в ТЕКУЩУЮ draw-страницу.
|
||||
*
|
||||
* БАНК ЗДЕСЬ TRANSPARENT, А НЕ SPRITE. Полосовой переход копирует страницу
|
||||
* акселератором, а тот читает ОЗУ-копию (gfx_copy_page); SPRITE — это
|
||||
* NOSHADOW + TRANSPARENT, то есть в копию он не пишет, и на титрах
|
||||
* переезжал бы один фон без строк. TRANSPARENT даёт ту же прозрачность
|
||||
* 0xFF, но обновляет и копию, поэтому таблица проявляется уже собранной. */
|
||||
static void hof_draw_rows(uint8_t edit_row)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
||||
pop_text_map();
|
||||
for (i = 0; i < HOF_COUNT; i++) {
|
||||
char value[12];
|
||||
const hof_entry_t *entry = &hof_table[i];
|
||||
int baseline = HOF_BASELINE(i);
|
||||
|
||||
/* Строка, которую сейчас вписывают, уже содержит результат: имени
|
||||
* ещё нет, а время справа оригинал показывает сразу (show_hof
|
||||
* рисует все hof_count строк, включая только что вставленную). */
|
||||
if (!entry->name[0] && i != edit_row) continue;
|
||||
if (entry->name[0]) hof_text_left(HOF_LEFT, baseline, entry->name);
|
||||
hof_result_text(entry, value);
|
||||
hof_text_right(HOF_RIGHT, baseline, value);
|
||||
}
|
||||
pop_text_unmap();
|
||||
}
|
||||
|
||||
/* Готовая страница: фон из архива, полоса ввода и строки. -1 — фон не
|
||||
* прочитан, и тогда таблица просто не показывается: ни игра, ни title от
|
||||
* этого не зависят. */
|
||||
static int hof_page_build(uint8_t page, uint8_t edit_row)
|
||||
{
|
||||
if (pop_intro_hof_bg(page) != 0) return -1;
|
||||
gfx_set_draw_page(page);
|
||||
if (edit_row != HOF_NO_ROW) hof_edit_bar(edit_row);
|
||||
hof_draw_rows(edit_row);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Имя пишется ПРЯМО НА ЭКРАН, как input_str оригинала
|
||||
* (current_target_surface = onscreen_surface_): страницы при вводе не
|
||||
* меняются местами, поэтому дорисовывать фон второй страницы не нужно. */
|
||||
static void hof_edit_draw(uint8_t row, const char *name, uint8_t cursor)
|
||||
{
|
||||
int x = HOF_LEFT, baseline = HOF_BASELINE(row);
|
||||
|
||||
gfx_set_draw_page(gfx_get_visible_page());
|
||||
hof_edit_bar(row);
|
||||
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
||||
pop_text_map();
|
||||
/* Тёмный набор: на золотой полосе оригинал пишет цветом 0xBE. */
|
||||
if (name[0]) {
|
||||
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, name);
|
||||
x += pop_text_width_mapped(POP_TEXT_BIG_DARK, name) + 1;
|
||||
}
|
||||
/* draw_text_cursor(): подчерк той же гарнитуры сразу за текстом,
|
||||
* мигающий сменой цвета на фоновый — у нас проще перерисовать. */
|
||||
if (cursor) pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_");
|
||||
pop_text_unmap();
|
||||
}
|
||||
|
||||
/* ---- ожидания -------------------------------------------------------- *
|
||||
*
|
||||
* Любое ожидание этого экрана держит ПОТОКОВУЮ музыку: победная тема
|
||||
* (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
|
||||
* игрок смотрит таблицу и печатает имя — иначе насос догонит загрузчика и
|
||||
* пойдёт по кругу старыми данными.
|
||||
*
|
||||
* Прерывание — по НОВОМУ нажатию (детектор фронта, как в сценах intro):
|
||||
* клавиша, которой закончили предыдущий экран, в момент входа ещё зажата,
|
||||
* и считать её за «пропустить» нельзя. */
|
||||
typedef struct { uint8_t armed; } hof_skip_t;
|
||||
|
||||
static void hof_skip_begin(hof_skip_t *sk)
|
||||
{
|
||||
kbd_raw_sync();
|
||||
sk->armed = (uint8_t)!kbd_raw_any_down();
|
||||
}
|
||||
|
||||
static uint8_t hof_skip_requested(hof_skip_t *sk)
|
||||
{
|
||||
kbd_raw_sync();
|
||||
if (!kbd_raw_any_down()) { sk->armed = 1; return 0; }
|
||||
return sk->armed;
|
||||
}
|
||||
|
||||
/* Возврат 1 — ожидание прервано нажатием. */
|
||||
static uint8_t hof_wait_frames(uint16_t frames)
|
||||
{
|
||||
hof_skip_t skip;
|
||||
|
||||
hof_skip_begin(&skip);
|
||||
while (frames--) {
|
||||
if (hof_skip_requested(&skip)) return 1;
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* `while (check_sound_playing() && !key_test_quit())` (seg001:637): финал
|
||||
* дослушивает победную тему и прерывается любой клавишей. */
|
||||
static void hof_wait_music(void)
|
||||
{
|
||||
hof_skip_t skip;
|
||||
|
||||
hof_skip_begin(&skip);
|
||||
while (pop_music_busy() && !hof_skip_requested(&skip)) {
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
}
|
||||
@@ -277,8 +409,10 @@ static const uint8_t hof_key_code[30] = {
|
||||
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
|
||||
};
|
||||
|
||||
/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
|
||||
* перерисовать строку. */
|
||||
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
uint8_t *previous)
|
||||
uint8_t *previous, uint8_t *changed)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
@@ -288,12 +422,16 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
if (i < 26 && *length < HOF_NAME_SIZE - 1) {
|
||||
entry->name[(*length)++] = (char)('A' + i);
|
||||
entry->name[*length] = 0;
|
||||
*changed = 1;
|
||||
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
|
||||
entry->name[(*length)++] = ' ';
|
||||
entry->name[*length] = 0;
|
||||
*changed = 1;
|
||||
} else if (i == 27 && *length) {
|
||||
entry->name[--(*length)] = 0;
|
||||
*changed = 1;
|
||||
} else if (i == 28 || i == 29) {
|
||||
previous[i] = down;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -302,22 +440,42 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Ввод имени в строку row. Пустое имя оригинал не отвергает, но пустая
|
||||
* строка в таблице выглядела бы как свободное место — подставляем PLAYER. */
|
||||
/* Курсор мигает каждые шесть тиков 60 Гц (start_timer(timer_0, 6) в
|
||||
* input_str) — в кадрах луча это HOF_BLINK. */
|
||||
#define HOF_BLINK POP_T60(6)
|
||||
|
||||
static void hof_edit(uint8_t row)
|
||||
{
|
||||
uint8_t length = 0, previous[30], i;
|
||||
uint8_t length = 0, previous[30], i, changed = 1;
|
||||
uint8_t cursor = 1, phase = 0;
|
||||
hof_entry_t *entry = &hof_table[row];
|
||||
|
||||
for (i = 0; i < sizeof(previous); i++) previous[i] = 0;
|
||||
hof_wait_release();
|
||||
/* Стартовое состояние клавиш: зажатое с прошлого экрана не считается
|
||||
* нажатием, поэтому первый sync только заполняет previous[]. */
|
||||
kbd_raw_sync();
|
||||
for (i = 0; i < sizeof(previous); i++)
|
||||
previous[i] = kbd_raw_down(hof_key_code[i]);
|
||||
for (;;) {
|
||||
kbd_raw_sync();
|
||||
if (hof_name_tick(entry, &length, previous)) {
|
||||
if (hof_name_tick(entry, &length, previous, &changed)) {
|
||||
if (!length) strcpy(entry->name, "PLAYER");
|
||||
hof_save();
|
||||
hof_wait_release();
|
||||
return;
|
||||
}
|
||||
hof_draw(row);
|
||||
if (changed) { cursor = 1; phase = 0; }
|
||||
if (++phase >= HOF_BLINK) {
|
||||
phase = 0;
|
||||
cursor = (uint8_t)!cursor;
|
||||
changed = 1;
|
||||
}
|
||||
if (changed) {
|
||||
hof_edit_draw(row, entry->name, cursor);
|
||||
changed = 0;
|
||||
}
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,46 +483,48 @@ void pop_hof_show(void) __banked
|
||||
{
|
||||
uint8_t row;
|
||||
|
||||
pop_sfx_pause();
|
||||
hof_load();
|
||||
row = hof_insert_current();
|
||||
|
||||
pop_pal_story_load();
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
hof_draw(row);
|
||||
pop_pal_fade_in(HOF_FADE);
|
||||
|
||||
if (row != HOF_NO_ROW) {
|
||||
hof_edit(row);
|
||||
} else {
|
||||
hof_wait_release();
|
||||
while (!kbd_raw_any_down()) {
|
||||
kbd_raw_sync();
|
||||
gfx_wait_vsync();
|
||||
uint8_t page;
|
||||
|
||||
/* fade_out_2 гасит титульную картинку перед таблицей (seg001:601).
|
||||
* Палитру не меняем: story.pal с красным фоном рамки уже стоит от
|
||||
* финального HAIL, и снимок для fade сделан ею же. */
|
||||
pop_pal_fade_out(HOF_FADE);
|
||||
page = pop_screen_begin();
|
||||
if (hof_page_build(page, row) == 0) {
|
||||
pop_screen_present(page);
|
||||
pop_pal_fade_in(HOF_FADE);
|
||||
hof_edit(row);
|
||||
hof_save();
|
||||
/* Имя вписано: строку показываем уже обычным золотым текстом,
|
||||
* без полосы (restore_peel + show_hof_text оригинала). */
|
||||
page = pop_screen_begin();
|
||||
if (hof_page_build(page, HOF_NO_ROW) == 0)
|
||||
pop_screen_present(page);
|
||||
hof_wait_frames(HOF_AFTER_TICKS);
|
||||
(void)pop_title_main_ltr();
|
||||
}
|
||||
hof_wait_release();
|
||||
}
|
||||
hof_wait_music();
|
||||
pop_pal_fade_out(HOF_FADE);
|
||||
pop_music_free();
|
||||
pop_sfx_pause();
|
||||
}
|
||||
|
||||
void pop_hof_show_title(void) __banked
|
||||
uint8_t pop_hof_show_title(void) __banked
|
||||
{
|
||||
/* Один keypress должен пропускать всю цепочку title/intro, а не внезапно
|
||||
* останавливаться на уже известной таблице. */
|
||||
if (kbd_raw_any_down()) return;
|
||||
uint8_t page;
|
||||
|
||||
hof_load();
|
||||
if (!hof_has_entries()) return;
|
||||
pop_sfx_pause();
|
||||
pop_pal_story_load();
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
hof_draw(HOF_NO_ROW);
|
||||
pop_pal_fade_in(HOF_FADE);
|
||||
hof_wait_release();
|
||||
while (!kbd_raw_any_down()) {
|
||||
kbd_raw_sync();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
hof_wait_release();
|
||||
pop_pal_fade_out(HOF_FADE);
|
||||
if (!hof_has_entries()) return 0;
|
||||
page = pop_screen_begin();
|
||||
/* Страница собирается ЦЕЛИКОМ до показа — как offscreen оригинала:
|
||||
* draw_full_image + show_hof, и только потом transition_ltr
|
||||
* (seg000:2063). Ни fade, ни смены палитры: экран проявляется прямо
|
||||
* поверх credits, на той же story.pal. */
|
||||
if (hof_page_build(page, HOF_NO_ROW) != 0) return 0;
|
||||
pop_screen_present_ltr();
|
||||
return hof_wait_frames(HOF_TITLE_TICKS);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
#ifndef POP_HOF_H
|
||||
#define POP_HOF_H
|
||||
|
||||
/* Показать Hall of Fame, при подходящем результате запросить имя и сохранить
|
||||
* POP.HOF. Ошибка или повреждение файла означают пустую таблицу; gameplay и
|
||||
* последующий title от этого не зависят. */
|
||||
#include <stdint.h>
|
||||
|
||||
/* Хвост end_sequence: при подходящем результате — запрос имени, сохранение
|
||||
* POP.HOF и возврат к титульной картинке; затем в любом случае
|
||||
* дослушивается победная тема и гаснет экран. Ошибка или повреждение
|
||||
* файла означают пустую таблицу; gameplay и последующий title от этого не
|
||||
* зависят. Победная тема на входе ещё звучит — освобождает её эта
|
||||
* функция. */
|
||||
void pop_hof_show(void) __banked;
|
||||
|
||||
/* Показать уже существующую непустую таблицу между credits и demo. Новая
|
||||
* запись здесь не создаётся; удержанная клавиша title/intro пропускает экран
|
||||
* целиком. */
|
||||
void pop_hof_show_title(void) __banked;
|
||||
/* Показать уже существующую непустую таблицу между credits и demo
|
||||
* (seg000:2060): полосовой переход, 240 тиков, ни fade, ни смены палитры.
|
||||
* Возврат 1 — игрок нажал клавишу, то есть всю цепочку title/intro надо
|
||||
* прервать. */
|
||||
uint8_t pop_hof_show_title(void) __banked;
|
||||
|
||||
#endif /* POP_HOF_H */
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "pv_arc.h"
|
||||
#include "pop_cutscene.h"
|
||||
#include "pop_intro.h"
|
||||
#include "pop_hof.h" /* таблица рекордов между credits и demo */
|
||||
#include "pop_title.h" /* титульная картинка в финале */
|
||||
#include "pop_pace.h" /* pop_wait_edge */
|
||||
#include "pop_pal.h"
|
||||
#include "pop_timer.h"
|
||||
@@ -78,6 +80,7 @@ enum {
|
||||
INTRO_STORY_CREDITS,
|
||||
INTRO_STORY_HAIL,
|
||||
INTRO_STORY_FRAME,
|
||||
INTRO_STORY_HOF, /* рамка + логотип: фон таблицы рекордов */
|
||||
INTRO_PV_WAITING,
|
||||
INTRO_PV_JAFFAR,
|
||||
INTRO_PV_MAGIC,
|
||||
@@ -90,7 +93,10 @@ enum {
|
||||
|
||||
enum {
|
||||
INTRO_PAL_STORY = 0,
|
||||
INTRO_PAL_PV = 1
|
||||
INTRO_PAL_PV = 1,
|
||||
/* Та же story.pal, но фон текстовой рамки красный:
|
||||
* load_title_images(0) в end_sequence (seg001:586). */
|
||||
INTRO_PAL_STORY_END = 2
|
||||
};
|
||||
|
||||
/* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же
|
||||
@@ -98,10 +104,10 @@ enum {
|
||||
* res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты
|
||||
* принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет
|
||||
* — индекс первой полосы экрана считается умножением. */
|
||||
#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 5 * INTRO_PARTS
|
||||
#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 6 * INTRO_PARTS
|
||||
#error "экраны PV в архиве лежат не по пять полос подряд"
|
||||
#endif
|
||||
#define INTRO_SCENES 13
|
||||
#define INTRO_SCENES 14
|
||||
|
||||
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
|
||||
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */
|
||||
@@ -151,10 +157,9 @@ static const pop_cs_cmd_t intro_marry[] = {
|
||||
{ POP_CS_END, 0 }
|
||||
};
|
||||
|
||||
static const pop_cs_cmd_t intro_credits[] = {
|
||||
{ POP_CS_WAIT, POP_T60(0x78) },
|
||||
{ POP_CS_SHOW, INTRO_STORY_CREDITS },
|
||||
{ POP_CS_WAIT, POP_T60(0x168) },
|
||||
/* Гашение — отдельным шагом: между credits и ним оригинал показывает
|
||||
* таблицу рекордов, если файл с ней есть (seg000:2060). */
|
||||
static const pop_cs_cmd_t intro_tail[] = {
|
||||
{ POP_CS_FADE_OUT, 0 },
|
||||
{ POP_CS_END, 0 }
|
||||
};
|
||||
@@ -201,16 +206,19 @@ static const pop_cs_cmd_t time_expired_script[] = {
|
||||
/* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии;
|
||||
* затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */
|
||||
/* Хвост финала после живой сцены встречи: только исходный story-экран. */
|
||||
/* FADE_OUT в конце НЕТ намеренно: оригинал не гасит HAIL, а проявляет
|
||||
* поверх него титульную картинку полосами (draw_full_image(TITLE_MAIN) +
|
||||
* transition_ltr, seg001:590). Гасит экран уже таблица рекордов. */
|
||||
static const pop_cs_cmd_t ending_hail_script[] = {
|
||||
{ POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL },
|
||||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 },
|
||||
{ POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
|
||||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
|
||||
{ POP_CS_END, 0 }
|
||||
};
|
||||
static const pop_cs_cmd_t ending_script[] = {
|
||||
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
|
||||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 54 }, { POP_CS_FADE_OUT, 0 },
|
||||
{ POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL },
|
||||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 },
|
||||
{ POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
|
||||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
|
||||
{ POP_CS_END, 0 }
|
||||
};
|
||||
|
||||
@@ -342,7 +350,7 @@ static void intro_prepare_palette(uint8_t pal)
|
||||
static void intro_commit_palette(void)
|
||||
{
|
||||
if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load();
|
||||
else pop_pal_story_load();
|
||||
else pop_pal_story_load((uint8_t)(intro_pending_pal == INTRO_PAL_STORY_END));
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
intro_pal_pending = 0;
|
||||
}
|
||||
@@ -841,6 +849,22 @@ static int intro_pv_animated(void)
|
||||
torch_right, sand_frame, flash,
|
||||
render_tick);
|
||||
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
||||
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
||||
*
|
||||
* Ниже страница следующей реплики читается в ПАУЗЕ кадра, и это
|
||||
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
||||
* нет: кадр дорогой, шкала работает по принципу «долг не догоняем»
|
||||
* (snd_want подтягивается к snd_done), и цикл ожидания выходит
|
||||
* сразу. Замер в MAME 2026-08-26: ОДНА страница за семь секунд,
|
||||
* то есть одиннадцать страниц m53 не успевали за всю сцену —
|
||||
* pop_music_play() на своём тике не находил готового трека, и
|
||||
* реплики Джафара пропадали совсем.
|
||||
*
|
||||
* Страница стоит 33 мс против кадра сцены в 100-133 мс, поэтому
|
||||
* один безусловный шаг в кадр сцену не ломает: он лишь тратит
|
||||
* часть той же паузы, а если паузы не было — добавляет к кадру
|
||||
* четверть его длины, и шкала это учтёт сама. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||||
* целиком, вместе с принцессой и факелами. */
|
||||
if (!faded_in) { faded_in = 1; pop_pal_fade_in(INTRO_FADE); }
|
||||
@@ -1012,29 +1036,8 @@ static int intro_story_prepare(void)
|
||||
return intro_draw_page(INTRO_STORY_ABSENCE, target);
|
||||
}
|
||||
|
||||
/* Полосовой показ подготовленной страницы (transition_ltr оригинала). */
|
||||
static int intro_story_transition(void)
|
||||
{
|
||||
gfx_rect_t stripe;
|
||||
uint8_t visible = gfx_get_visible_page();
|
||||
int x;
|
||||
|
||||
gfx_set_draw_page(visible); /* source gfx_copy_page = target */
|
||||
stripe.y = 0;
|
||||
stripe.h = GFX_HEIGHT;
|
||||
for (x = 0; x < GFX_WIDTH; x += 4) {
|
||||
if (kbd_raw_any_down()) {
|
||||
stripe.x = 0; stripe.w = GFX_WIDTH;
|
||||
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
||||
break;
|
||||
}
|
||||
stripe.x = x;
|
||||
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
|
||||
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* Полосовой показ подготовленной страницы живёт в pop_ui: тем же приёмом
|
||||
* появляются титульная картинка финала и таблица рекордов. */
|
||||
|
||||
static void intro_flash(uint16_t ticks)
|
||||
{
|
||||
@@ -1323,6 +1326,10 @@ static int cut_begin(uint8_t hg_state)
|
||||
intro_commit_palette();
|
||||
if (intro_pv_draw_base() != 0) return -1;
|
||||
if (cut_load() != 0) return -1;
|
||||
/* Атласы сцены прочитаны — можно открывать вывод. Без этого сцена шла
|
||||
* молча: pop_ending_show глушит CBL первым действием, а включить его
|
||||
* обратно было некому, и «arrived to princess» (26) не звучал вовсе. */
|
||||
(void)pop_sfx_start();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1500,12 +1507,42 @@ static uint8_t intro_run(const pop_cs_cmd_t *script)
|
||||
} else if (ev == POP_CS_EVENT_FADE_IN) pop_pal_fade_in(INTRO_FADE);
|
||||
else if (ev == POP_CS_EVENT_FADE_OUT) pop_pal_fade_out(INTRO_FADE);
|
||||
else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg);
|
||||
pop_music_service(); /* подкачка потокового трека (финал) */
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
|
||||
return aborted;
|
||||
}
|
||||
|
||||
|
||||
/* Ожидание кадров у шагов, которые показываются НЕ сценарием, а напрямую.
|
||||
* Оно не глушит поток: победная тема играет кольцом страниц, и дочитывать
|
||||
* её надо каждый кадр, иначе насос догонит загрузчика. Прерывается новым
|
||||
* нажатием (детектор фронта); 1 — прервано. */
|
||||
static uint8_t intro_wait_frames(uint16_t frames)
|
||||
{
|
||||
intro_skip_t skip;
|
||||
|
||||
intro_skip_begin(&skip);
|
||||
while (frames--) {
|
||||
if (intro_skip_requested(&skip)) return 1;
|
||||
pop_music_service();
|
||||
pop_wait_edge();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Титры проявляются полосами, как и всё остальное в show_title()
|
||||
* (draw_full_image(STORY_CREDITS) + transition_ltr, seg000:2055). */
|
||||
static uint8_t intro_credits_ltr(void)
|
||||
{
|
||||
uint8_t page = pop_screen_begin();
|
||||
|
||||
if (intro_draw_page(INTRO_STORY_CREDITS, page) == 0)
|
||||
pop_screen_present_ltr();
|
||||
return intro_wait_frames(POP_T60(0x168));
|
||||
}
|
||||
|
||||
uint8_t pop_intro_show(void) __banked
|
||||
{
|
||||
int pv_result;
|
||||
@@ -1538,7 +1575,7 @@ uint8_t pop_intro_show(void) __banked
|
||||
}
|
||||
(void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */
|
||||
pop_music_play();
|
||||
(void)intro_story_transition();
|
||||
pop_screen_present_ltr();
|
||||
}
|
||||
if (intro_run(intro_before_pv)) {
|
||||
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
|
||||
@@ -1594,7 +1631,27 @@ uint8_t pop_intro_show(void) __banked
|
||||
}
|
||||
pop_music_free();
|
||||
pop_sfx_pause();
|
||||
if (intro_run(intro_credits)) {
|
||||
/* ТИТУЛЬНЫЙ ЭКРАН МЕЖДУ «СВАДЬБОЙ» И ТИТРАМИ. show_title() рисует
|
||||
* TITLE_MAIN + логотип + Jordan Mechner и проявляет их полосами
|
||||
* (seg000:2045), и только через 0x78 тиков показывает credits — тоже
|
||||
* полосами. Раньше обе картинки возникали мгновенно. */
|
||||
if (pop_title_logo_ltr() == 0 && intro_wait_frames(POP_T60(0x78))) {
|
||||
intro_restore_game_palette();
|
||||
return 1;
|
||||
}
|
||||
if (intro_credits_ltr()) {
|
||||
intro_restore_game_palette();
|
||||
return 1;
|
||||
}
|
||||
/* ТАБЛИЦА РЕКОРДОВ между credits и attract-demo — ровно там же, где её
|
||||
* показывает show_title(): та же story-палитра, тот же полосовой
|
||||
* переход, никакого fade (seg000:2060). Пустой таблицы это не
|
||||
* касается: она просто не показывается. */
|
||||
if (pop_hof_show_title()) {
|
||||
intro_restore_game_palette();
|
||||
return 1;
|
||||
}
|
||||
if (intro_run(intro_tail)) {
|
||||
intro_restore_game_palette();
|
||||
return 1;
|
||||
}
|
||||
@@ -1691,6 +1748,14 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
|
||||
pre_cut_finish(intro_run(script));
|
||||
}
|
||||
|
||||
/* Фон таблицы рекордов — та же рамка story плюс логотип PRINCE OF PERSIA
|
||||
* (STORY_FRAME + HOF_POP оригинала). Рисуем в ЗАДАННУЮ страницу и её не
|
||||
* показываем: палитрой, строками и переходом распоряжается pop_hof. */
|
||||
int pop_intro_hof_bg(uint8_t page) __banked
|
||||
{
|
||||
return intro_draw_page(INTRO_STORY_HOF, page);
|
||||
}
|
||||
|
||||
void pop_time_expired_show(void) __banked
|
||||
{
|
||||
pop_sfx_pause();
|
||||
@@ -1719,6 +1784,7 @@ void pop_time_expired_show(void) __banked
|
||||
intro_restore_game_palette();
|
||||
}
|
||||
|
||||
|
||||
void pop_ending_show(void) __banked
|
||||
{
|
||||
pop_sfx_pause();
|
||||
@@ -1730,7 +1796,22 @@ void pop_ending_show(void) __banked
|
||||
/* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по
|
||||
* скрипту идёт только текст HAIL, поэтому статический сценарий ниже
|
||||
* начинается с него; при нехватке ресурсов отыгрывается целиком. */
|
||||
if (cut_ending() == 0) intro_run(ending_hail_script);
|
||||
else intro_run(ending_script);
|
||||
intro_restore_game_palette();
|
||||
if (cut_ending() == 0) {
|
||||
/* ПОБЕДНАЯ ТЕМА (56) — потоком с диска: 115 секунд и 1,2 МБ в память
|
||||
* не влезают никак. Оригинал пускает её здесь же, сразу после
|
||||
* встречи с принцессой и до экрана HAIL (seg001:583). Дальше
|
||||
* страницы дочитывает pop_music_service из циклов сцен. */
|
||||
(void)pop_sfx_start();
|
||||
(void)pop_music_ring_start(POP_MUS_WON);
|
||||
intro_run(ending_hail_script);
|
||||
} else {
|
||||
intro_run(ending_script);
|
||||
}
|
||||
/* ТИТУЛЬНАЯ КАРТИНКА ПОВЕРХ ТЕКСТА. end_sequence заводит таймер на 240
|
||||
* тиков, проявляет TITLE_MAIN полосами и ждёт его (seg001:590). Экран
|
||||
* при этом не гаснет: рамка HAIL просто переползает в рисунок дворца.
|
||||
* Дальше — состояние HALL_OF_FAME; тема продолжает звучать, и
|
||||
* освобождает её уже таблица рекордов, как в оригинале. */
|
||||
if (pop_title_main_ltr() == 0)
|
||||
(void)intro_wait_frames(POP_T60(240));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,13 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked;
|
||||
* возвращается в TITLE. Любой raw-ввод сокращает ожидание. */
|
||||
void pop_time_expired_show(void) __banked;
|
||||
|
||||
/* Финал level 14: Princess room, затем исходный story-screen HAIL. */
|
||||
/* Финал level 14: Princess room, story-screen HAIL и титульная картинка.
|
||||
* Победная тема при выходе ЕЩЁ ЗВУЧИТ: её дослушивает таблица рекордов,
|
||||
* как в end_sequence оригинала. */
|
||||
void pop_ending_show(void) __banked;
|
||||
|
||||
/* Фон таблицы рекордов (рамка story + логотип) в заданную СКРЫТУЮ
|
||||
* страницу; -1 — ресурс не прочитан. Ни палитры, ни показа. */
|
||||
int pop_intro_hof_bg(uint8_t page) __banked;
|
||||
|
||||
#endif /* POP_INTRO_H */
|
||||
|
||||
@@ -62,6 +62,34 @@ static void slot_release(uint8_t n)
|
||||
slot_blocks[n] = 0;
|
||||
}
|
||||
|
||||
/* --- ПОТОКОВЫЙ ТРЕК: КОЛЬЦО СТРАНИЦ ---------------------------------- *
|
||||
*
|
||||
* Финальная тема (`won`, 115 с) — это 1,2 МБ и 78 страниц EMM. Держать её
|
||||
* в памяти нельзя ни при каком бюджете, поэтому она играется из НЕСКОЛЬКИХ
|
||||
* страниц по кругу: насос идёт по кольцу (pop_mus_ring), а загрузчик
|
||||
* дочитывает файл в те страницы, которые насос уже прошёл.
|
||||
*
|
||||
* Кто кого догоняет. Страница звучит 1,5 с, а читается 33 мс — запас
|
||||
* сорокакратный, так что «не успеть» можно только застряв на секунду.
|
||||
* Дистанцию считаем без отдельных счётчиков: страница ровно 128 блоков
|
||||
* насоса, поэтому проигранных страниц = (всего блоков - осталось) / 128.
|
||||
* Пока прочитано меньше, чем проиграно плюс размер кольца, — в кольце есть
|
||||
* свободный слот, и следующую страницу можно читать.
|
||||
*
|
||||
* Файл читаем ПОСЛЕДОВАТЕЛЬНО, без lseek: страницы идут по порядку, и fd
|
||||
* сам стоит там, где надо. */
|
||||
#define RING_PAGES 6 /* 96 КБ = 9 с звучания, запас с лихвой */
|
||||
|
||||
static uint8_t ring_on; /* идёт потоковый трек */
|
||||
static uint8_t ring_blk; /* EMM-блок кольца */
|
||||
static uint8_t ring_fd_open;
|
||||
static int ring_fd;
|
||||
static uint8_t ring_file_pages; /* страниц в файле */
|
||||
static uint8_t ring_loaded; /* сколько уже прочитано */
|
||||
static uint16_t ring_total; /* длина трека в блоках насоса */
|
||||
|
||||
void pop_music_ring_stop(void) __banked;
|
||||
|
||||
void pop_music_stop(void) __banked
|
||||
{
|
||||
pop_mus_left = 0;
|
||||
@@ -69,6 +97,7 @@ void pop_music_stop(void) __banked
|
||||
|
||||
void pop_music_free(void) __banked
|
||||
{
|
||||
if (ring_on) pop_music_ring_stop();
|
||||
if (ld_busy) { close(ld_fd); ld_busy = 0; }
|
||||
pop_mus_left = 0;
|
||||
slot_release(0);
|
||||
@@ -96,6 +125,7 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
char name[12];
|
||||
uint8_t pages, n;
|
||||
|
||||
if (ring_on) pop_music_ring_stop(); /* потоковый трек и слоты не дружат */
|
||||
if (ld_busy) load_abort();
|
||||
if (id >= POP_MUS_COUNT) return -1;
|
||||
pages = pop_mus_tbl[id].pages;
|
||||
@@ -174,6 +204,7 @@ int8_t pop_music_stream(uint8_t id) __banked
|
||||
{
|
||||
uint8_t i, n;
|
||||
|
||||
if (ring_on) pop_music_ring_stop(); /* см. pop_music_play */
|
||||
if (pop_music_load_begin(id) != 0) return -1;
|
||||
if (pop_music_load_step() < 0) return -1; /* первая страница */
|
||||
n = ld_slot;
|
||||
@@ -197,6 +228,10 @@ void pop_music_play(void) __banked
|
||||
uint8_t n, i;
|
||||
|
||||
if (!mus_ready) return;
|
||||
/* СНЯТЬ КОЛЬЦО. Если до этого играл потоковый трек (финал), насос всё
|
||||
* ещё заворачивал бы страницы по кругу шести — обычный трек звучал бы
|
||||
* своей первой третью без конца (поймано на титрах после победы). */
|
||||
if (ring_on) pop_music_ring_stop();
|
||||
n = (uint8_t)(mus_ready - 1);
|
||||
/* Насос обязан замолчать ДО подмены таблицы страниц: иначе он успеет
|
||||
* взять порцию по новому индексу из старого курсора. */
|
||||
@@ -215,6 +250,84 @@ void pop_music_play(void) __banked
|
||||
pop_mus_left = slot_blocks[n];
|
||||
}
|
||||
|
||||
|
||||
static void ring_close_file(void)
|
||||
{
|
||||
if (ring_fd_open) { close(ring_fd); ring_fd_open = 0; }
|
||||
}
|
||||
|
||||
void pop_music_ring_stop(void) __banked
|
||||
{
|
||||
pop_mus_left = 0;
|
||||
pop_mus_ring = 0;
|
||||
ring_close_file();
|
||||
if (ring_blk) { mem_free_block(ring_blk); ring_blk = 0; }
|
||||
ring_on = 0;
|
||||
}
|
||||
|
||||
int8_t pop_music_ring_start(uint8_t id) __banked
|
||||
{
|
||||
char name[12];
|
||||
uint8_t i, page;
|
||||
|
||||
if (id >= POP_MUS_COUNT || !pop_mus_tbl[id].pages) return -1;
|
||||
pop_music_free(); /* обычные слоты кольцу не нужны */
|
||||
pop_music_ring_stop();
|
||||
mus_name(name, id);
|
||||
ring_fd = open(name, O_RDONLY);
|
||||
if (ring_fd < 0) return -1;
|
||||
ring_fd_open = 1;
|
||||
ring_blk = mem_alloc_pages(RING_PAGES);
|
||||
if (!ring_blk) { ring_close_file(); return -1; }
|
||||
ring_file_pages = pop_mus_tbl[id].pages;
|
||||
ring_total = pop_mus_tbl[id].blocks;
|
||||
/* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд
|
||||
* форы, и первая же тяжёлая сцена финала не догонит насос. */
|
||||
for (i = 0; i < RING_PAGES; i++) {
|
||||
page = mem_get_page(ring_blk, i);
|
||||
pop_mus_page[i] = page;
|
||||
if (!page ||
|
||||
(i < ring_file_pages && bank_read_page(ring_fd, page, 0, 16384) < 0)) {
|
||||
pop_music_ring_stop();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
ring_loaded = (uint8_t)(ring_file_pages < RING_PAGES ? ring_file_pages
|
||||
: RING_PAGES);
|
||||
if (ring_loaded >= ring_file_pages) ring_close_file();
|
||||
ring_on = 1;
|
||||
pop_mus_id = id;
|
||||
pop_mus_pg = 0;
|
||||
pop_mus_ptr = 0;
|
||||
pop_mus_ring = RING_PAGES; /* насос пойдёт по кругу */
|
||||
pop_mus_left = ring_total;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Один шаг подкачки: страница файла в слот, который насос уже прошёл. */
|
||||
static void ring_step(void)
|
||||
{
|
||||
uint16_t played;
|
||||
uint8_t played_pg, page;
|
||||
|
||||
if (!ring_on) return;
|
||||
if (!pop_mus_left) { /* трек доиграл — отпустить память */
|
||||
pop_music_ring_stop();
|
||||
return;
|
||||
}
|
||||
if (ring_loaded >= ring_file_pages) return; /* файл прочитан весь */
|
||||
played = (uint16_t)(ring_total - pop_mus_left);
|
||||
played_pg = (uint8_t)(played >> 7); /* 128 блоков на страницу */
|
||||
if ((uint8_t)(ring_loaded - played_pg) >= RING_PAGES) return; /* полно */
|
||||
page = pop_mus_page[ring_loaded % RING_PAGES];
|
||||
if (!page || bank_read_page(ring_fd, page, 0, 16384) < 0) {
|
||||
pop_music_ring_stop();
|
||||
return;
|
||||
}
|
||||
ring_loaded++;
|
||||
if (ring_loaded >= ring_file_pages) ring_close_file();
|
||||
}
|
||||
|
||||
void pop_music_service(void) __banked
|
||||
{
|
||||
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
|
||||
@@ -225,6 +338,7 @@ void pop_music_service(void) __banked
|
||||
* демо проигрывает записанные нажатия, и любой музыкальный повод —
|
||||
* подобранный меч, зелье — зазвучал бы там, где оригинал играет одни
|
||||
* эффекты. */
|
||||
if (ring_on) { ring_step(); return; } /* потоковый трек — свой путь */
|
||||
if (!pop_mus_want || pop_app_state == POP_APP_DEMO) {
|
||||
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
|
||||
* продолжают звучать — у них свой тумблер. */
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#define POP_MUS_STORY_2 50 /* принцесса ждёт */
|
||||
#define POP_MUS_STORY_3 53 /* входит Джафар */
|
||||
#define POP_MUS_STORY_4 52 /* Джафар уходит */
|
||||
#define POP_MUS_WON 56 /* победа: 115 с, только потоком */
|
||||
|
||||
/* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет
|
||||
* загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не
|
||||
@@ -45,6 +46,8 @@ extern volatile uint16_t pop_mus_left; /* порций до конца, 0
|
||||
/* Номер играющего трека (нумерация оригинала). Нужен менеджеру звука:
|
||||
* музыка участвует в таблицах приоритета наравне с эффектами. */
|
||||
extern uint8_t pop_mus_id;
|
||||
/* Размер кольца страниц для потокового трека; 0 — обычный режим. */
|
||||
extern uint8_t pop_mus_ring;
|
||||
|
||||
/* ДВА СЛОТА. Заставочные сцены идут цепочкой треков (титры -> история ->
|
||||
* PV из трёх реплик), и загрузка каждого — это полсекунды-секунда диска.
|
||||
@@ -88,6 +91,14 @@ void pop_music_stop(void) __banked;
|
||||
* стрима. Всё чтение с диска живёт здесь. */
|
||||
void pop_music_service(void) __banked;
|
||||
|
||||
/* ПОТОКОВЫЙ ТРЕК ИЗ КОЛЬЦА СТРАНИЦ — для того, что в память не влезает.
|
||||
* Финал `won` весит 1,2 МБ (78 страниц); играется из шести по кругу, а
|
||||
* дочитывает его pop_music_service, пока трек звучит.
|
||||
* 0 — играет, -1 — нет файла/памяти. Останавливает pop_music_ring_stop
|
||||
* (её же зовёт pop_music_free). */
|
||||
int8_t pop_music_ring_start(uint8_t id) __banked;
|
||||
void pop_music_ring_stop(void) __banked;
|
||||
|
||||
/* Трек ещё звучит? 0 — доиграл или не запускался. */
|
||||
uint8_t pop_music_busy(void) __banked;
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_MUS_COUNT 56
|
||||
#define POP_MUS_COUNT 57
|
||||
#define POP_MUS_BLOCK 128
|
||||
#define POP_MUS_MAX_PAGES 20
|
||||
#define POP_MUS_MAX_PAGES 78
|
||||
|
||||
typedef struct {
|
||||
uint8_t pages; /* 0 — трека нет */
|
||||
@@ -78,6 +78,7 @@ static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
|
||||
{ 11, 1403 }, /* 53 story_3_Jaffar_enters */
|
||||
{ 17, 2064 }, /* 54 intro_theme */
|
||||
{ 11, 1350 }, /* 55 story_1_absence */
|
||||
{ 78, 9865 }, /* 56 won */
|
||||
};
|
||||
|
||||
#endif /* POP_MUSIC_TBL_H */
|
||||
|
||||
@@ -103,9 +103,18 @@ void pop_pal_title_load(void) __banked
|
||||
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
|
||||
}
|
||||
|
||||
void pop_pal_story_load(void) __banked
|
||||
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
|
||||
* load_title_images(bgcolor) SDLPoP красит его в #100060 на титрах и в
|
||||
* #800000 в end_sequence (seg001:586); у нас фон запечён отдельным
|
||||
* индексом POP_PAL_STORY_BG именно ради этой подмены. Она делается ПОСЛЕ
|
||||
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
|
||||
void pop_pal_story_load(uint8_t ending) __banked
|
||||
{
|
||||
pal_file_load("PV\\story.pal", "a:\\story.pal");
|
||||
if (!ending) return;
|
||||
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||
pop_ui_palette_snapshot();
|
||||
}
|
||||
|
||||
void pop_pal_pv_load(void) __banked
|
||||
|
||||
@@ -19,7 +19,10 @@ void pop_pal_black(void) __banked;
|
||||
void pop_pal_game_load(void) __banked;
|
||||
void pop_pal_level_load(uint8_t full) __banked;
|
||||
void pop_pal_title_load(void) __banked;
|
||||
void pop_pal_story_load(void) __banked;
|
||||
/* Фон текстовой рамки story лежит в отдельном индексе палитры: ending=1
|
||||
* красит его тёмно-красным (финал), 0 оставляет файловый тёмно-синий. */
|
||||
#define POP_PAL_STORY_BG 16
|
||||
void pop_pal_story_load(uint8_t ending) __banked;
|
||||
void pop_pal_pv_load(void) __banked;
|
||||
|
||||
/* Показ текущего снимка: 0 — исходная яркость, 4 — чёрный. */
|
||||
|
||||
@@ -55,6 +55,10 @@ uint8_t pop_mus_id;
|
||||
/* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */
|
||||
volatile uint8_t pop_snd_tick;
|
||||
|
||||
/* Размер КОЛЬЦА страниц для потоковой музыки, 0 — обычный трек целиком в
|
||||
* памяти. Читает насос, ставит загрузчик (pop_music.c). */
|
||||
uint8_t pop_mus_ring;
|
||||
|
||||
/* Заявка на музыкальный трек: номер в нумерации оригинала, 0 — нет заявки
|
||||
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
|
||||
uint8_t pop_mus_req;
|
||||
@@ -141,8 +145,18 @@ int pop_sfx_fill(uint16_t n)
|
||||
pop_mus_ptr += n;
|
||||
if (pop_mus_ptr >= 0x4000) {
|
||||
pop_mus_ptr = 0;
|
||||
if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) pop_mus_pg++;
|
||||
else pop_mus_left = 0;
|
||||
/* КОЛЬЦО. Финальная тема — 1,2 МБ, в память целиком не влезает
|
||||
* никак, поэтому она играется из нескольких страниц по кругу, а
|
||||
* загрузчик дочитывает в те, что мы уже прошли (pop_music.c).
|
||||
* Конец трека определяет left, а не номер страницы. */
|
||||
if (pop_mus_ring) {
|
||||
if ((uint8_t)(pop_mus_pg + 1) < pop_mus_ring) pop_mus_pg++;
|
||||
else pop_mus_pg = 0;
|
||||
} else if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) {
|
||||
pop_mus_pg++;
|
||||
} else {
|
||||
pop_mus_left = 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,26 @@ static int title_draw(uint8_t scene, uint8_t show)
|
||||
return page;
|
||||
}
|
||||
|
||||
/* draw_full_image(TITLE_MAIN) + transition_ltr() финала (seg001:590 и
|
||||
* :629): титульный рисунок проявляется полосами поверх текущего экрана.
|
||||
* Палитру не трогаем — story.pal и title.pal это одни и те же цвета
|
||||
* (обе сделаны из res51). -1 — архив не прочитан. */
|
||||
int pop_title_main_ltr(void) __banked
|
||||
{
|
||||
if (title_draw(TITLE_MAIN, 0) < 0) return -1;
|
||||
pop_screen_present_ltr();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Тот же приём для композиции с логотипом и подписью Jordan Mechner: её
|
||||
* show_title() показывает МЕЖДУ «свадьбой» и титрами (seg000:2045). */
|
||||
int pop_title_logo_ltr(void) __banked
|
||||
{
|
||||
if (title_draw(TITLE_LOGO, 0) < 0) return -1;
|
||||
pop_screen_present_ltr();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t title_wait(uint16_t ticks)
|
||||
{
|
||||
while (ticks--) {
|
||||
|
||||
@@ -14,4 +14,14 @@
|
||||
* напрямую; 0 — последовательность завершилась штатно, продолжать intro. */
|
||||
uint8_t pop_title_show(void) __banked;
|
||||
|
||||
/* Одна титульная картинка (TITLE_MAIN) полосами слева направо — так финал
|
||||
* возвращается к ней после HAIL и после ввода имени в таблицу рекордов.
|
||||
* Палитру не меняет: story.pal и title.pal сделаны из одного res51.
|
||||
* -1 — архив титров не прочитан. */
|
||||
int pop_title_main_ltr(void) __banked;
|
||||
|
||||
/* Композиция с логотипом PRINCE OF PERSIA и Jordan Mechner тем же
|
||||
* переходом — шаг show_title() между «свадьбой» и титрами. */
|
||||
int pop_title_logo_ltr(void) __banked;
|
||||
|
||||
#endif /* POP_TITLE_H */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <kbd_raw.h>
|
||||
#include <graphics.h>
|
||||
#include <gfx.h>
|
||||
#include <sprite.h>
|
||||
@@ -20,11 +21,19 @@ typedef struct {
|
||||
uint8_t base;
|
||||
uint8_t ascent;
|
||||
uint8_t space;
|
||||
uint8_t first; /* первый код, который есть в наборе */
|
||||
uint8_t last;
|
||||
} pop_font_desc_t;
|
||||
|
||||
/* Тёмный набор — тот же мелкий шрифт другим цветом; глифов в нём меньше
|
||||
* (см. pop_font.h), поэтому диапазон кодов хранится у каждого набора. */
|
||||
static const pop_font_desc_t font_desc[] = {
|
||||
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE },
|
||||
{ POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE }
|
||||
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE,
|
||||
POP_FONT_FIRST, POP_FONT_LAST },
|
||||
{ POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
|
||||
POP_FONT_FIRST, POP_FONT_LAST },
|
||||
{ POP_FONT_DARK_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
|
||||
POP_FONT_DARK_FIRST, POP_FONT_DARK_LAST }
|
||||
};
|
||||
|
||||
static atlas_t font_at;
|
||||
@@ -38,8 +47,8 @@ __at (0x4000) uint8_t pop_pal_buf[256];
|
||||
|
||||
static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
|
||||
{
|
||||
if (ch < POP_FONT_FIRST || ch > POP_FONT_LAST) ch = '?';
|
||||
return (uint8_t)(font->base + ch - POP_FONT_FIRST);
|
||||
if (ch < font->first || ch > font->last) ch = '?';
|
||||
return (uint8_t)(font->base + ch - font->first);
|
||||
}
|
||||
|
||||
int pop_ui_init(void) __banked
|
||||
@@ -339,6 +348,37 @@ void pop_screen_present(uint8_t page) __banked
|
||||
gfx_set_visible_page(page);
|
||||
}
|
||||
|
||||
/* transition_ltr() SDLPoP: подготовленная СКРЫТАЯ страница переползает на
|
||||
* видимую узкими вертикальными полосами слева направо. У оригинала
|
||||
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1,3 с), не
|
||||
* удваивая число полных копий 320x256.
|
||||
*
|
||||
* Страницы при этом НЕ меняются местами: копируем в видимую, как оригинал
|
||||
* копирует offscreen в onscreen. Поэтому у вызывающего после перехода на
|
||||
* ЭКРАНЕ и в скрытой странице лежит одно и то же. */
|
||||
void pop_screen_present_ltr(void) __banked
|
||||
{
|
||||
gfx_rect_t stripe;
|
||||
uint8_t visible = gfx_get_visible_page();
|
||||
int x;
|
||||
|
||||
gfx_set_draw_page(visible); /* source gfx_copy_page = target */
|
||||
stripe.y = 0;
|
||||
stripe.h = GFX_HEIGHT;
|
||||
for (x = 0; x < GFX_WIDTH; x += 4) {
|
||||
if (kbd_raw_any_down()) {
|
||||
/* Прервали — дорисовать остаток разом, а не бросить полукадр. */
|
||||
stripe.x = 0; stripe.w = GFX_WIDTH;
|
||||
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
||||
return;
|
||||
}
|
||||
stripe.x = x;
|
||||
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
|
||||
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
}
|
||||
|
||||
/* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих
|
||||
* загрузчиков ниже: они отличаются только тем, откуда взялся атлас. */
|
||||
static int strip_blit(atlas_t *at, int x, int *y)
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
|
||||
typedef enum {
|
||||
POP_TEXT_SMALL = 0,
|
||||
POP_TEXT_BIG = 1
|
||||
POP_TEXT_BIG = 1,
|
||||
/* Крупный шрифт вторым цветом (POP_FONT_DARK_COLOR): тень строки и
|
||||
* текст на светлом фоне — так show_hof_text() SDLPoP рисует таблицу
|
||||
* рекордов дважды (тень со сдвигом (1,1), затем сам текст). Набор
|
||||
* обрезан по 'Z', см. pop_font.h. */
|
||||
POP_TEXT_BIG_DARK = 2
|
||||
} pop_text_font_t;
|
||||
|
||||
/* Жизненный цикл единственного FONT\FONT.ATL. Загрузка — в холодном boot,
|
||||
@@ -86,6 +91,11 @@ void pop_screen_fill_game(void) __banked;
|
||||
* входа в комнату. */
|
||||
void pop_screen_fill_field(void) __banked;
|
||||
void pop_screen_present(uint8_t page) __banked;
|
||||
/* Показать подготовленную скрытую страницу полосами слева направо
|
||||
* (transition_ltr оригинала). Любая зажатая клавиша достраивает переход
|
||||
* мгновенно. Страницы местами не меняются: содержимое копируется в
|
||||
* видимую, поэтому после возврата обе страницы одинаковы. */
|
||||
void pop_screen_present_ltr(void) __banked;
|
||||
|
||||
/* Нарисовать в уже выбранную draw-страницу набор вертикальных полос SPA1.
|
||||
* Полоса загружается, блитится и тут же освобождается; функция не вызывает
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#ifndef PV_ARC_H
|
||||
#define PV_ARC_H
|
||||
|
||||
#define ARC_PV_COUNT 78
|
||||
#define ARC_PV_COUNT 83
|
||||
|
||||
#define ARC_PV_S0_0 0
|
||||
#define ARC_PV_S0_1 1
|
||||
@@ -30,58 +30,63 @@
|
||||
#define ARC_PV_S4_2 22
|
||||
#define ARC_PV_S4_3 23
|
||||
#define ARC_PV_S4_4 24
|
||||
#define ARC_PV_P0_0 25
|
||||
#define ARC_PV_P0_1 26
|
||||
#define ARC_PV_P0_2 27
|
||||
#define ARC_PV_P0_3 28
|
||||
#define ARC_PV_P0_4 29
|
||||
#define ARC_PV_P1_0 30
|
||||
#define ARC_PV_P1_1 31
|
||||
#define ARC_PV_P1_2 32
|
||||
#define ARC_PV_P1_3 33
|
||||
#define ARC_PV_P1_4 34
|
||||
#define ARC_PV_P2_0 35
|
||||
#define ARC_PV_P2_1 36
|
||||
#define ARC_PV_P2_2 37
|
||||
#define ARC_PV_P2_3 38
|
||||
#define ARC_PV_P2_4 39
|
||||
#define ARC_PV_P3_0 40
|
||||
#define ARC_PV_P3_1 41
|
||||
#define ARC_PV_P3_2 42
|
||||
#define ARC_PV_P3_3 43
|
||||
#define ARC_PV_P3_4 44
|
||||
#define ARC_PV_P4_0 45
|
||||
#define ARC_PV_P4_1 46
|
||||
#define ARC_PV_P4_2 47
|
||||
#define ARC_PV_P4_3 48
|
||||
#define ARC_PV_P4_4 49
|
||||
#define ARC_PV_P5_0 50
|
||||
#define ARC_PV_P5_1 51
|
||||
#define ARC_PV_P5_2 52
|
||||
#define ARC_PV_P5_3 53
|
||||
#define ARC_PV_P5_4 54
|
||||
#define ARC_PV_P6_0 55
|
||||
#define ARC_PV_P6_1 56
|
||||
#define ARC_PV_P6_2 57
|
||||
#define ARC_PV_P6_3 58
|
||||
#define ARC_PV_P6_4 59
|
||||
#define ARC_PV_P7_0 60
|
||||
#define ARC_PV_P7_1 61
|
||||
#define ARC_PV_P7_2 62
|
||||
#define ARC_PV_P7_3 63
|
||||
#define ARC_PV_P7_4 64
|
||||
#define ARC_PV_B0_0 65
|
||||
#define ARC_PV_B0_1 66
|
||||
#define ARC_PV_B0_2 67
|
||||
#define ARC_PV_B0_3 68
|
||||
#define ARC_PV_B0_4 69
|
||||
#define ARC_PV_A0 70
|
||||
#define ARC_PV_A1 71
|
||||
#define ARC_PV_A2 72
|
||||
#define ARC_PV_J0 73
|
||||
#define ARC_PV_J1 74
|
||||
#define ARC_PV_J2 75
|
||||
#define ARC_PV_H0 76
|
||||
#define ARC_PV_T0 77
|
||||
#define ARC_PV_S5_0 25
|
||||
#define ARC_PV_S5_1 26
|
||||
#define ARC_PV_S5_2 27
|
||||
#define ARC_PV_S5_3 28
|
||||
#define ARC_PV_S5_4 29
|
||||
#define ARC_PV_P0_0 30
|
||||
#define ARC_PV_P0_1 31
|
||||
#define ARC_PV_P0_2 32
|
||||
#define ARC_PV_P0_3 33
|
||||
#define ARC_PV_P0_4 34
|
||||
#define ARC_PV_P1_0 35
|
||||
#define ARC_PV_P1_1 36
|
||||
#define ARC_PV_P1_2 37
|
||||
#define ARC_PV_P1_3 38
|
||||
#define ARC_PV_P1_4 39
|
||||
#define ARC_PV_P2_0 40
|
||||
#define ARC_PV_P2_1 41
|
||||
#define ARC_PV_P2_2 42
|
||||
#define ARC_PV_P2_3 43
|
||||
#define ARC_PV_P2_4 44
|
||||
#define ARC_PV_P3_0 45
|
||||
#define ARC_PV_P3_1 46
|
||||
#define ARC_PV_P3_2 47
|
||||
#define ARC_PV_P3_3 48
|
||||
#define ARC_PV_P3_4 49
|
||||
#define ARC_PV_P4_0 50
|
||||
#define ARC_PV_P4_1 51
|
||||
#define ARC_PV_P4_2 52
|
||||
#define ARC_PV_P4_3 53
|
||||
#define ARC_PV_P4_4 54
|
||||
#define ARC_PV_P5_0 55
|
||||
#define ARC_PV_P5_1 56
|
||||
#define ARC_PV_P5_2 57
|
||||
#define ARC_PV_P5_3 58
|
||||
#define ARC_PV_P5_4 59
|
||||
#define ARC_PV_P6_0 60
|
||||
#define ARC_PV_P6_1 61
|
||||
#define ARC_PV_P6_2 62
|
||||
#define ARC_PV_P6_3 63
|
||||
#define ARC_PV_P6_4 64
|
||||
#define ARC_PV_P7_0 65
|
||||
#define ARC_PV_P7_1 66
|
||||
#define ARC_PV_P7_2 67
|
||||
#define ARC_PV_P7_3 68
|
||||
#define ARC_PV_P7_4 69
|
||||
#define ARC_PV_B0_0 70
|
||||
#define ARC_PV_B0_1 71
|
||||
#define ARC_PV_B0_2 72
|
||||
#define ARC_PV_B0_3 73
|
||||
#define ARC_PV_B0_4 74
|
||||
#define ARC_PV_A0 75
|
||||
#define ARC_PV_A1 76
|
||||
#define ARC_PV_A2 77
|
||||
#define ARC_PV_J0 78
|
||||
#define ARC_PV_J1 79
|
||||
#define ARC_PV_J2 80
|
||||
#define ARC_PV_H0 81
|
||||
#define ARC_PV_T0 82
|
||||
|
||||
#endif
|
||||
|
||||
@@ -294,15 +294,11 @@ int main(void)
|
||||
}
|
||||
}
|
||||
if (pop_app_state == POP_APP_INTRO) {
|
||||
/* Таблица рекордов между credits и demo показывается ВНУТРИ
|
||||
* pop_intro_show — там же, где её показывает show_title(). */
|
||||
uint8_t skipped = pop_intro_show();
|
||||
pop_app_event_t ev;
|
||||
|
||||
if (!skipped) {
|
||||
/* show_title() SDLPoP помещает сохранённую таблицу после credits
|
||||
* и до attract-demo. При skip таблица тоже пропускается. */
|
||||
pop_hof_show_title();
|
||||
}
|
||||
ev = skipped ? POP_APP_EV_NEW_GAME : POP_APP_EV_CUTSCENE_FINISHED;
|
||||
pop_app_event_t ev = skipped ? POP_APP_EV_NEW_GAME
|
||||
: POP_APP_EV_CUTSCENE_FINISHED;
|
||||
if (!pop_app_dispatch(ev)) {
|
||||
failed = 1;
|
||||
break;
|
||||
|
||||
@@ -100,16 +100,16 @@ TC_TEST(restart_game_returns_to_intro)
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
/* Финал ВРЕМЕННО ведёт мимо Hall of Fame прямо в title: экран HOF после
|
||||
* победы показывает чёрное, и застревать в нём хуже, чем не показывать
|
||||
* (решение пользователя 2026-08-24, BUGS_OPEN.md#hof-after-ending). Сам
|
||||
* маршрут HOF -> TITLE остаётся живым: в него ходит title-экран. */
|
||||
/* Финал уходит в таблицу рекордов: она же дослушивает победную тему и
|
||||
* гасит экран, как хвост end_sequence (seg001:5F1). */
|
||||
TC_TEST(ending_to_title_route)
|
||||
{
|
||||
pop_app_state = POP_APP_PLAYING;
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED));
|
||||
TC_EQ(pop_app_state, POP_APP_ENDING);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_HALL_OF_FAME);
|
||||
TC_TRUE(pop_app_dispatch(POP_APP_EV_HOF_FINISHED));
|
||||
TC_EQ(pop_app_state, POP_APP_TITLE);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# ===========================================================================
|
||||
# SprPoP — .gitignore приложения
|
||||
# ===========================================================================
|
||||
# Пока SprPoP лежит внутри репозитория C-Compiler, git читает этот файл как
|
||||
# правила ДЛЯ ЭТОГО ПОДДЕРЕВА, и он дополняет корневой .gitignore тулчейна.
|
||||
# Когда SprPoP выделят в самостоятельный проект, файл станет КОРНЕВЫМ и
|
||||
# продолжит работать как есть: все пути написаны от корня SprPoP и ничего
|
||||
# не знают ни про applications/, ни про раскладку тулчейна.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ЧУЖИЕ ДАННЫЕ — в репозиторий не идут никогда
|
||||
# ---------------------------------------------------------------------------
|
||||
# assets/orig/ — оригинальные данные игры (копирайт). В репозитории вместо
|
||||
# них лежит ЗНАНИЕ, откуда их взять: tools/fetch_orig.py (`make fetch`)
|
||||
# качает SDLPoP и записи саундтрека, MSDOS/ пользователь кладёт сам (он
|
||||
# необязателен). Скачанные архивы кэшируются в assets/orig/.cache/.
|
||||
#
|
||||
# Шаблон со ЗВЁЗДОЧКОЙ, а не `assets/orig/`: git не заходит внутрь
|
||||
# игнорируемого каталога, и исключение ниже иначе не сработало бы, а
|
||||
# README.md — единственное, что отсюда ОБЯЗАНО быть в репозитории: без
|
||||
# него в чистом клоне не написано, откуда брать данные.
|
||||
assets/orig/*
|
||||
!assets/orig/README.md
|
||||
|
||||
# Уровни res20xx.bin ложатся на диск ПРЯМО из оригинала, конвертации нет —
|
||||
# то есть это тот же копирайтный материал, только в раскладке диска.
|
||||
# Остальное содержимое assets/packed/ версионируется намеренно: без него из
|
||||
# чистого клона не собрать игру, а исходников (assets/orig/) может не быть.
|
||||
assets/packed/LEVELS/
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ВЫХОД СБОРКИ
|
||||
# ---------------------------------------------------------------------------
|
||||
# build/ — .exe, раскладка диска, hdd/, промежуточные атласы, stamp'ы.
|
||||
# Правило без слэша спереди намеренно: под ним и tests/host/build/.
|
||||
build/
|
||||
|
||||
# Промежуточный каталог sprinter-cc.
|
||||
.sprinter-cc-*/
|
||||
|
||||
# Артефакты компиляции, которые SDCC кладёт рядом с целью — в корень
|
||||
# приложения. Только корень: единственный версионируемый ассемблерный
|
||||
# файл (tests/host/bank_stub.s) лежит глубже и под правило не попадает.
|
||||
/*.exe
|
||||
/*.asm
|
||||
/*.lst
|
||||
/*.lk
|
||||
/*.ihx
|
||||
/*.noi
|
||||
/*.sym
|
||||
/*.map
|
||||
/*.rel
|
||||
/*.cdb
|
||||
/*.mem
|
||||
/*.rst
|
||||
|
||||
# Хостовые тесты и утилиты собираются нативным cc.
|
||||
*.o
|
||||
*.obj
|
||||
*.dSYM/
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ОС / редакторы / локальные настройки
|
||||
# ---------------------------------------------------------------------------
|
||||
.DS_Store
|
||||
._*
|
||||
*~
|
||||
*.swp
|
||||
*.bak
|
||||
*.orig
|
||||
*.rej
|
||||
.vscode/
|
||||
.idea/
|
||||
.kilo/
|
||||
.claude/
|
||||
@@ -0,0 +1,217 @@
|
||||
# SprPoP — правила приложения
|
||||
|
||||
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
|
||||
sprinter-cc / libc / libbgi. Действуют правила корневого `CLAUDE.md`
|
||||
(сборка, libc, ABI, MAME-автотест); ниже — только специфика порта.
|
||||
Общение и комментарии — на русском.
|
||||
|
||||
Приложение **автономно**: код, оригинальные данные, конверторы и сборка
|
||||
лежат внутри этой папки. Наружу оно знает ровно один путь — корень
|
||||
тулчейна, `SPRINTER_ROOT` (по умолчанию `../..`).
|
||||
|
||||
История: SprPoP выделен из `applications/PoP/roomtest` 2026-08-26. Та
|
||||
папка ЗАМОРОЖЕНА и остаётся архивом (закрытые задачи и баги с разбором
|
||||
корней — `../PoP/roomtest/TASKS_CLOSED.md` и `BUGS_CLOSED.md`; там же
|
||||
исполненные планы в `../PoP/docs/`). Разработка идёт только здесь.
|
||||
|
||||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
||||
|
||||
**`assets/orig/SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ
|
||||
авторитетный источник того, как оригинальный движок это делает.** В
|
||||
репозитории его нет: заполняется `make fetch` (`make fetch-check` — что уже
|
||||
на месте). Правило без исключений:
|
||||
|
||||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
||||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
||||
константы) — СНАЧАЛА найди и прочитай соответствующий код в SDLPoP и
|
||||
портируй по нему. Не пиши по памяти, не выводи логику «из общих
|
||||
соображений», не угадывай значения — это источник багов, которые потом
|
||||
ловятся в MAME часами.
|
||||
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
|
||||
в каком порядке слои, когда меняется тайл, какая скорость/задержка, что
|
||||
делает такой-то кадр анимации) — ответ ищи в SDLPoP, а не строй гипотезу.
|
||||
Если не нашёл — это повод копать дальше в исходнике, а не додумывать.
|
||||
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
|
||||
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
|
||||
зафиксировать почему в комментарии **и записью в `docs/impl_diff.md`**:
|
||||
что делает оригинал, что делаем мы, чем платим, что проверять при
|
||||
регрессе).
|
||||
|
||||
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
|
||||
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
|
||||
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
|
||||
См. memory `pop_check_sdlpop_first`.
|
||||
|
||||
Вторичные референсы (когда в SDLPoP непонятно или нужен другой ракурс)
|
||||
лежат вне приложения, в `../PoP/`: `Prince-of-Persia-Apple-II/` —
|
||||
оригинальный 6502-исходник 1989 (Мехнер), `PR/` — Princed Resources,
|
||||
`mininim/` — независимая реализация. Все они — **справочник логики,
|
||||
структур и констант**, но НЕ код для копирования (лицензии несовместимы,
|
||||
наш ABI другой): читаем и переписываем под наш движок.
|
||||
|
||||
## Доски
|
||||
|
||||
**Что в работе сейчас — `docs/TASKS_OPEN.md`** (приоритеты, критерии
|
||||
готовности). Баги — `docs/BUGS_OPEN.md` (только ОТКРЫТЫЕ).
|
||||
|
||||
Правило разнесения: как только задача или баг закрыт — запись целиком
|
||||
переезжает в архив roomtest (`../PoP/roomtest/TASKS_CLOSED.md` /
|
||||
`BUGS_CLOSED.md`), а в открытом файле остаётся ссылка. Перед заведением
|
||||
нового бага грепни архив по симптому: корень мог уже разбираться.
|
||||
Открытые файлы обязаны читаться целиком за раз.
|
||||
|
||||
Индекс документации с отметками актуальности — **`docs/README.md`**,
|
||||
начинать чтение оттуда.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
```
|
||||
make собрать build/sprpop.exe и разложить build/
|
||||
make hdd + образ build/hdd/sprpop.chd
|
||||
make mame-link однократно: подставить образ в MAME (см. ниже)
|
||||
make -C tests/host модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
||||
make test-tools тесты упаковщиков на хосте (форматы, которые читает Z80)
|
||||
make fetch скачать внешние данные в assets/orig/ (SDLPoP + музыка)
|
||||
make resources перегенерировать ресурсы из assets/orig/
|
||||
make music-mp3 музыка из другого набора (flac|mp3|ogg|mt32)
|
||||
make clean снести build/ (ассеты не трогает)
|
||||
make distclean clean + снести assets/packed/ (вернуть — make resources)
|
||||
```
|
||||
|
||||
`MEMORY=huge`, `--gfx 256`, 11 банков кода в W3. Отладочные ключи сборки:
|
||||
`make LEVEL=9` (стартовый уровень), `make ROOM=15 POS=2` (стартовая комната
|
||||
и тайл `row*10+col`), `make PROF=1` (профиль фаз кадра полосами бордюра),
|
||||
`make ALLOCS=100000` (плотная упаковка регистров — для замеров размера).
|
||||
Сравнивать занятость банков можно **только при одном `ALLOCS`**.
|
||||
|
||||
**Ловушка `make mame-link`:** он подменяет `mame/v306/IMG/test_hdd.chd`
|
||||
символьной ссылкой на наш образ. Пока ссылка стоит, `make hdd` любого
|
||||
другого приложения (в т.ч. замороженной roomtest) писал бы через неё в
|
||||
наш `build/hdd/` — если понадобится собрать чужой образ, ссылку сначала
|
||||
убрать.
|
||||
|
||||
## Раскладка папки
|
||||
|
||||
| Папка | Что |
|
||||
|---|---|
|
||||
| `src/` | рукописный C: главный цикл `sprpop.c`, холодная половина `sprpop_cold.c`, движок `pop_*.c` |
|
||||
| `gen/` | генерируемые заголовки (индексы архивов, таблицы кадров, шрифт, палитры). **Руками не править** — их печатают упаковщики; лежат в репозитории, потому что без них `src/` не собрать |
|
||||
| `assets/orig/` | оригинальные данные (SDLPoP, записи музыки; MSDOS — необязательно). Вне репозитория — копирайт; вместо них в репозитории лежит `tools/fetch_orig.py`, который их качает (`make fetch`). Правила — в `assets/orig/README.md` |
|
||||
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
|
||||
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках. Адреса ВНЕШНИХ источников — так же в одном: `tools/fetch_orig.py` |
|
||||
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
|
||||
| `tests/host/` | модульные тесты движка под ucsim_z80 |
|
||||
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
|
||||
|
||||
Звуковые ЭФФЕКТЫ по умолчанию берутся из SDLPoP (`SND_SRC=sdlpop`) — сборка
|
||||
обязана работать без оригинального дистрибутива DOS. У кого лежит
|
||||
`assets/orig/MSDOS/`, включает его набор явно (`make SND_SRC=msdos`): там
|
||||
оцифровка полнее — в SDLPoP пуст звук 48 `spiked` (насаживание на пики).
|
||||
Разбор — `docs/sound_plan.md`.
|
||||
|
||||
Музыка собирается из одного из четырёх наборов записей
|
||||
(`assets/orig/PoP1_DOS_music/`): `flac` по умолчанию, плюс `mp3`, `ogg` и
|
||||
`mt32` (исполнение Roland MT-32 — звучит иначе, длина треков другая). Набор
|
||||
входит в имя stamp'а, поэтому `make music-mt32` пересобирает музыку сам, без
|
||||
`-B`; разовая сборка без смены умолчания — `make MUSIC_FMT=ogg resources-music`.
|
||||
|
||||
Ресурсы: упаковщик пишет промежуточные атласы в `build/atl/<набор>/`, из них
|
||||
Makefile склеивает архивы прямо в `assets/packed/<КАТАЛОГ>/`, а `make`
|
||||
копирует их в `build/`. Порядок файлов в списках `*_ATL` Makefile — это
|
||||
порядок индексов внутри архива, и он же напечатан в `gen/*_arc.h`: менять
|
||||
порядок нельзя, не перегенерировав заголовок.
|
||||
|
||||
**Каноническая спецификация форматов `.DAT`** —
|
||||
`docs/PoP/POP-DAT-FormatSpecifications.pdf` (грепаемая копия — `.txt`):
|
||||
первоисточник Princed для DAT v1.0. Наши разборы
|
||||
(`docs/PoP/MSDOS_RESOURCE_FORMAT.md`, `docs/PoP/APPLEII_RESOURCE_FORMAT.md`) —
|
||||
практические заметки; при расхождении источник истины — спецификация.
|
||||
|
||||
## Проверка в MAME
|
||||
|
||||
Только через `toolchain/mame_interactive.py` или MCP-мост `mame-z80`
|
||||
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
||||
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
||||
|
||||
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
|
||||
старт `run_bridge.sh` → 6 с → `go` → 8 с → `keyseq d:{ENTER}` +
|
||||
`keyseq sprpop{ENTER}` → 5 с → программа работает.
|
||||
|
||||
Отладочные тумблеры в живой сессии (`src/sprpop.c`): **1** — заморозить
|
||||
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
|
||||
(`src/pop_cheat.h`): **K** — убить стража, **I** — бессмертие,
|
||||
**Shift+L** — следующий уровень, **+/−** — обход комнат (`ROOMNAV`).
|
||||
Не чит, а штатная клавиша оригинала: **Ctrl+S** — вкл/выкл звук (пурпурная
|
||||
палочка в борте = звука нет). Целевая раскладка управления, к которой
|
||||
подгоняем SprPoP, — `docs/keys.txt`.
|
||||
|
||||
Логику, которую можно проверить без железа, покрывать в `tests/host/`
|
||||
(обвязка — `testkit/`). MAME остаётся для отрисовки, банков, таймингов и
|
||||
клавиатуры.
|
||||
|
||||
## Модули
|
||||
|
||||
| Файл | Роль (порт SDLPoP) |
|
||||
|------|--------------------|
|
||||
| `sprpop.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), порядок tick→draw. |
|
||||
| `sprpop_cold.c` | Холодная половина главного цикла (банк 8): старт уровня, отладочная навигация. |
|
||||
| `pop_tile.c/.h` | Общие ЛИСТЬЯ слоя фона — РЕЗИДЕНТ W1: блит куска атласа, чтение тайла комнаты, `tile_table`, метка «фон трогали вот здесь» (`pop_cd_touch`). Резидент потому, что их зовут обе половины слоя фона из разных банков, а `const`-таблицы банка из чужого банка не видны. |
|
||||
| `pop_bg.c/.h` | ГОРЯЧАЯ половина слоя фона (банк 2, каждый кадр): fore-окклюзия поверх ЛЮБОГО персонажа (`seg003` redraw_at_char/char2 — один проход на всех Char), оверлеи кромки, кладка стены (`wall_pattern`), клип. `pop_bg.h` — публичный API ВСЕГО слоя. |
|
||||
| `pop_room.c` | ХОЛОДНАЯ половина слоя фона (банк 7, раз на комнату/событие): полная отрисовка комнаты (`seg008` draw_tile), точечные перерисовки тайлов (пики, ворота, кнопки, дверь уровня, дрожащие плиты), падающие куски loose (`seg007` mob), загрузка атласов. |
|
||||
| `_pop_bg.h` | Внутренний контракт между половинами: общее состояние + три тонкие `__banked`-обёртки; там же почему рез прошёл именно здесь и сколько стоит. |
|
||||
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. |
|
||||
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||
| `pop_sfx.c` + `pop_sfx_cold.c` | Звук: насос CBL в резиденте + холодная загрузка набора. Раскладка эффектов НЕ компилируется в EXE — читается с диска (`SND/snd.idx`, формат `PSI1`); см. `docs/sound_plan.md` §10. |
|
||||
| `pop_music.c` | Музыка (банк 9): загрузка треков, кольцо для финальной темы. Длины и длительности — из `MUS/mus.idx` (EMM-страница + accessor `pop_music_info`), в EXE их нет; см. `docs/sound_plan.md` §11. |
|
||||
| `pop_snd_tbl.h` + `pop_snd_data.c` | Тип записи набора и инварианты (рукописный заголовок) + резидентные `pop_snd_tbl`/`pop_snd_page`/`pop_snd_pages`. Размер записи 5 байт — часть дискового контракта, проверяется статически. |
|
||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||
|
||||
## Банки: две мины, которых компилятор не видит
|
||||
|
||||
Обе стреляют молча, обе поймали нас 2026-08-27 (разбор — в
|
||||
`../PoP/roomtest/BUGS_CLOSED.md`, поиск «FINAL-BANKCALL»).
|
||||
|
||||
**1. Прямой вызов в чужой банк.** Трамплин SDCC выбирает по `__banked`
|
||||
в ОБЪЯВЛЕНИИ, а не по тому, где функция лежит. Функция без пометки
|
||||
зовётся прямым `call` — верно ровно пока вызывающий и вызываемый в одном
|
||||
банке. Перенесли модуль ради разгрузки банка — и тот же `call` уходит в
|
||||
пустой хвост чужого банка, процессор ползёт по 0xFF до 0x0000, DSS убивает
|
||||
процесс. Ловит `toolchain/check_bank_calls.py`, она встроена в сборку и
|
||||
ВАЛИТ её (проверено намеренной поломкой).
|
||||
|
||||
**2. Указатель на данные своего банка, отданный в чужой.** Обратная
|
||||
сторона: пока обе стороны в одном банке, указатель на литерал или
|
||||
const-таблицу работает; пометили функцию `__banked` — и трамплин на время
|
||||
вызова переключает W3, а указатель показывает уже в чужой банк. Так экран
|
||||
таблицы рекордов залило знаками вопроса: в `pop_text_draw_mapped` (банк 9)
|
||||
уходил литерал `"_"` из банка 10.
|
||||
|
||||
**Правило:** всё, что уходит указателем в другой банк, обязано лежать в
|
||||
W2 — стек, `_DATA` или копия. `const`-таблицы и строковые литералы
|
||||
банкового модуля наружу отдавать нельзя (то же, что уже записано про
|
||||
`pop_tile.c`). Вторая проверка скрипта предупреждает о явных случаях, но
|
||||
она эвристическая и гарантией не является.
|
||||
|
||||
## Порядок слоёв в кадре (важно для окклюзии)
|
||||
|
||||
`sprpop.c` каждый тик рисует в СКРЫТУЮ страницу: `pop_char_heal` (стереть
|
||||
прошлый кадр — по слоту на персонажа) → `kid_tick` → `pop_phys_tick` →
|
||||
`pop_loose_tick` (loose СЛОЙ ФОНА — ДО персонажей, чтобы они были поверх
|
||||
плиты) → `pop_char_draw`/`pop_char_fore` для соперника и Кида (кто позже —
|
||||
тот поверх, порядок задаёт обход тайлов) → `pop_room_clip_borders` (чистка
|
||||
бортов, гейт по флагу — только в кадрах падения) → флип на vsync.
|
||||
|
||||
**Дабл-буфер:** у каждой страницы своя видео-ОЗУ и теневая ОЗУ-копия; heal
|
||||
берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой
|
||||
динамический элемент (Kid, loose-плита, падающий кусок) обязан чистить свой
|
||||
прошлый кадр на КАЖДОЙ из двух страниц — иначе остаток виден через кадр как
|
||||
**мерцание**. Типовой источник багов «остаётся кусочек» (memory
|
||||
`pop_fall_debug_baseline`).
|
||||
|
||||
## Ключевые memory
|
||||
|
||||
`pop_port_project`, `pop_check_sdlpop_first`, `pop_banking_architecture`,
|
||||
`pop_background_strategy`, `pop_kid_plan`, `pop_hang_state`, `pop_fore_layer`,
|
||||
`pop_fall_debug_baseline`, `accel_vertical_copy`, `kbd_raw_fifo_drain`,
|
||||
`pop_perf_registry`.
|
||||
@@ -0,0 +1,627 @@
|
||||
# SprPoP — порт Prince of Persia на Sprinter Sp2000.
|
||||
#
|
||||
# Приложение АВТОНОМНО: весь код, все исходные ресурсы, все конверторы лежат
|
||||
# здесь. Наружу оно знает ровно один путь — корень тулчейна sprinter-cc:
|
||||
#
|
||||
# export SPRINTER_ROOT=/путь/к/C-Compiler
|
||||
# make
|
||||
#
|
||||
# Без переменной берётся ../.. (раскладка applications/SprPoP внутри дерева
|
||||
# компилятора), так что в родном дереве ничего настраивать не надо.
|
||||
#
|
||||
# Раскладка:
|
||||
# src/ рукописный C
|
||||
# gen/ генерируемые заголовки (в репозитории: без них src/ не
|
||||
# собрать, а генератору нужны оригиналы из assets/orig/)
|
||||
# assets/orig/ оригинальные данные — SDLPoP, MSDOS, записи музыки.
|
||||
# НЕ в репозитории (копирайт), см. assets/orig/README.md
|
||||
# assets/packed/ то, что ложится на диск игры, в раскладке диска
|
||||
# tools/ конверторы ресурсов
|
||||
# build/ ВЫХОД: sprpop.exe + BG/ KID/ ... + hdd/sprpop.chd
|
||||
#
|
||||
# Основные цели:
|
||||
# make собрать sprpop.exe и разложить build/
|
||||
# make hdd + образ build/hdd/sprpop.chd
|
||||
# make mame-link однократно связать образ с MAME (см. ниже)
|
||||
# make resources перегенерировать ресурсы из assets/orig/
|
||||
# make music-mp3 музыка из другого набора записей (flac|mp3|ogg|mt32)
|
||||
# make clean снести build/ (ассеты не трогает)
|
||||
# make distclean clean + снести упакованные ассеты assets/packed/
|
||||
|
||||
SPRINTER_ROOT ?= $(abspath $(CURDIR)/../..)
|
||||
PROJ_ROOT := $(SPRINTER_ROOT)
|
||||
EXAMPLE := sprpop
|
||||
SRC_DIR := src
|
||||
BUILD_DIR := build
|
||||
|
||||
# huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3.
|
||||
MEMORY ?= huge
|
||||
# --max-allocs-per-node: во сколько вариантов размещения регистров упирается
|
||||
# SDCC на узел. Дефолт sprinter-cc (100000) на модулях такого размера — это
|
||||
# МИНУТЫ на банк. 6000 — компромисс: сборка дольше секунд на двадцать, зато
|
||||
# резидент W1 худеет примерно на 150 байт против дефолтных 3000 SDCC, а это
|
||||
# ровно те байты, которых не хватает куче.
|
||||
#
|
||||
# make — обычная сборка (ALLOCS=6000)
|
||||
# make ALLOCS=100000 — минимальный код, для замеров размера и релиза
|
||||
#
|
||||
# ВАЖНО: сравнивать занятость банков можно только при ОДНОМ ALLOCS — иначе
|
||||
# сравниваются не правки, а уровни оптимизации.
|
||||
ALLOCS ?= 6000
|
||||
# Build info не хранится в C: ID и дата извлекаются из текущего commit.
|
||||
BUILD_ID ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
|
||||
BUILD_DATE ?= $(shell git log -1 --format=%cs 2>/dev/null || echo unknown)
|
||||
BUILD_FLAGS := -DPOP_BUILD_ID=\"$(BUILD_ID)\" -DPOP_BUILD_DATE=\"$(BUILD_DATE)\"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Банковая раскладка
|
||||
# ---------------------------------------------------------------------------
|
||||
# Единственный список банковых исходников: из него строятся И ключи
|
||||
# sprinter-cc, И зависимости sprpop.exe. Держать два списка нельзя — так уже
|
||||
# ловили stale-бинарник (модуль компилировался, но make не замечал правок).
|
||||
#
|
||||
# Банк 2 = отрисовка (pop_bg + pop_cdraw в соседних банках): вызовы банк→банк
|
||||
# работают, трамплин сохраняет страницу окна на СТЕКЕ. Резидента --w3 нет.
|
||||
# Банк 5 = pop_ctrl.c: кандидат выбран не по размеру, а по частоте вызова —
|
||||
# диспетчер управления дёргается раз в кадр на персонажа и горячих банк→банк
|
||||
# переходов не создаёт (в отличие от pop_level, чей pop_level_tile зовётся из
|
||||
# банка 2 на КАЖДЫЙ тайл).
|
||||
# Банк 9 = оболочка (menu/settings/config/font, автомат, title, attract-demo,
|
||||
# таблица рекордов) — мелкие редкие точки входа, но не рендеры заставок.
|
||||
# Банк 10 = единственный владелец экранных палитр/fade; отделён от UI, потому
|
||||
# что его зовут и игровой lifecycle, и все ветки катсцен.
|
||||
# Банк 11 = тяжёлая кинематика заставок, чтобы анимации не вытесняли
|
||||
# menu/config из банка оболочки.
|
||||
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 := sprpop_cold.c pop_arc.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
|
||||
BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_app.c pop_timer.c \
|
||||
pop_ui.c pop_title.c pop_demo.c pop_flow.c
|
||||
# Банк 10 = палитры + РЕДКОЕ С ФАЙЛАМИ. pop_config (POP.CFG) и pop_hof
|
||||
# (POP.HOF) переехали сюда из банка 9, когда тот дошёл до 98,5% (247 байт
|
||||
# свободно). Критерий тот же, что и у остальной раскладки, — частота, а не
|
||||
# размер: обе вещи работают раз за партию и упираются в диск, где `open`
|
||||
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
||||
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
||||
# 485 байт, то есть 3% страницы.
|
||||
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c pop_path_bank.c
|
||||
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
||||
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
||||
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
||||
BANK_FLAGS := $(foreach n,$(BANK_NUMS),\
|
||||
$(foreach f,$(BANK$(n)_SRCS),--bank $(n)=$(SRC_DIR)/$(f)))
|
||||
|
||||
# Модули резидента (кроме sprpop.c — его подставляет app.mk).
|
||||
EXTRA_SRCS := $(addprefix $(SRC_DIR)/,pop_vflip.c pop_pace.c pop_sfx.c pop_state.c \
|
||||
pop_snd_data.c \
|
||||
pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c \
|
||||
pop_qsave_io.c)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Отладочные ключи
|
||||
# ---------------------------------------------------------------------------
|
||||
# Профилирование полосами бордюра (PROF() в sprpop.c): каждая фаза кадра красит
|
||||
# бордюр в свой цвет, высота полосы на скриншоте MAME = время фазы в строках.
|
||||
PROF ?= 0
|
||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||
# Стартовый уровень: make LEVEL=9 — начать сразу с девятого.
|
||||
LEVEL ?= 1
|
||||
PROF_FLAGS += -DFIRST_LEVEL=$(LEVEL)
|
||||
# Стартовая КОМНАТА и позиция Кида: make ROOM=15 POS=2 — начать прямо в
|
||||
# целевой комнате, минуя проход уровня. POS — тайл 0..29, то есть row*10+col.
|
||||
# make ROOM= (пусто) возвращает штатный старт из данных уровня.
|
||||
ROOM ?=
|
||||
POS ?= 2
|
||||
ifneq ($(strip $(ROOM)),)
|
||||
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
|
||||
endif
|
||||
|
||||
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/gen -I $(CURDIR)/$(SRC_DIR) \
|
||||
--max-allocs $(ALLOCS) $(BANK_FLAGS) $(PROF_FLAGS) $(BUILD_FLAGS)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ресурсы: что уходит на диск
|
||||
# ---------------------------------------------------------------------------
|
||||
# assets/packed/<КАТАЛОГ>/ повторяет раскладку диска (8.3, как в DSS), поэтому
|
||||
# отдельного списка «файл → каталог образа» больше нет: каталог виден из пути.
|
||||
PACKED := assets/packed
|
||||
ATL := $(BUILD_DIR)/atl
|
||||
|
||||
# Промежуточные атласы (в build/atl, в репозиторий не идут) — из них
|
||||
# упаковщик архивов склеивает то, что реально ложится на диск. Порядок в
|
||||
# списках = порядок индексов внутри архива, а их упаковщик печатает в gen/*.h;
|
||||
# менять порядок нельзя, не перегенерировав заголовок.
|
||||
BG_SETS := pop pal
|
||||
BG_PAGES := 0 1 2 3 4 5 6 7 8 9
|
||||
BG_ATL := $(foreach s,$(BG_SETS),\
|
||||
$(foreach n,$(BG_PAGES),$(ATL)/bg/$(s)_env$(n).atl) \
|
||||
$(ATL)/bg/$(s)_wall.atl $(ATL)/bg/$(s)_fore.atl) \
|
||||
$(ATL)/bg/pop_pot.atl
|
||||
# ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23) не
|
||||
# рисуются. Плюс зеркальный ПО ВЕРТИКАЛИ набор (зелье инверсии, уровень 9):
|
||||
# разворот готовит упаковщик, а не рантайм — страница разворачивается 0,59 с
|
||||
# против 0,045 с на её чтение с диска.
|
||||
KID_N := 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
||||
KID_ATL := $(foreach n,$(KID_N),$(ATL)/kid/kid$(n).atl) \
|
||||
$(foreach n,$(KID_N),$(ATL)/kid/kid$(n)_v.atl) \
|
||||
$(ATL)/kid/sword.atl $(ATL)/kid/sword_v.atl
|
||||
GUARD_ATL := $(foreach n,0 1 2 3 4,$(ATL)/guard/g$(n).atl) \
|
||||
$(foreach n,0 1 2 3 4,$(ATL)/guard/g$(n)_v.atl)
|
||||
# Скелет — ОТДЕЛЬНЫЙ набор (SKEL.DAT), его грузит уровень 3; кадров меньше.
|
||||
# Зеркального набора у него нет: зелье инверсии — уровень 9, там обычный страж.
|
||||
SKEL_ATL := $(foreach n,0 1 2 3,$(ATL)/skel/g$(n).atl)
|
||||
# Джафар (VIZIER.DAT) — уровень 13; кадров столько же, сколько у стража, но
|
||||
# палитра своя, из файла набора.
|
||||
VIZIER_ATL := $(foreach n,0 1 2 3 4,$(ATL)/vizier/g$(n).atl)
|
||||
# ТЕНЬ — набор, запечённый упаковщиком (наложение спрайта на себя со сдвигом;
|
||||
# наш блит так не умеет). sk* — кадры вне боя, sf* — кадры 150..189.
|
||||
SHADOW_ATL := $(foreach n,$(KID_N),$(ATL)/shadow/sk$(n).atl) \
|
||||
$(foreach n,0 1 2 3,$(ATL)/shadow/sf$(n).atl)
|
||||
# Звук: оригинальная оцифровка на частоте CBL (10 937,5 Гц, 8 бит моно).
|
||||
# Число файлов задаёт упаковщик (POP_SND_PAGES в gen/pop_sound_tbl.h) —
|
||||
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
|
||||
# Страниц звука СТАТИЧЕСКИ НЕ ПОСЧИТАТЬ: их число зависит от источника
|
||||
# (SDLPoP даёт 9, MSDOS — 10, см. SND_SRC). Поэтому списка s0..sN здесь нет,
|
||||
# а архив зависит от stamp'а упаковщика и берёт страницы из каталога уже
|
||||
# после того, как тот отработал (см. правило SND/snd.arc ниже).
|
||||
TITLE_ATL := $(foreach s,0 1 2 3,$(foreach p,0 1 2 3 4,$(ATL)/title/t$(s)_$(p).atl))
|
||||
# PV/INTRO: story и стадии режутся на полосы; у настоящего PV intro есть ещё
|
||||
# чистый фон b0_* и шесть <=16K actor-атласов — они остаются в EMM на время
|
||||
# сцены, а не перечитывают полный 320x200 кадр с диска.
|
||||
PV_ATL := $(foreach s,0 1 2 3 4 5,$(foreach p,0 1 2 3 4,$(ATL)/pv/s$(s)_$(p).atl)) \
|
||||
$(foreach s,0 1 2 3 4 5 6 7,$(foreach p,0 1 2 3 4,$(ATL)/pv/p$(s)_$(p).atl)) \
|
||||
$(foreach p,0 1 2 3 4,$(ATL)/pv/b0_$(p).atl) \
|
||||
$(ATL)/pv/a0.atl $(ATL)/pv/a1.atl $(ATL)/pv/a2.atl \
|
||||
$(ATL)/pv/j0.atl $(ATL)/pv/j1.atl $(ATL)/pv/j2.atl \
|
||||
$(ATL)/pv/h0.atl $(ATL)/pv/t0.atl
|
||||
|
||||
# МУЗЫКА: записи DOS-версии в формате CBL. Номера — оригинала, чтобы игровой
|
||||
# код просил трек тем же числом, каким SDLPoP зовёт play_sound.
|
||||
# заставка 54 55 50 53 52 | катсцены 26 27 35 36 40
|
||||
# игра 24 25 28 29 30 32 33 37 39 41 43 | 56 = won (стриминг с диска)
|
||||
MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55 56
|
||||
# Набор записей, из которого делается музыка: flac (по умолчанию — лучший
|
||||
# исходник), mp3, ogg, mt32 (то же в исполнении Roland MT-32). Все четыре
|
||||
# лежат в assets/orig/PoP1_DOS_music/, имена треков совпадают.
|
||||
# Формат входит в имя stamp'а, поэтому смена MUSIC_FMT сама по себе делает
|
||||
# музыку устаревшей — принудительная пересборка не нужна.
|
||||
MUSIC_FMT ?= flac
|
||||
# Источник оцифрованных ЭФФЕКТОВ (digisnd1..3): sdlpop (умолчание) или msdos.
|
||||
# Умолчание — SDLPoP, потому что он выкачивается автоматически (`make fetch`)
|
||||
# и сборка обязана работать вообще без оригинального дистрибутива DOS.
|
||||
# Наборы НЕ одинаковы: в MSDOS оцифровка полнее (в SDLPoP `spiked` (48)
|
||||
# фактически пустой, `sword_vs_sword` (10) короче — разбор в
|
||||
# docs/sound_plan.md). У кого дистрибутив есть — `make SND_SRC=msdos`.
|
||||
# Источник входит в имя stamp'а, поэтому смена SND_SRC сама делает звук
|
||||
# устаревшим, как и смена MUSIC_FMT.
|
||||
SND_SRC ?= sdlpop
|
||||
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015 (copy
|
||||
# protection) намеренно не пакуется и не существует в маршруте загрузчика.
|
||||
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
|
||||
|
||||
# ЕДИНСТВЕННЫЙ список того, что ложится на диск. Из него выводятся и
|
||||
# staging в build/, и аргументы упаковщика образа.
|
||||
# Архивы вместо россыпи файлов не ради места: замер 2026-08-25 — `open` 51,4 мс
|
||||
# против 32,6 мс на чтение 16 КБ, то есть на одних открытиях набор Кида терял
|
||||
# три секунды. Четыре архива актёров, а не один: имена внутри наборов
|
||||
# совпадают (g0.atl есть и у стража, и у скелета, и у Джафара), а константы
|
||||
# индексов упаковщик делает из имени файла.
|
||||
# README для игрока идёт НА ДИСК рядом с программой, В КОРЕНЬ. Русский
|
||||
# обязан быть в CP866: DSS и любой местный просмотрщик читают именно её,
|
||||
# UTF-8 покажется кракозябрами. Имена по 8.3, язык — суффиксом, а не
|
||||
# расширением: расширение .TXT должно остаться, чтобы файл открывался
|
||||
# просмотрщиком как текст.
|
||||
DISK := README_E.TXT README_R.TXT \
|
||||
BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
|
||||
KID/kid.arc KID/kid.pal KID/kid.ani \
|
||||
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
||||
SND/snd.arc SND/snd.idx \
|
||||
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) MUS/mus.idx \
|
||||
FONT/font.atl \
|
||||
TITLE/title.arc TITLE/title.pal \
|
||||
PV/pv.arc PV/story.pal PV/pv.pal \
|
||||
$(foreach n,$(LVL_NUMS),LEVELS/res20$(n).bin)
|
||||
|
||||
# Все шестнадцать генерируемых заголовков перечислены явно, а не wildcard'ом:
|
||||
# список — это и зависимость exe, и то, что умеет чинить себя само.
|
||||
GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
|
||||
shadow_arc.h title_arc.h pv_arc.h
|
||||
# pop_sound_tbl.h здесь БОЛЬШЕ НЕТ: раскладка эффектов уехала на диск
|
||||
# (SND/snd.idx), и EXE от звукового набора не зависит — пару
|
||||
# snd.idx + snd.arc можно менять без перекомпиляции.
|
||||
# Ни звукового, ни музыкальных заголовков здесь БОЛЬШЕ НЕТ: раскладка
|
||||
# эффектов и длины треков уехали на диск (SND/snd.idx, MUS/mus.idx), и EXE
|
||||
# не зависит ни от звукового набора, ни от набора записей.
|
||||
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \
|
||||
pop_guard_pal.h pop_shadow_atlas.h pop_font.h)
|
||||
|
||||
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
||||
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
|
||||
|
||||
EXTRA_DATA := $(STAGED)
|
||||
# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог HDD_DEST_DIR, голый путь —
|
||||
# непосредственно в HDD_DEST_DIR. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё
|
||||
# `README_E.TXT` превращался
|
||||
# в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с
|
||||
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
||||
# образе).
|
||||
HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \
|
||||
$(foreach f,$(DISK),$(if $(findstring /,$(f)),\
|
||||
$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\
|
||||
$(BUILD_DIR)/$(f)))
|
||||
# Всё дерево SprPoP лежит рядом с EXE, но не меняет дефолт глобального
|
||||
# упаковщика: остальные приложения по-прежнему попадают в корень HDD.
|
||||
HDD_DEST_DIR := games/sprpop
|
||||
# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с
|
||||
# MAME — одна символьная ссылка, ставится однократно: make mame-link.
|
||||
HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd
|
||||
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Сборка приложения
|
||||
# ---------------------------------------------------------------------------
|
||||
# sprinter-cc собирает все модули одним вызовом и depfile не выдаёт, поэтому
|
||||
# зависимости перечисляем сами. wildcard по заголовкам намеренно широкий:
|
||||
# цена лишней полной сборки ниже риска получить stale-бинарник.
|
||||
HEADERS := $(wildcard $(SRC_DIR)/*.h)
|
||||
# ОТПЕЧАТОК КЛЮЧЕЙ СБОРКИ. LEVEL/ROOM/POS/PROF/ALLOCS — это -D на входе
|
||||
# компилятора, а не файлы, и make их «не видит»: после `make LEVEL=14`
|
||||
# обычный `make` считал бы отладочный бинарь свежим и молча оставил его.
|
||||
# Поэтому ключи пишутся в файл, и exe зависит от него; меняется строка —
|
||||
# перелинковка. Файл переписывается ТОЛЬКО при изменении, иначе каждый
|
||||
# make дёргал бы полную сборку заново.
|
||||
BUILD_SIG := MEMORY=$(MEMORY) ALLOCS=$(ALLOCS) $(PROF_FLAGS) $(BANK_FLAGS)
|
||||
BUILD_STAMP := $(BUILD_DIR)/.buildflags
|
||||
# Отпечаток обновляется НА РАЗБОРЕ Makefile, а не рецептом. Так время файла
|
||||
# уже верное к моменту, когда make сверяет его с exe — иначе не работает ни
|
||||
# один из простых вариантов:
|
||||
# * рецепт с cmp — под `make -n` не выполняется, и make врёт про работу;
|
||||
# * подпись в имени файла — при ВОЗВРАТЕ к прежним ключам файл уже лежит
|
||||
# и оказывается старше exe, то есть перелинковки не будет.
|
||||
# Запись идёт только при отличии, поэтому обычный make ничего не трогает.
|
||||
# На целях очистки пропускаем, чтобы `make clean` не создавал build/ заново.
|
||||
ifeq ($(filter clean distclean,$(MAKECMDGOALS)),)
|
||||
$(shell mkdir -p $(BUILD_DIR); printf '%s\n' '$(BUILD_SIG)' | cmp -s - $(BUILD_STAMP) \
|
||||
|| printf '%s\n' '$(BUILD_SIG)' > $(BUILD_STAMP))
|
||||
endif
|
||||
|
||||
# Цепочка: exe -> gen/*.h -> ресурсы -> упаковщик -> assets/orig (см. ниже).
|
||||
$(EXE): Makefile $(PROJ_ROOT)/app.mk $(addprefix $(SRC_DIR)/,$(BANKED_SRCS)) \
|
||||
$(HEADERS) $(GEN_H) $(BUILD_STAMP)
|
||||
|
||||
# Полная сборка = exe + разложенный build/.
|
||||
all: stage
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# staging: assets/packed/ -> build/
|
||||
# ---------------------------------------------------------------------------
|
||||
# build/ обязан быть самодостаточным (его можно снести целиком), поэтому
|
||||
# ресурсы именно КОПИРУЮТСЯ, а не берутся с диска из assets/.
|
||||
stage: $(STAGED)
|
||||
|
||||
$(BUILD_DIR)/%: $(PACKED)/%
|
||||
@mkdir -p $(dir $@)
|
||||
@cp $< $@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ресурсы: цепочка зависимостей
|
||||
# ---------------------------------------------------------------------------
|
||||
# Направление одно и сквозное:
|
||||
#
|
||||
# build/sprpop.exe -> gen/*.h -> архивы и атласы -> упаковщик -> assets/orig/
|
||||
#
|
||||
# Код зависит от генерируемых заголовков, заголовки — от ресурсов, которые они
|
||||
# описывают (индексы внутри .arc, число страниц звука, таблицы кадров), а те —
|
||||
# от упаковщика и оригиналов. Поэтому собрать код против устаревших констант
|
||||
# нельзя: gen/*.h делаются ТЕМ ЖЕ запуском упаковщика, что и сам ресурс, и
|
||||
# объявлены его целями наравне с ним.
|
||||
#
|
||||
# Правила генерации живут в ветке GENERATE=1 и включаются в двух случаях:
|
||||
# * явная цель `make resources*` — перегенерация с учётом таймстемпов
|
||||
# (поправил упаковщик — ресурс пересоберётся);
|
||||
# * какого-то ресурса или заголовка НЕТ на месте (после `make distclean`,
|
||||
# либо файл снесли руками) — тогда обычный `make` сам уходит в
|
||||
# рекурсивный вызов и доделывает недостающее вместе с его gen/*.h.
|
||||
#
|
||||
# Почему не одни таймстемпы на все случаи: assets/packed/ и gen/ лежат в
|
||||
# репозитории, а git не сохраняет времена файлов — в свежем клоне упаковщик
|
||||
# и его выхлоп получают одинаковую метку, и любое сравнение по времени
|
||||
# превращается в лотерею «перепаковать 6 МБ ресурсов на ровном месте, причём
|
||||
# на машине, где assets/orig/ может вообще не быть». Отсутствие файла —
|
||||
# признак однозначный, и именно на нём построена автоматика.
|
||||
TOOLS := $(CURDIR)/tools
|
||||
PY := python3
|
||||
|
||||
ifeq ($(GENERATE),1)
|
||||
# ===========================================================================
|
||||
# Ветка генерации
|
||||
# ===========================================================================
|
||||
STAMP := $(BUILD_DIR)/.stamps
|
||||
$(STAMP):
|
||||
@mkdir -p $@
|
||||
|
||||
# Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило
|
||||
# заставляет make при `-B` запускать один и тот же упаковщик отдельно для
|
||||
# каждого выходного файла. Stamp представляет ОДИН запуск генератора; все
|
||||
# созданные им файлы зависят от stamp без собственных рецептов.
|
||||
#
|
||||
# check-orig — order-only: без оригиналов запускать упаковщик бессмысленно, и
|
||||
# понятное сообщение лучше трейсбека Python. На «устарел ли stamp» он не
|
||||
# влияет, только выполняется раньше.
|
||||
define stamp_rule
|
||||
$(STAMP)/$(1): $(2) | $(STAMP) check-orig
|
||||
cd $(TOOLS) && $(PY) $(3)
|
||||
@touch $$@
|
||||
endef
|
||||
|
||||
# ВАРИАНТЫ РЕСУРСА (источник звука SND_SRC, набор записей MUSIC_FMT). Оба
|
||||
# варианта пишут в ОДИН выход — build/atl/sound, build/atl/music, — поэтому
|
||||
# одного stamp'а на вариант мало: вернувшись к прежнему варианту, make видит
|
||||
# его stamp свежим и не делает НИЧЕГО, а в выходе лежит чужое (поймано на
|
||||
# sdlpop -> msdos -> sdlpop: заголовок остался с 10 страницами вместо 9).
|
||||
# Лечится файлом с именем текущего варианта: он переписывается только при
|
||||
# смене (cmp), а его свежесть делает stamp устаревшим.
|
||||
.PHONY: variant-check
|
||||
variant-check: ;
|
||||
# СМЕНА ВАРИАНТА СНОСИТ STAMP'Ы СВОЕГО СЕМЕЙСТВА, а не полагается на то,
|
||||
# что make заметит свежесть variant-файла. Причина — секундная
|
||||
# гранулярность mtime: упаковка укладывается в одну секунду с записью
|
||||
# variant, и stamp того же имени считается актуальным. Возврат к прежнему
|
||||
# варианту (`sdlpop -> msdos -> sdlpop`) тогда ТИХО оставлял чужой набор.
|
||||
# Нет файла — правило обязано выполниться, и время тут ни при чём.
|
||||
define variant_rule
|
||||
$(STAMP)/$(1).variant: variant-check | $(STAMP)
|
||||
@echo "$(2)" | cmp -s - $$@ || { echo "$(2)" > $$@; rm -f $(STAMP)/$(1)-*; }
|
||||
endef
|
||||
$(eval $(call variant_rule,sound,$(SND_SRC)))
|
||||
$(eval $(call variant_rule,music,$(MUSIC_FMT)))
|
||||
|
||||
$(eval $(call stamp_rule,bg,$(TOOLS)/pop_pack_bg.py $(TOOLS)/render_room.py $(TOOLS)/paths.py,pop_pack_bg.py dungeon && $(PY) pop_pack_bg.py palace))
|
||||
$(eval $(call stamp_rule,kid,$(TOOLS)/pop_pack_kid.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/paths.py,pop_pack_kid.py))
|
||||
$(eval $(call stamp_rule,kid-bin,$(TOOLS)/pop_extract_kid_data.py $(TOOLS)/paths.py,pop_extract_kid_data.py))
|
||||
$(eval $(call stamp_rule,guard,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py))
|
||||
$(eval $(call stamp_rule,skel,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py SKEL))
|
||||
$(eval $(call stamp_rule,vizier,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py VIZIER))
|
||||
$(eval $(call stamp_rule,shadow,$(TOOLS)/pop_pack_shadow.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/pop_pack_kid.py $(TOOLS)/paths.py,pop_pack_shadow.py))
|
||||
# ЗВУК — ОСОБЫЙ СЛУЧАЙ: упаковка, сборка архива и копия индекса делаются
|
||||
# ОДНИМ рецептом, а не тремя правилами через таймстемпы.
|
||||
#
|
||||
# Почему. Упаковщик и копирование укладываются в одну секунду, а make
|
||||
# сравнивает mtime с секундной гранулярностью: цель, сделанная в ту же
|
||||
# секунду, что и её зависимость, считается актуальной. При смене SND_SRC
|
||||
# это давало ТИХО НЕВЕРНЫЙ результат — `sdlpop -> msdos -> sdlpop`
|
||||
# перепаковывал build/atl, но в assets/packed оставался прежний набор
|
||||
# (10 страниц вместо 9; поймано 2026-08-31, с паузой в секунду не
|
||||
# воспроизводится). Внутри одного рецепта сравнивать нечего.
|
||||
$(STAMP)/sound-$(SND_SRC): $(TOOLS)/pop_pack_sound.py $(TOOLS)/pop_idx.py \
|
||||
$(TOOLS)/paths.py $(TOOLS)/pop_pack_arc.py \
|
||||
$(STAMP)/sound.variant | $(STAMP) check-orig
|
||||
cd $(TOOLS) && $(PY) pop_pack_sound.py --source $(SND_SRC)
|
||||
@mkdir -p $(PACKED)/SND
|
||||
$(ARC) $(PACKED)/SND/snd.arc $$(ls $(ATL)/sound/s*.bin | sort -V)
|
||||
@cp $(ATL)/sound/snd.idx $(PACKED)/SND/snd.idx
|
||||
@touch $@
|
||||
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/pop_idx.py $(TOOLS)/paths.py $(STAMP)/music.variant,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
||||
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
|
||||
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
|
||||
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
|
||||
|
||||
# Файл, сделанный упаковщиком, зависит только от stamp. Если stamp свежий, а
|
||||
# файла нет (снесли руками, либо build/ вычистили) — перезапускаем генератор
|
||||
# принудительно. Заголовки gen/, которые упаковщик печатает попутно, стоят в
|
||||
# ТЕХ ЖЕ списках: это и есть «gen зависит от ресурсов, для которых сделан».
|
||||
define from_stamp
|
||||
$(1): $(STAMP)/$(2)
|
||||
@test -f $$@ || { $(MAKE) GENERATE=1 -B $(STAMP)/$(2); test -f $$@; }
|
||||
endef
|
||||
$(eval $(call from_stamp,$(BG_ATL) $(ATL)/bg/pop_tile.pal $(ATL)/bg/pal_tile.pal gen/pop_bg_atlas.h,bg))
|
||||
$(eval $(call from_stamp,$(KID_ATL) $(ATL)/kid/kid.pal,kid))
|
||||
$(eval $(call from_stamp,$(ATL)/kid/kid.ani gen/kid_data.h,kid-bin))
|
||||
$(eval $(call from_stamp,$(GUARD_ATL) gen/pop_guard_pal.h,guard))
|
||||
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
||||
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
|
||||
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
|
||||
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) $(ATL)/music/mus.idx,music-$(MUSIC_FMT)))
|
||||
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
|
||||
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
|
||||
$(eval $(call from_stamp,$(PV_ATL) $(ATL)/pv/story.pal $(ATL)/pv/pv.pal,pv))
|
||||
|
||||
# Архивы: собираются ПРЯМО в assets/packed — это уже дисковый артефакт.
|
||||
# Заголовок с индексами объявлен целью НАРАВНЕ с архивом: индексы описывают
|
||||
# именно это содержимое и разъехаться с ним не должны.
|
||||
ARC := $(PY) $(TOOLS)/pop_pack_arc.py
|
||||
define arc_rule
|
||||
$(PACKED)/$(1)/$(2).arc gen/$(3)_arc.h: $(4) $(TOOLS)/pop_pack_arc.py
|
||||
@mkdir -p $(PACKED)/$(1)
|
||||
$(ARC) --header gen/$(3)_arc.h --prefix $(5) $(PACKED)/$(1)/$(2).arc $(4)
|
||||
endef
|
||||
$(eval $(call arc_rule,BG,bg,bg,$(BG_ATL),BG))
|
||||
$(eval $(call arc_rule,KID,kid,kid,$(KID_ATL),KID))
|
||||
$(eval $(call arc_rule,GUARD,guard,guard,$(GUARD_ATL),GUARD))
|
||||
$(eval $(call arc_rule,SKEL,skel,skel,$(SKEL_ATL),SKEL))
|
||||
$(eval $(call arc_rule,VIZIER,vizier,vizier,$(VIZIER_ATL),VIZIER))
|
||||
$(eval $(call arc_rule,SHADOW,shadow,shadow,$(SHADOW_ATL),SHADOW))
|
||||
$(eval $(call arc_rule,TITLE,title,title,$(TITLE_ATL),TITLE))
|
||||
$(eval $(call arc_rule,PV,pv,pv,$(PV_ATL),PV))
|
||||
# У звука своего *_arc.h нет: содержимое адресуется таблицей pop_sound_tbl.h,
|
||||
# которую печатает сам упаковщик звука.
|
||||
# Зависимость на stamp, а не на список страниц: сколько их будет, известно
|
||||
# только после запуска упаковщика (источник задаёт число). Порядок страниц
|
||||
# в архиве обязан быть ЧИСЛОВЫМ — pop_snd_tbl адресует их индексом, а голый
|
||||
# ls даёт s10 перед s2. sort -V, а не `-t s -k n`: буква s есть и в пути.
|
||||
# Пара snd.arc + snd.idx согласована по построению: её делает ОДИН рецепт
|
||||
# stamp'а выше (см. там же, почему не раздельными правилами). Здесь
|
||||
# только восстановление, если файл снесли руками.
|
||||
$(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx: $(STAMP)/sound-$(SND_SRC)
|
||||
@test -f $@ || { $(MAKE) GENERATE=1 -B $(STAMP)/sound-$(SND_SRC); test -f $@; }
|
||||
|
||||
# Файлы, которые ложатся на диск как есть, — простое копирование из staging.
|
||||
define copy_rule
|
||||
$(PACKED)/$(1): $(2)
|
||||
@mkdir -p $$(dir $$@)
|
||||
@cp $$< $$@
|
||||
endef
|
||||
$(eval $(call copy_rule,BG/pop_tile.pal,$(ATL)/bg/pop_tile.pal))
|
||||
$(eval $(call copy_rule,BG/pal_tile.pal,$(ATL)/bg/pal_tile.pal))
|
||||
$(eval $(call copy_rule,KID/kid.pal,$(ATL)/kid/kid.pal))
|
||||
$(eval $(call copy_rule,KID/kid.ani,$(ATL)/kid/kid.ani))
|
||||
$(eval $(call copy_rule,FONT/font.atl,$(ATL)/font/font.atl))
|
||||
$(eval $(call copy_rule,TITLE/title.pal,$(ATL)/title/title.pal))
|
||||
$(eval $(call copy_rule,PV/story.pal,$(ATL)/pv/story.pal))
|
||||
$(eval $(call copy_rule,PV/pv.pal,$(ATL)/pv/pv.pal))
|
||||
$(foreach i,$(MUS_IDS),$(eval $(call copy_rule,MUS/m$(i).bin,$(ATL)/music/m$(i).bin)))
|
||||
$(eval $(call copy_rule,MUS/mus.idx,$(ATL)/music/mus.idx))
|
||||
# Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик
|
||||
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
|
||||
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
|
||||
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
|
||||
SND_ORIG_MSDOS := assets/orig/MSDOS
|
||||
define lvl_rule
|
||||
$(PACKED)/LEVELS/res20$(1).bin: $(LVL_ORIG)/res20$(1).bin | check-orig
|
||||
@mkdir -p $$(dir $$@)
|
||||
@cp $$< $$@
|
||||
endef
|
||||
$(foreach n,$(LVL_NUMS),$(eval $(call lvl_rule,$(n))))
|
||||
|
||||
# README: перевод строки CRLF (текст читают в DOS-подобной среде) и, для
|
||||
# русского, перекодировка в CP866. iconv БЕЗ -c намеренно: потеря символа
|
||||
# должна ломать сборку, а не молча портить текст (memory
|
||||
# iconv_cp866_workflow; //TRANSLIT на macOS падает).
|
||||
$(PACKED)/README_E.TXT: dist/README.txt
|
||||
@mkdir -p $(dir $@)
|
||||
@sed 's/$$/\r/' $< > $@
|
||||
|
||||
# ПОРЯДОК ВАЖЕН: CRLF ставится ДО перекодировки. BSD sed в UTF-8 локали
|
||||
# отказывается работать с байтами CP866 («RE error: illegal byte sequence»),
|
||||
# а с валидным UTF-8 — работает.
|
||||
$(PACKED)/README_R.TXT: dist/README.ru.txt
|
||||
@mkdir -p $(dir $@)
|
||||
@sed 's/$$/\r/' $< | iconv -f UTF-8 -t CP866 > $@
|
||||
|
||||
resources-readme: $(PACKED)/README_E.TXT $(PACKED)/README_R.TXT
|
||||
|
||||
resources-bg: $(PACKED)/BG/bg.arc $(PACKED)/BG/pop_tile.pal $(PACKED)/BG/pal_tile.pal
|
||||
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
|
||||
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
|
||||
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
|
||||
resources-sound: $(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx
|
||||
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin) $(PACKED)/MUS/mus.idx
|
||||
resources-font: $(PACKED)/FONT/font.atl
|
||||
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
|
||||
resources-pv: $(PACKED)/PV/pv.arc $(PACKED)/PV/story.pal $(PACKED)/PV/pv.pal
|
||||
resources-levels: $(foreach n,$(LVL_NUMS),$(PACKED)/LEVELS/res20$(n).bin)
|
||||
resources: resources-bg resources-kid resources-actors resources-sound \
|
||||
resources-music resources-font resources-title resources-pv \
|
||||
resources-levels $(GEN_H)
|
||||
|
||||
check-orig:
|
||||
@test -d $(LVL_ORIG) || { \
|
||||
echo "SprPoP: нет assets/orig/SDLPoP — генерировать ресурсы не из чего." >&2; \
|
||||
echo " Скачать: make fetch (подробности — assets/orig/README.md)" >&2; false; }
|
||||
@test "$(SND_SRC)" != msdos || test -d $(SND_ORIG_MSDOS) || { \
|
||||
echo "SprPoP: SND_SRC=msdos, но нет $(SND_ORIG_MSDOS)." >&2; \
|
||||
echo " Дистрибутив DOS не качается — положить руками, либо собирать" >&2; \
|
||||
echo " со звуком из SDLPoP: make SND_SRC=sdlpop" >&2; false; }
|
||||
|
||||
else
|
||||
# ===========================================================================
|
||||
# Обычная сборка: assets/packed/ и gen/ — готовые входы
|
||||
# ===========================================================================
|
||||
# Ни одного правила с таймстемпами: пока файлы на месте, make их даже не
|
||||
# рассматривает. Пропал файл — доделываем ровно его, рекурсивным вызовом в
|
||||
# ветку генерации (она подтянет и заголовок, и оригиналы).
|
||||
$(PACKED_FILES) $(GEN_H):
|
||||
@echo "SprPoP: нет $@ — генерирую ресурсы"
|
||||
@$(MAKE) GENERATE=1 $@
|
||||
|
||||
RES_TARGETS := resources resources-readme resources-bg resources-kid \
|
||||
resources-actors resources-sound resources-music resources-font \
|
||||
resources-title resources-pv resources-levels
|
||||
$(RES_TARGETS):
|
||||
@$(MAKE) GENERATE=1 $@
|
||||
# Перегенерация ВСЕГО, не глядя на таймстемпы.
|
||||
resources-rebuild:
|
||||
@$(MAKE) GENERATE=1 -B resources
|
||||
.PHONY: $(RES_TARGETS) resources-rebuild
|
||||
endif
|
||||
# ---------------------------------------------------------------------------
|
||||
# Прочее
|
||||
# ---------------------------------------------------------------------------
|
||||
# Образ HDD собирается после staging — иначе на диск уедет старое содержимое.
|
||||
# Рецепта здесь нет намеренно: он в app.mk, а это только зависимости.
|
||||
hdd: stage | $(BUILD_DIR)/hdd
|
||||
$(BUILD_DIR)/hdd:
|
||||
@mkdir -p $@
|
||||
|
||||
# Однократная связка с MAME: run_bridge.sh жёстко открывает IMG/test_hdd.chd,
|
||||
# поэтому подсовываем ему символьную ссылку на наш образ. chdman переписывает
|
||||
# сам файл, ссылка остаётся живой — но MAME после пересборки образа ОБЯЗАН
|
||||
# полный рестарт (новый inode).
|
||||
mame-link: hdd
|
||||
ln -sf $(HDD_IMG) $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd
|
||||
@echo "MAME: IMG/test_hdd.chd -> $(HDD_IMG)"
|
||||
|
||||
# Музыка из конкретного набора записей. Каждая цель пересобирает музыку и
|
||||
# сразу раскладывает её в build/, чтобы `make hdd` взял уже новую.
|
||||
# make music-mp3 собрать музыку из mp3
|
||||
# make music-mt32 вариант Roland MT-32
|
||||
# Разовая сборка без смены умолчания: make MUSIC_FMT=ogg resources-music
|
||||
MUSIC_TARGETS := music-flac music-mp3 music-ogg music-mt32
|
||||
$(MUSIC_TARGETS):
|
||||
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) resources-music
|
||||
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) stage
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Внешние исходные данные (assets/orig/)
|
||||
# ---------------------------------------------------------------------------
|
||||
# В репозитории чужих данных нет — есть tools/fetch_orig.py, который знает,
|
||||
# откуда их взять, и таблица источников внутри него. Качаем только то,
|
||||
# чего нет; `FETCH_ARGS=--force` перекачивает заново, `FETCH_ARGS=--ref v1.23`
|
||||
# берёт конкретный тег SDLPoP.
|
||||
# make fetch SDLPoP + записи музыки
|
||||
# make fetch-sdlpop только SDLPoP (data/ + src/-эталон)
|
||||
# make fetch-music только записи саундтрека (43 МБ)
|
||||
# make fetch-check что уже на месте
|
||||
# make fetch-list таблица источников
|
||||
# MSDOS/ не качается — это копирайтный дистрибутив; он НЕОБЯЗАТЕЛЕН, всё
|
||||
# нужное (уровни, оцифровка) есть в SDLPoP. См. SND_SRC выше.
|
||||
FETCH := $(PY) $(TOOLS)/fetch_orig.py
|
||||
fetch: ; $(FETCH) all $(FETCH_ARGS)
|
||||
fetch-sdlpop: ; $(FETCH) sdlpop $(FETCH_ARGS)
|
||||
fetch-music: ; $(FETCH) music $(FETCH_ARGS)
|
||||
fetch-check: ; @$(FETCH) --check
|
||||
fetch-list: ; @$(FETCH) --list
|
||||
FETCH_TARGETS := fetch fetch-sdlpop fetch-music fetch-check fetch-list
|
||||
|
||||
# Тесты инструментов (хост, питон): форматы, которые читает Z80. Секунды,
|
||||
# ни эмулятора, ни оригиналов не нужно.
|
||||
test-tools:
|
||||
@cd $(CURDIR) && $(PY) -m unittest discover -s tools/tests
|
||||
|
||||
# Перелинковка приложения без пересборки библиотек и ресурсов.
|
||||
relink:
|
||||
$(MAKE) -W Makefile $(EXE)
|
||||
|
||||
# clean (из app.mk) сносит только build/ — ассеты остаются на месте.
|
||||
# distclean дополнительно убирает УПАКОВАННЫЕ ассеты; вернуть их можно
|
||||
# командой `make resources`, но для неё нужны оригиналы в assets/orig/.
|
||||
# Сам assets/orig/ не трогается НИКОГДА: он не производный, а исходный, и
|
||||
# восстановить его командой нельзя — только положить руками.
|
||||
# gen/ тоже остаётся: без него не скомпилировать src/, а вес его — байты.
|
||||
distclean: clean
|
||||
rm -rf $(PACKED)
|
||||
@echo "assets/packed снесён; вернуть — make resources (нужен assets/orig)"
|
||||
|
||||
# Цели генерации ресурсов объявлены .PHONY в своей ветке (см. выше): в
|
||||
# GENERATE=1 они настоящие агрегаты, в обычной сборке — переходники.
|
||||
.PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \
|
||||
resources-actors resources-sound resources-music resources-font \
|
||||
resources-title resources-pv resources-levels $(MUSIC_TARGETS) \
|
||||
$(FETCH_TARGETS) test-tools relink distclean mame-link
|
||||
@@ -0,0 +1,81 @@
|
||||
# SprPoP — Prince of Persia на Sprinter Sp2000
|
||||
|
||||
Порт оригинального Prince of Persia (1989) на компьютер Sprinter Sp2000:
|
||||
режим 320×256×256, дабл-буфер, звук через CBL, код разложен по одиннадцати
|
||||
16-килобайтным банкам.
|
||||
|
||||
Приложение **автономно** — код, оригинальные данные, конверторы ресурсов и
|
||||
сборка лежат внутри этой папки. Наружу нужен только компилятор:
|
||||
|
||||
```sh
|
||||
export SPRINTER_ROOT=/путь/к/C-Compiler # не нужен, если папка лежит внутри его дерева
|
||||
make # build/sprpop.exe + ресурсы в build/
|
||||
make hdd # + образ build/hdd/sprpop.chd
|
||||
```
|
||||
|
||||
Чужих данных в репозитории нет — есть адреса, откуда их взять. Чтобы
|
||||
перегенерировать ресурсы (или получить эталон механики), сначала:
|
||||
|
||||
```sh
|
||||
make fetch # SDLPoP + записи саундтрека DOS-версии в assets/orig/
|
||||
```
|
||||
|
||||
## Что где
|
||||
|
||||
```
|
||||
src/ рукописный C: главный цикл sprpop.c + движок pop_*.c
|
||||
gen/ генерируемые заголовки — руками не править
|
||||
assets/orig/ оригинальные данные игры (вне репозитория, качает `make fetch`)
|
||||
assets/packed/ то, что ложится на диск игры, в раскладке диска
|
||||
tools/ конверторы ресурсов; все пути — в tools/paths.py
|
||||
docs/ планы, доски задач и багов, справочники (начинать с docs/README.md)
|
||||
tests/host/ модульные тесты движка под ucsim_z80
|
||||
build/ выход: sprpop.exe, каталоги ресурсов, hdd/, промежуточные atl/
|
||||
```
|
||||
|
||||
## Цели сборки
|
||||
|
||||
| Команда | Что делает |
|
||||
|---|---|
|
||||
| `make` | собрать `build/sprpop.exe` и разложить ресурсы в `build/` |
|
||||
| `make hdd` | + образ жёсткого диска `build/hdd/sprpop.chd` |
|
||||
| `make mame-link` | однократно подставить образ в MAME (символьная ссылка на `IMG/test_hdd.chd`) |
|
||||
| `make fetch` | скачать внешние исходные данные в `assets/orig/`: `fetch-sdlpop`, `fetch-music`, `fetch-check`, `fetch-list` |
|
||||
| `make resources` | перегенерировать ресурсы из `assets/orig/` |
|
||||
| `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` |
|
||||
| `make SND_SRC=msdos` | оцифровка эффектов из дистрибутива DOS вместо SDLPoP (нужен свой `assets/orig/MSDOS/`) |
|
||||
| `make -C tests/host` | модульные тесты движка (секунды, без эмулятора) |
|
||||
| `make test-tools` | тесты упаковщиков на хосте: форматы файлов, которые читает Z80 |
|
||||
| `make clean` | снести `build/` (ассеты не трогает) |
|
||||
| `make distclean` | `clean` + снести упакованные ассеты `assets/packed/` |
|
||||
|
||||
Отладочные ключи: `make LEVEL=9` — стартовый уровень, `make ROOM=15 POS=2` —
|
||||
стартовая комната и тайл (`row*10+col`), `make PROF=1` — профиль фаз кадра
|
||||
полосами бордюра, `make ALLOCS=100000` — плотная упаковка регистров для
|
||||
замеров размера.
|
||||
|
||||
## Сборка из чистого клона
|
||||
|
||||
Работает без `assets/orig/`: упакованные ресурсы (`assets/packed/`) и
|
||||
генерируемые заголовки (`gen/`) лежат в репозитории. Исключение —
|
||||
`assets/packed/LEVELS/`: файлы уровней ложатся на диск прямо из оригинала,
|
||||
без конвертации, поэтому они, как и `assets/orig/`, вне репозитория. Что и
|
||||
откуда положить — `assets/orig/README.md`.
|
||||
|
||||
`assets/orig/` нужен только чтобы ресурсы **перегенерировать** (`make
|
||||
resources`) и как **эталон механики**: любая правка поведения сверяется с
|
||||
`assets/orig/SDLPoP/src/` до кодинга, см. `CLAUDE.md`. Заполняется он одной
|
||||
командой `make fetch` — качается SDLPoP (ресурсы + исходники-эталон) и записи
|
||||
саундтрека; адрес архива музыки скрипт берёт из самого SDLPoP.
|
||||
|
||||
Оригинальный дистрибутив DOS (`assets/orig/MSDOS/`) **не нужен**: и уровни, и
|
||||
оцифровка звука берутся из SDLPoP. Он даёт ровно одно — более полный набор
|
||||
эффектов (в SDLPoP пуст звук `spiked`, 48); у кого дистрибутив есть, включает
|
||||
его явно: `make SND_SRC=msdos`. Подробности — `assets/orig/README.md`.
|
||||
|
||||
## Происхождение
|
||||
|
||||
Выделен из `applications/PoP/roomtest` 2026-08-26; та папка заморожена и
|
||||
служит архивом закрытых задач, багов и исполненных планов. Референсные
|
||||
реализации (Apple II 6502, Princed Resources, mininim) остались в
|
||||
`applications/PoP/`.
|
||||
@@ -0,0 +1,82 @@
|
||||
# assets/orig — оригинальные данные
|
||||
|
||||
Здесь лежат **исходные** данные Prince of Persia, из которых `make resources`
|
||||
делает ресурсы Sprinter-версии. В репозиторий этот каталог не идёт —
|
||||
материал копирайтный (см. `.gitignore`). Вместо данных в репозитории лежит
|
||||
**знание, откуда они берутся**: `tools/fetch_orig.py` и таблица источников
|
||||
внутри него.
|
||||
|
||||
## Как заполнить
|
||||
|
||||
```
|
||||
make fetch # SDLPoP + записи саундтрека (всё, что качается)
|
||||
make fetch-check # что уже на месте
|
||||
make fetch-list # таблица источников с адресами
|
||||
```
|
||||
|
||||
Тонкости:
|
||||
|
||||
```
|
||||
make fetch-sdlpop только SDLPoP
|
||||
make fetch-music только музыка (43 МБ)
|
||||
make FETCH_ARGS=--force fetch перекачать заново
|
||||
make FETCH_ARGS="--ref v1.23" fetch-sdlpop конкретный тег SDLPoP
|
||||
```
|
||||
|
||||
Скачанные архивы кэшируются в `assets/orig/.cache/` — повторный `make fetch`
|
||||
сеть не трогает. Версия SDLPoP записывается в `assets/orig/SDLPoP/.fetch.json`
|
||||
(ветка/тег, sha, дата), и следующий `make fetch` по ней понимает, последняя
|
||||
у нас версия или нет.
|
||||
|
||||
Собрать игру без `assets/orig/` **можно**: упакованные ресурсы лежат в
|
||||
`../packed/`, а генерируемые заголовки — в `../../gen/`. `assets/orig` нужен
|
||||
только чтобы ресурсы **перегенерировать**.
|
||||
|
||||
Исключение — `LEVELS`: файлы `res20xx.bin` ложатся на диск игры как есть, без
|
||||
конвертации, поэтому `assets/packed/LEVELS/` тоже вне репозитория и заполняется
|
||||
из `orig` командой `make resources-levels`.
|
||||
|
||||
## Что и откуда
|
||||
|
||||
| Каталог | Что это | Где взять |
|
||||
|---|---|---|
|
||||
| `SDLPoP/data/` | Распакованные ресурсы DOS-версии: PNG-спрайты (`VDUNGEON`, `VPALACE`, `KID`, `GUARD`, `SKEL`, `VIZIER`, `SHADOW`, `TITLE`, `PV`, `PRINCE`), палитры, `LEVELS/res20xx.bin`, оцифровка `DIGISND*.DAT` | `make fetch-sdlpop` — <https://github.com/NagyD/SDLPoP> (GPLv3) |
|
||||
| `SDLPoP/src/` | Исходники SDLPoP. Нужны не только упаковщикам (`menu.c` и `seg009.c` содержат встроенные шрифты, из `seg005/seg006` берутся таблицы кадров и `seqtbl`), но и как **эталон механики**: расхождение в поведении всегда сверяется с ними | тем же `make fetch-sdlpop` |
|
||||
| `PoP1_DOS_music/` | Записи музыки DOS-версии в четырёх наборах: `flac/` (по умолчанию), `mp3/`, `ogg/`, `ogg_MT-32/` (исполнение Roland MT-32). Имена треков во всех наборах одинаковые; какой брать — задаёт `MUSIC_FMT` | `make fetch-music`. Адрес архива скрипт читает из самого SDLPoP (`ReadMe.amigaos4`, секция «AUDIO IS SLOW/AWFULL»), запасной — popot.org |
|
||||
| `MSDOS/` | **Необязательно.** Оригинальная поставка PoP 1.0 для MS-DOS. Используется только `digisnd*.dat` со звуком, и только если попросить: `make SND_SRC=msdos` | Не качается — дистрибутив игры, положить руками |
|
||||
|
||||
## MSDOS: зачем он и почему без него живётся
|
||||
|
||||
По умолчанию **всё** берётся из SDLPoP — и уровни, и оцифровка звука. Так
|
||||
сборка работает на чистой машине сразу после `make fetch`.
|
||||
|
||||
`MSDOS/` даёт ровно одно: более полный набор оцифрованных эффектов. Из 31
|
||||
звука 28 совпадают побайтно, а три — нет, и все три лучше в дистрибутиве:
|
||||
`spiked` (48, насаживание на пики) в наборе SDLPoP длиной 7 сэмплов, то есть
|
||||
его нет; `sword_vs_sword` (10) короче; `sword_moving` (11) — другая запись.
|
||||
Разбор — `docs/sound_plan.md`. Включается это явно:
|
||||
|
||||
```
|
||||
make SND_SRC=msdos resources-sound # перепаковать звук из дистрибутива
|
||||
make SND_SRC=msdos # и собрать с ним
|
||||
```
|
||||
|
||||
Источник входит в имя stamp'а, поэтому переключение `SND_SRC` само делает
|
||||
звук устаревшим — `-B` не нужен.
|
||||
|
||||
## Кто что читает
|
||||
|
||||
| Упаковщик | Читает |
|
||||
|---|---|
|
||||
| `pop_pack_bg.py`, `render_room.py` | `SDLPoP/data/{VDUNGEON,VPALACE,LEVELS}` |
|
||||
| `pop_pack_kid.py` | `SDLPoP/data/KID` |
|
||||
| `pop_pack_guard.py` | `SDLPoP/data/{GUARD,SKEL,VIZIER,PRINCE}` |
|
||||
| `pop_pack_shadow.py` | `SDLPoP/data/{KID,SHADOW}` |
|
||||
| `pop_pack_title.py`, `pop_pack_intro.py` | `SDLPoP/data/{TITLE,PV,PRINCE,KID}` |
|
||||
| `pop_extract_font.py` | `SDLPoP/src/{menu.c,seg009.c}` |
|
||||
| `pop_extract_kid_data.py` | `SDLPoP/src/` (таблицы кадров и `seqtbl`) |
|
||||
| `pop_pack_sound.py` | `SDLPoP/data/DIGISND*.DAT` (`--source sdlpop`, умолчание) либо `MSDOS/digisnd*.dat` (`--source msdos`) |
|
||||
| `pop_pack_music.py` | `PoP1_DOS_music/<набор>/` (`--format flac\|mp3\|ogg\|mt32`) |
|
||||
|
||||
Точная раскладка путей — в `tools/paths.py`; менять её нужно там, а не в
|
||||
отдельных упаковщиках. Адреса внешних источников — в `tools/fetch_orig.py`.
|
||||