Строки дорисовывались уже после полосового перехода: он копирует страницу акселератором, а тот читает ОЗУ-копию, куда GFX_BANK_SPRITE (NOSHADOW + TRANSPARENT) не пишет. Банк GFX_BANK_TRANSPARENT даёт ту же прозрачность 0xFF, но обновляет и копию, поэтому страницу можно собрать целиком до показа — как offscreen оригинала (draw_full_image + show_hof, и только потом transition_ltr). Проверено в MAME: на промежуточных кадрах перехода строка рекорда видна в уже проявившейся части экрана вместе с логотипом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 KiB
roomtest — ЗАКРЫТЫЕ задачи (архив досок, обновлено 2026-08-11)
Сделанное — с протоколами замеров, граблями и причинами решений. Файл
существует не ради истории: половина записей ниже — это ЧИСЛА (сколько тактов
стоил heal, сколько байт теряет клавиатура, почему static inline дорог) и
перечень того, что делать НЕЛЬЗЯ, потому что уже пробовали.
Открытые задачи — TASKS_OPEN.md; открытые баги —
BUGS_OPEN.md, закрытые с разбором корней —
BUGS_CLOSED.md.
QuickSave
QSAVE. QuickSave / QuickLoad — РЕАЛИЗОВАНО И ПРОВЕРЕНО 2026-08-22 (коммит f4b4852, тэг v0.6-pop-quicksave)
F6/F9 в roomtest. Формат снимка 'POPQ' v3: заголовок
{P,O,P,Q,ver=3,level,reserved}, payload всех игровых переменных (Kid/Guard
через qs_char, HP, fight seed, читы speed/immortal/sound с range-валидацией
на загрузке), хвост = размер payload + XOR-контрольная сумма (старший байт =
инвертированный XOR). Носитель — файл на HDD: транзакционная запись
unlink POP.NEW → bank_save_file → rename SAV→BAK → rename NEW→SAV
с откатом; загрузка пробует POP.SAV, потом POP.BAK; при другом уровне —
pop_next_level+pop_level_switch. Сериализация через резидентные W0-примитивы
pop_qs_* (паттерн request-flag + pop_qsave_process() на границе кадра вне
Char-окон); сериализаторы добавлены в pop_map/pop_loose_mob/pop_trob/
pop_guard_ai с восстановлением инвариантов (mobs_live, trob_drawn, redraw).
Дабл-буфер закрыт pop_qsave_restore_room(): полная перезагрузка комнаты +
перерисовка ОБЕИХ страниц + инвалидация кэшей спрайтов и HP. Три ГСЧ
(pop_t_seed, trob_seed, pop_fight_seed) — все в payload.
Файловое I/O — новые bank_load_file/bank_save_file libc (без правила W3).
Критерий приёмки выполнен: сохранить, выйти по ESC, запустить заново,
загрузить — оказались там же; проверено в MAME. Полный разбор и формат —
../docs/quicksave_plan.md (теперь справочник).
Уровни: спецсобытия
L8-MOUSE. Мышь уровня 8 — ЗАКРЫТА 2026-08-12
Спецсобытие комнаты 16: кнопка (0,7) открывает решётку (0,3), но с левой
площадки верхнего ряда до кнопки не дойти — между ними провал в три колонки.
Когда дверь уровня уже открыта, а Кид всё ещё в комнате 16, оригинал через
150 кадров присылает мышь: она пробегает справа налево, наступает на кнопку и
убегает. Порт по SDLPoP: do_timers seg003:0545, do_mouse seg003:0ABA,
autocontrol_mouse seg002:07EB.
Ассетов и отрисовки не потребовалось вовсе. Кадры 186..188 лежат в
таблице КИДА (load_frame seg006:0293 — общий case), их images 130..132 уже
упакованы в kid16.atl (idx 2/3/4: 17×4, 19×4, 12×10) — pop_pack_kid.py
берёт все res(401+N).png, что есть в data/KID. Отрисовка подхватывает
мышь сама: pop_frame_tbl_is_guard(24, …) отдаёт 0, значит и таблица кадров,
и набор атласов берутся кидовские, и палитра (0x70) тоже.
Что написано:
| где | что |
|---|---|
guards.c |
pop_check_mouse (условие + рождение), autocontrol_mouse, ветки мыши в autocontrol_opponent / play_guard / pop_check_can_guard_see_kid |
pop_state.c/h |
pop_leveldoor_open стал word, как в оригинале (data.h:361): в него же считается задержка, байт переполнился бы через 255 кадров и обнулил признак «дверь открыта» |
pop_guard.c |
leave_guard не записывает в данные уровня тень и мышь (seg002:02F5) — раньше эта проверка отсутствовала вовсе |
pop_cdraw.c |
полосы HP у мыши нет (draw_guard_hp, seg000:1159) |
pop_tune.h |
числа события (уровень/комната/задержка/пороги X) |
Правило появления — мышь ОДНОРАЗОВАЯ (сверено с оригиналом). Счётчик
живёт в leveldoor_open: 0 на старте уровня (seg003:97), 1 — когда створка
двери уровня доехала доверху (seg007:456, то есть Кид нажал кнопку в комнате
3), дальше ++ каждый кадр, пока Кид в комнате 16, и вызов ровно при
== 150. Отсюда три следствия:
- второго раза НЕТ: счётчик растёт дальше, а сравнение строгое. Не успел пройти решётку, пока она открыта — мышь больше не придёт;
- выход из комнаты 16 счётчик не сбрасывает, а ЗАМОРАЖИВАЕТ: ушёл на сотом кадре, вернулся — ждать осталось пятьдесят;
- обнуляет только
start_level, то есть смерть/рестарт уровня — но он же возвращает тайлы и модификаторы, так что дверь придётся открывать заново.
Значения 0x4D (музыка уровней 4 и 6) и 2 (победа над Джаффаром, уровень
13) в ту же переменную к уровню 8 отношения не имеют — там счёт идёт
монотонно 1 → 150.
Чего в условии НЕТ (проверено грепом по всем упоминаниям mouse_level /
mouse_room / do_mouse — единственная точка запуска это seg003:545):
оригинал не смотрит ни на состояние решётки (0,3), ни на то, с какой стороны
провала стоит Кид, ни на то, прошёл ли он её уже. «Кид заперт за закрытой
решёткой» получается САМО: находиться в комнате 16 двенадцать с половиной
секунд можно только топчась там, а стоит уйти в комнату 4 — счётчик
замирает. Обратная сторона той же простоты: вернувшись в комнату 16 с ЛЮБОЙ
стороны, игрок доигрывает накопленное ожидание и может получить мышь, даже
если решётка в этот момент открыта.
Тупика при этом нет: провал в колонках 4..6 ведёт на нижний ряд комнаты
(debris между колоннами, а колонна — проходимый пол, seg006:0628), оттуда
низом через комнату 4 с шипами есть путь к двери уровня в комнате 3. Цена —
падение на два ряда, то есть −1 HP. Мышь экономит эту единицу, а не спасает
от тупика.
Тонкость, на которой легко сломать уход: seq_107 прыгает на метку
Mscurry1, стоящую ПОСЛЕ опкода act(actions_1_run_jump) в seq_105 — на
обратном пути кадры бега проигрываются, а action остаётся стоечным. Именно
этим оригинал отличает «бежит к кнопке» от «убегает»: условие исчезновения
висит на action == 0.
Проверено. Хост-набор t_mouse (17 проверок, единственный, куда
линкуется guards.c) — рождение по таймеру, замороженный счётчик вне комнаты,
требование открытой двери, нажатие кнопки на пробеге, разворот и исчезновение,
невидимость для Кида, необращение в «стража комнаты». Регресс проверен
откатом: без ветки в autocontrol_opponent падают две проверки ухода.
Живьём в MAME (комната 16 уровня 8, leveldoor_open взведён через мост):
мышь появилась (charid = 0x18), добежала до колонки 7 (кадр 188, x = 164),
решётка (0,3) поехала вверх, мышь ушла вправо и погасла (direction = 0x56).
MEM-COLD1. Холодная половина главного цикла — в банк 8 (2026-08-12)
К уровню 8 куча в режиме huge упала до 879 Б. Убирать из roomtest.c
оказалось нечего — отладочное там всё живое (обход комнат, стоп-кадр, читы,
профиль), — поэтому вынесли в банк то, что отрабатывает раз на уровень или
только по клавише: pop_start_level + find_start_level_door, чит-навигацию
по комнатам и лейбл номера комнаты (roomtest_cold.c, --bank 8, n_banks
поднят до 8).
Результат: _CODE 25653 → 24825, куча 879 → 1707 Б, банк 8 занят на 5 %.
Кадровый путь не тронут: за обычный кадр в банк 8 уходит ноль вызовов, при
смене комнаты — один трамплин.
Что осталось в резиденте намеренно: enter_room_side и рабочие массивы
комнаты (их читает и главный цикл, и холодная половина), guard_over_kid с
хелперами (зовётся каждый кадр). Следующий шаг — MEM-COLD2.
Приёмка уровней
ПОЛИТИКА ПРИЁМОК — решение пользователя 2026-08-11
Уровни 1-4: smoke-тесты пройдены. Полные прогоны (обход всех комнат
каждого уровня) делаются по готовности ВСЕХ уровней, а не по одному за
этапом — отдельных задач L3-PASS/L4-PASS больше нет.
Основание: сквозные обходы дорогие, а половина находок на неполном наборе уровней всё равно оказывается «механики ещё нет». Smoke (пройти уровень от старта до двери) остаётся обязательным на каждом новом уровне — он снимает блокеры, а не косметику.
Уже сделанные полные обходы уровней 1 и 2 (ниже) остаются регресс-базой.
L1-PASS. Сквозное прохождение уровня 1 — ЗАКРЫТ 2026-08-07
Smoke 2026-08-05 (пользователь): успешный. От старта до выхода с уровня одним заходом — меч подобран, оба стража побеждены в честном бою (без читов), выход отработал корректно. Ценность прогона в том, что он снял главные риски этапа 1 разом — боёвка, предметы, переход с уровня — и стал регресс-базой для уровня 2.
Полный обход комнат 2026-08-07 (пользователь): крупных багов нет. Этим же прогоном закрыта таблица обхода 24 комнат и чек-листы ручной перепроверки фиксов — обе уехали в
BUGS_CLOSED.md.
Приёмка этапа 1 и одновременно регресс-база для уровня 2: от старта до двери
уровня одним заходом — подбор меча, страж, кнопки/ворота, пики, loose-полы,
зелье, падения. Точки, где смотрели внимательно, — закрытая косметика
окклюзии (потолок при прыжке вверх, шов при анимации решётки, грани дальней
колонны) и подъём на тайл-кнопку (оговорка к BUG-3 в BUGS_CLOSED.md).
L2-PASS. Приёмка уровня 2 — ЗАКРЫТ 2026-08-07
Smoke 2026-08-05 (пользователь): уровень 2 пройден. Из smoke пришли BUG-LOOSE-3 и BUG-GUARD-DEAF-1 — оба закрыты (
BUGS_CLOSED.md). Следом прогнан smoke уровня 3.Полный обход комнат 2026-08-07 (пользователь): крупных багов нет. Открытыми с этого прогона остались три записи в
BUGS_OPEN.md: BUG-GUARD-COLOR-1 (страж всегда одного цвета), BUG-GUARD-SPLASH-1 (нет брызг при попадании по стражу — закрыт 2026-08-07, разбор вBUGS_CLOSED.md) и BUG-CHEAT-FIGHT-1 (наш чит+/−в бою отнимает управление). Ни одна играть не мешает.
Ниже — карта содержимого уровня, снятая прямо с res2002.bin. Она
осталась в архиве не как отчёт, а как справочник для повторных прогонов и
для сравнения с SDLPoP: если механика в таблице есть, а в игре не сработала —
это баг, а не «так задумано».
Стражи — 5, в комнатах 4, 7, 11, 15, 24 (skill 1/2/1/1/3, цвета 1/3/1/1/6 — цвет мы пока игнорируем, атлас один, см. BUG-GUARD-COLOR-1). Заметить: страж комнаты 24 со skill 3 — первый по-настоящему опасный.
Кнопки и что они открывают (декодировано из LINKLOC/LINKMAP):
| Кнопка | Тип | Цель |
|---|---|---|
| к.9 @ряд1,кол1 | RAISE | дверь уровня к.23 @1,3 — то есть выход |
| к.11 @1,1 | RAISE | ворота к.18 @0,9 |
| к.18 @0,7 | RAISE | ворота к.7 @0,9 и к.18 @0,9 (две сразу) |
| к.18 @0,2 | DROP | закрывает ворота к.7 @0,9 |
| к.13 @1,4 | DROP | закрывает ворота к.13 @1,5 |
Ловушки и предметы по комнатам:
к. 3 loose@2,2 зелье@2,5 (здоровье)
к. 4 loose@1,7 loose@1,8 + СТРАЖ
к. 5 дверь уровня @1,2-3 — ВХОД (захлопывается за спиной)
к. 6 пики@1,3 loose@1,5 зелье@2,7 (здоровье)
к. 7 ворота@0,9 пики@2,7 + СТРАЖ
к. 8 зелье@2,2 (здоровье)
к. 9 кнопка RAISE@1,1 loose@2,6
к.10 пики@2,2
к.11 кнопка RAISE@1,1 + СТРАЖ
к.12 зелье@2,6 (здоровье) loose@2,8
к.13 зелье@1,3 (ВРЕДНОЕ, −1 HP) кнопка DROP@1,4 ворота@1,5 зелье@1,8
к.15 + СТРАЖ
к.18 кнопка DROP@0,2 loose@0,4 кнопка RAISE@0,7 ворота@0,9 зелье@2,6
к.19 пики@1,4
к.20 ЗЕЛЬЕ@1,2 — БОЛЬШАЯ СКЛЯНКА (+1 к потолку HP) пики@2,6 пики@2,7
к.23 дверь уровня @1,3-4 — ВЫХОД
к.24 + СТРАЖ (skill 3)
Отдельно проверялось то, что появилось именно на этом уровне:
- большая склянка (к.20) — потолок HP становится 4, индикатор рисует четыре деления, и это HP переносится на уровень 3;
- меч уже в руках с самого старта (
have_sword = level >= 2); - выход через дверь к.23 вживую (кнопка в к.9);
- смерть/респавн возвращают на уровень 2, а не на 1.
Справка: связность комнат уровней 1–3 (снято 2026-08-05)
Обход графа roomlinks (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет
соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись
13/18/24 (см. «НЕ БАГИ» в BUGS_CLOSED.md). Скрипт разовый,
в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.
| уровень | старт | недостижимы | признак |
|---|---|---|---|
| 1 | к.1 (0,0) | 13, 18, 24 | ссылки наружу есть, обратных нет |
| 2 | к.5 (1,3) | нет | граф полностью симметричен, все 24 достижимы |
| 3 | к.9 (2,4) | 23, 24 | то же, что на 1: односторонние ссылки, обе комнаты полностью пустые |
ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7
23 R→22, у 22 L=4 | 24 R→7, у 7 L=2
| 24 U→16, у 16 D=0
на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty
То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот же: в 23/24 возможен «мусор в шве» (наш рендер кромки читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов там низкий, в игре их не видно.
Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода (единственная связь — вверх, откуда Кид падает):
к.10 U→4 пики(2,2) + пол — падение из комнаты 4
к.14 U→21 шахта 2 тайла шириной, дно = обломки
к.17 U→15 то же
Это не баги данных: в 14/17 попадают только падением насмерть, а из 10 (если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать «застрял» багом.
Уровни и механика
L4-MIRROR. Зеркало уровня 4 и тень — ЗАКРЫТА 2026-08-11
Пять шагов из шести сделаны и проверены пользователем в MAME 2026-08-11; шестой (вид тени) осознанно отложен.
| шаг | что | коммит |
|---|---|---|
| 1 | зеркало в атласе: tile_table[0x0D] база 75 / фронт 77, MIRROR_ENV_IDS + FORE_ENV_IDS в pop_pack_bg.py |
1b2111f |
| 2 | постановка тайла: place_mirror в pop_trob.c по переходу pop_leveldoor_open 0/2 → 1 (animate_leveldoor, seg007:0457) |
1b2111f |
| 3 | отражение (check_mirror, seg003:0798) — отдельной функцией pop_mirror_draw, со своим heal |
8f0362f, 3913f1e |
| 4 | прыжок сквозь зеркало и рождение тени (seg004:0239 + seg003:0798..08A9 + seg002:081D/1131) | 844fa6d |
| 5 | клип тени слева obj_clip_left = 137 + (mirror_column−4)*32 (seg008:1699) + новый примитив gfx_blit_cols_part_wx в libbgi |
8f0362f |
Почему зеркало пришлось добавлять в атлас явно: тайла 13 нет ни в одном
уровне статически — проверено перебором всех 15 res200N.bin, ноль
попаданий, поэтому render_room.py его не видит и на месте зеркала был бы
чёрный провал (memory pop_atlas_dynamic_ids).
Почему отражение — отдельная функция, а не третий слот Char: это
структура самого оригинала — отражение идёт сокращённым путём
load_frame_to_obj + add_objtable(4), без клинка, брызг, пропуска кадра;
гейтить всё это в общем теле pop_cdraw значило бы добавить ветки в самый
горячий путь. Побочный эффект — FORE-DUP: передний
слой тайла рисуется дважды, когда футпринты Кида и отражения накрывают один
тайл (всегда, они стоят на одном тайле). Картинку не портит, тратит такты.
Почему левый клип сделан примитивом libbgi, а не «нарисовать и вернуть фон поверх лишнего» (подсказка пользователя): для column-major левая обрезка стоит ровно столько же, сколько правая — колонка это непрерывный кусок ОЗУ, меняются стартовая колонка источника и экранная X. Внутри это уже было (так клипается левый край экрана), наружу не было выведено.
Отложено — вид тени (шаг 6): в оригинале она рисуется ДВУМЯ блитами
одного спрайта, blitters_2_or на месте и blitters_3_xor со сдвигом +1 px
(seg008:1602); у нас пока обычная копия, то есть тень выглядит вторым Кидом.
Разбор, замеры и варианты — ../docs/shadow_render.md;
блочные операции акселератора под это в libbgi уже есть (tests/accop).
Краевой случай проверен и закрыт как НЕ БАГ —
MIRROR-FG-STALE: тайл ставится в момент,
когда дверь дорисовала открытие, а в реальном прохождении игрок в этот момент
в другой комнате; при обычном входе комната и снимок room_fg берутся из
данных уровня разом.
L3-CHOMP. Чомперы — СДЕЛАНЫ 2026-08-08
Коммиты dc0bd47 (анимация, отрисовка, смерть в челюстях), 4d4323f
(передние зубья через pop_fore_b + ветка в draw_tile), db4106a
(перед чомпером Кид разбегается сразу, без осторожного шага — safe_step по
оригиналу), 1461ed5 (фикс регрессии, см. ниже).
Портировано: animate_chomper (seg007:0448), next_chomper_timing
(seg007:0F9A — 15,12,9,6,13,10,7,14,11,8 по кругу), start_anim_chomper
(seg007:08C7), start_chompers (seg007:0F13) — все в pop_trob.c,
состояние в room_modif, как у пик и ворот. Смерть: SEQ_54_CHOMPED /
FRAME_178_CHOMPED + check_chomped_guard для соперника (pop_map.c).
Отрисовка — pop_chomp_pose (pop_bg.c) и холодная перерисовка тайла в
pop_room.c. Ассеты упакованы явным набором кадров (CHOMPER_BOT_IDS
101-105, TOP 111-113, FORE 106-110 + кровь 114-123 mono-силуэтом).
Грабли, стоившие регрессии (1461ed5): start_chompers вызывается на
смене ряда персонажа, а у нас seqtbl читается ЧЕРЕЗ ОКНО W0 — прямой вызов
из play_seq переключал окно посреди чтения байткода. Вызов отложен через
флаг chomp_pending (pop_kid.c) и делается ПОСЛЕ цикла интерпретатора.
Хвосты в багах: BUG-CHOMP-JUMP-1 (низкий,
маловоспроизводим) и закрытые BUG-TORCH-CHOMP-1/2 (пламя факела и застывший
чомпер — BUGS_CLOSED.md).
L3-SKEL. Скелет уровня 3 — СДЕЛАН 2026-08-07, принят smoke-прогоном
В данных уровня 3 стражей нет вообще (guards_tile пуст во всех 24
комнатах) — единственный враг это скелет, и он не «страж из данных», а
спецсобытие check_skel (seg002:1044): в комнате 1, когда
Kid.curr_col == 2 или 3 и дверь уровня открыта, тайл tiles_21_skeleton
(комната 1, тайлпос 15) стирается в пол, а на его месте поднимается
персонаж — charid_4_skeleton, меч сразу вынут, seq_88_skel_wake_up,
skill 2, HP 3. Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не
оживают. Атлас — pop_pack_guard.py SKEL → poc/res/skel/g0..g3.atl.
Проверено в MAME: бой, падение в пропасть, окклюзия (2026-08-07); принят smoke-прогоном уровней 1-4 (2026-08-11). Этот же механизм — «спецсобытие порождает персонажа в слоте соперника» — база для тени уровня 5 (L5-SHADOW).
L3-CHKP. Чекпойнт уровня 3 — СДЕЛАНО 2026-08-06 (коммит 0cd6b2d)
level3_set_chkp (seg002:0665): флаг взводится, когда Кид уходит ВЛЕВО ИЗ
комнаты 7; do_startpos (seg003:141) по нему подменяет старт на комнату 2,
тайлпос 6, направление влево и снимает loose-плиту (комната 7, колонка 4,
ряд 0). Константы — в pop_tune.h (POP_CHKP_*), механика hitp_beg_lev
была сделана раньше в L2.
Тонкость, на которой сначала ошиблись: level3_set_chkp вызван из
leave_room ДО goto_other_room, поэтому Char.room == 7 — это комната, ИЗ
которой уходят, а не в которую входят. Поймал пользователь прогоном в
SDLPoP: смерть В комнате 7 вернула его в стартовую 9, а плита осталась цела.
Проверено в MAME: вход в 7 флаг не ставит, уход влево — ставит; респавн в комнате 2; после обычной смерти (без чекпойнта) плиты восстанавливаются как раньше. Вживую в игре (не читом) — потрогать на приёмке уровня 3 (L3-PASS).
L2. Переход на уровень 2 и его игра — МАШИНЕРИЯ СДЕЛАНА 2026-08-04
Порт levels_plan.md §2 (шаг 1). Что появилось:
- Номер уровня стал состоянием.
pop_current_level(портcurrent_level) вpop_level.c;pop_next_levelбольше не флаг, а НОМЕР —END_LEVELего инкрементит (как++next_level, seg006:662), а главный цикл срабатывает по расхождениюpop_next_level != pop_current_level(портplay_level_2, seg003:0386). - Загрузка по номеру —
pop_level_load_num(n):LEVELS\res20NN.bin(fallbacka:\), старая EMM-страница отпускается ТОЛЬКО после успешной загрузки новой (нет файла — играем дальше на текущем). На диск кладутся все 15 уровней (34 КБ). - Потабличные различия (
data.h:840..848) — таблицы по 16 вpop_level.c:tbl_entry_pose(поза входа),tbl_guard_hp(HP стража, ушло из хардкода3),tbl_guard_type(−1 = стражей нет, иначе на 14/15 они полезли бы из данных комнат),tbl_level_type(тайлсет — пока только читается). find_start_level_door(seg003:02E6) — на уровне 2 это НЕ косметика: стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина двери уровня, и безmodif = 43+add_trob(...,3)Кид материализуется внутри глухой створки. Тип 3 = «быстро закрыть»: дверь захлопывается за спиной за три кадра, как в оригинале.- HP через уровень —
hitp_beg_lev(seg003): рестарт уровня откатывает HP к нему, пройденный уровень подтягивает его кhitp_max. Заодно реализована большая склянка (add_life, тип зелья 2: +1 к ПОТОЛКУ HP до 10) — на уровне 2 она есть, комната 20. - Меч —
have_sword = level >= 2(play_level, seg003:106), а не жёсткий ноль. - Чит Shift+L (seg000:698) —
pop_next_level = pop_current_level + 1. NB:Lбез Shift занят отладочным «осторожным шагом вправо» (pop_ctrlKBD_DBG_STEPR); с Shift шаг тоже пройдёт, но уровень тут же сменится — на практике не мешает.
Проверено в MAME (2026-08-04): старт уровня 1 → Shift+L → уровень 2 рисуется правильно (комната 5, большие колонны — новые для нас тайлы 8/9 — на месте, дверь захлопнута, HP 3) → влево в комнату 4, страж на месте → Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает повторно, а не только один раз.
Контент уровня 2 (сквозное прохождение, выход через дверь) закрыт отдельно — L2-PASS. Цвет стража из данных остался открытым багом — BUG-GUARD-COLOR-1.
L1-START. Старт по данным уровня — СДЕЛАНО 2026-08-01
Старт и оба рестарта (смерть, выпадение из уровня) сведены в один
pop_start_level() — порт start_level + do_startpos + set_start_pos
(seg003): комната/тайл/направление берутся из pop_level_start_*, направление
инвертируется (~start_dir), поза входа — из tbl_entry_pose. У уровня 1
это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
на row 1 — как по данным уровня.
#define ROOMNAV оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
оригинале не было, — как и S (выдать меч), K, I. Все они со временем
съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
L1-EXIT. Выход с уровня (дверь уровня) — СДЕЛАНО 2026-08-01
Портированы: ветка двери уровня из up_pressed + go_up_leveldoor
(seg005:0482/0574) — в pop_leveldoor_enter() (pop_map.c, тайлы и
геометрия) и up_pressed() (pop_ctrl.c, только последовательность);
опкод 0xF1 END_LEVEL в play_seq теперь инкрементит pop_next_level
(порт next_level), а главный цикл по нему перезапускает уровень — ровно
та точка, куда levels_plan.md §2.2 подключила загрузку уровня 2.
Открытость двери проверяем по modifier >= 42 (ветка fix_exit_door), а не
по ванильному leveldoor_open: с ванильным условием можно войти в ещё
ползущую створку.
Грабли, которые стоили отдельного разбора: go_up_leveldoor сначала
писал Char.x/Char.direction, и оба присваивания молча терялись — окно
Char вокруг диспетчера возвращает назад ТОЛЬКО curr_seq и sword
(pop_savekid_state). Направление оставалось «вправо», а все DX
последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево.
Вывод на будущее: геометрию персонажа в этом порте меняет pop_map (пишет в
Kid), а не диспетчер.
✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
проёма) — недоставало второй половины clip_char (obj_clip_right) и
обрезки СПРАВА у колоночного блита. В libbgi добавлен
gfx_blit_cols_part_w(..., maxw); разбор — в BUGS_CLOSED.md.
L1-TRIAGE. Ревизия багов — ЗАКРЫТА (часть 1 — 2026-08-01, хвост — 2026-08-07)
✅ Три Critical'а прогнаны в MAME и закрыты (протокол с числами — в
BUGS_CLOSED.md, раздел «Проверено в MAME 2026-08-01»):
BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2
(ping-pong при возврате) не воспроизводятся, BUG-3 (окклюзия climb-up на
кнопке) закрыт фиксом tile_code_drawn от 2026-07-28. Заодно снят неверный
диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное
поведение goto_other_room (SDLPoP/src/seg002.c:390 меняет только x).
Список разделён на BUGS_OPEN.md (открытое) и
BUGS_CLOSED.md (закрытое + разбор корней).
✅ Косметика окклюзии тоже закрыта — BUG-CEIL-1/2/3 и BUG-OCCL-1 были
починены кодом ещё в июле, а записи никто не снял: ceil_over_kid_tile
(pop_bg.c:1275), pop_ceil_modif + pop_ceil_shake_draw/_bake_empty
(:547,815,827), bar с POP_YOFF+3 в pop_room_redraw_seam_left (:793),
разделение слоёв по add_backtable vs ptr_add_table в overlay_mid_tile
(:1367). Разбор — в BUGS_CLOSED.md.
✅ Хвост закрыт 2026-08-07: таблица обхода 24 комнат уровня 1 — прогоном
всех комнат уровней 1 и 2 (крупных багов нет), см.
BUGS_CLOSED.md.
Отрисовка и память
DRAW-CHAR. Отрисовка — ОДНА на всех Char — СДЕЛАНО И ПРОВЕРЕНО 2026-08-08
Итог. Отрисовка персонажа сведена к одному набору функций над
Char(pop_cdraw.c, банк 4) — как физика после GUARD-PHYS. Было два независимых куска кода:kid_draw/kid_heal/kid_draw_splash/kid_fore_clip*в резиденте W1 иpop_guard_draw/pop_guard_healв банке 4, каждый со своей математикой кадра, своими heal-прямоугольниками и своим проходом окклюзии (pop_fore_over_kid/pop_fore_over_char).Основание в оригинале (сверено перед кодингом):
add_kid_to_objtable(seg008:22F0) иadd_guard_to_objtable(seg008:2324) имеют ИДЕНТИЧНОЕ тело и различаются окном (loadkid/loadshad), набором спрайтов и типом объекта;redraw_at_char/redraw_at_char2(seg003:0576/0645) гейтов поcharidне имеют вовсе — единственная ветка по персонажу там это объединение с ПРОШЛЫМ футпринтом у Кида (пометки перерисовки, у нас их нет).draw_hurt_splash(seg006:2003) разводит Кида и остальных ровно двумя числами: chtab/image и подъём((charid == kid) << 2) + 11.
Что теперь одно на всех.
| Было (два места) | Стало |
|---|---|
kid_draw / pop_guard_draw |
pop_char_draw(who) |
kid_heal / pop_guard_heal |
pop_char_heal(who) |
kid_draw_splash / ветка брызг внутри pop_guard_draw |
cd_splash (порт целиком, все три ветки кадров) |
kid_fore_clip + kid_fore_clip_restore / хвост pop_guard_draw |
pop_char_fore(who) |
pop_fore_over_kid / pop_fore_over_char |
pop_fore_over_char(ch, …) |
kid_fp_*() / pop_guard_fp_width() |
pop_cd[slot] (структура в _DATA, читается из любого банка без трамплина) |
pop_kid_set_render_dx |
pop_char_set_render_dx(who, dx) |
Слот (POP_CH_KID / POP_CH_OPP) выбирает ровно то, что различается в
оригинале: окно Char, набор атласов и кадр (kidp+kid_frame /
gp+pop_gframe), heal-прямоугольники по страницам дабл-буфера.
Что при этом ПОЧИНИЛОСЬ (расхождения, которые и были ценой дублирования).
clip_charу соперника не было вовсе — теперь общий (в оригинале он в обоихadd_*_to_objtable).- Клип полем 192 у Кида не было (
reset_obj_clip:obj_clip_bottom = 192): его спрайт рисовался на полосе HP, а следы за него подчищала полосаpop_room_clip_bordersпо флагуpop_clip_sprite. Теперь персонаж режется полем, как в оригинале, иborder_dirtyостался только у падающей плиты. - У Кида не было ветки брызг «мёртв / падение 106..110» (
obj_y += 4) — была только у соперника. char_width_halfСТРАЖА считался по спрайту КИДА:set_char_collisionиcheck_spike_below(pop_map.c) читалиkid_fp_width()безусловно, аpop_guard_fp_width()использовался только для порядка отрисовки. Теперь метрики берутся поcharidактивного персонажа (CD_ACT).- Расширение перебора тайлов окном спрайта (клинок и брызги уходят за
габарит кадра) было только у соперника — теперь общее. Заодно оно
перестало быть четырьмя лишними аргументами: окно fore-клипа уже лежит в
pop_bg.c, и проход читает его сам.
Цена / выигрыш (ALLOCS=3000, замер до и после):
было стало дельта
_CODE (W1) 24 881 20 524 −4 357 ← куча 2 023 → 6 333 Б (+4 310)
BANK2 pop_bg 15 310 15 045 − 265 ← свободно 1 074 → 1 339 Б
BANK3 pop_map 8 608 8 713 + 105 (CD_ACT — индекс по charid)
BANK4 pop_cdraw 4 625 5 905 +1 280 (сюда переехала отрисовка Кида)
итого −3 237
То есть слияние двух копий дало ~3.2 КБ, но главный выигрыш не в банке 2,
а в резиденте: отрисовка Кида уехала из W1 в банк, и куча выросла втрое
(2 023 → 6 333 Б). Банку 2 при этом досталось всего +265 Б свободного
места — под L3-CHOMP этого мало, разгрузку
pop_bg придётся делать отдельно (свободные номера банков — 7+).
Проверено: tests-host — все 5 наборов зелёные, трассы физики не
изменились ([phys] ok: 1723, тот же эталон; [char] ok: 65). В MAME
(свежий образ, полный рестарт): уровень 1 — комната 1 (Кид, кладка,
факелы, окклюзия колонны), падение на нижний ярус, комната 3 — страж своего
цвета с клинком и своей полосой HP, бой и смерть Кида. Артефактов
отрисовки и мусора на полосе HP нет.
Приёмка пользователем пройдена 2026-08-08. Прогон вживую: отрисовка персонажей, окклюзия и бой — без регрессий.
Единственное наблюдение — циан-полоса профиля (спрайты + fore) выросла: проход над Кидом теперь расширяется окном спрайта, как раньше только у соперника, то есть перебирает на колонку-другую больше. Решение пользователя: в пределах допустимого, оптимизация — ОТДЕЛЬНОЙ задачей (DRAW-COST).
MEM-BANK2. Разгрузка банка 2 — ЗАКРЫТА 2026-08-08
Банк 2 (pop_bg.c) упирался в потолок: 90.8 % ещё до чомперов. Разбор по
символам показал, что он держит ДВЕ разные вещи — горячий fore-проход (каждый
кадр) и холодную отрисовку тайлов (вход в комнату, точечные перерисовки), —
а общие «листья» (blit_b, tile_code, tile_table, wall_pattern) нужны
обеим.
Ключевое ограничение платформы (записано в memory
sdcc_banked_call_rules): писучие данные банка лежат в _DATA/W2 и видны
всем, а const-таблицы — в СТРАНИЦЕ банка, и из другого банка их не
прочитать вовсе. Плюс трамплин выбирается ОБЪЯВЛЕНИЕМ: пометить лист
__banked — значит заставить платить и горячих вызывающих (654 такта на
круг).
Шаг 1 — общие листья в РЕЗИДЕНТ (pop_tile.c + внутренний pop_tile.h).
W1 замаплен всегда, поэтому и горячая половина, и будущая холодная зовут их
прямым call и читают таблицы напрямую — без дублей и без трамплинов.
Уехали: blit_b, env_b/wall_b/fore_b/pot_b, tile_code/tile_mod/ tile_code_drawn/wall_modifier, heal_off, get_loose_frame,
potion_flask, pop_fore_set_clip, pop_room_set_below/above + таблицы
tile_table, COL_XH, WALL_FRAM_*, SPIKES_FRAM_LEFT,
LOOSE_FRAM_LEFT/BOTTOM. Заодно три функции перестали быть __banked —
минус трамплин на кадр.
Шаг 2 — дедуп внутри банка. wall_pattern (808 → 394) и wall_rnd
(786 → 654): четыре ветки по виду стены (SWS/SWW/WWS/WWW) отличались ТОЛЬКО
набором кусков и числами в одной и той же серии prandom — сведены к
таблицам WP_PARTS и WR_RULE. Порядок вызовов prandom сохранён
дословно (иначе разъедется раскладка кладки).
Грабля, пойманная замером: после выноса tile_table из static const в
глобальную SDCC перестала сворачивать tile_table[10].fore_x/fore_y в
константы и стала читать их в рантайме — potion_flask раздулся 115 → 265 Б.
Лечится макросами, которыми инициализируется та же запись таблицы (одно место
правки, дублирования данных нет): 265 → 230.
Замер (ALLOCS=3000):
было после ш.1 после ш.2
BANK2 14 815 12 460 11 942 (90.4 % -> 72.9 %, свободно 4442 Б)
_CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б)
С релизным ALLOCS=100000 банк 2 будет ещё примерно на 500-600 Б меньше
(замер до разгрузки: 14 815 -> 14 176).
Проверено: tests-host зелёные; в MAME комната 1 уровня 1 после дедупа
совпала с дорефакторным снимком попиксельно (0 различий из 227 520),
комната 3 (сплошная кладка, все четыре вида стены и марки) — та же раскладка
кирпича.
Шаг 3 — ХОЛОДНАЯ половина в банк 7 (pop_room.c + внутренний
_pop_bg.h). Граница проведена по частоте вызова, а не по размеру:
pop_bg.c (банк 2) ГОРЯЧЕЕ fore-проход, оверлеи, кладка, клип каждый кадр
pop_room.c (банк 7) ХОЛОДНОЕ draw_tile, точечные перерисовки, mob, раз на комнату
загрузка атласов / на событие
Обратных зависимостей нет вовсе: холодная половина зовёт горячую ровно в
трёх местах (wall_pattern, wall_pattern_reset, draw_gate_back) — и
через ТОНКИЕ __banked-обёртки, а не пометкой самих тел. Тела остаются
непомеченными, поэтому fore-проход, который зовёт wall_pattern до девяти
раз за кадр, платит ноль. Общее состояние (pop_loose_modif,
pop_ceil_modif, obj_row/obj_col) — писучее, лежит в _DATA/W2 и видно
обеим половинам; obj_row/obj_col для этого перестали быть static.
Цена трамплинов: полная отрисовка комнаты — ~40 вызовов через границу
(по два wall_pattern на стенной тайл), 26 000 тактов = 0.06 кадра РАЗОВО
на вход в комнату. Анимация пик/ворот — 2-4 вызова на кадр, ~1 %. Ровно
тот порядок, который был признан приемлемым при постановке задачи.
Заодно удалена мёртвая potion_bubble (169 Б): её работу давно делают
pop_potion_flask (резидент) и pop_potion_draw, вызывающих не осталось.
ГРАБЛЯ, стоившая прогона: n_banks объявляет САМО ПРИЛОЖЕНИЕ
(roomtest.c), а не sprinter-cc. Восьмой банк без правки этой константы
собирается и линкуется без единого предупреждения, _bank_pages[7]
остаётся 0xFF, и трамплин уводит в мусор: exe грузится, но программа встаёт
намертво ещё до первого кадра — на экране остаётся приглашение DSS.
Диагноз снят дампом _bank_pages из MAME (FF EC ED EE EF F0 F1 FF —
семь страниц вместо восьми), а не гаданием. Предупреждение об этом в
шапке roomtest.c было — и всё равно не сработало, потому что число банков
живёт в двух местах.
Замер (ALLOCS=3000):
было ш.1 ш.2 ш.3
BANK2 14 815 12 460 11 942 5 872 (90.4 % -> 35.8 %)
BANK7 — — — 6 035 (36.8 %)
_CODE 20 064 22 591 22 556 22 556 (куча 4301 Б — не тронута)
Проверено (шаг 3):
tests-hostзелёные;- построчная сверка
pop_bg.c+pop_room.cпротив дорефакторногоpop_bg.c: НИ ОДНОЙ строки логики не пропало и не изменилось — расхождения только шапки, включения, три обёртки и удалённый мёртвый код; - в MAME 8 комнат (стартовая + 7 по
ROOMNAV) сняты до и после и сверены попиксельно по активному экрану 320×256: различия ТОЛЬКО в фазе анимации (пламя факелов, кадр Кида), статическая кладка совпадает пиксель в пиксель; - живой прогон: переход между комнатами, бой со стражем, решётка ворот, окклюзия Кида ближней колонной — как на дорефакторном билде (сверено контрольным запуском HEAD-бинаря с тем же вводом).
Что осталось в запасе, если места снова не хватит (не делаем, пока нет
причины): вынести из РЕЗИДЕНТА холодные части — инициализацию в main +
enter_room_side/pop_start_level (~1.7 КБ), загрузчик атласов Кида
(~1.2 КБ), pop_room_load/pop_level_load (~1.4 КБ). Итого ~4-4.5 КБ
резидента, то есть куча 4301 -> ~8.6 КБ. Целиком pop_level.c в банк
нельзя: его pop_level_tile зовётся из банка на КАЖДЫЙ тайл — только
расщепление модуля.
CLIP-1. Аудит блитов: где клип не нужен — СДЕЛАНО 2026-08-01
Итог. Heal Кида и стража переведены на выбор ядра по тому же тесту, что давно стоит у блитов. Замер в MAME (счётчики
totalcyclesна входахkid_healиkid_tick, то есть вся группа heal за кадр; комната 1, Кид стоит, стража нет):
путь тактов на кадр кадров в выборке клипающее ядро (как было) 26 200 149 noclip (стало) 15 848 239 −10 352 такта на кадр, то есть −39.5 % с группы heal (≈0.49 мс при ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина — с пропатченным в памяти условием (
jr nz→jrвpop_heal_fast), то есть на той же геометрии и в той же сцене.Размер: −362 Б суммарно (не плюс!):
_CODE25 289 → 25 306 (+17), BANK2 13 792 → 13 676 (−116, свободно стало 2708 Б — это тот самый тесный банк из рисковlevels_plan.md§5), BANK3 6512 → 6249 (−263), BANK4 без изменений.Грабли, стоившие двух пересборок (вынесено в память
sdcc-static-inline-double-cost): первым заходом хелпер былstatic inlineвpop_bg.h— и SDCC 4.5 И встроил его тело (181 Б) в каждое место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит заголовок.pop_guard_healраздулся с ~60 до 663 Б, итого +1091 Б в_CODEи +636 Б в банке стража. Лечится обычной функцией в одном резидентном модуле (pop_draw.c, W1 — из банков это прямойcallбез трамплина, как уpop_sword_draw).Проверено визуально: обычная ходьба, прыжок, спуск и позиция «за решёткой шва» (straddle — там как раз работает клипающий фолбэк) — артефактов и следов нет.
Ниже — исходная постановка задачи (что и почему смотрели).
Зачем было сейчас. Кадр занят на ~86 %; подготовка клипающего варианта
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
4 617 тактов на спрайт 32×3 (libbgi/include/gfx.h). Это самая дешёвая
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
Что чинили:
- ✅
kid_heal()иpop_guard_heal()звалиgfx_heal— всегда с клипом, хотяgfx_heal_noclipсуществует иheal_offвpop_bg.cим уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общийpop_heal_fast(pop_draw.c+_pop_draw.h). - ⛔
pop_room_clip_borders()(pop_bg.c) —gfx_heal(0,0,320,…): оставлен клипающим осознанно. Полоса шириной 320 не лезет в 8-битный параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейтborder_dirtyпускает туда только в кадрах падения, и выигрыш подготовки тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы не «оптимизировать» повторно.
Что обязано остаться с клипом (зафиксировано комментариями в коде):
- кромочные тайлы фона (
blit_bвpop_bg.c) — тайл у края экрана режется по построению; - спрайты при straddle (
kid_render_dx = ∓140, комната Кида ≠ отрисованной) и при падении ниже поля — фолбэки вkid_draw/pop_guard_draw; - борта поля (
pop_room_clip_borders) — см. выше.
Что уже было правильно (шаблон, который и распространили): блиты Кида,
стража, клинка и брызг спрашивают pop_onscreen_cols() и уходят в
gfx_blit_cols_part_noclip. Отдельный случай — pop_kid_img_blit: noclip
БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по
фиксированным координатам; это тоже помечено в коде.
MEM-BANK5. Разгрузка W1/W2 новым банком кода — СДЕЛАНО 2026-08-05
Итог: pop_ctrl.c уехал в банк 5, куча 180 Б → 2298 Б (после снижения
--max-allocs, см. BUILD-FAST, — 2751 Б).
Вопрос 2026-08-04: «надо делать новый банк?». Да, и он лечит именно то,
что жмёт. В нашей раскладке (MEMORY=huge, small-вариант) CODE и DATA
живут в ОДНОМ 32-КБ пространстве W1+W2 — карта сборки на тот момент:
_CODE 0x4100..0xAA00 26880 Б
_DATA 0xAAD0..0xB9B0 3808 Б
_BSS 0xB9B8..0xBADA 290 Б
куча 0xBADA..0xBB00 38 Б ← упёрлись сюда, добавляя отладку
стек 0xBB00..0xC000 1280 Б
Поэтому каждый килобайт кода, уехавший в банк, становится килобайтом,
доступным данным. Отдельного «дефицита W2» у нас нет — дефицит один.
(38 байт кучи не опасны сами по себе: malloc'ом мы не пользуемся, страницы
берутся через mem_alloc_block. Опасно то, что следующая структура
данных упрётся в стек молча.)
Резидентный код по модулям (из .sprinter-cc-roomtest/*.rel):
| модуль | _CODE | как часто зовётся | в банк? |
|---|---|---|---|
pop_kid.c |
6287 | load_frame/play_seq — 2×/кадр (Кид + страж) |
частично: холодная половина (загрузка страниц спрайтов, pop_kid_load) — да; движок кадров — нет |
roomtest.c |
3893 | main-loop | нет (точка входа, зовёт всех) |
pop_trob.c |
2526 | do_trobs 1×/кадр, но pop_trob_modif — горячий аксессор |
кандидат, если вынести аксессор в резидент |
pop_level.c |
2292 | pop_level_tile — из pop_bg (банк 2) на каждый тайл |
нет: банк→банк на каждый тайл убьёт отрисовку |
pop_ctrl.c |
2189 | user_control 1×/кадр |
взят — дёшево и безопасно |
pop_guard.c |
923 | 1×/кадр | нет смысла |
Критерий кандидата — не размер, а частота вызова и отсутствие горячих
банк→банк переходов; pop_level показывает, что большой холодный на вид
модуль может быть горячим аксессором. Следующий шаг (расщепление
pop_kid.c) — в TASKS_OPEN.md, брать по факту
нехватки места.
BUILD-FAST. Сборка 10 минут → 1:48 — СДЕЛАНО 2026-08-06 (коммит 0cd6b2d)
--max-allocs-per-node — во сколько вариантов размещения регистров SDCC
упирается на узел. У sprinter-cc дефолт 100000 (агрессивно, как fast-сборки
библиотек), и на крупных модулях roomtest это МИНУТЫ на банк. Дефолт SDCC —
3000, для разработки его достаточно: разница в размере — единицы процента.
Итог: сборка с нуля 1:48 вместо >10 минут, куча 2751 Б вместо 2298
(на 3000 резидент иначе не влезает — замер: конец _HOME 0xBC69 при стеке с
0xBB00). В Makefile это ALLOCS ?= 3000:
make — быстрая сборка (ALLOCS=3000)
make ALLOCS=100000 — как раньше: минимальный код, для замеров размера
и для «релизного» образа
ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и
том же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
DBG-CHEATS. Отладочные читы SDLPoP — [/] СДЕЛАНЫ 2026-08-05, остальное — оценка
Мотив прямой: мост MAME теряет нажатия при быстрой отправке, поэтому подогнать Кида в нужную позу для отладки автоматикой нельзя — именно на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
Сделано:
[/]— сдвинуть Кида на пиксель влево/вправо. Оригинал —../SDLPoP/src/seg000.c:1828:У нас: коды PS/2 set 2if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x; else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x;[= 0x54,]= 0x5B вpop_cheat.hрядом сKBD_CHEAT_KILL/IMMO/SWORD; обработка — в том же блоке читовroomtest.c, по фронту (*_prev, как у остальных), иначе одно нажатие уедет на десяток пикселей. Работает поKid.xнапрямую: геометрию персонажа в этом порте меняет не диспетчер (см. грабли L1-EXIT).- Shift+L — следующий уровень (вошло в L2-машинерию).
Остальные — оценка, а не обязательство (актуальный статус —
TASKS_OPEN.md, «Отложено осознанно»):
| Чит | Вердикт |
|---|---|
| T — таймер | не сейчас: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой |
| F — остаток feather-fall | вместе с Shift+W: ветка JMP_IF_FEATHER (опкод 0xF7) в play_seq есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно |
| Shift+F9 — quickload с тем же уровнем | самое ценное и самое дорогое: это сериализация Char + room_modif всех комнат + trob'ов + стражей (levels_plan.md §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки |
KBD-1. Shift + стрелки: нажатия теряются — ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН (остаток отложен)
ПОПРАВКА К ПОСЫЛКЕ (2026-08-05). Ниже «fake shift» подан как установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой замер потока байт это не подтвердил: клавиатура MAME-Sprinter (
pc_kbd ms_naturl) обёрткуE0 F0 12/E0 12не шлёт вовсе — при зажатом Shift поток на стрелку ровноE0 75 E0 75 …. Значит удвоения трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO переполняется» отпадает; сам ФИКС (плотный опросkbd_raw_poll) остаётся верным и нужным — переполнение вызывает не Shift, а короткая жизнь импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия — BUG-KBD-5.Итог (2026-08-01). Причина — не наш код и не DI-окна графики: импульс запроса прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO SIO переполняется. Лечится ПЛОТНЫМ опросом:
kbd_raw_pollповешен idle-хуком на ожидание кадра (gfx_set_idle_hook, новый API libbgi) — процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home → 35 make, ноль потерь (до фикса — 9 из 10). Боевой сценарий тоже: четыре Shift+→ подряд дали четыре осторожных шага,
Kid.x114 → 147. Цена:_CODE+170 Б, кадровый бюджет не затронут (опрос стоит в простое).ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно лучше, но иногда при зажатом Shift стрелка всё-таки пропускается». Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали, значит остаточная частота заметно ниже прежних ~15 %. Задача осознанно ОТЛОЖЕНА до финальной полировки всей программы (решение пользователя); сейчас клавиатура пригодна для работы.
Где именно осталась дыра — чтобы на полировке не начинать с нуля. Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё ещё может потерять байт — ровно «иногда». Порядок действий, если вернёмся:
- Вернуть вызовы
kbd_raw_poll()между блитами занятой фазы (они бесплатны; сами по себе не помогали, но вместе с хуком закрывают именно этот зазор) и при необходимости внутрь тайловых цикловpop_bg— тогда слепым остаётся только тело одного блита.- Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
- Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code Set 3 через BIOS
$EA(убирает «fake shift» в корне, но в MAME непроверяемо — обратный путь к клавиатуре не разведён) и общийm_irq_off_timerв драйвере MAME.
Симптом (пользователь, 2026-08-01). Залипаний почти нет, но при УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше нажатия ← не отрабатываются, пока Shift не отпустишь.
Рабочая гипотеза, с которой начинали. Три факта складывались в одну картину:
- PS/2 Set 2, «fake shift». При зажатом Shift нажатие РАСШИРЕННОЙ
клавиши (стрелки —
E0-коды) обрамляется фиктивным отпусканием/нажатием шифта: нажатие ← шлётE0 F0 12+E0 6B= 5 байт (без шифта было бы 2), отпускание —E0 F0 6B+E0 12= 5 байт (было 3). (Опровергнуто замером 2026-08-05, см. поправку выше.) - Приёмный FIFO SIO — 3 байта. Пачка в 5 байт переживает только то,
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
«нажатие не отработало»; потерянный break = залипание (его лечит
kbd_raw_sync, но ценой сброса всех немодификаторных клавиш). - Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.
mame/sources/MAME/src/mame/sinclair/sprinter.cpp:on_kbd_data()выставляетm_irqs->in_set<1>()НА КАЖДЫЙ принятый байт (то есть старая запись вdocs/TODO.md«MAME не даёт per-byte INT» — неверна), но тут же заводитm_irq_off_timerна 32 такта, аirq_off()снимает линию. Если в эти 32 такта мы подDI— прерывание пропало насовсем, байт остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц). - Наши DI-окна длинные. Ядра акселератора держат
diна ВЕСЬ блит (libbgi/bgi256/_bgi_blit_cols_raw.c:47— «один DI на весь блит»); порядок цены прохода — 13.6 К тактов (libbgi/include/gfx.h), это сотни микросекунд, на порядки больше 32-тактового импульса.
Логика ввода (pop_ctrl.c, порт read_user_control/safe_step) сверена с
SDLPoP и корректна: safe_step() ставит control_forward = CONTROL_IGNORE,
и это снимается в read_user_control() при ОТПУСКАНИИ стрелки — то есть
повторные тапы ← при зажатом Shift обязаны работать. Не работали они
потому, что до нас не доезжал либо make, либо break стрелки.
ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
Методика. Симптом «нажатие не отработало» переведён в счётчики, чтобы не
спорить с глазами. Нажимается Home — тоже расширенная клавиша (тот же
E0-префикс), но игрой игнорируется, поэтому Кид стоит на месте и рельеф
комнаты на результат не влияет. Брейкпоинты с действием
{ b@ADDR = b@ADDR + 1 ; g } (счёт без остановки машины) в трёх точках: вход
клавиатурной ветки трамплина, чтение порта 0x18 внутри drain-цикла, запись
make-бита для кода 0x6C. Скратч-байты — хвост ovr_tile[].
Симптом воспроизведён скриптом: при зажатом Shift 10 нажатий → до декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил пользователь.
| Прогон | make дошло / нажато | overrun |
|---|---|---|
игра идёт, kbd_raw_poll ВКЛ |
9 / 10 | 3 |
игра идёт, kbd_raw_poll ВЫКЛ (патч ret в точке входа) |
9 / 10 | 4 |
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | 8 / 10 | 6 |
Вывод 1: наши DI-окна ни при чём. В замороженном кадре, где блитов нет и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
Вывод 2: kbd_raw_poll() в той расстановке бесполезен — 9/10 и с ним, и
без. Причина понятна задним числом: шесть вызовов стояли В ТЕХ ЖЕ точках,
где прерывания и так разрешены. Вызовы из roomtest.c убраны; сама функция
в libc оставлена — она корректна и нужна как заготовка под «плотный опрос».
Вывод 3 (главный): байт теряется НИЖЕ нашего кода. Счётчик чтений порта 0x18: 5 нажатий Shift+Home должны дать ровно 50 байт. Насчитано 49 — и ровно один make потерян. То есть до процессора байт не доехал вообще, декодер тут ни при чём.
Вывод 4: прерывание на байт теряется примерно в 44 % случаев. На тех же 49 прочитанных байтах — только 28 входов в клавиатурную ветку трамплина (1.75 байта за вход). Байты копятся в трёхбайтовом FIFO вплотную к потолку; одна неудачная пауза — и байт потерян.
ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
tests/kbdpoll — программа, которая не делает НИЧЕГО, кроме
kbd_raw_poll() в бесконечном цикле (ни графики, ни vsync, ни вывода:
любая работа разредила бы опрос и испортила замер). Это физический
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|---|---|---|---|
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
| Shift + Home | 25 | 25 | 250 / 250 |
Ни одного потерянного байта. Для сравнения: в игре при шести вызовах за кадр терялся 1 байт из 50. При такой частоте потерь вероятность случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не совпадение.
Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ. Нужно опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за 60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
Где взять частоту: логический тик = 60 мс, из них ~18 мс занято
работой и ~42 мс процессор простаивает внутри gfx_wait_vsync, опрашивая
луч. Опрос там стоит ноль и покрывает две трети периода с запасом.
Остаётся слепым только тело одного accel-блита под DI (до ~650 мкс) —
разорвать его нельзя (см. «что НЕ делать»).
Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30 нажатий в секунду»). Частота опроса определяется НЕ темпом нажатий, а темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Клавиатура выдаёт байты со скоростью провода: 11 бит на байт при ~10–16 кГц = ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем раз в ~1.5–2 мс (0.5 мс взято с запасом). Даже ОДНО нажатие в секунду переполнит FIFO, если в эти несколько миллисекунд его никто не разгребает. Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости. В норме разгребает прерывание; опрос понадобился только потому, что ~44 % импульсов здесь теряется.
Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
ускорять разгребание. BIOS $EA (FN_KBD_OUT, docs/new/09-input.md
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
- Scan Code Set 3 — нет ни «fake shift», ни
E0-префиксов: make = 1 байт, break = 2; - либо хотя бы отключить typematic (
0xF5/0xF7).
Но проверить это в MAME НЕЛЬЗЯ: в sprinter.cpp подключено только
направление клавиатура→SIO (m_kbd->out_data_cb() → rxa_w); обратный путь
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
кандидат на «когда дойдём до реального железа».
Что сделано по этому плану (2026-08-01):
- ✅ Idle-хук в libbgi:
gfx_set_idle_hook(fn), вызывается в цикле ожидания луча внутриgfx_wait_vsync. Приложение ставит тудаkbd_raw_poll. Полезен не только нам — любой программе даёт «качать» что-то в ожидании кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова нужен push/pop, а сам таймаут в итерациях станет длиннее по времени. Важно про цену: это НЕ новая нагрузка. Опрос ставится ровно туда, где процессор и так впустую крутитin a,(#0xFE)— 42 мс из 60. Ограничитель области, если «постоянный опрос» не нравится: потери случаются ТОЛЬКО при зажатом модификаторе (замерено), значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt. - ✅ Перемерено в roomtest тем же счётным методом: 35/35, потерь нет.
- ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило. Держать в уме, если на реальном железе или на более тяжёлых сценах (несколько стражей) потери появятся снова — накрыть блиты дешевле, чем изобретать что-то новое.
- ⏳ Ручная проверка пользователем — «стало значительно лучше, иногда всё ещё пропускает»; остаток отложен (см. врезку в начале записи).
Куда смотреть дальше, если плотного опроса не хватит.
- Драйвер MAME — НЕ ТРОГАЕМ (решение пользователя: пересборка MAME на
его машине занимает часы). Для протокола, подозрение осталось:
sinclair/sprinter.cppдержит запрос от клавиатуры ровно 32 такта CPU, иm_irq_off_timer— один на два источника (irq_on()экрана заводит его же,irq_off()гасит разом обе линии). То есть кадровое прерывание способно обрезать клавиатурный импульс — правдоподобное объяснение «44 % пропущенных импульсов». - Реальное железо. Если п.1 — чисто эмуляционный артефакт, на железе проблемы может не быть вовсе. Проверять при первом прогоне на живом Sprinter.
Про совпадение кадрового и клавиатурного прерываний (вопрос
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
0FFh, различать по биту приёма байта в порту клавиатуры — «не пришёл,
значит экран»; совпадение возможно, но «исключительно редкий случай»
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
делает ровно предписанное. Известный побочный эффект: при совпадении мы
обслуживаем клавиатуру и reti, пропуская кадровую цепочку и DSS — на
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка.
Что НЕ делать (проверено, стоило времени):
- Снимать
diв accel-ядрах libbgi нельзя. Патчdi→nopв_bgi_blit_cols_raw/_bgi_heal_rows_raw/_bgi_blit_rows_rawпрямо в памяти уронил машину в перезагрузку. То есть режим «акселератор работает при EI» изdocs/new/06-accel.md §6.6в этой прошивке/эмуляции недоступен — вопрос закрыт артефактом, а не рассуждением. - Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
L5-SHADOW. Тень уровня 5: крадёт зелье — СДЕЛАНА 2026-08-11
Единственная новая механика уровня 5 (тайлов новых нет вовсе, см. цель выше). Тень появляется в комнате 24, дожидается, пока откроется дверь, идёт к зелью, выпивает его и уходит за левый край. Боя нет.
Как это в оригинале (всё сверено по коду, custom->* — это дефолты 1.0):
| что | где | суть |
|---|---|---|
| появление | check_shadow, seg002:0064 |
при СМЕНЕ КОМНАТЫ: если current_level == 5 и drawn_room == 24, и тайл (кол 3, ряд 0) всё ещё tiles_10_potion — породить тень |
| порождение | do_init_shad, seg002:0000 |
memcpy(&Char, init_shad_5, 7) + seqtbl_offset_char(2 /*stand*/), charid = charid_1_shadow, demo_time = 0, guard_skill = 3, guardhp_* = 4, saveshad() |
| данные | init_shad_5 |
{0x0F, 0x37, 0x37, 0, 0xFF, 0, 0} = frame 15, x 55, y 55, direction 0, curr_col −1, curr_row 0, action 0 |
| поведение | autocontrol_shadow_level5, seg002:1157 |
в комнате 24: пока demo_time == 0 — ждать, пока дверь (кол 1, ряд 0) не откроется (modif >= 80), затем demo_index = 0; дальше каждый кадр do_auto_moves(shad_drink_move); при Char.x < 15 — clear_char() |
| движения | do_auto_moves, seg002:1089 |
крошечный интерпретатор: demo_time++, по таблице {time, move} выбирается запись, move = 0 nothing / 1 forward / 2 backward / 3 up / 4 down / 5 up+forward / 6 shift / 7 move_7; −1 = ничего, −2 = конец |
| таблица | shad_drink_move (data.h:866) |
{0x00,0} {0x01,1} {0x0E,0} {0x12,6} {0x1D,7} {0x2D,2} {0x31,1} {0xFF,−2} — 8 записей по 2 байта |
Что из этого у нас уже есть:
- механизм «спецсобытие порождает персонажа в слоте соперника» —
pop_check_skel(guards.c, зовётся изroomtest.cв тике); тень уровня 5 садится на тот же шов, только условие другое; - тень как
charid_1_shadow— заведена под уровень 4 (L4-MIRROR): своя ветка ИИ (autocontrol_shadow+autocontrol_shadow_level4), выбор таблицы кадров Кида (pop_frame_tbl_is_guard), отрисовка спрайтами Кида; - питьё зелья —
SEQ_78_DRINKиget_itemвpop_ctrl.c(Кид уже умеет); тень «нажимает» те же кнопки через автодвижения; - зелья как trob — фаза пузырька, тип в старших битах (
pop_trob.c).
Что писать:
do_auto_moves+ таблицаshad_drink_move+demo_time/demo_index— интерпретатор ~30 строк, кладётся рядом сautocontrol_shadowвguards.c(банк 1). «Движения» — это те же переменные управления, что заполняетread_user_control(pop_ctrl.c), так чтоmove_*сводятся к присваиваниям.do_init_shad(init_shad_5, seq stand)— общий порождатель тени; пригодится и на уровнях 6 и 12 (init_shad_6,init_shad_12— те же 7 байт).- Ветка
check_shadowдля уровня 5 — по образцуpop_check_skel, вызов из того же места тика. autocontrol_shadow_level5.- Ветка ТЕНИ в
check_guard_fallout(seg002:0241): тень падает, только если она в свободном полёте (action == 4), и тогдаloadshad(); clear_char(); saveshad(). Сейчас вpop_guard_fallout(pop_guard.c) есть ветки стража и скелета, а тени нет — комментарий там обещает её «вместе с L3-SKEL», но она относится именно к тени.
Чем подтверждать: smoke уровня 5 — дойти до комнаты 24, увидеть, как
тень выходит после открытия двери, выпивает зелье (тайл зелья исчезает) и
уходит влево. Сверять последовательность движений с живым SDLPoP на том же
месте — таблица shad_drink_move короткая, расхождение будет видно сразу.
Оговорка по виду: тень пока рисуется обычной копией спрайтов Кида, то есть выглядит вторым Кидом — это отложенный вопрос, к механике уровня 5 отношения не имеет.
GUARD-PHYS. Страж живёт по тем же правилам, что Кид — ЗАКРЫТА 2026-08-11
Что уже работает (решение пользователя: переносим физику на
Char, без предварительных замеров — иначе третий-четвёртый экземпляр той же логики неизбежен).
- физика переведена на
Char:pop_map.cцеликом работает с активным персонажем, а кто вChar— решают окнаloadkid/savekidиloadshad/saveshad, как в оригинале (seg006:809). Два входа:pop_phys_tick(порт хвоста play_kid_frame) иpop_guard_phys_tick(порт play_guard_frame) — списки вызовов отличаются ровно тем, чем в оригинале;take_hpстал общим (pop_take_hpв резидентеpop_guard.c): урон идёт тому, кто вChar, поcharid. Раньше у боёвки и у физики были свои копии, причём у физики неверная — правилаhitp_currмимо дельты и про не-Кидов не знала;- порт веток по charid в
land()(seg005:173): страж гибнет с двух рядов, тень падает как с одного, у не-Кида приземление даёт боевую стойку;check_guard_bumped(seg004:0522),droppedout+guard_follows_kid_down(seg002:09F8),check_guard_fallout(seg002:0241);pop_savekid_stateснова полноеKid = Char, аpop_load_fram_det_colпересчитывает колонку ЛЮБОМУ персонажу — обе заплатки существовали только потому, что физика знала одинKid.Проверено:
tests-host— трассы Кида не изменились ([phys] ok: 1723, тот же эталон), новый наборt_char(32 проверки) покрывает ветки по персонажам; в MAME проверено, что игра жива (респавн, бег, падение, приземление). Цена:_CODE+80 Б, банк 3 (pop_map) 7973 → 8485 (51.8 %), банк 1 (guards) 2042 → 2156.Проверено пользователем 2026-08-07: страж СПРЫГИВАЕТ ЗА КИДОМ на ряд ниже — связка «ИИ + физика» работает вживую, не только в тестах.
follow_guardпортирован и проверен в MAME (2026-08-07): уровень 1, бой в комнате 3, Кид отступает влево — страж приходит следом (Guard.room3 → 2, X перенесён через шов), ровно как в SDLPoP. Условия отбора покрыты тестамиt_char(7 сценариев: пороги 91/165, «не бой», мёртвый, вверх/вниз, занятая соседняя комната). Сцена вскрыла отдельный баг — BUG-SWORD-GHOST-1: при переходе в бою Кид прячет меч и дальше дерётся пустой рукой.Осталось (потому и запись открыта) — ревизия 2026-08-11 по коду:
— сделан вместе с L3-CHOMP (check_chomped_guardpop_map.c);- ветки
check_guard_fallout: скелет сделан (возрождается в комнате 3,pop_guard_falloutвpop_guard.c), ветки ТЕНИ нет — падает только в свободном полёте,loadshad/clear_char/saveshad; идёт в L5-SHADOW п. 5 (комментарий в коде обещает её «вместе с L3-SKEL» — устарел, это про тень);- страж, нажимающий напольную кнопку, вживую не проверялся (код — общий
check_press).
Почему это была ОДНА физика, а не «сделаем стражу свою». В оригинале
слот Guard — не «стражи», а все НЕ-Киды: тем же play_guard_frame ходят
страж (charid 2), скелет (4, ур. 3), тень (1, ур. 4/5/6/12),
визирь-Джаффар (ур. 13), толстяк (FAT, ур. 12) и мышь (0x18,
ур. 8) — tbl_guard_type = {0,0,0,2,0,0,1,0,0,0,0,0,4,3,−1,−1}, а
autocontrol_opponent (seg002:628) разводит их ТОЛЬКО по ИИ. То есть
второй экземпляр логики пришлось бы делать не один раз, а пять.
Гард по X: страж не уходит САМ — но его МОГУТ ПЕРЕНЕСТИ. Поправка к формулировке, которая была здесь раньше («страж не покидает комнату ни в каком виде») — она неверна, контрпример дал пользователь: в SDLPoP страж из комнаты 3 оказывается в комнате 2 вслед за отступающим Кидом.
Разделять надо два разных механизма:
-
своим ходом — не может. Физика персонажа слота Guard обёрнута
Char.room == drawn_roomиChar.x >= 44 && Char.x < 211(seg000:1252), и никакогоcheck_leaveв его списке вызовов нет. Провалившегося ниже комнаты убираетcheck_guard_fallout(seg002:0241) — вниз он не уходит. -
следом за Кидом — переносит движок.
exit_room(seg002:03C7) вызывается ПОСЛЕ того, как комнату сменил Кид, и решает судьбу стража:if (Guard.alive < 0 && Guard.sword == sword_2_drawn) { // жив и В БОЮ if (guards_tile[kid_room−1] >= 30 || // в новой комнате guards_seq_hi[kid_room−1] != 0) { // своего живого нет if (ушёл ВЛЕВО) { if (Guard.x >= 91) leave = 1; } // страж далеко — остаётся else if (ВПРАВО) { if (Guard.x < 165) leave = 1; } else if (ВВЕРХ) { if (Guard.curr_row >= 0) leave = 1; } // всегда → не идёт else { if (Guard.curr_row < 3) leave = 1; } // вниз → не идёт } else leave = 1; } else leave = 1; leave ? leave_guard() : follow_guard();follow_guard(seg002:039E) стираетguards_tileв ОБЕИХ комнатах (0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража черезgoto_other_roomв окнеloadshad/saveshad.
Что это значит для нас. У нас в enter_room (roomtest.c:265) стоит
безусловный pop_guard_leave() — то есть всегда ветка leave_guard, и
страж ВСЕГДА остаётся. Портировать надо сам exit_room-выбор: условия
«жив + меч вынут + в целевой комнате нет своего стража + он у нужного
края» и follow_guard. Пороги 91/165 — это «страж у того края, в который
ушёл Кид»; вверх и вниз оригинал не пускает никогда.
Чем подтверждать: уровень 1, комната 3 — начать бой, отступить влево в комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ живой страж — переход не происходит.
L6-SHADOW. Тень уровня 6 роняет решётку — СДЕЛАНА 2026-08-11
Сцена (уровень 6, комната 1). Кид жмёт opener (1,7) — решётка (1,2)
поднимается; он разбегается, прыгает через четырёхтайловую пропасть и
цепляется за порожек решётки, чтобы подтянуться. В этот момент тень делает
ОДИН осторожный шаг с границы (1,0)-(1,1) на closer (1,1), и решётка
падает. Обе плиты комнаты ведут на один тайл 12 (проверено по LINKLOC).
Порт. Две точки, обе в seg002.c:
check_shadow(seg002:0090): условий на содержимое комнаты нет — тень встаёт при КАЖДОМ входе в комнату 1.do_init_shadпереписан под таблицы (init_shad_5/init_shad_6— первые 7 полейChar, какmemcpy(&Char, source, 7)у оригинала);autocontrol_shadow_level6(seg002:1064):Kid.frame == 43(кадр бег-прыжка) иKid.x < 128->move_6_shift()+move_1_forward(). Это Shift+вперёд, то есть ОСТОРОЖНЫЙ ШАГ, а не прыжок; кадр 43 — условие на КИДА, тень им только триггерится.
Звук sound_25_presentation не портирован (звука нет вовсе); флаг
оригинала leveldoor_open = 0x4D не воспроизводим — чужую переменную
занимать незачем.
Проверено в MAME (пользователь, 2026-08-11): «Кид прыгнул, зацепился,
Тень сделал шаг, решётка упала». Тем самым закрыт и остаток
GUARD-PHYS: нажатие плиты НЕ-Кидом работает
(check_press в guard_phys, без гейта по charid).
Фон комнаты сверен с оригиналом методом из
BUGS_CLOSED.md: 539 пикселей
расхождения, и все — силуэты персонажей (тень пока рисуется палитрой Кида,
см. ../docs/shadow_render.md).
Цена: банк 1 2755 -> 2877 Б, резидент без изменений.
Переход 6 -> 7 падением — СДЕЛАН 2026-08-11
Уровень 6 не заканчивается дверью: Кид проваливается вниз из комнаты 1 и этим попадает на 7-й. Порт двух половин:
- выход —
leave_room(seg002:0504) для направления «вниз» на уровне 6 из комнаты 1 возвращает особый результат −2, а главный цикл (seg000:0893) разбирает его какKid.y = -1; ++next_level. У нас это ветка в обработкеpop_fell_out(roomtest.c), и она ОБЯЗАНА идти раньше связи вниз: у комнаты 1 сосед снизу есть (шахта, комната 3), и без спецсобытия Кид улетал туда, а оттуда — в рестарт уровня; - вход —
set_start_pos(seg003:0196): на 7-м уровне Кид ставится в комнату 17, после чего экран сразу переводится на комнату ПОД ней (goto_other_room(3): y −= 189, ряд пересчитать) — он влетает сверху.
Константы FALLING_EXIT_LEVEL/ROOM, FALLING_ENTRY_LEVEL/ROOM — в
pop_guard.h.
Проверено пользователем в MAME: «переход на уровень 7 сработал».
Остаток на следующую сессию: влетая в комнату, Кид должен уметь
зацепиться за край пола, мимо которого пролетает — этой механики у нас,
похоже, нет (см. NEXT_SESSION.md, п. 1).
HOF-ENTRY. Таблица рекордов — СДЕЛАНО И ПРОВЕРЕНО В MAME 2026-08-26
Порт двух показов оригинала. pop_hof.c был написан раньше, но никуда не
вызывался: после финала автомат уходил мимо него в title, а на титрах экран
получался ЧЁРНЫМ.
Где это в оригинале. show_title() (seg000:2060) показывает
сохранённую таблицу между credits и attract-demo — 240 тиков, переходом
слева направо, без fade. end_sequence() (seg001:5F1) после HAIL проявляет
титульную картинку (240 тиков), при подходящем результате гасит её, показывает
таблицу с золотой полосой ввода, принимает имя, ждёт 120 тиков, снова
проявляет титульную картинку и лишь потом дослушивает победную тему
(while (check_sound_playing() && !key_test_quit()), seg001:637).
Два корня, из-за которых экран был пуст.
story.pal— 256 записей, и запись 0x3F (цвет глифов шрифта,POP_FONT_COLOR) в ней ЧЁРНАЯ. Текст рисовался, но был не виден. Теперь генератор кладёт в 0x3F золотой 0xB7 оригинала.- Полосовой переход копирует страницу АКСЕЛЕРАТОРОМ, а тот читает ОЗУ-копию
(
gfx_copy_page: «копируется чистый фон без спрайтов»).GFX_BANK_SPRITE— это NOSHADOW + TRANSPARENT, то есть в копию он не пишет, и на титрах переезжал один фон без строк. Лечится банкомGFX_BANK_TRANSPARENT(0x58): та же прозрачность 0xFF, но с записью в копию — страница собирается ЦЕЛИКОМ до показа, как offscreen оригинала.
Что появилось попутно.
s5в архиве PV — фон таблицы: рамка story + логотип PRINCE OF PERSIA на y=24 (HOF_POPоригинала — тот же спрайт res54, что и в титрах).- Отдельный индекс палитры под фон текстовой рамки (
POP_PAL_STORY_BG, 16).load_title_images(bgcolor)красит его в #100060 на титрах и в #800000 в финале; раньше фон был ремаплен в индекс 9, а тот занят самой титульной картинкой (8265 пикселей), и подменить его было нельзя. - Второй набор глифов в
font.atlцветомPOP_FONT_DARK_COLOR(0x3E). У SDLPoP шрифт — маска, иshow_hof_textрисует текст дважды разным цветом; у нас цвет запечён в пиксели, поэтому «другой цвет» = другой набор. Каталог SPA1 держит счётчик картинок в одном байте, поэтому тёмный набор обрезан по '_' (32..95): 190 + 64 = 254 из 255 возможных. - Общий
pop_screen_present_ltr()вpop_ui.c— им теперь пользуются и story-переход интро, и таблица, и титульная картинка финала (CUTSCENE-LTR).
Формат POP.HOF — 6 записей (как MAX_HOF_COUNT), имя до 15 символов,
XOR+инверсия в хвосте; версия 2. Проверено на образе: после ввода имени файл
128 Б, PHOF, запись читается обратно и показывается на титрах.
Что проверено в MAME (сборка LEVEL=14, вход в комнату 5 читом обхода
комнат): цепочка HAIL → титульная картинка → таблица с полосой ввода и
временем справа → ввод имени → титульная картинка; на следующем запуске
таблица показывается между credits и demo.