b0e7130d0bcefa19fc8664243a21eecbbdff7d1f
88 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a3c5c600da |
Таблица рекордов: оба показа оригинала, ввод имени, переходы полосами
pop_hof.c был написан, но никуда не вызывался: после финала автомат уходил мимо него в title, а на титрах экран получался чёрным. Теперь это хвост end_sequence и шаг show_title между credits и attract-demo, как в оригинале. Два корня пустого экрана: - story.pal — 256 записей, и запись 0x3F (цвет глифов шрифта) в ней чёрная: текст рисовался, но был не виден. Генератор кладёт туда золотой 0xB7; - полосовой переход копирует страницу акселератором, а тот читает ОЗУ-копию, куда прозрачный блит текста не пишет. Строки рисуются после перехода, прямо в видимую страницу. Попутно: - s5 в архиве PV — фон таблицы (рамка story + логотип на y=24, HOF_POP); - отдельный индекс палитры под фон текстовой рамки: финал красит его в #800000, титры оставляют #100060 (load_title_images(bgcolor)). Прежний ремап в индекс 9 подменить было нельзя — им нарисована сама титульная картинка; - второй набор глифов в font.atl цветом 0x3E: у SDLPoP шрифт маска и show_hof_text рисует текст дважды разным цветом, у нас цвет запечён в пиксели. Каталог SPA1 держит счётчик в байте, поэтому набор обрезан по '_'; - общий pop_screen_present_ltr(): им теперь пользуются story-переход интро, титры, таблица и титульная картинка финала. Заодно вернулся пропущенный шаг show_title — экран с логотипом и Jordan Mechner между «свадьбой» и титрами (BUGS_CLOSED#cutscene-ltr). Формат POP.HOF — 6 записей, имя до 15 символов, версия 2. Проверено в MAME на сборке LEVEL=14: HAIL → титульная картинка → таблица с полосой ввода и временем справа → ввод имени → титульная картинка; на следующем запуске таблица показывается между credits и demo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0b15ed7b8f |
Разгрузка W1: смена уровня и служебный слой — в банк 8
_main занимал 3658 байт резидента, и добрая половина этого — события «раз в партию». Вынесены целиком: - pop_level_change — смена уровня (катсцена, гашение, загрузка, палитра, звук): 61 строка, наружу осталось одно условие и вызов; - pop_frame_ui — меню, F10 и QuickSave/Load: 58 строк, плюс туда же уехали edge-состояния клавиш и сами коды клавиш (снаружи они больше не нужны). _CODE 24102 -> 23626, запас W2 вырос с 86 до 559 байт. Заодно три правки по замечаниям: - затемнение фона под меню считалось числом «2», а шкала ступеней выросла с 4 до 32 — фон почти не гас. Теперь доля от шкалы (POP_PAL_DIM_BLACK/2); тем же прошлись по остаткам pop_pal_apply(4); - в катсценах персонажи появлялись только после fade in: первый кадр рисуется ПОД чёрной палитрой, и картинка проявляется целиком, вместе с актёрами (cut_scene_8/9/12_short/ending, pre_room_animated, intro_pv_animated); - отладочная полоса по умолчанию выключена. Host-тесты: 15 наборов. Проверено в MAME: меню открывается и закрывается, фон под ним затемнён, полосы нет. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4a939e42f8 |
Катсцены: тайминги, часы и «время вышло» — по оригиналу; тумблер музыки
ТАЙМИНГИ. Треки заставок между уровнями длиннее самих сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает: load_intro гасит экран и возвращает управление, а звук продолжает играть поверх загрузки уровня и первых секунд игры (seg001:690). Мы глушили CBL на выходе — музыка обрывалась на полуслове. Больше не глушим. «ВРЕМЯ ВЫШЛО». Оригинал (seg001:04D3) не зовёт ни одного init_*: на экране пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка нет). Мы показывали готовую композицию с принцессой и полными часами — противоположное по смыслу. Теперь это живая сцена без персонажей, 102 кадра, и трек 36 дослушивается под чёрным экраном, как в оригинале. ПЕСОЧНЫЕ ЧАСЫ. Сцена «времени мало» (cutscene_12) шла статической композицией, куда упаковщик запёк res953 — ПОЛНЫЕ часы. Отсюда и наблюдение «чем меньше времени, тем полнее часы»: картинка была одна и та же независимо от таймера. Сделана живой (cut_scene_12_short: принцесса стоит правее, через два кадра оборачивается), часы берутся от pre_hourglass_state. Заодно: при state 7 струйка песка не рисуется. МУЗЫКА — СВОЙ ТУМБЛЕР. Settings -> MUSIC, Ctrl+M и статус в debug bar. Флаг pop_mus_want отдельный от звукового: музыка у нас поток с диска, и выключают её по другим причинам, чем эффекты; выключение обрывает текущий трек, эффекты продолжают звучать. Строка-заглушка GAMEPLAY PROFILE («VANILLA ONLY») уступила место MUSIC — на десятую строку экрана не хватает. Подписи тумблеров в debug bar сокращены до S:/M:/I:. БАГИ. После меню персонаж оставался невидимым до первого движения: снимок слота продолжал утверждать, что кадр на странице уже нарисован. Общая pop_cd_forget() теперь зовётся после меню, QuickLoad и переворота экрана. «QUICKLOAD» мигал через кадр — pop_status_show_now снимал бит видимой страницы, а её успевало переписать восстановление комнаты. Host-тесты: 15 наборов. Проверено в MAME: debug bar, выход из меню. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7e6b38cca6 |
Музыка по ходу игры: смерть, зелья, меч, конец уровня, катсцены
Звуки 24..43 в наборе оцифровки пустые — в оригинале это Adlib-музыка, и в digisnd её нет. На этом и построено подключение: pop_sfx_play для звука с нулевой длиной кладёт номер в pop_mus_req, а разбирает заявку pop_music_service() — один вызов на кадр из любого цикла (игра, интро, катсцена). Всё чтение с диска живёт там. Ждать полной загрузки джингла нельзя — это фриз посреди игры. Поэтому pop_music_stream читает первую страницу (33 мс) и сразу пускает трек: она звучит 1,5 с, а следующая читается те же 33 мс. Остальные доливаются по одной за кадр. Расставлено по местам оригинала: смерть (24/28/32), вступление первого уровня и тень шестого (25), сцены перед уровнями (27/35/40), встреча с Джафаром (29), зелья (30/33/39), время вышло (36), меч и смерть стража (37), смерть Джафара (43), конец уровня (41/32), встреча с принцессой (26). Темы «раз за уровень» сбрасывает pop_music_level_start. Упакован 21 трек (2,4 МБ). Не озвучен только финал: won — 115 с и 78 страниц EMM, ему нужен кольцевой стриминг (задача MUS-WON на доске). Проверено в MAME: вступительная тема первого уровня отыгрывается на старте (курсор дошёл до конца, id 25). Host-тесты: 15 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ffb59b3470 |
Статус-строка: время, отсчёт ##:## в отладочной полосе, смерть как в оригинале
* Порт show_time() целиком: «N MINUTES LEFT», «N SECONDS LEFT» (у единственной секунды свой total=12, как у оригинала), «TIME HAS EXPIRED!». Флаг pop_show_time — это is_show_time: взводит и само ядро таймера (круглые пятёрки, каждая секунда последней минуты), и старт уровня, и читы времени. Значение 2 = «перебить текущую строку», как оригинал делает в последнюю минуту и после читов. * Отладочная полоса показывает оставшееся время ##:## у правого края. На табло уходит minutes-1: rem_min у оригинала — НОМЕР идущей минуты, а не остаток целых (старт 60 при rem_tick 719 = «почти 60:00»). Секунды считаются делением раз в 12 кадров, а не каждый кадр — это единственное деление на кадровом пути. * Смерть Кида по образцу оригинала: строка «Press Button to Continue» с 7-го кадра (Kid.alive > 6), 288 тиков = 24 секунды, последние 72 тика мигание с периодом 12 и звуком 38 на появлении. Промолчал — start_game (title), нажал Enter/Shift — рестарт уровня. Авто-респавна по таймеру больше нет. Отпускания клавиш оригинал не ждёт, а мы ждём: Shift у нас клавиша действия — см. impl_diff.md. * POP_APP_PLAYING принимает EV_RESTART_INTRO: start_game зовётся прямо из геймплея, раньше это событие принимал только pause menu. * expired() загейчен уровнем 13 (seg000:910). Время на уровне Джафара идёт до открытия двери, но КОНЧИТЬСЯ игра там уже не может — у нас этого гейта не было, и партия завершалась. Бюджет: _CODE 23979, куча 263 Б. Host-тесты: 15 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
47a4b084c4 |
Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
(UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
ABOUT. About показывает тот же текст, что стартовый Sprinter screen,
минус строка про клавиши — общий about_text(), чтобы экраны не
разъехались. CONTROLS собирается таблицей и центрируется по
фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
(SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
а не пункт меню. POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах. Нижняя:
порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
24 тика). Верхняя отладочная переведена с палочек на текст
«Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
своим форматированием чисел (без printf и без деления).
Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
не растёт, весь рендер в банке. Бюджет после правок не изменился
(_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
не блитятся вовсе, а вход в комнату заливает только игровое поле
(pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
статус-строке: геометрия, семантика text_time_total как идентификатора
сообщения, мигание, рестарт по истечении 36/288.
Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f4b4852d51 |
QuickSave F6/F9 в roomtest; bank_load_file/bank_save_file/gfx_w0_page_prepare; sprinter-cc: авто n_banks
roomtest: - QuickSave/QuickLoad (F6/F9): снапшот 'POPQ' v3 в POP.SAV/POP.BAK на HDD, транзакционная запись (POP.NEW -> rename, откат при ошибке), XOR-контрольная сумма payload'а; сериализация всех игровых переменных через W0-примитивы pop_qs_*; pop_qsave_process() на границе кадра вне Char-окон - pop_qsave_restore_room(): полная перезагрузка комнаты после загрузки (карта/края/швы, сброс bake-кэша, перерисовка обеих страниц, инвалидация кэшей спрайтов и HP) - сериализаторы в pop_map/pop_loose_mob/pop_trob/pop_guard_ai (+ восстановление инвариантов: mobs_live, trob_drawn, redraw) - immortal-чит 2 уровня: уровень 2 поглощает только малый урон Kid'а libc/libbgi: - bank_load_file()/bank_save_file() — резидентное файловое I/O в банк, без правила W3 (путь читается до переключения страницы) - gfx_w0_page_prepare(page) — подготовка W0-окна (IRQ/NMI-стабы) одной функцией; atlas_load.c и roomtest переведены на новые примитивы; ручные ISR-стабы удалены sprinter-cc / сборка: - --bank N=FILE.c: автогенерация n_banks (_n_banks_auto.c), ручные const n_banks удалены из тестов - roomtest/app.mk: ресурсы через stamp-файлы (.resource-stamps/) — один запуск упаковщика на группу вместо N под -B; HDD_PACK_ARGS |
||
|
|
c6828c0ad1 |
Ворота PoP: правильный гейт слышимости, звук «решётка встала», playsound кнопки
Проверка на сцене 9/9 (кнопка 1,8; челюсти 1,2; ворота — в комнате 4, тайл 1,9) вскрыла три расхождения. 1. Гейт слышимости стоял как «ворота в текущей комнате», а у оригинала (play_door_sound_if_visible, seg007:1239) слышны ещё и ворота в комнате СЛЕВА, если они в колонке 9; и НЕ слышны в колонке 9 своей комнаты; плюс особый случай «уровень 3, комната 2». Сцена 9/9 — ровно первый пункт, поэтому спуск решётки молчал. Подъём совпадал, потому что звук открытия идёт без гейта — эта асимметрия и была подсказкой. 2. Потерян звук 7 «решётка встала»: gate_stop (seg007:05E3) зовётся из трёх мест animate_door и каждый раз играет его через гейт. У нас во всех трёх стояло только type = -1. 3. У trigger_button оригинала есть параметр playsound, нулевой в трёх местах (вход на уровень, выход Джаффара, зелье «открыть»). Добавлен. Щелчок кнопки слышен через раз — это НЕ баг: prio 0x66 против 0x10 у челюстей, а укус занимает 465 мс из цикла 1229 мс (замерено). Разбор с цифрами — sound_plan.md §12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a630568a8b |
Звук PoP: приоритеты и перебиваемость вместо «всегда перебивать»
Пользователь услышал расхождение с SDLPoP: у нас решётка обрывалась приземлением Кида, в оригинале доигрывает до конца, а приземления не слышно. Оказалось, упущен целый механизм. play_sound (seg000:12C5) НЕ играет, а только номинирует кандидата на кадр — из нескольких выживает важнейший (меньше prio = важнее, при равенстве последний). play_next_sound (seg000:1304) раз в кадр решает, запускать ли: можно, только если ничего не играет ЛИБО текущий помечен перебиваемым и новый не менее важен. Иначе номинант выбрасывается — очереди в оригинале нет. Отсюда всё, что слышно: gate_closing_fast неперебиваем и доигрывает целиком; челюсти (prio 0x10) всегда важнее решётки (0x32), поэтому решётка звучит только в паузах между укусами. Таблицы из SDLPoP с учётом fix_sound_priorities (в его config.h он определён безусловно). Створка двери уровня — единственная запись, правимая на ходу, вынесена в отдельный байт. Добавлен пропущенный stop_sounds на завершении открытия двери (seg007:455). Проверено записью MAME: старт уровня 1 был 135+210 мс (решётка, обрезанная на 80 мс), стал один всплеск 455 мс с корреляцией огибающей +0,889 со звуком 6. Заведён BUG-SND-FIRSTRUN: искажение первого эффекта при первом запуске после загрузки системы — вероятно, лечится _cbl_prime, но проверить можно только на железе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8ea4c32e51 |
Звуковые эффекты PoP: оцифровка оригинала через CBL
Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 — размер блока запроса CBL, поэтому ни один блок не пересекает границу страницы и проигрыватель не знает слова «стык». Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих мест физики и из play_seq. Тишину льём свою (первый блок набора), а не через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная. Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и железо крутит хвост буфера — на слух скрежет. Разведены все места play_sound() SDLPoP, у которых есть оцифровка (id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч, зеркало, скелет, зелье. Таблица соответствий — docs/sound_plan.md §8. Музыкальные id остаются с нулевой длиной до фазы музыки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0ef8c4b60e |
Бессмертие: два уровня вместо тумблера
1 — только бой: удары мечом не отнимают HP (ветка в hurt_by_sword).
2 — плюс мелкий урон: не проходят «минус деление» от падения с двух
этажей и от падающей плиты (pop_take_hp гасит count < 100).
Мгновенная смерть остаётся на обоих: пики, чомпер, падение с трёх этажей
и удар вне боевой стойки приходят с count = 100. В коде ровно два
значения урона, 1 и 100, поэтому граница точная, а не эвристическая.
Заодно ушёл костыль «снять бессмертие на время вызова take_hp(100)» в
hurt_by_sword — он был нужен только потому, что прежний чит глушил и
смертельный урон.
Клавиша I идёт по кругу 0 -> 1 -> 2 -> 0; в отладочной метке число
красных палочек = уровень.
|
||
|
|
a6e39070af |
Фиксированный логический кадр по лучу + режимы FASTEST/FAST/NORMAL
Период стал max(n, ceil(W)) вместо ceil(W)+2: три gfx_wait_vsync после работы отсчитывались от её КОНЦА, поэтому бюджет кадра был один растр. Теперь ждём от якоря начала кадра, и при n=3 бюджет 1 290 240 тактов. Счёт кадров — программный, по биту 5 порта 0xFE (положение луча), а не по кадровым прерываниям: те теряются в DI-окнах акселератора фазозависимо (замер: 0..2,8 %, на полной перерисовке три подряд). Условие точности одно — зазор между выборками меньше 64 512 тактов; точки выборки расставлены по замеру, а не на глаз. Режимы (pop_pace.h), клавиша P по кругу, дефолт FASTEST. Условие боя взято у оригинала буквально (SDLPoP seg003.c:363): Kid.sword == SWORD_2_DRAWN, а не «идёт бой». Проверено в MAME на 11/15: счётчик без недосчёта на 270 кадров, период ровно 3 растра на 302 логических кадрах (ни длиннее 3,1, ни короче 2,9), NORMAL даёт ровно 4, с вынутым мечом — ровно 5. |
||
|
|
f47d79ded8 |
guard_over_kid — только когда кого-то рисуем: минус 14 118
Разложил остаток цианной фазы (44 007 на трёх вызовах): pop_loose_mob_draw 978 гейт mobs_live работает guard_over_kid 14 424 трамплин в банк 8 + два objtile_at_char pop_char_skip_mask 28 605 трамплин в банк 4 + два cd_quiet guard_over_kid отвечает на вопрос «кто рисуется поверх кого», а он не имеет смысла, когда не рисуется никто. Перенёс вызов ПОСЛЕ pop_char_skip_mask и сделал условным: при skip == 3 оба слота тихие, и порядок не нужен. Перестановка безопасна: обе функции только читают, и читают разное — skip_mask снимок cd_sig, guard_over_kid габариты pop_cd прошлого кадра. Замер 11/15, лёгкая позиция: циан 55 257 -> 41 280, работа кадра 456 780 -> 442 662. Проверено в MAME: статика чистая, в бою (Кид сближается и бьёт стража) персонажи перекрываются правильно, порядок не сломался. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6faf81016a |
Замер цианной фазы: крупного лишнего в отрисовке персонажа нет
Циан 188 004 не двигался ни от P1, ни от P5, ни от P2b — разложил его зондами. Хорошая новость: Кид УЖЕ пропускается (204 такта на pop_char_draw), то есть надежда P3 сбылась после P1 — метка от чомпера до него больше не дотягивается. Страж же перерисовывается каждый кадр честно: пламя правого факела (0,7) рисуется в ячейке (0,8), где он стоит, и реально накрывает ему голову (пламя занимает y 5..22, страж 12..62). Отрисовка стража — 148 302: pop_char_fore (2 трамплина в банк 2 + обход тайлов) 62 778 42 % клинок (sword_draw + overlay_add + clip_add) 27 522 19 % блит спрайта + clip_char_right 20 982 14 % загрузка кадра и геометрия 12 696 9 % pop_clip_char_top (трамплин банк 4 -> банк 3) 8 658 6 % снимок прямоугольника + cd_clip_add 7 890 5 % gfx_w0_unmap + cd_sig_make 4 968 3 % вход + cd_heal 2 946 2 % Единственная явно лишняя статья — трамплин clip_char_top, и снять его непросто: функции нужны get_tile и таблицы деления из банка 3, перенос в резидент вернёт тот же трамплин внутрь. Остальное — работа, которую персонаж действительно делает. Зонды переставлены с уже закрытых замеров (loose_tick, физика) внутрь pop_cdraw; оснастка снимается позицией P12, когда оптимизация закончится. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f8493a4c04 |
P2b: луч видимости стража 36 786 -> 13 002 (-65 %)
Замер отделил луч от pop_frame_timers: таймеры со всеми тремя
спецсобытиями уровней стоят 1 962, луч — 36 786, то есть 5,6 % работы
кадра на девять чтений байта.
Причина оказалась НЕ в алгоритме. Сверка трёх референсов:
SDLPoP (seg003:688) — идёт по x с шагом 14 и на каждом шаге переводит x
в колонку делением. Причём сам SDLPoP признаёт в комментарии, что
«DOS PoP does this: tile_div_tbl[xpos]» — то есть оригинал брал
таблицу, а порт заменил её на / и %, потому что на 32 битах так проще.
Apple II (MISC.S CHECKALERT) — тот же алгоритм байт в байт, но перевод
x -> блок через таблицу BlockTable[x]. Ровно то, что у нас уже было
сделано (POP_TILE_DIV, 2026-08-10).
mininim — другая архитектура (тайловые позиции, своя механика), для
сравнения реализации не годится.
То есть алгоритмически мы уже были на уровне Apple II, а платили за
другое: pop_tile_at объявлен __banked, луч живёт в guards.c (банк 1), и
на КАЖДУЮ колонку шёл трамплин банк 1 -> банк 3. На сцене 11/15 (Кид в
колонке 2, страж в 8) это девять трамплинов за кадр.
Сделано:
1. луч переведён на КОЛОНКИ вместо x-координат. Это эквивалентно:
начальные x — ровно центры тайлов персонажей, а обратный перевод даёт
ту же колонку (floor((58 + col*14 - 58)/14) == col). Ушли 16-битный
шаг, 16-битное сравнение и индексация таблицы на каждой итерации;
2. тайлы отрезка забираются ОДНИМ банковым вызовом (pop_row_tiles)
вместо девяти;
3. внутри pop_row_tiles — быстрый путь для отрезка целиком внутри
комнаты: get_tile при ряде 0..2 и колонке 0..9 сводится ровно к
g_fg[row*10+col] & 0x1F, идём указателем;
4. буфер тайлов — file-scope, а не локальный массив (иначе каждое
чтение это -n(ix)).
Замер по шагам: 36 786 -> 24 048 (колонки + один вызов) -> 13 002
(быстрый путь + буфер). Синяя фаза 283 215 -> 259 500, работа кадра
654 990 -> 628 542, то есть -26 448 при ожидании -30 000.
Кэш-гейт «пересчитывать только при смене позиции» НЕ понадобился:
расхождения с оригиналом нет, луч считается каждый кадр, как и должен.
Поведение проверено в MAME: страж в боевой стойке, но не идёт — между ним
и Кидом чомпер, то есть can_guard_see_kid = 1 («видит, но не пойдёт»).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f81b30eb68 |
Замер P2 и P6: главные статьи — физика двух Char и луч видимости
Синяя фаза (286 518) разложена зондами на 12 участков, process_trobs (89 784) — на три. Гипотеза, с которой я входил в замер, ОТВЕРГНУТА. Я ждал, что дорого обходятся банковые трамплины на спецсобытиях уровней — по аналогии с pop_clip_char_top, где трамплин ради одной проверки стоит 8 892. На деле три спецсобытия (skel, mouse, killed_shadow) вместе стоят 1 650: они гейтятся внутри и на уровне 11 выходят сразу. Настоящие статьи синей: физика двух Char 105 246 (61 266 Кид + 43 980 страж) heal двух Char 67 734 луч видимости стража до 37 032 (вместе с frame_timers) pop_ctrl_tick 18 648 логика стража 19 932 Физика съедает 13,7 % работы кадра при том, что ОБА персонажа стоят и кадр позы не меняется. Цена измерена, причина нет — это отдельная позиция P2a. Луч видимости считается каждый кадр, хотя никто не двигался: гейт по смене позиции/комнаты — позиция P2b, ждём −30 000. heal отдельной правки не требует, он уйдёт вместе с P1/P3. process_trobs: префетч кодов тайлов с маппингом окна 0 — 11 058, обход самих trob'ов ~43 000 (pop_trob_modif зовётся банковым вызовом на КАЖДЫЙ trob, хотя комната одна), два факела ~36 000. Цена одного pop_pot_b измерена отдельно: 17 346, и это единственная группа в распределении — значит в кадре его зовут только факелы. Пиксели пламени 16x18 — 1 716, то есть 10 % цены. Заодно посчитано, достижим ли период 3 растра: снять надо 338 000, а сумма ВСЕХ известных позиций даёт 357 000, из которых 160 000 держатся на одной (P1). Цель достижима, но без запаса. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3f643532c9 |
Трасса kidobj для сверки с SDLPoP + разбор боя у шва
Инструмент: pop_dbg_kidobj печатает то же, что вывод, добавленный
пользователем в SDLPoP add_kid_to_objtable — tilepos/frame/act/col/cols/rows
(+ наша комната). Считает по set_char_collision и set_objtile_at_char.
Выключен по умолчанию (DBG_KIDOBJ 0), включается одним define; вывод
забирает брейкпоинт MAME на резидентном pop_dbg_trap.
Сверка подтвердила фикс
|
||
|
|
f89b7dd0d2 |
Ускорение холостого хода: указательный обход вместо arr[i] в горячих циклах
Замер зелёного блока (комната 23 уровня 13, MAME, такты эмулятора) показал, что 86% его стоимости в ПОКОЕ — это pop_loose_tick, который не делает ничего. Причина — кодоген SDCC, подтверждена чтением .asm. С `int pos` и записью pop_loose_modif[pos] компилятор держал счётчик в IX-фрейме, каждую итерацию заново складывал 16-битный адрес элемента, клал его в локал и тут же вычитывал обратно парами `pop bc / pop hl / push hl / push bc`. 40 холостых итераций (30 тайлов + 10 потолков) стоили 27 936 тактов. То же в pop_loose_mob_tick: запись mobs[i] заставляла умножать i на sizeof(mob_t)=15 заново под КАЖДОЕ поле (.active/.clean/.x/.y), 14 пустых слотов — 35 790. Правка — обход указателем, счётчик uint8_t, пустые слоты отсеиваются в вызывающем цикле (а не гардом внутри mob_tick_one, до которого надо ещё дойти). Холостая итерация стала `ld a,(de) / or a,a / jp Z` — три инструкции вместо дюжины с обращениями к памяти. Результат (такты MAME, холостой кадр): циклы по тайлам 27 936 -> 9 852 (2,8x) pop_loose_mob_tick 35 790 -> 7 416 (4,8x) pop_loose_tick 70 866 -> 24 384 (2,9x) ЗЕЛЁНЫЙ БЛОК 82 242 -> 35 760 (2,3x) Банки ужались: BANK3 -90 Б, BANK7 -74 Б. Все 8 наборов tests-host проходят; комната 23 в MAME рисуется корректно. Плюс ВРЕМЕННАЯ оснастка замера (маркеры m9..m16, pop_dbg_kind) — она же показала, что пик зелёного блока сидит НЕ в тряске плит, а в запекании тайлов; разбор продолжается, оснастку снять перед закрытием темы. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
50e4eda2ad |
Падающие плиты: передний слой по пометкам, честные спрайты, замер кадра
Слой fore переведён на пометки ОБЪЕКТОВ вместо окна вокруг персонажа — порт set_redraw_fore/redraw_frames_fore (seg007:0550, seg008:214): pop_set_redraw_fore + pop_fore_needed + pop_fore_tile_b, пометки ставит падающий кусок вокруг себя (draw_mob, seg007:1147). Ключевая правка: пометки соседа считаются в ПРОХОДЕ ОТРИСОВКИ, а не в тике. Стояли в цикле тика — то есть по позиции ДО mob_tick_one, а кусок рисовался уже по новой; пока он летит внутри ряда, тайлы совпадают, но в кадр пересечения границы пометка указывала на прежний тайл и колонна кусок не перекрывала. Ровно один кадр, как и наблюдалось. Спрайты падающего куска: obj_id = 10 (add_mob_to_objtable, seg007:1170), части берутся по этому индексу — 70/74/72, а не 41/43/42 (плита В ПОКОЕ). Отсюда была «цельная ровная плита» вместо двух половин со сдвигом. Убрана подпорка «перерисовать соседний тайл поверх куска»: тащила на плиту чужой узор и окно и стоила по полной отрисовке тайла на кусок за кадр. Коридор heal сужен по высоте 32 -> 24 (реальный след спрайта 16 px). gfx_set_bank вынесен из потайлового цикла в обрамление прохода. Выяснено и зафиксировано: clip.right = 40 в add_mob_to_objtable — МЁРТВЫЕ данные. set_clip_rect вызывается под chtab_flip_clip[chtab_id], а для окружения флаг равен нулю — оригинал падающие куски не клипует вовсе. Портировать нечего; две попытки это сделать были ошибочны. Замер кадра (маркеры m1..m4, оставлены временно для проверки правок): покой 196 614 тактов (15,2 % кадра при периоде 1 290 000), во время падения гряды среднее 222 667, пик 1 428 990 — то есть 111 % кадра, шесть кадров подряд за пределом. Логика неизменна (88 020), растут фон (до 887 376) и спрайты (до 660 792). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
67d62e3e8b |
Уровни 12 и 13: тень, Джафар, падающие плиты + разбор слоёв
Уровень 12 (тень) — порт seg002/seg006: - check_shadow: подъём тени в комнате 15 по условию «меч подобран», init_shad_12, вход падением (seq 7); - autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ; - общий урон (ранил тень — ранил себя) и check_killed_shadow (убил тень — убил себя); - таймер вспышки слияния 42 -> -1, sword_disappears при уходе из комнаты 18, появление плит в комнатах 2/13 после слияния. Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт сбросить HP. Чит навигации по комнатам этот триггер придерживает — иначе комнату 23 двенадцатого уровня не посмотреть в принципе. Уровень 13 (Джафар): guard_notice_timer (фора после встречи), on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три исключения loose-механики. Плюс спрайты визиря VIZIER.DAT — без них он рисовался обычным стражем; заодно цвет из данных комнаты применяется только к обычному стражу, как в оригинале. Падающие плиты — семь дефектов, найденных прогонами: - MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние терялись без щебня; - одиночные сигналы посадки/провала больше не затираются в кадре; - честный loose_fall: сбитая плита рождается в том же кадре, от места удара, с половинной скоростью; - при снятии плиты метится и сосед справа (висел передний торец); - heal и отрисовка кусков разнесены на два прохода; - куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y (compare_curr_objs для пары 0x80 сортирует наоборот); - wall_pattern убран из ceil_over_kid_tile: у оригинала узор из draw_tile_bottom идёт в фон, а не в передний слой. Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44). TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам. Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами тени при слиянии, чит навигации не запускает бесшовный переход, чит K убивает через штатный путь смерти. Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER, GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
948d8f08f2 |
Переворот: перерисовка вместо отражения, переключение на границе кадра, clip_char
Три правки по следам прогона пользователя на зелье инверсии. 1. ЛИШНИЕ ЯЗЫКИ ПЛАМЕНИ. pop_flip_screen отражал уже нарисованное копией акселератора, а пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию (pop_torch_draw: heal'а у него нет, следующий кадр непрозрачно накрывает предыдущий). Отражённое вместе с фоном, оно оказывалось в зеркальной позиции, где накрывать его некому — и оставалось навсегда (уходило только после входа в комнату, который строит фон с нуля). Теперь переворот ПЕРЕРИСОВЫВАЕТ комнату тем же приёмом, что вход (ENTER-ROOM-FAST). Это совпадает с оригиналом: SDLPoP на need_redraw_because_flipped зовёт redraw_screen(0), а не отражает картинку (seg000.c:928). 2. МОМЕНТ ПЕРЕКЛЮЧЕНИЯ. Зелье выпивается из play_seq, в середине кадра, и pop_upside переключался прямо там — остаток кадра рисовался зеркально поверх ещё неперевёрнутого фона. Разделены pop_upside_want (пишут зелье, смерть, чит U) и pop_upside (читают слои); переключение — одно место, начало кадра, вместе с перерисовкой. Оригиналу этого не нужно: он всегда рисует в offscreen неперевёрнутым и зеркалит только на выводе — расхождение записано в docs/impl_diff.md. 3. CLIP_CHAR В ПЕРЕВОРОТЕ. Граница clip_char приходит в ЛОГИЧЕСКИХ координатах (y_clip), сравнивать её с уже отражённым top нельзя: то, что логически выше линии, на экране ниже неё. Теперь тот же срез применяется с другого конца — укорачивает кадр снизу (bcut), верх остаётся на месте; строки ленты не сдвигаются, потому что зеркальная лента отдаёт их в обратном порядке. Раньше в перевороте клип был отключён совсем, и висящий Кид рисовался поверх плиты. Проверено в MAME: переворот чистый, фон без остатков. tests-host 6/6. |
||
|
|
5b6664ffce |
Зеркальные кадры персонажей — готовыми файлами, а не разворотом в рантайме
Замеры в MAME (такты Z80, кадр = 430 000) решили вопрос однозначно: загрузка 28 атласов Кида с HDD — 27 160 092 такта (1.26 с), 970 003 на страницу; разворот ОДНОЙ страницы на месте — 12 645 582 такта (0.59 с), т.е. 771 такт/байт. Даже идеальный вариант (копия accel'ом ~0.25 кадра + asm-реверс ~2.2 кадра) дал бы ~1.05 млн на страницу — то есть в лучшем случае сравнялся бы с чтением готового файла. На 34 страницы: 1.5 с загрузки против 20 с разворота. * Упаковщики пишут второй набор: vflip_cols/vflip_page в pop_pack_kid.py -> kid0_v.atl…kid27_v.atl, sword_v.atl; pop_pack_guard.py -> g0_v.atl…g4_v.atl ТОЛЬКО для обычного стража (на уровне 9 tbl_guard_type = 0; тень рисуется атласами Кида, скелет/визирь с переворотом не встречаются). * pop_vflip.c схлопнулся до таблицы «страница -> зеркальная пара»; весь побайтовый разворот и аллокация EMM удалены. * pop_vflip_load_all (банк 8) грузит набор ОДНИМ вызовом и откуда угодно — задел под загрузку ресурсов во время интро. Зовётся при старте и при смене уровня, если это POP_UPSIDE_LEVEL; чит U на других уровнях поднимает набор лениво (страховка в главном цикле). * Стартовый уровень стал параметром сборки: make LEVEL=N (-DFIRST_LEVEL). Проверено в MAME на уровне 9: переворот мгновенный, pop_flip_screen = 897 263 такта (2.1 кадра) — это сама копия экрана, загрузки в кадре больше нет. tests-host 6/6. |
||
|
|
147cb185b1 |
Резидент: pop_kid/pop_guard/main расколоты по частоте вызова (куча 6669 Б)
MEM-COLD3, шаги 3-5. Кандидаты выбирались не по размеру, а по тому, КТО зовёт: если единственный вызывающий уже в банке, код едет к нему и трамплин не появляется вовсе. * pop_kdraw.c -> БАНК 4 (к pop_cdraw.c): клинок в руке (pop_sword_draw) и блит спрайта chtab_2 по id (pop_kid_img_blit). Оба звал ровно pop_cdraw, теперь вызовы прямые — контракт и условие корректности в _pop_kdraw.h. * pop_kboot.c -> БАНК 8: загрузка атласов Кида и страницы kid_data.bin (раз за игру). Дескриптор страницы — _pop_kid.h. * pop_guard_cold.c -> БАНК 8: страж идёт за Кидом + вход стража в комнату. Оба зовёт roomtest_cold.c из того же банка — снова без трамплина. * Из main в банк 8 уехали pop_boot (загрузка ресурсов, палитра, первый экран), pop_level_switch (смена уровня) и pop_shutdown. FIRST_LEVEL переехал в pop_tune.h — константа нужна обеим половинам цикла. _CODE 23005 -> 19861, куча 3525 -> 6669 Б. Банк 8 — 6.5 из 16 КБ. tests-host 6/6 (в наборы добавлены eng_pop_kboot и eng_pop_guard_cold). |
||
|
|
7c1ded340c |
L9-INVERT: зеркальный слой фона + вход в комнату в банк 8
II.3: pop_upside проведён в единственную точку тайлового слоя (pop_blit_b / blit_b_clip): позиция через FLIP_TOP, блит — vflip-двойник. Этим зеркалятся полная отрисовка комнаты, точечные редрои, trob-анимации, fore-слой и кладка. В клипованном пути обрезка сверху экрана съедает нижние строки источника, поэтому кусок пересчитывается как sy' = h - sy - dh. MEM-COLD2 п.1: enter_room_side/enter_room — в банк 8 (куча 450 -> 1231 Б). Рабочие массивы комнаты остались в _DATA, в банк уехал только код. Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком, пол вверху, дверь и кладка вверх ногами. tests-host 6/6, size-check OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
70c1668e9f |
ENTER-ROOM-FAST + вынос порядка отрисовки в банк 8
Вход в комнату: одна отрисовка в скрытую страницу, показ флипом, вторая страница — gfx_copy_page(DIRECT) вместо повторной отрисовки. Процесс рисования тайлов больше не виден, обе страницы синхронны (два снимка подряд идентичны). Видимую страницу главный цикл теперь перечитывает у железа в начале кадра: её меняют и вход в комнату, и переворот — оба из банка. MEM-COLD2 п.2: guard_over_kid с хелперами (objtile_at_char/char_x_left_of/ tile_div_mod) — в банк 8. Куча 1005 -> 1574 Б, цена — один трамплин на кадр. tests-host 6/6, size-check OK, в MAME проверены старт уровня 9, переходы между комнатами и отрисовка стража. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b1ef48bd16 |
L9-INVERT: состояние переворота, зелье типа 4 и pop_flip_screen
Шаг II.1 плана. pop_upside/pop_upside_dirty (порт upside_down и need_redraw_because_flipped), ветка case 4 в proc_get_object (только тоггл — ни вспышки, ни урона, как в оригинале), сброс стартом уровня и смертью Кида. pop_flip_screen (банк 8) переворачивает уже нарисованное двумя копиями акселератора: VFLIP в скрытую страницу, показ, DIRECT во вторую; затем инвалидация слотов персонажей, метки фона и полосы HP. Чит-клавиша U — порт seg000:0793 (в оригинале переворот тоже на чите): без неё эффект не проверить, чит-телепорт до зелья в комнате 7 не дотягивается. Проверено в MAME на уровне 9: комната переворачивается целиком за кадр, повторное нажатие возвращает. Персонажи и факелы пока не зеркалятся — это шаги II.3/II.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
361c9d9f78 |
Мышь уровня 8 + холодная половина главного цикла в банк 8
Спецсобытие уровня 8 (SDLPoP seg003:0545/0ABA, seg002:07EB): когда дверь уровня открыта, а Кид остаётся в комнате 16, через 150 кадров приходит мышь, пробегает справа налево по верхнему ряду, наступает на кнопку (0,7) — решётка (0,3) открывается — и убегает. Ассетов не потребовалось: кадры 186..188 идут по таблице Кида, их спрайты (images 130..132) уже лежат в kid16.atl, отрисовка подхватывает мышь сама. - guards.c: pop_check_mouse, autocontrol_mouse, ветки мыши в autocontrol_opponent / play_guard / check_can_guard_see_kid; - pop_leveldoor_open стал word, как в оригинале (в него же считается задержка — байт переполнился бы через 255 кадров); - leave_guard больше не записывает тень и мышь в данные уровня (seg002:02F5); - полосы HP у мыши нет (seg000:1159); - tests-host/t_mouse: 17 проверок, единственный набор с guards.c. Разгрузка резидента (куча в huge упала до 879 Б): pop_start_level, find_start_level_door, чит-навигация по комнатам и лейбл номера уехали в roomtest_cold.c (--bank 8, n_banks = 8). _CODE 25653 -> 24825, куча -> 1707 Б; кадровый путь не тронут. Проверено: tests-host 6/6, make size-check OK, живьём в MAME (мышь появилась, нажала кнопку, решётка поехала вверх, мышь ушла). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d7d18aef77 |
Зелье медленного падения (перо) + цвет пузырьков по типу зелья
L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO. - pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225, ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда). fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33. proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра. - pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и удар в ветки stepfloat/bumpfloat (плавные кадры, без урона). - roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ); сброс pop_feather в pop_start_level (seg003:189). - Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе: 3/4 зелёный, 5/6 СИНИЙ, остальные красный. Mono-блиттера с параметром цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие), pop_potion_draw выбирает набор. Атлас 23 -> 37 спрайтов (+423 Б). - Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP. Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)), а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным. Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host 5/5 (1727 в [phys]), size-check OK. Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида; синее зелье без своей вспышки). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b5951c9b5c |
Зацеп за кромку нижнего ряда: do_fall пускает ряд до 3 + check_grab в midair
Уровень 7, комната 14: спуск с ряда 0 на ряд 2 через зацеп (повис на кромке кнопки (0,2), отпустил, взялся за (2,2)) не работал ни при какой фазе и задержке — Кид пролетал мимо и разгонялся до fall_y = 33, после чего окно зацепа закрыто уже по скорости. Корень: do_fall не давал curr_row выйти за 2, а check_grab целится в тайл ряда curr_row-1 — то есть ряд 2 своей комнаты становится целью только при curr_row == 3. В оригинале (seg005:0030) inc_curr_row безусловный, а get_tile для ряда 3 уходит по links.down (find_room_of_tile, seg006:005D). - pop_map.c do_fall: inc_curr_row теперь 2 -> 3; ветка «достиг y_land» гейтится по curr_row <= 2 (при ряде 3 ни in_wall, ни land звать нельзя — тайлов своей комнаты там нет, а до y_land[4] дело не доходит: комнату меняет check_leave_below на y >= 211). - pop_map.c check_action: восстановлена ветка ACT_MIDAIR (кадры 102..105, seg006:0619) — первые четыре кадра падения, где fall_y ещё не разогнан, зацеп не работал вовсе. Отсюда же «иногда цепляется, иногда нет». - tests-host/t_grab.c: регресс grab_below_room_edge_window_exists — сцена комнаты 14 + переход в 15 по pop_fell_out. До фикса ни одного зацепа, после — окно из 8 фаз X при задержках 0..5 кадров; проверяется и играбельность (зацеп при «отпустил и сразу зажал Shift»). - roomtest.c + pop_guard.h: чит-навигация ставит Кида на верхний ряд в комнате 14 уровня 7 (GRAB_DOWN_LEVEL/ROOM) — снизу этот спуск не проверить. Общее правило (снизу вверх) не тронуто: в верхнем ряду пола чаще нет и Кид проваливается сразу после телепорта. Проверено: tests-host 5/5 (55 в [grab]), size-check OK, живьём в MAME — Кид повис на кромке (2,2): frame=91 y=55 row=0 room=15. Доски: GRAB-BELOW-ROOM в BUGS_CLOSED, GRAB-KBD-TIMING в BUGS_OPEN (отложен пользователем до готовности всех уровней), план L7-FEATHER в TASKS_OPEN. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9ecea17ba6 |
Уровень 6: переход на 7-й, портал, многокомнатное падение плиты
Всё проверено пользователем в MAME. 1. Переход 6 -> 7 падением. leave_room (seg002:0504) для «вниз» на уровне 6 из комнаты 1 даёт особый результат −2, главный цикл (seg000:0893) читает его как ++next_level; вход на 7-м (set_start_pos, seg003:0196) ставит Кида в комнату 17 и сразу переводит экран на комнату под ней. Проверка ОБЯЗАНА идти раньше связи вниз: у комнаты 1 сосед снизу есть (шахта, комната 3). 2. BUG-BALCONY-RIGHT: правая половина портала не рисовалась. id 12 стоял в FORE_ENV_IDS (в tile_table он fore_id ЗЕЛЬЯ), но зелье рисуется из chtab_1, а в chtab_6 под этим номером — правая часть арки балкона 32x62, идущая в backtable. Спрайт уезжал в fore-атлас, env-страница получала дырку (idx 12: w=0 h=0). Найдено трассой POP_TRACE_BT + чтением каталога .atl; дифф комнаты с оригиналом 1151 -> 375. 3. BUG-BELOWROW-WALL: жёлтые треугольники в шахте падения. load_rowbelow (seg008:368) при отсутствующей комнате снизу подставляет tiles_0_empty колонкам 1..9 и tiles_20_wall только левому краю; у нас стеной забивались все 11 позиций. 4. BUG-MOB-MULTIROOM: честный mob_down_a_row (seg007:1387) — кусок переходит в комнату снизу и летит дальше. Плита (1,7) комнаты 6 пролетает шахту комнаты 7 и жмёт кнопку (2,7) комнаты 11, открывая ворота комнаты 18. 5. BUG-MOB-STALE-PAGE: чистка следа куска стояла под гейтом «в отрисованной комнате» — улетевший вниз оставлял себя на одной из страниц навсегда. 6. BUG-CHAR-STALE-PAGE: тень застывала на двух страницах в РАЗНЫХ позах. Прямоугольник и valid пишутся внутри `if (w && h)`, а снимок состояния брался безусловно — при нулевом спрайте слот залипал «тихим». Остаток (почему спрайт нулевой) заведён как SPRITE-ZERO-W0 в BUGS_OPEN. NEXT_SESSION переписан: следующая задача — зацеп за край пола в свободном падении (вход на уровень 7). tests-host 5/5, size-check чист. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
abe4a83d38 |
Уровень 5: тень не дерётся, кнопка в шве, узор паласа после зелья
Три бага одной отладочной сессии, все проверены в MAME пользователем. 1. SHADOW-FIGHT-L5. Кид доставал меч, а тень вставала в боевую стойку и рисовалась спрайтами стража. В pop_check_can_guard_see_kid первым множителем стояло `Guard.charid != 0`, тогда как оригинал (seg003:702) пишет `Guard.charid != charid_1_shadow || current_level == 12`: тень боевая только на 12-м уровне. Оба симптома шли из одной ветки control_standing (seg005:352). Возвращён и пропущенный множитель `Guard.direction != dir_56_none`. 2. SEAM-BUTTON-STALE. Кнопка в шве не меняла вид ни при нажатии, ни при отжатии. Сигнатура редроя шва сравнивала room_modif, а у кнопки modif — это индекс LINKLOC, константа уровня: нажатие живёт в doorlinks2, и в сигнатуру не приходило никогда. seam_row_sig подмешивает бит нажатости. Оригинал шов в этом случае не перерисовывает вовсе — расхождение осознанное, записано как D-2 в docs/impl_diff.md. 3. BUG-POTION-STRIPE. Синяя лента паласа пропадала на месте выпитого зелья и не возвращалась даже после перезахода в комнату. У оригинала curr_room_tiles — сама таблица уровня, у нас fg живёт в двух местах, и страницу правил главный цикл уже после pop_process_trobs. В этот зазор trob зелья крутил фазу пузырька поверх обнулённого модификатора: bubble_next_frame(0) = 1, а для пола в паласе modif 1 означает «узор не рисовать» (seg008:499). do_pickup теперь правит страницу уровня сразу. Тем же лечится меч: animate_sword делал `--mod[tp]` из нуля. tests-host 5/5 (добавлена заглушка pop_level_set_tile), size-check чист. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8dc53e6b57 |
L5-SHADOW: тень уровня 5 крадёт зелье (спецсобытие комнаты 24)
Порт seg002: check_shadow (0064), do_init_shad (0000), do_auto_moves (1089),
autocontrol_shadow_level5 (1157) + таблица shad_drink_move (data.h:866).
Механика. При входе в комнату 24 уровня 5, если зелье (кол 3, ряд 0) ещё
на месте, слот соперника занимает ТЕНЬ — и занимает его ВМЕСТО стража:
enter_guard в этой комнате оригинал не зовёт НИКОГДА (в данных страж есть,
guards_tile[23] = 8, но check_shadow уходит в return до него — в том числе
когда зелье уже выпито). Поэтому pop_check_shadow вернула 1 = «событие
обработано, стража не поднимать», и вызов стоит ПЕРЕД pop_guard_enter.
Тень стоит, пока не откроются ворота комнаты (openness >= 80), затем
проигрывает ЗАПИСЬ ходов: подойти, взять, выпить, развернуться, уйти за
левый край. do_auto_moves — тот же движок, что у демо-режима заставки;
тонкость выбора записи (при достигнутом времени индекс сдвигается, а ход
берётся из ещё не сдвинутой записи) портирована дословно.
Три вещи, найденные по дороге:
1. ГЕЙТ ВЫКЛЮЧЕННОГО ПЕРСОНАЖА. play_guard_frame (seg000:1248) начинается
с `if (Guard.direction != dir_56_none)`, и clear_char выключает
персонажа именно так — он не трогает charid. У нас гейт был только по
charid, поэтому ушедшая за край тень продолжала тикать: do_auto_moves за
концом таблицы отдаёт последнюю запись {0x31,1} = «вперёд», тень
разворачивалась, вбегала обратно, пробегала верхний ряд и падала в проём
(наблюдение пользователя в MAME). Гейт добавлен и в pop_guard_tick, и в
pop_guard_phys_tick.
2. ЭФФЕКТ ЗЕЛЬЯ — ТОЛЬКО КИДУ. proc_get_object (seg006:16CB) начинается с
`if (Char.charid != charid_0_kid || ...) return`: предмет забирает любой
персонаж, а эффект достаётся только Киду. На этом стоит всё спецсобытие
— тень пьёт, зелье исчезает и не достаётся никому. Без проверки
hitp_delta уходил бы Киду, то есть тень его ЛЕЧИЛА бы.
3. ВЕТКА ТЕНИ в check_guard_fallout (seg002:0241): исчезает, только если
реально падает (action == 4). Была помечена в коде как «появится с
L3-SKEL» — на самом деле она про тень.
Чит ROOMNAV: телепорт ставил Кида в первый попавшийся пол, то есть почти
всегда (0,0). Теперь ищет пол С МАКСИМАЛЬНЫМ Y (ряд 2 -> 1 -> 0, по
просьбе пользователя): в комнате 24 старая посадка была вплотную к тени, и
вместо сцены кражи начиналась схватка, которой там быть не должно.
Отладочный тумблер POP_DBG_SHADOW_NOWAIT (pop_tune.h, по умолчанию 0) —
посмотреть сцену, не проходя уровень до кнопки.
Проверено в MAME (уровень 5, комната 24): при закрытых воротах тень стоит
за левым краем и не видна; с тумблером — подошла, выпила (зелье исчезло),
развернулась, ушла за край, выключилась и НЕ вернулась. tests-host 5/5,
make size-check OK. Цена: резидент +102 Б (24637 -> 24739), банк 1
+368 Б, банк 3 +11 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8f0362f3d4 |
L4-MIRROR шаги 3 и 5 + левый клип колонок в libbgi
libbgi: новая gfx_blit_cols_part_wx — обрезка СЛЕВА (skipw) в дополнение к верхней (skip/rows) и правой (maxw). По подсказке пользователя сделано примитивом, а не обходным путём «нарисовать и вернуть фон поверх лишнего»: для column-major левая обрезка стоит ровно столько же, сколько правая — колонка это непрерывный кусок ОЗУ, меняется стартовая колонка источника и экранная X. Внутри это уже было (так клипается левый край экрана), наружу не было выведено. Полное тело блита колонками переехало туда, gfx_blit_cols_part_w стала обёрткой (skipw=0). size-check: OK, роста нет (62 программы, -22..-33 Б на пользователей блита). Шаг 5 — клип ТЕНИ (seg008:1699): на уровне зеркала она показывается только СПРАВА от него, obj_clip_left = 137 + (mirror_column-4)*32. Шаг 3 — ОТРАЖЕНИЕ (check_mirror, seg003:0798): пока Кид стоит на тайле зеркала, каждый кадр рисуется его зеркальная копия с клипом left = (curr_col<<5)+9 и top = y_clip[curr_row+1]. Отдельной функцией pop_mirror_draw, а НЕ третьим слотом Char — это структура самого оригинала: отражение идёт сокращённым путём load_frame_to_obj + add_objtable(4), без клинка, брызг, fore-прохода и пропуска кадра; гейтить всё это в общем теле значило бы добавить ветки в самый горячий путь. Свой heal (pop_mirror_heal) рядом с pop_char_heal, в skip-маске не участвует. tests-host: заглушки pop_mirror_draw/pop_mirror_heal (pop_map теперь на них ссылается), все 5 наборов прошли. Цена: _CODE 24501 -> 24629, банк 3 10551, банк 4 8267 -> 9590. НЕ ПРОВЕРЕНО В MAME. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
844fa6d767 |
L4-MIRROR шаг 4: прыжок сквозь зеркало и рождение тени
Порт seg003:0798..08A9 + seg004:0239 + seg002:081D/1131 + seg006:1945. - is_obstacle (pop_map.c): ветка зеркала — Кид, кадры бегового прыжка 39..43, направление ВЛЕВО -> modif = 0x56, pop_jumped_mirror = -1, препятствия нет (пролетает насквозь). - mirror_image / jump_through_mirror / pop_check_mirror (pop_map.c): отражённый Char уходит в слот Guard как CHARID_1_SHADOW, guardhp = hitp_max, у Кида hitp_curr = 1. savekid НЕ делается — как в оригинале, отражается только копия. Полосы HP перерисует pop_hp_draw сам. - pop_check_mirror() зовётся из главного цикла ПЕРЕД отрисовкой персонажей (в оригинале — первая строка draw_people, seg008:228A). - autocontrol_shadow + autocontrol_shadow_level4 + clear_char (guards.c): тень идёт СВОЕЙ веткой целиком, к стражьему ИИ не сводится — на уровне 4 она не дерётся, а бежит влево и при x < 80 исчезает. АТЛАС ТЕНИ — вскрылось при чтении seg006:0532. Тень вне боевых кадров 150..189 ходит по таблице КИДА, и image оттуда индексирует спрайты Кида, а не стража. Выбор атласа в pop_cdraw шёл по СЛОТУ, то есть тень рисовалась бы спрайтами стража. Условие вынесено в pop_frame_tbl_is_guard() (pop_kid.c) — его теперь читают и load_frame, и отрисовка, разъехаться не могут. Заодно из pop_load_fram_det_col выделен pop_load_frame() без determine_col: jump_through_mirror берёт ось отражения из curr_col, и пересчёт колонки по x там был бы вреден. Константы MIRROR_* и DIR_56_NONE переехали в pop_guard.h — нужны и постановке тайла (банк 6), и ИИ тени (банк 1). Звука sound_45_jump_through_mirror в порте нет, пропущен. Шаг 5 (клип тени слева от зеркала) НЕ сделан и оказался не однострочником: в pop_cdraw есть клип сверху/снизу/справа, левого нет вовсе — нужен новый примитив либо срез исходных колонок. Расписано в TASKS_OPEN. Цена: _CODE +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551, банк 4 8243 -> 8267. tests-host: все 5 наборов прошли. НЕ ПРОВЕРЕНО В MAME — сценарий проверки записан в TASKS_OPEN. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
08ac38ce3d |
Пункт 0: отсев кладки по окну fore-клипа; закрыт BUG-CHEAT-FIGHT-1
Три шага пункта 0 из docs/perf_backlog.md. 1. Ранний выход из wall_pattern_palace по окну fore-клипа: узор целиком лежит в x [xh*8, xh*8+32), y [dmy-59, dby], и если окно его не задевает — возврат до первой заливки. Замер: от входа в узор до конца всего прохода 6 055 тактов вместо ~60 000 на тайл. 2. Отсев КАЖДОГО декаля (wp_blit) по реальному габариту. pop_blit_b тоже отсеивает до маппинга страницы, но по заведомо большему 64x64 — куски кладки высотой 3..12 px он пропускал и платил полный atlas_image + gfx_w0_map (~9 760 тактов на блит), чтобы там обнаружить, что рисовать нечего. Размеры сняты из каталогов атласов и заданы верхней границей по группе. 3. То же для ПОДЗЕМЕЛЬЯ: ранний выход wall_pattern (габарит выше — левая марка уходит на dby+POP_YOFF-67) плюс wp_blit на RNDBLOCK, обоих разделителях и обеих марках. Замер (уровень 4, комната 18, Кид сдвигается читом ] по пикселю, skip выключен, шесть тайлов в fore-окне, три из них — дворцовая стена): тайл стены ~12 700 вместо 60 000-74 000, fore-проход целиком 70 681 вместо 171 693, работа за кадр 419 839, период 3 растровых кадра вместо 4. Уровень 1 (подземелье) проверен снимком — кладка, марки, разделители на месте. Остаток в проходе — пролог pop_char_fore 17 208 тактов (пункт 1 backlog'а). Отдельно записано: на ШВЕ пролог разбухает до 134 730, причина не разобрана. BUG-CHEAT-FIGHT-1 (заведён 2026-08-07) закрыт по своему же плану: ветка ROOMNAV после kid_init/pop_kid_hp_reset гасит состояние схватки (Kid.sword = SWORD_0_SHEATHED, holding_sword, offguard, guard_refrac). Меч в инвентаре не теряется. Это болезнь чита: в оригинале телепорта между комнатами нет и в режим боя без соперника попасть нечем. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4718bff767 |
Уровень 4: skip по маске тайлов + порт loose_land, ворота 0xFF, пламя в фон
ОПТИМИЗАЦИЯ. Метка «фон трогали» была одним union-прямоугольником на страницу, и три факела комнаты склеивались в полосу x 40..280 на всю комнату — Кид, стоящий между крайними факелами, терял пропуск перерисовки и каждый кадр платил полным fore-проходом. Теперь это маска тайлов (uint16_t pop_cd_dmask[2][3]: бит = колонка, слово = ряд, набор = страница), проверка — три AND через резидентный pop_cd_hit. Гранулярность тайла — это гранулярность оригинала (redraw_frames_anim[tilepos], set_wipe; подтайловое уточнение там только по высоте, wipe_heights). Замер на (1,7): циан 233 515 -> 24 781, работа за кадр 517 609 -> 306 553, период 4 -> 3 растровых кадра. BUG-LOOSE-BUTTON-1. Упавшая плита не нажимала кнопку. Три слоя: порт loose_land не звал trigger_button вовсе; pop_room_col_landing считала площадкой только чистый пол, а у оригинала их семь (пол, пика, обе кнопки, зелье, оба факела); сигнал приходил в момент ОТРЫВА плиты, из-за чего ворота начинали открываться, пока она ещё в воздухе. Нажатие в оригинале ОДНО, но с button_type = tiles_14_debris — это «открыть НАСОВСЕМ» (modifier 0xFF), и кнопка съедается. Посадка в комнате снизу переехала на новый сигнал pop_loose_exit (взводит mob_tick_one, когда кусок ушёл ниже поля). BUG-GATE-FF-1. 0xFF был перегружен: сторожевое «тайла нет» в gate_modif и живое «открыто навсегда» из trigger_gate. gate_passable заворачивал Кида в воротах, нарисованных открытыми. Мёртвая ветка убрана. BUG-TORCH-CHOMP-1. Чомпер (0,7) комнаты 23 healит x 224..255 / y 30..93 и стирал пламя факела (0,6), которое рисуется в ячейке правого соседа. Фикс — запекать пламя (GFX_BANK_NORMAL): у факела все девять кадров на общем канвасе 16x18 и непрозрачны, протухнуть в ОЗУ-копии нечему, а heal чомпера сам возвращает огонь и кладёт челюсти поверх — z-порядок как в оригинале. Пузырёк зелья так нельзя (ползёт вверх, нужен heal) — остаётся в SPRITE. Заведено открытым: died_on_button (seg007:776) не портирован — нужен тайл tiles_5_stuck в атласе. Проверено пользователем в MAME (уровень 4: плита 16(1,1) -> кнопка 17(0,1) -> ворота 23(0,9); комната 23 с чомпером и факелом); make -C tests-host — все 5 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6a824dd7ab |
Закрыты BUG-GUARD-IX-1 и BUG-GATE-SEAM-ROW1 (уровень 4)
BUG-GUARD-IX-1 — «зависание» в бою со стражем. Не зависание: главный цикл крутился, а отладочная локаль frozen сама становилась ненулевой. Корень — у main затирался IX (0xBFFA -> 0xBF00), и все его локали адресовали живой стековый мусор. Затирал check_chomped_guard: coll_row() пишет по flags + scan_off, длину берёт из win_lo/win_hi, а scan_off выставляла только coll_scan_prepare() из пути Кида. Ряд стража писался по смещению Кида длиной стража и при Киде у правого края комнаты вылезал за flags[13] — прямо в сохранённый IX. Фикс: coll_scan_prepare() в начале get_row_collision_data(); заодно чинится расчёт (scan_left0 задаёт x колонок, чомпер-коллизия стража считалась по координатам Кида). На уровне 1 не проявлялось: бой идёт левее середины, запись оставалась внутри массива — данные были неверны молча. BUG-GATE-SEAM-ROW1 — решётка в шве не анимировалась. Плита комнаты 1 открывает ворота комнаты 8 в (1,9), видимые через левый шов, а change-driven редрой смотрел только m[9] (ряд 0) и перерисовывал жёстко draw_tile(0,0). На уровне 1 та же связка работала лишь потому, что решётка соседа стояла в (0,9). Фикс: сигнатура по всем трём рядам, маска изменившихся рядов в seam_rows (переживает оба кадра дабл-буфера), pop_room_redraw_seam_left(rows) перерисовывает только помеченные. Плюс ряд выше (changed | changed>>1): верх решётки (draw_tile_anim_topright, seg008:0568) рисует тайл над-справа от ворот, без этого чёрный треугольник над ними оставался статичным. Проверено пользователем в MAME (бой в комнате 18; анимация решётки в стартовой комнате), детекторы IX висели без починки и не сработали; make -C tests-host — все 5 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dbf9166836 |
Цвета 6 и 12 базовой палитры: игра правит их на старте, мы брали VGA-значения
Симптом (наблюдение пользователя): кирпичи дворцовой кладки правильного
цвета, а швы между ними — нет.
Корень шире, чем швы. init_game_main (seg000:164) подменяет ДВЕ записи
16-цветной палитры сразу после загрузки:
// (blood, hurt flash) #E00030 = red
set_pal(12, 0x38, 0x00, 0x0C);
// (palace wall pattern) #C09850 = light brown
set_pal( 6, 0x30, 0x26, 0x14);
Подтверждено дампом палитры живого SDLPoP: PAL[6] = 48,38,20,
PAL[12] = 56,0,12. У нас в таблице VGA16 стояли стандартные VGA-цвета
(42,21,0) и (63,21,21). Цветом 6 рисуются швы дворцовой кладки
(blitters_46h_mono_6), цветом 12 — кровь чомпера и вспышка урона, так что
промах был не только в стенах.
Заодно исправлена вспышка урона в roomtest.c: было flash_bg(255,85,85)
(стандартный brightred), стало (224,0,48).
Проверка: ряд стен уровня 4 теперь совпадает с эталоном SDLPoP по всем
восьми цветам и их количествам один в один (5333/4013/3110/2065/1785/1372/
1075/447). Остаточное различие значений — только наше масштабирование
6->8 бит: (v*255)/63 против v*4 у SDLPoP, то есть (194,153,80) против
(192,152,80).
ПОПРАВКА к
|
||
|
|
89400ff1d4 |
Тайлсет дворца: палитра применялась до kid.pal и затиралась ею
Симптом (эталон SDLPoP против нашего кадра, уровень 4): дворцовая геометрия рисовалась подземельными красками — сине-серые арки вместо песочных, бирюзовая дверь уровня вместо кремовой, сланцевый пол вместо коричнево-розового. Бирюза и зелень — это dungeon-слоты 0x5E (0,117,76) и 0x5F (0,165,157). Причина в порядке старта: атласы (и вместе с ними палитра тайлсета) грузятся ДО initgraph, потому что тот снимает DSS-страницу W0. А kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON (pop_pack_kid.py build_palette), — читается ПОСЛЕ initgraph и затирает слоты 0x50..0x6F. pop_bg_pal_apply() возвращает 32 записи текущего набора; зовётся сразу за gfx_pal_sync(). На смене уровня палитра по-прежнему едет внутри pop_bg_load — там initgraph давно позади. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
97929b55d3 |
Уровень 4: второй тайлсет (дворец) — ассеты и переключение (шаги A и B)
Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. Упаковщик (toolchain/pop_pack_bg.py): - аргумент набора: `pop_pack_bg.py dungeon|palace`. Каскад каталогов — сначала свой набор, потом чужой фолбэком (в распакованном data/ res230/ 231/348 есть только в VDUNGEON, два десятка — только в VPALACE). - ОБА набора пакуются по одному объединению id, поэтому раскладка id -> (страница, idx) общая и заголовок один: коду достаточно подменить имена файлов. - PALACE_ENV_IDS: 78/80/82 (doortop_fram_bot), 81/83 (doortop_fram_top), 84 (полоска стены), 145 (stripe_id) — их рисует только палас, render_room про них не знает. - ENV_SHIFT 5 -> 4: с паласными кусками страница 2 переваливала за 16 КБ (16 996). Цена — 10 страниц EMM на набор вместо 5, при 215 свободных. - *tile.pal: 32 записи (env 0x50..0x5F + wall 0x60..0x6F) на набор. Полная kid.pal не трогается — Кид, страж, меч и зелья в других слотах. Движок: - pop_level_type() (tbl_level_type, SDLPoP data.h:840): дворцовые уровни 4, 5, 6, 10, 11, 14. Живёт в pop_level.c, потому что по типу расходятся не только атласы, но и ветки отрисовки seg008, кладка стены и модификатор пустой клетки от упавшей плиты (remove_loose, seg007:0EB8). - pop_bg_load(set): no-op при том же наборе, при смене выгружает старый (иначе текут 12 EMM-страниц) и правит 32 записи палитры в ОБЕ страницы дабл-буфера. Зовётся на старте и на границе уровня, не в кадре. - Путь к атласу склеивается на месте (bg_path): двадцать строк-имён в банке — лишние полкилобайта. Проверено в MAME: уровень 1 (подземелье) рисуется как прежде; уровень 4 (-DFIRST_LEVEL=4) — дворцовые арки, окна, пол, решётчатая дверь уровня, Кид не перекрашен. Стены пока чёрные: паласный wall_pattern — шаг C. tests-host: 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9234c03020 |
BUG-GATE-PASS-1: история флагов коллизии переживает боковой переход
Оригинал (seg004:0004) индексирует флаги перекрытия колонкой ВНУТРИ разрешённой комнаты и хранит рядом её номер, поэтому решётка комнаты 8 остаётся в своём слоте и после перехода 8->6: переход флага 0->1 виден, bumped() срабатывает. У нас индекс — колонка отрисованной комнаты, тот же тайл менял слот, и enter_room вынужден был выбрасывать историю целиком — на кадре входа бампа не было, и Кид с разбега уходил сквозь закрытые ворота. Вариант B (сдвиг вместо тега комнаты): при БОКОВОМ переходе история не выбрасывается, а перенумеровывается на 10 слотов. check_leave двигает x ровно на ∓140 = 10 тайлов, координата грани едет на те же 140 вместе с габаритом Кида — сами флаги инвариантны, меняется только номер слота. Сдвигаются curr/above/below (prev на следующем кадре всё равно перезапишет move_coll_to_prev), освободившиеся слоты = 3 «уже перекрывал». Вверх/вниз и прочие входы в комнату — по-прежнему полная инвалидация. Дословный вариант A (10 слотов + массив номеров комнат) не взят: он тянет за собой сужение окна перебора колонок, то есть отказ от FIX_COLL_FLAGS. Заведён docs/impl_diff.md — список осознанных расхождений с SDLPoP; правило «фиксировать расхождение» в обоих CLAUDE.md теперь указывает туда. tests-host: все 5 наборов прошли. Приёмка в MAME впереди. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dc0bd47368 |
L3-CHOMP: чомперы — анимация, отрисовка и смерть в челюстях
Порт SDLPoP: animate_chomper / start_chompers / start_anim_chomper / next_chomper_timing (seg007) -> pop_trob.c; draw_tile_anim + draw_tile_fore, ветка tiles_18_chomper (seg008) -> pop_room.c (низ/кровь/верх, backtable) и pop_bg.c (передний слой); check_chomped_kid / check_chomped_guard / chomped (seg004) -> pop_map.c. Состояние — в room_modif, как у пик и ворот: младшие 7 бит фаза 1..N, старший бит «перемололо кого-то» (кровь остаётся на тайле навсегда). Номер позы chomper_fram1 и передние куски лежат в РЕЗИДЕНТЕ (pop_tile.c): их читают обе половины слоя фона, а const-таблица банка из чужого банка не видна. start_chompers зовётся там же, где в оригинале: SEQ_UP/SEQ_DOWN в play_seq (pop_kid.c), start_fall и land (pop_map.c), вход в комнату (roomtest.c). Поэтому чомперы щёлкают только пока персонаж в ИХ ряду — так в оригинале. check_chomped_guard у оригинала отдельное тело (страж не проходит через check_collisions). У нас та же формула уже есть в get_row_collision_data, поэтому флаги ряда считаются во ВРЕМЕННЫЙ массив: coll_curr/above/below — это кадр Кида, из них move_coll_to_prev берёт прошлые флаги, затирание сломало бы ему бамп. Период смыкания — POP_CHOMPER_SPEED в pop_tune.h (15, как в оригинале). Заодно: устаревшая заглушка pop_fore_set_clip в tests-host была __banked, хотя функция давно переехала в резидент — всплыло при пересборке. Ассеты уже были упакованы (pop_pack_bg.py, 2026-08-07). Банк 6: 20.0 %, банк 7: 39.7 %, банк 3: 53.8 %. tests-host зелёные (5 наборов). Зубья в MAME рисуются; анимация и смерть — на ручной приёмке. |
||
|
|
a25ce58869 |
DRAW-COST шаг 1: пропуск неизменившегося персонажа — 210% -> 116% кадра
Персонаж, у которого с прошлой отрисовки ЭТОЙ страницы дабл-буфера не изменился ни один вход отрисовки, а фон в его прямоугольнике не трогали, уже нарисован правильно: heal, блит и fore-проход пропускаются целиком. Не спецкейс «мёртвый страж», а общее правило — покрывает и труп, и стоящего Кида, и ждущего стража. Механизм: снимок входов по страницам (pop_cdraw.c, cd_sig/cd_quiet) + позиционная метка «фон трогали вот здесь» (pop_cd_touch в резидентном pop_tile.c, зовёт сам pop_blit_b). Решение перепроверяется перед отрисовкой, а pop_char_draw страхуется собственным heal — если тик всё-таки сдвинул персонажа, прошлый кадр стирается там. Слоты рядом (32 px) — перерисовываем оба, иначе heal соседа выест кусок из «тихого». Метка обязана быть ПОЗИЦИОННОЙ: с флагом «фон трогали хоть где-то» выигрыш был ровно нулевым — факелы анимируются каждый кадр и гасили пропуск для всех сразу (597 684 такта, как без оптимизации). Замеры (MAME, брейкпоинты по totalcycles, бюджет кадра 430 000): комн. 1.3, труп стража, Кид стоит: 210 % -> 116 % (500 772 такта), ноль вызовов pop_heal_fast за кадр, весь фон — 2 блита (44 136); комн. 1.1, Кид стоит вдали от факелов: 404 112 (94 %), цикл 4 -> 3 кадра. Узкое место сместилось на ЛОГИКУ: 60 % кадра уходит на тик персонажей, которые СТОЯТ, ещё 28 % — на loose_tick + process_trobs в комнате без единой ловушки. Разбивка и план — TASKS_OPEN.md#draw-cost. |
||
|
|
14190f0210 |
MEM-BANK2 шаг 3: холодная половина слоя фона в банк 7 (90.4% -> 35.8%)
pop_bg.c разрезан по ЧАСТОТЕ вызова, а не по размеру:
pop_bg.c (банк 2) горячее fore-проход, оверлеи, кладка, клип
pop_room.c (банк 7) холодное draw_tile, точечные перерисовки, mob,
загрузка атласов
Стык — три тонкие __banked-обёртки (wall_pattern, wall_pattern_reset,
draw_gate_back): тела остаются непомеченными, поэтому горячий fore-проход,
зовущий wall_pattern до девяти раз за кадр, платит ноль, а трамплин
достаётся только холодному пути — ~40 вызовов на вход в комнату, 26 000
тактов = 0.06 кадра РАЗОВО. Общее состояние (pop_loose_modif, pop_ceil_modif,
obj_row/obj_col) писучее, лежит в _DATA/W2 и видно обеим половинам.
Заодно удалена мёртвая potion_bubble (169 Б).
Грабля: n_banks объявляет само приложение (roomtest.c), а не sprinter-cc.
Восьмой банк без правки константы линкуется молча, _bank_pages[7] остаётся
0xFF, и программа встаёт намертво до первого кадра. Диагноз снят дампом
_bank_pages из MAME.
Замер (ALLOCS=3000): BANK2 11 942 -> 5 872, BANK7 6 035; _CODE и куча не
тронуты (22 556 / 4 301).
Проверено: tests-host зелёные; построчная сверка pop_bg.c+pop_room.c против
дорефакторного pop_bg.c — ни одной строки логики не пропало; 8 комнат в MAME
до/после совпали попиксельно по активному экрану (различия только в фазе
анимации факелов и кадре Кида); живой прогон с переходами комнат, боем и
воротами сверен контрольным запуском HEAD-бинаря.
DRAW-COST поднят в приоритете: замер пользователя (ур.1 комната 3, страж
убит, Кид стоит) — синяя 80%, зелёная 20%, циан 110%, итого ~210 %
кадрового периода В ПОКОЕ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
bbf91d10ee |
DRAW-CHAR: отрисовка одна на всех Char; разгрузка банка 2 (90.4% -> 72.9%)
DRAW-CHAR. Отрисовка персонажа сведена к одному набору функций над Char — как физика после GUARD-PHYS. В оригинале add_kid_to_objtable (seg008:22F0) и add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются окном (loadkid/loadshad), набором спрайтов и типом объекта, а redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе. pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из любого банка без трамплина. Проход окклюзии тоже один (pop_fore_over_char), pop_fore_over_kid больше нет. Починилось само (расхождения, которые и были ценой дублирования): у соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг «мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА. Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ. Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация заведена отдельной задачей DRAW-COST. MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h). Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а const-таблицы — в странице банка, из другого банка их не прочитать; трамплин же выбирается объявлением, то есть __banked на листе бьёт и по горячим вызывающим (654 такта). W1 замаплено всегда — оттуда обе половины зовут листья прямым call и читают таблицы напрямую. MEM-BANK2, шаг 2: дедуп внутри банка. wall_pattern 808 -> 394 и wall_rnd 786 -> 654: четыре ветки по виду стены отличались только набором кусков и числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE, порядок вызовов prandom сохранён дословно. Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт макро-инициализатор, массив определяет один pop_kid.c. Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556, куча 4301 Б. tests-host зелёные (65/39/53/1723/1); в MAME комната 1 совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 — та же раскладка кладки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6673279cef |
PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика): - pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3 curr_guard_color = 0, оригинал палитру не подменяет); - pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом стража и был невидим; - load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень — только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался; - check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в комнате 3 при падении (seg002:252), autocontrol_skeleton; - leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня. Цвета стражей (BUG-GUARD-COLOR-1, закрыт): - все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257). Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS, а тот читает только #4000-#BFFF — таблицу из банка копируем в стек. Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт): - pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19. Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать обратно. +48 байт W2. Окклюзия соперника: - pop_fore_over_char получил проход other_overlay_tile (порядок midtable, seg008:1B06) и расширение перебора объединённым прямоугольником «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх кладки и верхней грани пола; - клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP; - ROOMNAV после смерти Кида делает честный pop_start_level: телепорт «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с вернувшейся кучей костей. Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком (101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) — render_room анимированные тайлы пропускает, и в атласе не было ни одного. Число EMM-страниц не изменилось. Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок (резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен гард «код наехал на данные» (DATA_LOC). Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3: level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT). В roomtest.c временно оставлен автостоп на падении соперника (отладка падений скелета) — помечен ВРЕМЕННО. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0cd6b2d737 |
L3-CHKP, зацеп в прыжке опцией, pop_tune.h; сборка 10 мин -> 1:48
L3-CHKP (чекпойнт уровня 3). Флаг взводится, когда Кид уходит ВЛЕВО ИЗ комнаты 7, а do_startpos по нему подменяет старт на комнату 2, тайл (0,6), лицом влево и снимает loose-плиту (7,0,4). Тонкость, на которой я сначала ошибся: level3_set_chkp (seg002:0665) вызван из leave_room ДО goto_other_room, поэтому `Char.room == 7` — это комната, ИЗ которой уходят, а не в которую входят. Поймал пользователь прогоном в SDLPoP: смерть В комнате 7 вернула его в стартовую 9, а плита осталась цела. Проверено в MAME: вход в 7 флаг не ставит, уход влево — ставит; респавн в комнате 2; после обычной смерти (без чекпойнта) плиты восстанавливаются как раньше. Зацеп ПРЯМО В ПРЫЖКЕ (check_grab_run_jump, seg006:1228) — портирован за выключателем POP_ENABLE_JUMP_GRAB. Это НЕ ваниль: в оригинале зацепиться можно только в начале падения (кадры 102..105), то есть Shift приходится жать уже в полёте; у SDLPoP это enable_jump_grab, и работает он лишь при включённых fixes-and-enhancements. Три точки вызова как у оригинала: check_action и обе ветки check_bumped (зацеп за верх стены вместо удара). pop_tune.h — настраиваемые константы в одном месте (аналог custom_options_type SDLPoP): чекпойнт, выключатель зацепа и отладочная крутилка POP_DBG_GATE_HOLD (сколько кадров решётка держится поднятой; оригинал 5, потолок 30 — таймер связи пятибитный, 31 = «заклинено»). Задача TUNE-1 в TASKS.md: читать это из ini рядом с exe. СБОРКА. --max-allocs-per-node снижен со 100000 (дефолт sprinter-cc) до 3000 (дефолт SDCC) через `make ALLOCS=...`, а pop_trob.c уехал в БАНК 6 — на 3000 резидент иначе не влезает (замер: конец _HOME 0xBC69 при стеке с 0xBB00). Итог: сборка с нуля 1:48 вместо >10 минут, куча 2751 Б вместо 2298. Релизная сборка — make ALLOCS=100000; сравнивать занятость банков можно только при одинаковом ALLOCS. Отдельно (вне git, SDLPoP в .gitignore): из референса вычищена вся наша отладка DBG-GRAB — трасса JMP, GRAB try/probe/fail/OK/skip, автоскриншоты seg003, печати DBG mob/mid/overlay/kidobj и счётчик dbg_shots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dc8b2b7115 |
Приёмка ур. 2–3: сквозь стену, клин в шве, бар под плитой, глухой страж
Четыре разбора с прогонов пользователя; три бага закрыты, четвёртый (BUG-SPIKE-1, пики) заведён с замером и гипотезой. BUG-JUMPWALL-1 (Critical) — недолетевший прыжок проходил СКВОЗЬ стену. check_collisions держал флаги перекрытия ОДНОГО ряда, а оригинал (seg004:0004) — трёх, и move_coll_to_prev (seg004:00DF) берёт «прошлые» флаги из нужного. Наш prev=3 («уже перекрывал») подавлял бамп ровно на кадре смены ряда, а в падении ряд меняется почти каждый кадр — переход 0→1 на стене приходился как раз на него. Порт трёх рядов дословно. Воспроизведено и закрыто на харнессе (новый набор t_wall: свип по 20 стартовым X, 4 давали проход сквозь кладку); 1723 трассы t_phys НЕ изменились — правка поведение-сохраняющая. Живьём подтвердил пользователь. BUG-GUARD-DEAF-1 (Major) — is_guard_notice не взводился НИГДЕ, поэтому неактивный страж не оборачивался на Кида за спиной никогда. Портированы все пять мест оригинала: опкод SOUND в play_seq (звуки 0..2), bumped_sound, мягкое/среднее приземление, обрушенная плита, щелчок кнопки. Ждёт игровой проверки боем в комнате 11 уровня 2. BUG-SEAM-WEDGE-1 — клин кладки в пустом (2,0). Сосед угла снизу-слева лежит в комнате по диагонали (room_BL); мы безусловно считали его стеной, оригинал (load_rowbelow, seg008:368) — только когда такой комнаты нет. Ряд «снизу» стал 11-байтным: [10] = тайл (0,9) диагональной комнаты. BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком. pop_ceil_bake_empty стирал полосу и восстанавливал только два тайла ряда −1, а в полосу лезет графика соседа слева и верхушки ряда 0. Теперь перерисовываются ряды −1 и 0, колонки col−1..col+1. Проверено попиксельной сверкой с эталонной перерисовкой: 0 различий. Плюс карта связности комнат уровней 1–3 (TASKS.md): на ур. 2 недостижимых нет, на ур. 3 это 23 и 24 — те же односторонние ссылки, что дали 13/18/24 на уровне 1, только комнаты полностью пустые. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4737ec323c |
MEM-BANK5: pop_ctrl.c → банк 5; чит [/] подгонки Кида по X
Три правки едут вместе намеренно: n_banks и --bank обязаны меняться атомарно, иначе промежуточный коммит — зависание (см. ниже). MEM-BANK5. CODE и DATA делят одно 32-КБ пространство W1+W2, поэтому килобайт кода, уехавший в банк, — это килобайт, доступный данным. Кандидат выбран не по размеру, а по частоте вызова: диспетчер управления дёргается раз в кадр на персонажа и горячих банк→банк переходов не создаёт (в отличие от pop_level, чей pop_level_tile зовётся из банка 2 на КАЖДЫЙ тайл). _CODE 26 780 -> 24 662 Б (−2 118) куча 180 -> 2 298 Б банк 5 2 211 / 16 384 (13.5 %) Шина control_* (8 глобалов) переехала в pop_state.c. Сегодня она уцелела бы и в pop_ctrl.c — банки собираются без --bank-data, их писучие данные остаются в общем _DATA, — но это флаг сборки, а не свойство кода, а шину трогают уже три банка: 5 пишет с клавиатуры, 1 подаёт синтетический ввод ИИ (autocontrol_*, seg002), 3 читает через pop_ctrl_shift_held. Заодно pop_ctrl.c наконец включает собственный заголовок — раньше объявления жили прямо в нём. ГРАБЛИ, на которые наступили (стоили дольше самой задачи): n_banks в roomtest.c захардкожен, и его надо править вместе с числом --bank. С n_banks=4 и пятым банком crt0 выделил четыре страницы, _bank_pages[5] остался нулём, и первый же вызов pop_ctrl_init() через трамплин отобразил в W3 страницу 0 и прыгнул на 0xC874 в мусор — исполнение забрело в дисковый код DSS и осталось крутить чтение секторов. Симптом: загрузка ресурсов проходит целиком (open=45 — все атласы), комната и Кид успевают нарисоваться из enter_room, а HP и номер комнаты уже нет, и kid_tick не вызывается ни разу. Ровно предупреждение из шапки runtime/bank.s. Сверку n_banks с реальным максимальным индексом банка записал в docs/TODO.md (Auto-banking) — это должно быть ошибкой сборки. DBG-CHEATS: [ (0x54) и ] (0x5B) двигают Кида на пиксель (seg000:1828), по фронту нажатия, под pop_cheats. Нужны потому, что мост MAME теряет нажатия при быстрой отправке и подогнать Кида в позу скриптом нельзя — на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1. ROOMNAV больше не зовёт pop_trob_reset: reset обнуляет room_seen, то есть чит ОТМАТЫВАЛ МИР (открытые/закрытые ворота, нажатые кнопки). Навигация обязана только телепортировать. Проверено в MAME: старт уровня 1 рисуется полностью (комната, Кид, HP, номер), бег вправо и падение на второй ряд отрабатывают, ] даёт x+1 и [ даёт x−1 по одному нажатию, Shift+→ — осторожный шаг (x 131 -> 142, колонка 4 -> 5) и при удержании 90 кадров не срывается в бег (x 142 -> 151), то есть pop_ctrl_shift_held работает через границу банк 3 -> банк 5. Наборы под ucsim: geom 39, grab 53, phys 1723 — все зелёные. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e36828ae6e |
L2: переход между уровнями и игра уровня 2
Порт levels_plan.md §2 (шаг 1) — машинерия смены уровня целиком. - Номер уровня стал состоянием: pop_current_level (порт current_level), pop_next_level больше не флаг, а НОМЕР; главный цикл срабатывает по расхождению next != current (порт play_level_2, seg003:0386). - pop_level_load_num(n): LEVELS\res20NN.bin (fallback a:\), старая EMM-страница отпускается только после успешной загрузки новой. На диск кладутся все 15 уровней (34 КБ). - Потабличные различия (data.h:840..848) — таблицы по 16 в pop_level.c: tbl_entry_pose, tbl_guard_hp (HP стража ушло из хардкода 3), tbl_guard_type (−1 = стражей на уровне нет), tbl_level_type. - find_start_level_door (seg003:02E6): на уровне 2 стартовый тайл — правая половина двери уровня, без 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). Проверено в MAME: уровень 1 → Shift+L → уровень 2 (комната 5, новые для нас тайлы 8/9 «большая колонна» на месте, дверь захлопнута, HP 3) → влево в комнату 4 со стражем → Shift+L → уровень 3. Вход в стартовую дверь уровня 2 по Up не срабатывает — как и должно (start_room-гард). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |