Compare commits

...

13 Commits

Author SHA1 Message Date
snark13 f76914849f SprPoP: колонки Controls сдвинуты вправо
Слева оставалось 6 свободных точек, справа 36 — блок выглядел прижатым к
краю.  Содержимое шириной 278 точек, сдвиг на +15 делает поля 21 и ~20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:58:16 +03:00
snark13 6cc8611d03 SprPoP: README для игрока и экран Controls в две колонки
README, который едет НА ДИСК рядом с программой, в двух версиях: README.TXT
(английский) и README.RUS (русский).  Краткое описание, запуск, полная
раскладка управления, читы, файлы игры, чего не хватает против оригинала,
благодарности.

Русский обязан быть в CP866 — DSS и местные просмотрщики читают именно её,
UTF-8 показался бы кракозябрами.  Исходники лежат в dist/ как UTF-8 (чтобы
читались в репозитории), перекодировка и CRLF делаются правилом Makefile.
ПОРЯДОК В КОНВЕЙЕРЕ ВАЖЕН: sed ставит CRLF ДО iconv — BSD sed в UTF-8
локали отказывается работать с байтами CP866 («RE error: illegal byte
sequence»), а с валидным UTF-8 работает.  iconv БЕЗ -c намеренно: потеря
символа должна ломать сборку, а не молча портить текст.

ЭКРАН CONTROLS переписан в две колонки.  В одну раскладка больше не
помещалась: после фаз A-C клавиш стало вдвое больше, а по высоте есть
только 200 точек вместе с заголовком.  Слева игра (бег, лазание, бой),
справа служебное и читы; высота блока считается по ДЛИННОЙ колонке.
Строка «IN A FIGHT» — заголовок, а не клавиша: ниже неё те же стрелки и
Shift означают другое, и без разделителя список читался бы противоречиво.

Заодно экран перестал врать: там до сих пор висели P, I, U и F7/F8 —
клавиши, переназначенные ещё в фазе A.

Проверено в MAME: Backspace открывает меню, Settings -> Show key bindings
показывает обе колонки целиком.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:50:59 +03:00
snark13 659071838d SprPoP: KEYS-D, KEYS-R и KEYS-F12 — на доску отложенного
Три остатка плана keys_plan.md записаны в TASKS_OPEN.md с полным разбором,
чтобы не выводить его заново.

KEYS-D (осмотр соседних комнат) — главное, что стоит помнить: у SDLPoP это
три строки, потому что там drawn_room влияет ТОЛЬКО на отрисовку, а физика
ходит через get_tile(room, col, row) с явной комнатой.  У нас наоборот —
карта коллизий грузится для ОТРИСОВАННОЙ комнаты: room_fg, lcol_fg,
rcol_fg, above_fg, below_fg это ОДИН комплект на программу.  Уведи cur_room
к соседу, не трогая kid_room, и Кид считает столкновения по чужим тайлам.
Задел под расхождение уже стоит (kid_room отдельной переменной,
update_kid_render_dx со сдвигом ±140), но enter_room_side пишет обе разом —
это незакрытая часть S3 straddle.

Записаны оба пути с ценой: честный (правка ядра, дни) и смотровой режим с
остановкой игры (150-250 байт, часы) — плюс что главный риск не в
отрисовке, а в возврате.

KEYS-R (воскрешение) — четыре места, которые обязаны знать про окно
неуязвимости, со ссылками на seg-код.

KEYS-F12 (скриншот) — «возможно, когда-то», по пометке пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:38:22 +03:00
snark13 f8e96c0495 SprPoP: читы здоровья и пера — фаза C плана keys_plan.md
Shift+S — одна единица HP (малое красное зелье), Shift+T — поднять потолок
HP (большая склянка), Shift+W — медленное падение (зелье пера).  Механика
у всех трёх в движке уже была: читы просто ставят те же переменные, что и
подобранное зелье.

СВЕРКА С SDLPoP дала деталь, которую легко проглядеть: ветка ЧИТА и ветка
ЗЕЛЬЯ различаются.  Зелье в seg006:1871 зовёт stop_sounds, а чит в
seg000:838 — нет.  Повторяем чит, а не зелье.  Исключение — перо: там
stop_sounds сидит внутри самой feather_fall(), поэтому остаётся.

Вторая деталь: Shift+T НЕ проверяет, полное ли HP, — потолок растёт всегда
(упираясь в POP_MAX_HITP = 10, как max_hitp_allowed оригинала).  У Shift+S
условие hitp_curr != hitp_max есть и сохранено.

Модификаторы разведены: Shift+S делит скан-код с Ctrl+S (звук), поэтому
требует отпущенного Ctrl; Shift+T делит с голым T (таймер), но тот сам
требует отпущенного Shift.

Проверено в MAME по памяти, а не на глаз: Shift+T 3/3 -> 4/4, Shift+W
взводит pop_feather в 148, Shift+S на подпорченном отладчиком HP 2/4 -> 3/4.

R (воскрешение) НЕ входит: это не ещё один чит, а правка модели смерти —
окно неуязвимости, размазанное по кадровой цепочке (счётчик в seg003:512,
пропуск пик и челюстей в seg000:1245, пропуск урона мечом в seg000:876,
восстановление позы в seg006:1352).  Задевает гейт «мёртв», про который
memory pop_level_restart_scope прямо предупреждает.  Отложено по решению
пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:34:19 +03:00
snark13 76f02e76db SprPoP: раскладка управления — фазы A и B плана keys_plan.md
Приводим клавиши к keys.txt (раскладка SDLPoP).  Работа делится надвое, и
здесь только то, что не трогает игровую логику: переназначения (A) и три
мелкие функции поверх готовых механизмов (B).  Читы, требующие правки
логики, и осмотр соседних комнат — фазы C и D, отложены.

ПЕРЕНАЗНАЧЕНИЯ (A)

  U            -> Shift+I   переворот экрана; U отдан «комнате сверху»
  Esc          -> Backspace меню; Esc остаётся дублёром, как у SDLPoP
  F7 / F8      -> - / +     ±минута, основной ряд и цифровой блок
  - / +        -> Ctrl+- / Ctrl++  обход комнат (наш отладочный телепорт)
  I            -> Ctrl+I    бессмертие
  P            -> Ctrl+P    режим скорости
  1 и 2        -> Ctrl+F    стоп-кадр, теперь одной клавишей
  F10 в игре   -> Ctrl+Q    вторая клавиша выхода; F10 ловится глобально

Плюс новое на готовых путях: Ctrl+A — рестарт уровня, Ctrl+V — версия
сборки (функция была, её показывал только старт), Ctrl+D — отладочная
строка (тот же тумблер, что в Settings; POP.CFG не пишем), Home / Page Up —
дублёры диагональных прыжков (у SDLPoP это не отдельное действие, а те же
Up+Left / Up+Right, поэтому просто добавляются к стрелкам).

Все наши сверхштатные клавиши ушли под Ctrl, чтобы не занимать голые буквы
из раскладки, и вписаны в keys.txt отдельным разделом.  Ctrl+B намеренно
не занята: keys.txt держит её под «вернуться в комнату Кида» (фаза D).

ФУНКЦИИ (B)

  Space  «сколько осталось» (seg000:612).  Не печатает сама: поднимает тот
         же pop_show_time, которым пользуется автоматическое объявление
         минут, и строку собирает time_msg() — «59 MINUTES LEFT» и «11
         SECONDS LEFT» остаются в одном месте.
  T      постоянный показ таймера.  Переиспользует поле DBG_F_TIME
         отладочной строки, своего рендера нет.  У верхней полосы теперь
         три состояния, и отслеживается РЕЖИМ (0 нет / 1 таймер / 2 всё),
         а не флаг: переход «таймер -> полоса» тоже перерисовывает всё.
  Ctrl+R возврат в заставку — тот же переход, что «Restart Game» в меню.

ДВЕ ЛОВУШКИ, найденные по дороге

  Модификатор обязан входить в САМО значение, а не в условие блока: с
  `if (ctrl) { nav = ...; nav_prev = nav; }` при отпускании Ctrl кромка
  застревала ненулевой и следующее нажатие глохло.

  Один скан-код на два чита: Shift+I и Ctrl+I — это 0x43 в обоих случаях.
  По той же причине ±минута требует ОТПУЩЕННОГО Ctrl (иначе сработает и
  время, и обход комнат), а T — отпущенных Shift и Ctrl (Shift+T отдан
  «добавить HP» в фазе C).

Обработчики положены в pop_frame_ui (банк 8), а не в резидент: там куча
всего 308 байт.  Проверено в MAME: Ctrl+D поднимает строку
«Level 1, Room 1, Speed: NORMAL...», T — один таймер 59:30 без подписей,
Space — watchpoint на pop_show_time ловит запись значения 2 (именно
обработчик клавиши, автообъявление пишет 1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:22:21 +03:00
snark13 c71981fdf9 SprPoP: pop_config и pop_hof — из банка 9 в банк 10
Банк 9 дошёл до 98,5% (247 свободных байт), и следующая заметная правка
в menu/status/ui упёрлась бы в потолок.  Критерий переселения тот же, что
у остальной раскладки, — ЧАСТОТА ВЫЗОВА, а не размер: POP.CFG и POP.HOF
работают раз за партию и упираются в диск, где один `open` стоит 51 мс,
так что трамплин банк→банк на их фоне не существует.

Отдельный банк заводить не пришлось: в десятом лежал один pop_pal.c на
485 байт, то есть 3% страницы.  Новой страницы в образе не появилось.

  банк 9:  16137 (98,5%) -> 11948 (72,9%), свободно 247 -> 4436
  банк 10:   485 ( 3,0%) ->  4674 (28,5%), свободно 11710

Переезд безопасен, потому что все публичные функции обоих модулей
помечены __banked: трамплин выбирается по пометке в объявлении, а не по
банку, и непомеченная функция пережила бы переезд только внутри своего
банка.  pop_settings — обычный глобал в W2, виден отовсюду.

Следующий рычаг, если понадобится: pop_menu (4745 байт, 29% банка 9,
работает только при открытом меню).  Трогать нельзя pop_ui, pop_status,
pop_music и pop_timer — они в кадровом пути.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:49:01 +03:00
snark13 808c2a5349 SprPoP: заставку можно прервать в любой её точке, F10 — выход
Три места, где нажатие раньше не работало или работало наполовину.

FADE.  fade_run() был замкнутым циклом на 2,13 с без опроса клавиш, а на
стыке экранов их два: пропуск работал внутри сценария, но не между его
шагами, и заставка ощущалась невыключаемой.  Добавлены прерываемые
варианты (pop_ui_fade_*_skip, обёртки в pop_pal) — отдельными функциями,
а не флагом в прежних: в меню паузы и на переходе уровня прерывать
нечего, и менять там поведение молча не следует.  Прерванный fade всё
равно доводит палитру до конца, экран не остаётся на промежуточной
ступени.  Подключено в интерпретаторе сценария, сцене с принцессой,
четырёх катсценах cut_*, титрах и таблице рекордов.

ПРОЯВЛЕНИЕ ПОЛОСАМИ.  pop_screen_present_ltr() была void и нажатие
ГЛОТАЛА: полосы схлопывались, картинка появлялась целиком — и всё,
вызывающий о нажатии не узнавал.  На заставке это выглядело как
«клавиша срабатывает наполовину».  Теперь возвращает признак прерывания,
и он проброшен по маршруту: первый экран истории, титры финала, логотип
между «свадьбой» и титрами.

Везде считается КРОМКА нажатия от входа, как в сценах: клавиша, которой
закончили предыдущий экран, ещё зажата, и принимать её за новое нажатие
нельзя — иначе весь маршрут заставки схлопывался бы сам собой.

F10 — НЕМЕДЛЕННЫЙ ВЫХОД, откуда угодно.  Проверка стоит в kbd_idle(), а
этот хук висит на gfx_wait_vsync, то есть вызывается везде, где программа
ждёт кадр: заставка, титры, fade, проявление полосами, меню, игра.  Одна
точка вместо десятка по циклам ожидания.  Флаг pop_quit_req резидентный —
взводится и читается без трамплина из любого банка.  Проверка в НАЧАЛЕ
витка автомата обязательна: заставку прерывает любая клавиша, и без неё
F10 успевал уронить программу в загрузку уровня перед закрытием.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:49:01 +03:00
snark13 31b82661eb 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>
2026-08-27 12:12:28 +03:00
snark13 4b74478d19 Таблица рекордов на титрах проявляется собранной
Строки дорисовывались уже после полосового перехода: он копирует страницу
акселератором, а тот читает ОЗУ-копию, куда GFX_BANK_SPRITE (NOSHADOW +
TRANSPARENT) не пишет. Банк GFX_BANK_TRANSPARENT даёт ту же прозрачность
0xFF, но обновляет и копию, поэтому страницу можно собрать целиком до
показа — как offscreen оригинала (draw_full_image + show_hof, и только
потом transition_ltr).

Проверено в MAME: на промежуточных кадрах перехода строка рекорда видна в
уже проявившейся части экрана вместе с логотипом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:18:58 +03:00
snark13 747783c422 Реплики сцены с принцессой: вернуть подкачку в кадр
Регрессия 4086dde: тот коммит перенёс шаг постраничной подкачки «в паузу
кадра» — внутрь цикла ожидания тика насоса — и одновременно ввёл защиту от
промотки (snd_want подтягивается к snd_done). Вместе это убило подкачку:
паузы в этой сцене почти нет, цикл ожидания выходит сразу, шаг не
вызывается. Реплики Джафара не успевали загрузиться к своему тику,
pop_music_play() молча ничего не делал, и после первой реплики сцена шла
в тишине.

Замер в MAME (адреса статиков pop_music.c — от public _pop_mus_page по
смещениям из .sym): до фикса ld_next стоял на нуле 20 секунд при активной
загрузке; после — 11 страниц из 11, mus_ready=1, и звучат все три реплики
m50 → m53 → m52.

Фикс: один безусловный load_step в кадре сцены; шаг в паузе оставлен, он
бесплатный, когда время есть.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:00:16 +03:00
snark13 a3c5c600da Таблица рекордов: оба показа оригинала, ввод имени, переходы полосами
pop_hof.c был написан, но никуда не вызывался: после финала автомат уходил
мимо него в title, а на титрах экран получался чёрным. Теперь это хвост
end_sequence и шаг show_title между credits и attract-demo, как в оригинале.

Два корня пустого экрана:

- story.pal — 256 записей, и запись 0x3F (цвет глифов шрифта) в ней чёрная:
  текст рисовался, но был не виден. Генератор кладёт туда золотой 0xB7;
- полосовой переход копирует страницу акселератором, а тот читает ОЗУ-копию,
  куда прозрачный блит текста не пишет. Строки рисуются после перехода,
  прямо в видимую страницу.

Попутно:

- s5 в архиве PV — фон таблицы (рамка story + логотип на y=24, HOF_POP);
- отдельный индекс палитры под фон текстовой рамки: финал красит его в
  #800000, титры оставляют #100060 (load_title_images(bgcolor)). Прежний
  ремап в индекс 9 подменить было нельзя — им нарисована сама титульная
  картинка;
- второй набор глифов в font.atl цветом 0x3E: у SDLPoP шрифт маска и
  show_hof_text рисует текст дважды разным цветом, у нас цвет запечён в
  пиксели. Каталог SPA1 держит счётчик в байте, поэтому набор обрезан по '_';
- общий pop_screen_present_ltr(): им теперь пользуются story-переход интро,
  титры, таблица и титульная картинка финала. Заодно вернулся пропущенный
  шаг show_title — экран с логотипом и Jordan Mechner между «свадьбой» и
  титрами (BUGS_CLOSED#cutscene-ltr).

Формат POP.HOF — 6 записей, имя до 15 символов, версия 2. Проверено в MAME
на сборке LEVEL=14: HAIL → титульная картинка → таблица с полосой ввода и
временем справа → ввод имени → титульная картинка; на следующем запуске
таблица показывается между credits и demo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:41:41 +03:00
snark13 faec9a7d3a Финальная тема won: кольцевой стриминг с диска
Последний неозвученный кусок игры. 115 секунд и 1,2 МБ в память не влезают
никак, поэтому тема играется кольцом из шести страниц (96 КБ = 9 с): насос
идёт по кругу, а pop_music_service дочитывает файл в те страницы, которые
насос уже прошёл. Страница звучит 1,5 с и читается 33 мс — запас
сорокакратный; дистанция считается без отдельных счётчиков, потому что
страница это ровно 128 блоков насоса.

Три вещи, всплывшие при живой проверке:

- кольцо обязано сниматься при любом обычном запуске трека, иначе
  следующая тема играет по кругу первых шести страниц (поймано на титрах
  сразу после победы);
- живые сцены комнаты принцессы теперь открывают CBL сами (cut_begin):
  pop_ending_show глушит звук первым действием, и «arrived to princess»
  не звучал вовсе;
- тема дослушивается до конца с возможностью прервать клавишей — как
  seg001:637. У оригинала там стоит ввод имени в таблицу рекордов; когда
  он появится, ожидание переедет за него (задача HOF-ENTRY).

Проверено в MAME на сборке LEVEL=14: после встречи с принцессой звучит
тема победы (id 56, кольцо 6), курсор уходит далеко за размер кольца —
подкачка успевает. Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:32:40 +03:00
snark13 5d912f0f3f Доска: ARC-ATLASES закрыта, SND закрыта иначе (PCM вместо AY)
Обе задачи выполнены, но SND — не так, как планировалась: музыка пошла не
на AY, а тем же PCM через CBL. Исходные постановки свёрнуты в details,
сверху — что фактически сделано и какие грабли попались.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:04:37 +03:00
298 changed files with 53404 additions and 272 deletions
+13
View File
@@ -131,3 +131,16 @@ applications/PoP/PoP1_DOS_music/
# R1 — рабочая копия roomtest для экспериментов пользователя, в репозиторий
# не идёт (сама roomtest и есть версируемая ветка разработки).
applications/PoP/R1/
# SprPoP — автономное приложение. В репозиторий идут исходники, генерируемые
# заголовки (gen/) и упакованные ресурсы (assets/packed/), потому что без них
# из чистого клона ничего не собрать. НЕ идут:
# assets/orig/ оригинальные данные игры — копирайт (SDLPoP, MSDOS,
# записи музыки DOS-версии); откуда их взять,
# написано в assets/orig/README.md
# assets/packed/LEVELS/ уровни res20xx.bin ложатся на диск прямо из
# оригинала, конвертации нет — то есть это тот же
# копирайтный материал, только в раскладке диска
# build/ уже покрыт общим правилом build/ выше
applications/SprPoP/assets/orig/
applications/SprPoP/assets/packed/LEVELS/
+3 -1
View File
@@ -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
+30 -9
View File
@@ -36,17 +36,37 @@ 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
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)
CC_FLAGS := --memory $(MEMORY)
ifneq ($(STACK_SIZE),)
@@ -54,14 +74,15 @@ 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)
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
$(if $(strip $(BUILD_DIR)),@mkdir -p $(dir $@))
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
$(MKEXE):
@@ -76,13 +97,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,7 +115,7 @@ 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
hdd: $(EXE)
$(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
@echo
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
+31 -5
View File
@@ -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), курсор уходит далеко
за размер кольца — то есть подкачка успевает.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+59
View File
@@ -3571,3 +3571,62 @@ 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`.
+8 -10
View File
@@ -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).
---
+2 -2
View File
@@ -214,7 +214,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 +235,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) \
+55
View File
@@ -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.
+64 -7
View File
@@ -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), куча
+4 -7
View File
@@ -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;
+7 -1
View File
@@ -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
+246 -86
View File
@@ -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);
}
+13 -7
View File
@@ -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 */
+121 -40
View File
@@ -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));
}
+7 -1
View File
@@ -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 */
+114
View File
@@ -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) {
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
* продолжают звучать у них свой тумблер. */
+11
View File
@@ -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;
+3 -2
View File
@@ -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 */
+10 -1
View File
@@ -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
+4 -1
View File
@@ -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 — чёрный. */
+16 -2
View File
@@ -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;
}
+20
View File
@@ -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--) {
+10
View File
@@ -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 */
+44 -4
View File
@@ -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)
+11 -1
View File
@@ -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.
* Полоса загружается, блитится и тут же освобождается; функция не вызывает
+59 -54
View File
@@ -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
+4 -8
View File
@@ -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;
+4 -4
View File
@@ -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);
}
+179
View File
@@ -0,0 +1,179 @@
# 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) — ЕДИНСТВЕННЫЙ
авторитетный источник того, как оригинальный движок это делает.** Правило
без исключений:
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 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, записи музыки). Вне репозитория — копирайт; что и откуда взять, написано в `assets/orig/README.md` |
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках |
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
| `tests/host/` | модульные тесты движка под ucsim_z80 |
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
Музыка собирается из одного из четырёх наборов записей
(`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_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
## Порядок слоёв в кадре (важно для окклюзии)
`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`.
+519
View File
@@ -0,0 +1,519 @@
# 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
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_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) —
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
SND_ATL := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(ATL)/sound/s$(n).bin)
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
# 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: README.TXT и README.RUS.
DISK := README.TXT README.RUS \
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 \
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) \
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
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_atlas.h kid_data.h \
pop_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \
pop_music_tbl.h pop_music_ticks.h pop_font.h)
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
EXTRA_DATA := $(STAGED)
HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \
$(foreach f,$(DISK),$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f))
# Образ живёт в 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
$(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))
$(eval $(call stamp_rule,sound,$(TOOLS)/pop_pack_sound.py $(TOOLS)/paths.py,pop_pack_sound.py))
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/paths.py,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 gen/kid_atlas.h,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,$(SND_ATL) gen/pop_sound_tbl.h,sound))
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) gen/pop_music_tbl.h gen/pop_music_ticks.h,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,
# которую печатает сам упаковщик звука.
$(PACKED)/SND/snd.arc: $(SND_ATL) $(TOOLS)/pop_pack_arc.py
@mkdir -p $(dir $@)
$(ARC) $@ $(SND_ATL)
# Файлы, которые ложатся на диск как есть, — простое копирование из 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)))
# Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
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.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.RUS: dist/README.ru.txt
@mkdir -p $(dir $@)
@sed 's/$$/\r/' $< | iconv -f UTF-8 -t CP866 > $@
resources-readme: $(PACKED)/README.TXT $(PACKED)/README.RUS
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
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin)
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/ — генерировать ресурсы не из чего." >&2; \
echo " Что и откуда положить, написано в assets/orig/README.md" >&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
# Перелинковка приложения без пересборки библиотек и ресурсов.
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) \
relink distclean mame-link
+64
View File
@@ -0,0 +1,64 @@
# 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
```
## Что где
```
src/ рукописный C: главный цикл sprpop.c + движок pop_*.c
gen/ генерируемые заголовки — руками не править
assets/orig/ оригинальные данные игры (вне репозитория, см. там README)
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 resources` | перегенерировать ресурсы из `assets/orig/` |
| `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` |
| `make -C tests/host` | модульные тесты движка (секунды, без эмулятора) |
| `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`.
## Происхождение
Выделен из `applications/PoP/roomtest` 2026-08-26; та папка заморожена и
служит архивом закрытых задач, багов и исполненных планов. Референсные
реализации (Apple II 6502, Princed Resources, mininim) остались в
`applications/PoP/`.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,130 @@
===========================================================================
SprPoP -- Prince of Persia ¤«ï Sprinter Sp2000
===========================================================================
®àâ ¨£àë Prince of Persia „¦®à¤ ­  Œ¥å­¥à  (1989) ­  Sprinter Sp2000.
ƒà ä¨ª  ¢§ïâ  ¨§ VGA-¢¥àᨨ ¤«ï MS-DOS, ०¨¬ 320x256 á ¯ «¨âன ­  256
梥⮢; ¬ã§ëª  ¨ §¢ãª®¢ë¥ íä䥪âë - ®à¨£¨­ «ì­ë¥ ®æ¨ä஢ª¨, ¨£à îâáï ç¥à¥§
ª ­ « CBL. ‚ ¨£à¥ ¢á¥ ç¥âëà­ ¤æ âì ã஢­¥©, § áâ ¢ª , íªà ­ë ¨áâ®à¨¨,
¢áâ㯫¥­¨¥ á ¯à¨­æ¥áᮩ ¨ „¦ ä à®¬, 䨭 « ¨ â ¡«¨æ  ४®à¤®¢.
‡€“‘Š
---------------------------------------------------------------------------
ˆ§ ª®¬ ­¤­®© áâப¨ DSS, ­  ⮬ ¤¨áª¥, £¤¥ «¥¦¨â ¨£à :
sprpop
冷¬ á ¯à®£à ¬¬®© ¤®«¦­ë «¥¦ âì ¥ñ ª â «®£¨ á ¤ ­­ë¬¨:
BG\ â ©«ë ä®­  KID\ ¯à¨­æ
GUARD\ áâà ¦¨ SKEL\ ᪥«¥â
VIZIER\ „¦ ä à SHADOW\ ⥭ì
SND\ §¢ãª®¢ë¥ íä䥪âë MUS\ ¬ã§ëª 
FONT\ èà¨äâ TITLE\ § áâ ¢ª 
PV\ ¨áâ®à¨ï ¨ ¢áâ㯫¥­¨¥ LEVELS\ ç¥âëà­ ¤æ âì ã஢­¥©
¥§ ª ª®£®-­¨¡ã¤ì ª â «®£  ¨£à  ¢áñ à ¢­® § ¯ãáâ¨âáï, ­® â®, çâ® ¢ ­ñ¬
«¥¦ «®, ®ª ¦¥âáï ¡¥§§¢ãç­ë¬ ¨«¨ ­¥¢¨¤¨¬ë¬.
€‚‹…ˆ…
---------------------------------------------------------------------------
¥£ ¨ « § ­¨¥
‚«¥¢® / ‚¯à ¢® ¯®¢¥à­ãâìáï ¨«¨ ¡¥¦ âì
‚¢¥àå ¯à릮ª ¨«¨ ¯®¤âï­ãâìáï
‚­¨§ ¯à¨á¥áâì ¨«¨ á¯ãáâ¨âìáï
‚­¨§ + ‚«¥¢®/‚¯à ¢® ¯à릮ª ­  ¬¥áâ¥
Shift ¯®¤®¡à âì ¯à¥¤¬¥â
Shift + ‚«¥¢®/‚¯à ¢® ®áâ®à®¦­ë© è £
Home ¨«¨ ‚¢¥àå+‚«¥¢® ¯à릮ª ¢«¥¢®
PgUp ¨«¨ ‚¢¥àå+‚¯à ¢® ¯à릮ª ¢¯à ¢®
‚¢¥àå ­  ¡¥£ã ¯à릮ª á à §¡¥£ 
Shift ¢ ¯ ¤¥­¨¨ § æ¥¯¨âìáï §  ª à­¨§
®© ­  ¬¥ç å
‚«¥¢® / ‚¯à ¢® ­ áâ㯠âì ¨«¨ ®âáâ㯠âì
Shift 㤠à
‚¢¥àå § é¨â 
‚­¨§ ã¡à âì ¬¥ç
(Shift ¤®áâ ñâ ¥£® á­®¢ )
® 室㠨£àë
Backspace ¨«¨ Esc ¯ ã§  ¨ ¬¥­î
஡¥« ᪮«ìª® ®áâ «®áì ¢à¥¬¥­¨
T ¤¥à¦ âì â ©¬¥à ­  íªà ­¥
F6 ¡ëáâ஥ á®åà ­¥­¨¥
F9 ¡ëáâà ï § £à㧪 
Ctrl+A ­ ç âì ã஢¥­ì § ­®¢®
Ctrl+R ¢¥à­ãâìáï ª § áâ ¢ª¥
Ctrl+S §¢ãª ¢ª« / ¢ëª«
Ctrl+M ¬ã§ëª  ¢ª« / ¢ëª«
Ctrl+V ¯®ª § âì ¢¥àá¨î ᡮન
Ctrl+Q ¨«¨ F10 ¢ë室
F10 à ¡®â ¥â ¢¥§¤¥, ¢ ⮬ ç¨á«¥ ¢® ¢à¥¬ï § áâ ¢ª¨. ‹î¡ ï ¤àã£ ï ª« ¢¨è  ­ 
§ áâ ¢ª¥ ¨«¨ ­  íªà ­ å ¨áâ®à¨¨ ¯à®¯ã᪠¥â ¨å ¨ ­ ç¨­ ¥â ¨£àã.
—ˆ’›
---------------------------------------------------------------------------
—¨âë ®à¨£¨­ « ; ¢ í⮩ ᡮથ ®­¨ ¢ª«îç¥­ë ¯® 㬮«ç ­¨î. ‚몫îç¨âì ¬®¦­® ­ 
íªà ­¥ ­ áâ஥ª.
Shift+L á«¥¤ãî騩 ã஢¥­ì
+ / - ­  ¬¨­ãâã ¡®«ìè¥ / ¬¥­ìè¥
K 㡨âì áâà ¦ 
Shift+I ¯¥à¥¢¥à­ãâì íªà ­
Shift+W ¬¥¤«¥­­®¥ ¯ ¤¥­¨¥
Shift+S ¢¥à­ãâì ®¤­ã ¥¤¨­¨æã §¤®à®¢ìï
Shift+T ¯à¨¡ ¢¨âì §¤®à®¢ìï (¤® ¤¥áïâ¨)
[ / ] ¯®¤¢¨­ãâì ¯à¨­æ  ­  ¯¨ªá¥«ì
„¢ãå ç¨â®¢ ®à¨£¨­ «  ¯®ª  ­¥â: R (¢®áªà¥á¨âì ¯à¨­æ ) ¨ H / J / U / N ¢¬¥áâ¥
á Ctrl+B (¯®á¬®âà¥âì á®á¥¤­îî ª®¬­ âã).
Žâ« ¤®ç­ë¥ ª« ¢¨è¨ í⮣® ¯®àâ , ª®â®àëå ¢ ®à¨£¨­ «¥ ­¥â ¢®¢á¥. ‚ᥠ®­¨ ¯®¤
Ctrl, çâ®¡ë ­¥ § ­¨¬ âì ¯à®áâë¥ ¡ãª¢ë ¨§ à áª« ¤ª¨ ¢ëè¥.
Ctrl+I ¡¥áᬥà⨥: ¢ëª« / ¢ ¡®î / ¯®ç⨠¢® ¢áñ¬
Ctrl+P ०¨¬ ᪮à®áâ¨
Ctrl+F ®áâ ­®¢¨âì ª ¤à ¨ á­®¢  ¯ãáâ¨âì
Ctrl+D ®â« ¤®ç­ ï áâப : ã஢¥­ì, ª®¬­ â , ᪮à®áâì, §¢ãª
Ctrl++ / Ctrl+- ¯¥à¥©â¨ ¢ á«¥¤ãîéãî / ¯à¥¤ë¤ãéãî ª®¬­ âã ¯® ­®¬¥àã
”€‰‹›, ŠŽ’Ž›… ‘އ„€ð’ ˆƒ
---------------------------------------------------------------------------
POP.CFG ­ áâனª¨
POP.SAV ¡ëáâ஥ á®åà ­¥­¨¥, POP.BAK - ¯à¥¤ë¤ã饥
POP.HOF â ¡«¨æ  ४®à¤®¢
‚ᥠ®­¨ ¯®ï¢«ïîâáï à冷¬ á ¯à®£à ¬¬®©, ¨ «î¡®© ¬®¦­® 㤠«¨âì - ¨£à  ¯à®áâ®
­ ç­ñâ á® §­ ç¥­¨© ¯® 㬮«ç ­¨î.
—’Ž …™ð … ƒŽ’Ž‚Ž
---------------------------------------------------------------------------
‚ᥠç¥âëà­ ¤æ âì ã஢­¥© ¯à®©¤¥­ë. ®à⠯த®«¦ ¥â à §¢¨¢ âìáï, â ª çâ®
è¥à®å®¢ â®á⨠¥éñ ¢áâà¥ç îâáï; â¥, ® ª®â®àëå ¬ë §­ ¥¬, ¢¥¤ãâáï ¢ ᯨ᪥
¡ £®¢ ¯à®¥ªâ .
à®â¨¢ ®à¨£¨­ «  ­¥ 墠⠥â: ç¨â  R (¢®áªà¥á¨âì ¯à¨­æ ), ç¨â®¢ H / J / U / N
¨ Ctrl+B ¤«ï ®á¬®âà  á®á¥¤­¨å ª®¬­ â ¨ ª« ¢¨è¨ á­¨¬ª  íªà ­  (F12).
‹€ƒŽ„€Ž‘’ˆ
---------------------------------------------------------------------------
Prince of Persia ¯à¨¤ã¬ « ¨ ­ ¯¨á « „¦®à¤ ­ Œ¥å­¥à ¢ 1989 £®¤ã.
Œ¥å ­¨ª  í⮣® ¯®àâ  á¢¥àï« áì á SDLPoP (github.com/NagyD/SDLPoP) - íâ®
á ¬®¥ â®ç­®¥ ¯à®ç⥭¨¥ ⮣®, ª ª ¢¥¤ñâ á¥¡ï ®à¨£¨­ «ì­ë© ¤¢¨¦®ª. ƒà ä¨ª 
¢§ïâ  ¨§ ¢¥àᨨ ¤«ï MS-DOS, §¢ãª®¢ë¥ íä䥪âë - ¨§ ¥ñ ä ©«®¢ DIGISND, ¬ã§ëª 
- ¨§ § ¯¨á¥© á ã­¤â४  DOS-¢¥àᨨ.
‘ ¬ ¯®àâ ᮡ࠭ ­ è¨¬ ª®¬¯¨«ïâ®à®¬ sprinter-cc ­  ®á­®¢¥ SDCC ¤«ï Sprinter
Sp2000.
@@ -0,0 +1,134 @@
===========================================================================
SprPoP -- Prince of Persia for the Sprinter Sp2000
===========================================================================
A port of Jordan Mechner's Prince of Persia (1989) to the Sprinter Sp2000.
The graphics are the VGA ones from the MS-DOS release, in 320x256 with a
256-colour palette; the music and the sound effects are the digitised
originals, played through the CBL channel. All fourteen levels, the title
sequence, the story screens, the intro with the Princess and Jaffar, the
ending and the hall of fame are in.
RUNNING IT
---------------------------------------------------------------------------
From the DSS prompt, on the drive the game sits on:
sprpop
The game needs its data directories next to the program:
BG\ background tiles KID\ the prince
GUARD\ guards SKEL\ the skeleton
VIZIER\ Jaffar SHADOW\ the shadow
SND\ sound effects MUS\ music
FONT\ text font TITLE\ title screens
PV\ story and intro screens LEVELS\ the fourteen levels
If a directory is missing the game still starts, but whatever lived there
will be silent or invisible.
CONTROLS
---------------------------------------------------------------------------
Running and climbing
Left / Right turn, or run
Up jump, or climb up
Down crouch, or climb down
Down + Left/Right hop
Shift pick things up
Shift + Left/Right careful step
Home or Up+Left jump to the left
PgUp or Up+Right jump to the right
Up while running running jump
Shift while falling grab onto a ledge
Sword fighting
Left / Right advance or retreat
Shift strike
Up block
Down put the sword away
(Shift draws it again)
While the game runs
Backspace or Esc pause and open the menu
Space how much time is left
T keep the timer on screen
F6 quicksave
F9 quickload
Ctrl+A restart the level
Ctrl+R back to the title screen
Ctrl+S sound on / off
Ctrl+M music on / off
Ctrl+V show the build version
Ctrl+Q or F10 quit
F10 works everywhere, including the title sequence. Any other key during
the titles or the story screens skips straight into the game.
CHEATS
---------------------------------------------------------------------------
The cheats of the original, and they are on by default in this build. You
can turn them off on the Settings screen.
Shift+L go to the next level
+ / - one minute more / less
K kill the guard
Shift+I turn the screen upside down
Shift+W slow falling
Shift+S restore one hit point
Shift+T one more hit point (up to ten)
[ / ] nudge the prince one pixel
Two cheats of the original are NOT here yet: R (resurrect the prince) and
H / J / U / N with Ctrl+B (look at the neighbouring room).
Debug keys of this port, which the original has no equivalent for. They
all sit under Ctrl so that no plain letter is taken away from the layout
above.
Ctrl+I immortality: off / in combat / almost everything
Ctrl+P speed mode
Ctrl+F freeze the frame, and unfreeze it
Ctrl+D debug bar: level, room, speed, sound, timer
Ctrl++ / Ctrl+- jump to the next / previous room by number
FILES THE GAME WRITES
---------------------------------------------------------------------------
POP.CFG settings
POP.SAV quicksave, and POP.BAK the one before it
POP.HOF hall of fame
All of them appear next to the program, and all of them can be deleted --
the game will simply start from its defaults again.
WHAT IS NOT FINISHED
---------------------------------------------------------------------------
All fourteen levels have been played through. The port is still under
active development, so rough edges do turn up; the ones we know about are
tracked in the project's own bug list.
Missing against the original: the R cheat (resurrect the prince), the
H / J / U / N and Ctrl+B cheats for looking at neighbouring rooms, and the
screenshot key (F12).
CREDITS
---------------------------------------------------------------------------
Prince of Persia was created by Jordan Mechner in 1989.
The mechanics of this port were checked against SDLPoP
(github.com/NagyD/SDLPoP), which is the authoritative reading of how the
original engine behaves. The graphics come from the MS-DOS release, the
sound effects from its DIGISND files, and the music from recordings of the
DOS soundtrack.
The port itself is built with sprinter-cc, our SDCC-based toolchain for the
Sprinter Sp2000.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More