0e74aaa7eee3ed3e59a6f27615ddf25cac5b68f3
23 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f206a4cca6 |
SprPoP: завести CHANGELOG, версия и дата — из git-тега
До сих пор «что изменилось» восстанавливалось только из git log, а он устроен по ПРИЧИНАМ правок, а не по тому, что заметит игрок. Заводим отдельный файл — как это уже сделано у mdview2 (changelog в репозитории ведётся на приложение, а корневой RELEASE_NOTES.md описывает тулчейн и живёт своей жизнью). Начат с v0.9.6: зацеп, зацикленный взмах клинка, кнопка в паласе. Более ранняя история остаётся в git log и досках — переписывать её задним числом смысла нет. ВЕРСИЯ И ДАТА БЕРУТСЯ ИЗ GIT, а не проставляются руками: заголовок записи = имя аннотированного тега, дата = дата его создания (`git for-each-ref refs/tags/<тег>`). Так запись не может разойтись с историей. Правило записано в CLAUDE.md рядом с дисциплиной досок, иначе файл заведётся один раз и умрёт. В запись идёт только то, что видно ИГРОКУ или меняет сборку/запуск, по схеме «симптом -> причина одной фразой -> хеш»; полный разбор остаётся в сообщении коммита и не дублируется. Файл в UTF-8, как остальные доки SprPoP. CP866 у mdview2 — вынужденная мера (его changelog читает сам MDView), здесь такого требования нет. Ссылка добавлена во входные точки docs/README.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1 |
||
|
|
400f5cba63 |
SprPoP: поправка в CLIMB-VS-GUARD — звук 11 значит ПРОМАХ
В записи стояло, будто оригинал играет звук 11 «сразу на кадре укола, ДО проверки расстояния», то есть при ЛЮБОМ уколе, попал тот или нет. Это было описание НАШЕГО кода, а не SDLPoP: ровно так звук у нас и стоял — внутри ветки «не парировано» и до присвоения Opp.action = 99_hurt (снято предыдущим коммитом). Классическое нарушение правила «источник истины — SDLPoP»: за оригинал приняли собственную реализацию, и вывод из неё уехал в доску как факт. На деле звук 11 в оригинале защищён условием Opp.action != 99_hurt, то есть звучит ТОЛЬКО на промахе: на попадании играет боль, на парировании Opp.frame уже 161. Что это меняет для самого бага: - УЦЕЛЕЛО наблюдение «в оригинале 11, у нас 8» — это данные. Арифметика приоритетов их подкрепляет: snd_prio[11] = 0x12 против snd_prio[8] = 0x4B (меньше значит важнее), взмах не мог быть заглушён упором в стену. - ОТПАЛ вывод «оригинал ведёт стража атакой с промахом, а мы — столкновением»: он опирался на неверную посылку. - УСИЛИЛАСЬ версия про ПОРОГ ДИСТАНЦИИ: раз в оригинале слышен именно 11, укол у стража СОСТОЯЛСЯ и ПРОМАЗАЛ. Прежняя «версия про столкновение» вытеснила дистанцию зря — возвращаем её в главные подозреваемые. Отдельно предупреждение тому, кто вернётся к багу: диагностика по звуку теперь значит другое, старые заметки прогона будут вводить в заблуждение, сцену надо переснимать. Положена таблица соответствий (11 = промах, 13 = попадание, 8 = столкновение, тишина = ранний выход из check_hurting). На механику урона правки не влияют — расхождение «удар убивает» остаётся открытым как было. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1 |
||
|
|
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 |
||
|
|
decbec79de |
SprPoP: три находки прогона 2026-08-31 — звук мигания, часы в быстрых режимах, полоса HP после рестарта
Разбор без правок кода; все три отмечены как задачи по решению пользователя. SND-SPEAKER-38 (TASKS_OPEN, P1). Надпись «Press Button» мигает молча. Наш код не виноват: pop_dead_prompt зовёт звук 38 на каждом появлении, как оригинал, но слот 38 в наборе ПУСТ. Причина — три параллельных набора звука у оригинала: оцифровка (0-23, 44-49, 51), мелодии (24-43 частично, 50, 52-56) и ноты PC-спикера (весь диапазон). Звук 38 есть ТОЛЬКО среди нот спикера, поэтому провалился между нашими конвейерами. Полная ревизия: не оцифровка и не мелодия — номера 31, 34, 38, 42, из них 31/34/42 пустые заглушки, реально звучит ровно один — 38. Решение выбрано пользователем: синтезировать ноты в PCM и класть в наш атлас; формат разобран по спецификации Princed и записан в задачу. TIME-SPEED (TASKS_OPEN, P1). Часы уменьшаются на каждом логическом кадре (как оригинал), но длину кадра у нас меняет режим скорости — в FAST минута проходит на треть быстрее. Разная длина кадра в игре и в бою есть и в оригинале (поправка пользователя), поэтому замедление часов в бою не трогаем; вопрос только в наших добавочных режимах. Два варианта с рекомендацией оставить как есть. HP-BAR-RESTART (BUGS_OPEN). После гибели и Ctrl+A (у нас это рестарт уровня) на одной из страниц остаётся полоса по результатам боя. Механизм перерисовки на месте — счётчик страниц, все холодные пути его взводят. Подозреваемый: пока висит статус-текст, стирание чистит только края и не трогает середину, а полоса стража при большом запасе HP заходит именно туда. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
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 |
||
|
|
4e12aa50d1 |
SprPoP: падение сквозь стену больше не проходит
Портировано опциональное исправление SDLPoP (fix_glide_through_wall, seg005 в do_fall). В ванили персонаж, падающий после разворота в беге, может оказаться внутри кладки и лететь «в стене» — баг оригинала, воспроизведённый пользователем в игре и затем на host-тесте. Решением 2026-08-31 фикс взят в ТЕКУЩИЙ билд: играбельность важнее буквальности. Реализация вынесена отдельной функцией glide_through_wall_guard() в pop_map.c намеренно — при разделении VANILLA/ENHANCED это готовая точка отвязки, достаточно не звать её в ванильном режиме. ПРОВЕРКА. Набор t_wall был заранее написан так, чтобы сторожить ЧИСЛО заходов в кладку: до фикса их было ровно два из четырнадцати стартовых позиций, после — ноль. Остальные 15 наборов (в том числе phys с 1733 проверками и grab) остались зелёными. Живая проверка в MAME пользователем: корректно. Ожидание в тесте обновлено ОСОЗНАННО, прежнее число сохранено рядом отдельной константой с пометкой «сколько было до фикса»: оно измерено, и понадобится, когда появится режим VANILLA — там ожидание станет зависеть от режима. ЦЕНА: +57 байт в банке 3 (свободно 3043), резидент и куча не изменились. По скорости попадание только на кадры падения: пересчёт колонки — одно деление, дистанция до кромки считается лишь если персонаж действительно внутри кладки. Документы: в аудите находка 21 переведена в «портировано» с сохранением исходного разбора; в vanilla_vs_bugfixed статус фикса стал ВЗЯТ, сводка пересчитана (5 взято, 32 кандидата в ENHANCED). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
c218e8b983 |
SprPoP: инвентаризация всех 43 фиксов SDLPoP со статусами
Документ vanilla_vs_bugfixed.md дополнен полным перечнем опциональных исправлений оригинала, какие есть в SDLPoP, с их статусом у нас и местом в коде, где решение принято. Расклад: 4 взяты (дверь выхода, звуки ворот, перо только для Кида, приоритеты звуков), 4 сознательно оставлены ванильными (падение на стража, прыжок через стража, трюк 35, кровь скелета), 1 в работе (падение сквозь стену), 33 не реализованы — кандидаты в ENHANCED. ГЛАВНОЕ СЛЕДСТВИЕ: наш билд — это не VANILLA, а «ваниль плюс четыре исправления». При разделении режимов придётся пройтись по уже сделанным отступлениям и распределить их, иначе текущее поведение нельзя считать эталоном ни для одного режима. ИСПРАВЛЕНА МОЯ НЕВЕРНАЯ ОЦЕНКА. Ранее было записано, что фиксы, требующие правки байткода seqtbl, у нас недоступны без переделки конвейера данных. Это неверно: байткод можно менять и у нас. Лучший способ — держать ОБЕ версии в одной странице EMM: kid.ani занимает около 4 КБ при странице в 16 КБ, так что обе помещаются рядом, а переключение режима сводится к смене базового смещения — без патчей и с мгновенным откатом. У SDLPoP, к слову, рабочая таблица и неизменная копия оригинала тоже существуют раздельно. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
a3aaa30e53 |
SprPoP: тесты смерти от меча (базовое поведение до правок 12/13)
КОД ИГРЫ НЕ МЕНЯЛСЯ. Новый набор t_death — 9 проверок, всего наборов 16. Заведён ПЕРЕД правкой находок 12/13, чтобы поймать деградацию: правка 13 в изоляции уже ломала смерть (мёртвый оставался с ненулевой скоростью падения, проваливался за нижнюю границу, игра уходила на рестарт, не показав тела). Поэтому проверяется ровно то, на что эти правки влияют: * удар не в боевой стойке смертелен независимо от запаса HP; * удар с мечом снимает одно HP, на последнем — убивает; * ПЕРЕЖИВШИЙ удар ставится на пол своего ряда с нулевой скоростью падения (в оригинале это единственная ветка, где координата трогается); * тело после смерти остаётся в своём ряду — и на ровном полу, и у самого обрыва (целевая сцена находки 12; после правки ожидание изменится осознанно). По дороге тест дважды показал не баг движка, а мои ошибки в самой сцене: обвязка выставляет признак «жив» только стражу, а урон применяется не сразу — удар выставляет дельту, и HP меняет отдельный шаг кадра, как в оригинале. Оба раза чинился тест. VANILLA/BUGFIXED: записано, что переключатель уже существует в настройках и зафиксирован в положении VANILLA, второй заводить не нужно. Отмечено главное следствие — наш «ванильный» билд УЖЕ не чистая ваниль (часть ванильных багов пофикшена), поэтому при разделении режимов придётся пройтись по сделанным отступлениям и распределить их; отдельные фиксы (падение сквозь стену) могут быть сделаны и в нынешнем билде. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
90e304f071 |
SprPoP: тесты стены + задача VANILLA/BUGFIXED, находка 24 отложена
КОД ИГРЫ НЕ МЕНЯЛСЯ — правка находки 24 сделана и откачена. ТЕСТЫ. Набор t_wall вырос с одной проверки до трёх: добавлены проверка X на каждом кадре падения (персонаж не должен оказываться внутри кладки) и симметричный прыжок через провал с другой стороны. Ключевое в них — форма ожидания. Тест НЕ требует нуля заходов в кладку, а сторожит их ЧИСЛО: сейчас ровно два случая из четырнадцати стартовых позиций. Это ванильное поведение оригинала, для которого SDLPoP держит отдельное опциональное исправление; больше двух — значит правка сделала нас хуже ванили, меньше — значит фикс кем-то портирован. То есть тест сразу готов обслуживать оба будущих режима. По дороге тест дважды ловил не баг движка, а мою ошибку в самой сцене (старт в пустой клетке; перелёт через площадку считался нарушением). Оба раза чинился тест, а не движок. НАХОДКА 24 ОТЛОЖЕНА. Перезагрузка кадра в in_wall верна по букве оригинала, но эффекта показать не удалось: все 15 наборов host-тестов дают одинаковый результат до и после. При этом правка не бесплатна — маппинг окна и перезагрузка кадра на каждое выталкивание. Платить за недоказанное не стали. НОВАЯ ЗАДАЧА: docs/vanilla_vs_bugfixed.md — поддержка двух поведений, ванильного и с багфиксами. Туда переехали находка 24, три опциональных фикса SDLPoP (скольжение сквозь стену, прыжок над воротами, гобелен) и готовый детектор из t_wall. Открытые вопросы записаны: чем переключать (возможно, объединить с уже существующим VANILLA/ENHANCED), цена рантайм-проверки в горячем пути, что считать умолчанием, как гонять тесты в двух режимах. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
b55d4d11e3 |
SprPoP: аудит — правки 12 и 13 неделимы (проверено на живой машине)
Правка 13 (перенос прижатия к полу в ветки пережитого удара, как в оригинале) была сделана в изоляции и СЛОМАЛА смерть: страж убивает Кида, а вместо тела и паузы идут вспышка, стопкадр и мгновенный выход в заставку. Причина: у нас прижатие к полу работало КОМПЕНСАЦИЕЙ отсутствующей ветки «убит и сброшен с уступа» (находка 12). Без неё мёртвый остаётся с ненулевой fall_y, физика ведёт его вниз, он пересекает нижнюю границу, взводится pop_fell_out — и приложение уходит на рестарт РАНЬШЕ отрисовки, поэтому тела не видно вовсе. Оценка «чистое перемещение двух строк, риск низкий» была неверной. В документе исправлено: риск ВЫСОКИЙ, пока ветка 12 отсутствует; обе находки — одна правка, и порядок внутри неё обратный: сперва добавить seq_81 с экспортом тайловых запросов, убедиться, что смерть на краю отыгрывается ею, и только потом снимать страховку. Правка откачена, дерево вернулось к проверенному состоянию. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
a5252b1c60 |
SprPoP: глубокое ревью находок А/Б — исправимость и цена по скорости
КОД НЕ МЕНЯЛСЯ. Разбор одиннадцати находок рангов А и Б: что менять, во что это обойдётся по скорости и памяти, каков риск. СНЯТО ГЛАВНОЕ ПРЕПЯТСТВИЕ. Обоснование двух упрощений (guards.c:1011 — «нужны тайловые запросы от Char, а pop_map умеет только от Kid») УСТАРЕЛО: get_tile_at_char, get_tile_infrontof_char, get_tile_behind_char и distance_to_edge_weight в pop_map.c уже работают от Char, они лишь не выведены в заголовок. Данные тоже на месте — pop_char_set_seq ставит любую из 115 последовательностей, то есть seq_81 и seq_64 доступны без единого нового байта. Три находки упираются не в архитектуру, а в четыре строки объявлений. СКОРОСТЬ. Места классифицированы по частоте вызова: play_seq и ИИ стража — горячие, land/in_wall/bumped/hurt_by_sword — событийные. Из одиннадцати правок две УСКОРЯЮТ код (уходит условие из горячего цикла; звук перестаёт играть в двух случаях из трёх), большинство бесплатны (перестановка строк), и ни одна не требует переделки архитектуры. Единственный конфликт со скоростью — отложенная побудка чомперов: play_seq маппит страницу байткода в W0 один раз перед циклом, и звать start_chompers внутри цикла значило бы снимать и возвращать окно на каждый переход ряда. Дешёвая замена: копить не один флаг, а битовую маску рядов и разбудить их после цикла — теряться ряды перестанут, цена в цикле нулевая. Для стражей аналогично: не межбанковый вызов wall_type, а копия таблицы в 32 байта в своём банке. Порядок работ — от «одна-две строки, низкий риск» (13, 24) к тем, где правка может компенсировать наши отличия в другом месте (1, 7). Политика: для критичных фиксов скорость не вето — такие выносятся в отдельный разбор с поиском дешёвого способа. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
3a0e847353 |
SprPoP: аудит расхождений с SDLPoP — Кид, стражи, seqtbl, отрисовка
КОД НЕ МЕНЯЛСЯ. Построчный разбор наших реализаций против оригинала: 26 позиций за пять проходов, каждая с рангом вероятности (А..Д), с описанием «чем грозит» и сценарием проверки. Расхождения группируются в три узла, и это главный вывод аудита: 1. СМЕРТЬ ПРИ АКТИВНОЙ ФИЗИКЕ — здесь все находки ранга А. У нас смерть это флаг, а физика продолжает вести персонажа как живого: нет ветки «убит и сброшен с уступа» (оригинал выбирает её по тайлу позади), прижатие к полу в hurt_by_sword стало безусловным (в оригинале только для выжившего удара), в land лишний пересчёт колонки. Этим объясняется наблюдение пользователя: заколотый на краю Кид доезжает этажом ниже и садится в присед. 2. ГРАНИЦЫ МОДУЛЕЙ — pop_map не отдаёт наружу тайловые запросы от произвольного Char, wall_type и загрузку кадра. Три ветки упрощены НЕ по логике, а по доступности функций: отсутствующая ветка уступа, «стена впереди» сужена у стражей до одного тайла (оригинал считает преградой ещё ворота, верх двери, зеркало и чомпер), in_wall не перезагружает кадр. Чинить это заплатками неправильно — сначала расширять интерфейс pop_map. 3. МОМЕНТ ПОБОЧНЫХ ДЕЙСТВИЙ — делаем то же самое, но раньше или позже: сброс fall_x, побудка чомперов (у нас отложена до конца play_seq), звук удара, перезагрузка кадра. По отдельности мелочь, вместе — сдвиг состояния на кадр. Восемь позиций СВЕРЕНЫ И СОВПАДАЮТ (диспетчер control, все 15 опкодов seqtbl, control_with_sword, parry, swordfight, sword_strike, check_sword_hurt, check_hurting, bumped_fall, таблицы кадров) — их не нужно перепроверять. Дважды по ходу работы едва не записана ложная находка из-за чтения отфильтрованного вывода; отсюда правило: фиксировать расхождение только после чтения обеих реализаций целиком. Отдельно: второе наблюдение пользователя (падение частично в стене) — у SDLPoP есть ТРИ опциональных фикса ровно про это, то есть в ванили баг присутствует, и мы его намеренно повторяем. Но найдены два места, где мы можем быть хуже ванили (гард curr_row<=2 в do_fall и in_wall выше). Незакрытое перечислено в файле: тела autocontrol_*, check_grab, check_bumped_look_left, старшие биты байта клинка. Также отмечено, что ни одно найденное осознанное отличие не занесено в docs/impl_diff.md, хотя правило проекта этого требует. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
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 |
||
|
|
2349481b86 |
SprPoP: звуковые эффекты без перелинковки — раскладка уехала на диск
Часть II плана music_runtime_index_plan.md (SI0..SI4). gen/pop_sound_tbl.h БОЛЬШЕ НЕ ГЕНЕРИРУЕТСЯ: раскладка набора читается из SND/snd.idx (формат PSI1, писатель и разборщик — tools/pop_idx.py, 22 теста в make test-tools). Один и тот же sprpop.exe работает с набором SDLPoP (9 страниц) и MSDOS (10) — sha256 бинарника при смене набора не меняется. Заодно умолчание источника эффектов переведено на SDLPoP (SND_SRC=sdlpop): сборка обязана работать без оригинального дистрибутива DOS. У кого он есть, включает лучший набор явно — make SND_SRC=msdos (там полнее оцифровка: в SDLPoP звук 48 spiked пустой). Устройство: pop_snd_tbl/pop_snd_page/pop_snd_pages — резидентные данные (pop_snd_data.c), тип и инварианты — рукописный pop_snd_tbl.h. Записи читаются ОДНИМ read прямо в таблицу, поэтому sizeof(pop_snd_ent_t) == 5 стало частью дискового контракта: проверяется статически и полем размера записи в заголовке. POP_SND_PAGES как compile-time размер набора исчез — вместо него POP_SND_MAX_PAGES (вместимость, 16) и runtime pop_snd_pages. Цена: таблица переехала из _CODE в _DATA, суммарный резидент почти не изменился (куча 239 -> 229 Б); банк 8 +601 Б на чтение и валидацию. Валидация не доверяет файлу: заголовок целиком плюс каждая запись (страница, смещение, кратность блоку, непересечение с блоком тишины, выход за последнюю страницу). Последнее считается В БЛОКАХ — байтовый адрес конца не влезает в uint16, а 32-битная арифметика на Z80 дорога. НЕТ ИНДЕКСА — ЭФФЕКТОВ НЕТ, НО МУЗЫКА ИГРАЕТ. Первая версия просто возвращала ошибку, и игра становилась непроходимой: тишину льёт первый блок набора, без набора CBL не открывался, а с ним вставала музыка (её блоки считает тот же насос) — заставка ждала конца трека вечно. Теперь поднимается пустой набор с блоком тишины. Заливается ровно 128 байт и под DI: gfx_w0_page_prepare ставит в страницу IRQ-стабы, и заливка всей страницы затирала их — первое же прерывание давало чёрный экран. Грабли сборки: смена SND_SRC тихо давала неверный результат (sdlpop -> msdos -> sdlpop оставлял чужой набор в assets/packed). Причина не в логике, а в секундной гранулярности mtime. Лечение убирает время из решения: смена варианта сносит stamp'ы своего семейства, а упаковка, сборка архива и копия индекса делаются одним рецептом. То же получила и музыка (MUSIC_FMT). Проверено в MAME: таблица в памяти совпадает с файлом из образа побайтово; один EXE поднимает оба набора; отладочный --order reverse (30 из 31 записей отличаются от штатных) звучит правильно; битый индекс выключает эффекты, не роняя игру; без индекса PV-сцена проходит с музыкой; Ctrl+S работает в обоих режимах. Разбор — docs/sound_plan.md §10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY |
||
|
|
ea8efdb0fd |
SprPoP: обобщить HDD-сборку и очистить метаданные
Добавить общий каталог назначения для HDD и удалить локальную копию упаковщика.\n\nУбрать устаревшие generated-имена ресурсов, выводить число страниц Kid из kid.arc и ограничить звуковую таблицу горячим модулем.\n\nЗафиксировать планы runtime-индексов музыки и PCM-эффектов. |
||
|
|
4ac3584bf9 |
SprPoP: идея «готовить следующий уровень под мелодию» — в бэклог, на дальнюю версию
Записана с оговорками, найденными при сегодняшнем разборе: загрузку придётся разрезать на дисковую и палитро-экранную половины, шаг подкачки держать полустраничным, проверить EMM-бюджет на два уровня разом. Половина идеи уже работает — трек заставки играет поверх загрузки (порядок оригинала, замер насоса приложен в записи). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
770b946a36 |
SprPoP: доски — SND-PACE-DEAD снят, PV-RENDER-BOUND исправлен
Обе записи закрыты сегодняшними правками: вторая шкала по насосу удалена вместе с гонкой, которая её выбирала, а «сцена дороже бюджета» оказалась не ценой кадра, а местом отсчёта интервала. Исходные разборы оставлены под заголовками — они объясняют, как искали. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6dabe9b4b1 |
libc: пока raw-клавиатура открыта, не звать обработчик DSS — он крал наши скан-коды
Корень двух багов SprPoP (KBD-STUCK-WAIT, частично GRAB-KBD-TIMING) нашёлся в исходниках DSS (docs/sources/Estex-DSS): обработчик прерывания DSS живёт в IM1 по 0x0038 и ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`, а тот вычерпывает FIFO SIO досуха. Наш трамплин проверял «есть ли клавиатурный байт» один раз, на входе в прерывание, а хвост кадрового пути уходил в DSS — значит скан-код, прилетевший позже, доставался DSS и уезжал в его буфер. Rx-overrun при этом НЕ взводится (байт не потерян железом, а прочитан не тем владельцем) — отсюда и загадка исходного диагноза: бит залип при `_kbdraw_overrun == 0`. Измерено в MAME (брейки + totalcycles): окно 738 тактов (~34 мкс) на каждом кадровом прерывании, из них 481 такт — пролог самого DSS. Поэтому проверка FIFO перед chain'ом снимает лишь треть и не годится (пробовали, кражи продолжались); кадровый путь при открытом raw теперь заканчивается приватным RETI, окно = 0. Цена: на это время у DSS замирает опрос мыши и мигание текстового курсора — зафиксировано в <kbd_raw.h>. Пойманный случай (старая сборка): DSS прочитал 0x74 (make стрелки «вправо») при _kbdraw_pending = EXT, то есть посылку E0 74 разорвало пополам между двумя владельцами канала. Проверка: брейк на входе KEYSCAN с условием «страница точно DSS + raw открыт» до правки срабатывал мгновенно (50/с), после — молчит; положительный контроль на нашем RETI срабатывает сразу. Трамплин 300 -> 310 Б (буфер W2-копии поднят 336 -> 384, запас 74 Б); размерный эталон обновлён: +10 Б у программ, линкующих IRQ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |