Commit Graph

9 Commits

Author SHA1 Message Date
snark13 63bfd997a9 SprPoP: звук мигания, равномерные часы, полоса HP после рестарта, надпись ждёт мелодию
Четыре правки прогона 2026-08-31.  Две ПРОВЕРЕНЫ пользователем в MAME
(звук мигания, порядок «мелодия -> надпись»), две ждут проверки — образ
собран.

ЗВУК МИГАНИЯ (проверено).  Упаковщик научился синтезировать ноты PC-
спикера в обычный сэмпл: заголовок с темпом, тройки «частота + длина»,
меандр на нашей частоте вывода.  Берём из нот ТОЛЬКО номера, которых нет
ни в оцифровке, ни среди мелодий, — иначе синтез перекрыл бы музыку,
которую мы играем из MUS/.  На поставке SDLPoP это ровно один номер: 38,
сигнал под мигание «Press Button»; 31, 34 и 42 там пустые заглушки.
Громкость по слуховой проверке снижена вдвое (44 -> 22): на полном
размахе сигнал перекрикивал игру.  EXE не меняется — раскладка читается с
диска, набор занял те же 9 страниц.

НАДПИСЬ ЖДЁТ МЕЛОДИЮ (проверено).  Порядок оригинала: ветка мёртвого
(seg006:1351) на седьмом шаге выходит, пока звук играет, и «Press Button»
появляется только после музыки смерти.  Чтобы ожидание не было
принудительным, три быстрых пути (Ctrl+A, обе быстрые загрузки, пункты
меню) музыку глушат — оригинал при Ctrl+A делает то же (seg000:0617).
Обычная кнопка во время мелодии не действует: она ответ НА надпись.

ЧАСЫ (ждёт проверки).  Тик стоит столько кадров ЛУЧА, сколько их в кадре
режима NORMAL, поэтому FAST/FASTEST больше не ускоряют время.  Считаем
ФАКТИЧЕСКИ прошедшие кадры луча, а не ожидаемый делитель: логический кадр
не всегда укладывается в бюджет, и часы «по делителю» шли рывками (первый
прогон это показал — «несколько секунд быстро, потом притормаживание»).
Вклад одного вызова ограничен, иначе пауза и меню прыгнули бы вперёд.
Четыре новых теста: NORMAL не сдвинулся ни на тик (и вне боя, и в бою),
FAST и FASTEST держат реальное время.

ПОЛОСА HP ПОСЛЕ Ctrl+A (ждёт проверки).  Корень: счётчик считает
СТРАНИЦЫ, а тратился по КАДРАМ — между двумя вызовами переворота может не
быть, и оба прохода уходили в одну страницу, вторая оставалась с
делениями прошлого боя.  Теперь проход тратится только при смене
gfx_get_draw_page().  Плюс полная чистка всей ширины при инвалидации:
старая полоса могла заходить под статус-текст, где щадящая чистка её не
трогала; текст сразу перезапрашивается.

Все 16 наборов host-тестов зелёные, check_bank_calls чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 23:10:35 +03:00
snark13 b0e7130d0b SprPoP: мёртвое тело больше не приземляется в присед, надпись смерти не залипает
Два независимых фикса, оба проверены в MAME.

1. ПРИЗЕМЛЕНИЕ МЁРТВОГО (симптом «убили Кида, а он сел этажом ниже»).
   Оригинал спрашивает у приземляющегося, жив ли он (seg005:0173): вся
   развязка «мягко / средне / разбиться» отведена ЖИВОМУ, телу — своя
   ветка (добить HP, звук падения насмерть, seq_22).  У нас развилки не
   было, и в глаза это не бросалось только из-за высоты: тело, сброшенное
   ударом с ОДНОГО ряда, набирает fall_y < 22 — урона нет, «последнее HP»
   не тратится, ветка «разбился» не выбирается никогда.  Труп уходил в
   мягкое приземление и садился (кадр 109).

   Цена — один тест байта на вызов land(), то есть на событие касания
   земли, а не на кадр.  Живой путь не изменился ни на операцию.

2. ЗАЛИПАВШАЯ НАДПИСЬ «Press Button to Continue».  Счётчик кадров смерти
   живёт снаружи главного витка и потому переживает возврат на заставку.
   Ответ игрока кнопкой его обнулял, а выход по таймауту (24 с молчания
   -> title) уходил мимо сброса.  Дальше счётчик оставался израсходованным
   на всю сессию, и в следующей игре ПЕРВАЯ же смерть мгновенно уводила в
   title, не показав надписи; лечилось только перезапуском программы.
   Сброс поставлен на входе в игровой маршрут — закрывает и остальные
   боковые дороги (выпадение за нижнюю границу, смена уровня).

Здесь же довезена связка находок 12/13 аудита: смерть безоружного у
обрыва уходит в свою последовательность (seq_81), а прижатие к полу
осталось страховкой для прочих веток — снять его целиком не вышло дважды,
подробности в комментарии guards.c.

Тесты: t_death дополнен обеими сторонами развилки (мёртвый обязан
разбиться, живой с той же высоты — сесть без урона), 18 проверок; все 16
наборов host-тестов зелёные.

В доску записан CLIMB-VS-GUARD: Кид подтягивается к стражу этажом выше —
у нас удар порой смертелен, в оригинале Кид срывается без урона.  Цепочка
засчитывания удара сверена с оригиналом и совпадает дословно, расходятся
входные данные.  Лучшая зацепка — ЗВУК: оригинал играет взмах клинка (11)
при любом уколе, до всякой проверки попадания, а у нас слышен упор в
стену (8) — значит страж не атакует, а сталкивается.  Набор звуков
проверен и не виноват.  Отложено по решению пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 22:17:38 +03:00
snark13 df5071a967 SprPoP: музыка без перелинковки — длины и длительности уехали на диск
Часть I плана music_runtime_index_plan.md (MI0..MI5).  gen/pop_music_tbl.h
и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик
читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в
make test-tools).  Один и тот же sprpop.exe работает с любым из четырёх
наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется.

ГДЕ ЖИВЁТ ИНДЕКС.  228 байт таблицы в W2 не положить (свободной кучи там
порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в
резиденте от него два байта.  Данные в странице — со смещения 0x100:
gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля
они попали бы прямо в записи id 10 и 21.  Со смещением работает штатная
защита, а не запрет прерываний (тот же приём, что CFG_BASE в
pop_config.c).  Число страниц в индексе не хранится — считается из blocks,
чтобы не разъехалось.

ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ.  pop_endmus_left и
POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() —
«заявка лежит, идёт загрузка или трек звучит».  Одного busy мало: между
заявкой и первой нотой 190-230 мс (замер в sound_plan §9).  Прежний
счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого
делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3
кадра луча, а не 4).  Побочно исправилось расхождение с SDLPoP: при
выключенном звуке заявка не кладётся, и уровень меняется сразу, как в
оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный
уровень лишние 12 секунд в тишине.

PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из
индекса при входе в сцену; прежние выражения шкалы не изменились.  План
предлагал протащить структуру времён через пять функций — для сцены,
которая идёт раз за запуск, это того не стоит.

ПАМЯТЬ.  За обе фазы резидент не вырос, а освободился: _CODE 23865 ->
23544, куча 239 -> 256 Б.  Банк 9 похудел на 118 Б (ушла pop_mus_tbl из
rodata), банк 11 — на длительности реплик.

ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в
индексах различаются, и контрольные значения совпали с предсказанными
планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462
блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его
длительностям; без mus.idx музыки нет, эффекты работают, игра проходима.

НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в
игре.  Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 17:17:59 +03:00
snark13 623199337e SprPoP: HDD-раскладка и пути от каталога EXE 2026-08-30 11:21:11 +03:00
snark13 f25ed37d85 SprPoP: единые часы в сцене с Джафаром + конец уровня ждёт свою мелодию
1. СЦЕНА С ДЖАФАРОМ — ОДНИ ЧАСЫ, КАДРЫ ЛУЧА.  Было две шкалы, и выбор между
   ними делался гонкой на старте сцены: одна выборка pop_snd_tick через кадр,
   «успел ли диск раскрутить звук».  От прогона к прогону сцена шла то по
   тикам насоса CBL, то по кадрам луча, и кода реплики приходилась каждый раз
   на другое место картинки (наблюдение пользователя).  Насос был нужен
   потому, что кадр рисовался дольше своего интервала; теперь отрисовка
   разложена по интервалам (pv_restore_bg), и счёт кадров честен — ветка
   насоса убрана целиком.

2. ПОДКАЧКА ТРЕКА — ПОЛСТРАНИЦЫ ЗА ШАГ (pop_music_load_step).  8 КБ ≈ 16 мс
   влезают в кадровый интервал, целая страница (33 мс) не влезала и
   растягивала кадр сцены.  В сцене шаг остаётся безусловным (иначе реплики
   не успевали грузиться, memory pv_music_stall_regression) и оплачивается
   ровно одним интервалом.

3. КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ.  Оригинал (seg003:387, play_level_2) не
   сменяет уровень, пока `check_sound_playing()`: экран пройденного уровня
   живёт с анимацией факелов, пока звучит трек.  Мы уходили на смену сразу и
   обрывали мелодию на первых нотах.  Теперь ждём большего из двух:
   pop_music_busy() и счётчика pop_endmus_left по длине записи
   (gen/pop_music_ticks.h) — второе нужно потому, что при ВЫКЛЮЧЕННОЙ музыке
   busy ложен, а оригинал выдерживает паузу и молча.

Стартовый уровень возвращён на 1 (отладочный LEVEL=14 был только для замера).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:19:43 +03:00
snark13 4e43890fce SprPoP: финал больше не убивает программу — прямой вызов в чужой банк
Пройденная игра доходила до таблицы рекордов и умирала: программа
исчезала, машина следом вставала намертво (di;halt на 0x0000) либо уходила
в reset.  Одинаково из Flex Navigator и из голого DSS.

КОРЕНЬ.  pop_ui.h объявлял группу pop_text_*_mapped БЕЗ __banked.  Пока
pop_hof.c лежал в банке 9 рядом с pop_ui.c, прямой call был верен; после
переноса pop_hof/pop_config/pop_pal в банк 10 тот же call стал уходить в
пустой хвост чужого банка.  Процессор полз по 0xFF до 0x0000, где ловушка
DSS ставит B=0x27 и сворачивает процесс — подмена страниц W1/W2/W3,
которую было видно на трупе, оказалась уборкой, а не причиной.

Точную инструкцию (call $E503 = _pop_text_map банка 9) дала трассировка
MAME на узком участке: trace включалась брейкпоинтом на входе в
pop_hof_show и выключалась на процедуре завершения процесса DSS (0x1E56).

ЧТО СДЕЛАНО

* pop_ui.h/.c — группа text_*_mapped помечена __banked.
* toolchain/check_bank_calls.py — две проверки банкового кода:
  1) прямой call в чужой банк (доказательна, ВАЛИТ сборку — проверено
     намеренной поломкой);
  2) указатель на данные своего банка, отданный в чужой (эвристика по
     форме кода, только предупреждает).
  Встроена в app.mk, запускается сразу после линковки.
* pop_hof.c — курсор ввода строится на стеке: литерал "_" лежал в _BANK10
  и после пометки __banked уезжал из-под ног чужому банку, заливая экран
  знаками вопроса.
* libc: kbd_raw_keypad_as_ext() — kbd_raw_sync переносит голые коды
  нумпада в EXT-половину карты.  Лечит залипание стрелок (потерянный
  префикс E0 сажал make в PLAIN как код нумпада, и снять его было нечем),
  заодно нумпад стал управлением: 7/8/9, 4/6, 2 и 5 = вниз.
* pop_pace.c — цикл ожидания луча зовёт тот же idle-хук, что и
  gfx_wait_vsync: без этого F10 в геймплее не работал вовсе.
* pop_hof.c — Esc в таблице рекордов отменяет запись (расхождение с
  оригиналом записано в docs/impl_diff.md).
* Экран версии показывается только через Menu/Settings/About: стартовый
  показ и Ctrl+V убраны, мёртвый код снят.
* sprpop_cold.c — pop_start_level зовёт pop_hp_invalidate: после Ctrl+A с
  выросшим за уровень максимумом полоса HP моргала между страницами.

Разбор всех четырёх багов — в applications/PoP/roomtest/BUGS_CLOSED.md
(FINAL-BANKCALL, FINAL-HOF-GARBAGE, KBD-ARROW-PHANTOM, F10-GAMEPLAY),
правило про банки — в applications/SprPoP/CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:11:01 +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 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