Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.
Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
Проверка 2 искала форму «адрес метки своего банка положен в аргументы вызова в
чужой банк», но регулярка принимала только `ld hl,#_метка` и `#_метка+6`. Адрес
поля структуры SDCC пишет в скобках:
ld bc, #(_mstream + 6)
push bc
...
call ___sdcc_bcall_ehl
Из-за этого целый класс ошибок собирался «чисто». Поймано вживую 2026-09-03 в
порту Loom: mask.c (банк 5, --bank-data 5) отдавал res_copy (банк 11) адрес поля
своей структуры — поток маски читался в чужую страницу, маска выходила «всё
заполнено», тайлов в комнате стало 30 вместо 17. Фон при этом оставался
побайтово верным, потому что его путь идёт из резидентного кода, так что
попиксельная сверка экрана молчала.
Класс проверки не меняется: адрес метки ДАННЫХ своего банка, положенный в стек
как аргумент вызова в ЧУЖОЙ банк, неверен всегда — это по-прежнему
доказательный случай, а не эвристика.
Проверено в трёх режимах: с ошибкой срабатывает, на исправленном коде молчит, и
на всех банковых сборках самого тулчейна (banktest, w3probe, banked, bankedbg,
banklocl, w3bankgfx, roomtest, сборка SprPoP) ложных срабатываний нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxu9rwsmyDyYPxG198nzSR
Заголовок записи стал `<имя тега> — <дата> — <коммит>`.
Зачем коммит, если есть имя тега: тег можно передвинуть или
переименовать, а ревизия, на которой релиз собран, должна оставаться в
записи однозначно — по ней билд воспроизводится точно. Заодно это тот же
хеш, что зашит в EXE как BUILD_ID и виден на экране About, так что
принесённый пользователем скриншот сразу сопоставляется с записью
changelog.
Берётся именно `*objectname` (дереференс аннотированного тега), а не
`objectname`: последний вернул бы хеш самого объекта тега, а не коммита.
Команда целиком выписана и в шапке CHANGELOG, и в правиле в CLAUDE.md,
чтобы не восстанавливать её каждый раз.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
До сих пор «что изменилось» восстанавливалось только из 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
С обобщения HDD-сборки (ea8efdb) один образ рассчитан на несколько
приложений, и EXE переехал в подкаталог. В инструкции по запуску это не
отразили, поэтому старая последовательность (`d:` + `sprpop`) отвечает
`Bad command or file name`, а `dir D:\` показывает только каталог GAMES —
на это можно потратить время впустую.
Записаны и сам путь, и исправленная цепочка клавиш для моста.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
Симптом (пользователь, 2026-09-02, уровень 4 комната 18): Кид
подтягивается на открывающую кнопку (0,4), и кромка, которая его
прячет, нарисована кирпичом вместо кнопки.
РАЗБОР ПО ЖИВОМУ КАДРУ (заморозка): Kid frame 139, x 123, col 4, row 1,
room 18 — то есть персонаж действительно под кромкой кнопки (0,4).
Отсюда путь: кадр подъёма -> climb_overlay_tile; тайл не floor-подобный
(кнопка) -> other_overlay_tile; сосед слева (0,3) пуст -> overlay_mid_tile,
полная перерисовка тайла ПОВЕРХ персонажа.
ПРИЧИНА. draw_tile_base оригинала подменяет базу кнопки на «левую
половину без пола слева» (id 148) по ТРЁМ условиям (seg008:628): тайл —
opener, слева пусто И tbl_level_type[current_level] == 0, то есть только
в ПОДЗЕМЕЛЬЕ. Третье условие у нас было потеряно ИМЕННО В ОВЕРЛЕЕ:
статическая отрисовка (pop_room.c, draw_tile_base) его имеет и даже
ссылается на ту же строку оригинала, а pop_bg.c — нет. Уровень 4
дворцовый, поэтому оверлей брал спрайт 148, а в дворцовом наборе это
другая картинка.
Этим же объясняется, почему баг не виден «просто так»: пока тайл рисует
статика, кнопка правильная — кирпич появляется ровно в тот момент, когда
персонаж встаёт под кромку и включается оверлей.
Цена: +6 байт в банке 2 (8976 -> 8982, свободно 7402).
Проверять стоит на любом ДВОРЦОВОМ уровне, где у открывающей кнопки
слева пусто и персонаж лезет на неё снизу. Живая проверка в MAME
пользователем: корректно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
В записи стояло, будто оригинал играет звук 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
Симптом (пользователь, 2026-09-02): на подъёме Кида шёл нескончаемый
свист клинка, хотя меча у него нет.
ОПОЗНАНИЕ. Звук снят с живой машины, а не угадан: snd_curr == 11
(sound_11_sword_moving). Заодно проверено, что тракт исправен —
указатели насоса (pg 2, ptr 0x2B80, left 512) сошлись с записью 11 в
SND/snd.idx (страница 2, off 0x2880, длина 1280, конец 0x2D80) байт в
байт. То есть играл честно заявленный эффект из своих данных, и виновата
была ЗАЯВКА, а не звук.
ПРИЧИНА. Хвост check_hurting был портирован не до конца. В оригинале
(seg002:1039..1044) звук 11 стоит В КОНЦЕ функции, после ОБЕИХ веток, и
защищён тремя вещами: ранним выходом по dir_56_none, кадром 154 и
условием Opp.action != actions_99_hurt. У нас он стоял ВНУТРИ ветки «не
парировано», до присвоения Opp.action = 99_hurt, без проверки на
попадание и — главное — без гарда по dir_56_none, который автор SDLPoP
подписал прямым текстом: «Fix looping sword moving sound». Направление
dir_56_none означает «персонаж выключен» (clear_char), махать ему нечем.
Наш движок эту константу знает и применяет в pop_guard_tick — в
check_hurting она просто не доехала.
ТОНКОСТЬ ПОРТА: хвост оригинала ПЕРЕЧИТЫВАЕТ Char.frame и Opp.frame у
персонажей, а не берёт снимки начала функции. Ветка парирования только
что записала Opp.frame, а play_seq в ней — Char.frame; на кэшированных
cf/of условие дало бы неверный ответ.
Живая проверка в MAME пользователем: корректно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
Регресс от 4e12aa5: glide_through_wall_guard() звался в do_fall сразу
после check_grab и отменял ТОЛЬКО ЧТО СОСТОЯВШИЙСЯ зацеп.
Причина в имени последовательности: seq_15 — это grab_ledge_MIDAIR, и
после удачного захвата Char.action == 3, то есть персонаж формально всё
ещё «в воздухе», уже подтянутый вплотную к кромке. А под кромкой в
комнатах оригинала стоит кладка. Guard видел ровно её (замерено на живой
сцене: t == TILE_WALL, d == 10, col 0, row 1), считал это пролётом сквозь
стену, отбрасывал персонажа на 5 пикселей назад и гасил fall_x. Зацеп
РИСОВАЛСЯ и тут же срывался — ловилось на длинном прыжке уровня 3
(комната 7) и в attract-демо (комната 2, прыжок с места на кромку 0,2,
после срыва Кид падал на пики).
ФИКС. check_grab() возвращает признак «зацепился», и при нём guard не
зовётся. Цена — один тест байта на кадр падения; фикс «падение сквозь
стену» цел, t_wall зелёный.
ПОЧЕМУ ПРЕЖНИЕ НАБОРЫ ЭТОГО НЕ ПОЙМАЛИ — и главный урок. t_grab, t_phys
(1733 проверки) и t_wall судят по «действие стало вис», а вис-то
наступал, он просто не жил. Первый A/B guard'а по этой же метрике дал
ЛОЖНО-ОТРИЦАТЕЛЬНЫЙ ответ, и подозрение с него было снято зря. Разница
между «зацепился» и «зацепился и держится» — это и есть разница между
багом и нормой.
Отсюда новый набор t_hang: критерий — вис ДЕРЖИТСЯ три кадра подряд, и в
отчёте различаются «не наступило» и «наступило и сорвалось». Окно — 34
фазы разбега из 41; при намеренно возвращённой поломке 0 из 41, тест
краснеет (проверено). Сцена — геометрия уровня 3 комнаты 7, приведённая
внутрь одной комнаты, чтобы шов не примешивался; кромка-пол и
кромка-решётка проверяются отдельно.
Живая проверка в MAME пользователем: корректно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
atlas значился 9193 Б против 9627 фактических, и расхождение держалось
через все правки этой сессии: замерял со СПРЯТАННЫМИ правками — те же
9627, то есть к ним оно отношения не имеет.
Похоже на механику самого инструмента: size_check.py читает .map С ДИСКА,
а не пересобирает. Программа, которую в тот раз не пересобрали, попадает
в эталон со СТАРЫМ числом — и дальше висит расхождением, пока её однажды
не соберут заново. Сегодня после make clean впервые за долгое время
пересобралось всё, и накопленный рост libc/libbgi стал видимым разом.
Записываю честное число. Полезный вывод на будущее: `make size-baseline`
имеет смысл только после полной пересборки тестов, иначе он фиксирует
смесь свежих и устаревших размеров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Выхлоп tools/pop_quiet.py на текущих наборах: те же snd.arc и m*.bin, но
вдвое тише (out = in/2 + 0x40, тишина 0x80 остаётся на месте), плюс
неизменённые индексы. Раскладка совпадает с диском игры.
Это ПРОИЗВОДНЫЙ артефакт: восстанавливается из assets/packed одной
командой `python3 tools/pop_quiet.py` за секунды. Лежит в репозитории по
той же логике, что и assets/packed — чтобы вариант был под рукой без
пересборки; если решим, что 3,6 МБ того не стоят, снимается одним
git rm --cached плюс строка в .gitignore.
На образ игры dist/ автоматически не попадает: make hdd берёт assets/packed
через build/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Правки пользователя, разобранные по диффу и закоммиченные как есть.
Makefile: умолчание --max-allocs поднято с 6000 до 10000 — плотнее
упаковка регистров. Напоминание из шапки остаётся в силе: занятость
банков сравнима только при ОДНОМ значении ALLOCS, иначе сравниваются не
правки, а уровни оптимизации.
pop_map.c: банковая обёртка pop_dist_to_edge_weight() переехала НИЖЕ
статического distance_to_edge_weight(), который она зовёт, — раньше стояла
до его определения.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Правка пользователя, разобранная по диффу и закоммиченная как есть.
Было «всё или ничего»: --bank-data уводил писучие данные В СТРАНИЦУ для
ВСЕХ банков сразу. Теперь флаг принимает необязательный номер и
повторяется: --bank-data 5 --bank-data 6. Без аргумента поведение прежнее
(все банки), поэтому существующие сборки не меняются.
Зачем поштучно: данные в странице банка НЕ ВИДНЫ снаружи, поэтому модуль с
экспортируемым глобалом обязан остаться на общем _DATA в W1/W2 — а его
сосед в это же время может держать большой приватный буфер вне резидентного
бюджета. Одним флагом на всю программу это не выражается.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Правка пользователя, разобранная по диффу и закоммиченная как есть.
Программа может временно маппить свою EMM-страницу в окно 0 (атласы
спрайтов, gfx_w0_map). Пока страница там, любое прерывание уходит на
#0038 ЭТОЙ страницы, поэтому gfx_w0_page_prepare прошивает туда переход на
_gfx_w0_isr. Сам стаб живёт в _CODE, а в режимах small и huge _CODE
начинается с 0x4100 — то есть попадает в W1, окно, которое DSS перемаплет
на время СВОИХ вызовов. Прерывание в этот момент уходит по адресу,
которого сейчас нет: W1 читается как #FF.
Ловится тяжело: собирается молча, проявляется недетерминированно —
зависанием примерно на каждом третьем холодном старте. Поймано вживую
2026-09-01.
check_w0_isr.py берёт адрес __gfx_w0_isr из .map: символа нет — W0-путь не
слинкован, молчим; адрес >= 0x8000 — он в W2, всё хорошо; иначе WARNING.
Сборку не валит намеренно: свои страницы в W0 кладут не все. Вызов
добавлен в app.mk рядом с check_bank_calls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Берёт то, что уже лежит в assets/packed, делает тише и кладёт в dist/ в
раскладке диска, перезаписывая прежнее. Оригиналы не трогаются, поэтому
скрипт идемпотентен: сколько раз ни запусти, громкость упадёт один раз.
Формула — та, что предложил пользователь: PCM у нас 8 бит без знака с
тишиной 0x80, значит вдвое тише это out = in/2 + 0x40 (беззнаковый сдвиг).
Таблица на 256 значений строится floor-делением отклонения от центра, что
при gain = 1/2 совпадает с этой формулой байт в байт — то есть результат
можно сверять с реализацией на Z80 (srl a / add a,#0x40). Тишина остаётся
тишиной при любом gain, клиппинга нет по построению.
Пересчитываются: MUS/m*.bin целиком (чистый PCM) и ТОЛЬКО тела записей
PBA1 в SND/snd.arc — заголовок и выравнивающие хвосты остаются как есть,
там нули упаковщика, и пересчёт превратил бы их в 0x40. Индексы
копируются без изменений (длины те же), но с проверкой магии.
Прогон на текущих наборах: 10 записей архива и 22 трека, пик 128 -> 64,
размеры байт в байт прежние, заголовок PBA1 не тронут.
Тест закрепляет формулу и неприкосновенность служебных байт; make
test-tools — 45 тестов. Раскладка dist/ добавлена в tools/paths.py, по
правилу «пути знает один файл».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Два бага одного пути завершения, оба видны только на железе.
1. ЗВУК ПРОДОЛЖАЛСЯ ПОСЛЕ ВЫХОДА. cbl_close() закрывает СЕССИЮ, но не
гасит железо: bit7 порта 0x004E держит gfx_wait_vsync ради бита луча,
поэтому порт оставался включённым ("немой" режим), и CBL крутил свои 256
слотов уже под шеллом — тихо ровно до первой чужой записи в порт данных,
а дальше она зацикливалась. Новый cbl_shutdown() гасит bit7 независимо
от держателей и центрует ЦАП обычного COVOX; pop_shutdown зовёт его
последним действием, а _cbl_open_raw регистрирует в atexit его, а не
cbl_close.
2. ЦЕПОЧКА atexit НЕ ВЫПОЛНЯЛАСЬ ПРИ ВОЗВРАТЕ ИЗ main. crt0 уходил прямо
в ESTEX EXIT, то есть нарушал контракт C (возврат из main = exit(status)).
Молча терялись не только гашение звука и снятие vsync-ссылки, но и
_fclosall: буферизованная запись в файлы пропадала, если программа не
звала exit() явно. Теперь crt0 после main дёргает _atexit_hook.
Косвенность обязательна: прямая ссылка crt0 на разматыватель притащила бы
его и стек хендлеров в КАЖДУЮ программу. Указатель живёт в отдельном
data-модуле (два байта _DATA, ни байта кода), ставит его сам atexit() при
первой регистрации — нет регистраций, нет и кода. Тот же приём, что у
_irq_cbl_hook.
Цена замерена: +14 Б всем программам (блок в crt0) и +64 Б тем
одиннадцати, что реально регистрируют хендлеры (CBL, файловые через
_fclosall, irqtest, gfx_dbuf, solidt) — у них раньше эти хендлеры были
мёртвым кодом. Эталоны обновлены (кроме atlas: его +434 Б не отсюда,
замерен тот же и без этих правок).
Проверено в MAME: старт и звук как были, выход по F10 возвращает в шелл
чисто (текстовый режим восстановлен, зависания нет), повторный запуск
работает. Пункт 1 проверяется только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
make SND_SRC=msdos MUSIC_FMT=flac resources-rebuild — полная перегенерация
всех ресурсов. Изменился только SND/: у MSDOS-набора оцифровка полнее,
чем у SDLPoP (там пуст звук 48 spiked и короче 10 sword_vs_sword — разбор
в docs/sound_plan.md).
Музыка и остальные архивы после перегенерации совпали с закоммиченными
байт в байт — набор flac и есть тот, из которого они сделаны.
Умолчание сборки НЕ меняется: SND_SRC ?= sdlpop, чтобы клон без
оригинального дистрибутива DOS собирался целиком (make fetch). Вернуть
прежний набор — make resources-sound после make clean stamps, или
make SND_SRC=sdlpop -B resources-sound.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
pop_boot_sound() был целиком под разовым флагом, а pop_sfx_start() стоял
внутри него. До перестановки старта CBL открывал сам шаг 0 титров, и
делал это КАЖДЫЙ раз; теперь второй вызов уходил по раннему return, а
pop_title_show начинается с pop_sfx_pause() — значит после Restart Game
насос оставался выключенным и заставка шла молча.
Тем же корнем объясняется зависание: с закрытым CBL курсор трека не
двигается, pop_mus_left не убывает, и ожидания «дослушать тему» в титрах
и интро (while (pop_music_busy())) висели до нажатия клавиши — на экране
Prince of Persia.
- pop_boot_sound: под разовым флагом осталась только ЗАГРУЗКА (индекс
звука, первая страница набора, индекс музыки, settings_apply);
pop_sfx_start() зовётся всегда — он идемпотентен и уважает Ctrl+S;
- pop_music_busy(): признак теперь «звучит», а не «есть курсор» —
спрашивает и про открытый вывод (pop_snd_ok). Ждать неиграющую музыку
нельзя в принципе, и эта строка закрывает весь класс подвисаний.
Проверено в MAME: уровень 1 -> ESC -> RESTART GAME -> титул с музыкой
(pop_snd_ok=1, курсор трека прошёл страницы 8..16, pop_mus_id=54), дальше
последовательность сама уходит в сцену с принцессой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
От запуска до проявления титула проходило ~3 с чёрного экрана в тишине:
pop_boot читал ВСЕ ресурсы игры до первого пикселя, а pop_title_show перед
fade_in вычитывал целиком трек заставки (248 КБ).
Старт разбит на три фазы (sprpop_cold.c):
- pop_boot() — настройки, POP.CFG, графика, чёрный экран, FONT.ATL;
- pop_boot_sound() — snd.idx + ОДНА страница snd.arc, индекс музыки, CBL;
- pop_boot_rest() — остальные страницы звука, kid.ani, Тень, атласы Кида;
идемпотентно, между кусками доливает трек.
pop_title_show теперь: fade_in -> pop_boot_sound -> pop_music_stream(54) ->
pop_boot_rest под стоящим титулом. Порядок «сначала fade_in, потом тема»
сохранён как у show_title (seg000.c:1981). Первый такт заставки укорочен
на фактически потраченное время (часы насоса 85,4 Гц -> кадры луча
сдвигами), иначе сцена уехала бы относительно музыки. title_wait доливает
трек по полстраницы за кадр.
Музыка играет с ПЕРВОЙ страницы: pop_music_stream уже был (игровые
джинглы), кольцо won не понадобилось — m54 это 16 страниц из 20 доступных.
Звук поднимается в два приёма (pop_sfx_init_begin/finish): насосу для
тишины нужен ровно один блок, и упаковщик обязан класть его первым блоком
страницы 0 (tools/pop_idx.py) — значит для открытия CBL хватает ОДНОЙ
страницы (33 мс вместо 440). Пока набор неполон, pop_sfx_play пропускает
эффекты; заявки на музыку проходят.
Четыре загрузки из boot УБРАНЫ, а не отложены: атлас фона, страж, уровень 1
и pop_pal_game_load — их и так делает pop_level_switch на входе в любой
уровень (а pop_new_game_load ещё и палитру перед ним), и оба маршрута в
игру, LEVEL_LOAD и DEMO, упираются в него. Это ~0,77 с на ровном месте.
Так же устроен оригинал: init_game_main до заставки читает только меч,
пламя, звуки и палитры, а chtab_5/6/7 грузит load_lev_spr на старте уровня.
Страховка на случай пропуска заставки в первую секунду — идемпотентный
pop_boot_rest() в начале pop_new_game_load и pop_level_switch.
FONT.ATL остаётся в минимальной фазе, и это не про шрифт: снимок палитры
физически лежит в хвосте его страницы (pop_ui.c), а вся машинерия яркости
начинается с `if (!font_ready) return`. С отложенным шрифтом заставка
возникала разом на полной яркости — поймано пользователем.
Проверено в MAME: титул виден сразу и проявляется полосами, pop_mus_left
убывает (трек стримится), pop_snd_pages=10 (набор дочитался), цепочка
титул -> интро -> пропуск -> уровень 1 работает. Банк 8: 95,5 % (732 Б).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
cbl_close() не пишет в порт управления ноль: пока gfx_wait_vsync держит
ссылку ради бита луча (в PoP её берёт pop_pace_arm на всю программу),
bit7 остаётся включённым. А буфер CBL (256 слотов) при включённом bit7
крутится по кругу и не чистится ничем — железо бесконечно поёт хвост
последнего сэмпла, тоном и громкостью по последней мелодии. Пойман
пользователем на железе: Ctrl+S во время музыки и пропуск заставки
давали ноту до следующего cbl_open.
В MAME не воспроизводится: "немым" кодом частоты был reserved-код 2, у
которого divs[2]==0 и таймер не заводится вовсе. На железе reserved-коды
не определены, ЦАП тактируется — тишина держалась на свойстве эмулятора,
а не железа.
Теперь тишину даёт СОДЕРЖИМОЕ БУФЕРА:
- _cbl_port_sync() после каждой записи в порт управления зовёт
_cbl_prime(0x80). Это закрывает и паузу звука, и включение bit7 ради
луча на холодном старте (в буфере лежал мусор от прошлой программы), и
полное выключение — при bit7=0 те же 256 записей уходят прямо в ЦАП
обычного COVOX и центруют его, снимая щелчок;
- _CBL_VSYNC_FREQ переведён с reserved-кода 2 на документированный 8
(7,8125 кГц): поведение определено и на железе, и в MAME, прерывания
по-прежнему выключены (bit4=0), а бит 7 порта 0xFE трамплин смотрит
только при живом хуке насоса;
- cbl_close() зовёт sync внутри той же DI-скобки, где снимает хук, иначе
насос долил бы буфер уже после заливки.
Цена: +2 Б программам со звуком, +11 Б графическим (тянется _cbl_prime
следом за gfx_wait_vsync), 256 OUT'ов (~0,3 мс) на редкое событие —
эталоны cblstream/cbltest/cblwav/gfx_dbuf обновлены. Рост atlas в
size-check к этой правке отношения не имеет (замерен тот же и без неё).
Проверено: кодоген _cbl_port.asm; SprPoP пересобран и прогнан в MAME
(титры → пропуск заставки → уровень 1, пейсинг по лучу жив). Сам баг
проверяется только на железе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
Четыре правки прогона 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
Разбор без правок кода; все три отмечены как задачи по решению пользователя.
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
Два независимых фикса, оба проверены в 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
Портировано опциональное исправление 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
Документ 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
КОД ИГРЫ НЕ МЕНЯЛСЯ. Новый набор 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
КОД ИГРЫ НЕ МЕНЯЛСЯ — правка находки 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
Правка 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
tests/host не линковались: не разрешались pop_chdir_home (его тянет
pop_kboot из банка 10) и kbd_raw_keypad_as_ext (тянет pop_ctrl из libc).
Поломка предсуществующая — воспроизводится и на коммите до всех правок
этой сессии. Ни файловой системы, ни клавиатуры в хостовых тестах нет,
поэтому обе заглушены пустышками в общей обвязке.
Теперь все пятнадцать наборов проходят: geom 3144 проверки, phys 1733,
char 72, grab 55, jaffar 44, shadow 45, app 58, cfg 51, demo 30, flow 27,
timer 28, cutscene 13, gate 10, mouse 17, wall 1.
Это условие для дальнейшей работы: логику движка снова можно проверять за
секунды, без сборки образа и ручного прохождения в MAME.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY