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
+104
View File
@@ -0,0 +1,104 @@
/*
* pop_music_tbl.h — каталог музыкальных треков, СГЕНЕРИРОВАН.
* Источник: assets/orig/PoP1_DOS_music/flac (записи DOS-версии).
* Генератор: tools/pop_pack_music.py. Руками не править.
*
* Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —
* сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека
* в порциях насоса по 128 байт (в байтах не влезло бы в uint16).
*
* Длительности реплик в тиках вынесены в pop_music_ticks.h: таблица
* ниже — 171 байт ДАННЫХ, и модулю, которому нужна одна константа,
* платить за неё копией таблицы незачем (поймано на pop_intro.c:
* +172 байта в банк 11).
*
* POP_MUS_TICKS_<id> — та же длина в ТИКАХ СЦЕНЫ (60 Гц оригинала).
* Нужна там, где сценарий ждёт окончания реплики: у оригинала это
* `do { frame(1) } while (check_sound_playing())`, у нас — пауза
* длиной в нашу запись. Считается ЗДЕСЬ, а не в C, по двум
* причинам: blocks*40 переполняет uint16 на won (9855 порций), и
* число обязано соответствовать НАБОРУ, которым собрали (flac и
* mt32 — разные исполнения разной длины).
*
* Отношение 40/57 — обратное к тому, которым шкала сцены переводит
* тики в порции насоса (57/40, см. pop_intro.c). Берём именно его,
* а не точное 10937,5/128/60, чтобы длина реплики и ход шкалы
* считались одной и той же арифметикой. Округление — к ближайшему
* (+28 перед делением): отбрасывание вниз теряло по тику на реплику.
*/
#ifndef POP_MUSIC_TBL_H
#define POP_MUSIC_TBL_H
#include <stdint.h>
#define POP_MUS_COUNT 57
#define POP_MUS_BLOCK 128
#define POP_MUS_MAX_PAGES 78
typedef struct {
uint8_t pages; /* 0 — трека нет */
uint16_t blocks; /* длина в порциях по 128 байт */
} pop_mus_ent_t;
static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
{ 0, 0 }, /* 0 */
{ 0, 0 }, /* 1 */
{ 0, 0 }, /* 2 */
{ 0, 0 }, /* 3 */
{ 0, 0 }, /* 4 */
{ 0, 0 }, /* 5 */
{ 0, 0 }, /* 6 */
{ 0, 0 }, /* 7 */
{ 0, 0 }, /* 8 */
{ 0, 0 }, /* 9 */
{ 0, 0 }, /* 10 */
{ 0, 0 }, /* 11 */
{ 0, 0 }, /* 12 */
{ 0, 0 }, /* 13 */
{ 0, 0 }, /* 14 */
{ 0, 0 }, /* 15 */
{ 0, 0 }, /* 16 */
{ 0, 0 }, /* 17 */
{ 0, 0 }, /* 18 */
{ 0, 0 }, /* 19 */
{ 0, 0 }, /* 20 */
{ 0, 0 }, /* 21 */
{ 0, 0 }, /* 22 */
{ 0, 0 }, /* 23 */
{ 3, 305 }, /* 24 regular_death */
{ 2, 196 }, /* 25 find_mirror_and_find_shadow */
{ 9, 1132 }, /* 26 embrace */
{ 8, 915 }, /* 27 cutscene_before_2_4_6_12 */
{ 6, 656 }, /* 28 fight_death */
{ 4, 428 }, /* 29 meet_Jaffar */
{ 3, 299 }, /* 30 life_potion */
{ 0, 0 }, /* 31 */
{ 5, 624 }, /* 32 end_level_4_and_killed_by_shadow */
{ 2, 205 }, /* 33 potion */
{ 0, 0 }, /* 34 */
{ 7, 888 }, /* 35 cutscene_before_8_9 */
{ 10, 1256 }, /* 36 out_of_time */
{ 3, 280 }, /* 37 guard_death_and_obtaining_the_sword */
{ 0, 0 }, /* 38 */
{ 13, 1591 }, /* 39 floating */
{ 5, 596 }, /* 40 cutscene_not_much_time_left */
{ 9, 1043 }, /* 41 story_5_level_end_and_merge_with_shadow */
{ 0, 0 }, /* 42 */
{ 3, 285 }, /* 43 killed_Jaffar */
{ 0, 0 }, /* 44 */
{ 0, 0 }, /* 45 */
{ 0, 0 }, /* 46 */
{ 0, 0 }, /* 47 */
{ 0, 0 }, /* 48 */
{ 0, 0 }, /* 49 */
{ 10, 1184 }, /* 50 story_2_princess */
{ 0, 0 }, /* 51 */
{ 20, 2443 }, /* 52 story_4_Jaffar_leaves */
{ 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 */