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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 12:12:28 +03:00
parent 4b74478d19
commit 31b82661eb
235 changed files with 51293 additions and 10 deletions
+160
View File
@@ -0,0 +1,160 @@
/*
* pop_level.h — данные уровня PoP из сырого файла res200N.bin (Фаза L1).
*
* Уровень загружается ОДИН раз в отдельную EMM-страницу (как атласы:
* данные с offset 0x100, ISR-стаб в первых байтах — страница безопасна
* для маппинга в W0). Файл — сырой blueprnt DAT 1.0 (2305 байт, БЕЗ
* checksum-байта; Table 6 спецификации POP-DAT):
* foretable(fg) @0 backtable(bg) @720 door I/II @1440/1696
* links @1952 (24×{L,R,U,D}) start @2112 ...
* Тайл-код = байт fg & 0x1F (верхние биты — модификатор, пока не нужны);
* bg передаётся как есть. См. applications/PoP/docs/POP-DAT-* .
*
* Рабочая копия ТЕКУЩЕЙ комнаты живёт в обычной памяти (W2) у приложения
* (мутабельная — loose→empty); pop_room_load извлекает её из страницы +
* тонкие срезы соседей для кромок (правый столбец left-комнаты, верхний
* ряд down-комнаты). Страница маппится в W0 только на время извлечения.
*/
#ifndef POP_LEVEL_H
#define POP_LEVEL_H
#include "pop_qsave.h"
#include <stdint.h>
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
* (open/alloc/размер). Звать ПОСЛЕ первого atlas_load (pop_bg_load) —
* он снимает страницу ядра DSS для W0-unmap. */
/* pop_level_load — внутренняя для pop_level_cold.c (грузим по НОМЕРУ) */
void pop_level_free(void) __banked;
/* ---- Номер уровня и потабличные различия (levels_plan.md §1) -------- */
/* Единый допустимый диапазон ресурсов. 0 зарезервирован под demo (FG7),
* 1..14 — настоящая игра; level 15/copy protection намеренно отсутствует.
* Эти границы используют загрузчик, QuickLoad и отладочная навигация, чтобы
* ни один путь не мог запросить несуществующий res2015.bin. */
#define POP_LEVEL_DEMO 0
#define POP_LEVEL_FIRST 1
#define POP_LEVEL_LAST 14
/* Порт current_level (seg000 load_lev_spr: current_level = next_level =
* level). Читают потабличные функции ниже, стражи (HP/тип) и стартовая
* логика. */
extern uint8_t pop_current_level;
void pop_level_qsave(pop_qs_io_t *io);
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
int pop_level_load_num(uint8_t n) __banked;
/* tbl_entry_pose (data.h:848): 1 — падение внутрь (ур. 1), 2 — вбегание
* (ур. 13), 0 — разворот на месте. Индекс — номер уровня. */
uint8_t pop_level_entry_pose(uint8_t n) __banked;
/* tbl_guard_hp (data.h:846) — база HP стража на уровне (get_guard_hp,
* seg002:0044: extrastrength[skill] + tbl_guard_hp[level]). */
uint8_t pop_level_guard_hp(uint8_t n) __banked;
/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь,
* 4 тень, −1 — стражей на уровне НЕТ (уровень 14). */
int8_t pop_level_guard_type(uint8_t n) __banked;
/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается
* только для контроля — второй тайлсет это levels_plan.md §3. */
uint8_t pop_level_tileset(uint8_t n) __banked;
/* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
* (кромка col0), [3..5] предпоследний (col8) —
* для коллизии на колонке −2 (Kid стоит В шве);
* rcol_fg[6] симметрично (col0 / col1);
* нет комнаты → стена/0;
* below_fg[11] — [0..9] верхний ряд (row0) комнаты СНИЗУ,
* [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА («стена
* вниз»); нет комнаты → стена.
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
uint8_t *lcol_fg, uint8_t *lcol_bg,
uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg) __banked;
/* Связь комнаты по стороне (0=L,1=R,2=U,3=D) → номер соседней (1..24) или
* 0, если стороны нет. Для переходов между комнатами (L2/L3). */
uint8_t pop_room_link(uint8_t room, uint8_t side);
/* Тайл-код (fg & 0x1F) по комнате (1..24) и позиции tilepos (0..29,
* row*10+col); вне диапазона → 0 (empty). Для trob-диспетчера (pop_trob):
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
/* Записать тайл в ЖИВУЮ foretable уровня (порт curr_room_tiles[tp] = tile:
* do_pickup / remove_loose / loose_land). Изменение персистентно — уровень
* лежит в ОЗУ-странице, а не в ПЗУ, — то есть переживает выход из комнаты,
* как в оригинале. Отменяется только pop_level_reset_tiles. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile);
/* Рестарт уровня: вернуть ВСЕ тайлы в исходное (порт load_level, который
* play_level зовёт на каждой итерации — в т.ч. после смерти Кида). */
void pop_level_reset_tiles(void) __banked;
/* Рестарт уровня: вернуть ВСЕХ стражей (тот же load_level + pos_guards).
* Убитый страж после respawn снова жив — как в оригинале. */
void pop_level_reset_guards(void) __banked;
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
* Между begin/end — только pop_level_tile_raw, без рисования. */
void pop_level_access_begin(void);
void pop_level_access_end(void);
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos);
/* Скопировать backtable (модификаторы) комнаты room в out[30]. Исходное
* состояние per-room modifier (openness ворот, состояние пик и т.п.) для
* ленивой инициализации room_modif в pop_trob. Вне диапазона → нули. */
void pop_level_room_bg(uint8_t room, uint8_t *out30);
/* Таблицы связей дверей (LINKLOC@1440 / LINKMAP@1696, по 256 Б). Копируются
* в W2-RAM при загрузке (LINKMAP мутабельна — таймеры кнопок). i — индекс
* цепочки (модификатор кнопки). Декод — в pop_trob (get_doorlink_*). */
uint8_t pop_doorlink1(uint8_t i);
uint8_t pop_doorlink2(uint8_t i);
void pop_doorlink2_set(uint8_t i, uint8_t v);
/* Ряд приземления падающего loose-куска в колонке col комнаты room: первый
* НЕ-пустой тайл сверху вниз; возвращает его ряд, если это floor (→ debris),
* иначе -1 (пролетел/крушение о стену). */
int8_t pop_room_col_landing(uint8_t room, uint8_t col) __banked;
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
* Порт enter_guard (seg002:0112). */
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
* копии массивов уровня. dead != 0 — сохранить и последовательность, тогда
* при возврате в комнату страж поднимется трупом там же, где лёг.
* tile == 30 (или больше) — «стража в комнате больше нет». */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead);
/* Последовательность запомненного стража (0 — поднимать стандартной
* стойкой, как у живого). */
uint16_t pop_guard_state_seq(uint8_t room);
uint8_t pop_guard_state_x(uint8_t room);
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill) __banked;
/* tbl_level_type (SDLPoP data.h:840) — какой тайлсет у уровня:
* 0 подземелье, 1 дворец. Дворцовые: 4, 5, 6, 10, 11, 14. Индекс —
* НОМЕР уровня; элемент 0 — демо-уровень. Оригинал по нему выбирает и
* .DAT окружения (tbl_envir_ki, seg000:1108), и десяток веток отрисовки
* в seg008, и графику стража (GUARD1/GUARD2). */
uint8_t pop_level_type(void) __banked;
uint8_t pop_level_start_room(void) __banked;
uint8_t pop_level_start_pos(void) __banked;
int8_t pop_level_start_dir(void) __banked;
#endif