Три места, все — наши заявки на треки, поставленные без условий оригинала.
Вступление первого уровня (25) оказалось не «звуком при приседе», а
автоматом (seg005:02EB): пока need_level1_music не ноль, control_crouched
НЕ читает управление — Кид сидит, играет тема, и только когда она смолкла,
он встаёт. Мы играли трек при любом первом приседе, и тема догоняла игрока
посреди уровня: пробежал, спрыгнул, присел — заиграла. Теперь автомат
портирован целиком, а «ещё звучит» спрашивается у курсора насоса
pop_mus_left (если музыка выключена, курсор нулевой и Кид просто встаёт).
Демо-уровень: оригинал молчит музыкой и там, где играет её в игре. Смерть
Кида — прямое условие `current_level != 0 && != 15` (seg006:1366), убитый
страж — отдельная ветка «беги из комнаты» без звука (seg006:1929). Плюс
общий гейт в pop_music_service: заставочная демка озвучена одними
эффектами, и любой новый музыкальный повод (меч, зелье) звучал бы там, где
оригинал их не играет.
Гейт живёт в банке, а не в главном цикле: в резиденте W1 оставалось
27 байт.
Host-тесты: 15 наборов. Проверено в MAME: на старте первого уровня тема 25
отыгрывает целиком, курсор доходит до нуля.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шестнадцать ступеней на 2,13 с давали различимую лесенку (0,13 с на шаг).
Тридцать две меняются каждые 66 мс — это уже слитное затухание. Запас на
них есть: ступень стоит 487 тысяч тактов, тридцать три съедают около сорока
кадров из ста шести, остальное цикл ждёт луча.
Длительность каждого fade остаётся оригинальной (128 тиков), поэтому сумма
«fade in + сцена + fade out» совпадает с SDLPoP без правки самих сцен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Затемнение длилось около восьми секунд вместо заказанных двух. Причин
две, и обе измерены в MAME.
Первая: пересчёт палитры звался на КАЖДОМ кадре — сто с лишним раз за
fade, хотя ступеней всего восемь. Теперь пересчёт идёт только при смене
ступени, остальное время цикл просто ждёт луча.
Вторая: сам пересчёт стоил 2,44 млн тактов — почти шесть кадров.
Разложение показало, что заливка палитры через BIOS тут ни при чём
(136 тысяч на восемь вызовов). Съедали два цикла на C: 768 умножений
uint16 на канал и побайтовое копирование снимка из страницы шрифта.
Умножения заменены таблицей яркости на стеке (256 сложений, без единого
умножения), копирование — memcpy, то есть LDIR. Итог: 487 тысяч тактов,
в пять раз меньше.
Освободившийся запас потрачен на плавность: ступеней теперь шестнадцать
вместо восьми. Длительность fade считает POP_FADE_FRAMES — из 106 кадров
(2,13 с оригинала) вычитается то, что съедает сам пересчёт.
Host-тесты: 15 наборов. Проверено в MAME: заставка проходит цепочку
кадров штатно.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ДЛИТЕЛЬНОСТЬ. fade_in_1/fade_out_1 у оригинала — 64 шага палитры по два
тика, то есть 128 тиков = 2,13 с каждый; сцена перед уровнем 2 идёт с ними
около семи секунд. Наши четыре кадра укладывались в восемь сотых секунды,
и сцена выходила втрое короче. INTRO_FADE и TITLE_FADE теперь POP_T60(128).
Ступеней яркости стало восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8,
1/4, 1/8, 0 — каждая парой сдвигов, умножения на Z80 не нужно): растянуть
четыре ступени на две секунды значило бы получить четыре скачка яркости.
МУЗЫКА. Прошлый коммит отдавал трек доигрывать уже в игре — так делает
оригинал. На слух вышло хуже: музыка спотыкается, потому что загрузка
уровня не даёт насосу долить блок вовремя (у DOS-версии такой проблемы
нет). Дослушиваем под чёрным экраном, до отрисовки уровня — тем более что
следующий load_intro у оригинала всё равно начинается с ожидания тишины
(seg001:681). Пропуск сцены обрывает и музыку.
Заодно: WAIT перед PV пересчитан (584 вместо 707 — наш fade больше не
короче), а тема титров пускается ПОСЛЕ fade_in, как в show_title.
Host-тесты: 15 наборов. Проверено в MAME: плавное появление титров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Звуки 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>
Добавлены треки 54 (тема титров), 50/53/52 (три реплики сцены с
принцессой и Джафаром) — заставка звучит целиком, как в оригинале.
pop_music: два слота EMM. Реплики идут встык, паузы под загрузку нет,
поэтому следующая читается в НЕ играющий слот, а play подменяет
резидентную таблицу страниц. Своей копии таблицы слот не держит — её
хранит блок EMM. Плюс постраничная загрузка (load_begin/load_step):
страница стоит 33 мс, четверть логического кадра сцены, и подкачка
посреди анимации не видна.
Тайминги: все длительности сцен — в тиках оригинала (60 Гц), перевод в
кадры луча делает POP_T60. Пока сцены были немыми, разбег в 20 % был
незаметен; с музыкой реплика кончалась раньше картинки. Переведены
титры, история, хвост после PV и пейсинг самой PV-сцены.
Паузы, которые оригинал отмеряет концом сэмпла, теперь равны реальной
длине наших записей: конец m50 на тике 846, вход Джафара 1046, уход
2073, конец сцены 2500 (было 1959). Fade перед PV сдвинут так, чтобы
полная темнота наступала в тот же момент трека, что у SDLPoP: их fade
длиннее нашего на 123 тика, наш полосовой переход длиннее на 16.
Финальная сцена: убраны песочные часы — end_sequence_anim не трогает
hourglass_state, а reset_cutscene его обнулил. Уровень пройден, время
больше не идёт.
Проверено в MAME: цепочка 54 -> 55 -> 50 -> 53 -> 52 отыгрывается
целиком, интро доходит до демо-режима. Host-тесты: 15 наборов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
СИМПТОМ: затемнение (fade) хрипело — одинаково с играющей музыкой и в
тишине. Ключевое наблюдение пользователя: повтор тишины обязан звучать
тишиной, значит дело не в недоливе буфера.
ПОИСК: отладочные клавиши, каждая делала ровно один кусок fade. Ожидание
кадров — чисто; чтение палитры, запись палитры и 512 вызовов
bios_get_place() (видео вообще не трогает) — скрежет во всех трёх.
Последнее и решило: виновата не палитра, а ЛЮБОЙ вызов BIOS.
ПРИЧИНА: `rst 8` раскрывается в `out ($7C),a`, который включает системное
ПЗУ и перестраивает окно 0 (MAME sprinter.cpp, update_memory: m_pages[0] +
m_bank_view0.select). ПЗУ ложится ПОВЕРХ страничного регистра, поэтому
запись в порт 0x82 из прерывания бесполезна — OTIR вычитывает ПЗУ и
отдаёт его в звук. Отсюда же старое правило «глушить CBL на время
загрузки файлов»: причина была не в том, что ESTEX долго занимает CPU.
РЕШЕНИЕ (идея пользователя): качать через W3. Он управляется только
портом 0xE2, подмену из прерывания никто не перекрывает, а BIOS во время
нашего ISR не исполняется — окно возвращается до выхода, и для него
подмена невидима. После этого BIOS безопасен везде.
* pop_sfx.c — насос берёт взаймы W3 вместо W0, чтение по 0xC000 + смещение.
* pop_ui.c — буфер палитры по фиксированному 0x4000 (эти 256 байт DSS
занимает только при загрузке программы): 256 байт со стека долой.
* libbgi/common/gfx_pal_write.c — запись палитры прямо в видеопамять,
минуя BIOS. Писалась как обход скрежета, после переноса насоса не
нужна; оставлена как более быстрый примитив (2,5 тыс. тактов на 64
цвета против 10,8 тыс. у BIOS) с честной шапкой. Адресация разобрана
по исходникам BIOS (FUNC_SCREEN.ASM): Port_Y = индекс цвета, адрес
0xC3E0 + pal*4, порядок R/G/B/Y.
* libc/video/pal_get.c, pal_load.c — в шапках зафиксировано, что BIOS
выбирает окно ПО АДРЕСУ БУФЕРА (`BIT 7,H`).
* pop_intro.c — при пропуске интро клавишей не глушился CBL, и следующая
загрузка уровня шла с открытым буфером; добавлен pop_sfx_pause.
* pop_ctrl.c — убраны отладочные «осторожные шаги» на J/L (эмуляция
Shift+стрелка для MAME), у них и стоял TODO.
Разбор целиком — docs/sound_plan.md §5. Бюджет: _CODE 23966, куча 245 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Путь C из sound_plan.md вместо ранее выбранного A (ноты спикера на AY).
Причина смены: появились готовые записи DOS-версии, и строка «нужен
синтезатор на хосте», из-за которой C считался дорогим, отпала —
осталось `ffmpeg -ac 1 -ar 10937 -f u8`. Оцифровки музыки в самой игре
нет вовсе: digisnd*.dat содержит только эффекты, музыка лежит MIDI под
Adlib, так что конвертировать ресурс игры всё равно было не из чего.
* toolchain/pop_pack_music.py — запись flac -> сырой u8 на 10 937,5 Гц
(частота CBL, чтобы её не переключать никогда), нарезка на куски
MUS\m<id>_<nn>.bin по 16 КБ: bank_load_file читает файл только целиком
и только в одну страницу. Каталог -> pop_music_tbl.h; длина трека
хранится ПОРЦИЯМИ по 128 байт — 169 КБ в uint16 не влезает, 1350
блоков влезает (32-бит арифметики на Z80 избегаем).
* pop_music.c (банк 9) — загрузка трека в EMM и курсор; pop_sfx_fill
(резидент) получил третий источник: эффект важнее музыки, музыка
важнее тишины. Эффект музыку не сбрасывает — её курсор стоит, пока
тот доигрывает, и она продолжается с места.
* story_1_absence звучит на первом экране истории, где была тишина.
Грузим ПОСЛЕ сборки картинки: чтение идёт через ESTEX, и при открытом
CBL насос не успел бы долить блок.
Проверено записью звука из MAME: корреляция огибающих с эталоном 0,836
при сдвиге 1,8 с, RMS 22,6 против 18,3 (разница — 8-битное квантование).
Эффекты двери в PV-сцене после трека звучат по-прежнему.
Бюджет: резидент +83 Б кода и +25 Б данных, запас W2 151 Б (кучи в
приложении нет, malloc не слинкован); банк 9 занят на 88 %.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Порт 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>
* pop_status_level() — порт show_level (seg008:25A8): «LEVEL N» на старте
уровня. Демо-уровень 0 и номера от 14 молчат, тринадцатый показывается
двенадцатым, бесшовный переход 12->13 пропускается и гасит флаг за собой.
Первое сообщение с ПАРАМЕТРОМ: строка собирается вручную (dec2, без
printf и без деления), номер хранится снимком st_arg — иначе вторая
страница дабл-буфера нарисовала бы другое число.
* pop_status_show_now() — печать немедленно, в ВИДИМУЮ страницу.
QuickSave/QuickLoad заявляют лейбл ПЕРВЫМ действием, до mem_alloc/ESTEX:
запись снимка занимает доли секунды, и раньше игрок видел сначала
необъяснённый фриз, а надпись — уже после него. Отказ переписывает
строку на NO QUICKSAVE/NO QUICKLOAD обычной заявкой.
Расхождение с оригиналом (он печатает по результату) — impl_diff.md.
Проверено в MAME: watchpoint ловит заявку внутри pop_status_show_now,
step_out возвращает в pop_qsave_process — и QUICKSAVE уже на экране, диск
ещё не тронут. Бюджет прежний: _CODE 23981, куча 266 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (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>
menu_settings_plan.md:
- §10 переписан: выбран Вариант A — текстовые строки + собственный
растровый рендерер в новом банке; референс SDLPoP (hc_small_font /
hc_font — один рендерер, два шрифта); шрифт как ассет из паковщика,
прототип MS4 — системный CP866 ZG; двуязычность eng/rus через пару
(таблица строк CP866, файл шрифта)
- Restart Level / Restart Game выполняются сразу, без подтверждения
(§3, §8, из §11 убраны диалоги RESTART *?)
- §13 MS4: текстовый рендерер + два шрифта; §14: host-тест рендерера
quicksave_plan.md: статус «РЕАЛИЗОВАНО и проверено в MAME»
(v0.6-pop-quicksave), документ оставлен справочником по формату 'POPQ'
TASKS_OPEN/TASKS_CLOSED: запись QSAVE переехала в закрытые с полным
протоколом; docs/README.md аннотации обновлены
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
pop_tile.c был крупнейшим жильцом резидента (5972 Б кода). Целиком он не
уедет — его const-таблицы читают банки 2, 3, 7 и 8, а таблица в чужом банке
не видна. Поэтому разрез, а не перенос.
Отбор ЗАМЕРОМ, а не по смыслу: каждая функция посчитана брейкпоинтом-
счётчиком в MAME — сколько вызовов в кадре покоя и сколько в кадре полной
перерисовки комнаты (форсируется читом +/-). Порог — пик не больше 3.
Проверено на ДВУХ тайлсетах, подземелье и дворец: pop_mem_b рисует
композитный кусок и мог оказаться дворцовым, но и там 0 вызовов.
Уехало: pop_mem_b, pop_cd_hit (+hit_rect), pop_cd_hit_slot, pop_cd_init,
pop_cd_clear, pop_t_win_set/clear, pop_bar_black, pop_heal_off,
pop_potion_flask, pop_room_set_above/below.
Осталось: pop_blit_b со статиками (184 вызова на редрав), pop_cd_touch
(198), pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
pop_tile_mod (70), все таблицы. pop_fore_set_clip оставлен намеренно —
88 Б не стоят отказа от прямого вызова из банка 4.
Цена трамплина замерена: 252 такта пролог + 84 эпилог + ~50 у вызывающего
= ~410. Это ~1000 тактов на кадр покоя (0,2 % работы) и ~3700 на редрав.
blit_b_clip перестал быть static и объявлен в _pop_tile.h: вызов
банк -> резидент прямой, трамплин не нужен, поэтому статик горячей половины
переносить следом не пришлось.
Итог: _CODE 23716 -> 21928, свободно 747 -> 2535 Б (с 129 Б до всех работ).
Проверено в MAME на уровнях 1 и 4, с переходами комнат.
Метод замера, таблица частот и ловушка с данными банка — docs/resident_budget.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Возврат к состоянию после звуковых правок (c6828c0). Отменяются 3545826 и
9025573 целиком: --bank-data=SRC в sprinter-cc, его включение в PoP и
docs/resident_budget.md.
Что выяснено и почему откат, а не доводка. Перенос писучих данных
банкового модуля в его 16-КБ страницу даёт цветной мусор блоками и уводит
DSS. У pop_trob причина найдена: pop_trob_modif() возвращает указатель на
room_modif[24][30], и его разыменовывают банки 2/3/7 и резидент — то есть
пишут по 0xC000+ в СВОЮ страницу, поверх чужого кода. Def/Ref-анализ такое
не ловит: снаружи ссылки на символ нет, есть ссылка на функцию, отдающую
его адрес.
Но и один pop_room, у которого явной утечки указателя найти не удалось,
ломается так же — значит механизм понят не до конца. Пока не понят,
включать нельзя. Нулевая инициализация при этом ни при чём: mkexe -p 0
проверен по образу (прогон нулей 14304 Б, самый длинный прогон 0xFF — 14).
Место в резиденте искать другими путями: malloc (287 Б, требует раздельных
cbl_open для APP и SILENCE) и код pop_tile (5972 Б).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Симптом: через несколько комнат живого прохода перезагружался DSS.
pop_trob_modif() возвращает указатель на room_modif[24][30], а зовут её из
банков 2, 3, 7 и резидента. После переноса массив лежит по 0xC000+ в
странице банка 6, но разыменовывает указатель ЧУЖОЙ код — когда замаплена
его собственная страница. Значит чтение и запись идут поверх кода соседнего
банка.
Анализ Def/Ref такое не ловит: снаружи нет ссылки на символ, есть ссылка на
функцию, которая отдаёт его адрес. Условий для кандидата два, и второе
проверяется только чтением кода — ни один указатель на статику не должен
уходить наружу.
pop_room оба условия проходит (_mobs не читает никто; bake_copy статическая;
atlas_load(&pop_env[i]) берёт адрес глобала из резидентного pop_tile.c).
Остаётся −1620 Б: данные в W2 6774 -> 5157, свободно 129 -> 1746 Б.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sprinter-cc: новый повторяемый --bank-data=SRC — писучие данные ОДНОГО
банкового модуля в его же страницу. Прежний --bank-data был всё-или-ничего
и потому неприменим: у большинства банковых модулей часть глобалов читают
соседние банки и резидент (hitp_*, pop_upside, pop_loose_modif, pop_cd), и
такие данные обязаны остаться замапленными всегда.
Кандидаты отобраны по объектным файлам, а не на глаз: символ должен быть
Def только в своём .rel и нигде не Ref. Прошли ровно двое — pop_room
(_mobs не читает никто) и pop_trob (экспортируемых данных нет вовсе).
При любом --bank-data sprinter-cc сам добавляет mkexe -p 0: crt0 зануляет
только резидентный _DATA, а mkexe по умолчанию бьёт пустоты 0xFF — иначе
вся банковая статика поднялась бы мусором.
Итог: данные в W2 6774 -> 4228, свободно до стека 129 -> 2675 Б.
BANK7 87 %, BANK6 31 %. Проверено в MAME: уровень 1, комнаты 1-2, фон,
факелы, решётки, проваливающиеся полы, переход между комнатами.
Метод замера и оставшиеся кандидаты — docs/resident_budget.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка на сцене 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>
Пользователь услышал расхождение с 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>
Две разные болячки, обе разобраны записью звука MAME в WAV.
libc: буфер CBL железо не чистит, а запись в порт управления сразу пускает
воспроизведение с нулевого слота — первые 256 сэмплов (23,4 мс) уходит то,
что лежало раньше. Своими данными звук идёт лишь с третьей половины:
прерывание приходит на 128-м слоте и ставит указатель на противоположную
половину. _cbl_prime заливает буфер тишиной сразу после включения (раньше
нельзя — запись проходит только при поднятом bit7). В MAME это немо
(эмулируемый буфер стартует нулями при двухдополнительном ЦАП), на железе
это ровно тот мусор, что ловился на тестовых примерах CBL.
PoP: на выходе по ESC звучало ровно 11 мс шума на полной громкости — один
пропущенный блок (128 сэмплов). Причина: pop_shutdown освобождал атласы и
графику через ESTEX при открытом звуке, насос не успевал долить. Звук
гасим первым действием. Проверено записью — всплеска больше нет.
Заодно записан разбор стартового всплеска (sound_plan.md §10): это не
мусор, а gate_closing_fast из левой комнаты, обрываемый soft_land. Обрыв
одноголосьем — поведение оригинала (play_digi_sound начинается с
stop_digi, seg009.c:2402).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порт Ctrl+S из SDLPoP (seg000:657). Выключение реально закрывает CBL, а
не глушит сэмпл: иначе насос продолжал бы отдавать блоки тишины и платить
те же 3,26 % процессорного времени.
Флаг намерения pop_snd_want отдельно от pop_snd_ok: последний гасит
служебная пауза на время загрузки уровня, и правь Ctrl+S только его —
первая же смена уровня вернула бы выключенный звук.
Индикация: пурпурная палочка в борте, когда звука НЕТ (включённый слышно
и так, а молчание неотличимо от «нечему звучать»).
Чит S «выдать меч» был отладочным и больше не нужен — снят, клавиша ушла
под звук.
Замер цены звука — sound_plan.md §9: 8 001 такт на прерывание при периоде
245 759 (3,26 % времени), +2,2 % к работе кадра в сцене 11/15, период
кадра не сдвинулся ни разу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Набор 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>
Эффекты берём из MSDOS/digisnd*.dat: 28 из 31 звука совпадают побайтно с
набором SDLPoP, но три различаются не в пользу последнего — у него
sword_vs_sword короче, sword_moving другой, а spiked вообще пустой
(7 сэмплов против 5 069).
MIDI наоборот: у MSDOS формат 0 (всё слито в одну дорожку), у SDLPoP
формат 1 с 8-9 дорожками — готовое разделение голосов, если дойдём до
пути B (MIDI -> три канала AY).
Упаковщик: 31 эффект -> 10 937,5 Гц, 131 072 Б = 8 EMM-страниц. Начало
каждого звука выровнено на 128 (блок запроса CBL), а страница кратна 128,
поэтому ни один блок не пересекает границу страницы — проигрывателю не
нужна логика стыка.
Формат оригинала проверен по convert_digi_sound: один байт на кадр (моно),
байт беззнаковый с центром 0x80 — ровно формат нашего CBL. Стерео в
данных нет, каналы размножаются на выходе.
Живой замер памяти из работающей программы: занято 124 страницы из 256
(система с exe 43, наши ассеты 81), свободно 132 = 2,06 МБ. Самый
крупный ассет теперь набор Тени — 32 страницы. Эффекты WAV займут 8.
Эффекты — WAV через CBL; музыка первым заходом путь A (ноты на AY);
заставки потом WAV; музыка по ходу игры — открыто (WAV с гашением
эффектов либо путь B).
Единую частоту берём не 11 000, а ровно частоту CBL 10 937,5: тогда тон
точен (иначе −9,9 цента), а пересчитывать три файла всё равно надо.
112 922 -> 124 531 Б, 6,9 -> 7,6 EMM-страниц; рост целиком от
leveldoor_sliding (источник 2 750 Гц). Взамен CBL открывается один раз и
частота не меняется никогда.
Эффекты все 31 есть в WAV (digisnd, 8 бит PCM) — просьба «не спикер, а
wav» для них уже выполнена исходным планом. mt32snd оказался НЕ музыкой,
а теми же эффектами в MIDI для Roland MT-32.
У музыки WAV нет ни в одном наборе: только ноты спикера (7 КБ) или MIDI
(27 КБ). Посчитал третий путь — рендер MIDI в WAV на хосте: игровые
треки 74,7 с = 803 КБ = 50 EMM-страниц (влезает), заставки 248 с = 167
страниц (только стрим с диска).
Только спикером во всей игре остаётся один звук — blink (4 ноты).
Замеры по ассетам: 31 эффект уже 8-битным PCM на 11 000 Гц (у CBL есть
10 937,5 — расхождение 0,6 %, формат сэмпла совпадает байт в байт, то
есть конверсии нет вовсе), 103 941 Б = 6,3 EMM-страницы.
Вся музыка есть нотами PC-спикера — 7 КБ на 57 звуков, и нота там задана
в ГЕРЦАХ напрямую (проверено по speaker_callback), а не делителем PIT,
как кажется по числам. MIDI разбирать не нужно.
Отдельный таймер не нужен: секвенсор музыки двигает CBL-callback раз в
11,7 мс, а короче 12 мс во всей музыке 2 ноты из 1469.
Оригинал кладёт спрайт дважды — прозрачным блитом в x и XOR-блиттером в
x+1; пакетный блит так не умеет, поэтому результат запечён упаковщиком.
Две половины, как и у оригинала: sk* — кадры вне боя (спрайты Кида),
sf* — кадры 150..189 (SHADOW.DAT, тоже графика Кида). 251 спрайт,
32 EMM-страницы, палитра 16 цветов в 0xA0..0xAF.
Закрывает BUG-SHADOW-SET: раньше тип 4 уходил в guard_names, и Тень в
бою дралась серым стражем.
Грабля: kid.pal заливает все 256 записей и затирает слоты Тени —
палитра вынесена в pop_shadow_pal_apply рядом с pop_bg_pal_apply.
Проверено в MAME на 6-м уровне: силуэт с контуром, как в оригинале.
Поправка к вчерашнему выводу «в бою Тень рисуется спрайтами стража».
Набор берётся из cur_frame.sword>>6 (seg008.c:1752), chtab_base жёстко
равен Киду. Тень идёт через chtab_5, но chtab_5 — это «соперник уровня»,
и на 12-м это SHADOW.DAT: графика КИДА в боевых позах, палитра побайтно
равна палитре Кида. Пользователь прав — Тень всегда выглядит Кидом.
Наш pop_guard_load уводит тип 4 в guard_names, SHADOW.DAT в ассетах нет
вообще — заведён BUG-SHADOW-SET.
Пересчитал палитру на правильных наборах: 251 кадр, 45 цветов; 16 цветов
гибридом дают 76 грубых промахов на все кадры (было 129 на ошибочном
наборе).
XOR у оригинала идёт по 24-битному RGB, а blitters_2_or — обычный блит с
colour key 0. От фона зависит только кайма в один пиксель по левым
кромкам силуэта; на чёрном фоне запечка точна.
Замеры: 253 кадра (Кид 219 + страж 34, Тень в боевых кадрах рисуется
спрайтами СТРАЖА), 59 разных цветов. 32 цвета оставляют перцептивно
значимыми 232 пикселя из 96 746. Палитра: занято 112 слотов, свободно
144; берём 0xA0..0xBF.
Замеры в MAME: кадровые прерывания теряются фазозависимо (0..3%), на
полной перерисовке комнаты — три подряд. Причина: импульс запроса 32
такта (9,14 мкс) против DI-окон блита ~0,29 мс. Счёт попаданий
брейкпоинтом на этом драйвере недостоверен (WAIT-линия), достоверен
только детектор разрыва.
Блокер включения gfx_set_fps_div как есть: счётчиковый путь ждёт через
halt и не зовёт idle-хук, то есть возвращает KBD-1.
Максимумы по секциям против эталона mob-order-B-done: работа 911 862
(-1 986), синяя 149 106 (-10 704), зелёная 436 494 (-3 924), циан 382 770
(-10 230). Период 3 растра в 2341 кадре, 4 в 23, 5 в 2 — как в эталоне.
Записано, почему сумма минусов по фазам не равна минусу по работе:
максимумы разных фаз достигаются в разных кадрах, а «работа» — максимум
суммы, а не сумма максимумов (вопрос пользователя).
Отмечено, что зелёная по-прежнему выше растрового кадра и главный
оставшийся кандидат для этой сцены — P9 (G8): сосед падающей плиты
перезапекается целиком и повторно, при шести плитах это умножается.
И записан урок процесса: прогон 13/23 обязателен после каждой правки
loose-механики — именно он вскрыл пропущенный взвод гейта в check_fall_flo,
которого не поймали ни хост-тесты, ни сцена 11/15.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HEAL-WIDTH: плита 64->58, чомпер 64->61, габариты из каталогов атласов.
На 11/15 медиана не сдвинулась (плит нет), максимум -960. Основной
эффект ждёт прогона 13/23, где плит шесть одновременно.
P10 разобран: «просто передать готовое из физики» не выйдет, величины
РАЗНЫЕ. char_footprint берёт габарит кадра и расширяет диапазон на
колонку под меч; set_char_collision тот же fpw корректирует на FRAME_THIN
и меч не учитывает, а ряды у него — опорный curr_row, а не верх/низ
спрайта. У оригинала обе задачи пользуются одними величинами, потому что
он считает их один раз; у нас они исторически разошлись.
Значит P10 — это сведение двух геометрий к одной, с риском для физики,
которая сейчас работает правильно. Приоритет понижен до низкого, и
записано, чего не хватает: отдельного замера самого char_footprint
(сейчас известно лишь «вход + set_clip + footprint = 10 872»).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оценка пары P6a/P6b была -20 000, факт по P6a — -840. Записана причина:
оценку я перенёс по аналогии с лучом видимости, где трамплин звался девять
раз за кадр, а тут trob'ов в комнате всего несколько. Урок в реестре:
«тот же паттерн» не означает «тот же порядок величины».
P6b (кэш префетча, 11 058) не делался: инвалидацию пришлось бы ловить из
трёх источников (pop_level_set_tile, вход в комнату, добавление trob), а
пропуск любого даёт застывшую анимацию.
Бюджет лёгкой позиции: 437 484, до цели 7 484.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь заметил: Кид перерисовывается, хотя с пламенем не
пересекается; на пиксель левее — перестаёт.
Разбор: спрайт Кида занимает x 213..224, колонка считается как x >> 5,
и 224 — ровно первый пиксель колонки 7, где лежит метка от пламени
(y 33..50). По вертикали пересечение настоящее, по горизонтали его нет:
пламя в той же колонке занимает x 232..247, зазор восемь пикселей.
То есть P15 исправил огрубление по Y и оставил его по X.
Отложено по решению пользователя с его же аргументами: x не влезает в
байт (0..319), значит нужны 16-битные сравнения в горячем пути, а они у
SDCC z80 дороги настолько, что могут съесть выигрыш; огрубление вдвое —
лишний сдвиг при записи и проверке плюс потеря точности.
Записана непроверенная идея: хранить границы как смещение ВНУТРИ колонки
(0..31, пять бит) — байта хватит и сравнение 8-битное, но запись
усложняется для прямоугольников через несколько колонок.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P4 (каталог из W0) отменён по критерию пользователя: 408 тактов не стоят
второй публичной функции в libbgi с неявным контрактом «страница уже
подключена». Знание сохранено: gfx_w0_map стоит 324, поэтому потолок
непробованной части P4 — около 2 600, а не 10 000.
P17 — по замечанию пользователя про 16 бит там, где хватает 8: границы
экрана беззнаковыми сравнениями (-378) и габариты спрайтов в uint8_t
(-276 в статике, -1 134 в циане динамики, плюс 24 байта _DATA).
Бюджет: лёгкая 438 324, тяжёлая 603 684.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каталог атласа теперь читается из W0 вместо переключения W3 — минус 408
на кадре при ожидании минус 5 400. Цена блита 16 107 -> 16 005.
Причина записана: 672 такта atlas_image — это почти целиком вызов
функции и арифметика idx*8, а не переключение окна; после правки работа
переехала в статью «каталог + шапка + клип» (2 694), а сам gfx_w0_map
стоит всего 324.
Отсюда понижена оценка непробованной части P4 (один map на группу
блитов): потолок ~2 600 за кадр, а не 10 000.
Бюджет: лёгкая 438 570, тяжёлая 602 574. До цели 8 570 и 172 574.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fore-проход Кида в тяжёлой позиции (89 268) разложен зондами:
вход + set_clip + char_footprint 10 872
арифметика границ окна 3 786
шов ворот + overlay-цикл 3 294
ЦИКЛ fore_tile ПО ТАЙЛАМ 67 854 76 %
gate_over_char + хвост 3 462
Счётчик показал, что цикл обходит ВСЕГО 4 тайла, и 3 из них реально
рисуют. То есть 67 854 — не перебор лишних тайлов и не проверки, а цена
самих блитов переднего слоя: около четырёх блитов по ~16 000, где 6 765
на каждом — фиксированная накладная.
Значит отдельной оптимизации fore-прохода почти нет: срезать можно цену
блита (P4, ~27 000 из 67 854), char_footprint из физики (P10) и слияние
двух трамплинов в банк 2 (~4 000).
P4 поднят в очереди: он бьёт и по fore-проходу (4 блита), и по зелёной
фазе (8 блитов) — то есть работает и в динамике, и в статике.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Записаны, чтобы не повторять, и с разбором причины.
1. cd_sig_same блоком (сравнение 10 байт циклом вместо 13 сравнений
полей): по листингу короче (1939 -> 1290), на машине хуже
438 978 -> 450 426. Сумма тактов по листингу считает инструкцию один
раз, а тело цикла исполняется десять раз.
2. cd_touch_pb (пометка «для блита» из file-scope вместо четырёх
аргументов): 438 978 -> 442 242. В зелёной фазе блиты идут пакетным
путём, где нужны все четыре значения, а в регистрах они дешевле, чем
чтение из статиков.
3. Обёртка pop_cd_hit_slot поверх pop_cd_hit — 1799 против 1318 тактов;
помогло только когда сравнение переехало внутрь.
Общий урок записан там же: короткий листинг не равно быстрый код, и
снятие аргументов со стека помогает не всегда.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P16 (цианные проверки) — минус 25 818 тремя правками: снимок без
построения структуры, guard_over_kid только когда кого-то рисуем,
проверка слота без пяти аргументов. Записан и отрицательный результат
внутри третьей: обёртка, которая внутри всё равно звала pop_cd_hit с
пятью аргументами, сделала хуже.
P14 уточнён: fore-проход не «62 778…89 000», а от 4 122 (персонаж
пропущен) до 117 570 (труп Кида в челюстях — широкий кадр в тайле с
передним слоем). Значит в бою он будет ближе к сотне тысяч.
Бюджет лёгкой позиции: 801 768 -> 438 978. До цели 430 000 осталось
8 978 — одна правка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вопрос пользователя после боя в комнате 15: Кид вытеснил стража вправо
(из-за края торчал только меч), убил — труп не появился ни в 15, ни в
соседней справа.
Это не наш баг, а сложение трёх механизмов оригинала: физика стража
работает только в полосе x 44..211, поэтому комнату он не менял;
мёртвый за Кидом не идёт (follow_guard требует alive < 0), и leave_guard
сохраняет его в прежнюю комнату; а из чужой комнаты страж не рисуется
вовсе — при Guard.room != drawn_room оригинал гасит слот (seg000:422).
Труп остаётся приписан комнате 15 с guards_x за правым краем: при
возврате восстанавливается там же, то есть вне видимого поля.
Живьём в SDLPoP сценарий не воспроизводился — вывод из чтения кода, о чём
в записи сказано прямо.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Лёгкая: 628 542 -> 464 796 (-163 746), персонажи не рисуются вовсе.
Тяжёлая: 758 358 -> 617 487 (-140 871), рисуется только Кид — он
действительно стоит под пламенем, а страж нет. Пятирастровые кадры в
тяжёлой позиции исчезли (было 27 %).
Итог восьми позиций: 801 768 -> 464 796 в лёгкой (-42 %). До цели
430 000 осталось 35 000 в лёгкой и 187 000 в тяжёлой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пользователь поймал ошибку в моём расчёте, глядя на экран: страж целиком
правее пламени, пересекаться может только меч.
Проверка по памяти машины подтвердила и уточнила:
страж, спрайт x 257..284 y 18..56
страж, клинок x 241..261 y 31..37
пламя факела x 232..247 y 5..22
Ошибок было две. Первая: координаты пламени я взял по предположению
«факел в колонке 7», а он в колонке 6 (пламя рисуется в ячейке правого
соседа). Вторая, содержательная: ФИЗИЧЕСКОГО ПЕРЕКРЫТИЯ НЕТ ВООБЩЕ — по x
клинок и пламя пересекаются, но по y между ними девять пикселей зазора.
Настоящая причина: pop_cd_touch хранит метку как маску КОЛОНОК по 32 px на
ТРИ ряда по 63 px (cd_row_of). Пламя (y 5..22) и клинок (y 31..37)
попадают в один ряд 0 и одну колонку 7 — cd_quiet считает слот задетым.
148 302 такта, 23 % кадра, за ложную тревогу.
Решение стало проще и точнее: хранить на колонку диапазон y вместо номера
ряда (10 x 2 байта x 2 страницы = 40 байт). Расчётом проверено, что это
спасает стража и НЕ спасает Кида в тяжёлой позиции — там перекрытие
настоящее, и он честно перерисовывается. Вариант с 8-пиксельными полосами
тоже работает, 16-пиксельные уже нет.
Прежние предложения (частичная перерисовка по пересечению, обрезка фона под
персонажем) записаны как НЕ НУЖНЫЕ: они решали задачу, которой нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Постановка пользователя: проверять, нужна ли отрисовка стража, когда он
не двигается. Если движется — лишние ~150 000 тактов приемлемы: в
оригинале во время боя число физических кадров на логический тоже растёт
на единицу.
Замер чтением pop_cd из памяти машины показал, насколько цена
несоразмерна поводу:
страж x 257..284, y 18..56 28 x 39
пламя (0,7) x 264..279, y 5..22 16 x 18
пересечение x 264..279, y 18..22 16 x 5
То есть пламя задевает страже только макушку — 80 пикселей, — а
перерисовывается он целиком за 148 302 такта (85 524 спрайт с клинком и
снимком + 62 778 fore-проход), это 23 % работы кадра. Пересечение при
этом настоящее: дело не в грубости маски меток, проверено числами.
В задаче записаны два варианта: A — частичная перерисовка только
пересечения (безопаснее, укладывается в контракт pop_cd), B — не рисовать
фон там, где он всё равно перекрыт неподвижным персонажем (дешевле, но
обрезанное пламя попадёт в ОЗУ-копию и heal вернёт дыру, когда персонаж
сдвинется).
Заодно уточнено, чем НЕ является P13 (вопрос пользователя): это не
перерисовка комнаты заново каждый кадр — такой вариант стоил бы порядка
3 000 000 тактов, семь растровых кадров, и оригинал так тоже не делает.
Разница в цене ПОСЕЩЕНИЯ тайла: у нас fore_tile сразу блитит, у оригинала
add_*table только кладёт запись, а рисует один draw_table в конце.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>