47 Commits

Author SHA1 Message Date
Александр Петров 18d177407b TASKS: зацеп в прыжке — в список на финальную приёмку уровней 1-3
Точки вызова стоят и в обеих ветках check_bumped, поэтому регрессия
проявится не в зацепе, а в обычном ударе о стену с зажатым Shift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:59:59 +03:00
Александр Петров 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>
2026-08-06 00:57:51 +03:00
Александр Петров c6cadd0140 Закрыты BUG-GRAB-1 и BUG-GATEMOD-1; BUG-SPIKE-1 — в низкоприоритетные
Оба фикса подтверждены игрой, записи с разбором корней переехали в
bug_closed.md.  BUG-GATE-PASS-1 остался ждать сценария, но его оговорка
про BUG-GATEMOD-1 обновлена (тот закрыт).  BUG-SPIKE-1 понижен до низкого
приоритета: маловоспроизводим, смертельность пик подтверждена замером.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:50:26 +03:00
Александр Петров 3078886306 BUG-GUARD-DEAF-1 закрыт: страж оборачивается на вернувшегося Кида
Проверено в игре: комната 11 уровня 2, страж выталкивает Кида в 22, Кид
возвращается бегом — страж оборачивается и достаёт меч.  Разбор корня
(is_guard_notice не взводился ни в одном из пяти мест оригинала) переехал
в bug_closed.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:41:20 +03:00
Александр Петров 4424ea20a8 BUG-SPIKE-1: попиксельная подгонка X тоже не воспроизводит — состояние не позиционное
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:55:38 +03:00
Александр Петров eecb00f911 BUG-SPIKE-1: пользователь не воспроизвёл; смертельность подтверждена замером
Запись переведена в «ждёт сценария»: чистый пробег по убранным пикам
убивает (трасса по кадрам в записи), а наблюдавшееся «нет урона» — это
уже выдвинутые пики, безвредные для бегущего и в оригинале.  Открытым
остаётся только визуальное расхождение со скриншотом SDLPoP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:51:15 +03:00
Александр Петров 2b17408609 BUG-SPIKE-1: замер опроверг гипотезу о раннем триггере
Watchpoint на модификатор пики (ур.2 к.6, тайл 13) на чистом пробеге:
  modif=1 — кадр 11 (беговой), x=112, col=2
  modif=2 — кадр 12 (беговой), x=117, col=3   (Кид на тайле, h=2)
  modif=3 — кадр 177 (frame_177_spiked)       (напоролся)

То есть check_spike_below/check_spiked/is_spike_harmful и тайминг
выдвижения верны, «раннего» триггера нет.  Настоящий корень: пики
ЗАЛИПАЮТ выдвинутыми — пока габарит Кида накрывает колонку, каждый кадр
start_anim_spike переставляет отрицательный модификатор обратно в 0x8F.
Выдвинутые пики (h=1) для бегущего безвредны по правилам оригинала, отсюда
обе жалобы: пробег не убивает и острия остаются на экране.

Открытый вопрос сузился до 2–3 пикселей: код start_anim_spike совпадает с
оригиналом дословно, значит на скриншоте SDLPoP Кид стоит чуть левее и
колонку пики не задевает.  План закрытия — в записи.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:41:42 +03:00
Александр Петров 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>
2026-08-05 21:29:37 +03:00
Александр Петров 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>
2026-08-05 13:45:15 +03:00
Александр Петров cfc3602375 L2: падение с мечом, разбег-прыжок через 3 тайла, стартовое состояние ворот
BUG-FALL-SWORD-1 — start_fall (seg006:1044) был портирован не целиком:
не хватало трёх веток выбора последовательности и уборки меча в ножны.
Из-за seq_7 с set_fall(1,15) (дрейф 1 px/кадр) Кид с мечом уезжал примерно
на тайл вбок; в оригинале это seq_81_fightfall — падение строго вниз.
Сверено по логу SDLPoP: кадры 102..105 дают x = 155,157,159,160.

BUG-RJUMP-1 — run_jump (seg005:0AA8) не выравнивал Кида по кромке пола
перед толчком, а был заглушкой «полировка K3».  Суммарный dx seq_4 —
62 px при тайле 14, то есть провал ровно в три тайла берётся ТОЛЬКО с
кромки: без выравнивания Кид не перепрыгивал его никогда.  Порт —
pop_run_jump_align() в pop_map (беззнаковое сравнение оригинала = «сдвиг
не попал в [-8,-1]»).  На харнессе: было — толчок с x=165, кадр 44 в
колонке 3 (провал); стало — 5 кадров добега, толчок с x=149, кадр 44 даёт
x=87 col=1 row=1.  Остальные 8 сценариев не изменились.

BUG-GATEMOD-1 — load_alter_mod (seg008:198E) был портирован только для
зелий, поэтому ворота с bg=1 («Open» по спецификации DAT, табл. 8)
стартовали закрытыми.  Добавлены ветки gate (1 -> 188) и loose.  Ветка
wall намеренно НЕ портируется: связи стен наш pop_bg считает по типам
соседей в момент отрисовки.  На уровнях 1-3 таких ворот всего двое
(ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5)) — у остальных bg=2, а 2 и 0
ведут себя одинаково.

Убран временный трассировщик pop_dbg_trace/pop_dbg_draw; pop_dbg_trap()
оставлен как многоразовый инструмент.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:34:26 +03:00
Александр Петров 3d8b81c9f6 build: examples вне регрессной сборки; эталон размеров пересобран
`make all` собирал и examples/, из-за чего цикл «правка libc -> проверка»
упирался в mdview (компилируется минутами) и ничего нового про libc не
показывал.  Регресс ловит разжирение библиотеки, а для этого хватает
мелких tests/ — каждый тянет свой кусок libc и пересобирается за секунды.

- `make all` = tools lib tests; examples собираются явно (`make examples`,
  и как зависимость `make floppy`);
- size_check.py смотрит только tests/*.

Эталон принят заново.  Разбор расхождения, чтобы оно не выглядело
необъяснённым: эталон стоял с 30 июля (0280b05), а libc менялась 1 и 3
августа (b56f2b4 kbd_raw_poll, 1f16e8f fake shift) — _irq_tramp вырос
267 -> 336 Б и не был перебазирован, отсюда +33 Б у всех, кто линкует
трамплин (cbl*, irqtest, rt_test), и +22 у gfx_dbuf (gfx_set_idle_hook в
libbgi из того же KBD-1).  Текущий фикс BUG-KBD-5 вернул 36 Б из этих 69.
Заодно выкинуты мёртвые строки эталона (rpgprof/rpgwalk/scroll/space —
их давно нет в APPS, mdview/mdview2 — теперь вне регресса).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:33:34 +03:00
Александр Петров 030af74631 BUG-KBD-5: зажатый Shift снимался автоповтором стрелки
Симптом: Shift работал в одиночку и НЕ работал вместе со стрелками —
прыжок с зацепом не выходил (BUG-GRAB-1), а осторожный шаг срывался в бег.

Декодеры делали из «fake shift» два вывода, и второй был неверен:
  обёртка E0 F0 12 / E0 12 есть  -> Shift зажат -> взвести бит   — верно;
  расширенный make БЕЗ обёртки   -> Shift отпущен -> снять бит   — НЕТ.

Замер потока байт (MAME, breakpoint на выходе из in a,($18)): клавиатура
pc_kbd ms_naturl обёртку не шлёт вовсе — при зажатом Shift поток на ↑ ровно
`E0 75 E0 75 …`, ни одного F0/12.  А typematic-повторы идут непрерывно,
пока стрелка зажата, значит каждый повтор снимал реально зажатый Shift.
Короткий тап это маскировал: после отпускания стрелки Shift снова
становился последней клавишей, и его собственный автоповтор `12` взводил
бит обратно за ~30 мс.

Фикс: обратный вывод убран, расширенная клавиша о Shift не судит.
Состояние Shift ведут его собственные make/break 12 / F0 12 — они приходят
всегда.  Прямой вывод оставлен (дёшев и верен там, где обёртка есть).
Ушла ставшая ненужной _kbdraw_fakesh; трамплин короче на 36 Б (0x150→0x12C),
что важно — его клавиатурный блок упирается в диапазон jr.

Плата: потерянный при overrun break Shift снять нечем, модификатор может
залипнуть до перенажатия (BUG-KBD-3).  Размен решён как и раньше в
kbd_raw_sync: лучше залипание, чем отвал — сорванный посреди игры Shift
в PoP стоит жизни.

Проверено в MAME чтением _kbdraw_down: Shift+↑+→ зажаты 5 с (автоповтор
идёт) -> LSh остаётся 04; отпускание Shift -> 00.  Зацеп в игре
подтверждён пользователем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:33:15 +03:00
Александр Петров 3fe083331f tests-host: покадровый харнесс сценариев Кида (физика + зацеп)
Проверять физику Кида глазами в MAME дорого и ненадёжно: ошибка почти
всегда не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ через несколько
кадров.  Харнесс гоняет тот же кадр, что и главный цикл
(pop_ctrl_tick -> kid_tick -> pop_phys_tick -> pop_loose_tick), и
сравнивает трассу состояния с эталоном.

- scene.c/.h — раннер: комната + стартовая поза + скрипт ввода -> трасса;
  sc_kid_at_x задаёт точный X (исход часто зависит от фазы внутри тайла).
- stubs.c/.h — libc/libbgi/соседние модули; read() реально отдаёт
  kid_data.bin (иначе kdat_ok=0 и play_seq молчит — трасса замирает).
- t_phys.c — 9 характеризующих сценариев, 1723 сверки (golden/).
- t_grab.c — окно зацепа: существует, достижимо коротким шагом, не
  зависит от рисунка нажатий.
- record_golden.py — снятие эталона по одному сценарию за прогон.
- testkit/host-tests.mk — CODE_LOC настраиваемый, EXTRA_INC/EXTRA_CFLAGS.

Именно харнесс дал доказательство, что физика зацепа у нас верна, и тем
самым перевёл поиск BUG-GRAB-1 на клавиатуру.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 12:32:55 +03:00
Александр Петров 29d60665e0 docs: уточнение — при падении с мечом рендер не «кривой», а корректный
Отличие от остальных записей раздела «НЕ БАГИ»: там картинка кривая и
совпадает с оригиналом лишь потому, что оригинал сам так рисует (порядок
midtable).  Здесь же спрайт перекрывает кромку ровно настолько, насколько
персонаж за неё зашёл — геометрия и рендер согласованы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:20:27 +03:00
Александр Петров 969f3f1b9a docs: падение при отходе с мечом — НЕ БАГ (сверено с SDLPoP)
Пользователь проверил в SDLPoP v1.24: оригинал падает с той же позиции,
по той же траектории и с тем же видом кадра падения (голова/руки поверх
кромки пола).  Кадры совпадают один в один.

Механика записана с числами: у стоек с мечом weight_x = 13-14 против 3 у
обычной стойки, поэтому при взгляде влево точка веса уезжает на 13 px
вправо от Char.x, и кромку персонаж переступает раньше, чем выглядит.
Замер: x=151 -> dx_weight=164 -> колонка 7 (дыра) вместо 6 (пол).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:19:24 +03:00
Александр Петров 3164065243 Клинок расширяет футпринт перерисовки на колонку (redraw_at_char)
Порт seg003:0430 «If char is holding sword, it makes redraw-area bigger»:
при Char.sword >= sword_2_drawn футпринт расширяется на одну колонку в
сторону взгляда (вправо при dir>=0, влево иначе).  У нас char_footprint
этого не делал, и клинок торчал на колонку дальше области, где
перерисовываются передние грани: меч оставался поверх столба, а его след
— на фоне.

char_footprint получил параметр sword; pop_fore_over_char — тоже (страж
машет мечом ровно так же).  Ветка Кида берёт Kid.sword, ветка стража —
Guard.sword.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:15:34 +03:00
Александр Петров e67117219f BUG-CTRL-FRAME-1: геометрия Кида считалась по кадру стража
cur_frame — один глобал на всех персонажей (как в оригинале), владелец —
тот, кто последним прошёл load_frame.  Последним в кадре тикает страж,
поэтому к моменту control() Кида там лежал кадр СТРАЖА, а через
kid_cur_dx/dy/flags по нему считается вся геометрия управления:
dx_weight -> determine_col -> distance_to_edge_weight -> get_edge_distance
-> выбор ветки в check_jump_up.

Оригинал зовёт load_fram_det_col() (seg006:0144) сразу после
loadkid/loadshad и ДО control() — play_kid_frame (seg000:1211) и
play_guard_frame (seg000:1248).  У нас этого не было.

Замерено брейкпоинтом на pop_jump_up_seq: Kid x=156 col=6 кадр 15
(dx=0 weight_x=3) при кадре стража image17 (dx=-1 weight_x=8) дал
curr_col=7 и distance=2 вместо 6 и 10 — то есть jump_up_plain (вернулось
A=28, пустой прыжок) вместо «шаг назад на x=160 + зацеп».  Предсказание
по кадру стража совпало с намеренным до единицы.

Отсюда же плавающее поведение: кадр стража меняется каждый тик, distance
Кида скакал через порог 6 — то прыжок, то попытка зацепа с неверной X.
И «голова Кида поверх плиты (1,6)» — не баг отрисовки, а следствие позы,
которой в оригинале в этом месте не бывает.

Фикс: pop_load_fram_det_col() (pop_kid.c) + вызовы в pop_ctrl_tick и
pop_guard_tick.  determine_col — только на ветке Кида: у нас он
существует лишь для него (pop_map работает с Kid, а не с Char).

Разбор с числами — bug_closed.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:55:43 +03:00
Александр Петров 5353bdaaec docs: приёмка уровня 2 — первым приоритетом, карта содержимого уровня
Порядок работ по решению пользователя: L2-PASS -> L3-CHOMP/SKEL/CHKP.
Причина техническая — чомперы лягут в банк 2, где живёт отрисовка, и
чинить баги фона поверх свежей механики дороже.

TASKS: запись L2-PASS с картой уровня 2, снятой с res2002.bin —
стражи (5, комнаты 4/7/11/15/24), ловушки и зелья по комнатам, и
декодированные из LINKLOC/LINKMAP цепочки «кнопка -> что открывает»
(в т.ч. кнопка к.9 @1,1, открывающая дверь выхода в к.23).  Плюс
отдельный список того, что сделано именно в L2 и на уровне 1 не
проверялось: большая склянка, меч с начала уровня, выход через дверь,
респавн на своём уровне.

bug_list: заведён раздел «Уровень 2» под список багов отрисовки,
который пользователь подаст отдельно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:46:12 +03:00
Александр Петров 6aefee0cc7 docs: TASKS — цель «уровни 1-3 (подземелье)», разбор что нужно уровню 3
Palace (уровни 4+) отложен решением 2026-08-04.  На доску вынесены три
задачи уровня 3, снятые с данных и SDLPoP, а не с общих соображений:

- L3-CHOMP: 5 чомперов (комнаты 5, 16×3, 22); шаблон как у пик/ворот,
  риск — банк 2 занят на 86.5%.
- L3-SKEL: в данных уровня 3 стражей НЕТ ВООБЩЕ; единственный враг —
  скелет, и он спецсобытие check_skel (seg002:1044), а не страж из
  данных.  Нужен новый атлас (data/SKEL, 29 файлов) — pop_pack_guard.py
  прибит к GUARD/.
- L3-CHKP: чекпойнт (seg002:519 + seg003:141); hitp_beg_lev уже есть.

Плюс инвентарь тайлов по уровням: ур. 3 вводит только chomper(18),
palace-набор (lattice*) начинается с уровня 4 — отсюда и граница скоупа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:38:43 +03:00
Александр Петров 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>
2026-08-04 11:35:56 +03:00
Александр Петров 7ef007757b docs: BUG-SEAM-DRAW-1 закрыт (не воспроизводится), заведён BUG-GATE-PASS-1
BUG-SEAM-DRAW-1: Кид упирается в решётку и встаёт на x=61, curr_col=-1,
room=1 — и в комнате 1 РИСУЕТСЯ, за решёткой.  При x=61 он уже внутри
системы координат комнаты 1 (obj_x = 6), straddle-смещение не требуется.
Остаток теоретический: при x <= 60 спрайт ушёл бы за левую кромку; полное
лекарство — довести S3, но поводов нет, заводить обратно только по живому
наблюдению.

BUG-GATE-PASS-1: однократное наблюдение — Кид стоял НА тайле решётки (0,9)
комнаты 5, дождался закрытия, пошёл вправо и прошёл в комнату 1.  Повторить
не удалось: в том же месте при x=196/col=9 решётка держит штатно.
Плоскость блокировки для колонки 9 — x=205, а «колонка 9» по m7 это
x ∈ [191,205), то есть при curr_col==9 проход невозможен по построению;
значит наблюдался x >= 205, и поведение может оказаться штатным (решётка
закрылась за спиной).  Гипотеза НЕ подтверждена, поэтому баг оставлен
открытым со списком того, что снять в следующий раз, и с двумя запасными
кандидатами на корень.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:20:01 +03:00
Александр Петров f63751edce docs: BUG-LOOSE-2 закрыт — проверено вручную
Падающий кусок теперь привязан к своей комнате и долетает после ухода Кида;
подтверждено живым прогоном.  Автоматикой гонка не воспроизводилась — мост
MAME шлёт нажатия рывками, и «уйти раньше, чем долетит плита» через него не
набиралось; это отмечено в записи вместе с указанием, что кейс стоит первым
в плане host-тестов.

Вторая волна прогона уровня 1 закрыта целиком: BUG-KBD-4, BUG-RESPAWN-2,
BUG-DRAWORDER-1, BUG-LOOSE-2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:04:13 +03:00
Александр Петров a89b8b30c6 docs: BUG-DRAWORDER-1 закрыт; остаток «ноги поверх головы» — в НЕ БАГИ
Сверено в SDLPoP: Кид, стоящий колонкой правее лежащего трупа, и там
рисуется поверх его головы.  Значит порт верен, а артефакт врождённый:
set_objtile_at_char приписывает персонажа РОВНО ОДНОМУ тайлу, ширина
спрайта на выбор тайла не влияет, и всё, что стоит правее, перекрывает
выступающую часть тела.  Механизма «широкий объект в нескольких тайлах» в
оригинале нет — сверено с draw_objtable_items_at_tile, sort_curr_objs и
веткой tile_object_redraw == 0xFF (та про оверлеи пола).

Закрытая запись перечисляет все четыре корня, которые пришлось снять по
очереди: порядок по роли вместо тайла; своя регрессия с общим окном
fore-клипа; колонка трупа из тайла вместо X; непортированная ветка
actions_1_run_jump.  Записано и то, как отличать остаток от этих багов,
и цена «починки» остатка (тайл по центру габарита = отход от эталона).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 23:02:02 +03:00
Александр Петров 61d4255091 testkit: модульные тесты под ucsim_z80 + планы покрытия
Обвязка для быстрых тестов plain-C логики: секунды вместо прогона в MAME,
без образа диска.  ucsim_z80 идёт в комплекте нашего SDCC — новых
зависимостей нет.

ПОЧЕМУ ПОД Z80, А НЕ ХОСТОВЫМ GCC.  У SDCC z80 int 16 бит, у хоста 32, и
расходится это НЕ в объявлениях, а в выражениях: integer promotion
повышает операнды до int независимо от того, объявлены они как uint8_t
или uint16_t.  Перевод кода на фиксированные типы разницу не убирает —
убирает только исполнение с z80-семантикой.  Побочно проверяется
кодогенерация SDCC и модули с inline-asm, которых хостовая сборка не
видит в принципе.

Устройство: crt0_ucsim.s (SP, зануление, main, halt), tcheck.* (итог в
структуру в ОЗУ), run_ucsim.py (гоняет ucsim, дампит tc_result, печатает
отчёт), host-tests.mk (общие правила).  Вывода через printf нет: тестовый
бинарь линкуется без Sprinter-libc.  Через ucsim-simif не идём — номера
его команд плавают между версиями, halt + dump работают везде.

Наборы лежат РЯДОМ с проверяемым кодом, обвязка общая:
  testkit/t_selftest.c                     — самопроверка (sizeof(int)==2)
  applications/PoP/roomtest/tests-host/    — движок PoP

Первый содержательный набор — t_geom: сверяет рукописный asm-LCG из
pop_geom.c с наивной 32-битной формулой на 128 шагах.  Заявка «бит-в-бит
как в SDLPoP» до сих пор держалась на комментарии.  Тест проверен
мутацией: порча эталонной константы даёт красный.

Планы дальнейшего покрытия:
  docs/host-tests-plan.md                    — libc и libbgi (не начато)
  applications/PoP/docs/host_tests_plan.md   — движок PoP

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:36:16 +03:00
Александр Петров b5d2a81ee3 L1: фиксы прогона уровня 1 — уровень мутабелен, стражи, loose-плиты, порядок
Одиннадцать наблюдений первого прогона свелись к шести корням, четыре
наблюдения второго — ещё к четырём.  Разбор каждого — bug_closed.md.

Первая волна:
- BUG-LVLSTATE-1: уровень стал мутабельным (эталонная копия foretable для
  рестарта, pop_level_set_tile вместо таблицы оверрайдов);
- BUG-RESPAWN-1: рестарт = load_level, тайлы возвращаются из эталона;
- BUG-DEATH-1: смерть от меча доигрывается (порт control_kid, seg006:0CD1);
- BUG-GATE-ANIM-1: ворота в отрисованной комнате перерисовываются
  (POP_RD_GATE, порт draw_trob seg007:01E6);
- BUG-COLL-1: полный порт check_collisions/bumped (seg004) вместо поиска
  стены только в колонке переднего края;
- BUG-STANDUP-1: убран лишний guard в bumped_floor — вставание у стены
  роняло Кида сквозь пол.

Вторая волна:
- BUG-RESPAWN-2: рестарт возвращает и СТРАЖЕЙ (в оригинале play_level на
  каждой итерации делает load_level + pos_guards);
- BUG-LOOSE-2: падающий кусок привязан к своей комнате и долетает после
  ухода Кида (do_mobs крутит mobs[] независимо от drawn_room);
- BUG-DRAWORDER-1: порядок «Кид / страж» задаётся обходом тайлов
  (redraw_needed_tiles: ряды 2,1,0, колонки 0..9), а не ролью персонажа.

По BUG-DRAWORDER-1 понадобилось три захода, и два первых были неполны:
  1) сам порядок — но общее окно fore-клипа осталось стражьим, и Кид
     нарисовался поверх передних столбов (kid_fore_clip_restore);
  2) enter_guard брал curr_col из тайла, а leave_guard пишет туда
     get_tilepos(0,row) — у запомненного ТРУПА колонка была 0 при
     настоящей X.  Теперь колонка выводится из X, как в оригинале;
  3) ветка actions_1_run_jump в set_objtile_at_char оказалась не
     «упрощаемой»: в беге тайл берётся из нижнего ряда и ЛЕВОЙ колонки
     габарита, поэтому бегущий Кид уходит за объекты справа.  Считается
     для обоих персонажей — enter_guard ставит action=1 и стражу.

Проверено в MAME: зелья/меч/плиты переживают выход из комнаты и
восстанавливаются после смерти; кнопка room5 поднимает решётку; падение с
кнопки больше не роняет в комнату 6; убитый страж жив после respawn;
Кид проходит за телом стража.  make size-check — роста нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:35:41 +03:00
Александр Петров 1f16e8fa70 KBD: состояние Shift по «fake shift» — ни залипания, ни отвала
Клавиатура PS/2 обёртывает КАЖДЫЙ расширенный код парой E0 F0 12 / E0 12,
пока реально зажат Shift (замер на железе/MAME: правый шифт обёртывается
своим кодом 0x59).  Это прямое и непрерывное свидетельство состояния
Shift — единственное доступное, потому что опросить PS/2 нельзя, а
typematic повторяет последнюю нажатую клавишу, то есть стрелку.

Оба декодера (_irq_tramp.c, kbd_raw_poll.c) читают обёртку в обе стороны:
обёртка есть -> Shift взвести; расширенный make без обёртки -> Shift
снять.  Бит взводит общий писатель — достаточно обнулить префиксы, и код
уходит в plain-половину карты как make.

Это снимает размен, между крайностями которого мы метались:
  - исключать модификаторы из сброса по overrun -> Shift залипал навсегда
    (BUG-KBD-3);
  - сбрасывать всю карту, как DSS -> Shift сносился каждым overrun'ом, а
    при зажатом Shift тап стрелки это 10 байт в 3-байтовый FIFO, то есть
    overrun почти гарантирован (BUG-KBD-4).
Теперь kbd_raw_sync снова не трогает модификаторы, и это безопасно:
залипание снимается первым же нажатием стрелки.

Раскладка трамплина: клавиатурный блок перевалил за 127 байт, а jp внутри
запрещён (копия в W2).  Префиксные обработчики переехали вплотную к своим
cp, посередине тела стоят ретрансляторы tr_kbd_hub/tr_hub_notkbd/
tr_hub_dss.  В kbd_raw_poll такого ограничения нет — там три jp.

Проверено в MAME: Shift переживает пять тапов подряд; штатное отпускание
снимает; искусственно залипший бит снимается первым тапом.
make size-check — роста нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:16:27 +03:00
Александр Петров ba37bd1133 BUG-DOOR-CLIP: обрезка силуэта правым косяком двери уровня
Симптом (нашёл пользователь сразу после L1-EXIT): при подъёме по лестнице за
дверью уровня силуэт Кида вылезал ПРАВЕЕ правого косяка проёма; по высоте
обрезка была корректна.

Причина — недопортированная половина clip_char (seg006:1231).  Для кадров
двери оригинал ставит ДВА клипа, у нас был только первый:
    obj_clip_top   = leveldoor_ybottom + 1;   // было
    obj_clip_right = leveldoor_right;          // не было
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
проверяет >= frame_224_exit_stairs_8, то есть 224..228 — портировано по коду.

Fore-слоем это не лечится: створка и косяк уходят в оригинале целиком в
backtable (draw_leveldoor, все add_backtable), рисуются ПОД персонажем и
перекрыть его не могут.  Единственный способ — срезать сам спрайт.

libbgi: gfx_blit_cols_part_w(..., uint8_t maxw) — обрезка СПРАВА у
колоночного блита.  Для column-major это ровно уменьшение числа колонок, то
есть внутри ядра механизм уже был (так же клипается край экрана,
w = _bgi_maxx + 1 - x), наружу не выводился.  Тело блита переехало туда,
gfx_blit_cols_part стал тонкой обёрткой (maxw=0) — тем же приёмом, каким
gfx_blit_cols уже обёрнут вокруг gfx_blit_cols_part.  Работает и при flip:
первые maxw нарисованных колонок всегда ложатся в левую часть футпринта.
make size-check: роста нет.

PoP: pop_leveldoor_right / pop_leveldoor_ybottom (порт одноимённых глобалов)
пишет draw_leveldoor в pop_state — их читает clip_char из другого банка;
pop_clip_char_right() отдаёт границу, kid_draw превращает её в maxw и уводит
эти кадры с noclip-пути на общий.  Прямоугольник heal (kid_lw) сужается тоже
— стираем ровно нарисованное.

Проверено в MAME: pop_leveldoor_right = 176, что есть ровно (draw_xh<<3)+48
для двери комнаты 9; pop_leveldoor_ybottom = 112 у закрытой створки и 69 у
поднятой — сходится с формулой оригинала.  Отрисовку подтвердил пользователь
на живом подъёме.

Заодно: ROOMNAV остаётся включённым осознанно — это наш чит, которого в
оригинале не было, как и S/K/I; позже сведём в общий блок читов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:43:01 +03:00
Александр Петров e86f254b87 L1-START + L1-EXIT: старт по данным уровня и выход через дверь уровня
L1-START.  Старт и оба рестарта (смерть, выпадение из уровня) сведены в
pop_start_level() — порт start_level + do_startpos + set_start_pos (seg003).
Комната/тайл/направление берутся из pop_level_start_*, направление
инвертируется (~start_dir), поза входа — из tbl_entry_pose: у уровня 1 это
падение внутрь (seq_7_fall) плюс нажатие кнопки room5(0,2), то самое, что
захлопывает решётку за спиной.  Жёсткие START_ROOM/COL/ROW убраны.
Проверено в MAME: старт даёт room 1, col 0, падение на row 1 — как по данным.

L1-EXIT.  Ветка двери уровня из up_pressed + go_up_leveldoor (seg005:0482/
0574): тайлы и геометрия — pop_leveldoor_enter() в pop_map, последовательность
seq_70 — в pop_ctrl.  Опкод 0xF1 END_LEVEL в play_seq инкрементит
pop_next_level (порт next_level), главный цикл по нему перезапускает уровень
— ровно та точка, куда levels_plan §2.2 подключит загрузку уровня 2.
Открытость двери проверяется по modifier >= 42 (ветка fix_exit_door), а не по
ванильному leveldoor_open: иначе можно войти в ещё ползущую створку.

Отдельно стоило разбора: go_up_leveldoor сначала писал Char.x/Char.direction,
и оба присваивания молча терялись — окно Char вокруг диспетчера возвращает
назад только curr_seq и sword (pop_savekid_state).  Направление оставалось
«вправо», а все DX в seq_70 отрицательные, поэтому Кид уходил ИЗ проёма
влево (поймано стоп-кадром).  Геометрию персонажа в этом порте меняет
pop_map, пишет в Kid — как pop_down_action и pop_jump_up_seq.

Известный остаток — BUG-DOOR-CLIP в bug_list.md: нет обрезки силуэта правым
косяком проёма (недопортирован obj_clip_right в clip_char); нужен вариант
колоночного блита с ограничением ширины.  ROOMNAV пока оставлен включённым —
он нужен, чтобы попадать в комнату 9 для этой работы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:47:47 +03:00
Александр Петров 1146c57544 CLIP-1: heal Кида и стража — линейным ядром, когда клип не нужен
kid_heal/pop_guard_heal звали gfx_heal всегда, хотя рисуют ровно тот
прямоугольник, который блит в большинстве кадров кладёт noclip-ядром.
Все три места heal (+ heal_off фона) сведены к общему pop_heal_fast.

Замер в MAME, счётчики totalcycles на входах kid_heal и kid_tick (вся
группа heal за кадр), комната 1, Кид стоит:
  клипающее ядро  26 200 тактов/кадр (149 кадров)
  noclip          15 848 тактов/кадр (239 кадров)
−10 352 такта, −39.5 %.  A/B в одном прогоне: вторая половина снята с
пропатченным в памяти условием (jr nz → jr), то есть на той же геометрии.

Размер СУММАРНО −362 Б: _CODE +17, BANK2 −116 (свободно 2708 — это тесный
банк из рисков levels_plan §5), BANK3 −263, BANK4 без изменений.

Грабли по дороге: первым заходом хелпер был static inline в pop_bg.h —
SDCC 4.5 И встраивает тело (181 Б) в каждый вызов, И оставляет копию в
каждом TU, который видит заголовок.  pop_guard_heal раздулся с ~60 до
663 Б, итого +1091 Б в _CODE и +636 Б в банке стража.  Отсюда pop_draw.c:
обычная функция в резиденте W1, из банков это прямой call без трамплина.

pop_room_clip_borders оставлен клипающим осознанно (320 не лезет в 8 бит,
гейт border_dirty редкий) — причина записана в коде.

Проверено визуально: ходьба, прыжок, спуск, позиция за решёткой шва
(straddle — там работает клипающий фолбэк) — артефактов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:53:30 +03:00
Александр Петров d552cbaca9 docs: bug_list — только открытые баги; закрытые → bug_closed.md
Три Critical'а (BUG-1 провал на row 1 при боковом переходе, BUG-2 ping-pong
при возврате, BUG-3 окклюзия climb-up на кнопке) висели непроверенными с
2026-07-21.  Прогнал в MAME:

- BUG-1 не воспроизводится: room6 → кнопка (0,2) → открытая решётка →
  переход влево даёт room8, y=55, curr_row=0.  Заодно снят и сам диагноз
  записи — репроекция Y при БОКОВОМ переходе не нужна: goto_other_room
  (seg002.c:390) меняет только x, наш check_leave делает то же.
- BUG-2 не воспроизводится: шов room2↔room3, четыре пересечения с
  разворотом сразу после входа — комната меняется ровно раз на пересечение.
- BUG-3 закрыт фиксом tile_code_drawn от 2026-07-28 (это дубль уже
  записанного «спуск с кнопки»); оговорка про непереснятый подъём — в
  bug_closed.md.

bug_list.md теперь только открытое (BUG-CEIL-1/2/3, BUG-OCCL-1, T-1, T-2,
таблица обхода 24 комнат) + индекс с якорями.  bug_closed.md — закрытое
вместе с разбором корней (odd-pixel char_x, подстановка тайла кнопки, баг
кодогенератора SDCC), он и есть главная ценность архива.

TASKS.md: кросслинки на открытые баги в шапке, в L1-TRIAGE, L1-PASS и
«Отложено».  Указатели в CLAUDE.md/README/room_model_plan/layout_plan_v2
переведены на нужный из двух файлов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:57:33 +03:00
Александр Петров 6b4a3b6b41 docs: итог KBD-1 — что лечит плотный опрос и что осталось
Ручная проверка пользователем: стало значительно лучше, но редкие пропуски
стрелок при зажатом Shift всё же ощущаются.  Счётчики на 35 нажатиях подряд
потерь не показали, то есть остаточная частота заметно ниже прежних ~15 %.
Задача отложена до финальной полировки программы (решение пользователя) —
для работы клавиатура пригодна.

Записано, где именно осталась дыра, чтобы не начинать с нуля: idle-хук
покрывает простой (~2/3 кадра), а в занятой трети DI-окно одного
accel-прохода доходит до ~650 мкс при допуске FIFO ~300 мкс — пачка байт,
целиком попавшая в такое окно, ещё может потерять байт.  Порядок действий
на возврат: вызовы между блитами занятой фазы, замер тем же счётным методом
от 50 нажатий, и только потом рычаги вне нашего кода (Scan Code Set 3 через
BIOS $EA — в MAME непроверяемо; общий m_irq_off_timer в драйвере).

Заодно сняты оговорки «плотный опрос ещё не подтверждён замером» в
kbd_raw.h и libc-reference.md — теперь там штатный рецепт через
gfx_set_idle_hook.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:15:21 +03:00
Александр Петров 4b498d171b libbgi: idle-хук в ожидании кадра; им лечится потеря нажатий с Shift
Причина потерь (замеры — applications/PoP/roomtest/TASKS.md, KBD-1): при
зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», нажатие
стрелки становится 5 байтами вместо 2, а импульс запроса прерывания здесь
теряется примерно в 44 % случаев — трёхбайтовый FIFO SIO переполняется, и
байт пропадает ДО чтения порта.  Лечится только плотным вычерпыванием: раз
в ~0.5 мс.  Столько времени есть даром — при пейсинге «3 растровых кадра на
логический тик» процессор проводит ~42 мс из 60 в gfx_wait_vsync, крутя
опрос луча и больше ничего не делая.

- gfx_set_idle_hook(fn) — что вызывать, пока gfx_wait_vsync ждёт луч.
  Состояние в отдельном data-модуле (_gfx_idle_state.c), чтобы не тянуть
  сеттер в программы, которые хук не ставят.
- Лучевой цикл зовёт хук в обеих фазах.  BC (счётчик таймаута)
  сохраняется, косвенный вызов — push адреса возврата + jp (hl), так как
  `call (hl)` в Z80 нет; без хука это ret по нулевому указателю, порядка
  двух десятков тактов в цикле, который и так сжигает время.
- Путь FPS-делителя не затронут: там ожидание через HALT.
- roomtest вешает на хук kbd_raw_poll.

Проверка в MAME счётчиками (брейкпоинты с { b@ADDR = b@ADDR+1 ; g } на
чтении порта 0x18 и на установке make-бита): 35 нажатий Shift+Home → 35
make, ноль потерь; до фикса было 9 из 10.  Боевой сценарий: четыре Shift+→
подряд дали четыре осторожных шага (Kid.x 114 -> 147).  _CODE +170 Б,
кадровый бюджет не затронут.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:05:24 +03:00
Александр Петров b56f2b4582 libc/kbd: kbd_raw_poll + замер потери нажатий при зажатом Shift (KBD-1)
Симптом: при удерживаемом Shift часть нажатий стрелок не отрабатывает
(~15 % по наблюдению пользователя), без Shift потерь нет.

Переведено в числа: нажимается Home — тоже расширенная клавиша (тот же
E0-префикс и тот же «fake shift»), но игрой игнорируется, поэтому рельеф
комнаты на результат не влияет.  Счётчики — брейкпоинты MAME с действием
{ b@ADDR = b@ADDR+1 ; g } на входе клавиатурной ветки трамплина, на чтении
порта 0x18 и на установке make-бита.

Что измерено (10 нажатий Shift+Home, дошло make):
  игра идёт, опрос ВКЛ   9/10     игра идёт, опрос ВЫКЛ  9/10
  игра ЗАМОРОЖЕНА (блитов нет вообще, длинных DI нет)  8/10

Обе исходные гипотезы отпали:
- длина наших DI-окон ни при чём (в замороженном кадре потерь больше);
- снятие di в accel-ядрах libbgi УРОНИЛО машину — режим «акселератор при
  EI» из docs/new/06-accel.md §6.6 в этой прошивке недоступен.

Байт теряется НИЖЕ нашего кода: на 49 прочитанных байт пришлось только 28
входов в клавиатурную ветку, то есть ~44 % импульсов запроса прерывания не
обслуживается и трёхбайтовый FIFO SIO переполняется.

Потолок приёма измерен отдельной программой tests/kbdpoll (ничего, кроме
kbd_raw_poll в цикле): 25 нажатий -> 25 make, 250 байт из 250, ноль потерь.
Значит опрос лечит полностью, вопрос только в плотности: нужно раз в
~0.5 мс, а шесть вызовов за 60-мс кадр давали раз в 10 мс.

Поэтому вызовы из roomtest.c УБРАНЫ (они стояли там, где прерывания и так
разрешены, и дублировали трамплин — 9/10 с ними и без).  Сама функция
kbd_raw_poll оставлена в libc: она корректна и нужна как основа плотного
опроса.  В заголовке и в libc-reference — честная оговорка, чтобы её не
ставили в игровой цикл «на всякий случай» без замера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:33:16 +03:00
Александр Петров 774b1cc7c4 docs(PoP): документация к актуальному статусу + план следующих уровней
Документы отстали от кода: PORT_PLAN писал «PoC не начат», хотя играется
весь уровень 1, а четыре плана были исполнены целиком.

- PORT_PLAN: таблица статусов по разделам; фазы 0-3 сделаны, 4-6 нет;
  риски §8 п.1/п.3 закрыты, п.2 переформулирован под реальный движок
  (спрайтовый движок для персонажей не используется, лимит «21 спрайт»
  неприменим), п.4 — найдено расхождение таймингов: оригинал считает
  логический кадр за 5 тиков при BASE_FPS=60 (83.3 мс, в бою 100 мс), а мы
  ждём три vsync (60 мс) — игра идёт примерно на 39 % быстрее эталона.
- levels_plan.md — новый: машинерия перехода между уровнями, второй
  тайлсет (palace), потабличные различия и читы SDLPoP, которые окупаются
  сразу.  Инвентарь тайлов снят прямо с res200N.bin: уровень 2 не требует
  ни одного нового ассета и ни одной новой механики.
- roomtest/TASKS.md — новый: доска текущих задач с критериями готовности.
- Удалены как исполненные и перекрытые кодом: clip_char_plan,
  double_buffer_plan, loose_floors_plan, size_optimization_plan.  Его §8
  (замеры скорости отрисовки) не был перекрыт — перенесён в
  layout_plan_v2 §9, чтобы не потерять цифры.
- KID_PLAN / gates_spikes_plan — шапки «реализовано, оставлено
  справочником»; room_model_plan — «S1 сделан, остальное не срочно».
- docs/README.md стал индексом с отметками актуальности.
- ideas_backlog: зелье переворота экрана — оригинал переворачивает готовый
  буфер построчно, спрайты не трогает; по данным уровней тип 4 встречается
  только на уровне 9, до него механика не нужна.
- examples/scroll: ссылка на удалённый план вела к неверному факту
  «теневая копия одна — общая»; заменено на подтверждённое «у каждой
  страницы своя».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:32:48 +03:00
snark13 2f3e854854 PoP roomtest: отрисовка стража — в собственный банк (банк 2 упёрся в потолок)
Банк 2 (pop_bg + pop_gdraw) подошёл к границе страницы вплотную: 16 021 из
16 384, свободно 363 байта.  А расти ему ещё есть куда — тайлы поздних
уровней, чомперы, зеркало, анимации смерти стража.

pop_gdraw.c уехал в банк 4 (n_banks 3 -> 4):
  банк 2  16 021 -> 13 792  (84.2 %, свободно 2 592)
  банк 4              2 236  (13.6 %, свободно 14 148)

Цена: pop_fore_over_char стал кроссбанковым, поэтому помечен __banked —
один трамплин (~654 такта) за кадр, других вызывающих у него нет.
pop_fore_set_clip уже был __banked, так что там ничего не изменилось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:22:27 +03:00
snark13 75a51fb1db PoP roomtest: выпивание зелья (уровень 1 — склянка здоровья)
Каркас предметов уже был (check_get_item/do_pickup/proc_get_object), пустой
оставалась только ветка зелий.  Порт seg005 get_item + seg006
proc_get_object:

- pop_get_item_action теперь отдаёт 3 = «пить» и делает do_pickup с ТИПОМ
  зелья, который лежит в старших битах модификатора тайла (modif >> 3);
- pop_ctrl на код 3 запускает seq_78_drink;
- эффекты: тип 1 (здоровье) — +1 HP через hitp_delta и красная вспышка,
  причём как в оригинале только если HP не полные; тип 5 («злое») — −1 HP.
  Типы 2/3/4/6 (жизнь, перо, переворот, открыть ворота) — свойства поздних
  уровней, портируем вместе с ними.

Вспышка фона получила цвет: меч даёт ярко-жёлтую (было), зелье — красную
(flash_color оригинала; двух значений достаточно, других в игре нет).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:09:36 +03:00
snark13 a32b66700f toolchain: make_hdd.sh — закрывать mtools ОБА канала запроса, не один
Прошлая правка убрала только управляющий терминал (os.setsid), и mmd
переключился на stdin: lsof показал fd 0 = /dev/ttys002, процесс снова спал,
теперь уже после отметки «копирование файлов».

Каналов, откуда mtools может ждать ответ, два — /dev/tty и stdin — и
закрывать надо оба.  Обёртка mt() теперь и создаёт новую сессию, и подаёт
stdin из /dev/null.

Проверено: с stdin=/dev/null mmd на свежем образе отрабатывает с кодом 0, а
на уже существующем каталоге честно возвращает 1 и ничего не спрашивает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:01:39 +03:00
snark13 59e51f7e83 toolchain: make_hdd.sh не виснет при запуске из терминала
Симптом: сборка образа молча вставала навсегда сразу после эхо-строки
команды.  Появилось не «само» — ровно тогда, когда образ разложили по
подкаталогам (BG/KID/GUARD/LEVELS) и в скрипте появился mmd.

Диагноз по артефакту, а не по догадке: зависший процесс — `mmd z:/BG`,
и lsof показал fd 0 = /dev/null, fd 4 = /dev/tty.  То есть mtools (собран
с enable-raw-term) для интерактивного вопроса открывает УПРАВЛЯЮЩИЙ
ТЕРМИНАЛ напрямую, в обход stdin — поэтому ни `< /dev/null`, ни
перенаправления stdio не помогают.  А `2>/dev/null` на mmd прятал сам
вопрос, из-за чего это выглядело как зависание на пустом месте.
Из НЕинтерактивного запуска (CI, фоновая задача) терминала нет, вопрос не
задаётся, и баг не воспроизводится — потому и жил незамеченным.

Лечение: все вызовы mtools идут через обёртку mt(), которая запускает их в
НОВОЙ СЕССИИ (os.setsid + exec питоном; setsid(1) в macOS нет).  Без
управляющего терминала открывать /dev/tty нечего, и mtools выбирает
неинтерактивный путь.

Заодно добавлены отметки этапов («разметка и формат», «копирование
файлов», «конвертация RAW -> CHD»): если что-то встанет снова, будет сразу
видно где, а не после последнего аргумента команды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:57:39 +03:00
snark13 b22cee3456 PoP roomtest: убитый страж остаётся мёртвым; меч только по подбору или читу S
Смерть стража теперь персистентна между входами в комнату — по механизму
оригинала, а не отдельной таблицей «убит/не убит».  В SDLPoP массивы
level.guards_* лежат в ОЗУ и движок их ПЕРЕПИСЫВАЕТ: leave_guard (seg002:02F5)
кладёт туда позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq;
enter_guard, увидев непустой seq_hi, поднимает стража прямо в этой
последовательности и по кадру смерти (185/177/178) ставит alive = 1.

У нас уровень лежит в EMM-странице только на чтение, поэтому в W2 добавлена
живая копия — 6 байт на комнату (tile/dir/x/skill/seq_lo/seq_hi):
- pop_guard_leave() в начале enter_room запоминает уходящего стража;
- pop_guard_enter поднимает труп сохранённой последовательностью И
  сохранённой X (pos_guards пересчитывает её из колонки только при загрузке
  уровня, дальше ею владеет leave_guard — иначе тело прыгает в центр тайла).

ГРАБЛИ: guards_seq_lo/hi в ФАЙЛЕ уровня не используются, там 0xFF во всех
комнатах (оригинал чистит их в reset_level_unused_fields).  Прочитав их как
есть, я скормил интерпретатору curr_seq = 0xFFFF, и приложение зависало —
бордюр оставался синим, цикл не доходил до vsync.  Живая копия стартует
нулями: 0 = «поднимать стандартной стойкой».

Меч Киду больше не выдаётся автоматически: DEBUG_SWORD_ROOM убран, вместо
него чит S (выдать меч).  Штатный путь — подобрать с пола.

Проверено в MAME: чит K убивает стража, уход из комнаты 3 и возврат —
тело на месте, страж не воскресает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:33:21 +03:00
snark13 2e90eaf7d7 PoP roomtest: окно Char больше не затирает правки pop_map (спуск с уступа)
Регрессия от окна Char вокруг control() (cf06896): диспетчер работает с
копией Char, а часть его действий у нас исполняет pop_map (pop_down_action,
pop_jump_up_seq, safe_step, зацеп) — и пишет ПРЯМО в Kid, потому что на Char
он ещё не переведён.  Завершающее `Kid = Char` затирало эти правки:
выравнивание x и ряд терялись, и спуск с уступа через вис не срабатывал —
Кид просто приседал.

pop_savekid_state теперь копирует только то, что диспетчер реально меняет
у персонажа: curr_seq и sword.  Когда pop_map переведём на Char, вернётся
полное копирование — в комментарии это зафиксировано.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:51:49 +03:00
snark13 3983fa4513 PoP roomtest: HP-учёт, индикаторы HP, чит бессмертия; фикс кэша кадра
Боёвка (порт seg002/seg006):
- check_sword_hurting / check_hurting / check_sword_hurt / hurt_by_sword /
  take_hp через дельты; do_delta_hp сводит их раз в кадр;
- парирование (justblocked), refractimer после ранения стража;
- смерть по seq_71_dying — через неё же теперь работает чит K: страж
  действительно погибает, а не замирает на месте;
- парные окна Char/Opp: loadkid_and_opp / savekid_and_opp /
  saveshad_and_opp.

Индикаторы HP (порт draw_kid_hp / draw_guard_hp): Кид слева, страж справа.
Перерисовка ТОЛЬКО при изменении числа и тогда на ОБЕИХ страницах
дабл-буфера (счётчик hp_todo, иначе на второй странице осталось бы старое
значение и мерцало через кадр); pop_hp_invalidate при входе в комнату, где
фон перерисован целиком.

Чит I — бессмертие Кида (нашего изобретения, в оригинале его нет).
Перекрывает и путь «безоружного закалывают насмерть»: тот идёт мимо HP, и
без этого чит бесполезен ровно там, где нужен.

ДВА НАЙДЕННЫХ БАГА:
1. Полосу HP блитил row-major примитивом, а атласы Кида и стража хранятся
   COLUMN-major (ради бесплатного флипа) — марки выходили транспонированными.
   Теперь колоночный блит, стрелки как в оригинале.
2. Кэш кадра для ОТРИСОВКИ заполняли pop_savekid/pop_saveshad.  Любое окно
   Char БЕЗ play_seq — а это оба окна боёвки — записывало Киду кадр, который
   принадлежал СТРАЖУ, и kid_draw искал этот image в атласе Кида, рисуя
   произвольную позу.  Владельцем кэша стал load_frame: он один знает, чей
   кадр загружен (по Char.charid).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:43:39 +03:00
snark13 d014a3f577 PoP roomtest: ИИ стража — подход к Киду и боевые ветки диспетчера
Пункт 2 плана закрыт: страж не только замечает Кида, но и идёт к нему и
дерётся.  Порт по SDLPoP, диспетчер общий — ИИ выставляет те же control_*,
что и клавиатура игрока.

guards.c (банк 1), порт seg002:
- autocontrol_guard_active (737) + kid_in_sight (0A93) + kid_armed (0AC1)
  + kid_far (09CB);
- guard_advance / guard_block / guard_strike с таблицами вероятностей по
  12 градациям мастерства (seg002:26..38), бросок prob > prandom(255);
- move_2_backward / move_3_up / move_6_shift / move_down_back;
- таймеры justblocked / kid_sword_strike / guard_refrac убывают раз в кадр
  в autocontrol_opponent, как в оригинале.

pop_ctrl.c, порт seg005: control_with_sword (964), swordfight (0CDB),
sword_strike, parry, forward_with_sword, back_with_sword.  Ветвление у
Кида и у соперника разное — соперник блокирует только на кадре 152, Кид
ещё и по 153 (и тогда последовательность прокручивается сразу).

pop_guard.c: guard_skill из данных уровня (12 градаций, вне диапазона -> 3),
HP по get_guard_hp (extrastrength[skill] + tbl_guard_hp[уровень]),
собственный сид бросков pop_fight_seed — иначе перерисовка стены сбивала бы
решения стража.

char_opp_dist переехал из банка в pop_kid.c: он нужен по ОБЕ стороны
банковой границы — и ИИ, и диспетчеру боёвки.

Проверено в MAME (комната 3): страж проходит комнату, встаёт в дистанцию и
машет мечом, позы меняются.  Урона пока нет — HP-учёт и check_hurt
следующим шагом, без них бой не заканчивается.

Бюджет В БОЮ (175 кадров): 412 224 – 421 068 = 0.958–0.979 кадра.
В покое было 400 800.  Запас в худшем кадре ~9 000 — тесно, но в один
кадр укладываемся; оптимизация отложена сознательно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:24:53 +03:00
snark13 4f7d9c0596 PoP docs: запасные PRNG (LFSR/LCG, xorshift(7,9,8)) + оценка потолка выигрыша
Тексты обеих Z80-процедур, разбор их устройства и качества, почему НЕ берём
8-битный RND Apple II (вырожденные младшие биты — раскладка кладки читает
prandom(1), вышла бы шахматка), и главное — сколько это реально даст.

Потолок выигрыша 2 814 тактов за кадр (0.65 %): тело генератора уже не
основной расход, остаются обёртка pop_prandom, pop_rnd_fit и ABI вызова.
Поэтому первый шаг, если упрёмся, — слить приведение к диапазону в ту же
asm-процедуру (один call вместо трёх), и только потом менять генератор.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:10:35 +03:00
snark13 37fc572cc3 PoP roomtest: LCG оригинала на ассемблере — точность без потери скорости
Возврат к БИТ-В-БИТ генератору оригинала по умолчанию (POP_PRANDOM_EXACT=1):
по нему проще отлаживать и сверять картинку с эталоном.  Чтобы это не
стоило процента бюджета, сам шаг LCG переписан на Z80-ассемблере —
единственное место в порте, где это сделано, с явного разрешения.

Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 — схема Горнера по
РАЗРЕЖЕННОЙ записи константы.  Вместо 12 сложений (по числу единиц в
0x343FD) — 17 удвоений, три сложения и одно вычитание; величина 3*s,
нужная в конце, попадается по дороге на втором шаге.

Проверка в ДВА этапа:
- схема на хосте: horner(s) == s*214013+2531011 на 3 000 000 сидов;
- сама asm-транскрипция на живой машине: breakpoint на pop_prandom,
  11 последовательных состояний сида из MAME — каждый переход совпал с
  s*214013+2531011 бит-в-бит.

Замер, комната 3, 175 кадров (медиана кадра / prandom->torch_draw):
  C, бит-в-бит (16-бит половины)   403 632 / 10 933
  C, xorshift16 + шаг Вейля        397 986 /  7 927
  asm, бит-в-бит                   400 800 /  9 331
То есть asm вернул половину разрыва (2 832 такта за кадр), сохранив
совместимость с эталоном.  Ветка xorshift оставлена под
-DPOP_PRANDOM_EXACT=0 как запасной ход — брать её имеет смысл, только
если не хватит последних 2 800 тактов.

Итог оптимизационного круга: 416 154 -> 400 800 (0.968 -> 0.932 кадра).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:05:13 +03:00
snark13 99b430f2ed PoP/libbgi: убрана 32-бит арифметика, noclip для column-major, быстрый PRNG
Ревью на 32-бит сделан ПО ASM, а не по коду (искали и безымянные
временные): во всём приложении был ровно ОДИН 32-битный вызов —
__mullong в pop_prandom.  Замер в MAME: 8 430 тактов на вызов, два
вызова за кадр.  Прочие библиотечные вызовы 16-битные (__divsint 16,
__modsint 12, __moduchar 8, __divuchar 5).

1. pop_prandom.  Состояние 32-бит -> две 16-битные половины.  Два
   генератора, выбор через POP_PRANDOM_EXACT:
   - 0 (по умолчанию) — xorshift16 + шаг Вейля, без единого умножения;
   - 1 — LCG оригинала бит-в-бит, посчитанный половинами (для сверки
     картинки с эталоном).
   8-битный RND Apple II (5*x+23 mod 256) НЕ взят: у LCG по модулю 256
   вырождены младшие биты (бит 0 просто чередуется), а раскладка кладки
   берёт как раз prandom(1) и prandom(4) — вместо шума вышла бы
   правильная шахматка.  Шаг Вейля ещё и убирает ноль как неподвижную
   точку xorshift (сид кладки вполне может быть нулём).
   Бит-в-бит эквивалентность half-word версии проверена на хосте:
   70 000 сидов x 8 шагов + 7 крайних сидов x 2000 шагов.
   Остаток 0..maxv: делитель степень двойки — маска вместо __moduint.

2. libbgi: gfx_blit_cols_part_noclip — column-major блит без клипа
   (пара к gfx_blit_cols_part, как gfx_blit_part_noclip к
   gfx_blit_part).  Клипающий вариант платит ~5 622 такта подготовки на
   КАЖДЫЙ вызов независимо от того, вылезает край (замер: подготовка
   5 622 против 13 596 на сам accel-проход).  Kid, страж и клинок
   выбирают путь по pop_onscreen_cols.  size-check: роста нет.

Бюджет (175 кадров, комната 3, медиана):
  было (после клинка)   416 154   0.968 кадра
  стало                 397 986   0.926 кадра
Разница между генераторами, замер на одинаковой сборке:
  xorshift16 + Вейль    397 986   prandom->torch_draw  7 927
  бит-в-бит LCG         403 632   prandom->torch_draw 10 933
то есть точность обходится в 5 646 тактов за кадр (1.3 %).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:48:37 +03:00
snark13 d438a1d3da PoP roomtest: клинок в атласе целиком + раздельный heal накладных спрайтов
Меч в оригинале ОДИН на всех: и Кид, и страж рисуют клинок из chtab_0
(add_sword_to_objtable, seg006:1798).  Раньше sword.atl содержал только
кадры подъёма/ножен (sword_tbl 35..42), поэтому у стража меча не было
видно вовсе.

- pop_pack_kid.py пакует chtab_0 целиком (id 0..33, 5168 Б), индекс в
  атласе = id;
- pop_extract_kid_data.py вытаскивает sword_tbl (53 строки) в kid_data.h
  макро-инициализаторами — таблица ложится в один TU, а не в каждый;
- pop_sword_draw (pop_kid.c) — общая точка отрисовки клинка с полным
  условием оригинала (кадры 229..237 ИЛИ меч обнажён ИЛИ живой страж);
  зовут и kid_draw, и pop_guard_draw.

Раздельный heal накладных спрайтов.  Клинок и брызги урона раньше
объединялись в один прямоугольник с персонажем, а объединение почти вдвое
больше суммы двух (клинок уходит вперёд-вверх) — heal же стоит ровно по
площади.  Теперь у накладных свой прямоугольник и свой gfx_heal, а
объединение осталось ТОЛЬКО для окна fore-клипа: там это 4 сравнения без
рисования, но покрыть клинок обязано, иначе он полезет поверх столба.

Отладка: DEBUG_SWORD_ROOM — Киду выдаётся меч при входе в комнату 3
(там страж), чтобы не бегать за ним в комнату 15.

Бюджет (225 кадров, комната 3): 415 284 – 416 370 = 0.966–0.968 кадра.
Против 384 168 – 384 636 до этого шага, то есть +31 700.  Разложение по
фазам (медианы): process_trobs 83 190, pop_guard_draw 92 483 (спрайт
28 763 + клинок 23 820 + fore_over_char 39 906), kid_draw 59 089, fore
поверх Кида + борта 47 483, heal 40 068, логика 76 338, ввод 13 050.
Видно, что клинок 21x8 стоит почти как спрайт стража — это фиксированные
накладные расходы клипающего блита, а не пиксели; лечится noclip-путём
для column-major (см. gfx_blit_noclip_fast).  Отдельным шагом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:40:08 +03:00
snark13 79ea473910 PoP roomtest: страж замечает Кида и достаёт меч (ИИ, шаг 1)
Первый самостоятельный кусок ИИ стража (план, пункт 2).  В оригинале у
соперника нет своего диспетчера: autocontrol_* (seg002) выставляет те же
глобалы control_*, что и ввод игрока, а дальше исполняется общий control().
Инфраструктура под это встала прошлым коммитом, здесь — сама логика.

Порт:
- check_can_guard_see_kid (seg003:688) — луч видимости по ряду: стены и
  верхи дверей рвут его совсем, loose/чомпер/дыра/неподнятые ворота дают
  «вижу, но не пойду».  В guards.c (банк 1);
- Opp + loadshad_and_opp (seg006:841) и char_opp_dist (seg006:2135);
- autocontrol_guard_inactive (seg002:710) + move_* (seg002:0706..);
- ветки control(): control_guard_inactive (seg006:2123) и draw_sword
  (seg005:945) — соперник уходит сразу в seq_90 en garde;
- pop_guard_tick перестроен по play_guard_frame (seg000:1246): окно
  Char/Opp вокруг ИИ, диспетчера и play_seq.

По дороге:
- Kid.alive не выставлялся (= 0 = «мёртв» в семантике оригинала), из-за
  чего луч видимости не мог сработать в принципе — ставим -1 в kid_init;
- Kid.room не выставлялась вовсе; условие Kid.room == Guard.room всегда
  было ложным.  Ставим в enter_room (полная модель Kid.room != drawn_room
  у шва по-прежнему впереди);
- pop_tile_at — тайл текущей комнаты наружу из pop_map (луч видимости);
- control_x/y/shift открыты в шину: ИИ заполняет оси как есть, без
  flip_control_x (его «вперёд» уже в системе персонажа).

Проверено в MAME (комната 3, страж на tile 17): страж переходит из
стойки 166 в 171 stand_with_sword, sword=2, can_guard_see_kid=2; ввод
игрока не пострадал.  Клинок отдельным спрайтом пока не рисуется —
sword.atl содержит только кадры 229..237 (подъём меча Кидом), остальные
строки sword_tbl приедут с боёвкой.

Бюджет (225 кадров, комната 3): 384 168 – 384 636 тактов, 0.893–0.895
кадра, запас 45 364.  Прошлый замер 382 584 – 383 064 — шаг стоил ~1 570.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:09:12 +03:00
119 changed files with 10390 additions and 1616 deletions
+16 -4
View File
@@ -1,10 +1,14 @@
# Sprinter C Compiler — top-level Makefile # Sprinter C Compiler — top-level Makefile
# #
# make build host tools, libc archive, all tests, all apps # make build host tools, libc archive, all tests
# make tools build only host tools (mkexe) # make tools build only host tools (mkexe)
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi) # make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
# make tests build all libc feature tests under tests/ # make tests build all libc feature tests under tests/
# make examples build all real applications under examples/ # make examples build all real applications under examples/ (НЕ входит
# в `make all`: это регрессная сборка, а examples/ —
# крупные приложения, которые её только замедляют
# (mdview компилируется минутами) и ничего нового про
# libc не показывают. Собирать явно перед `make floppy`.)
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img # make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
# make check run mkexe unit tests # make check run mkexe unit tests
# make clean remove all build artefacts # make clean remove all build artefacts
@@ -39,9 +43,9 @@ DATA_FILES := \
examples/mdview/SAMPLE.MD examples/mdview/SAMPLE.MD
.PHONY: all tools lib tests examples check clean sdcc floppy \ .PHONY: all tools lib tests examples check clean sdcc floppy \
size-check size-baseline $(TESTS) $(APPS) size-check size-baseline host-tests $(TESTS) $(APPS)
all: tools lib tests examples all: tools lib tests
tools: tools:
$(MAKE) -C toolchain/mkexe $(MAKE) -C toolchain/mkexe
@@ -73,6 +77,14 @@ floppy: tests examples tests/seek/big.txt
@echo "Floppy ready: $(FLOPPY_IMG)" @echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh" @echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
# ucsim идёт в комплекте нашего SDCC.
HOST_TEST_DIRS := testkit applications/PoP/roomtest/tests-host
host-tests:
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv. # Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
size-check: size-check:
python3 toolchain/size_check.py python3 toolchain/size_check.py
+9 -5
View File
@@ -59,11 +59,15 @@ Princed для DAT v1.0 (контейнер/индекс/чек-сумма, ко
## Структура папки ## Структура папки
- `docs/` — планы и форматы: `PORT_PLAN.md` (общий план фаз), - `docs/` — планы и форматы; **индекс с отметками актуальности —
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы `docs/README.md`**, начинать чтение оттуда. Ключевое:
ресурсов. Начинать чтение отсюда. `levels_plan.md` (следующий этап), `layout_plan_v2.md` (раскладка кода по
- `roomtest/`**активная разработка**: комната 1 + Kid (анимация, ввод, окнам/банкам + скорость отрисовки), `PORT_PLAN.md` (карта фаз со
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`. статусами).
- `roomtest/`**активная разработка**: уровень 1 целиком (Kid, стражи,
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
`roomtest/TASKS.md`, открытые баги — `roomtest/bug_list.md`, закрытые с
разбором корней — `roomtest/bug_closed.md`.
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`. - `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии. - `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`). - `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
+11 -4
View File
@@ -4,15 +4,22 @@
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
0x81), VGA-256 ассеты оригинала переносятся почти впрямую. 0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md). **Состояние (2026-08-01): играется весь уровень 1** — комнаты и переходы,
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md). Kid со всем набором действий, ловушки, ворота, дверь уровня, меч и бой,
стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука,
таймера/HUD, сохранений.
- Что в работе прямо сейчас — [`roomtest/TASKS.md`](roomtest/TASKS.md).
- Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md).
- Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
- Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
## Что где ## Что где
| Папка | Назначение | | Папка | Назначение |
|-------|-----------| |-------|-----------|
| `roomtest/` | **Активная разработка.** Комната 1 уровня 1 живой композицией тайлов + Kid: анимация (seqtbl), управление с клавиатуры, коллизия, падение, зацеп/подтягивание, fore-окклюзия, проваливающиеся полы. Свой README/CLAUDE. | | `roomtest/` | **Активная разработка.** Уровень 1 целиком: фон композицией тайлов, Kid (seqtbl-анимация, ввод, коллизия, падение, зацеп, окклюзия), ловушки, ворота, стражи, бой. Свой README/CLAUDE/TASKS. |
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. | | `docs/` | Планы и разбор форматов ресурсов Apple II / DOS — см. индекс в [`docs/README.md`](docs/README.md). |
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. | | `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. | | `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. | | `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
+16 -1
View File
@@ -1,6 +1,21 @@
# Prince of Persia — Kid (персонаж): анализ и план # Prince of Persia — Kid (персонаж): анализ и план
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c` > **Статус: РЕАЛИЗОВАНО (2026-08-01).** Kid играется целиком: интерпретатор
> `seqtbl` + `frame_table` (`roomtest/pop_kid.c`), диспетчер `control()`
> (`pop_ctrl.c`), коллизия/физика/зацеп (`pop_map.c`), бой и HP. Модель
> персонажа стала общей: `Char`-окно (`pop_state.c`) обслуживает и Кида, и
> стражей. Таблицы кадров и `seqtbl` уехали из `_CODE` в EMM-страницу
> (`kid_data.bin`, см. `layout_plan_v2.md` шаг 1).
>
> Отступление от §2.1 плана: выбран ПАДДИНГ кадров (общий канвас), а не
> per-frame offset — компромисс зафиксирован в `PORT_PLAN.md §6.1`.
>
> **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`,
> категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен
> при портировании остальных акторов (скелет, тень, визирь). Текущие
> задачи — `../roomtest/TASKS.md`.
Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей), (ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты). `types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см. Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
+98 -52
View File
@@ -1,8 +1,29 @@
# Prince of Persia на ZX Sprinter — план порта # Prince of Persia на ZX Sprinter — план порта
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) — ## СТАТУС (обновлено 2026-08-01)
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на Документ составлен 2026-07-15 как план «с нуля» и с тех пор во многом
исполнен. Читать его надо так:
| Раздел | Что с ним сейчас |
|--------|------------------|
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS.md` (KBD-1) |
| §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) |
| §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) |
| §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой |
| §6 модель движения | **сделано**: `play_seq` + `frame_table` оригинала, не физика с нуля |
| §7 фазы | см. отметки статуса прямо в разделе |
| §8 риски | п.1 закрыт, п.3 закрыт (28 страниц-атласов Кида), п.2/п.4 — см. отметки в разделе |
| §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` |
**Где смотреть текущее состояние, а не план:** `../roomtest/README.md`
(что играется), `../roomtest/TASKS.md` (что в работе), `levels_plan.md`
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
---
Опирается на
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в `APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и `applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
@@ -227,6 +248,13 @@ dirty-биты, heal против фона через ОЗУ-копию); `room_
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP ## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
> дальше не развивается: управление ощущается как PoP, held-state работает.
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
> решений (в частности §5.1 — почему сначала был плейсхолдер).
**Объём**: одна комната (например Level 1, экран старта Кида), без **Объём**: одна комната (например Level 1, экран старта Кида), без
переходов между экранами, без стражников (стретч-цель, не обязательна). переходов между экранами, без стражников (стретч-цель, не обязательна).
@@ -364,45 +392,53 @@ memory/png_strip_padding_tradeoff.
## 7. Полноценное приложение — фазы (после PoC) ## 7. Полноценное приложение — фазы (после PoC)
Порядок — по риску и зависимостям, не по геймплейной важности. Порядок — по риску и зависимостям, не по геймплейной важности.
**Отметки статуса — на 2026-08-01.**
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает): **Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после - Хелд-стейт клавиатуры по §2 — сделан.
подтверждения пользователем). - Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный кладётся на образ как есть и читается по офсетам в рантайме
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме — (`roomtest/pop_level.c`), уровень живёт в EMM-странице.
не обязательно разворачивать в C-struct с указателями). - Конвертер фона в растры **отменён осознанно** (§4): фон собирается
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение `pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не страниц, риск §8 п.3 закрыт).
хватит текущего лимита — см. риск в §8).
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/ **Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`). комнатами во все четыре стороны. Осталось: **старт по данным уровня**
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
`../roomtest/TASKS.md`.
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф **Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы `LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение), потолка), зелья, дверь уровня (открывается), факелы. Подробности и
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`. справочник — `gates_spikes_plan.md`.
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/ **Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто персистентность трупа между комнатами.
спереди/сзади».
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида — **Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md` уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
§3), толстый стражник/визирь (общая база анимации с визирем). «уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из **Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
дешёвый бипер без CBL, если формат подтвердится простым парсингом. опциональный дешёвый бипер без CBL, если формат подтвердится простым
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
точки вызова уже на месте.
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни), **Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
сохранение прогресса (FILE*), финальные катсцены — по минимуму, (таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
геймплейно не критично. минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
palace, потабличные различия уровней). Отдельный документ —
`levels_plan.md`.
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME **Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета (`mame_interactive.py`), затем на реальном железе; профилирование бюджета
@@ -418,23 +454,33 @@ tiny/small.
(по правилу `defer_unexplained_quirks` — не гадать, проверять) (по правилу `defer_unexplained_quirks` — не гадать, проверять)
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать 1. ~~**Held-state клавиатуры** (§2)~~**закрыт** (`<kbd_raw.h>`). Открытый
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика). остаток — не «есть ли held-state», а потеря байт при аккордах
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками + Shift+стрелка: `../roomtest/TASKS.md`, KBD-1.
несколько анимированных ловушек может приблизиться к лимиту 2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным спрайтовый движок для персонажей не используется, поэтому лимит
уровням (сколько объектов одновременно активно в худшем экране). «~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько (~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
атласов на актора с переключением по фазе действия (стоять/идти отдельно в `roomtest.c`. План выжимания — `../roomtest/TASKS.md` (CLIP-1) и
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные `../roomtest/bug_list.md` (T-1/T-2).
атласы — оценить фактический байтовый вес конвертированных кадров Кида 3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
прежде чем проектировать. атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации мульти-страничного формата `.atl` не потребовалось. Побочно
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц подтвердился компромисс паддинга (§6.1).
Sprinter; если оригинал считался на другой частоте — потребуется 4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут расхождение.** Цифры оригинала (SDLPoP): базовый таймер `BASE_FPS = 60`
визуально быстрее/медленнее эталона. (`types.h:1373`), логический кадр игры — `base_speed = 5` тиков
(`data.h:869`), то есть **83.3 мс (12 лог. кадров/с)**; в бою
`fight_speed = 6` → **100 мс (10/с)**. У нас (`roomtest.c`) — три
ожидания `gfx_wait_vsync()` на итерацию, то есть **60 мс (16.7/с)** и
без отдельной скорости боя. Значит **игра идёт примерно на 39 %
быстрее эталона**. Точное соответствие даёт 4 ожидания vsync (80 мс
против 83.3) и 5 в бою (100 мс — совпадает точно).
Проверять не «на глаз», а секундомером по одинаковому отрезку
(SDLPoP рядом на том же экране), и только после того, как кадр
перестанет иногда вылезать за период (см. п.2) — иначе замедление
спрячет проблему бюджета вместо того, чтобы её показать.
--- ---
+42 -2
View File
@@ -1,4 +1,40 @@
# Форматы ресурсов Prince of Persia — сводка # `applications/PoP/docs` — индекс + сводка по форматам ресурсов
## Индекс документов (актуальность на 2026-08-01)
**Живые планы — читать перед работой:**
| Документ | О чём |
|----------|-------|
| [`../roomtest/TASKS.md`](../roomtest/TASKS.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) |
| [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `bug_closed.md`, дифф против SDLPoP |
| [`ideas_backlog.md`](ideas_backlog.md) | Осознанно отложенные гипотезы (мышь, PRNG) |
| [`prng_alternatives.md`](prng_alternatives.md) | Запасные генераторы, если упрёмся в бюджет кадра |
**Исполненные планы, оставленные как справочники:**
| Документ | Чем ещё полезен |
|----------|-----------------|
| [`PORT_PLAN.md`](PORT_PLAN.md) | Общая карта фаз со статусами; §6 (модель движения), §10 (режим памяти) |
| [`KID_PLAN.md`](KID_PLAN.md) | Модель персонажа: `char_type`, `actions_*`, устройство `play_seq` — нужна для скелета/тени/визиря |
| [`gates_spikes_plan.md`](gates_spikes_plan.md) | Раскладка объектов уровня 1 по комнатам, декод `LINKLOC`/`LINKMAP`, точные ссылки на seg-код |
**Форматы ресурсов** (ниже по этому файлу): `POP-DAT-FormatSpecifications.pdf`
/ `.txt` (первоисточник), `APPLEII_RESOURCE_FORMAT.md`,
`MSDOS_RESOURCE_FORMAT.md`.
Удалены 2026-08-01 как полностью исполненные и перекрытые кодом:
`clip_char_plan.md`, `double_buffer_plan.md`, `loose_floors_plan.md`,
`size_optimization_plan.md` (его §8 про скорость отрисовки перенесён в
`layout_plan_v2.md` §9). Ищутся в истории git, если понадобятся.
---
## Форматы ресурсов — сводка
**Каноническая спецификация форматов**`POP-DAT-FormatSpecifications.pdf` **Каноническая спецификация форматов**`POP-DAT-FormatSpecifications.pdf`
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования): (+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
@@ -70,7 +106,11 @@ DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`B
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
декодер сжатия пикселей DOS `.DAT`. декодер сжатия пикселей DOS `.DAT`.
## Что дальше (не сделано в этом заходе) ## Что дальше по форматам (не сделано и пока не нужно)
Порт читает уровень напрямую из `res200N.bin` (`roomtest/pop_level.c`), а
графику берёт из распакованных PNG `SDLPoP/data/` — поэтому ни один пункт
ниже сейчас не блокирует работу.
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только 1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
если понадобится читать именно нашу локальную копию `MSDOS/*.dat` если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
-112
View File
@@ -1,112 +0,0 @@
# План: порт `clip_char()` — обрезка спрайта персонажа
Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8»
(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке).
## Симптом
room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт
Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии
пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке.
## Корень
Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из
`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ
`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД
`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и
прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только
внутри него. Мы рисуем без клипа вообще.
## Что делает оригинал (дословно)
`reset_obj_clip()``left=0, top=0, right=320, bottom=192`.
Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый
`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена
ИЛИ пол»):
```
T_L = get_tile(room, char_col_left, char_top_row)
T_R = get_tile(room, char_col_right, char_top_row)
if (T_L — стена или пол) &&
( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */
|| (T_R — стена или пол) )
{
clip_row = Char.curr_row + 1;
clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y))
obj_clip_top = char_top_y = clip_y;
}
```
Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой
пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»).
Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО.
Метрики из `set_char_collision` (seg006:0723):
- `char_x_left = obj_x/2 + 58``-= char_width_half`, если смотрит вправо)
- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2`
- `char_top_y = obj_y - h + 1`; если `>= 192``0`
- `char_top_row = y_to_row_mod4(char_top_y)`
- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`,
`char_col_right = MIN(get_tile_div_mod(char_x_right), 9)`
Второй блок `clip_char``obj_clip_right` (doortop / стена / зеркало для
виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228
(выход в дверь уровня). **Фаза 2**, не в этом заходе.
## Наши точки касания
- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит
через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает
прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`.
- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`,
`get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже
есть в `check_spike_below()` (строка ~1198) — брать за образец.
- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0`
пропускает `sy` верхних строк колонки), но произвольной верхней границы нет.
## Шаги
1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`».
Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник
`+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`.
Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро).
2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w,
uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y
(0 = клипа нет). Экспорт в `pop_map.h`.
3. **pop_kid.c**: в `kid_draw()` после расчёта `top` —
`ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`,
рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`;
в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее
и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает
`reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять).
4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`,
`make size-check`; вывести свободное место в W2/W3 (порог 512 Б).
5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после
пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск.
Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`).
## Риски / что проверить отдельно
- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт 1 для
полосы у потолка; `get_tile(row 1)` уже умеет ряд 2 верхнего соседа.
- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип
прыгнет.
- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в
обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой
должно отсекать).
- Клип меняет прямоугольник heal → возможен «хвост» на второй странице
дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер).
## Дальше (Фаза 2, отдельно)
`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при
взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по
колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но
без тестовой сцены проверять нечем.
См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`,
`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.
@@ -1,69 +0,0 @@
# Double buffer (два экрана + флип) — план
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
промежуточные состояния heal-рендера). **Тумблер обязателен**
однобуферный режим удобнее для отладки багов рисования.
## Что уже готово (libbgi — трогать НЕ нужно)
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
set_visible_page(hidden);`
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
## Текущая модель рендера roomtest (однобуфер)
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
бланка, луч ловит промежуток.
## Работа на стороне PoP
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
(теневая копия одна — общая, из неё heal'ит любая страница).
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
plane-параметр).
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
через страницу). То же для fore/пол-оверлея, если они рисуют вне
футпринта Кида.
4. **Ping-pong в цикле**:
```
uint8_t back = dbuf ? (front ^ 1) : 0;
gfx_set_draw_page(back);
kid_heal(back); kid_tick; phys; kid_draw; fore;
gfx_wait_vsync();
if (dbuf) { gfx_set_visible_page(back); front = back; }
```
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
compile-флагом.
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
счётом кадров, чтобы скорость игры не изменилась.
## Порядок
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
heal (проверить, что флип работает, фон корректен на обеих).
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
- D4: пейсинг (fps_div) — вернуть исходную скорость.
## Связанные
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
обе страницы по той же дисциплине.
+18 -3
View File
@@ -1,8 +1,23 @@
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план # Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ > **Статус: РЕАЛИЗОВАНО (2026-08-01).** Все фазы плана (P0 персистентный
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст). > per-room `room_modif`, S пики, B кнопки+ворота) сделаны и играются:
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`. > `roomtest/pop_trob.c` (trob-диспетчер, `LINKLOC`/`LINKMAP`, ворота, дверь
> уровня, факелы, зелья), `pop_map.c` (HP, смерть на пиках, урон падения),
> `pop_redraw.c` (пометки перерисовки вместо прямых блитов). Ограничения
> из §0 закрыты: тайлы персистентны, HP/смерть есть, loose обобщён в trob;
> L3-вверх (climb-up в комнату сверху) тоже сделан (`pop_leave_dir = 3`).
> Из §5 остаётся открытым только **переход на следующий уровень через дверь
> уровня** — он вынесен в `levels_plan.md`.
>
> **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка
> объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные
> ссылки на механику SDLPoP) продолжают экономить время при отладке.
> Текущие задачи — `../roomtest/TASKS.md`.
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
«с чистого листа» (пустой контекст). Всё сверено с
`applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**, Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
перед кодингом читать соответствующий код seg*.c, не гадать. перед кодингом читать соответствующий код seg*.c, не гадать.
+142
View File
@@ -0,0 +1,142 @@
# План: модульные тесты движка roomtest под ucsim_z80
Обвязка общая — `testkit/` в корне репозитория (там же объяснение, почему
прогон именно под z80, а не хостовым gcc). Наборы лежат в
`../roomtest/tests-host/`.
Задача плана: **перестать чинить одно и то же дважды**. За два прогона
уровня 1 (2026-08-03) закрыто восемь корней, и часть из них — регрессии
соседней механики, внесённые предыдущим фиксом. Такие вещи ловятся тестом
за миллисекунды, а в MAME — часами ручного вождения Кида.
## Что уже есть
| набор | модуль | статус |
|-------|--------|--------|
| `t_geom` | `pop_geom.c` | 39 проверок, включая побитовую сверку asm-LCG с 32-битной формулой на 128 шагах |
`pop_geom.c` выбран первым, потому что не тянет ничего за собой. Дальше
начинаются швы.
## Фаза 1. Два шва (блокирует всё остальное)
### 1.1 Доступ к странице уровня
`pop_level.c` ходит по абсолютным адресам: `gfx_w0_map(lvl_page)`, затем
разыменование `(uint8_t *)(LVL_DATA_OFF + …)`. В тестовом бинаре это
обращение в никуда.
Нужен макрос `W0PTR(off)`:
- на таргете — `((uint8_t *)(off))`, то есть ровно как сейчас;
- в тестах — смещение в обычном массиве-подложке.
Правка механическая и компайл-таймовая, на размер продукта не влияет.
Заодно снимает магию абсолютных констант из тела функций.
Тестовая подложка должна уметь: загрузить синтетическую комнату (10×3
байта fg + mod) и целый синтетический уровень на 24 комнаты, чтобы
проверять межкомнатные вещи.
### 1.2 Журналирующий рендерер
Вместо `pop_bg.c`/`pop_gdraw.c` в тестовый бинарь линкуется модуль с теми
же прототипами, который **не рисует, а записывает вызовы**: какой тайл
помечен к перерисовке, каким кодом, с каким счётчиком страниц.
Это не обход проблемы, а самостоятельная ценность: `BUG-GATE-ANIM-1` был
ровно такой формы — ворота меняли состояние, но пометка на перерисовку не
ставилась. Проверяется утверждением, а не глазами.
Минимум, который надо перехватывать: `pop_set_redraw`,
`pop_set_redraw_above`, `pop_loose_mob_spawn`, `pop_gate_redraw`.
## Фаза 2. Регрессионные кейсы из `bug_closed.md`
После швов `bug_closed.md` превращается в готовую спецификацию: у каждой
записи есть симптом и ожидаемое поведение. Кандидаты, которые ловятся
логикой (без отрисовки и без железа):
| баг | что закрепить тестом |
|-----|----------------------|
| `BUG-LVLSTATE-1` | запись тайла переживает выход из комнаты |
| `BUG-RESPAWN-1` | рестарт уровня возвращает ВСЕ тайлы из эталонной копии |
| `BUG-RESPAWN-2` | рестарт возвращает таблицу стражей; убитый снова жив |
| `BUG-GATE-ANIM-1` | смена состояния ворот ставит пометку `POP_RD_GATE`; закрывающиеся — на обе страницы, открывающиеся — на одну |
| `BUG-COLL-1` | `check_collisions` сканирует ряд справа налево и выбирает НАИМЕНЬШУЮ занятую колонку |
| `BUG-STANDUP-1` | `bumped_floor` у трупа (`alive >= 0`) только выравнивает и не трогает последовательность |
| `BUG-DEATH-1` | `hitp_curr == 0` при живом Киде переводит его в «умирает» ровно один раз |
| `BUG-LOOSE-2` | кусок, начавший падать, долетает и кладёт щебень ПОСЛЕ смены комнаты |
| `BUG-CEIL-2` | loose-плита ряда 2 верхнего соседа живёт как «ряд −1» |
`BUG-LOOSE-2` стоит взять первым: он до сих пор помечен в `bug_list.md`
как непроверенный именно потому, что гонку «уйти из комнаты раньше, чем
долетит плита» через мост MAME воспроизвести не удалось. На уровне логики
это несколько строк — заспавнить кусок, сменить комнату, тикать до
приземления, проверить щебень в данных уровня.
Не берутся (нужна картинка либо железо): `BUG-DOOR-CLIP`, `BUG-CEIL-1`,
`BUG-CEIL-3`, `BUG-OCCL-1`, `BUG-KBD-4`, `BUG-3`.
## Фаза 3. Сценарные тесты
Сейчас шаг кадра размазан по `main()` в `roomtest.c`. Вынести его в
`pop_frame_tick()` — тогда появляются тесты вида «поставить Кида в
известное состояние, скормить N тиков ввода, проверить итог»:
```
дано: комната 5, Кид на кнопке (0,6)
когда: 40 тиков без ввода
тогда: комната по-прежнему 5, Кид на полу ряда 2
```
Это тот самый BUG-STANDUP-1, который ловили потиковой трассой в MAME.
Ввод подаётся не через `kbd_raw_down()`, а через подменяемый источник —
это же даст возможность проигрывать записанные сценарии.
## Фаза 4. Дифф против SDLPoP
`SDLPoP/src/` лежит в дереве, собирается на хосте, и там **уже стоят
отладочные трассы** (`DBG kidobj tilepos=…` в seg008, `DBG make_loose_fall`
в seg007). Значит эталон можно заставить печатать потиковую трассу
автоматически.
Схема: общий формат скрипта ввода и общий формат трассы (тик, frame, x, y,
room, col, row, action, alive, hp). Гоняем обе реализации, диффим, первое
расхождение — номер тика и есть баг. Это ровно то, что делалось руками
через MAME, только бесплатно и повторяемо: `BUG-COLL-1` и `BUG-STANDUP-1`
такой дифф нашёл бы за секунды.
**Лицензия.** SDLPoP — GPLv3, правило подпроекта — читать и переписывать,
не линковать. Оракул обязан быть **отдельным исполняемым файлом**,
общающимся через файлы трасс, а не слинкованным с нашим кодом в один
бинарь.
Требование к детерминизму: сиды PRNG должны совпадать. У нас
`POP_PRANDOM_EXACT` даёт ту же последовательность, что в оригинале, и это
уже закреплено тестом `geom_lcg_matches_reference`.
## Чего эти тесты не поймают
Отрисовку, банки и W-окна, тайминги, клавиатуру — за этим остаётся MAME.
И отдельный класс: **баги порядка вызовов**. Свежий пример — окно
fore-клипа (`pop_fore_set_clip`) одно на всех, и его ставит каждый, кто
рисует персонажа; когда порядок «Кид/страж» стал переменным, окно осталось
стражьим, и Кид нарисовался поверх передних столбов. Это не «функция
вернула не то», unit-тест такое не видит. Ловится инвариантом,
вкомпилированным в safe-сборку: «в момент `pop_fore_over_kid` окно клипа
принадлежит Киду». Отдельный инструмент, дополняющий тесты.
## Порядок работ
1. Шов `W0PTR` + подложка уровня.
2. Журналирующий рендерер.
3. `BUG-LOOSE-2` — закрыть висящий вопрос.
4. Остальные кейсы из таблицы фазы 2.
5. `pop_frame_tick()` + сценарные тесты.
6. Дифф против SDLPoP.
Правило приёмки: тест не считается написанным, пока не проверен мутацией —
сломать проверяемое место и убедиться, что набор краснеет.
+57
View File
@@ -4,6 +4,63 @@
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
сделать это прямо сейчас. сделать это прямо сейчас.
## Зелье «переворот экрана» (upside-down)
**Вопрос пользователя (2026-08-01).** Тайлы фона у нас лежат строками, а
кадры Кида/стражей — КОЛОНКАМИ (`transpose_cols` в `pop_pack_kid.py`, ради
бесплатного горизонтального зеркала). Значит вертикальный переворот для
персонажей заметно сложнее, чем для фона. Верно; но прежде чем это чинить,
надо знать три факта.
**Факт 1 — когда оно вообще нужно.** Зелье переворота — тип 4
(`proc_get_object`, `seg006.c:1885``toggle_upside()`). Скан всех уровней
по данным (`res200N.bin`, тайл 10 = зелье, тип в backtable): тип 4
встречается **впервые на уровне 9** (две склянки), и больше нигде. Тип 3
(перо, медленное падение) — уровень 7. То есть **до уровня 9 механика не
нужна вообще**, и «на первом этапе просто не реализовывать» — не компромисс,
а точное соответствие данным уровней 1..8.
**Факт 2 — что именно делает оригинал.** НЕ переворачивает спрайты.
`flip_screen` (`seg009.c:1042`) → `flip_not_ega` (`seg009.c:1023`) меняет
местами СТРОКИ готового offscreen-буфера (top↔bottom, порядок пикселей
внутри строки не трогает — это вертикальное зеркало, не поворот на 180°).
Вызывается вокруг отрисовки кадра целиком (`seg003.c:296..301`): перевернул
буфер → дорисовал → перевернул обратно. Так что в оригинале это
post-process всего экрана, и вопрос «как перевернуть колоночный спрайт»
там просто не возникает.
**Факт 3 — почему нам этот приём не подходит как есть.** У нас нет шага
«готовый offscreen → экран»: рисуем прямо в видеостраницу, а heal берёт фон
из ОЗУ-копии этой же страницы. Переворот всей страницы построчно — это
320×192 Б копирования КАЖДЫЙ кадр, что мимо бюджета на порядок.
**Варианты, которые надо будет взвесить (не сейчас):**
1. **Предпечённые перевёрнутые атласы.** Второй набор кадров
Кида/стража, перевёрнутый по вертикали ещё в `pop_pack_kid.py` (там уже
есть транспонирование — добавляется одной строкой). Рантайм: выбор
набора + зеркальная арифметика Y. Память: ещё ~28 страниц EMM при
бюджете ~3.3 МБ — не проблема. Похоже, самый дешёвый по тактам путь.
2. **Фон рисовать с обратным Y** — для row-major тайлов строка остаётся
непрерывным accel-прогоном, меняется только адрес назначения; цена —
вызов на строку вместо вызова на тайл. Померить, прежде чем закладывать.
3. **Аппаратная помощь** — до проектирования проверить, есть ли у
акселератора направление копирования «вниз» (обратный инкремент адреса);
если есть, вариант 1 может и не понадобиться. Смотреть
`docs/new/06-accel.md` и `docs/reference/accel_r.txt`.
**Почему не сейчас.** Уровни 1..8 этого не требуют, а к уровню 9 у нас уже
будет ответ на вопрос «сколько стоит кадр» (задачи CLIP-1/T-2) — без него
выбирать между вариантами выше бессмысленно.
## Заменить генератор псевдослучайных чисел
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
## Отключать мышь на время игры ## Отключать мышь на время игры
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h, **Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
+71 -4
View File
@@ -1,8 +1,26 @@
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам # roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
> **Замер 2026-08-01 (актуальная сборка).** `_CODE` 25 119 Б, `_DATA` 3 709,
> куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792,
> 3 (`pop_map.c`) 6 331, 4 (`pop_gdraw.c`) 2 236 — все из 16 384.
> **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло
> главное ограничение резидента (из банка его было не достать) — банк→банк
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
> вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к
> потолку (16 021 из 16 384) — коммит `2f3e854`.
>
> **Свободного места в банке 2 осталось ~2.6 КБ**, а туда же просятся
> чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать
> `levels_plan.md` §3 — посчитать, куда это ляжет. Свободные номера банков
> есть, гранулярность — файл.
>
> Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`,
> `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по
> решению пользователя)**.
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру. Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный как полностью перекрытый этим документом. Документ самодостаточный
— рассчитан на старт с пустого контекста. — рассчитан на старт с пустого контекста.
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
@@ -442,5 +460,54 @@ dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`, `bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`, `sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
`libc_one_function_per_module`. `libc_one_function_per_module`.
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21, - `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас.
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
---
## 9. Скорость отрисовки: замеры и запас
Перенесено из удалённого `size_optimization_plan.md` §8 (замер 2026-07-27) —
единственная его часть, которая не была перекрыта этим документом.
Профилирование в MAME: маркеры в порт 0xFE + `wpiset … totalcycles` (приём из
memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже стоят полосы
бордюра `PROF()`. Кадр Sprinter = **430 080 тактов**.
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
пересчёт src, нарезка полос >256), а не за пиксели:
| путь (спрайт 32×3) | тактов |
|---|---|
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
| линейное ядро без клипа | 4 617 |
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
кадра**; сам `bar` — только 13 308.
**Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`).
**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS.md`.
**ВАЖНО:** W3-скобку (`_bgi_begin`/`_bgi_end`) ставит САМА libbgi — вызывать
её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
белый экран).
**Запас, когда перестанет хватать бюджета кадра:**
1. **Батчинг W3-скобки** — одна `_bgi_begin`/`_bgi_end` на весь `draw_tile`
вместо скобки на блит; нужен публичный batch-API в libbgi.
**Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`,
длинная серия задержит кадровое прерывание — а по разбору KBD-1
(`../roomtest/TASKS.md`) длинные DI-окна и есть причина потери байт
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
2. **Решётка ворот одним спрайтом**`draw_gate_back` рисует бары по одному
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
`gfx_blit_part` с обрезкой по фазе `gate_bot_y & 7`: 7 блитов → 1.
3. **Не перерисовывать статичные части шва** — грань ворот, пол и кромка при
анимации решётки не меняются (см. OPT-1 в `../roomtest/bug_closed.md`
решено не делать, стоимость транзиентная).
4. **T-1 / T-2** (`../roomtest/bug_list.md`) — перерисовка пик по причине и
idle-skip Кида: самый большой оставшийся резерв, потому что убирает работу
целиком, а не удешевляет её.
+197
View File
@@ -0,0 +1,197 @@
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
Статус: план, 2026-08-01. Продолжает `PORT_PLAN.md` §7 (Фаза 1: «переходы
между экранами» → теперь между УРОВНЯМИ). Текущая точка: `roomtest` играет
уровень 1 целиком в одной комнате-за-комнатой модели, но уровень нельзя
ни выбрать, ни закончить.
Источник истины — `../SDLPoP/src/` (правило `../CLAUDE.md`). Ключевые
места: `seg000.c: load_lev_spr/play_level_2/init_game`, `seg005.c:
up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`,
`seg002.c` (спецсобытия уровней), `data.h:835..850` (потабличные различия
уровней).
---
## 0. Что уже готово (не проектировать заново)
- **Формат и загрузчик уровня.** `pop_level.c/.h` читает сырой
`res200N.bin` (2305 Б) в отдельную EMM-страницу; путь — параметр
`pop_level_load(const char *)`. Мультиуровневость здесь стоит одной
функции формирования имени.
- **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`,
`pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока
НЕ вызываются (см. `../roomtest/TASKS.md` L1-START).
- **Страж по данным уровня**: `pop_level_guard()` (порт `enter_guard`),
сохранение состояния между комнатами (`pop_guard_state_save`).
- **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py`
`build_palette`): env 0x50, wall 0x60, pot 0x40, kid 0x70, меч 0x80,
страж 0x90. Это тот же раскрой, что `set_pal_arr(0x50/0x60)` в
`seg000.c:1140..1148`, — значит смена тайлсета не требует переиндексации
спрайтов Кида (см. §3).
- **Все 16 файлов уровней распакованы**: `../SDLPoP/data/LEVELS/res2000..
res2015.bin` (0 — демо-уровень).
---
## 1. Что реально различается между уровнями (замер по данным, не по памяти)
Таблицы из `../SDLPoP/src/data.h:840..847` + инвентарь тайлов, снятый прямо
с `res200N.bin` (маска `fg & 0x1F`):
| Ур. | Тайлсет | Страж | Новое против предыдущих |
|-----|---------|-------|--------------------------|
| 1 | dungeon | обычный | — (текущая база) |
| **2** | **dungeon** | **обычный** | **ничего нового: тот же набор объектов минус меч** |
| 3 | dungeon | СКЕЛЕТ | чомперы |
| 4 | palace | обычный | **тайлсет palace**, зеркало (спецсобытие `mirror_level`) |
| 5 | palace | обычный | — |
| 6 | palace | ТОЛСТЫЙ | падение на входе (спецсобытие) |
| 7 | dungeon | обычный | — |
| 8, 9 | dungeon | обычный | — |
| 10, 11 | palace | обычный | — |
| 12 | dungeon | ТЕНЬ | seamless-выход (комната 23), исчезающий меч |
| 13 | dungeon | ВИЗИРЬ | мышь, особый выход |
| 14 | palace | нет | — |
| 15 | dungeon | нет | финал |
Прямое следствие для порядка работ: **уровень 2 не требует ни одного нового
ассета и ни одной новой механики** — он проверяет ровно машинерию перехода.
Это и есть первый шаг.
Прочие потабличные различия, которые придётся завести массивами по 16:
`tbl_level_type` (тайлсет), `tbl_guard_type` (−1 = стражей нет),
`tbl_guard_hp`, `tbl_level_color` (вариантные палитры, 1.3), `tbl_entry_pose`.
---
## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) — **СДЕЛАН 2026-08-04**
> **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 →
> Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие
> колонны рисуются) → уровень 3. Разбор что именно сделано и что по
> уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**.
>
> Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется:
> **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина
> двери уровня) и **большую склянку** `add_life` (тип зелья 2, комната 20).
Порядок именно такой; каждый пункт проверяем в MAME отдельно.
**2.1 Выход с уровня.** Портировать `up_pressed()` ветку двери
(`seg005.c:410..423`) + `go_up_leveldoor()` (`seg005.c:497`): дверь рядом
(при/за/перед персонажем) И `drawn_room != level.start_room` И створка
открыта полностью (`curr_room_modif >= 42` — вариант `fix_exit_door`) →
`Char.x = x_bump[...] + 10`, направление влево, `seq_70_go_up_on_level_door`.
Затем оживить опкод `0xF1 END_LEVEL` в `play_seq` (`../roomtest/pop_kid.c:418`
— сейчас пустой `break`): `++pop_next_level`, как `seg006.c:662`.
**2.2 Цикл уровня.** В `main()` после тика: `if (pop_next_level !=
pop_current_level) → load_level(pop_next_level)`. Порядок сноса/подъёма
состояния (порт `load_lev_spr` + `play_level_2`):
`pop_level_free` → `pop_level_load("LEVELS\\res200%d.bin")` →
`pop_trob_reset` → `pop_guard_reset` → сброс tile-override'ов
(`ovr_*` в `roomtest.c`) → `enter_room(pop_level_start_room())` →
`kid_init(поза/позиция/направление из данных уровня)`.
**HP через уровень переносится** (в оригинале `hitp_beg_lev`), не сбрасывать
в максимум — сверить с `seg000.c` `init_game`/`play_level_2`.
**2.3 Стражи по уровню.** Завести `tbl_guard_type[16]`/`tbl_guard_hp[16]`;
`-1` = стражей на уровне нет (уровни 14, 15) — `pop_guard_enter` обязан это
понимать, иначе на 14-м полезут стражи из мусора. Для шага 1 (уровни 2, 3)
достаточно обычного стража, но проверку `-1` заложить сразу.
**2.4 Чит «следующий уровень» (Shift+L).** Реализуется ровно тем же
`pop_next_level` — и без него отладка уровней превращается в прохождение
игры руками. Делать в этом же шаге, не позже (см. §4).
**Приёмка шага 1:** дверь уровня 1 → уровень 2 играется целиком → его дверь
→ уровень 3 стартует (чомперы могут быть ещё не портированы — тогда
фиксируем как известное ограничение, а не «баг»).
---
## 3. Шаг 2 — второй тайлсет (palace, уровни 4+)
**Ассеты.** `toolchain/pop_pack_bg.py` уже читает PNG каскадом
VDUNGEON→VPALACE (та же логика, что в игре), но печёт ОДИН набор атласов
(`pop_env0..4.atl`, `pop_wall.atl`, `pop_fore.atl` ≈ 75 КБ). Нужен второй
набор из VPALACE (`pal_env*.atl` / `pal_wall.atl`), плюс `torch_debris` —
тайл, который встречается только на palace-уровнях. По EMM это ещё ~6
страниц при бюджете ~3.3 МБ — не проблема.
**Палитра — главный технический вопрос, и он уже решён раскроем.**
Тайлсет живёт в слотах `0x50..0x5F` (env) и `0x60..0x6F` (wall); Кид, меч,
страж, склянки — в других слотах. Значит смена тайлсета = перезапись 32
записей палитры (`gfx_pal_set` на обе страницы, как `flash_bg` в
`roomtest.c`), а НЕ перезагрузка `kid.pal` и не переиндексация спрайтов.
Сделать `pal_dungeon.bin` / `pal_palace.bin` (по 32 записи) и грузить при
смене типа уровня. Проверить артефактом: скриншот palace-комнаты против
рендера `render_room.py` для того же уровня.
**Вариантные цвета уровней** (`tbl_level_color`, `level_var_palettes` — это
уже 1.3, в 1.0 их нет): по той же механике, тот же диапазон слотов. Решение
на будущее — сначала базовые два тайлсета, потом при желании цвета.
**Выбор набора в коде.** `pop_bg_load()` сейчас грузит фиксированные имена;
превратить в `pop_bg_load(type)` с двумя таблицами имён + выгрузка старых
атласов при смене типа (`atlas_free`). Переключение — только на границе
уровня, не в кадре.
---
## 4. Читы SDLPoP: что взять на следующем этапе
Из `../SDLPoP/README.md` (раздел Cheats). У нас уже есть: **K** — убить
стража, **I** — бессмертие (наш, в оригинале нет), **S** — выдать меч (наш),
**+/−** — обход комнат (`ROOMNAV`, наш).
**Брать сразу вместе с переходами уровней** (без них отладка дороже самой
работы):
| Чит | Что даёт | Цена |
|-----|----------|------|
| **Shift+L — следующий уровень** | единственный вменяемый способ тестировать уровни 2..15 | тривиально: `++pop_next_level` из §2.2 |
| **R — воскресить Кида** | у нас респавн по ↑ + таймаут; порт `resurrect` ближе к оригиналу и не мешает управлению | низкая |
| **Shift+S / Shift+T — +1 HP / +максимум** | отладка боёвки без «ровно трёх попыток»; честная замена нашему читу бессмертия | низкая, HP-машинерия уже есть |
| **[ и ] — сдвинуть Кида на пиксель** (debug-чит SDLPoP) | прямо бьёт в наш класс багов «окклюзия/шов на один пиксель» — воспроизведение позы без ловли момента | тривиально |
**Брать во вторую очередь:**
| Чит | Почему позже |
|-----|--------------|
| **H / J / U / N + Ctrl+B — смотреть соседние комнаты** | требует честной модели `drawn_room ≠ Kid.room` (наш S3-straddle, каркас есть: `update_kid_render_dx`). Зато потом заменяет самодельный `ROOMNAV` и попутно закрывает straddle-задачу |
| **Shift+W — медленное падение (feather)** | ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` уже есть, но не проверена ничем — чит станет её единственным тестом |
| **C / Shift+C — номера комнат** | у нас номер рисуется палочками именно потому, что текст тянет 2 КБ знакогенератора в W2 (`roomtest.c`). Ждёт своего шрифта |
**Не брать:** `Shift+I` (переворот экрана), `Shift+B` (blind mode) —
развлекательные, к отладке порта отношения не имеют. `/+` (время) — нужен
таймер уровня, которого у нас нет (Фаза 6).
**Отдельно, дорого, но очень ценно — `F6`/`F9` (quicksave/quickload точного
состояния).** Это сериализация `Char` + `room_modif` всех комнат + trob'ов +
состояния стражей. Даёт то, чего нам сейчас сильно не хватает:
воспроизводимый регресс в MAME («вот кадр, где баг») вместо ручного подхода
к позиции. Кандидат сразу после того, как заработают уровни.
---
## 5. Риски и что проверить артефактом до кодинга
1. **Размер кода.** Замер сборки 2026-08-01: `_CODE` 25 119 Б,
куча ~2.4 КБ, банк 2 (`pop_bg`) 13 792 / 16 384, банк 3 (`pop_map`)
6 331, банк 1 (`guards`) 1 896, банк 4 (`pop_gdraw`) 2 236. Чомперы,
зеркало, скелет и второй тайлсет пойдут в банк 2 — там осталось 2.6 КБ.
**Прежде чем начинать §3, посчитать, куда лягут новые тайлы**, иначе
повторится история «банк 2 упёрся в потолок» (коммит 2f3e854). Свободные
номера банков есть (5+), гранулярность — файл.
2. **Спецсобытия уровней** (`seg002.c`: `level3_set_chkp`, `sword_disappears`,
`Jaffar_exit`, зеркало, мышь) — их НЕ надо портировать заранее. Для
уровней 2 и 3 нужен только чекпойнт уровня 3. Остальное — по мере
подхода к уровню.
3. **Чомперы** (уровень 3 и почти все дальше) — отдельная механика
(`animate_chomper` + коллизия + смерть); шаблон работы тот же, что у
пик/ворот, см. `gates_spikes_plan.md`.
4. **`tbl_guard_type = -1`** на уровнях 14/15: без проверки страж
«появится» из неинициализированных данных.
5. **Уровень 0 (демо)** существует в данных, но в скоуп не входит.
-143
View File
@@ -1,143 +0,0 @@
# Loose floors (проваливающиеся полы) — план порта
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
## 1. Хранение состояния
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
падает). После падения тайл → `0` (tiles_0_empty).
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
- `0` — покой (обычный loose-пол);
- `0x80..0x83`**трясётся** (бит7); за ~4 кадра затухает обратно в 0;
- `1..11`**обратный отсчёт до падения** (на нём что-то стоит);
достигает `loose_floor_delay = 11` → падает.
## 2. Анимация тряски (shake) — когда включается
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock``check_knock()`
`do_knock(room, curr_row (knock>0))`.
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
`loose_make_shake()`.
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
`modif = 0x80`, `add_trob(type 1)`.
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]`
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
knock-смещение ряда может задеть и соседний ряд.)
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84`
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
(sound 20/21/22) по таблице `loose_sound[]`.
## 3. Анимация падения (fall) — когда включается
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
`(sbyte)modif <= 0``modif = modifier`, `add_trob(type 0)`.
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
## 4. Падающий кусок (mob)
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
(урон Киду/страже, если попал).
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
debris(14)**.
## 5. Отрисовка по статусу
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
(env-спрайты, seg008:518/596/608).
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
задержке — низкие индексы.
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
## 6. Что нужно в нашем движке (сейчас НЕТ)
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
**динамического тайлового слоя**:
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
переиспользовать/рядом) — состояние каждого тайла.
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
физики Kid: приземление→knock, стойка на loose→fall).
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
Фазой 2.
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
текущим модификатором (сейчас draw_tile берёт статический bg).
**Порядок реализации (предложение):**
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
- L4: mob (падающий кусок → debris снизу).
- L5: урон Киду от падающего куска.
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;``return m;`
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
Триггеры (call-sites):
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1`
Kid стоит на loose (start_fall НЕ зовётся). Тело:
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
## Интеграция в наш движок (roomtest) — подход
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
(Сейчас g_fg — `const`; сделать неконстантным.)
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
при стоянии на 11), `animate` каждый кадр.
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
печёный фон) + нарисовать loose-кадр (left/right/bottom по
get_loose_frame) банком SPRITE;
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
отложено — упавший loose = пусто. **L5** урон — позже.
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
bake) — единственное тонкое место; остальное — прямой порт логики выше.
## Связанные
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
+148
View File
@@ -0,0 +1,148 @@
# Генераторы псевдослучайных чисел: запасные варианты
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
**сейчас менять ничего не нужно**.
## Что стоит сейчас
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
место в порте). Схема Горнера по разреженной записи константы:
```
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
```
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
с эталоном.
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
Совместимость теряется.
Замер в MAME, комната 3, 175 кадров (медиана кадра):
| вариант | кадр | prandom → torch_draw |
|---|---|---|
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
| C, xorshift16 + Вейль | 397 986 | 7 927 |
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
```z80
prng16:
seed1=$+1
ld hl, 9999
ld b, h
ld c, l
add hl, hl
add hl, hl
inc l
add hl, bc
ld (seed1), hl
seed2=$+1
ld hl, 987
add hl, hl
sbc a, a
and 101101b
xor l
ld l, a
ld (seed2), hl
add hl, bc
ret
```
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
независимые последовательности — раскладка кладки и анимация тайлов.
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
(это +20…40 тактов, не принципиально).
## Вариант B — xorshift(7,9,8), ~86 тактов
Самый быстрый, период 65535.
```z80
xrnd:
ld hl, 1 ; seed must not be 0
ld a, h
rra
ld a, l
rra
xor h
ld h, a
ld a, l
rra
ld a, h
rra
xor l
ld l, a
xor h
ld h, a
ld (xrnd+1), hl
ret
```
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
считается как `номер комнаты + смещение ряда + колонка` и вполне может
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
что в варианте A.
## Чего НЕ брать: RND из Apple II
Оригинальный `Prince-of-Persia-Apple-II`:
```
RNDseed := (5 * RNDseed + 23) mod 256
```
```asm
RND
lda RNDseed
asl
asl
clc
adc RNDseed
clc
adc #23
sta RNDseed
rts
```
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
аккуратная шахматка.
## Сколько реально можно выиграть
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
любой из вариантов A/B ниже него не опустится.
Порядок действий, если понадобится:
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
на вызов приходился один `call`, а не три. Это ничего не ломает и не
трогает совместимость с эталоном.
2. И только если этого мало — менять генератор, начиная с варианта A
(качество последовательности у него не хуже LCG, в отличие от B).
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
цена вопроса вырастет во столько же раз.
+20 -2
View File
@@ -1,5 +1,21 @@
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4) # PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
> **Статус: ЖИВОЙ ПЛАН, сделан частично (сверено 2026-08-01).**
> - **S1 — сделан:** `kid_room` заведён отдельно от `cur_room`,
> `update_kid_render_dx()` (`roomtest.c`) даёт рендер-смещение ∓140, а
> `pop_kid_set_render_dx` применяет его в отрисовке. Фактически это пока
> каркас: `enter_room` держит `kid_room == cur_room`, так что смещение
> всегда 0.
> - **S2/S3/S4 — не сделаны и не срочны.** Исходный повод (баг #4,
> пинг-понг у шва) закрыт иначе — поправкой odd-pixel в
> `char_x_forward_edge` + `pop_leave_timer` (разбор корня —
> `../roomtest/bug_closed.md`, BUG-SEAM-PINGPONG).
>
> **Зачем документ остаётся.** Полная straddle-модель понадобится для:
> (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4),
> (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у
> шва (S4). Брать из `../roomtest/TASKS.md`, когда дойдёт очередь.
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате, Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP. пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
@@ -62,7 +78,9 @@ drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol`
### S4. Полировка ### S4. Полировка
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край. - Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
## Связанные баги (bug_list.md) ## Связанные баги — все ЗАКРЫТЫ (`../roomtest/bug_closed.md`)
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке), BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
колонны). Memory: `pop_seam_room_model`. колонны) — починены без полной straddle-модели. То есть S4 «полировка
окклюзии» осталась актуальной только для окклюзии У ШВА при straddle.
Memory: `pop_seam_room_model`.
@@ -1,335 +0,0 @@
# roomtest — план оптимизации по размеру + переход на huge/banking
Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный
(рассчитан на старт с пустого контекста). Цель — освободить место: сейчас
`applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ.
Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти
сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`,
`bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией —
`make size-check`-подобный замер до/после (здесь — руками по `.map`).
---
## 0. Как мерить
- Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`,
`--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map`
(адреса сдвигаются при каждой пересборке!).
- Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`).
- Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю
(скрипт-однострочник на python в истории; группировать символы `.map` по
3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`).
- MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти —
типовой источник «молча ломается», см. `sprinter_memory_modes`).
## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21)
Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с
0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` =
linker chains), стек — вверху W2.
| Область | Размер | Диапазон |
|---------|--------|----------|
| `_CODE` | ~27 250 Б (0x6A6F) | 0x41000xAB6F |
| `_HOME` | 227 Б | 0xAB6F |
| `_DATA` | 3 449 Б (0x0D79) | 0xAC780xB9F1 |
| `_BSS` | 290 Б | |
**Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.**
Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах),
но запас критично мал.
### Вклад модулей в _CODE (по .map, приблизительно)
```
7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern)
6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже)
3762 pop_map (коллизия/физика/пики)
1455 pop_trob (кнопки/ворота/пики-каркас)
1224 pop_level (загрузка уровня, doorlink)
914 roomtest (главный цикл)
~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…)
```
### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`)
- **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется
pop_kid):
- `kid_seqtbl[2310]` — байткод последовательностей (play_seq).
- `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword).
- `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq.
- `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH,
WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE,
BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.50.7 КБ.
- `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б.
- В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б
(pop_trob/pop_level) + рабочие массивы roomtest.
---
## 2. ПУТЬ A — оптимизация КОДА (без смены модели)
1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size`
у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код,
но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs`
давал −1.4 КБ). Замерить каждый модуль отдельно.
2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck`
по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c).
Вынести в `static inline`/helper `subst_pressed_button(code,mod)`.
3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) —
пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG
оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только
если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность.
4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие
геометрические хелперы дублируются — свести в один internal-модуль.
5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg).
Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме,
оптимистично, ~1–2 КБ. Недостаточно как единственная мера.
---
## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level)
**Идея (по замечанию пользователя):** EMM-страницы атласов и уровня
использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» —
свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA,
если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0.
**Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur`
спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания).
Уровень (pop_level) маппит свою страницу в W0 на время извлечения
(`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и
данные из неё по адресам 0x0000..0x3FFF.
**Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):**
- **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы.
Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа.
ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой
спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий
атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста →
«в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная
страница в W0 в этот тик.
- **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там
~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор
комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`.
- **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить
резидентной (если дубли дороже экономии).
- **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО
`gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как
правило оставить резидентной.
**Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):**
- `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И
в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a),
частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq`
делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить
бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной
страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw.
- Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный
по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру.
**Паттерн переноса writable/const данных в банк/страницу:** см. memory
`bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix
+ mkexe -p 0) и `sdcc_static_storage_gotcha`.
### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б)
```
BG-атласы: размер свободно
pop_env0.atl 10578 5806
pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg
pop_env2.atl 10798 5586
pop_env3.atl 5032 11352 <- много места
pop_env4.atl 8498 7886
pop_wall.atl 11543 4841
pop_fore.atl 7763 8621
Kid-атласы (28 стр): free min=6161 max=15452 avg=9722
Level (res2001.bin): данные 2305, свободно ~13823 (16384 0x100 стаб 2305)
```
**Выводы по вместимости:**
- **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см.
таблицу). Связывающее ограничение — самая тесная нужная страница (env1 =
3935 Б; не перегружать её).
- Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как
level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть
в хвост ПОСЛЕ атласа.
- **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в
отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить
раз на кадр, не конфликтуя с kid-атласами блита).
- **Level-таблицы** — вагон места в level-странице (~13.8 КБ).
- **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см.
граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0.
- **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ
(−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см.
`sprinter_emm_budget`: 215/3440 КБ free на старте).
- **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это
резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5;
дробление ломает эту адресацию и множит страницы). Сначала использовать
СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы.
---
## 4. ПУТЬ C — переход на huge (banked code)
### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`)
- `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**,
banked code в W3. crt0_banked, как crt0_small, авто-детектит W2.
Помечено `[TODO]` — не обкатано.
- Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge
ФИКСИРУЕТ DATA на 0x8000.
### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически
Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за
0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) →
коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным
CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.**
Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в
банках W3». Это первый пункт работ по huge.
### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск)
`pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы
используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО
НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать
графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3),
трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что
banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска —
**big + BANK_W1** (банк кода в W1, не W3), рекомендованная в
`pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать:
huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1).
### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую)
Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle|
bar(|GFX_BANK|initgraph`):
```
pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка)
roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц)
pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ
pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split)
pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control)
pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму
pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика)
```
- **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой
графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1.
- **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg
(перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это
кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный
24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк
= прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3
вокруг вызова в графический pop_bg (см. §4.3).
- **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор
(логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными.
Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU
(1 функция = 1 модуль, см. `libc_one_function_per_module`).
- **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать
можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие).
### 4.5 Почему графику нельзя в W3 (контекст)
Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`,
`_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см.
`sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно
кода-банка; смешивать с окном, которое графика перемапливает, нельзя без
save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`.
---
## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии)
1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до.
2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый 1..2 КБ.
3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный
пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем
резиденте (пока без выноса — просто huge-раскладка = small + пустой W3).
4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить
кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1.
5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу
Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`).
Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d).
## 6. Ссылки
- `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC).
- `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`.
- memory: `sprinter_memory_modes`, `memory_modes_implemented`,
`setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`,
`pop_banking_architecture`, `avoid_32bit_arith_z80`,
`libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`.
- `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют
оптимизацию, но держать в уме при рефакторе pop_map/pop_bg).
---
## 7. Лишние блиты в горячем пути (добавлено 2026-07-27)
Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал
спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа
каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта
Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в
оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть,
а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем).
Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны.
**Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли
этот спрайт в оригинале в ЭТОЙ таблице?):**
1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по
скорости, но по ним стоит сверить состав слоёв (backtable vs foretable).
2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`;
проверить, не рисуем ли `base_id` там, где оригинал его не рисует
(loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable).
3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row,
mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на
один кадр; можно ограничить только тайлом, который реально пересекается
с куском.
4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски;
проверить, нужен ли второй тайл (правую грань loose в полосе потолка
оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`).
5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита,
а оригинал (`redraw_needed_above`) — только для колонок с флагом
`redraw_frames_above`; сузить до колонок, реально задетых спрайтом.
6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить,
не зовём ли её там, где оригинал ограничивается `wall_fram_main`.
---
## 8. Скорость отрисовки: замеры и запас (2026-07-27)
Профилирование в MAME (маркеры в порт 0xFE + `wpiset … totalcycles`, приём из
memory `mame_mcp_bridge`). Кадр Sprinter = **430 080 тактов**.
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
пересчёт src, нарезка полос >256), а не за пиксели:
| путь (спрайт 32×3) | тактов |
|---|---|
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
| линейное спрайтовое ядро без клипа (`putsprite` при `gfx_sprite_clip(0)`) | 4 617 |
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
кадра**; сам `bar` — только 13 308.
**СДЕЛАНО (шаг 1):** в libbgi добавлен `gfx_blit_noclip()`
(`common/gfx_blit_noclip.c`, прототип в `include/gfx.h`) — блит без клипа в
ТЕКУЩЕМ банке через линейное ядро; `pop_bg.blit_b` уходит на него, когда
спрайт целиком на экране и не нужен `g_clip_top`. Выигрыш ~2.9× на каждом
фоновом блите (подтверждено в MAME).
**ВАЖНО:** W3-скобку (`_bgi_begin/_bgi_end`) ставит САМА libbgi — вызывать её
из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
белый экран).
**ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:**
1. **Батчинг W3-скобки** — одна `_bgi_begin/_bgi_end` на весь `draw_tile`
вместо скобки на блит; нужен публичный batch-API в libbgi (как у
спрайтового движка). Осторожно: между begin/end стоит `DI` — длинная
серия задержит кадровое прерывание.
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
(`env 52`, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор
бара на высоту тайла) и выводить одним `gfx_blit_part` с обрезкой по фазе
`gate_bot_y & 7`: 7 блитов → 1.
3. **Не перерисовывать статичные части шва** — грань ворот (env 47, 26×62),
пол (41) и кромка (43) при анимации решётки не меняются; если стирать
только полосу баров, уйдут ещё 3 блита из 9.
4. См. также §7 (лишние блиты, которых нет в оригинале).
+1 -1
View File
@@ -37,5 +37,5 @@ static const char *const kid_atl[KID_PAGES] = {
}; };
#define KID_PAL_FILE "kid.pal" #define KID_PAL_FILE "kid.pal"
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */ #define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */ #define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
#endif #endif
Binary file not shown.
+14 -1
View File
@@ -11,13 +11,23 @@
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP = по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
по умолчанию баг у нас. по умолчанию баг у нас.
**Что в работе сейчас — `TASKS.md`** (доска задач: приоритеты, критерии
готовности). Баги — `bug_list.md` (только ОТКРЫТЫЕ) и `bug_closed.md`
(закрытые + разбор корней: перед заведением нового бага грепни там по
симптому). План следующих уровней — `../docs/levels_plan.md`.
## Сборка и запуск ## Сборка и запуск
``` ```
make # собрать roomtest.exe (упаковав ассеты через toolchain/) make # собрать roomtest.exe (упаковав ассеты через toolchain/)
make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док) make run # exe + EXTRA_DATA на дискету + запуск MAME (см. корневой док)
make -C tests-host # модульные тесты движка под ucsim_z80 (секунды, без MAME)
``` ```
Логику, которую можно проверить без железа, покрывать в `tests-host/`
(обвязка — `testkit/`, там же почему прогон именно под z80). MAME остаётся
для отрисовки, банков, таймингов и клавиатуры.
`MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`, `MEMORY=small`, `--gfx 256`. Ассеты (`pop_env0..4.atl`, `pop_wall.atl`,
`pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами `pop_fore.atl`, `kid0..27.atl`, `kid.pal`) генерируются python-скриптами
`../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты — `../toolchain/` — Makefile дёргает их сам при изменении. Данные комнаты —
@@ -34,7 +44,10 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`).
Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл Отладочные тумблеры в живой сессии (`roomtest.c`): **SPACE** — вкл/выкл
дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр); дабл-буфер (в однобуфере видно баги рисования без мерцания-через-кадр);
**1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии); **1** — заморозить кадр, **2** — продолжить (разбор позы/окклюзии);
**ESC** — выход. **ESC** — выход. Читы (`pop_cheat.h`): **K** — убить стража, **I**
бессмертие, **S** — выдать меч, **Shift+L** — следующий уровень,
**+/−** — обход комнат (`ROOMNAV`). Уровень грузится по номеру
(`pop_level_load_num`), на диске лежат все 15.
## Модули ## Модули
+30 -5
View File
@@ -14,19 +14,44 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и # банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory # логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
# sdcc_banked_call_rules. # sdcc_banked_call_rules.
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 2=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS) # Банк 5 = pop_ctrl.c (MEM-BANK5). CODE и DATA делят одно 32-КБ
# пространство W1+W2, поэтому килобайт кода, уехавший в банк, — это
# килобайт, доступный данным. Кандидат выбран не по размеру, а по частоте
# вызова: диспетчер управления дёргается один раз за кадр на персонажа и
# горячих банк→банк переходов не создаёт (в отличие от pop_level, чей
# pop_level_tile зовётся из банка 2 на КАЖДЫЙ тайл). Шина control_*
# переехала в pop_state.c — см. там.
# --max-allocs-per-node: во сколько вариантов размещения регистров SDCC
# упирается на узел. У sprinter-cc дефолт 100000 (агрессивно, как fast-сборки
# библиотек) — на roomtest это МИНУТЫ на банк, потому что модули крупные.
# Дефолт SDCC — 3000, и для разработки его достаточно: разница в размере
# единицы процента, а сборка кратно быстрее. Замер (2026-08-05) — в шапке
# TASKS.md, запись BUILD-FAST.
#
# make — быстрая сборка (ALLOCS=3000)
# make ALLOCS=100000 — как раньше: минимальный код, для замеров размера
# и для «релизного» образа
#
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 3000
EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
# MAME = время фазы в растровых строках. # MAME = время фазы в растровых строках.
PROF ?= 1 PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF) PROF_FLAGS := -DPROF_BORDER=$(PROF)
EXTRA_SRCS := pop_state.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_redraw.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid KID_DIR := $(CURDIR)/../poc/res/kid
LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS LVL_DIR := $(CURDIR)/../SDLPoP/data/LEVELS
LVL_DATA := $(LVL_DIR)/res2001.bin # ВСЕ уровни игры (res2001..res2015, по 2305 Б = 34 КБ на всех): переход
# уровень→уровень и чит Shift+L требуют, чтобы файл следующего лежал на
# диске. res2000 — демо-уровень, в скоуп не входит.
LVL_NUMS := 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
LVL_DATA := $(foreach n,$(LVL_NUMS),$(LVL_DIR)/res20$(n).bin)
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \ BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \ $(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \ $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \
@@ -58,7 +83,7 @@ hdd: $(EXAMPLE).exe
$(foreach f,$(BG_DATA),BG:$(f)) \ $(foreach f,$(BG_DATA),BG:$(f)) \
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \
LEVELS:$(LVL_DATA) $(foreach f,$(LVL_DATA),LEVELS:$(f))
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py $(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
@@ -71,4 +96,4 @@ $(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
cd $(TC) && python3 pop_extract_kid_data.py cd $(TC) && python3 pop_extract_kid_data.py
$(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) $(EXAMPLE).exe: guards.c pop_guard.c pop_guard.h pop_gdraw.c pop_draw.c _pop_draw.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
+10 -5
View File
@@ -39,9 +39,14 @@ ESC выход.
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid - `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
(оба генерируются скриптами `../toolchain/`). (оба генерируются скриптами `../toolchain/`).
## Статус ## Статус (2026-08-01)
Готово и проверено в MAME: статический фон комнаты 1; Kid анимация, Играется весь уровень 1: комнаты и переходы между ними, Kid (анимация,
управление, коллизия/падение, зацеп/подтягивание/спуск, fore-окклюзия управление, коллизия/падение, зацеп/подтягивание/спуск, окклюзия), кнопки и
пола/стены над Kid. В работе: проваливающиеся полы (loose floors) — см. ворота, пики, проваливающиеся полы, дверь уровня, меч и бой, стражи с ИИ,
`../docs/loose_floors_plan.md`. HP и зелья.
Не сделано: выход в следующий уровень (дверь открывается, но войти в неё
нельзя), звук, таймер/HUD времени, сохранения. **Что берём в работу
сейчас — [`TASKS.md`](TASKS.md)**; план следующих уровней —
[`../docs/levels_plan.md`](../docs/levels_plan.md).
+822
View File
@@ -0,0 +1,822 @@
# roomtest — доска текущих задач (обновлено 2026-08-04)
Не список багов (открытые — [`bug_list.md`](bug_list.md), закрытые с разбором
корней — [`bug_closed.md`](bug_closed.md)) и не план фаз
(`../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`, `../docs/levels_plan.md`),
а то, **что берём в работу сейчас и в каком порядке**. Каждая запись: что
сделать, почему именно сейчас, чем подтверждать результат.
**Открытых багов нет** ([`bug_list.md`](bug_list.md)): остались незакрытые
оптимизации [T-1](bug_list.md#t-1), [T-2](bug_list.md#t-2) и незаконченная
[таблица обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1).
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
гипотезой (memory `defer_unexplained_quirks`).
---
## ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком
Решение 2026-08-04: **palace (уровни 4+) откладываем**, доводим до
играбельности три dungeon-уровня. Основание — они не требуют ни одного
нового ассета фона: инвентарь тайлов, снятый с `res200N.bin`, показывает,
что новое появляется только так —
```
ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris,
opener, level_door L/R, torch, wall, skeleton, sword ← всё есть
ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04)
ур. 3 chomper(18) ← НЕТ механики
ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено
```
**Порядок (решение пользователя 2026-08-04): СНАЧАЛА приёмка уровня 2,
потом уровень 3.** Причина техническая, а не вкусовая: чомперы лягут в
тот же банк 2, где живёт отрисовка, и чинить баги фона поверх свежей
механики дороже, чем до неё.
| # | Задача | Что | Блокирует |
|---|--------|-----|-----------|
| 1 | [L2-PASS](#l2-pass) | приёмка уровня 2 + баги отрисовки | ← **сейчас** |
| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 |
| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 |
| 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 |
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 |
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
| ✔ | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** |
| ✔ | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б |
---
## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3
Сюда попадает то, что уже работает в проверочном прогоне, но должно быть
подтверждено на сквозных прогонах уровней — потому что задевает механику
шире, чем собственный сценарий.
- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан
2026-08-06, предварительно проверен пользователем). Почему нужен именно
финальный прогон: точки вызова стоят не только в `check_action`, но и в
ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным
ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о
стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На
уровнях 1–3 это надо специально потрогать в паре мест каждого уровня.
Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`),
так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при
включённых enhancements.
## P0 — делаем сейчас
### <a id="l2-pass"></a>L2-PASS. Приёмка уровня 2 — **SMOKE ПРОЙДЕН 2026-08-05**
> **Прогон 2026-08-05 (пользователь): уровень 2 пройден.** Полная приёмка по
> всем комнатам — позже; из smoke пришли [BUG-LOOSE-3](bug_list.md#bug-loose-3)
> и [BUG-GUARD-DEAF-1](bug_list.md#bug-guard-deaf-1). Следом прогнан smoke
> уровня 3 — его наблюдения в [разделе «Уровень 3»](bug_list.md#уровень-3).
Баги отрисовки уровня 2 пользователь подаёт списком отдельно — они идут в
[`bug_list.md`](bug_list.md), раздел «Уровень 2». Ниже — **карта
содержимого уровня, снятая прямо с `res2002.bin`**, чтобы приёмка шла по
списку, а не «на глаз»: если механика в таблице есть, а в игре не
сработала — это баг, а не «так задумано».
**Стражи — 5, в комнатах 4, 7, 11, 15, 24** (skill 1/2/1/1/3, цвета
1/3/1/1/6 — цвет мы пока игнорируем, атлас один). Заметить: страж
комнаты 24 со skill 3 — первый по-настоящему опасный.
**Кнопки и что они открывают** (декодировано из LINKLOC/LINKMAP):
| Кнопка | Тип | Цель |
|--------|-----|------|
| к.9 @ряд1,кол1 | RAISE | **дверь уровня** к.23 @1,3 — то есть выход |
| к.11 @1,1 | RAISE | ворота к.18 @0,9 |
| к.18 @0,7 | RAISE | ворота к.7 @0,9 **и** к.18 @0,9 (две сразу) |
| к.18 @0,2 | DROP | закрывает ворота к.7 @0,9 |
| к.13 @1,4 | DROP | закрывает ворота к.13 @1,5 |
**Ловушки и предметы по комнатам:**
```
к. 3 loose@2,2 зелье@2,5 (здоровье)
к. 4 loose@1,7 loose@1,8 + СТРАЖ
к. 5 дверь уровня @1,2-3 — ВХОД (захлопывается за спиной)
к. 6 пики@1,3 loose@1,5 зелье@2,7 (здоровье)
к. 7 ворота@0,9 пики@2,7 + СТРАЖ
к. 8 зелье@2,2 (здоровье)
к. 9 кнопка RAISE@1,1 loose@2,6
к.10 пики@2,2
к.11 кнопка RAISE@1,1 + СТРАЖ
к.12 зелье@2,6 (здоровье) loose@2,8
к.13 зелье@1,3 (ВРЕДНОЕ, −1 HP) кнопка DROP@1,4 ворота@1,5 зелье@1,8
к.15 + СТРАЖ
к.18 кнопка DROP@0,2 loose@0,4 кнопка RAISE@0,7 ворота@0,9 зелье@2,6
к.19 пики@1,4
к.20 ЗЕЛЬЕ@1,2 — БОЛЬШАЯ СКЛЯНКА (+1 к потолку HP) пики@2,6 пики@2,7
к.23 дверь уровня @1,3-4 — ВЫХОД
к.24 + СТРАЖ (skill 3)
```
**Отдельно проверить то, что сделано именно сегодня и на уровне 1 не
проверялось:**
- **большая склянка** (к.20) — потолок HP становится 4, индикатор рисует
четыре деления, и это HP **переносится на уровень 3**;
- **меч уже в руках** с самого старта (`have_sword = level >= 2`) — на
уровне 1 его надо было подбирать;
- **выход через дверь к.23** вживую (кнопка в к.9) — путь тот же, что
закрыт на уровне 1 (L1-EXIT), но на этом уровне не прогонялся;
- **смерть/респавн** возвращают на уровень 2, а не на 1.
### <a id="rooms-graph"></a>Связность комнат уровней 1–3 (снято 2026-08-05)
Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет
соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись
13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый,
в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.
| уровень | старт | недостижимы | признак |
|---------|-------|-------------|---------|
| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет |
| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы |
| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** |
```
ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7
23 R→22, у 22 L=4 | 24 R→7, у 7 L=2
| 24 U→16, у 16 D=0
на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty
```
То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в
брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот
же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю
колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов
там низкий, в игре их не видно.
**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная
связь — вверх, откуда Кид падает):
```
к.10 U→4 пики(2,2) + пол — падение из комнаты 4
к.14 U→21 шахта 2 тайла шириной, дно = обломки
к.17 U→15 то же
```
Это не баги данных: в 14/17 попадают только падением насмерть, а из 10
(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать
«застрял» багом.
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
на каждом уровне, так что это вложение не только в ур. 3.
**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`,
как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`);
коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка
кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`).
**Риск:** банк 2 (`pop_bg`) занят на **86.5 %, свободно 2213 Б** — считать
место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся
в потолок» (коммит 2f3e854). Свободные номера банков есть (5+).
### <a id="l3-skel"></a>L3-SKEL. Скелет — единственный противник уровня 3
**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во
всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных»,
а **спецсобытие** `check_skel` (seg002:1044):
> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта,
> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на
> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут,
> `seq_88_skel_wake_up`, skill 2, HP 3.
Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают.
**Что нужно:**
- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при
`tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и
последовательностью `seq_63_guard_active_after_fall`, а не
`seq_77_guard_stand_inactive`;
- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill`
проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к
жизни (seg002:252);
- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py`
сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` =
GUARD/FAT/SKEL/VIZIER/SHADOW).
### <a id="l3-chkp"></a>L3-CHKP. Чекпойнт уровня 3
`level3_set_chkp` (seg002:519): вход в комнату 7 ставит `checkpoint = 1` и
`hitp_beg_lev = hitp_max`. `do_startpos` (seg003:141) при `checkpoint`
подменяет старт: комната 2, тайлпос 6, направление влево, и убирает
loose-плиту (комната 7, колонка 4, ряд 0). Механика `hitp_beg_lev` уже
есть (сделана в L2), остаётся сам флаг и подмена старта.
### <a id="tune-1"></a>TUNE-1. Параметры движка — в конфиг, а не в код
**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном
заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки
(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`).
Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а
не константой по месту.
**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без
пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под
моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа,
комментарии `;`, неизвестные ключи игнорировать), файл необязателен —
нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`,
`[debug]`, `[enhancements]`.
**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) +
`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его
(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и
опций — часть задачи: у него уже разложены по группам стартовые
HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало,
чекпойнт), тайминги ворот и пик, скорости, а отдельной группой —
`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже
портировали). Брать всё подряд не надо: переносим по мере того, как
константа реально понадобилась в игре.
**Оговорка по памяти.** Парсер и таблица параметров — холодный код,
исполняется один раз при старте: кандидат в банк, а не в резидент W1.
### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН**
> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как
> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой
> замер потока байт это не подтвердил: клавиатура MAME-Sprinter
> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при
> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения
> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO
> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся
> верным и нужным — переполнение вызывает не Shift, а короткая жизнь
> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия
> — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
>
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при
> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса
> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO
> SIO переполняется. Лечится ПЛОТНЫМ опросом: `kbd_raw_poll` повешен
> idle-хуком на ожидание кадра (`gfx_set_idle_hook`, новый API libbgi) —
> процессор всё равно проводит там ~42 мс из 60, крутя опрос луча.
>
> **Проверка в roomtest тем же счётным методом: 35 нажатий Shift+Home →
> 35 make, ноль потерь** (до фикса — 9 из 10). Боевой сценарий тоже:
> четыре Shift+→ подряд дали четыре осторожных шага, `Kid.x` 114 → 147.
> Цена: `_CODE` +170 Б, кадровый бюджет не затронут (опрос стоит в
> простое). Ниже — полный протокол, как к этому пришли.
>
> **ОСТАТОК (ручная проверка пользователем, 2026-08-01): «стало значительно
> лучше, но иногда при зажатом Shift стрелка всё-таки пропускается».**
> Ощущение, не замер — счётчики на 35 нажатиях подряд потерь не показали,
> значит остаточная частота заметно ниже прежних ~15 %. **Задача осознанно
> ОТЛОЖЕНА до финальной полировки всей программы** (решение пользователя);
> сейчас клавиатура пригодна для работы.
>
> **Где именно осталась дыра — чтобы на полировке не начинать с нуля.**
> Idle-хук покрывает простой, то есть ~2/3 кадра. Оставшаяся треть — это
> занятая фаза, и там DI-окно одного accel-прохода доходит до ~650 мкс при
> допуске FIFO ~300 мкс. Пачка байт, целиком попавшая в такое окно, всё
> ещё может потерять байт — ровно «иногда». Порядок действий, если
> вернёмся:
> 1. Вернуть вызовы `kbd_raw_poll()` между блитами занятой фазы (они
> бесплатны; сами по себе не помогали, но вместе с хуком закрывают
> именно этот зазор) и при необходимости внутрь тайловых циклов
> `pop_bg` — тогда слепым остаётся только тело одного блита.
> 2. Мерить тем же счётным методом (см. ниже), а не на ощупь: скриптовые
> нажатия ровнее человеческих, поэтому набирать выборку от 50 нажатий.
> 3. Если и это не добьёт — остаются два рычага вне нашего кода: Scan Code
> Set 3 через BIOS `$EA` (убирает «fake shift» в корне, но в MAME
> непроверяемо — обратный путь к клавиатуре не разведён) и общий
> `m_irq_off_timer` в драйвере MAME.
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
нажатия ← не отрабатываются, пока Shift не отпустишь.
**Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»).**
Три известных факта складываются в одну картину:
1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ
клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием
шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было
бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). То есть
ровно в связке Shift+стрелка трафик удваивается.
2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то,
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
«нажатие не отработало»; потерянный break = залипание (его лечит
`kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш).
3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.**
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()`
выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая
запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же
заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию.
**Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт
остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц).
4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит
(`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»);
порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это
сотни микросекунд, на порядки больше 32-тактового импульса.
Отсюда: **обе версии пользователя — про одно и то же.** Логика ввода
(`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с SDLPoP и
выглядит корректной: `safe_step()` ставит `control_forward = CONTROL_IGNORE`,
и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть
повторные тапы ← при зажатом Shift обязаны работать. Не работают они
потому, что до нас не доезжает либо make, либо break стрелки.
**План проверки — по шагам, каждый даёт артефакт:**
1. Счётчики в MAME: брейк на `_kbdraw_overrun` (запись) и на ветке
`tr_kbd_drain` — сколько overrun'ов за 10 с при «Shift зажат, тапаю ←»
против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше.
2. Замер максимального DI-окна кадра: брейкпоинты на `di`/`ei` в
`_bgi_blit_cols_raw` + `{printf totalcycles; g}` — получить реальную длину
в тактах и в микросекундах.
3. **Спайк «блит без DI».** `docs/new/06-accel.md §6.6`: новая прошивка
допускает работу акселератора при EI (по приходу прерывания он
отключается, по `RETI` включается обратно); старая — нет. Собрать libbgi
с убранным `di` в блит/heal-ядрах, прогнать roomtest в MAME: (а) не
рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да —
причина подтверждена, и дальше это вопрос «какая прошивка на живом
железе» (по умолчанию оставить DI, режим без DI — опцией libbgi).
4. **Независимо от п.3 — `kbd_raw_poll()`.** Вычерпывание FIFO ОПРОСОМ
(порт `0x19` бит 0 → читать `0x18`, тот же декодер make/break, что в
трамплине) из главного цикла 2–4 раза за кадр между фазами `PROF()`.
Снимает зависимость от «поймали ли мы импульс IRQ» вообще, стоит сотни
тактов, графику не трогает. Реализация: вынести drain-цикл из
`libc/irq/_irq_tramp.c` в общий кусок либо продублировать в
`libc/kbd/kbd_raw_poll.c`; тело обязано идти под `DI` (гонка с ISR за
деструктивное чтение порта 0x18).
5. Побочно сюда же играет **[T-2](bug_list.md#t-2) (idle-skip)**: не
перерисовывать Кида, пока поза/координаты не менялись, — это минус
heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз
и тапают Shift+стрелку.
6. Только если после 3–4 симптом жив — копать логику
`control_shift2`/`CONTROL_IGNORE` против `seg005.c:374..390`.
**Критерий готовности:** при зажатом Shift десять тапов ← дают десять
осторожных шагов (проверка в MAME через `:kbd:ms_naturl:*` напрямую, НЕ
через `press_key` — тот дёргает обе клавиатуры, см. `docs/libc-reference.md`
`<kbd_raw.h>`).
---
### KBD-1: ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не
спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же
`E0`-префикс и тот же «fake shift», что у стрелок), но игрой игнорируется,
поэтому Кид стоит на месте и рельеф комнаты на результат не влияет.
Брейкпоинты с действием `{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки
машины) в трёх точках: вход клавиатурной ветки трамплина, чтение порта 0x18
внутри drain-цикла, запись make-бита для кода `0x6C`. Скратч-байты — хвост
`ovr_tile[]` (в этом сценарии не используется).
**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до
декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил
пользователь.
| Прогон | make дошло / нажато | overrun |
|--------|---------------------|---------|
| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 |
| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 |
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 |
**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет
и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
**Вывод 2: `kbd_raw_poll()` в текущей расстановке бесполезен** — 9/10 и с
ним, и без. Причина понятна задним числом: шесть вызовов стоят В ТЕХ ЖЕ
точках, где прерывания и так разрешены, то есть добавляют ровно то, что
трамплин сделал бы сам. Вызовы из `roomtest.c` убраны; сама функция в libc
оставлена — она корректна и нужна как заготовка под «плотный опрос» (см.
ниже), но в горячем цикле её держать не за что.
**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта
0x18: 5 нажатий Shift+Home должны дать ровно 50 байт (нажатие `E0 F0 12` +
`E0 6C`, отпускание `E0 F0 6C` + `E0 12` = по 10 на цикл). Насчитано **49**
— и ровно один make потерян. То есть до процессора байт не доехал вообще,
декодер тут ни при чём.
**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же
49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина
(1.75 байта за вход). То есть больше сорока процентов импульсов запроса
не были обслужены, и байты копятся в трёхбайтовом FIFO вплотную к его
потолку; одна неудачная пауза — и байт потерян.
### KBD-1: ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме
`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода:
любая работа разредила бы опрос и испортила замер). Это физический
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|--------|---------|-----------|-----------------------------|
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
| Shift + Home | **25** | **25** | **250 / 250** |
**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах
за кадр терялся 1 байт из 50. При такой частоте потерь вероятность
случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не
совпадение.
**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно
опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за
60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято
работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая
луч. Опрос там стоит ноль и покрывает две трети периода с запасом по
плотности. Остаётся слепым только тело одного accel-блита под DI (до
~650 мкс) — разорвать его нельзя (см. «что НЕ делать»).
**Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30
нажатий в секунду»).** Частота опроса определяется НЕ темпом нажатий, а
темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Одно нажатие при
зажатом Shift — это 5 байт подряд (`E0 F0 12`, `E0 6C`), отпускание — ещё 5,
и клавиатура выдаёт их со скоростью провода: 11 бит на байт при ~10–16 кГц
= ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями
имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем
раз в ~1.5–2 мс (0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду
переполнит FIFO**, если в эти несколько миллисекунд его никто не разгребает.
Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости.
В норме разгребает прерывание; опрос понадобился только потому, что ~44 %
импульсов здесь теряется.
**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md`
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт,
break = 2. Нажатие с шифтом перестаёт превышать FIFO в принципе.
- либо хотя бы отключить typematic (`0xF5`/`0xF7`).
**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только
направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
кандидат на «когда дойдём до реального железа», а не на сейчас.
**Что сделано по этому плану (2026-08-01):**
1. ✅ Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
нужен push/pop, а сам таймаут в итерациях станет длиннее по времени.
**Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда,
где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. Полезной
работы не отнимается нисколько.
**Ограничитель области, если «постоянный опрос» всё равно не нравится:**
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
тогда опрос работает исключительно в той ситуации, ради которой заведён.
2. ✅ Перемерено в roomtest тем же счётным методом: **35/35**, потерь нет.
3. ⏳ Вызовы в занятую треть кадра НЕ возвращены: одного idle-хука хватило.
Держать в уме, если на реальном железе или на более тяжёлых сценах
(несколько стражей) потери появятся снова — накрыть блиты дешевле, чем
изобретать что-то новое.
4. ⏳ Ручная проверка пользователем — без неё этап не закрыт: скриптовые
нажатия ровнее человеческих, и «залипания до отпускания Shift» они не
воспроизводили с самого начала.
**Куда смотреть дальше, если плотного опроса не хватит.**
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
его машине занимает часы). Для протокола, подозрение осталось:
`sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта
CPU**, и `m_irq_off_timer`**один на два источника** (`irq_on()` экрана
заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое
прерывание способно обрезать клавиатурный импульс — правдоподобное
объяснение «44 % пропущенных импульсов».
2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе
проблемы может не быть вовсе. Проверять при первом прогоне на живом
Sprinter.
**Про совпадение кадрового и клавиатурного прерываний** (вопрос
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл,
значит экран»; совпадение возможно, но «исключительно редкий случай»
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
делает ровно предписанное. Известный побочный эффект: при совпадении мы
обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка,
в KBD-1 не входит.
**Что НЕ делать (проверено, стоило времени):**
- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di``nop` в
`_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в
памяти **уронил машину в перезагрузку**. То есть режим «акселератор
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
недоступен — вопрос закрыт артефактом, а не рассуждением.
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
---
### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01**
> **Итог.** Heal Кида и стража переведены на выбор ядра по тому же тесту,
> что давно стоит у блитов. Замер в MAME (счётчики `totalcycles` на входах
> `kid_heal` и `kid_tick`, то есть вся группа heal за кадр; комната 1, Кид
> стоит, стража нет):
>
> | путь | тактов на кадр | кадров в выборке |
> |------|----------------|------------------|
> | клипающее ядро (как было) | **26 200** | 149 |
> | noclip (стало) | **15 848** | 239 |
>
> **−10 352 такта на кадр, то есть −39.5 % с группы heal** (≈0.49 мс при
> ~21 МГц). A/B честный: оба замера сняты в ОДНОМ прогоне, вторая половина —
> с пропатченным в памяти условием (`jr nz` → `jr` в `pop_heal_fast`), то
> есть на той же геометрии и в той же сцене.
>
> **Размер: −362 Б суммарно** (не плюс!): `_CODE` 25 289 → 25 306 (+17),
> BANK2 13 792 → **13 676** (116, свободно стало 2708 Б — это тот самый
> тесный банк из рисков `levels_plan.md` §5), BANK3 6512 → 6249 (263),
> BANK4 без изменений.
>
> **Грабли, стоившие двух пересборок** (вынесено в память
> `sdcc-static-inline-double-cost`): первым заходом хелпер был `static
> inline` в `pop_bg.h` — и SDCC 4.5 И встроил его тело (181 Б) в каждое
> место вызова, И оставил отдельную копию в КАЖДОМ TU, который видит
> заголовок. `pop_guard_heal` раздулся с ~60 до 663 Б, итого +1091 Б в
> `_CODE` и +636 Б в банке стража. Лечится обычной функцией в одном
> резидентном модуле (`pop_draw.c`, W1 — из банков это прямой `call` без
> трамплина, как у `pop_sword_draw`).
>
> **Проверено визуально:** обычная ходьба, прыжок, спуск и позиция «за
> решёткой шва» (straddle — там как раз работает клипающий фолбэк) —
> артефактов и следов нет.
**Ниже — исходная постановка задачи (что и почему смотрели).**
**Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
**Что уже правильно** (шаблон, который надо распространить): блиты Кида,
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
`gfx_blit_cols_part_noclip`, иначе в клипающий вариант
(`pop_kid.c:86,591,676`, `pop_gdraw.c:105`).
**Что чинили:**
-`kid_heal()` и `pop_guard_heal()` звали `gfx_heal` — **всегда с
клипом**, хотя `gfx_heal_noclip` существует и `heal_off` в `pop_bg.c` им
уже пользовался. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего
ядра — 11 658 тактов на heal 22×22. Теперь все три идут через общий
`pop_heal_fast` (`pop_draw.c` + `_pop_draw.h`).
-`pop_room_clip_borders()` (`pop_bg.c`) — `gfx_heal(0,0,320,…)`:
**оставлен клипающим осознанно**. Полоса шириной 320 не лезет в 8-битный
параметр noclip-ядра, а бить её на два куска по 160 нет смысла: гейт
`border_dirty` пускает туда только в кадрах падения, и выигрыш подготовки
тонет в цене самих 320×28 пикселей. Причина записана прямо в коде, чтобы
не «оптимизировать» повторно.
**Что обязано остаться с клипом** (зафиксировано комментариями в коде):
- кромочные тайлы фона (`blit_b` в `pop_bg.c`) — тайл у края экрана режется
по построению;
- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной)
и при падении ниже поля — фолбэки в `kid_draw`/`pop_guard_draw`;
- борта поля (`pop_room_clip_borders`) — см. выше.
**Что уже было правильно** (шаблон, который и распространили): блиты Кида,
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
`gfx_blit_cols_part_noclip`. Отдельный случай — `pop_kid_img_blit`: noclip
БЕЗ проверки, потому что единственный вызывающий (полоса HP) рисует по
фиксированным координатам; это тоже помечено в коде.
---
## P1 — сразу после P0 (закрываем уровень 1 как ИГРУ, а не стенд)
### L1-START. Старт по данным уровня — **СДЕЛАНО 2026-08-01**
Старт и оба рестарта (смерть, выпадение из уровня) сведены в один
`pop_start_level()` — порт `start_level` + `do_startpos` + `set_start_pos`
(seg003): комната/тайл/направление берутся из `pop_level_start_*`, направление
инвертируется (`~start_dir`), поза входа — из `tbl_entry_pose`. У уровня 1
это «падение внутрь» плюс нажатие кнопки room5(0,2) — то самое, что
захлопывает решётку за спиной. Проверено: старт даёт room 1, col 0, падение
на row 1 — как по данным уровня.
`#define ROOMNAV` оставлен ВКЛЮЧЁННЫМ осознанно: это наш чит, которого в
оригинале не было, — как и `S` (выдать меч), `K`, `I`. Все они со временем
съедутся в общий блок читов, разрешаемый в настройках (решение 2026-08-01).
### L1-EXIT. Выход с уровня (дверь уровня) — **СДЕЛАНО 2026-08-01**
Портированы: ветка двери уровня из `up_pressed` + `go_up_leveldoor`
(seg005:0482/0574) — в `pop_leveldoor_enter()` (`pop_map.c`, тайлы и
геометрия) и `up_pressed()` (`pop_ctrl.c`, только последовательность);
опкод `0xF1 END_LEVEL` в `play_seq` теперь инкрементит `pop_next_level`
(порт `next_level`), а главный цикл по нему перезапускает уровень — ровно
та точка, куда `levels_plan.md` §2.2 подключит загрузку уровня 2.
Открытость двери проверяем по `modifier >= 42` (ветка `fix_exit_door`), а не
по ванильному `leveldoor_open`: с ванильным условием можно войти в ещё
ползущую створку.
**Грабли, которые стоили отдельного разбора:** `go_up_leveldoor` сначала
писал `Char.x`/`Char.direction`, и оба присваивания молча терялись — окно
`Char` вокруг диспетчера возвращает назад ТОЛЬКО `curr_seq` и `sword`
(`pop_savekid_state`). Направление оставалось «вправо», а все `DX`
последовательности seq_70 отрицательные, поэтому Кид уходил ИЗ проёма влево.
Вывод на будущее: геометрию персонажа в этом порте меняет `pop_map` (пишет в
`Kid`), а не диспетчер.
✅ Косметика тоже закрыта: BUG-DOOR-CLIP (обрезка силуэта правым косяком
проёма) — недоставало второй половины `clip_char` (`obj_clip_right`) и
обрезки СПРАВА у колоночного блита. В libbgi добавлен
`gfx_blit_cols_part_w(..., maxw)`; разбор — в [`bug_closed.md`](bug_closed.md).
### L1-TRIAGE. Ревизия багов — **ЧАСТЬ 1 СДЕЛАНА 2026-08-01**
**Три Critical'а прогнаны в MAME и закрыты** (протокол с числами — в
[`bug_closed.md`](bug_closed.md), раздел «Проверено в MAME 2026-08-01»):
BUG-1 (провал на row 1 при переходе через открытые ворота) и BUG-2
(ping-pong при возврате) **не воспроизводятся**, BUG-3 (окклюзия climb-up на
кнопке) закрыт фиксом `tile_code_drawn` от 2026-07-28. Заодно снят неверный
диагноз BUG-1: репроекция Y при БОКОВОМ переходе — не наш пробел, а точное
поведение `goto_other_room` (`SDLPoP/src/seg002.c:390` меняет только `x`).
Список разделён на [`bug_list.md`](bug_list.md) (открытое) и
[`bug_closed.md`](bug_closed.md) (закрытое + разбор корней).
**Косметика окклюзии тоже закрыта** — BUG-CEIL-1/2/3 и BUG-OCCL-1 были
починены кодом ещё в июле, а записи никто не снял: `ceil_over_kid_tile`
(`pop_bg.c:1275`), `pop_ceil_modif` + `pop_ceil_shake_draw`/`_bake_empty`
(`:547,815,827`), `bar` с `POP_YOFF+3` в `pop_room_redraw_seam_left` (`:793`),
разделение слоёв по `add_backtable` vs `ptr_add_table` в `overlay_mid_tile`
(`:1367`). Разбор — в [`bug_closed.md`](bug_closed.md).
**Осталось:** закрыть таблицу обхода 24 комнат
([`bug_list.md`](bug_list.md#обход-всех-24-комнат-уровня-1)) — заполнена на
5 строк из 24, инструмент (`ROOMNAV`) готов. Делать вместе с L1-PASS: это
единственный оставшийся источник новых багов уровня 1.
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка —
секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».
### <a id="l1-pass"></a>L1-PASS. Сквозное прохождение уровня 1 — **SMOKE ПРОЙДЕН 2026-08-05**
> **Прогон 2026-08-05 (пользователь): успешный.** От старта до выхода с
> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою**
> (без читов), выход отработал корректно. Это smoke: пройдены не все
> комнаты, поэтому таблица обхода 24 комнат остаётся открытой. Ценность
> прогона в том, что он снял главные риски этапа 1 разом — боёвка,
> предметы, переход с уровня — и стал регресс-базой для уровня 2.
От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота,
пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно
регресс-база для уровня 2.
Совмещать с [таблицей обхода 24 комнат](bug_list.md#обход-всех-24-комнат-уровня-1):
известные баги закрыты, значит новые придут только отсюда. Точки, где стоит
смотреть внимательно, — уже закрытая косметика окклюзии (потолок при прыжке
вверх, шов при анимации решётки, грани дальней колонны) и подъём на
тайл-кнопку: см. оговорку к BUG-3 в [`bug_closed.md`](bug_closed.md).
### L2. Переход на уровень 2 и его игра — **МАШИНЕРИЯ СДЕЛАНА 2026-08-04**
Порт `levels_plan.md` §2 (шаг 1). Что появилось:
- **Номер уровня стал состоянием.** `pop_current_level` (порт
`current_level`) в `pop_level.c`; `pop_next_level` больше не флаг, а
НОМЕР — `END_LEVEL` его инкрементит (как `++next_level`, seg006:662), а
главный цикл срабатывает по расхождению `pop_next_level !=
pop_current_level` (порт `play_level_2`, seg003:0386).
- **Загрузка по номеру** — `pop_level_load_num(n)`: `LEVELS\res20NN.bin`
(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 = стражей нет**, иначе на
14/15 они полезли бы из данных комнат), `tbl_level_type` (тайлсет —
пока только читается).
- **`find_start_level_door`** (seg003:02E6) — на уровне 2 это НЕ косметика:
стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина двери
уровня, и без `modif = 43` + `add_trob(...,3)` Кид материализуется внутри
глухой створки. Тип 3 = «быстро закрыть»: дверь захлопывается за спиной
за три кадра, как в оригинале.
- **HP через уровень** — `hitp_beg_lev` (seg003): рестарт уровня
откатывает HP к нему, пройденный уровень подтягивает его к `hitp_max`.
Заодно реализована **большая склянка** (`add_life`, тип зелья 2: +1 к
ПОТОЛКУ HP до 10) — на уровне 2 она есть, комната 20.
- **Меч** — `have_sword = level >= 2` (play_level, seg003:106), а не
жёсткий ноль.
- **Чит Shift+L** (seg000:698) — `pop_next_level = pop_current_level + 1`.
NB: `L` без Shift занят отладочным «осторожным шагом вправо»
(`pop_ctrl` `KBD_DBG_STEPR`); с Shift шаг тоже пройдёт, но уровень тут же
сменится — на практике не мешает.
**Проверено в MAME (2026-08-04):** старт уровня 1 → Shift+L → уровень 2
рисуется правильно (комната 5, большие колонны — новые для нас тайлы 8/9 —
на месте, дверь захлопнута, HP 3) → влево в комнату 4, страж на месте →
Shift+L → уровень 3 (комната 9). То есть цепочка загрузок работает
повторно, а не только один раз.
**Что осталось по уровню 2 (не машинерия, а контент):**
- сквозное прохождение уровня 2 руками — приёмка, как L1-PASS;
- выход через дверь уровня 2 (комната 23) вживую: путь тот же, что уже
закрыт на уровне 1 (L1-EXIT), но не прогнан на этом уровне;
- цвет стража из данных (`guards_color`, на уровне 2 их три разных) — у нас
один атлас; отдельная задача, к переходам отношения не имеет.
### <a id="dbg-cheats"></a>DBG-CHEATS. Отладочные читы SDLPoP — разведка сделана, код не написан
Записано 2026-08-03 (по ходу охоты за BUG-GATE-PASS-1, отложено на потом).
Мотив прямой: мост MAME **теряет нажатия при быстрой отправке**, поэтому
подогнать Кида в нужную позу для отладки автоматикой сейчас нельзя — именно
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.
**Берём (дёшево и бьёт в наш класс багов):**
- **`[` / `]` — сдвинуть Кида на пиксель влево/вправо.** Оригинал —
`../SDLPoP/src/seg000.c:1828`:
```c
if (key_states[SDL_SCANCODE_RIGHTBRACKET] & key_state) ++Char.x;
else if (key_states[SDL_SCANCODE_LEFTBRACKET] & key_state) --Char.x;
```
У нас: коды PS/2 set 2 `[` = **0x54**, `]` = **0x5B** в `pop_cheat.h`
рядом с `KBD_CHEAT_KILL/IMMO/SWORD`; обработка — в том же блоке читов
`roomtest.c` (~строка 479), **по фронту** (`*_prev`, как у остальных),
иначе одно нажатие уедет на десяток пикселей. Работает по `Kid.x`
напрямую: геометрию персонажа в этом порте меняет не диспетчер (см.
грабли L1-EXIT).
Первый же потребитель — BUG-GATE-PASS-1: там надо снять `Kid.x` и
`Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё стоит
на её тайле.
- **Shift+L — следующий уровень.** Уже входит в L2-машинерию
(`../docs/levels_plan.md` §2.4): `++pop_next_level` и всё.
**Остальные — оценка, а не обязательство:**
| Чит | Вердикт |
|-----|---------|
| **T — таймер** | **не сейчас**: таймера уровня у нас нет вообще (Фаза 6), чит пришлось бы делать вместе с механикой |
| **F — остаток feather-fall** | **вместе с Shift+W**: ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` есть, но не проверена ничем; индикатор без самого зелья бесполезен, а пара «включить + видеть остаток» закрывает ветку целиком. Перо — уровень 7, так что не срочно |
| **Shift+F9 — quickload с тем же уровнем** | **самое ценное и самое дорогое**: это сериализация `Char` + `room_modif` всех комнат + trob'ов + стражей (`levels_plan.md` §4). Даёт воспроизводимый регресс «вот кадр, где баг» вместо ручной подгонки. Кандидат сразу после того, как заработают уровни |
---
### <a id="mem-bank5"></a>MEM-BANK5. Разгрузка W1/W2 новым банком кода
Вопрос 2026-08-04: «надо делать новый банк?». **Да, и он лечит именно то,
что жмёт.** В нашей раскладке (`MEMORY=huge`, small-вариант) CODE и DATA
живут в ОДНОМ 32-КБ пространстве W1+W2 — карта текущей сборки:
```
_CODE 0x4100..0xAA00 26880 Б
_DATA 0xAAD0..0xB9B0 3808 Б
_BSS 0xB9B8..0xBADA 290 Б
куча 0xBADA..0xBB00 38 Б ← упёрлись сюда, добавляя отладку
стек 0xBB00..0xC000 1280 Б
```
Поэтому **каждый килобайт кода, уехавший в банк, становится килобайтом,
доступным данным**. Отдельного «дефицита W2» у нас нет — дефицит один.
(38 байт кучи не опасны сами по себе: malloc'ом мы не пользуемся, страницы
берутся через `mem_alloc_block`. Опасно то, что следующая структура
данных упрётся в стек молча.)
Резидентный код по модулям (из `.sprinter-cc-roomtest/*.rel`):
| модуль | _CODE | как часто зовётся | в банк? |
|--------|------:|-------------------|---------|
| `pop_kid.c` | 6287 | `load_frame`/`play_seq` — 2×/кадр (Кид + страж) | частично: холодная половина (загрузка страниц спрайтов, `pop_kid_load`) — да; движок кадров — нет |
| `roomtest.c` | 3893 | main-loop | нет (точка входа, зовёт всех) |
| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | **кандидат №1**, если вынести аксессор в резидент |
| `pop_level.c`| 2292 | `pop_level_tile` — из `pop_bg` (банк 2) на каждый тайл | **нет**: банк→банк на каждый тайл убьёт отрисовку |
| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **кандидат №2** — дёшево и безопасно |
| `pop_guard.c`| 923 | 1×/кадр | нет смысла |
Порядок действий, когда упрёмся: `pop_ctrl.c` → банк 5 (2.2 КБ, один
banked-вызов за кадр), затем расщепление `pop_kid.c` на горячее ядро и
холодную загрузку. Критерий кандидата — **не размер, а частота вызова и
отсутствие горячих банк→банк переходов**; `pop_level` показывает, что
большой холодный на вид модуль может быть горячим аксессором.
Брать по факту нехватки места, не заранее.
---
## Отложено осознанно (не брать, пока не появится причина)
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти
бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается.
- **Отключение мыши на время игры** и **замена PRNG** —
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость
транзиентная; разбор в [`bug_closed.md`](bug_closed.md).
+47
View File
@@ -0,0 +1,47 @@
/*
* _pop_draw.h — внутренний заголовок слоя отрисовки: выбор ядра libbgi
* (клипающее / линейное «noclip») по одному и тому же тесту для блита и
* для heal.
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ ФАЙЛ, А НЕ pop_bg.h. Обе функции — `static inline`, а
* SDCC 4.5 оставляет тело такой функции в КАЖДОМ TU, который видит
* объявление, даже если тот её не зовёт (memory
* `sdcc_inline_codegen_findings`). pop_bg.h включают почти все модули PoP,
* то есть из общего заголовка эти ~100 Б размножились бы десятком мёртвых
* копий (замер CLIP-1, 2026-08-01: +1091 Б в _CODE и +636 Б в банке
* стража — за код, который там никто не вызывает). Здесь их видят ровно
* три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_gdraw.c.
*/
#ifndef POP_DRAW_H
#define POP_DRAW_H
#include <stdint.h>
#include <gfx.h>
/* Спрайт целиком на экране И укладывается в 8-битные параметры noclip-
* примитивов libbgi? Условие входа в gfx_blit_cols_part_noclip: клипающий
* вариант платит ~5.6 К тактов подготовки на КАЖДЫЙ вызов, независимо от
* того, вылезает край или нет. */
static inline uint8_t pop_onscreen_cols(int x, int y, uint16_t w, uint16_t h)
{
return (uint8_t)(x >= 0 && y >= 0 && w < 256 && h < 256 &&
x + (int)w <= 320 && y + (int)h <= 256);
}
/* Стереть прямоугольник (heal из ОЗУ-копии) тем же приёмом, что и блит:
* целиком на экране → линейное ядро без клипа. Условие входа у
* gfx_heal_noclip ровно то же, что у gfx_blit_cols_part_noclip, поэтому
* тест один — общее ядро gfx_heal платит за клип и 16-бит, а не за пиксели
* (замер: 11 658 тактов на heal 22×22).
*
* НЕ inline (тело — pop_draw.c, резидент W1): SDCC 4.5 встраивал бы его в
* каждое место вызова по 181 Б И оставлял мёртвую копию в каждом TU — см.
* шапку pop_draw.c с замером. Зовётся 5 раз за кадр, цена вызова тонет в
* стоимости самого heal.
*
* w/h тут int, а не uint16_t: вызывающие считают их вычитанием (h − skip), и
* отрицательный результат обязан быть no-op, а не превратиться в огромный
* unsigned. */
void pop_heal_fast(int x, int y, int w, int h);
#endif
File diff suppressed because it is too large Load Diff
+293 -343
View File
@@ -1,363 +1,281 @@
# roomtest — список известных багов (Фаза B и смежное) # roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас** Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней)
— задача следующего этапа. Правило проекта: механику сверять с переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
`applications/PoP/SDLPoP/src/` ДО кодинга. грепни там по симптому.
Статус на момент записи: 2026-07-21. Тестовый старт — комната 6 (`START_ROOM` Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта:
в `roomtest.c`). Кнопка room6(0,2, opener) → ворота room8(0,9), видны в левом механику сверять с `../SDLPoP/src/` ДО кодинга.
шве room6.
Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать
наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены
в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень
стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень),
BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1
(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в
`bumped_floor`). Клавиатурный BUG-KBD-3 доведён до конца во второй волне
(см. ниже); из первой волны открытым остался косметический BUG-SEAM-DRAW-1.
Ревизия **2026-08-03, вечер** (вторая волна с прогона): все четыре наблюдения
закрыты — BUG-KBD-4 (фантомный отжим Shift), BUG-RESPAWN-2 (стражи не оживают),
BUG-DRAWORDER-1 (Кид поверх тела стража) и BUG-LOOSE-2 (осколки только от одной
из двух плит). У BUG-DRAWORDER-1 остался артефакт «ноги поверх головы»,
сверенный с SDLPoP и записанный в «НЕ БАГИ». Тогда же закрыт как
невоспроизводящийся BUG-SEAM-DRAW-1 (Кид в шве рисуется) и заведён
BUG-GATE-PASS-1 — однократное наблюдение прохода сквозь закрывшуюся решётку,
которое ЖДЁТ надёжного сценария.
| ID | что | тип | статус |
|----|-----|-----|--------|
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария** (BUG-GATEMOD-1, из-за которого решётка стартовала не в том состоянии, закрыт — перепроверить) |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
--- ---
## BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **Critical** <a id="уровень-2"></a>
# Уровень 2 — баги с приёмки (заведено 2026-08-04)
**Симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов) Уровень 2 заиграл 2026-08-04 (см. [`TASKS.md`](TASKS.md) запись **L2**).
Kid оказывается на ряду **row 1** вместо row 0 — «проваливается» на этаж ниже Пользователь при первом прогоне увидел **несколько багов отрисовки** и
(стоит в дверном проёме на неправильной высоте). подаст их списком отдельно — раздел заведён под них заранее, чтобы
наблюдения не растеклись по переписке.
**Где:** `pop_map.c` `check_leave()` (порт seg002 leave_room) — боковой уход **Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный
`pop_leave_dir`; репроекция координат `Kid.x ∓= 140` есть, но Y/`curr_row` бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла).
при боковом переходе НЕ репроецируются (в отличие от `check_leave_below`,
где `Kid.y -= 189; curr_row = y_to_row`). При входе в соседнюю комнату у
шва Kid, видимо, попадает в позицию, где `determine_col`/физика следующего
кадра сбрасывают его на row 1 (нет пола на row 0 у входа, либо неверный y).
**Сверить с SDLPoP:** `goto_other_room` (seg002) — как именно **Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся
пересчитываются `Char.curr_row`/`Char.y` при side-переходе (left/right). здесь целиком — фиксы берём, когда уровень пройден до конца. Причина:
Для вертикального перехода мы это сделали, для бокового — нет. косметика отрисовки почти всегда сводится к общему корню (ср. волну
BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит
чинить один и тот же корень трижды.
## BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **Critical** Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие
кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass),
запись **L2-PASS**: по ней видно, «механика не сработала» это или
«так и задумано».
**Симптом:** после перехода в соседнюю комнату попытка сразу вернуться Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по
обратно приводит к тому, что Kid снова закидывается в ту же комнату — симптому: почти вся косметика окклюзии уровня 1 уже разбиралась
переход «пинг-понгует», выйти нельзя. (BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.
**Где:** `pop_map.c` `check_leave()`. После перехода Kid репроецирован к <a id="уровень-3"></a>
ПРОТИВОПОЛОЖНОЙ кромке новой комнаты (`x ∓ 140`). На СЛЕДУЮЩЕМ же кадре # Уровень 3 — баги с приёмки (заведено 2026-08-05)
передний край снова за порогом ухода в обратную сторону → `pop_leave_dir`
взводится опять → мгновенный обратный переход. Нужен guard «только что
перешёл» (подавить повторный leave на N кадров / пока Kid не отойдёт от
кромки), как в оригинале (`next_room`/leave обрабатываются раз за кадр +
Kid входит достаточно внутрь).
**Сверить с SDLPoP:** порядок `leave_room`/`goto_other_room` + на какой Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
X ставится персонаж при входе (должен быть НЕ на самой кромке, иначе из них — **не баги, а неначатые задачи**, и записаны как таковые:
мгновенный обратный триггер).
## BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия (Kid поверх кнопки) — **Critical** | наблюдение | что это на самом деле |
|------------|------------------------|
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта |
| к.10: скелет не оживает | [L3-SKEL](TASKS.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 |
**Симптом:** когда Kid подтягивается (climb-up) на тайл, верх которого — Остальные два были настоящими багами и **закрыты в тот же день** — разбор
КНОПКА (opener/closer), а не обычный пол, отрисовка неверная: Kid рисуется корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги
ПОВЕРХ кнопки, хотя передняя грань пола/кнопки должна перекрывать нижнюю коллизий считались для одного ряда вместо трёх — бамп терялся на кадре
часть Kid во время подъёма. смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался
стеной).
**Где:** `pop_bg.c` `climb_overlay_tile()` — floor-overlay при подъёме <a id="bug-spike-1"></a>
рисуется только для floor-подобных тайлов `code ∈ {1,3,5,19}`. Кнопка ## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
(opener 0x0F / closer 0x06) в этот набор НЕ входит → пол-оверлей кнопки не
рисуется поверх Kid. Нужно: для нажатой кнопки (draw как floor/stuck)
трактовать её как floor-тайл в climb-overlay (учесть подстановку из
`get_tile_to_draw`), и/или добавить opener/closer в набор.
**Сверить с SDLPoP:** `draw_floor_overlay` (seg008) + `get_tile_to_draw` > **Статус 2026-08-05, вечер.** Пользователь **повторить не смог**, а замер
(нажатая кнопка → tiles_1_floor / tiles_5_stuck) — оверлей должен идти по > (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То
ПОДСТАВЛЕННОМУ типу тайла. > есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай —
> пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в
> оригинале. Запись оставлена открытой ровно из-за визуального расхождения
> со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.
>
> **Как получить состояние нарочно:** подойти к пикам вплотную (выдвинутся),
> отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним.
> Пользователь пробовал и это, и попиксельную подгонку X читом `[`/`]` —
> не поднялось. Вывод для будущего разбора: состояние **не чисто
> позиционное**, одной шириной габарита его не объяснить; следующий
> подозреваемый — момент, в который `process_trobs` застаёт модификатор
> относительно кадра Кида.
**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 6, пики (1,3):
| действие | что происходит |
|----------|----------------|
| длинный прыжок с ряда 0 на пики | **смерть — правильно** (путь `fell_on_spikes`) |
| пробег по ряду 1 прямо по пикам | **урона нет** |
| после уборки пик | **на экране остаются белые остатки остриёв** (в оригинале чисто) |
| прыжок на месте, стоя на пиках | урона нет |
| просто стоять на выдвинутых пиках | можно сколько угодно |
**Замер (MAME, чтение `room_modif` комнаты 6).** Пока Кид стоит на тайле,
модификатор пики (индекс 13) = **0x8E**, то есть «пики ПОЛНОСТЬЮ вышли и
идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом:
```
is_spike_harmful (seg007:1178): 0/-1 → 0; <0 → 1; 1..4 → 2; >=5 → 0
check_spiked (seg006:0658): убивает при h>=2 на кадрах бега 7..14
и при h!=0 на кадрах приземления 43/26
```
То есть **при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале**
смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши
`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked`
сверены с seg006/seg007 построчно и совпадают дословно.
**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью
выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного
окна 1..4. Триггер — `check_spike_below` (seg006:1199), и у нас он берёт
габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а
оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`).
Это первый подозреваемый; второй — момент вызова `process_trobs`
относительно `play_frame` (сколько кадров модификатор успевает пройти,
пока Кид добегает).
**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет
гипотезу.** Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и
«спрятались не все — часть артефактов осталась». Скриншоты рядом:
[наш](bugscreens/l2-r6-spikes-ours.png) и
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый
кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе
переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми»
и отсчёт до уборки не доходит. В оригинале в той же позе они убраны,
значит его `check_spike_below` эту колонку УЖЕ не задевает.
**ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).** Чистый
пробег по УБРАННЫМ пикам убивает штатно:
```
запись modif=1 : кадр 11 (беговой), x=112, curr_col=2 ← пики пошли вверх
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3 ← Кид уже НА тайле, h=2
запись modif=3 : кадр 177 (frame_177_spiked) ← напоролся
```
То есть `check_spike_below`, `check_spiked`, `is_spike_harmful` и тайминг
выдвижения работают правильно, и «раннего» триггера нет.
**Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми.** Пока габарит Кида
накрывает колонку пики, `check_spike_below` каждый кадр зовёт
`start_anim_spike`, а тот при отрицательном модификаторе переставляет его
обратно в 0x8F — отсчёт до уборки не доходит. А выдвинутые пики (h = 1)
для бегущего БЕЗВРЕДНЫ по правилам оригинала. Отсюда обе жалобы: пробег
по уже вышедшим пикам не убивает, и они же остаются торчать на экране.
**Что осталось выяснить (и это единственный открытый вопрос).** Код
`start_anim_spike` у нас с оригиналом совпадает дословно, значит оригинал
тоже удерживал бы пики, стой Кид там же. На скриншоте SDLPoP в похожей
позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку
пики уже не задевает. Разница в 2–3 пикселя посадки, а у нас такие
расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания
площадки SDLPoP уводит Кида на 134, мы — на 141).
**Как закрывать:** инструментировать SDLPoP (печать `char_x_left/right`,
`left/right_checked_col` и модификатора пики каждый кадр), проиграть ту же
сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить
с нашей трассой ПОЗИЦИЮ КИДА после приземления. Если позиции совпадут, а
диапазоны колонок разойдутся — виноват габарит (`kid_fp` против
`set_char_collision` текущего кадра); если разойдутся позиции — это отдельный
баг посадки, а пики — его следствие.
--- ---
## Оптимизации (отложено)
### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22 <a id="ручная-перепроверка-2026-08-03"></a>
# Ручная перепроверка фиксов (2026-08-03)
**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`:
openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`** чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы
(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально показать, что конкретный сценарий больше не воспроизводится, но НЕ
анимируются только бары решётки — `draw_gate_back` (~9 `env_b`). достаточно, чтобы поймать регрессии в соседней механике. Ниже — сценарии
ровно в тех формулировках, в которых баги были заведены.
**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода, Собрано и лежит в образе (`make` + `make hdd` уже прогнаны; libc пересобран,
**но только пока openness меняется** — т.е. во время открытия и медленного `make size-check` без роста).
авто-закрытия (~5 сек после схода с кнопки). В покое (ворота открыты-держим
кнопку / закрыты / FF) — 0%. Замерено в MAME: брейк на
`_pop_room_redraw_seam_left` (0x5A54) срабатывает ⟺ сегмент дорогой.
**Приём** (как у пик, но сложнее — heal НЕ годится: печёные бары устаревшие, | # | что проверить | ожидаемо |
поэтому и стоит `bar(BLACK)`+redraw): `bar(BLACK)` только по полосе баров |---|---------------|----------|
(x=0, gate_top..gate_bot) + `draw_gate_back(lmod,...)` + дорисовать статику | 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет |
тайла (0,0), задетую полосой. Ожидаемо срезает бóльшую часть 30-50%. | 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же |
**Пиксель-чувствительно** — полоса баров пересекается со статикой col0, | 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает |
обязательна выверка в MAME по кромкам. | 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается |
| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся |
| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) |
| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; перелистывание экрана штатно (см. BUG-SEAM-DRAW-1 в `bug_closed.md`) |
| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены |
| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177``seq_47``fr=15 x=173`) |
| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 |
| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет |
| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег (доработано во второй волне — п.17) |
**Решение (2026-07-22):** оставляем как есть — стоимость транзиентная, в бюджет ## Вторая волна (2026-08-03, вечер)
помещаемся. Делать, если упрёмся в кадровый бюджет на сценах с воротами.
| # | что проверить | ожидаемо |
|---|---------------|----------|
| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |
Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он
трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке,
осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме
решётки, переходы через швы (старый BUG-SEAM-PINGPONG).
--- ---
## Исправлено # Открытые баги уровня 1
- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука <a id="bug-gate-pass-1"></a>
срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины: ## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ
(а) не был портирован `clip_char()` (seg006:1749) — верхняя обрезка спрайта по
`y_clip[curr_row+1]`, когда тайл над головой стена/пол; сделано
(`pop_clip_char_top` в pop_map.c + `gfx_blit_cols_part` в libbgi, heal чистит
уже обрезанный прямоугольник);
(б) `climb_overlay_tile` выбирал ветку `draw_floor_overlay` (seg008:1E3A) по
СЫРОМУ коду тайла — а нажатая кнопка в `get_tile_to_draw` (seg008:240)
подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в
`draw_other_overlay` и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки
`floor_left_overlay[frame-137]`. Фикс — `tile_code_drawn()` (одна подстановка
на все слои). Проверено покадрово в MAME (кадры 148..138).
- **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО **Статус: наблюдался один раз, воспроизвести повторно НЕ удалось.** Заведён,
2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
`sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];` «не баг», пока нет надёжного сценария.
компилировался в `sub (seam_sig)` (A ← разность) + `ld (seam_sig),a`
сохранял РАЗНОСТЬ `m[9]-seam_sig`, не `m[9]`. `seam_sig` осциллировала
(напр. 25↔231), `m[9]!=seam_sig` истинно каждый кадр → `draw_tile(0,0)`
каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения
(`seam_sig = g;` из чистого `g` ДО `sub`), подтверждён в .asm. Прочёс всех
модулей PoP: других случайных compare-then-store нет.
- **Пики: 2×полный `draw_tile` на кадр анимации** → хирургический редрой, **Что наблюдалось (2026-08-03).** Комната 5: Кид стоял НА тайле решётки (0,9)
2026-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1
статику (база 127, пол, грани); поверх анимируются только два острия и упал на (1,1).
(`SPIKES_FRAM_LEFT` в своей ячейке + `SPIKES_FRAM_RIGHT` в соседней).
Заменили 2×`draw_tile` на 2×`env_b` — пиксель-в-пиксель тот же результат,
в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).
- **Отрисовка нажатой кнопки (0,2)/(0,3)** — ИСПРАВЛЕНО. Причина: `fore_tile` **Что уже измерено и в чём загвоздка.** Сразу после наблюдения повторить не
(pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки». ДЕРЖИТ — то есть штатно.
Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и
`draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс
`pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в
0,3), низ строго yb+64 (не залезать в стену ряда 1).
## НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить) Арифметика оригинала объясняет разницу. `is_obstacle` (seg004) ставит
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
колонки 9 это **x = 205**. При этом «колонка 9» по `get_tile_div_mod_m7`
это `x ∈ [191, 205)`. Пока `curr_col == 9`, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.
- **Голова стоящего Кида поверх падающей на него loose-плиты.** Комната 12: Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него — по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29): эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.
там ровно то же самое. Артефакт оригинального движка (порядок midtable), а
не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев,
которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и
падение вместе с плитой — там плита обязана быть поверх Кида.
## Заметки **Что снять в следующий раз** (без этих чисел вопрос не закрыть):
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо 1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в стоит на её тайле — до шага вправо;
сторону = разворот (как в оригинале safe_step), движение со второго. 2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`). считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
- `START_ROOM=6` — временный тестовый старт; вернуть на есть пока она опустилась достаточно низко относительно РОСТА кадра;
`pop_level_start_room()` после отладки объектов. 3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
--- Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME.
## BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **Medium** **Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — `char_height` в
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.
**Симптом (2026-07-22):** при прыжке вверх (SEQ up, кадры 67..79) руки/голова # Оптимизация отрисовки (записано 2026-07-29)
Kid заходят в полосу кладки у потолка (row -1) и рисуются ПОВЕРХ неё — потолок
должен окклюдить руки (как передняя грань).
**Где:** `pop_bg.c` — полоса потолка (`draw_tile(-1)`) сейчас только фон
(запечена в pop_room_draw), не окклюдер поверх Kid. Нужен ceiling-overlay в
`pop_fore_over_kid` (перерисовать срез полосы над Kid поверх спрайта), по
аналогии с fore-тайлами. Инфраструктура `g_clip_top`/`gfx_blit_part` уже есть.
## BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row -1) не работает — **Medium**
**Симптом (2026-07-22):** loose-плита в ряду 2 верхнего соседа (room5 (2,5) →
потолок room6 над (0,5)) при прыжках Kid на (0,5) НЕ трясётся и не разбивается.
**Где:** верхний сосед не «живой» — `above_fg` берётся из статики уровня,
loose-состояние соседней комнаты не тянем (per-room loose/modif не персистятся,
P0). check_press/knock работают только по текущей комнате. Нужно: (а) пробой
loose сверху через границу комнаты, (б) персистентное loose-состояние соседа +
его отрисовка в полосе. Пока полоса рисует loose статически (кадр покоя).
## BUG-OCCL-1. Тень дальней колонны перекрывает Kid в (0,4)-(0,5) — **Medium**
**Симптом (2026-07-22):** Kid, стоя/прыгая у (0,4)-(0,5), частично перекрыт
тёмной штриховкой — это ТЕНЬ/боковая грань ДАЛЬНЕЙ колонны (сзади по глубине),
которая НЕ должна окклюдить персонажа. Over-occlusion fore-слоя.
**Где:** `pop_bg.c` `pop_fore_over_kid`/`fore_tile` — рисует fore футпринт-тайлов
без учёта глубины (какая грань РЕАЛЬНО перед Kid). Порт seg003 redraw_at_char
окклюдит только передние грани; дальняя колонна (за Kid) не должна. Сверить с
SDLPoP redraw_at_char (какие тайлы/грани идут в fore vs back по y-сортировке).
## BUG-CEIL-3. Анимация ворот стирает потолок над ними — **Medium**
**Симптом (2026-07-22):** при опускании/поднятии решётки шва (анимация openness)
полоса кладки у потолка НАД воротами пропадает (см. потолок над левым швом).
**Где:** `pop_bg.c` `pop_room_redraw_seam_left``pop_button_redraw`/
`pop_spike_redraw` — общий класс): чёрный `bar` по col0 row0 стирает потолочную
полосу (row-1), а redraw рисует только сам гейт, не восстанавливая полосу.
Нужно: при перерисовке шва/тайла у потолка перерисовать и срез потолочной полосы
(draw_tile(-1,col) с g_clip_top) над затронутыми колонками.
---
# BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22**
Статус: 2026-07-22, вечер. НАСТОЯЩИЙ корень найден потиковой трассой ЖИВОГО
SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char +
char_x_left/right; fixes выключены = vanilla). Все прежние гипотезы ниже
оставлены для истории — они НЕ были причиной.
## КОРЕНЬ (подтверждён трассой + исходником)
`set_char_collision` (seg006:0723): `char_x_right = obj_x/2 + 58`, где
`load_frame_to_obj` (seg008:1728) считает `obj_x = 2*char_dx_forward(dx) - 116`
и **добавляет +1** для кадров «чётного пикселя»:
`if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;`
(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит).
Деление `obj_x/2` — C-усечение К НУЛЮ, поэтому при `e = x+dx <= 57`
(obj_x < 0, зона левого шва) поправка +1 даёт `char_x_right = e+1`, а при
e >= 58 формула сокращается к чистому `e`.
Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 —
бит 0x80 не стоит), имеет **char_x_right = 58** и порога leave-left (<=57)
НЕ достигает. Наш движок считал передний край как `Kid.x + dx` без поправки
→ 57 → ложный leave → пинг-понг.
Эталонный цикл SDLPoP (сн normalized по трассе): стойка x=61 → тап вправо →
safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 →
seq47 dx(-4) → **x=57** (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у
всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает;
тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап →
bump → 57 … по кругу. Char.room и drawn_room НЕ меняются вообще.
## Фикс (pop_map.c)
`char_x_forward_edge()`: `e = char_dx_forward(dx); if (((flags ^ (dir<0 ?
0x80 : 0)) & 0x80) == 0 && e <= 57) e++;` — используется в `char_front_coll`
(коллизия/bump/edge_distance) и в `check_leave` (порог ухода). Плюс порт
doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого
выхода нет). `pop_leave_timer` (exit_room_timer) оставлен — он реален в
seg002/seg003.
## Гипотезы, ОТПАВШИЕ после трассы (история диагноза)
## Симптом (подтверждён пользователем + инструментально)
Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке
room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя
состояниями:
- **A**: показывается room6, Kid у левой кромки за решёткой (спрайт виден на 2/3);
- **B**: показывается room8, Kid у его правой кромки.
Эталон SDLPoP (описан пользователем): drawn_room **всегда остаётся room6**,
Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ
переключается. То есть у нас лишний свитч A→B.
## Тестовый контекст
START_ROOM=6. Ворота room8(0,9) закрыты, видны в col0 room6. Kid-структура
kframe: см. pop_kid.h (frame,x,y,direction,curr_col,curr_row,action,...).
Адрес _Kid и статиков — в .sprinter-cc-roomtest/roomtest.map (сдвигается от
сборки к сборке). MAME: memory `mame_mcp_bridge`/`mame_hdd_test_disk`.
Отладочные: J/L — короткий шаг влево/вправо (эмуляция Shift+стрелка).
## Инструментальный диагноз (watchpoint на pop_leave_dir)
В момент лишнего свитча A→B: `pop_leave_dir=1 (LEFT)`, Kid **frame=15 (СТОЯ,
не transient!), Kid.x=57** (до репроекции +140). То есть:
- `char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57`.
- Порог leave-left (взгляд вправо): `char_x_right <= 57` → срабатывает РОВНО на 57.
- Грань ворот (где их держит коллизия) = **61** (`wall_dist_from_left[1]=10 +
coll_tile_left_xpos=51`). Между 57 и 61 — **зазор 4px**: Kid НЕ удержан
воротами (d=6157=4≥0 → check_bumped не бампит), но уже на пороге ухода.
- Kid оседает на 57 из-за recoil отскока: seq_47 = `act(bumped), dx(-4),
frame_50, 51, 52`; SEQ_DX(4) двигает Char.x на −4 суммарно; frame_50.dx=4
компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку
(frame15, dx=0) `char_x_right = Char.x = aligned4 = 57`.
## Что ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)
- Формула char_x: `char_x_right = obj_x/2+58 = Char.x+frame.dx` (seg006
set_char_collision) — совпадает с нашим char_dx_forward.
- Позиция грани ворот: `get_left_wall_xpos = wall_dist_from_left[1](10) +
xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184140)+7 = 61` — совпадает с нашим
(`x_bump[1+5]=44`, +7, +10 = 61).
- Порог leave: SDLPoP leave_room looking-right `char_x_right<=57` — совпадает.
- Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15
(image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.
## Почему точечные фиксы НЕ работают
- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА,
не SDLPoP (в leave_room такого нет); откачено.
- `exit_room_timer=2` (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как
pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасает:
положение Char.x=57 **устойчивое** (Kid стоит), а не transient — после
истечения таймера leave срабатывает.
- По коду SDLPoP raw-рассуждением раз за разом выходит, что leave-left ДОЛЖЕН
сработать и там (Char.x=57), но эталон показывает, что нет. Значит различие
не в этих кусках, а в ДИНАМИКЕ: где именно оседает Char.x у шва И как
вычисляется drawn_room.
## КОРЕНЬ (гипотеза)
Наш движок держит `drawn_room == комната_Kid` ВСЕГДА, а шов подделан:
- коллизия — самодельные снапшоты кромок соседа (g_lcol/g_rcol, 1 колонка) +
`x_bump[tcol=-1]`, а НЕ порт `curr_row_coll_room[]`/`get_all_coll_row`/
`xpos_in_drawn_room` из seg004;
- уход — мгновенный `enter_room(сосед)` при пороге char_x, а НЕ отложенный
drawn_room через `next_room`/`check_the_end`.
Из-за упрощённой коллизии Char.x у шва оседает иначе, чем в SDLPoP (у нас ровно
57 = порог), и/или drawn_room переключается там, где в оригинале нет.
## План S2/S3 (порт coll_room + отложенный drawn_room) — НЕ потребовался для
## этого бага (трасса показала: в эталоне Char.room/drawn_room вообще не
## меняются). Остаётся как задел под стражей/двух персонажей в кадре.
Правило проекта: SDLPoP — источник истины, переводить 1:1, НЕ рассуждать «как
должно быть». Нужно портировать связку целиком:
1. **Раздельные комнаты.** `Char.room` (реальная комната персонажа) ≠
`drawn_room` (отрисованная). У нас уже есть `kid_room` (S1) + рендер-смещение
`pop_kid_set_render_dx(∓140)` (порт xpos_in_drawn_room для СПРАЙТА).
2. **Коллизия по Char.room через coll_room (seg004, S2).** Портировать
`check_collisions` → `get_row_collision_data` (строки curr/below/above) →
`get_left_wall_xpos`/`get_right_wall_xpos` (по `xpos_in_drawn_room(x_bump[
col+FIRST_ONSCREEN_COLUMN])`, ±140 когда колонка принадлежит соседу) →
массивы `curr_row_coll_room[]`/`_flags[]`, `bump_col_left_of_wall`/
`bump_col_right_of_wall` → `check_bumped_look_left/right` → `bumped()`.
Тайлы берутся из РЕАЛЬНОЙ комнаты колонки (get_tile(room,col,row) для любой
комнаты), НЕ из снапшота. Именно это определяет, где Char.x оседает у шва.
3. **Отрисовка по drawn_room, персонаж со сдвигом.** Рисуем drawn_room; Kid
(и будущие стражи) — со смещением ±140, если их комната ≠ drawn_room.
4. **Отложенная смена drawn_room (seg002/seg000, S3).** Уход =
`leave_room` (seg002:0504, ПОРТ ЦЕЛИКОМ, включая up/down/side, guard'ы
climb/standup/turn, doortop-check для right) → `goto_other_room`
(seg002:0486: Char.room=сосед, x±140/y±189). `exit_room` (seg002:03C7):
`exit_room_timer` + `next_room = Char.room`. `check_the_end` (seg000:0FBD):
`if (next_room != 0 && next_room != drawn_room) drawn_room = next_room;
redraw`. drawn_room меняется ТОЛЬКО так, а не по нашему прямому enter_room.
Ключевой момент, который надо переводить БУКВАЛЬНО (а не выводить): порядок
вызовов в play_frame (seg000:863 `play_frame`: … play_kid_frame → exit_room →
check_the_end) + set_char_collision перед leave_room в exit_room.
Карта сегментов: seg002 (leave/goto/exit_room), seg004 (коллизия/coll_room/
bumped), seg006 (set_char_collision/play_seq/frame_table_kid), seg000
(play_frame/check_the_end/draw). Наши файлы: pop_map.c (коллизия/физика/leave),
roomtest.c (enter_room/drawn_room/главный цикл), pop_kid.c (кадры/рендер-сдвиг).
Реализовано на 2026-07-22: S1 (kid_room + render_dx) + pop_leave_timer.
S2/S3 отложены — баг закрыт odd-pixel поправкой (см. КОРЕНЬ выше).
Память: [[pop_seam_room_model]], [[sdlpop_odd_pixel_char_x]].
---
# TODO: оптимизация отрисовки (записано 2026-07-29)
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
@@ -365,6 +283,7 @@ S2/S3 отложены — баг закрыт odd-pixel поправкой (с
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
безусловно» корректен, но дорог. безусловно» корректен, но дорог.
<a id="t-1"></a>
## T-1. Пики: перерисовывать по причине, а не безусловно ## T-1. Пики: перерисовывать по причине, а не безусловно
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для **Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
@@ -388,6 +307,9 @@ S2/S3 отложены — баг закрыт odd-pixel поправкой (с
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
прямоугольников (см. память `manual_hints_over_auto_detect`). прямоугольников (см. память `manual_hints_over_auto_detect`).
**Приоритет:** отдаётся почти бесплатно ПОСЛЕ T-2, отдельно не окупается.
<a id="t-2"></a>
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит ## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
**Сейчас:** `kid_heal``kid_draw``pop_fore_over_kid` идут каждый кадр, **Сейчас:** `kid_heal``kid_draw``pop_fore_over_kid` идут каждый кадр,
@@ -402,9 +324,11 @@ S2/S3 отложены — баг закрыт odd-pixel поправкой (с
неизменного кадра — иначе одна из двух страниц останется со старым неизменного кадра — иначе одна из двух страниц останется со старым
содержимым. Условие «обе страницы уже получили это состояние». содержимым. Условие «обе страницы уже получили это состояние».
**Связь с T-1:** после T-1 idle-skip не требует ничего дополнительного для **Связь с T-1:** после T-2 пики отпадают сами — раз Кида не перерисовываем,
пик: раз Кида не перерисовываем — heal'а нет стирать пики нечем редрой heal'а нет, стирать пики нечем, редрой не нужен.
не нужен, он отпадает сам.
**Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно
там, где тапают Shift+стрелку (см. остаток KBD-1 в [`TASKS.md`](TASKS.md)).
--- ---
@@ -416,24 +340,12 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
единицы (`||` `||||` = 24). Убирается закомментированием одной строки. единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
Комнаты **13, 18, 24 недостижимы** в обычной игре — это свойство данных
уровня, разбор в [`bug_closed.md`](bug_closed.md) («НЕ БАГИ»). Приоритет
багов в них низкий.
**Связность уровня 1 (проверено по `res2001.bin`, links @1952).** Обход графа Таблица заполнена на 5 строк из 24 — доделать в рамках L1-TRIAGE
от стартовой комнаты 1 показывает: комнаты **13, 18, 24 НЕДОСТИЖИМЫ** в ([`TASKS.md`](TASKS.md)).
обычной игре. У всех трёх ссылки наружу есть, а на них не ссылается никто
(24: `L→9`, но у 9 `R=0`; 13 и 18 связаны только друг с другом). Признак
«комнату выкинули из компоновки, связи не почистили» — несимметричные ссылки
ровно у этих трёх, у остальных 21 симметрия полная:
```
13 L→22, у 22 R=16 | 18 L→15, у 15 R=12 | 24 L→9, у 9 R=0
13 R→16, у 16 L=22 | 18 R→12, у 12 L=15
| 18 D→19, у 19 U=12
```
Следствие для обхода: в 13/18/24 возможен «мусор в шве» — наш рендер кромки
читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
считает. Это свойство данных, а не наш баг. Приоритет багов в этих трёх
комнатах — низкий (в игре их не видно).
| комната | статус | что не так | | комната | статус | что не так |
|---------|--------|------------| |---------|--------|------------|
@@ -448,7 +360,7 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) | | 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
| 10 | | | | 10 | | |
| 11 | | | | 11 | | |
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» | | 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» в `bug_closed.md` |
| 13 | недостижима в игре | | | 13 | недостижима в игре | |
| 14 | | | | 14 | | |
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` | | 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
@@ -461,3 +373,41 @@ Kid ставится на первый пол, `pop_trob_reset()` возвращ
| 22 | | | | 22 | | |
| 23 | | | | 23 | | |
| 24 | недостижима в игре | | | 24 | недостижима в игре | |
---
## Заметки (отладка)
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
сторону = разворот (как в оригинале safe_step), движение со второго.
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
**S** — выдать меч.
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
---
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
## Сырые наблюдения (прогон 2026-08-03) → корень
Формулировки с прогона, как записаны. Закрытые корни — в
[`bug_closed.md`](bug_closed.md).
| # | наблюдение (кратко) | корень | статус |
|---|---------------------|--------|--------|
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт |
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт |
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт |
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт |
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт |
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт |
| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) |
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) |
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт |
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт |
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт |
+80
View File
@@ -0,0 +1,80 @@
Активные баги на уровне 1 -
- После того как кувшин со здоровьем выпит и Кид попадает в комнату во второй раз
то иногда кувшин мы не показываем (это правильно) но показываем анимацию пузырьков над
кувшином (чего показывать больше не должны - нет кувшина, нет пузырьков),
а в комнате 14 пузырек вообще восстанавливается полностью после того как Кид
выйдет из комнаты и вернется в нее обратно (есть ощущение что даже в одной комнате
поведение пузырька - будет ли он восстановлен или нет зависит от чего-то непонятного.
(есть еще ощущение что первый выпитый кувшин не восстанавливается а остальные
восстанавливаются)
- Меч - один раз подобранный он больше не должен появляться в своей (15-ой) комнате
иногда он появляется снова, иногда он больше не появляется но на дальней стене начинают
моргать контуры кирпичной кладки (странно что она не впечатана в heal уровень потому
что она должна быть отрисована всегда). причем мерцает по странному - не как double-buff
а как буд-то вместо искры на мече - изредка на несколько (один логический?) кадров.
- комната 12 - опять проблема с телепортацией через решетку - решетка в 0,9 - Кид стоит
за шаг (пол-шага) от решетки - бежит и пробегает через закрытую решетку.
- после respawn - остаются разбитыми плиты что были разбиты, выпитыми пузырьки и прочее -
это надо сравнить с оригиналом (SDLPoP) - но главное другое - если до respawn какая-то
плита не была разбита то теперь если ее разбить, выйти из комнаты и вернуться - то
плита восстановится (еще интереснее вариант когда плита-потолок в комнате 12 разбивается,
Кид через проем поднимается в верхнюю комнату и поднявшись плита в новой комнате опять
закрывает проем.
- проблема с плитой из комнаты 12 в верхнюю и пузырек в комнате 22 - они возникают даже
без respawn - когда Кид заходит в комнату 22 из комнаты 16, выпивает пузырек, выходит опять
в комнату 16 и опять возвращается в комнату 22 - пузырек возникает снова. Когда Кид разбивает
плиту из комнаты 12 в комнату 16 то после подъема в комнату 16 плита опять возникает.
аналогично комната 17 - если войти в нее из комнаты 23, разбить плиты 1,5 и 1,6, опуститься на
2,3 и выпить пузырек. вернуться в комнату 23 и опять в 17 - то и плиты вернутся и пузырек.
но могут плита и пузырек не вернуться - такое ощущение что если это первая разбитая плита
(Кида мы перенесли в комнату 12 через cheats) - то разбитая плита остается разбитой.
- комната 6 - Кид бежит по уровню ноль справа налево (начало бега 0,9), делает длинный
прыжок между 0,6 и 0,7 - он пролетает во внутрь стены и респавнится - скриншоты в bugscreens
- иногда залипают все-таки клавиши (пока замечено на кнопке вверх - отлипает только после
следующего нажатия). надо проверить - есть ли возможность не только отслеживать события
нажатие/отжатие но и текущий статус нажатых кнопок. Может вообще отказаться от прямой работы
с клавиатурой а работать с нею через BIOS ?
- иногда таки залипает и shift =((((
- комната 5 - кнопка на 0,4 не открывает решетку/ворота на 0,5 - это двойная кнопка - она
должна сразу открывать ворота на 0,5 и на 0,9 (возможно у нас она работает только на 0,9)
возможно что логически решетка на 0,5 открывается но анимации ее открытия нету =(
- комната 5 - Кид падает с кнопки 0,6 на 2,7 - получает урон, присяд в 2,8 - когда встает
он проваливается еще ниже в комнату 6 на между 2,7-2,8.
- если стражник убивает Кида то Кид респавнится на месте смерти и стражник тут же его убивает
еще раз и так по циклу
--------
- проверить как отрисовывается Кид вместе с мертвым стражником - в оригинале стражник поверх
Кида (Кид проходит за телом стража), у нас Кид поверх (перед) стражем;
- сейчас у нас шифт не залипает но теперь он теряет статус нажатости - то есть при нажатом шифте
второе-треть-четвертое нажатие стрелки отрабатывает не как осторожный шаг а переводит Кида на бег
по идее с новым алгоритмом должно было бы корректно отрабатывать нажатие шифта ? теперь же залипаний
нету но есть фантомный отжим ? такое поведение работы с клавиатурой хуже чем залипание шифта (потому
что игрок надеется что шифт нажат и будет короткий шаг а Кид начинает бежать и может провалиться в
ловушку/яму или набежать на пики и погибнуть). то есть надо как-то более точно проверять статус нажатости
шифта.
то есть - когда нажат шифт и нажимаются стрелки то вариант редких пропусков стрелок предпочтительнее чем
вариант когда мы перестаем понимать что шифт нажат.
- после respawn - должны ли оживать стражники ? сейчас они остаются мертвые.
- небольшой баг отображения (такой баг у нас уже был и ты его фиксил) - комната 12 - две падающие плиты
рядом (0,1 и 0,2) - после падения осколки только от одной на 1,2 а от 0,1 на 1,1 осколков нету.
при том что в комнате 7 две плиты 0,5 и 0,6 падая на 2,5 и 2,6 дают двое осколков.
возможная причина - когда Кид пробегает в комнате 12 по падающим плитам то он убегает сразу в комнату 15
и плита 0,2 успевает упасть и поменять статус пола 1,2 на пол с осколками а плита 0,1 еще остается в падении
когда Кид уже уходит в комнату 15 и потому статус пола на 1,1 на 'пол с осколками' измениться не успевает
--------
Подумай вот над чем -
интересует возможность покрытия уже написаного кода автотестами (тесткейсами).
автотесты внутри MAME я думаю будут достаточно затруднительными, но поскольку это у нас plain-C
то можно наверное делать автотесты и на host OS.
возможно это нам поможет не создавать ситуации когда нам каждый раз приходится фиксить баги которые
мы уже фиксили (по крайней мере постараемся избегать регресса при реализации новых фич)
я понимаю что это скорее всего будет достаточно сложно совместить с тем что финальный продукт идет
для Sprinter. но подумать над этим надо.
Пока ничего не предпринимай, только подумай что и как можно сделать.
Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

+480 -4
View File
@@ -10,17 +10,493 @@
* пока исполняется банк, резидентной страницы в адресном пространстве * пока исполняется банк, резидентной страницы в адресном пространстве
* нет. Поэтому фон/окклюзию за стража рисует главный цикл. * нет. Поэтому фон/окклюзию за стража рисует главный цикл.
* *
* Сейчас — каркас: тик есть, ИИ и боёвка (порт seg005 control_guard*, * Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000
* seg004 fight/collision) добавляются сюда. * play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой:
* ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а
* дальше работает общий pop_control().
*/ */
#include "pop_guard.h" #include "pop_guard.h"
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "pop_geom.h"
#include "pop_trob.h"
#include "pop_state.h" /* pop_immortal — чит бессмертия */
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7
#define TILE_LOOSE 11
#define TILE_DOORTOP 12
#define TILE_CHOMPER 18
#define TILE_WALL 20
/* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и
* «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его
* через границу банка дороже, чем повторить. */
static uint8_t tile_is_floor(uint8_t t)
{
switch (t) {
case 0: case 9: case 12: case 20:
case 26: case 27: case 28: case 29:
return 0;
default:
return 1;
}
}
/* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid,
* seg003:761): xpos 7 делится на ширину тайла с округлением вниз. */
static int8_t tile_col; /* колонка последнего tile_at_kid */
static uint8_t tile_at_kid(int16_t xpos)
{
/* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos 7 58)/14),
* СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]).
* Оригинал берёт её из tile_div_tbl, у нас — делением с округлением
* вниз, как в pop_map. */
int16_t x = xpos - 7 - SCREENSPACE_X;
int16_t col = x / TILE_SIZEX;
if (x % TILE_SIZEX < 0) --col;
tile_col = (int8_t)col;
return pop_tile_at(tile_col, Kid.curr_row);
}
/* check_can_guard_see_kid (seg003:688) — луч видимости по ряду.
* Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт /
* 2 видит и пойдёт. */
void pop_check_can_guard_see_kid(void) __banked
{
uint8_t kid_frame = Kid.frame;
int16_t left_pos, right_pos;
uint8_t tile;
if (!(Guard.charid != 0 &&
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
Kid.alive < 0 && Guard.alive < 0 &&
Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) {
can_guard_see_kid = 0;
return;
}
can_guard_see_kid = 2;
left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
if (left_pos > right_pos) {
int16_t t = left_pos; left_pos = right_pos; right_pos = t;
}
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
if (tile_at_kid(left_pos) == TILE_CHOMPER)
left_pos += TILE_SIZEX;
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
if (tile_at_kid(right_pos) == TILE_GATE)
right_pos -= TILE_SIZEX;
while (left_pos <= right_pos) {
tile = tile_at_kid(left_pos);
/* Сквозь это не видно вовсе. */
if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) {
can_guard_see_kid = 0;
return;
}
/* Видно, но страж не пойдёт: провалится / попадёт под чомпер /
* упрётся в неоткрытые ворота / шагнёт в дыру. */
if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) {
can_guard_see_kid = 1;
} else if (tile == TILE_GATE &&
pop_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
can_guard_see_kid = 1; /* ворота подняты не до конца */
}
left_pos += TILE_SIZEX;
}
}
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void)
{
control_shift = control_y = control_x = control_shift2 = 0;
control_down = control_up = control_backward = control_forward = 0;
}
static void move_1_forward(void)
{
control_x = POP_CONTROL_HELD_FORWARD;
control_forward = POP_CONTROL_HELD;
}
static void move_2_backward(void)
{
control_backward = POP_CONTROL_HELD;
control_x = POP_CONTROL_HELD_BACKWARD;
}
static void move_3_up(void)
{
control_y = POP_CONTROL_HELD_UP;
control_up = POP_CONTROL_HELD;
}
static void move_4_down(void)
{
control_down = POP_CONTROL_HELD;
control_y = POP_CONTROL_HELD_DOWN;
}
static void move_6_shift(void)
{
control_shift = POP_CONTROL_HELD;
control_shift2 = POP_CONTROL_HELD;
}
static void move_down_back(void)
{
control_down = POP_CONTROL_HELD;
move_2_backward();
}
static void move_down_forw(void)
{
control_down = POP_CONTROL_HELD;
move_1_forward();
}
/* autocontrol_guard_inactive (seg002:710): страж с убранным мечом.
* Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида
* (is_guard_notice), от которого страж оборачивается. */
static void autocontrol_guard_inactive(void)
{
int16_t distance;
if (Kid.alive >= 0) return;
distance = pop_char_opp_dist();
/* В оригинале условие записано как (word)distance < (word)-8 —
* беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1.
* То есть: Кид не «вплотную за спиной». */
if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) {
if (is_guard_notice) {
is_guard_notice = 0;
if (distance < 0) {
if (distance < -4) move_4_down(); /* обернуться на шум */
return;
}
} else if (distance < 0) {
return;
}
}
if (can_guard_see_kid) {
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
move_down_forw();
}
}
/* ---- Активный страж (меч обнажён), seg002:737.. -------------------- *
* Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок
* — prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены
* по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */
static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48};
static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150};
static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255};
static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175};
static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100};
/* Бросок «сработало ли»: в оригинале prob > prandom(255). */
static uint8_t roll(uint8_t prob)
{
return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255));
}
/* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда,
* остальные — только пока Кид не замахнулся. */
static void guard_advance(void)
{
if (guard_skill == 0 || kid_sword_strike == 0) {
if (roll(ADVPROB[guard_skill])) move_1_forward();
}
}
/* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что
* пробил блок (justblocked) — своя, «импровизационная» вероятность. */
static void guard_block(void)
{
uint8_t of = Opp.frame;
if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 ||
of == FRAME_162_BLOCK_TO_STRIKE) {
if (roll(justblocked ? IMPBLOCKPROB[guard_skill]
: BLOCKPROB[guard_skill]))
move_3_up();
}
}
/* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151);
* из своего парирования — по отдельной вероятности «добить». */
static void guard_strike(void)
{
uint8_t of = Opp.frame, cf = Char.frame;
if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return;
if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) {
if (roll(RESTRIKEPROB[guard_skill])) move_6_shift();
} else {
if (roll(STRIKEPROB[guard_skill])) move_6_shift();
}
}
/* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и
* далеко — наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и
* только отдышавшись бьём. */
static void autocontrol_guard_kid_armed(int16_t distance)
{
if (distance < 10 || distance >= 29) {
guard_advance();
} else {
guard_block();
if (guard_refrac == 0) {
if (distance < 12 || distance >= 29) guard_advance();
else guard_strike();
}
}
}
/* autocontrol_guard_kid_in_sight (seg002:0A93). */
static void autocontrol_guard_kid_in_sight(int16_t distance)
{
if (Opp.sword == SWORD_2_DRAWN) {
autocontrol_guard_kid_armed(distance);
} else if (guard_refrac == 0) {
/* Кид безоружен: подойти и рубить. */
if (distance < 29) move_6_shift();
else move_1_forward();
}
}
/* Тайл перед стражем / через один (порт get_tile_infrontof_char и
* get_tile_infrontof2_char, seg004) — но для Char, а не для Kid. */
static uint8_t tile_infront(int8_t step)
{
int8_t d = (int8_t)(Char.direction < 0 ? -step : step);
return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row);
}
/* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя
* бы через тайл; иначе пятимся (не шагать в пропасть). */
static void autocontrol_guard_kid_far(void)
{
if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2)))
move_1_forward();
else
move_2_backward();
}
/* autocontrol_guard_active (seg002:737). */
static void autocontrol_guard_active(void)
{
uint8_t cf = Char.frame;
uint8_t of;
int16_t distance;
/* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */
if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1)
return;
if (can_guard_see_kid == 0) {
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
if (Char.charid != CHARID_4_SKELETON) move_down_back();
return;
}
/* can_guard_see_kid == 2 */
of = Opp.frame;
distance = pop_char_opp_dist();
/* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */
if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5)
return;
if (distance < 35) {
if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) {
/* Слишком близко: если смотрим в одну сторону — развернуться. */
if (Char.direction == Opp.direction) move_2_backward();
else move_1_forward();
} else {
autocontrol_guard_kid_in_sight(distance);
}
return;
}
if (guard_refrac != 0) return;
if (Char.direction != Opp.direction) {
/* Кид разбежался — встречаем ударом, а не шагом. */
if (of >= 7 && of < 15) { /* бег */
if (distance < 40) move_6_shift();
return;
} else if (of >= 34 && of < 44) { /* разбег в прыжок */
if (distance < 50) move_6_shift();
return;
}
}
autocontrol_guard_kid_far();
}
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
static void autocontrol_opponent(void)
{
move_0_nothing();
/* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */
if (justblocked) --justblocked;
if (kid_sword_strike) --kid_sword_strike;
if (guard_refrac) --guard_refrac;
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
else autocontrol_guard_active();
}
/* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */
#define ACTION_99_HURT 99
#define ACTION_1_RUN 1
#define FRAME_154_POKING 154
#define SEQ_69_ATTACK_WAS_PARRIED 69
#define SEQ_74_HIT_BY_SWORD 74
#define SEQ_85_STABBED_TO_DEATH 85
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
{ 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0};
/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
* Возврат 1 — HP кончились. Чит бессмертия ловим здесь: анимацию удара
* Кид получает, а HP не теряет. */
static uint8_t take_hp(uint8_t count)
{
if (Char.charid == CHARID_0_KID) {
if (pop_immortal) return 0;
if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
hitp_delta = (int8_t)-(int8_t)count;
} else {
if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
guardhp_delta = (int8_t)-(int8_t)count;
}
return 0;
}
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
* «получил удар».
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
* идёт ровно нашей веткой. */
static void hurt_by_sword(void)
{
if (Char.alive >= 0) return;
/* Чит бессмертия перекрывает и «безоружного закалывают насмерть»:
* без этого Кид гибнет с одного удара мимо стойки, и чит бесполезен
* ровно там, где нужен. Анимацию удара оставляем. */
if (Char.charid == CHARID_0_KID && pop_immortal) {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq();
return;
}
if (Char.sword != SWORD_2_DRAWN) {
take_hp(100);
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
} else if (Char.charid != CHARID_4_SKELETON && take_hp(1)) {
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
} else {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
}
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq(); /* TODO: звук 12/13 */
}
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
* Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */
static void check_hurting(void)
{
int16_t distance;
uint8_t cf, of, min_range;
if (Char.sword != SWORD_2_DRAWN) return;
if (Char.curr_row != Opp.curr_row) return;
cf = Char.frame;
if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return;
distance = pop_char_opp_dist();
of = Opp.frame;
if (distance < 0 || distance >= 29 ||
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
/* Соперник НЕ парирует. */
if (cf == FRAME_154_POKING) {
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
distance = pop_char_opp_dist();
if (distance >= (int16_t)min_range && distance < 29)
Opp.action = ACTION_99_HURT;
}
} else {
/* Парировано. */
Opp.frame = FRAME_161_PARRY;
if (Char.charid != CHARID_0_KID) justblocked = 4;
pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED);
play_seq();
}
}
/* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */
void pop_check_sword_hurting(void) __banked
{
uint8_t kf = Kid.frame;
if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */
pop_loadshad_and_opp();
check_hurting();
pop_saveshad_and_opp();
pop_loadkid_and_opp();
check_hurting();
pop_savekid_and_opp();
}
/* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если
* ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же:
* его action сбрасывается ДО проверки). */
void pop_check_sword_hurt(void) __banked
{
if (Guard.action == ACTION_99_HURT) {
if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN;
pop_loadshad();
hurt_by_sword();
pop_saveshad();
guard_refrac = (int8_t)REFRACTIMER[guard_skill];
} else if (Kid.action == ACTION_99_HURT) {
pop_loadkid();
hurt_by_sword();
pop_savekid();
}
}
/* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»:
* пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до
* нуля, он переводится в «умирает» (alive = 0), и дальше тот же control()
* ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто
* доигрывает последовательность. */
static void play_guard(void)
{
if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
* которого нет; для уровня 1 других эффектов там нет. */
}
autocontrol_opponent();
pop_control();
}
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) —
* отдельным шагом. */
void pop_guard_tick(void) __banked void pop_guard_tick(void) __banked
{ {
if (Guard.charid == 0) return; /* стража в комнате нет */ if (Guard.charid == 0) return; /* стража в комнате нет */
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
/* Симметрично Киду (play_guard_frame, seg000:1248): cur_frame один на
* всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */
pop_load_fram_det_col();
play_guard();
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров — /* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
* выбирается по Char.charid внутри load_frame (seg006:0293). */ * выбирается по Char.charid внутри load_frame (seg006:0293). */
pop_guard_play(); play_seq();
/* TODO: порт control_guard/fight (seg005/seg004) — состояние в Guard. */ pop_saveshad();
} }
+27
View File
@@ -42,4 +42,31 @@ static const uint16_t kid_seq_off[KID_NSEQ] = {
0x21FC,0x2240,0x2257, 0x21FC,0x2240,0x2257,
}; };
/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.
* Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.
* Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),
* x — «вперёд» по направлению персонажа. */
#define KID_NSWORD 51
#define KID_SWORD_ID_INIT { \
0xFF,0x00,0x05,0x01,0x02,0x06,0x07,0x03,0x04,0x1E,0x08,0x09, \
0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x13,0x11, \
0x11,0x07,0x01,0x1C,0x08,0x04,0x04,0x0A,0x0D,0x0D,0x0D,0x14, \
0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0xFF,0xFF,0x1D,0x08,0x1F, \
0x20,0x21,0x08, \
}
#define KID_SWORD_DX_INIT { \
0,0,-9,7,17,7,0,17,16,12,13,7, \
10,10,22,28,13,0,0,21,14,21,22,7, \
15,0,7,14,7,6,9,11,24,19,21,7, \
14,14,14,28,28,21,14,14,21,0,8,14, \
14,7,8, \
}
#define KID_SWORD_DY_INIT { \
0,-9,-29,-25,-26,-14,-5,-16,-19,-9,-34,-25, \
-16,-11,-21,-23,-35,-38,-29,-19,-23,-22,-23,-13, \
-18,-8,-27,-28,-27,-23,-21,-18,-23,-23,-23,-32, \
-32,-31,-29,-28,-28,-25,-22,-25,-25,-16,-37,-24, \
-24,-14,-37, \
}
#endif #endif
+132 -24
View File
@@ -14,10 +14,12 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include "pop_bg.h" #include "pop_bg.h"
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_state.h" #include "pop_state.h"
#include "pop_geom.h" #include "pop_geom.h"
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ #include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */ #include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */
#include "pop_guard.h" /* SWORD_2_DRAWN — расширение футпринта под клинок */
/* ---- Атласы (прямая адресация) ------------------------------------- */ /* ---- Атласы (прямая адресация) ------------------------------------- */
static atlas_t env[5]; static atlas_t env[5];
@@ -135,18 +137,12 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
gfx_w0_unmap(); gfx_w0_unmap();
} }
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ /* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose.
* Выбор ядра (noclip / клипающее) — общий с Кидом и стражем, pop_heal_fast
* в pop_bg.h. */
static void heal_off(int x, int y, int w, int h) static void heal_off(int x, int y, int w, int h)
{ {
int sy = y + POP_YOFF; pop_heal_fast(x, y + POP_YOFF, w, h);
/* Без клипа — линейным ядром (~2.9×, как gfx_blit_noclip): общий
* gfx_heal платит за клип/16-бит/split, а не за пиксели (замер:
* 11 658 тактов на heal 22×22). */
if (x >= 0 && sy >= 0 && w > 0 && h > 0 && w < 256 && h < 256 &&
x + w <= 320 && sy + h <= 256)
gfx_heal_noclip(x, sy, (uint8_t)w, (uint8_t)h);
else
gfx_heal(x, sy, w, h);
} }
/* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а /* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а
@@ -354,7 +350,7 @@ static uint8_t wall_modifier(int row, int col)
} }
/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */ /* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
static unsigned long rnd_seed; static pop_rnd_t rnd_seed;
#define prandom(maxv) pop_prandom(&rnd_seed, (maxv)) #define prandom(maxv) pop_prandom(&rnd_seed, (maxv))
@@ -417,7 +413,7 @@ static void wall_rnd(int row, int col)
* соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */ * соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1; int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
uint8_t a, b, bg; uint8_t a, b, bg;
unsigned long saved; pop_rnd_t saved;
if (idx >= 0 && idx < WP_N && wp_ok[idx]) { if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
a = wp_a[idx]; b = wp_b[idx]; a = wp_a[idx]; b = wp_b[idx];
wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7); wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7);
@@ -430,7 +426,7 @@ static void wall_rnd(int row, int col)
saved = rnd_seed; saved = rnd_seed;
/* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху /* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
* (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */ * (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
rnd_seed = (unsigned long)(g_room + TBL_LINE[row < 0 ? 2 : row] + col); pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
prandom(1); /* сброс */ prandom(1); /* сброс */
wr_mid_div = (uint8_t)prandom(1); wr_mid_div = (uint8_t)prandom(1);
wr_mid_off = (uint8_t)prandom(4); wr_mid_off = (uint8_t)prandom(4);
@@ -590,6 +586,11 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
int ybottom = dmy - 13; int ybottom = dmy - 13;
int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */ int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */
int y = ybottom - (int)modif; int y = ybottom - (int)modif;
/* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО
* тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из
* draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней
* (tbl_level_type) — на уровне 1 не применяется. */
pop_leveldoor_right = xh * 8 + 48;
env_b(99, x, ybottom); env_b(99, x, ybottom);
if (modif) if (modif)
env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */ env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */
@@ -598,6 +599,9 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy)
if (y > ylo) ylo += 4; if (y > ylo) ylo += 4;
else break; else break;
} }
/* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое
* значение (низ поднятой створки), а не стартовое. */
pop_leveldoor_ybottom = ylo;
env_b(34, x, dmy - 64); env_b(34, x, dmy - 64);
} }
@@ -629,7 +633,11 @@ static void draw_tile(int row, int col)
* room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */
if (row + 1 > 2) { if (row + 1 > 2) {
int lc = col - 1; int lc = col - 1;
if (lc < 0) rbl_code = 20; /* кромка уровня = стена */ /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате
* (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда;
* «стена» там стоит, только если такой комнаты нет — как в
* load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */
if (lc < 0) rbl_code = g_below ? (uint8_t)(g_below[10] & 0x1F) : 20;
else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1; else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1;
} else rbl_code = tile_code(row + 1, col - 1); } else rbl_code = tile_code(row + 1, col - 1);
@@ -839,8 +847,22 @@ void pop_ceil_bake_empty(int col) __banked
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
g_clip_top = POP_YOFF; g_clip_top = POP_YOFF;
bake_rest = 1; /* фон = покой (см. bake_rest) */ bake_rest = 1; /* фон = покой (см. bake_rest) */
draw_tile(-1, col); /* тайл уже EMPTY → колодец наверх */ /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только
if (col + 1 < 10) draw_tile(-1, col + 1); * два тайла ряда 1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей
* нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут:
* • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка;
* • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px.
* Раньше перерисовывались только (−1,col) и (1,col+1), и в полосе
* оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при
* полосе 44..52), который heal возвращал каждый кадр. Признак, что дело
* именно в запечке: после выхода из комнаты и возврата бара нет. */
{
int c0 = (col > 0) ? col - 1 : 0;
int c1 = (col + 1 < 9) ? col + 1 : 9;
int c;
for (c = c0; c <= c1; c++) draw_tile(-1, c);
for (c = c0; c <= c1; c++) draw_tile(0, c);
}
bake_rest = 0; bake_rest = 0;
g_clip_top = 0; g_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
@@ -919,6 +941,35 @@ void pop_leveldoor_redraw(int row, int col) __banked
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
} }
/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob,
* seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота;
* решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому
* wipe и redraw идут по ней. Логика ровно как у левого шва
* (pop_room_redraw_seam_left), только колонка произвольная.
*
* Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при
* ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно
* зона решётки: от gate_top_y (dby62 = 63*row+3) до низа (63*row+64);
* выше начинается ряд −1, ниже — стена ряда ниже.
*
* Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их
* не видно — оригинал такой trob тоже пропускает
* (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа
* работает pop_room_redraw_seam_left. */
void pop_gate_redraw(int row, int col) __banked
{
int x, yt;
if (col + 1 > 9 || row < 0 || row > 2) return;
x = COL_XH[col + 1] * 8;
yt = POP_YOFF + 63 * row;
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, yt + 3, x + 31, yt + 64);
gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(row, col + 1);
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт
* redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
* тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
@@ -983,6 +1034,7 @@ typedef struct {
int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */
int8_t speed; int8_t speed;
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
} mob_t; } mob_t;
static mob_t mobs[MOB_MAX]; static mob_t mobs[MOB_MAX];
@@ -1011,6 +1063,7 @@ void pop_loose_mob_spawn(int row, int col) __banked
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
m->row = (int8_t)row; m->row = (int8_t)row;
m->col = (int8_t)col; m->col = (int8_t)col;
m->room = g_room; /* curmob.room (add_mob, seg007:1010) */
m->speed = 0; m->speed = 0;
m->active = 1; m->active = 1;
m->clean = 0; m->clean = 0;
@@ -1044,14 +1097,42 @@ void pop_loose_mob_reset(void) __banked
} }
} }
/* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только
* в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо
* от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид
* не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12
* плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только
* heal-историю: новая комната перерисована целиком, старых пикселей нет. */
void pop_loose_mob_room_changed(void) __banked
{
uint8_t i;
for (i = 0; i < MOB_MAX; i++) {
mobs[i].defer = 0;
mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE;
/* дочистка хвоста относилась к покинутой комнате — она уже не нужна */
if (!mobs[i].active) mobs[i].clean = 0;
}
}
/* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки
* и соседей), в чужой — прямо из данных уровня. */
static uint8_t mob_tile_at(mob_t *m)
{
if (m->room == g_room) return (uint8_t)(tile_code(m->row, m->col) & 0x1F);
if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */
return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F);
}
static void mob_tick_one(mob_t *m, uint8_t pg) static void mob_tick_one(mob_t *m, uint8_t pg)
{ {
uint8_t here;
if (!m->active && !m->clean) return; if (!m->active && !m->clean) return;
here = (uint8_t)(m->room == g_room);
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической * плита-потолок падает через весь ряд 0, поэтому чистим по фактической
* прошлой позиции. */ * прошлой позиции. */
if (m->prev_y[pg] != MOB_Y_NONE) { if (here && m->prev_y[pg] != MOB_Y_NONE) {
heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
m->prev_y[pg] = MOB_Y_NONE; m->prev_y[pg] = MOB_Y_NONE;
} }
@@ -1064,9 +1145,17 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
* пол — приземление (loose_land: debris + do_knock). */ * пол — приземление (loose_land: debris + do_knock). */
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
uint8_t t = (uint8_t)(tile_code(m->row, m->col) & 0x1F); uint8_t t = mob_tile_at(m);
if (t == 0 || t == 11) { if (t == 0 || t == 11) {
m->row++; /* mob_down_a_row */ m->row++; /* mob_down_a_row */
} else if (!here) {
/* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная
* пара pop_loose_landed/pop_debris_at обслуживает только текущую
* комнату — щебень кладём прямо в данные уровня (loose_land пишет
* в curr_room_tiles комнаты curmob.room, а не отрисованной). */
pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E);
m->active = 0; m->clean = 0;
return;
} else if (pop_loose_landed) { } else if (pop_loose_landed) {
/* сигнал прошлого приземления ещё не разобран (один за кадр) — /* сигнал прошлого приземления ещё не разобран (один за кадр) —
* подержим кусок на границе и приземлим следующим кадром */ * подержим кусок на границе и приземлим следующим кадром */
@@ -1079,9 +1168,10 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
} }
} }
if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */ if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
m->active = 0; m->clean = 2; m->active = 0; m->clean = (uint8_t)(here ? 2 : 0);
return; return;
} }
if (!here) return; /* чужая комната — считаем, но не рисуем */
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
* тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
* ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто
@@ -1162,8 +1252,13 @@ void pop_loose_mob_draw_over(void) __banked
uint8_t pop_loose_mob_pos(int *col, int *y) __banked uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{ {
uint8_t i; uint8_t i;
/* check_loose_fall_on_kid (seg007:1192) начинается с Char.room ==
* curmob.room: кусок, падающий в покинутой комнате, Киду по голове не
* бьёт. */
for (i = 0; i < MOB_MAX; i++) for (i = 0; i < MOB_MAX; i++)
if (mobs[i].active) { *col = mobs[i].col; *y = mobs[i].y; return 1; } if (mobs[i].active && mobs[i].room == g_room) {
*col = mobs[i].col; *y = mobs[i].y; return 1;
}
return 0; return 0;
} }
@@ -1190,6 +1285,11 @@ void pop_room_clip_borders(void) __banked
{ {
if (!border_dirty) return; if (!border_dirty) return;
border_dirty--; border_dirty--;
/* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320
* не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска
* по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах
* падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там
* тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */
gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */ gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */
gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */ gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */
} }
@@ -1466,7 +1566,7 @@ static void other_overlay_tile(int row, int col)
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw; static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;
static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h, static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir) int8_t dir, uint8_t sword)
{ {
int wh = (int)((w + 1) >> 1); /* char_width_half */ int wh = (int)((w + 1) >> 1); /* char_width_half */
int xl = obj_x / 2 + 58; /* char_x_left (58-база) */ int xl = obj_x / 2 + 58; /* char_x_left (58-база) */
@@ -1477,6 +1577,14 @@ static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */ cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */
cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9; cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9;
cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9; cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9;
/* redraw_at_char (seg003:0430): «If char is holding sword, it makes
* redraw-area bigger» — КЛИНОК торчит на колонку дальше габарита кадра,
* и без этого расширения передние грани в той колонке не
* перерисовываются: меч остаётся поверх столба, а его след — на фоне. */
if (sword >= SWORD_2_DRAWN) {
if (dir >= 0) { if (++cR > 9) cR = 9; }
else { if (--cL < 0) cL = 0; }
}
/* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y), /* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y),
* причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе * причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе
* «ряд 3») трактуется как ряд 3, НЕ ряд 0. Верхний ряд считается от * «ряд 3») трактуется как ряд 3, НЕ ряд 0. Верхний ряд считается от
@@ -1508,7 +1616,7 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
{ {
int8_t cL, cR, cLraw, rT, rB, rTraw, r, c; int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */ fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir); char_footprint(obj_x, obj_y, w, h, dir, Kid.sword);
cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw; cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw; rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
/* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
@@ -1592,11 +1700,11 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
* (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий * (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий
* порт redraw_at_char2, а не догадки сейчас. */ * порт redraw_at_char2, а не догадки сейчас. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir) int8_t dir, uint8_t sword) __banked
{ {
int8_t r, c; int8_t r, c;
fclip_on = 1; /* окно = прямоугольник спрайта */ fclip_on = 1; /* окно = прямоугольник спрайта */
char_footprint(obj_x, obj_y, w, h, dir); char_footprint(obj_x, obj_y, w, h, dir, sword);
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
for (r = fp_rT; r <= fp_rB; r++) for (r = fp_rT; r <= fp_rB; r++)
for (c = fp_cL; c <= fp_cR; c++) for (c = fp_cL; c <= fp_cR; c++)
+25 -5
View File
@@ -25,6 +25,11 @@
/* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */ /* Экранный y нижней границы поля (ниже — статус-полоса + борт → клип). */
#define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H) #define POP_CLIP_BOTTOM (POP_YOFF + POP_PLAYFIELD_H)
/* Выбор ядра libbgi (pop_onscreen_cols / pop_heal_fast) переехал в
* `_pop_draw.h` — его включают только три рисующих модуля. Из общего
* заголовка SDCC размножал мёртвые копии этих inline'ов по всем TU: см.
* шапку `_pop_draw.h`. */
/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load /* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load
* трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ * трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ
* после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */ * после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */
@@ -41,9 +46,11 @@ void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg, const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked; const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked;
/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright, /* Задать ряд 0 комнаты СНИЗУ — для draw_tile_topright, когда тайл нижнего
* когда тайл нижнего ряда стал empty (после падения loose): «стена вниз» * ряда стал empty (после падения loose): «стена вниз» рисуется по коду
* рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */ * room_B(0, col-1). ОДИННАДЦАТЬ байт: [0..9] — колонки комнаты снизу,
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
void pop_room_set_below(const uint8_t *below_row0_fg) __banked; void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
@@ -69,8 +76,11 @@ void pop_fore_set_clip(int x, int y, int w, int h) __banked;
/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка, /* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка,
* без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата * без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата
* (до ×8/7), как у kid_fp_obj_x(). */ * (до ×8/7), как у kid_fp_obj_x(). */
/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому
* вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в
* кадр, других вызывающих нет — цена одного трамплина здесь не видна. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
int8_t dir); int8_t dir, uint8_t sword) __banked;
/* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ /* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ
* (draw_other_overlay/floor_overlay/полоса потолка): он выходит за * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за
@@ -120,12 +130,22 @@ void pop_button_redraw(int row, int col) __banked; /* = pop_floor_bake (кно
* запечкой — створка анимируется (pop_trob animate_leveldoor). */ * запечкой — створка анимируется (pop_trob animate_leveldoor). */
void pop_leveldoor_redraw(int row, int col) __banked; void pop_leveldoor_redraw(int row, int col) __banked;
/* Ворота ВНУТРИ отрисованной комнаты: перерисовать по живому modif.
* (row,col) — координаты САМИХ ворот; бары рисует ячейка правого соседа
* (draw_tile, ветка lcode==4), туда и идёт wipe+redraw. Порт draw_trob
* (seg007:01E6), которым заканчивается animate_door. */
void pop_gate_redraw(int row, int col) __banked;
/* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла /* Падающий кусок loose (mob, seg007 move_loose): спавн при падении тайла
* (row,col) и покадровое продвижение+отрисовка на back-странице. tick * (row,col) и покадровое продвижение+отрисовка на back-странице. tick
* зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */ * зовётся каждый кадр (внутри pop_loose_tick); клип низа делает clip. */
void pop_loose_mob_spawn(int row, int col) __banked; void pop_loose_mob_spawn(int row, int col) __banked;
void pop_loose_mob_tick(void) __banked; void pop_loose_mob_tick(void) __banked;
void pop_loose_mob_reset(void) __banked; /* остановить кусок (смена комнаты) */ void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
/* Смена комнаты: куски продолжают падать в СВОИХ комнатах (do_mobs крутит
* mobs[] независимо от drawn_room), сбрасывается только heal-история. */
void pop_loose_mob_room_changed(void) __banked;
/* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs,
* seg008:1561). Звать после kid_draw, перед fore-слоем. */ * seg008:1561). Звать после kid_draw, перед fore-слоем. */
+10 -1
View File
@@ -6,7 +6,8 @@
* просто глобальный флаг: main ставит его при старте (на время разработки — * просто глобальный флаг: main ставит его при старте (на время разработки —
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл. * ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
* *
* Реализовано: K — убить стража (seg000:786). Остальные читы оригинала * Реализовано: K — убить стража (seg000:786), I/S/Shift+L (наши отладочные),
* [ / ] — подгонка Кида по X (seg000:1828). Остальные читы оригинала
* (время, перо, воскрешение, переход по комнатам) пока не портированы. * (время, перо, воскрешение, переход по комнатам) пока не портированы.
*/ */
#ifndef POP_CHEAT_H #ifndef POP_CHEAT_H
@@ -17,5 +18,13 @@
extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */ extern uint8_t pop_cheats; /* 0 = выключены, иначе включены */
#define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */ #define KBD_CHEAT_KILL 0x42 /* K (PS/2 set 2) — kill guard */
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
/* Последний уровень игры (tbl_* в pop_level рассчитаны на 0..15). */
#define POP_LAST_LEVEL 15
#endif #endif
+282 -23
View File
@@ -17,8 +17,10 @@
*/ */
#include <stdint.h> #include <stdint.h>
#include <kbd_raw.h> #include <kbd_raw.h>
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_map.h" #include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
/* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в /* Отладочные клавиши осторожного шага (эмуляция Shift+стрелка) для тестов в
* MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2 * MAME, где Shift трудно подать: J = шаг влево, L = шаг вправо. PS/2 set2
@@ -51,6 +53,7 @@
#define SEQ_49_STAND_UP_FROM_CROUCH 49 #define SEQ_49_STAND_UP_FROM_CROUCH 49
#define SEQ_50_CROUCH 50 #define SEQ_50_CROUCH 50
#define SEQ_79_CROUCH_HOP 79 #define SEQ_79_CROUCH_HOP 79
#define SEQ_78_DRINK 78 /* выпить зелье */
#define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */ #define SEQ_91_PICKUP_SWORD 91 /* поднять меч (pickupsword) */
#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */ #define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */
#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */ #define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */
@@ -58,6 +61,7 @@
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */ #define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */ #define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */
#define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */ #define SEQ_25_HANG_AGAINST_WALL 25 /* вис у стены (Shift в висе) */
#define SEQ_GO_UP_LEVELDOOR 70 /* seq_70_go_up_on_level_door */
#define FRAME_7_RUN 7 #define FRAME_7_RUN 7
#define FRAME_11_RUN 11 #define FRAME_11_RUN 11
@@ -70,15 +74,15 @@
#define FRAME_109_CROUCH 109 #define FRAME_109_CROUCH 109
/* ---- Состояние управления (seg006 globals) ------------------------ * /* ---- Состояние управления (seg006 globals) ------------------------ *
* control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно: * Сами control_* ЖИВУТ В pop_state.c, а не здесь. Модуль уехал в БАНК 5,
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный * а шину пишут с трёх сторон (этот банк, guards.c из банка 1) и читают из
* автомат held/released/ignore (авто-повтор); ctrl1_* — их память между * банка 3 (pop_map) — это ровно тот случай, для которого pop_state и
* кадрами (rest/save). */ * заведён. Сегодня оно работало бы и отсюда: банки собираются БЕЗ
static int8_t control_x, control_y, control_shift; * --bank-data, поэтому их писучие данные остаются в общем _DATA. Но это
/* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт * флаг сборки, а не свойство кода: включат --bank-data ради экономии
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же * W1/W2 — и шина молча уедет в страницу банка.
* control(), что и у игрока. Поэтому шина общая. */ *
int8_t control_forward, control_backward, control_up, control_down, control_shift2; * ctrl1_* остаются здесь: их трогает только этот модуль. */
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2; static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ * /* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
@@ -125,20 +129,30 @@ static uint8_t get_item_action(void)
seqtbl_offset_char(SEQ_91_PICKUP_SWORD); seqtbl_offset_char(SEQ_91_PICKUP_SWORD);
return 1; return 1;
} }
if (a == 3) { /* зелье — выпить (get_item) */
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_78_DRINK);
return 1;
}
return 0; return 0;
} }
/* run_jump (seg005:0AA8). Выравнивание по кромке пола живёт в pop_map
* (там тайловые запросы) — pop_run_jump_align; здесь только диспетчерская
* часть. Отказ выравнивателя = прыжка в этом кадре НЕТ, и control_up
* гасить нельзя: Кид бежит дальше с зажатой «вверх» и попробует снова на
* следующем кадре. Ровно так игрок и «ловит» фазу перед провалом. */
static void run_jump(void) static void run_jump(void)
{ {
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
* полировка K3; K2b просто запускает run-jump. */
if (Char.frame >= FRAME_7_RUN) { if (Char.frame >= FRAME_7_RUN) {
if (!pop_run_jump_align()) return;
control_up = release_arrows(); control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP); seqtbl_offset_char(SEQ_4_RUN_JUMP);
} }
} }
static void safe_step(void); /* fwd */ static void safe_step(void); /* fwd */
static void draw_sword(void); /* fwd: нужен в control_standing */
static void forward_pressed(void) static void forward_pressed(void)
{ {
@@ -179,8 +193,15 @@ static void jump_up(void)
static void up_pressed(void) static void up_pressed(void)
{ {
/* верь уровня/телепорт — нет в тесте.) ↑+forward = прыжок с места; /* Дверь уровня рядом и открыта → войти в неё: постановку в проём
* иначе вертикальный прыжок вверх. */ * (go_up_leveldoor) делает pop_leveldoor_enter, здесь остаётся только
* последовательность подъёма по лестнице. Телепортов у нас нет — это
* модификация SDLPoP, а не оригинал.
* Иначе: ↑+forward = прыжок с места, иначе вертикальный прыжок вверх. */
if (pop_leveldoor_enter()) {
seqtbl_offset_char(SEQ_GO_UP_LEVELDOOR);
return;
}
if (control_x == CONTROL_HELD_FORWARD) if (control_x == CONTROL_HELD_FORWARD)
standing_jump(); standing_jump();
else else
@@ -232,7 +253,32 @@ static void control_standing(void)
if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD && if (control_shift2 == CONTROL_HELD && control_shift == CONTROL_HELD &&
get_item_action()) get_item_action())
return; return;
/* Kid charid_0, без меча: остаётся нижняя часть control_standing. */ /* Ветку «charid != kid && вниз+вперёд -> draw_sword» не портируем: у нас
* соперник до control_standing не доходит, его перехватывает
* control_guard_inactive в самом диспетчере (порядок как в seg005). */
/* seg005:352 — Кид с мечом ВИДИТ активного стража: достаёт клинок сам.
* Без этого игрок стоит с мечом в ножнах, пока его рубят. */
if (pop_have_sword) {
if (offguard != 0 && control_shift >= CONTROL_RELEASED) {
/* Меч убран намеренно (down в бою) — не доставать, пока не
* отпустят Shift; в оригинале это переход на loc_6213. */
} else if (can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d >= -10 && d < 90) {
holding_sword = 1;
/* (word)d < (word)-6 — беззнаковое: истинно для ВСЕГО, кроме
* −6..−1. То есть «страж вплотную за спиной» -> отступить,
* иначе достать меч. */
if (d >= -6 && d < 0) { back_pressed(); return; }
draw_sword();
return;
}
} else {
offguard = 0;
}
}
if (control_shift == CONTROL_HELD) { if (control_shift == CONTROL_HELD) {
if (control_backward == CONTROL_HELD) back_pressed(); if (control_backward == CONTROL_HELD) back_pressed();
else if (control_up == CONTROL_HELD) up_pressed(); else if (control_up == CONTROL_HELD) up_pressed();
@@ -324,10 +370,199 @@ static void control_jumpup(void)
standing_jump(); standing_jump();
} }
/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */ /* draw_sword (seg005:945): достать меч. Кид переходит в seq_55 (подъём
void pop_control(void) * клинка со звуком), соперник — сразу в стойку en garde (seq_90). Стрелки
* гасятся, чтобы «вперёд», которым меч достали, не сработало второй раз
* уже как выпад. */
static void draw_sword(void)
{
uint8_t seq_id = SEQ_55_DRAW_SWORD;
control_forward = control_shift2 = release_arrows();
if (Char.charid == CHARID_0_KID) {
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
} else if (Char.charid != CHARID_1_SHADOW) {
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
}
Char.sword = SWORD_2_DRAWN;
seqtbl_offset_char(seq_id);
}
/* control_guard_inactive (seg006:2123): неактивный страж реагирует ТОЛЬКО в
* кадре 166 (стойка) и только на «вниз»: вниз+вперёд — достать меч,
* вниз без «вперёд» — развернуться (stand flipped). */
static void control_guard_inactive(void)
{
if (Char.frame == FRAME_166_STAND_INACTIVE && control_down == CONTROL_HELD) {
if (control_forward == CONTROL_HELD) {
draw_sword();
} else {
control_down = CONTROL_IGNORE;
seqtbl_offset_char(SEQ_80_STAND_FLIPPED);
}
}
}
/* ---- Боёвка (seg005:964..) ---------------------------------------- *
* Всё это ветки ОБЩЕГО диспетчера: у стража тот же control(), просто
* control_* ему выставляет ИИ (autocontrol_*, guards.c), а не клавиатура. */
/* back_with_sword (seg005): шаг назад — только из стоек. */
static void back_with_sword(void)
{ {
uint8_t f = Char.frame; uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_backward = CONTROL_IGNORE; /* без авто-повтора */
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD);
}
}
/* forward_with_sword (seg005): шаг вперёд; у стража своя последовательность. */
static void forward_with_sword(void)
{
uint8_t f = Char.frame;
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_forward = CONTROL_IGNORE;
seqtbl_offset_char(Char.charid != CHARID_0_KID ? SEQ_56_GUARD_FORWARD
: SEQ_86_FORWARD_WITH_SWORD);
}
}
/* sword_strike (seg005): выпад из стойки/шага либо добивание после парирования. */
static void sword_strike(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_157_WALK_WITH_SWORD || f == FRAME_158_STAND_WITH_SWORD ||
f == FRAME_170_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD ||
f == FRAME_165_WALK_WITH_SWORD) {
seq_id = (Char.charid == CHARID_0_KID) ? SEQ_75_STRIKE : SEQ_58_GUARD_STRIKE;
} else if (f == FRAME_150_PARRY || f == FRAME_161_PARRY) {
seq_id = SEQ_66_STRIKE_AFTER_PARRY;
} else {
return;
}
control_shift2 = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
}
/* parry (seg005): блок. Условие сильно разное у Кида и у соперника —
* соперник блокирует ТОЛЬКО на кадре 152 (второй кадр удара), а Кид ещё и
* по 153 (и тогда последовательность прокручивается сразу). */
static void parry(void)
{
uint8_t cf = Char.frame, of = Opp.frame;
uint8_t seq_id = SEQ_62_PARRY;
uint8_t do_play_seq = 0;
if (cf == FRAME_158_STAND_WITH_SWORD || cf == FRAME_170_STAND_WITH_SWORD ||
cf == FRAME_171_STAND_WITH_SWORD || cf == FRAME_168_BACK ||
cf == FRAME_165_WALK_WITH_SWORD) {
if (pop_char_opp_dist() >= 32 && Char.charid != CHARID_0_KID) {
back_with_sword();
return;
} else if (Char.charid == CHARID_0_KID) {
if (of == FRAME_168_BACK) return;
if (of != FRAME_151_STRIKE_1 && of != FRAME_152_STRIKE_2 &&
of != FRAME_162_BLOCK_TO_STRIKE) {
if (of == FRAME_153_STRIKE_3) do_play_seq = 1;
}
} else {
if (of != FRAME_152_STRIKE_2) return;
}
} else {
if (cf != FRAME_167_BLOCKED) return;
seq_id = SEQ_61_PARRY_AFTER_STRIKE;
}
control_up = CONTROL_IGNORE;
seqtbl_offset_char(seq_id);
if (do_play_seq) play_seq();
}
/* swordfight (seg005:0CDB) — разбор ввода в бою. Порядок веток важен:
* удар старше «убрать меч», тот старше блока, блок старше шагов. */
static void swordfight(void)
{
uint8_t f = Char.frame;
uint8_t seq_id;
if (f == FRAME_161_PARRY && control_shift2 >= CONTROL_RELEASED) {
seqtbl_offset_char(SEQ_57_BACK_WITH_SWORD); /* отскок после парирования */
return;
} else if (control_shift2 == CONTROL_HELD) {
if (Char.charid == CHARID_0_KID) kid_sword_strike = 15;
sword_strike();
if (control_shift2 == CONTROL_IGNORE) return;
}
if (control_down == CONTROL_HELD) {
if (f == FRAME_158_STAND_WITH_SWORD || f == FRAME_170_STAND_WITH_SWORD ||
f == FRAME_171_STAND_WITH_SWORD) {
control_down = CONTROL_IGNORE;
Char.sword = SWORD_0_SHEATHED;
if (Char.charid == CHARID_0_KID) {
offguard = 1; /* убрал намеренно — сразу не доставать */
guard_refrac = 9; /* и страж на это время придержан */
holding_sword = 0;
seq_id = SEQ_93_PUT_SWORD_AWAY_FAST;
} else if (Char.charid == CHARID_1_SHADOW) {
seq_id = SEQ_92_PUT_SWORD_AWAY;
} else {
seq_id = SEQ_87_GUARD_BECOME_INACT; /* Кид ушёл из виду */
}
seqtbl_offset_char(seq_id);
}
} else if (control_up == CONTROL_HELD) {
parry();
} else if (control_forward == CONTROL_HELD) {
forward_with_sword();
} else if (control_backward == CONTROL_HELD) {
back_with_sword();
}
}
/* control_with_sword (seg005:964): пока соперник виден и близко — бой;
* иначе страж убирает меч, а Кид просто стоит. */
static void control_with_sword(void)
{
if (Char.action >= 2) return; /* actions_2_hang_climb и выше */
if (pop_tile_at(Char.curr_col, Char.curr_row) == 11 /* loose */ ||
can_guard_see_kid >= 2) {
int16_t d = pop_char_opp_dist();
if (d < 90) {
if (d >= 0) { swordfight(); return; }
/* Соперник за спиной: далеко — развернуться, вплотную — драться. */
if (d < -4) { seqtbl_offset_char(SEQ_60_TURN_WITH_SWORD); return; }
swordfight();
return;
}
}
/* Не видит / далеко. */
if (Char.charid < CHARID_2_GUARD) {
if (Char.frame == FRAME_171_STAND_WITH_SWORD) {
Char.sword = SWORD_0_SHEATHED;
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY); /* страж убит */
}
} else {
swordfight();
}
}
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
void pop_control(void) __banked
{
uint8_t f = Char.frame;
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
* Из любой стойки он валится в seq_71_dying; остальное управление в
* этом состоянии не обрабатывается. Через это же место проходит чит K
* (pop_guard_kill ставит Guard.alive = 0). */
if (Char.alive >= 0) {
if (f == FRAME_15_STAND || f == FRAME_166_STAND_INACTIVE ||
f == FRAME_158_STAND_WITH_SWORD || f == FRAME_171_STAND_WITH_SWORD)
seqtbl_offset_char(SEQ_71_DYING);
return;
}
/* seg005 control(): в отскоке (bumped) и в свободном падении управление /* seg005 control(): в отскоке (bumped) и в свободном падении управление
* НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок * НЕ обрабатывается вовсе — только гасятся стрелки. Без этого игрок
* прерывает присед после удара (medland/плита по голове) нажатием ↑, и * прерывает присед после удара (medland/плита по голове) нажатием ↑, и
@@ -338,7 +573,13 @@ void pop_control(void)
release_arrows(); release_arrows();
return; return;
} }
if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) /* Порядок веток — как в seg005: меч старше charid, charid старше кадров.
* Пока Kid безоружен, обе ветки — про стража. */
if (Char.sword == SWORD_2_DRAWN)
control_with_sword();
else if (Char.charid >= CHARID_2_GUARD)
control_guard_inactive();
else if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53))
control_standing(); control_standing();
else if (f == FRAME_48_TURN) else if (f == FRAME_48_TURN)
control_turning(); control_turning();
@@ -434,7 +675,7 @@ static void save_ctrl_1(void)
} }
/* ---- Публичный API ------------------------------------------------ */ /* ---- Публичный API ------------------------------------------------ */
int pop_ctrl_init(void) int pop_ctrl_init(void) __banked
{ {
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED; control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
control_shift2 = CONTROL_RELEASED; control_shift2 = CONTROL_RELEASED;
@@ -442,27 +683,45 @@ int pop_ctrl_init(void)
return kbd_raw_open(); return kbd_raw_open();
} }
void pop_ctrl_close(void) void pop_ctrl_close(void) __banked
{ {
kbd_raw_close(); kbd_raw_close();
} }
int8_t pop_ctrl_shift_held(void) int8_t pop_ctrl_shift_held(void) __banked
{ {
return (int8_t)(control_shift == CONTROL_HELD); return (int8_t)(control_shift == CONTROL_HELD);
} }
void pop_ctrl_tick(void) void pop_ctrl_tick(void) __banked
{ {
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */ kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
/* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж
* мёртв. Без этой ветки смерть от меча стража не доводилась до конца:
* hurt_by_sword играл seq_85, hitp_curr падал в 0, но Kid.alive
* оставался 1 и pop_kid_dead не взводился — Кид досматривал сиквенс,
* возвращался в стойку с нулём HP, и страж убивал его снова по кругу
* (BUG-DEATH-1). pop_kid_dead — наш сигнал главному циклу на респавн
* (в оригинале это счётчик Char.alive в play_kid, seg006:0BEE). */
if (Kid.alive < 0 && hitp_curr == 0) {
Kid.alive = 0;
pop_kid_dead = 1;
}
rest_ctrl_1(); rest_ctrl_1();
read_input(); read_input();
read_user_control(); read_user_control();
/* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ /* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ
* персонажем (как в оригинале), поэтому ввод игрока применяется к * персонажем (как в оригинале), поэтому ввод игрока применяется к
* Киду только внутри loadkid/savekid. Кадр не перезагружаем — * Киду только внутри loadkid/savekid.
* play_seq в kid_tick сделает это следующим шагом. */ *
* load_fram_det_col ОБЯЗАТЕЛЕН здесь — порт play_kid_frame
* (seg000:1209), где он стоит ровно между loadkid_and_opp() и
* play_kid(). cur_frame один на всех персонажей, и в кадре последним
* его перезаписывает страж; без перезагрузки диспетчер считает
* геометрию Кида (dx_weight -> determine_col -> distance_to_edge_weight
* -> get_edge_distance) по кадру СТРАЖА. Замерено: BUG-CTRL-FRAME-1. */
pop_loadkid(); pop_loadkid();
pop_load_fram_det_col();
user_control(); /* -> pop_control(): может сменить Char.curr_seq */ user_control(); /* -> pop_control(): может сменить Char.curr_seq */
pop_savekid_state(); pop_savekid_state();
save_ctrl_1(); save_ctrl_1();
+23 -5
View File
@@ -18,19 +18,26 @@
#include <stdint.h> #include <stdint.h>
/* МОДУЛЬ ЖИВЁТ В БАНКЕ 5 (--bank 5=pop_ctrl.c), поэтому всё, что зовут
* снаружи, помечено __banked. Зовут с трёх сторон: главный цикл
* (резидент), guards.c (банк 1) и pop_map.c (банк 3) банкбанк
* работает, трамплин сохраняет страницу окна на стеке. Диспетчер
* дёргается один раз за кадр на персонажа, так что цена перехода
* незаметна. Сама шина control_* лежит в pop_state.c (см. ниже). */
/* Открыть raw-канал клавиатуры и обнулить память авто-повтора. /* Открыть raw-канал клавиатуры и обнулить память авто-повтора.
* 0 OK, -1 ошибка (EBUSY и пр., см. kbd_raw_open). */ * 0 OK, -1 ошибка (EBUSY и пр., см. kbd_raw_open). */
int pop_ctrl_init(void); int pop_ctrl_init(void) __banked;
void pop_ctrl_close(void); void pop_ctrl_close(void) __banked;
/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать /* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать
* диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД * диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД
* kid_tick() (play_seq продолжит с новой последовательности). */ * kid_tick() (play_seq продолжит с новой последовательности). */
void pop_ctrl_tick(void); void pop_ctrl_tick(void) __banked;
/* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map): /* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map):
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */ * зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
int8_t pop_ctrl_shift_held(void); int8_t pop_ctrl_shift_held(void) __banked;
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ---- /* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего * В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
@@ -39,12 +46,23 @@ int8_t pop_ctrl_shift_held(void);
#define POP_CONTROL_RELEASED 0 #define POP_CONTROL_RELEASED 0
#define POP_CONTROL_IGNORE 1 #define POP_CONTROL_IGNORE 1
#define POP_CONTROL_HELD (-1) #define POP_CONTROL_HELD (-1)
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
* заполняет как есть, без flip_control_x. */
/* Определены в pop_state.c (резидент W1/W2), а не в pop_ctrl.c: шину
* трогают три банка сразу, а данные банкового модуля живут в _DATA лишь
* пока сборка идёт без --bank-data. */
extern int8_t control_x, control_y, control_shift;
#define POP_CONTROL_HELD_FORWARD (-1)
#define POP_CONTROL_HELD_BACKWARD 1
#define POP_CONTROL_HELD_UP (-1)
#define POP_CONTROL_HELD_DOWN 1
extern int8_t control_forward, control_backward, control_up, control_down, extern int8_t control_forward, control_backward, control_up, control_down,
control_shift2; control_shift2;
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через /* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж из * pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж из
* своего тика напрямую. */ * своего тика напрямую. */
void pop_control(void); void pop_control(void) __banked;
#endif #endif
+29
View File
@@ -0,0 +1,29 @@
/*
* pop_draw.c выбор ядра libbgi для heal: линейное «noclip», если
* прямоугольник целиком на экране, иначе общее клипающее. Объявление и
* мотивация `_pop_draw.h`.
*
* ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ, а не inline в заголовке. Замер
* CLIP-1 (2026-08-01), SDCC 4.5: `static inline` тут обходится втридорога
* компилятор И встраивает тело в каждый вызов (181 Б на место), И оставляет
* отдельную мёртвую копию в каждом TU. У стража это раздуло
* `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ
* ради экономии ~50 тактов на вызов при том что BANK2 занят на 86 %.
*
* Резидент W1 (_CODE), а не банк: сюда зовут из ДВУХ разных банков
* (pop_bg.c BANK2, pop_gdraw.c BANK4) и из резидента (pop_kid.c). W1
* замаплено всегда, поэтому из банка это обычный `call` без трамплина
* тот же приём, что у pop_sword_draw.
*/
#include <stdint.h>
#include <gfx.h>
#include "_pop_draw.h"
void pop_heal_fast(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) return;
if (pop_onscreen_cols(x, y, (uint16_t)w, (uint16_t)h))
gfx_heal_noclip(x, y, (uint8_t)w, (uint8_t)h);
else
gfx_heal(x, y, w, h);
}
+139 -8
View File
@@ -15,7 +15,10 @@
#include <gfx.h> #include <gfx.h>
#include <sprite.h> #include <sprite.h>
#include "pop_guard.h" #include "pop_guard.h"
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ #include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */
#include "kid_data.h" #include "kid_data.h"
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */ /* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
@@ -29,6 +32,21 @@ static int gx_l[2], gy_l[2];
static uint16_t gw_l[2], gh_l[2]; static uint16_t gw_l[2], gh_l[2];
static uint8_t gvalid[2]; static uint8_t gvalid[2];
/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение
* почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит
* ровно по площади. Объединение считаем только для окна fore-клипа. */
static int sx_l[2], sy_l[2];
static uint16_t sw_l[2], sh_l[2];
static uint8_t svalid[2];
/* Ширина спрайта последнего кадра стража — нужна set_char_collision
* (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам:
* это свойство кадра, а не буфера. Отставание на кадр допустимо так же
* работает kid_fp_width() у Кида. */
static uint16_t g_last_w;
uint16_t pop_guard_fp_width(void) __banked { return g_last_w; }
int pop_guard_load(void) __banked int pop_guard_load(void) __banked
{ {
static const char *const names[GUARD_PAGES] = { static const char *const names[GUARD_PAGES] = {
@@ -50,19 +68,28 @@ void pop_guard_free(void) __banked
g_ok = 0; g_ok = 0;
} }
/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же
* причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в
* большинстве кадров noclip-ядром. Два прямоугольника, а не объединение
* (см. комментарий к sx_l выше). */
void pop_guard_heal(void) __banked void pop_guard_heal(void) __banked
{ {
uint8_t p = gfx_get_draw_page() & 1; uint8_t p = gfx_get_draw_page() & 1;
if (gvalid[p]) gfx_heal(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]); if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]);
gvalid[p] = 0; if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]);
gvalid[p] = svalid[p] = 0;
} }
void pop_guard_draw(void) __banked void pop_guard_draw(void) __banked
{ {
int obj_x, obj_y, top, fwd, fp_x, bx0 = 0, top0 = 0; int obj_x, obj_y, top, fwd, fp_x;
uint8_t page, idx, p, flip; uint8_t page, idx, p, flip;
const uint8_t *img; const uint8_t *img;
uint16_t w, h; uint16_t w, h;
/* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам
* футпринт (pop_fore_over_char) считается по спрайту стража, поэтому
* w/h ниже НЕ трогаем. */
int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0;
if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return; if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return;
/* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */ /* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */
@@ -85,22 +112,126 @@ void pop_guard_draw(void) __banked
flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */ flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */
if (flip) bx -= (int)w; if (flip) bx -= (int)w;
top = obj_y - (int)h + 1; top = obj_y - (int)h + 1;
bx0 = bx; top0 = top;
p = gfx_get_draw_page() & 1; p = gfx_get_draw_page() & 1;
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
else
gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0); gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1; gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = h; gvalid[p] = 1;
g_last_w = w; /* для char_width_half, см. выше */
clip_x = bx; clip_y = top; clip_w = w; clip_h = h;
}
/* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable,
* seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник
* heal; окно fore-клипа объединение (без него клинок полез бы поверх
* переднего столба). */
{
int sx, sy; uint16_t sw, sh;
if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y,
&sx, &sy, &sw, &sh)) {
p = gfx_get_draw_page() & 1;
sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh;
svalid[p] = 1;
if (!clip_w) {
clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh;
} else {
int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h;
if (sx < clip_x) clip_x = sx;
if (sy < clip_y) clip_y = sy;
if (sx + (int)sw > x1) x1 = sx + (int)sw;
if (sy + (int)sh > y1) y1 = sy + (int)sh;
clip_w = (uint16_t)(x1 - clip_x);
clip_h = (uint16_t)(y1 - clip_y);
}
}
} }
gfx_w0_unmap(); gfx_w0_unmap();
/* Окно fore-слоя = прямоугольник стража (см. pop_bg.c, fclip_on): /* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c,
* без него проход возвращал ЦЕЛЫЕ тайлы 432 К тактов на кадр. */ * fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы 432 К тактов. */
pop_fore_set_clip(bx0, top0 + POP_YOFF, (int)w, (int)h); pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h);
/* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись /* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись
* midtable, а foretable рисуется после всех midtable-записей, поэтому * midtable, а foretable рисуется после всех midtable-записей, поэтому
* колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid * колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid
* (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний * (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний
* столб. */ * столб. */
if (w && h) if (w && h)
pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction); pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword);
}
/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- *
* Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри
* тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража
* справа налево от x = 314. Пустые деления у Кида свой спрайт (chtab_2
* id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы
* просто не печатаем полоса и так на чёрном фоне.
*
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
* pop_kid_img_blit; спрайт стража image 0 его chtab (наша страница 0). */
#define HP_Y 194
#define HP_STEP 7
#define HP_KID_FULL 216
#define HP_KID_EMPTY 217
#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */
/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ
* страницах дабл-буфера (иначе на второй останется старое значение и его
* будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг.
*
* Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80
* собирает неверно SUB затирает A, и в s уезжает разность (memory
* sdcc_z80_cmp_store_a_bug). */
static uint8_t hp_todo; /* сколько страниц ещё обновить */
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
* комнату (фон перерисован целиком и стёр её) и при старте. */
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
void pop_hp_draw(void) __banked
{
uint8_t i, n;
uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0);
uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0);
{ /* Изменилось хоть одно число — обновить обе страницы. */
uint8_t k = hitp_curr, km = hitp_max;
uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev ||
gd != hp_gd_prev || gm != hp_gdmax_prev);
hp_kid_prev = k; hp_kidmax_prev = km;
hp_gd_prev = gd; hp_gdmax_prev = gm;
if (changed) hp_todo = 2;
}
if (hp_todo == 0) return;
hp_todo--;
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);
for (i = hitp_curr; i < n; i++)
pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF);
for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++)
pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF);
/* Страж: только полные деления, справа налево. Скелет полосы не имеет. */
if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) {
const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0);
uint16_t w, h;
gfx_w0_map(gp[0].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h) {
/* Атлас стража тоже column-major — блит колоночный. */
n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd);
for (i = 0; i < n; i++)
gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP,
HP_Y + POP_YOFF, img, 0, 0, 0);
}
gfx_w0_unmap();
}
} }
+217 -4
View File
@@ -15,9 +15,222 @@ int8_t pop_y_to_row(int16_t y)
return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1); return (int8_t)((y + 60) / TILE_SIZEY % 4 - 1);
} }
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv) /* Уложить значение в диапазон 0..maxv. Вызовы оригинала — prandom(1),
* prandom(255), prandom(0xFF): делитель степень двойки, то есть маска, а не
* деление. __moduint на Z80 стоит заметно дороже проверки n & (n-1). */
static uint16_t pop_rnd_fit(uint16_t v, uint16_t maxv)
{ {
unsigned long s = *seed * 214013UL + 2531011UL; uint16_t n = (uint16_t)(maxv + 1);
*seed = s; if ((uint16_t)(n & (uint16_t)(n - 1)) == 0)
return (uint16_t)((uint16_t)(s >> 16) % (uint16_t)(maxv + 1)); return (uint16_t)(v & (uint16_t)(n - 1));
return (uint16_t)(v % n);
} }
#if POP_PRANDOM_EXACT
/* ---- LCG оригинала: s = s*214013 + 2531011 (seg009:321) ------------ *
*
* ЕДИНСТВЕННОЕ место в порте на ассемблере, и вот почему. На Z80 нет
* умножения, а SDCC для 32-битного `s * 214013` зовёт __mullong 8 430
* тактов на вызов (замер в MAME). Написанное на C через 16-битные
* половины это ~3 500. Здесь ~1 000, при полностью той же
* последовательности, что в SDLPoP. Альтернатива «взять генератор
* попроще» (8-битный RND Apple II, xorshift) экономит меньше и ломает
* совместимость с эталоном, по которому сверяем картинку.
*
* Приём: 214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3 схема Горнера по
* РАЗРЕЖЕННОЙ записи константы. Вместо 12 сложений (по числу единиц в
* 0x343FD) 17 удвоений, ТРИ сложения и одно вычитание. Величина 3*s,
* нужная в конце, попадается по дороге на втором шаге её и сохраняем.
*
* ABI __sdcccall(1): arg1 (указатель на сид) -> HL, возврат -> DE. IX не
* используется, сохранять нечего. Клоббер AF/BC/DE/HL. Аккумулятор
* DE:HL (DE старшее слово); s.lo живёт в BC, s.hi и 3*s в статиках
* (регистров на всё не хватает).
*/
uint16_t pop_pr_ptr; /* адрес сида */
uint16_t pop_pr_sh; /* старшее слово исходного сида */
uint16_t pop_pr_3s_lo, pop_pr_3s_hi; /* 3*s, нужное в конце */
static uint16_t pop_lcg_step(pop_rnd_t *seed) __naked
{
(void)seed;
__asm
ld (_pop_pr_ptr), hl
ld c, (hl)
inc hl
ld b, (hl) ; BC = s.lo
inc hl
ld a, (hl)
ld (_pop_pr_sh), a
inc hl
ld a, (hl)
ld (_pop_pr_sh + 1), a ; (pop_pr_sh) = s.hi
ld h, b
ld l, c
ld de, (_pop_pr_sh) ; акк = s
;; акк = акк*2 + s => 3s
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh) ; LD A,(nn) флаги НЕ трогает
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
ld (_pop_pr_3s_lo), hl
ld (_pop_pr_3s_hi), de
;; акк = акк*4 + s => 13s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк = акк*16 + s => 209s
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, bc
ex de, hl
ld a, (_pop_pr_sh)
adc a, l
ld l, a
ld a, (_pop_pr_sh + 1)
adc a, h
ld h, a
ex de, hl
;; акк *= 1024 => 214016s (10 удвоений)
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
add hl, hl
rl e
rl d
;; акк -= 3s => 214013s
ld bc, (_pop_pr_3s_lo)
or a ; CY = 0
sbc hl, bc
ld bc, (_pop_pr_3s_hi) ; LD BC,(nn) флаги НЕ трогает
ex de, hl
sbc hl, bc
ex de, hl
;; акк += 2531011 = 0x0026_9EC3
ld bc, #0x9EC3
add hl, bc
ld bc, #0x0026
ex de, hl
adc hl, bc
ex de, hl
;; сохранить сид, вернуть СТАРШЕЕ слово в DE
push de
ex de, hl ; DE = младшее слово
ld hl, (_pop_pr_ptr)
ld (hl), e
inc hl
ld (hl), d
inc hl
pop de ; DE = старшее слово
ld (hl), e
inc hl
ld (hl), d
ret
__endasm;
}
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
return pop_rnd_fit(pop_lcg_step(seed), maxv);
}
#else /* !POP_PRANDOM_EXACT — генератор попроще, НЕ совместимый с эталоном */
/* xorshift16 + шаг Вейля. Зачем не LCG оригинала: тот 32-битный, и даже
* без __mullong (см. ветку выше) стоит ~7 000 тактов на вызов при двух
* вызовах за кадр это 3 % бюджета на генератор случайных чисел. Здесь
* умножений нет вовсе, только сдвиги и xor.
*
* Почему НЕ 8-битный RND Apple II (RNDseed := 5*RNDseed + 23 mod 256,
* Prince-of-Persia-Apple-II): он ещё быстрее, но у LCG по модулю 256
* младшие биты вырождены бит 0 просто чередуется. Наши вызовы это
* видят: раскладка кладки берёт prandom(1) (один бит) и prandom(4), и
* вместо шума получилась бы правильная шахматка. У xorshift такой
* структуры нет, а стоит он примерно столько же.
*
* Шаг Вейля (+0x9E37, нечётный) нужен ещё и потому, что у чистого
* xorshift ноль неподвижная точка, а сид кладки (номер комнаты + ряд +
* колонка) вполне может оказаться нулём.
*
* ПОСЛЕДСТВИЕ: последовательность отличается от SDLPoP другая (но
* статистически такая же) раскладка кладки и другие броски в боёвке.
* Поэтому по умолчанию собирается НЕ эта ветка: после переписывания LCG
* на ассемблер выигрыш от смены генератора почти исчез, а сверять
* картинку с эталоном стало важнее. Ветка оставлена как запасной ход. */
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv)
{
uint16_t s = seed->lo;
s ^= (uint16_t)(s << 7);
s ^= (uint16_t)(s >> 9);
s ^= (uint16_t)(s << 8);
s = (uint16_t)(s + 0x9E37u);
seed->lo = s;
seed->hi = 0; /* старшая половина не используется */
/* Отдаём СТАРШИЙ байт: у него нет короткопериодической структуры
* младших бит, а вызывающие берут от результата именно младшие. */
return pop_rnd_fit((uint16_t)(s >> 8), maxv);
}
#endif
+31 -4
View File
@@ -26,9 +26,36 @@ extern const int16_t pop_y_land[5];
* над комнатой, 3 = под комнатой). */ * над комнатой, 3 = под комнатой). */
int8_t pop_y_to_row(int16_t y); int8_t pop_y_to_row(int16_t y);
/* prandom (seg009:321) — 32-битный LCG оригинала. Сид ВНЕШНИЙ: у стен /* prandom (порт seg009:321). Сид ВНЕШНИЙ: у стен (раскладка кладки) и у
* (раскладка кладки) и у анимаций тайлов (фаза факелов) свои * анимаций тайлов (фаза факелов) свои последовательности, смешивать их
* последовательности, смешивать их нельзя. */ * нельзя.
uint16_t pop_prandom(unsigned long *seed, uint16_t maxv); *
* ДВА генератора, выбор через POP_PRANDOM_EXACT (см. pop_geom.c):
* 1 (по умолчанию) LCG оригинала (s*214013+2531011) БИТ-В-БИТ: та же
* раскладка кладки и те же броски, что в SDLPoP, поэтому по картинке
* можно сверяться с эталоном;
* 0 xorshift16 + шаг Вейля, без единого умножения. Дешевле ровно на
* 5 646 тактов за кадр, 1.3 % бюджета (замер A/B в MAME, комната 3,
* 175 кадров: 403 632 против 397 986). Последовательность другая
* статистически эквивалентная, но не та же.
*
* Обе версии считают 16-битными половинами, а не unsigned long: на 32-бит
* умножении SDCC зовёт __mullong, и это 8 430 тактов на вызов (замер в
* MAME, breakpoint на __mullong).
*
* ЗАДЕЛ: разрыв между версиями определяется не алгоритмом, а кодогенерацией
* SDCC те же генераторы на Z80-ассемблере укладываются в 86..148 тактов.
* Если процент понадобится, сначала переписать в asm ОРИГИНАЛЬНЫЙ LCG (там
* умножение на константу), а менять генератор только если и этого мало. */
typedef struct { uint16_t lo, hi; } pop_rnd_t;
#ifndef POP_PRANDOM_EXACT
#define POP_PRANDOM_EXACT 1
#endif
uint16_t pop_prandom(pop_rnd_t *seed, uint16_t maxv);
/* Сид из 16-битного числа (старшая половина = 0). */
#define pop_prandom_set(s, v) do { (s).lo = (uint16_t)(v); (s).hi = 0; } while (0)
#endif #endif
+104 -6
View File
@@ -10,11 +10,22 @@
#include "pop_geom.h" #include "pop_geom.h"
#include "pop_level.h" #include "pop_level.h"
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */ #include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
#include "pop_map.h" /* hitp_* — HP Кида */
#include "kid_data.h" #include "kid_data.h"
pop_char_t Guard; pop_char_t Guard;
uint8_t guardhp_curr, guardhp_max; uint8_t guardhp_curr, guardhp_max;
int8_t guardhp_delta; int8_t guardhp_delta;
int8_t can_guard_see_kid;
int8_t is_guard_notice;
uint8_t guard_skill;
int8_t guard_refrac, justblocked, kid_sword_strike;
int8_t offguard, holding_sword;
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
* см. pop_geom.h иначе перерисовка стены сбивала бы броски стража. */
pop_rnd_t pop_fight_seed;
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его /* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */ * заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
@@ -32,6 +43,10 @@ void pop_guard_reset(void)
Guard.direction = 0; Guard.direction = 0;
guardhp_curr = 0; guardhp_curr = 0;
guardhp_delta = 0; guardhp_delta = 0;
can_guard_see_kid = 0;
is_guard_notice = 0;
guard_refrac = justblocked = kid_sword_strike = 0;
offguard = holding_sword = 0;
} }
/* kill guard cheat (seg000:786): /* kill guard cheat (seg000:786):
@@ -44,11 +59,35 @@ void pop_guard_kill(void)
{ {
if (Guard.charid == CHARID_4_SKELETON) return; if (Guard.charid == CHARID_4_SKELETON) return;
if (!pop_guard_present()) return; if (!pop_guard_present()) return;
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
* общий control() ставит ему seq_71_dying. */
guardhp_delta = (int8_t)-guardhp_curr; guardhp_delta = (int8_t)-guardhp_curr;
guardhp_curr = 0;
Guard.alive = 0; Guard.alive = 0;
} }
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void)
{
int16_t hp;
if (hitp_delta) {
hp = (int16_t)hitp_curr + hitp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
hitp_curr = (uint8_t)hp;
pop_kid_hurt = 1; /* -> «брызги» поверх Кида */
hitp_delta = 0;
}
if (guardhp_delta) {
hp = (int16_t)guardhp_curr + guardhp_delta;
if (hp < 0) hp = 0;
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
guardhp_curr = (uint8_t)hp;
guardhp_delta = 0;
}
}
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у /* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где * стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame 149, где
* add_frame = 70 для кадров 102..106 (seg006:535). */ * add_frame = 70 для кадров 102..106 (seg006:535). */
@@ -61,11 +100,39 @@ void pop_guard_load_frame(void)
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe); pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
} }
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
* последовательностью тогда при возврате он лежит там же, а не встаёт
* живым. */
void pop_guard_leave(void)
{
if (Guard.charid == 0) return; /* стража не было */
pop_guard_state_save(Guard.room,
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
Guard.direction, Guard.x, guard_skill,
Guard.curr_seq,
(uint8_t)(Guard.alive >= 0));
}
/* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа —
* floor((xpos 7 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */
static int8_t guard_col_from_x(uint8_t xpos)
{
int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN];
int16_t col = x / TILE_SIZEX;
if (x % TILE_SIZEX < 0) --col;
return (int8_t)col;
}
void pop_guard_enter(uint8_t room) void pop_guard_enter(uint8_t room)
{ {
uint8_t tile, color, skill; uint8_t tile, color, skill;
int8_t dir; int8_t dir;
pop_guard_reset(); pop_guard_reset();
/* load_lev_spr (seg000:1112): tbl_guard_type == 1 — стражей на уровне
* НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки
* на таком уровне полезли бы стражи из данных комнат. */
if (pop_level_guard_type(pop_current_level) < 0) return;
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return; if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из /* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
* колонки тайла, а не берётся из файла. */ * колонки тайла, а не берётся из файла. */
@@ -80,13 +147,44 @@ void pop_guard_enter(uint8_t room)
Guard.alive = -1; Guard.alive = -1;
Guard.action = 1; /* actions_1_run_jump */ Guard.action = 1; /* actions_1_run_jump */
Guard.fall_x = Guard.fall_y = 0; Guard.fall_x = Guard.fall_y = 0;
guardhp_max = guardhp_curr = 3; /* get_guard_hp: skill 0, уровень 1 */ /* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
* всё, что вне диапазона, оригинал сводит к 3. */
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
* extrastrength (seg002:38) ненулевой только у skill 4 (=1). */
guardhp_max = guardhp_curr =
(uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0));
guardhp_delta = 0; guardhp_delta = 0;
/* enter_guard (seg002:0208): seqtbl_offset_char(seq_77_guard_stand_inactive). /* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
* Последовательность крутит общий интерпретатор (pop_guard_play), а * ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность он лежит
* таблицу кадров он выбирает по Char.charid у стража своя. */ * ровно там, где лёг (порт ветки guards_seq_hi != 0). */
{
uint16_t saved_seq = pop_guard_state_seq(room);
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
* пересчитывает её из колонки только при загрузке уровня, а дальше
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
* тайла. */
if (saved_seq) Guard.x = pop_guard_state_x(room);
/* ...и КОЛОНКУ пересчитываем из X (enter_guard, seg002:180:
* `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Брать её из
* тайла нельзя: leave_guard сохраняет get_tilepos(0, row), то
* есть колонку 0 всегда. У живого стража это незаметно (X там
* сам считается из колонки), а у ЗАПОМНЕННОГО ТРУПА X
* настоящая, и curr_col оставался нулём при теле, лежащем в
* колонке 2. Тайл трупа шёл в обходе раньше тайла Кида, и Кид
* рисовался ПОВЕРХ тела вместо того, чтобы пройти за ним
* (BUG-DRAWORDER-1). Колонка нужна не только отрисовке на
* неё смотрят коллизия и check_can_guard_see_kid. */
Guard.curr_col = guard_col_from_x(Guard.x);
pop_loadshad(); pop_loadshad();
pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE); if (saved_seq) Char.curr_seq = saved_seq;
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
pop_saveshad(); pop_saveshad();
pop_guard_play(); /* до первого кадра */ pop_guard_play(); /* до первого кадра */
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
Guard.alive = 1;
guardhp_curr = 0;
}
}
} }
+97 -1
View File
@@ -20,9 +20,45 @@
#include <stdint.h> #include <stdint.h>
#include "kid_data.h" /* kframe */ #include "kid_data.h" /* kframe */
#include "pop_char.h" #include "pop_char.h"
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
/* seqids (types.h:1070) — последовательности стража. */ /* seqids (types.h:1052+) — последовательности боя и стойки. */
#define SEQ_55_DRAW_SWORD 55
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
#define SEQ_57_BACK_WITH_SWORD 57
#define SEQ_58_GUARD_STRIKE 58
#define SEQ_60_TURN_WITH_SWORD 60
#define SEQ_61_PARRY_AFTER_STRIKE 61
#define SEQ_62_PARRY 62
#define SEQ_66_STRIKE_AFTER_PARRY 66
#define SEQ_71_DYING 71
#define SEQ_75_STRIKE 75 /* удар (Кид) */
#define SEQ_77_GUARD_STAND_INACTIVE 77 #define SEQ_77_GUARD_STAND_INACTIVE 77
#define SEQ_80_STAND_FLIPPED 80
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
#define SEQ_87_GUARD_BECOME_INACT 87
#define SEQ_90_EN_GARDE 90
#define SEQ_92_PUT_SWORD_AWAY 92
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
/* frameids (types.h:911+) и состояния меча (types.h:332). */
#define FRAME_150_PARRY 150
#define FRAME_151_STRIKE_1 151
#define FRAME_152_STRIKE_2 152
#define FRAME_153_STRIKE_3 153
#define FRAME_157_WALK_WITH_SWORD 157
#define FRAME_158_STAND_WITH_SWORD 158
#define FRAME_161_PARRY 161
#define FRAME_162_BLOCK_TO_STRIKE 162
#define FRAME_165_WALK_WITH_SWORD 165
#define FRAME_166_STAND_INACTIVE 166
#define FRAME_167_BLOCKED 167
#define FRAME_168_BACK 168
#define FRAME_169_BEGIN_BLOCK 169
#define FRAME_170_STAND_WITH_SWORD 170
#define FRAME_171_STAND_WITH_SWORD 171
#define SWORD_0_SHEATHED 0
#define SWORD_2_DRAWN 2
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */ /* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
#define CHARID_0_KID 0 #define CHARID_0_KID 0
@@ -31,12 +67,53 @@
#define CHARID_4_SKELETON 4 #define CHARID_4_SKELETON 4
#define CHARID_5_PRINCESS 5 #define CHARID_5_PRINCESS 5
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
#define NUM_GUARD_SKILLS 12
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */ /* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */ extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
extern uint8_t guardhp_curr, guardhp_max; extern uint8_t guardhp_curr, guardhp_max;
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */ extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
* 0 не видит, 1 видит, но не пойдёт (между ними loose/чомпер/дыра),
* 2 видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
extern int8_t can_guard_see_kid;
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
* снимает сам ИИ. */
extern int8_t is_guard_notice;
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
* кадр в autocontrol_opponent как в оригинале.
* guard_skill 0..11, из данных уровня; индекс в таблицы вероятностей;
* guard_refrac «отдышка» стража: пока >0, он не атакует и не наступает;
* justblocked Кид только что парировал (влияет на вероятность блока);
* kid_sword_strike Кид только что бил (стража это придерживает). */
extern uint8_t guard_skill;
extern int8_t guard_refrac, justblocked, kid_sword_strike;
/* Состояние меча КИДА (seg005/seg006):
* offguard меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
* достаёт клинок автоматически, даже видя стража;
* holding_sword клинок в руке (для индикатора HP стража). */
extern int8_t offguard, holding_sword;
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
extern pop_rnd_t pop_fight_seed;
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
void pop_check_can_guard_see_kid(void) __banked;
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked;
void pop_check_sword_hurt(void) __banked;
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2, /* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
* читает отрисовка в резиденте W3. image == 255 рисовать нечего. */ * читает отрисовка в резиденте W3. image == 255 рисовать нечего. */
extern kframe pop_gframe; extern kframe pop_gframe;
@@ -54,12 +131,27 @@ void pop_guard_free(void) __banked;
* seg002:0112) либо убрать его, если в комнате стража нет. */ * seg002:0112) либо убрать его, если в комнате стража нет. */
void pop_guard_enter(uint8_t room); void pop_guard_enter(uint8_t room);
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
* enter_room, пока Guard ещё держит прежнего. */
void pop_guard_leave(void);
/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE, /* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE,
* как kid_draw) и стереть прошлый (heal). Реализация pop_gdraw.c * как kid_draw) и стереть прошлый (heal). Реализация pop_gdraw.c
* (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */ * (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
* ПОСЛЕ персонажей она ничего не перекрывает. */
void pop_hp_draw(void) __banked;
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
void pop_hp_invalidate(void) __banked;
void pop_guard_heal(void) __banked; void pop_guard_heal(void) __banked;
void pop_guard_draw(void) __banked; void pop_guard_draw(void) __banked;
/* Ширина спрайта последнего нарисованного кадра стража (char_width_half
* в set_char_collision). Нужна для его ТАЙЛА ОБЪЕКТА от него зависит
* порядок отрисовки относительно Кида. */
uint16_t pop_guard_fp_width(void) __banked;
/* Убрать стража со сцены (смена комнаты / инициализация). */ /* Убрать стража со сцены (смена комнаты / инициализация). */
void pop_guard_reset(void); void pop_guard_reset(void);
@@ -67,6 +159,10 @@ void pop_guard_reset(void);
* при включённых читах проверку делает вызывающий. */ * при включённых читах проверку делает вызывающий. */
void pop_guard_kill(void); void pop_guard_kill(void);
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале в play_frame). */
void pop_do_delta_hp(void);
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */ /* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
void pop_guard_tick(void) __banked; void pop_guard_tick(void) __banked;
+326 -92
View File
@@ -12,6 +12,7 @@
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */ #include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */ #include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h" /* pop_clip_char_top (clip_char) */ #include "pop_map.h" /* pop_clip_char_top (clip_char) */
#include "pop_geom.h" #include "pop_geom.h"
#include "kid_data.h" #include "kid_data.h"
@@ -22,21 +23,28 @@ pop_char_t Kid;
* в play_seq, потребляется pop_check_knock (pop_map) трясёт loose-полы. */ * в play_seq, потребляется pop_check_knock (pop_map) трясёт loose-полы. */
int8_t knock; int8_t knock;
/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
* seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
* текущим уровнем и грузит следующий. Счётчик, а не флаг ровно как в
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
* легли сюда же без переделки. */
uint8_t pop_next_level;
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */ /* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
#define KID_MAXPAGES 28 #define KID_MAXPAGES 28
static atlas_t kidp[KID_MAXPAGES]; static atlas_t kidp[KID_MAXPAGES];
static uint8_t kid_npages; static uint8_t kid_npages;
/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): отдельный /* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
* спрайт клинка поверх Kid в кадрах «нашёл меч»/убирание в ножны (229..237). * ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
* Из sword_tbl взяты только строки 35..42 те, что адресуют эти кадры (бой * Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
* не портирован); индекс в атласе = sword_frame 35. Смещения таблицы * sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
* в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ calc_screen_x_coord. */ * Смещения в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
#define SWORD_SF_LO 35 * calc_screen_x_coord, x «вперёд» по направлению персонажа. */
#define SWORD_SF_HI 42 static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
static const int8_t SWORD_DX[8] = { 7, 14, 14, 14, 28, 28, 21, 14 }; static const int8_t SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
static const int8_t SWORD_DY[8] = {-32,-32,-31,-29,-28,-28,-25,-22 }; static const int8_t SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
static atlas_t swordp; static atlas_t swordp;
static uint8_t sword_ok; static uint8_t sword_ok;
@@ -47,6 +55,74 @@ int pop_sword_load(void)
return 0; return 0;
} }
/* add_sword_to_objtable (seg006:1798). Клинок рисуется, если персонаж в
* кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
* в руке всегда, даже пока sword ещё sheathed). Страницу W0 оставляем
* замапленной вызывающий делает gfx_w0_unmap сам. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh)
{
uint8_t sf, id, flip;
const uint8_t *simg;
uint16_t w, h;
int bx, top;
if (!sword_ok) return 0;
if (!((ch->frame >= 229 && ch->frame < 238) ||
ch->sword != 0 ||
(ch->charid == CHARID_2_GUARD && ch->alive < 0)))
return 0;
sf = (uint8_t)(fr->sword & 0x3F);
if (sf == 0 || sf >= KID_NSWORD) return 0;
id = SWORD_ID[sf];
if (id == 0xFF) return 0;
simg = (const uint8_t *)atlas_image(&swordp, id);
gfx_w0_map(swordp.page);
w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (!w || !h) return 0;
flip = (uint8_t)(ch->direction >= 0);
bx = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE);
/* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
* подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0);
else
gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
*rx = bx; *ry = top; *rw = w; *rh = h;
return 1;
}
/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
* Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
* Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */
void pop_kid_img_blit(uint8_t image, int x, int y)
{
uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */
uint8_t idx = (uint8_t)(image & 7);
const uint8_t *img;
uint16_t w, h;
if (pg >= kid_npages) return;
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
gfx_w0_map(kidp[pg].page);
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
/* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
* ради бесплатного горизонтального флипа), поэтому блит обязан быть
* колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной.
* noclip БЕЗ проверки (CLIP-1): единственный вызывающий полоса HP
* (pop_gdraw.c), у неё координаты фиксированные и заведомо на экране. */
if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
gfx_w0_unmap();
}
int pop_kid_load(uint8_t npages) int pop_kid_load(uint8_t npages)
{ {
static const char *const names[KID_MAXPAGES] = { static const char *const names[KID_MAXPAGES] = {
@@ -132,13 +208,61 @@ static kframe kid_frame; /* кадр Kid для kid_draw */
* копирование платится один раз на переключение персонажа. */ * копирование платится один раз на переключение персонажа. */
pop_char_t Char; pop_char_t Char;
void pop_loadkid(void) { Char = Kid; } /* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
void pop_savekid(void) { Kid = Char; kid_frame = cur_frame; } * рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809). Через неё
/* Выгрузка БЕЗ кадра: control() кадр не трогает, а cur_frame в этот * ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
* момент принадлежит тому, кто последним крутил play_seq. */ * ними: для стража Opp = Kid, для Кида Opp = Guard. */
void pop_savekid_state(void) { Kid = Char; } pop_char_t Opp;
/* Снимок последовательности на момент входа в окно Char: по нему
* pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
* от «её сменил pop_map (прямо в Kid)». */
static uint16_t kid_seq_at_load;
void pop_loadkid(void) { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
void pop_savekid(void) { Kid = Char; }
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
* меняет у активного персонажа последовательность и состояние меча.
*
* Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
* действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
* safe_step...), и он пишет ПРЯМО в Kid там ещё не портирован переход на
* Char. Полное копирование затирало эти правки: Kid терял выравнивание x
* и ряд, и спуск с уступа через вис не срабатывал персонаж просто
* приседал. Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
void pop_savekid_state(void)
{
/* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
* (pop_char_set_seq), pop_map прямо в Kid (kid_set_seq: спуск с
* уступа SEQ_68, зацеп, осторожный шаг). Решение pop_map старше:
* диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
* их спуск в проём не срабатывал, а персонаж даже не приседал, потому
* что pop_down_action уже вернул «обработано». */
uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
if (!map_changed) Kid.curr_seq = Char.curr_seq;
Kid.sword = Char.sword;
}
void pop_loadshad(void) { Char = Guard; } void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; pop_gframe = cur_frame; } void pop_saveshad(void) { Guard = Char; }
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
void pop_loadkid_and_opp(void) { Char = Kid; Opp = Guard; }
void pop_savekid_and_opp(void) { Kid = Char; Guard = Opp; }
/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
* +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
* приходится на ширину тайла между ними. Живёт здесь, а не в guards.c,
* потому что нужен по обе стороны банковой границы и ИИ, и диспетчеру
* боёвки (pop_ctrl). */
int16_t pop_char_opp_dist(void)
{
int16_t distance;
if (Char.room != Opp.room) return 999;
distance = (int16_t)Opp.x - (int16_t)Char.x;
if (Char.direction < 0) distance = -distance;
if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
return distance;
}
int pop_kid_data_load(const char *path) int pop_kid_data_load(const char *path)
{ {
@@ -215,6 +339,15 @@ static void load_frame(void)
cur_frame.dy = (int8_t)f[2]; cur_frame.dy = (int8_t)f[2];
cur_frame.flags = f[3]; cur_frame.flags = f[3];
cur_frame.sword = f[4]; cur_frame.sword = f[4];
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
* на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
* Кида и рисовал произвольную позу. Теперь владелец один тот, для
* кого кадр реально загружен. */
if (Char.charid == CHARID_0_KID) kid_frame = cur_frame;
else pop_gframe = cur_frame;
} }
static uint8_t char_dx_forward(int8_t dx) static uint8_t char_dx_forward(int8_t dx)
@@ -226,6 +359,7 @@ static uint8_t char_dx_forward(int8_t dx)
/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу — /* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
* их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */ * их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t kid_cur_dx(void) { return cur_frame.dx; } int8_t kid_cur_dx(void) { return cur_frame.dx; }
int8_t kid_cur_dy(void) { return cur_frame.dy; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; } uint8_t kid_cur_flags(void) { return cur_frame.flags; }
/* seqtbl_offset_char: сменить последовательность Kid (единая точка — /* seqtbl_offset_char: сменить последовательность Kid (единая точка —
@@ -289,10 +423,21 @@ void play_seq(void)
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */ case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1; knock = -1;
break; break;
case 0xF2: /* SOUND — потребить 1 байт */ case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
* ВТОРОЙ смысл «Кид нашумел», и его терять нельзя.
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
* назван silent потому, что НЕ звучит, а стражи его всё
* равно замечают (стоит, например, в seq доставания меча).
* DRINK(3)/LEVEL(4) не шум. См. BUG-GUARD-DEAF-1. */
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
Char.curr_seq++; Char.curr_seq++;
break; break;
case 0xF1: /* END_LEVEL */ case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
* В оригинале это `++next_level`, а главный цикл
* (play_level_2, seg003:0386) выходит, когда next_level
* разошёлся с current_level. У нас так же: счётчик, а
* решение снаружи (roomtest.c). */
pop_next_level++;
break; break;
case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
* (порт play_seq -> proc_get_object, seg006) */ * (порт play_seq -> proc_get_object, seg006) */
@@ -311,6 +456,40 @@ void play_seq(void)
} }
} }
/* load_fram_det_col (seg006:0144) = load_frame() + determine_col().
*
* Зачем это отдельной публичной точкой. cur_frame ОДИН глобал на всех
* персонажей (так и в оригинале), и его владелец тот, кто последним
* прошёл load_frame. В кадре последним тикает страж, поэтому к моменту
* управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame
* считается вся геометрия: dx_weight -> determine_col ->
* distance_to_edge_weight -> get_edge_distance, то есть буквально выбор
* ветки в check_jump_up.
*
* Оригинал от этого страхуется явно: play_kid_frame (seg000:1209) и
* play_guard_frame (seg000:1246) сразу после loadkid/loadshad зовут
* load_fram_det_col() ДО control(). У нас этого не было, и Кид считал
* свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
* зацеп». Результат менялся от кадра к кадру вместе с анимацией стража.
*
* determine_col у нас существует только для Кида: pop_map работает с Kid
* напрямую, а не с абстрактным Char. Поэтому вторую половину связки делаем
* только на его ветке стражу пока хватает перезагрузки самого кадра (его
* колонку ведёт pop_guard_enter/leave). Char.curr_col синхронизируем
* руками: окно Char копия Kid. */
void pop_load_fram_det_col(void)
{
if (!kdat_ok) return;
gfx_w0_map(kdat_page);
load_frame();
gfx_w0_unmap();
if (Char.charid == CHARID_0_KID) {
pop_determine_col();
Char.curr_col = Kid.curr_col;
}
}
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки /* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
* loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот * loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот
* работает с Char и сам по себе не знает, чей это персонаж. */ * работает с Char и сам по себе не знает, чей это персонаж. */
@@ -327,6 +506,7 @@ void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
Kid.fall_x = Kid.fall_y = 0; Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0; Kid.repeat = 0;
Kid.charid = 0; /* charid_0_kid: своя таблица кадров */ Kid.charid = 0; /* charid_0_kid: своя таблица кадров */
Kid.alive = -1; /* в оригинале ЖИВОЙ = alive < 0 */
Kid.curr_seq = kid_seq_off[seq_id]; Kid.curr_seq = kid_seq_off[seq_id];
pop_kid_play(); /* до первого кадра */ pop_kid_play(); /* до первого кадра */
} }
@@ -363,6 +543,64 @@ static int kid_lx[2], kid_ly[2];
static uint16_t kid_lw[2], kid_lh[2]; static uint16_t kid_lw[2], kid_lh[2];
static uint8_t kid_lvalid[2]; static uint8_t kid_lvalid[2];
/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона).
* Раньше он объединялся с прямоугольником персонажа но объединение сильно
* больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил
* почти вдвое шире и выше самого Kid, а heal стоит ровно по площади.
* Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение
* оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования
* зато клинок/брызги честно уходят под передние грани тайлов). */
static int kid_ox[2], kid_oy[2];
static uint16_t kid_ow[2], kid_oh[2];
static uint8_t kid_ovalid[2];
/* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и
* брызги в одном кадре встречаются редко и оба маленькие их объединение
* дешевле третьего слота. */
static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h)
{
if (!kid_ovalid[dp]) {
kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h;
kid_ovalid[dp] = 1;
return;
}
{
int x0 = kid_ox[dp], y0 = kid_oy[dp];
int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp];
if (x < x0) x0 = x;
if (y < y0) y0 = y;
if (x + (int)w > x1) x1 = x + (int)w;
if (y + (int)h > y1) y1 = y + (int)h;
kid_ox[dp] = x0; kid_oy[dp] = y0;
kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0);
}
}
/* Окно fore-клипа = объединение «персонаж + накладные». Считается по
* прямоугольникам, ничего не рисует. */
static void kid_fore_clip(uint8_t dp)
{
int x0, y0, x1, y1;
if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; }
if (!kid_ovalid[dp]) {
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
(int)kid_lw[dp], (int)kid_lh[dp]);
return;
}
if (!kid_lvalid[dp]) {
pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF,
(int)kid_ow[dp], (int)kid_oh[dp]);
return;
}
x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp];
y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp];
x1 = kid_lx[dp] + (int)kid_lw[dp];
y1 = kid_ly[dp] + (int)kid_lh[dp];
if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp];
if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp];
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}
/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg): /* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
* obj_x ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y низ, w/h размер. */ * obj_x ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y низ, w/h размер. */
static int kid_fp_x, kid_fp_y; static int kid_fp_x, kid_fp_y;
@@ -406,38 +644,38 @@ void kid_draw_splash(void)
int top = obj_y - (int)h + 1; int top = obj_y - (int)h + 1;
if (flip) bx -= (int)w; if (flip) bx -= (int)w;
gfx_set_bank(GFX_BANK_SPRITE); gfx_set_bank(GFX_BANK_SPRITE);
if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
else
gfx_blit_cols(bx, top + POP_YOFF, img, flip); gfx_blit_cols(bx, top + POP_YOFF, img, flip);
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
/* Прямоугольник splash расширяет область heal ЭТОЙ страницы: иначе /* Брызги — накладной спрайт: свой прямоугольник heal (см.
* брызги останутся на экране после кадра. */ * kid_overlay_add), окно fore пересчитываем объединением. */
{ {
uint8_t dp = gfx_get_draw_page(); uint8_t dp = gfx_get_draw_page();
if (kid_lvalid[dp]) { kid_overlay_add(dp, bx, top, w, h);
int x0 = kid_lx[dp] < bx ? kid_lx[dp] : bx; kid_fore_clip(dp);
int y0 = kid_ly[dp] < top ? kid_ly[dp] : top;
int x1 = kid_lx[dp] + (int)kid_lw[dp];
int y1 = kid_ly[dp] + (int)kid_lh[dp];
if (bx + (int)w > x1) x1 = bx + (int)w;
if (top + (int)h > y1) y1 = top + (int)h;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0); kid_lh[dp] = (uint16_t)(y1 - y0);
/* Окно fore-слоя расширяем тем же объединением: брызги
* тоже должны уходить под передние грани тайлов. */
pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}
} }
} }
gfx_w0_unmap(); gfx_w0_unmap();
} }
/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */ /* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid.
*
* pop_heal_fast, а не голый gfx_heal: прямоугольник тут это ровно тот
* прямоугольник, который kid_draw нарисовал noclip-ядром, то есть в
* подавляющем большинстве кадров он заведомо на экране. Клипающий путь
* остаётся для straddle (kid_render_dx = 140) и падения ниже поля
* там координата реально уходит за экран. */
void kid_heal(void) void kid_heal(void)
{ {
uint8_t p = gfx_get_draw_page(); uint8_t p = gfx_get_draw_page();
if (kid_lvalid[p]) if (kid_lvalid[p]) /* +YOFF: центрирование */
gfx_heal(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); /* +YOFF: центрирование */ pop_heal_fast(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]);
kid_lvalid[p] = 0; if (kid_ovalid[p])
pop_heal_fast(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]);
kid_lvalid[p] = kid_ovalid[p] = 0;
} }
void kid_draw(void) void kid_draw(void)
@@ -482,7 +720,8 @@ void kid_draw(void)
uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */ uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */
int bx = obj_x + KID_XOFF; int bx = obj_x + KID_XOFF;
int skip = 0; /* строк спрайта срезано сверху */ int skip = 0; /* строк спрайта срезано сверху */
int ct; int ct, cr;
uint16_t vis_w = w; /* видимая ширина (клип справа) */
if (flip) if (flip)
bx -= (int)w; /* seg008: xpos -= image->w */ bx -= (int)w; /* seg008: xpos -= image->w */
top = obj_y - (int)h + 1 + KID_YOFF; top = obj_y - (int)h + 1 + KID_YOFF;
@@ -495,9 +734,28 @@ void kid_draw(void)
skip = ct - top; skip = ct - top;
if (skip >= (int)h) skip = (int)h; /* обрезан целиком */ if (skip >= (int)h) skip = (int)h; /* обрезан целиком */
} }
/* clip_char, вторая половина: правый косяк двери уровня. Дверь
* рисуется ПОД персонажем (вся в backtable), поэтому спрятать его
* можно только срезав спрайт см. pop_clip_char_right. */
cr = pop_clip_char_right();
if (cr) {
int avail = cr - bx;
if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */
else if (avail < (int)w) vis_w = (uint16_t)avail;
}
gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */
if (skip < (int)h) /* +YOFF: центрирование */ if (skip < (int)h && vis_w) { /* +YOFF: центрирование */
gfx_blit_cols_part(bx, top + skip + POP_YOFF, img, flip, skip, 0); /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа
* идём общим путём: это редкие кадры (подъём в двери уровня раз
* за уровень), цена подготовки тут не видна. */
if (vis_w == w && pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
(uint16_t)((int)h - skip)))
gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
(uint8_t)skip, 0);
else
gfx_blit_cols_part_w(bx, top + skip + POP_YOFF, img, flip,
skip, 0, (uint8_t)(vis_w == w ? 0 : vis_w));
}
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
* узко, только по прямоугольнику Кида и только при выходе. */ * узко, только по прямоугольнику Кида и только при выходе. */
@@ -505,62 +763,38 @@ void kid_draw(void)
/* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
* срезом (мусор/дыра в кладке на второй странице дабл-буфера). */ * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
kid_lx[dp] = bx; kid_ly[dp] = top + skip; kid_lx[dp] = bx; kid_ly[dp] = top + skip;
kid_lw[dp] = w; kid_lh[dp] = (uint16_t)((int)h - skip); kid_lw[dp] = vis_w; kid_lh[dp] = (uint16_t)((int)h - skip);
kid_lvalid[dp] = (skip < (int)h); kid_lvalid[dp] = (skip < (int)h && vis_w != 0);
kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */
} }
/* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
* Kid. Прямоугольник heal расширяем объединением иначе клинок, выйдя * Kid со СВОИМ прямоугольником heal (kid_overlay_add). */
* за габарит Kid, оставит след. */ {
if (sword_ok && Kid.frame >= 229 && Kid.frame < 238) { int sx, sy; uint16_t sw, sh;
uint8_t sf = (uint8_t)(fr.sword & 0x3F); if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
if (sf >= SWORD_SF_LO && sf <= SWORD_SF_HI) { kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
uint8_t si = (uint8_t)(sf - SWORD_SF_LO);
const uint8_t *simg = (const uint8_t *)atlas_image(&swordp, si);
uint16_t sw, sh;
gfx_w0_map(swordp.page);
sw = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
sh = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
if (sw && sh) {
uint8_t sflip = (Kid.direction >= 0);
int sbx = obj_x + ((Kid.direction < 0) ? -(int)SWORD_DX[si]
: (int)SWORD_DX[si]);
int stop = obj_y + (int)SWORD_DY[si] - (int)sh + 1;
uint8_t dp = gfx_get_draw_page() & 1;
if (sflip) sbx -= (int)sw;
gfx_set_bank(GFX_BANK_SPRITE);
gfx_blit_cols_part(sbx, stop + POP_YOFF, simg, sflip, 0, 0);
gfx_set_bank(GFX_BANK_NORMAL);
if (!kid_lvalid[dp]) {
kid_lx[dp] = sbx; kid_ly[dp] = stop;
kid_lw[dp] = sw; kid_lh[dp] = sh;
kid_lvalid[dp] = 1;
} else {
int x0 = kid_lx[dp], y0 = kid_ly[dp];
int x1 = x0 + (int)kid_lw[dp], y1 = y0 + (int)kid_lh[dp];
if (sbx < x0) x0 = sbx;
if (stop < y0) y0 = stop;
if (sbx + (int)sw > x1) x1 = sbx + (int)sw;
if (stop + (int)sh > y1) y1 = stop + (int)sh;
kid_lx[dp] = x0; kid_ly[dp] = y0;
kid_lw[dp] = (uint16_t)(x1 - x0);
kid_lh[dp] = (uint16_t)(y1 - y0);
}
}
}
} }
gfx_w0_unmap(); gfx_w0_unmap();
/* Окно fore-слоя = итоговый прямоугольник спрайта (с клинком и /* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
* обрезкой clip_char): fore-проход вернёт куски тайлов ТОЛЬКО поверх * fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
* него, а не целыми тайлами см. pop_bg.c, fclip_on. Ставим и когда * тайлами см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего:
* рисовать нечего (kid_lvalid=0): нулевое окно = fore-проход не * нулевое окно = fore-проход не трогает экран, что и правильно. */
* трогает экран, что и правильно перекрывать нечего. */ kid_fore_clip(gfx_get_draw_page() & 1);
{ }
uint8_t dp = gfx_get_draw_page() & 1;
if (kid_lvalid[dp]) /* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid.
pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF, *
(int)kid_lw[dp], (int)kid_lh[dp]); * Окно (pop_fore_set_clip) одно на всех, и его ставит КАЖДЫЙ, кто рисует
else * персонажа: kid_draw в конце своё, pop_guard_draw в конце своё. Пока
pop_fore_set_clip(0, 0, 0, 0); * страж рисовался строго ДО Кида, к моменту pop_fore_over_kid в окне
} * оказывался Kid, и всё сходилось само. Как только порядок стал
* переменным (guard_over_kid в roomtest.c порядок задаёт обход тайлов),
* при «страж поверх» окно оставалось СТРАЖЬИМ и fore-проход Кида отсекался
* целиком: Кид рисовался поверх передних столбов.
*
* Поэтому окно восстанавливается явно перед pop_fore_over_kid, а не
* держится «по счастливому порядку вызовов». */
void kid_fore_clip_restore(void)
{
kid_fore_clip(gfx_get_draw_page() & 1);
} }
+43
View File
@@ -12,6 +12,7 @@
#include <stdint.h> #include <stdint.h>
#include "pop_char.h" #include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */ /* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
extern pop_char_t Kid; extern pop_char_t Kid;
@@ -20,6 +21,11 @@ extern pop_char_t Kid;
* (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */ * (свой ряд). Ставит play_seq, читает/сбрасывает pop_check_knock. */
extern int8_t knock; extern int8_t knock;
/* Порт next_level (seg000): END_LEVEL в play_seq инкрементит — «уровень
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
* там рестарт того же уровня; загрузка следующего levels_plan.md. */
extern uint8_t pop_next_level;
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph. /* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
* 0 OK, -1 ошибка. */ * 0 OK, -1 ошибка. */
int pop_kid_load(uint8_t npages); int pop_kid_load(uint8_t npages);
@@ -43,6 +49,15 @@ void pop_kid_data_free(void);
int pop_sword_load(void); int pop_sword_load(void);
void pop_kid_free(void); void pop_kid_free(void);
/* Нарисовать клинок chtab_0 поверх персонажа (порт add_sword_to_objtable,
* seg006:1798) общая точка для Кида и стража: меч в оригинале ОДИН на
* всех. obj_x/obj_y та же опорная точка, что у спрайта персонажа.
* Возвращает 1 и прямоугольник клинка (для heal), 0 клинка нет.
* Страницу W0 оставляет замапленной: вызывающий делает gfx_w0_unmap. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
int obj_x, int obj_y,
int *rx, int *ry, uint16_t *rw, uint16_t *rh);
/* Инициализировать Kid: последовательность seq_id, клетка col/row, /* Инициализировать Kid: последовательность seq_id, клетка col/row,
* направление dir (-1/0). Ставит x/y по x_bump/y_land. */ * направление dir (-1/0). Ставит x/y по x_bump/y_land. */
void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir); void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir);
@@ -59,11 +74,22 @@ void play_seq(void); /* работает с Char — см. pop_kid_play
* Интерпретатор последовательностей общий для Kid и стража: у них общая * Интерпретатор последовательностей общий для Kid и стража: у них общая
* seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */ * seqtbl, но РАЗНЫЕ таблицы кадров (load_frame выбирает по Char.charid). */
extern pop_char_t Char; extern pop_char_t Char;
/* Соперник активного персонажа (порт Opp): ставится вместе с Char через
* pop_loadshad_and_opp. Читают ИИ стража и боёвка (char_opp_dist). */
extern pop_char_t Opp;
void pop_loadkid(void); void pop_loadkid(void);
void pop_savekid(void); void pop_savekid(void);
void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */ void pop_savekid_state(void); /* Kid = Char, кадр не трогаем */
void pop_loadshad(void); void pop_loadshad(void);
void pop_saveshad(void); void pop_saveshad(void);
void pop_loadshad_and_opp(void); /* Char = Guard, Opp = Kid (seg006:841) */
void pop_saveshad_and_opp(void); /* Guard = Char, Kid = Opp */
void pop_loadkid_and_opp(void); /* Char = Kid, Opp = Guard */
void pop_savekid_and_opp(void); /* Kid = Char, Guard = Opp */
/* Дистанция до соперника в системе Char: >0 впереди, <0 за спиной,
* 999 разные комнаты (порт char_opp_dist, seg006:2135). */
int16_t pop_char_opp_dist(void);
/* Прокрутить последовательность персонажа целиком (load + play_seq + /* Прокрутить последовательность персонажа целиком (load + play_seq +
* save). Звать вместо голого play_seq. */ * save). Звать вместо голого play_seq. */
@@ -71,9 +97,17 @@ void pop_char_set_seq(uint8_t seq_id); /* seq активного персон
void pop_kid_play(void); void pop_kid_play(void);
void pop_guard_play(void); void pop_guard_play(void);
/* load_fram_det_col (seg006:0144) — перезагрузить cur_frame под АКТИВНОГО
* персонажа (Char) и пересчитать его колонку. Звать сразу после pop_loadkid/
* pop_loadshad и ДО диспетчера control(), как это делают play_kid_frame /
* play_guard_frame (seg000:1209/1246): cur_frame один на всех, и без этого
* геометрия одного персонажа считается по кадру другого. */
void pop_load_fram_det_col(void);
/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования
* таблицы во втором TU). */ * таблицы во втором TU). */
int8_t kid_cur_dx(void); int8_t kid_cur_dx(void);
int8_t kid_cur_dy(void);
uint8_t kid_cur_flags(void); uint8_t kid_cur_flags(void);
/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая /* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая
@@ -92,10 +126,19 @@ void pop_kid_set_render_dx(int dx);
* теневой копии; запоминает прямоугольник для kid_heal). */ * теневой копии; запоминает прямоугольник для kid_heal). */
void kid_draw(void); void kid_draw(void);
/* Вернуть окно fore-клипа на прямоугольник последнего кадра Kid. Звать
* ПЕРЕД pop_fore_over_kid, если между ним и kid_draw кто-то ещё рисовал
* персонажа (pop_guard_draw ставит окно по стражу). */
void kid_fore_clip_restore(void);
/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid — /* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid —
* звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */
void kid_draw_splash(void); void kid_draw_splash(void);
/* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP
* (id 216 полное деление / 217 пустое). */
void pop_kid_img_blit(uint8_t image, int x, int y);
/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid — /* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid —
* вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */
void kid_heal(void); void kid_heal(void);
+198 -10
View File
@@ -16,6 +16,12 @@
extern void _gfx_w0_isr(void); extern void _gfx_w0_isr(void);
#define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */ #define LVL_DATA_OFF 0x100u /* данные с 0x100; 0x00..0xFF — ISR-стаб */
/* Эталонная копия foretable В ТОЙ ЖЕ странице: страница 16 КБ, данных
* уровня 2.3 КБ место даром. Нужна, потому что foretable теперь
* МУТАБЕЛЬНАЯ (pop_level_set_tile), а рестарт уровня в оригинале
* это load_level() заново (play_level, seg003:57), то есть все плиты целы
* и зелья на месте. pop_level_reset_tiles восстанавливает из неё. */
#define LVL_PRISTINE_OFF 0x1000u
/* Смещения блоков blueprnt относительно начала данных (Table 6). */ /* Смещения блоков blueprnt относительно начала данных (Table 6). */
#define BP_FG 0u #define BP_FG 0u
@@ -27,12 +33,18 @@ extern void _gfx_w0_isr(void);
#define BP_GTILE 2119u #define BP_GTILE 2119u
#define BP_GDIR 2143u #define BP_GDIR 2143u
#define BP_GX 2167u #define BP_GX 2167u
#define BP_GSEQLO 2191u
#define BP_GSKILL 2215u #define BP_GSKILL 2215u
#define BP_GSEQHI 2239u
#define BP_GCOLOR 2263u #define BP_GCOLOR 2263u
#define BP_START 2112u #define BP_START 2112u
#define ROOM_TILES 30u #define ROOM_TILES 30u
#define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */ #define MIN_SIZE (BP_START + 3u) /* минимум, чтобы прочитать старт */
/* fwd: инициализация живой копии стражей (тело ниже). */
static void gstate_init(void);
static uint8_t lvl_blk, lvl_page, lvl_ok; static uint8_t lvl_blk, lvl_page, lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */ /* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
static uint8_t dl1[256], dl2[256]; static uint8_t dl1[256], dl2[256];
@@ -68,12 +80,16 @@ int pop_level_load(const char *path)
dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i]; dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i]; dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
} }
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
for (i = 0; i < 24u * ROOM_TILES; i++)
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
} }
sprinter_page_w3(saved_w3); sprinter_page_w3(saved_w3);
close(fd); close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; } if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
lvl_ok = 1; lvl_ok = 1;
gstate_init(); /* живая копия состояния стражей — из уровня */
return 0; return 0;
} }
@@ -82,6 +98,52 @@ void pop_level_free(void)
if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; } if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
} }
/* ---- Номер уровня и потабличные различия ---------------------------- */
uint8_t pop_current_level;
/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = демо).
* Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а
* расхождение с оригиналом сразу видно глазами. */
static const uint8_t tbl_entry_pose[16] =
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 };
static const uint8_t tbl_guard_hp[16] =
{ 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 };
static const int8_t tbl_guard_type[16] =
{ 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 };
static const uint8_t tbl_level_type[16] =
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 };
uint8_t pop_level_entry_pose(uint8_t n) { return n < 16 ? tbl_entry_pose[n] : 0; }
uint8_t pop_level_guard_hp(uint8_t n) { return n < 16 ? tbl_guard_hp[n] : 3; }
int8_t pop_level_guard_type(uint8_t n) { return n < 16 ? tbl_guard_type[n] : -1; }
uint8_t pop_level_tileset(uint8_t n) { return n < 16 ? tbl_level_type[n] : 0; }
int pop_level_load_num(uint8_t n)
{
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
* резидент W1 форматтер, которого здесь больше негде применить. */
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
uint8_t saved_blk = lvl_blk, saved_page = lvl_page, saved_ok = lvl_ok;
if (n > 15) return -1;
pri[12] = alt[8] = (char)('0' + n / 10);
pri[13] = alt[9] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* pop_level_load сам аллоцирует страницу, и если файла нет остаёмся
* на текущем уровне вместо падения в пустой уровень. */
lvl_ok = 0; /* чтобы pop_level_load не мешался */
if (pop_level_load(pri) != 0 && pop_level_load(alt) != 0) {
lvl_blk = saved_blk; lvl_page = saved_page; lvl_ok = saved_ok;
return -1;
}
if (saved_ok) mem_free_block(saved_blk);
pop_current_level = n;
return 0;
}
/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже /* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
* замаплена вызывающим). */ * замаплена вызывающим). */
static const uint8_t *room_fg_ptr(uint8_t r) static const uint8_t *room_fg_ptr(uint8_t r)
@@ -148,12 +210,23 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; } for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
} }
/* belowrow: верхний ряд (row0) комнаты снизу */ /* belowrow: верхний ряд (row0) комнаты снизу — ОДИННАДЦАТЬ байт.
* [0..9] колонки 0..9 комнаты снизу; [10] тайл (0,9) комнаты
* ПО ДИАГОНАЛИ СНИЗУ-СЛЕВА (room_BL у оригинала). Одиннадцатый нужен
* тайлу (2,0): его «сосед снизу-слева» это колонка 1 ряда 3, то есть
* чужая комната, и load_rowbelow (seg008:368) резолвит её именно так,
* с дефолтом «стена» только когда такой комнаты НЕТ. Раньше мы клали
* туда стену БЕЗУСЛОВНО, и в пустом углу рисовался клин кладки
* (BUG-SEAM-WEDGE-1, ур. 3 комн. 18). */
if (down >= 1 && down <= 24) { if (down >= 1 && down <= 24) {
const uint8_t *df = room_fg_ptr(down); const uint8_t *df = room_fg_ptr(down);
uint8_t dl = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(down - 1) * 4];
for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F); for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
below_fg[10] = (dl >= 1 && dl <= 24)
? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F)
: 20; /* кромка уровня */
} else { } else {
for (i = 0; i < 10; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */ for (i = 0; i < 11; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */
} }
gfx_w0_unmap(); gfx_w0_unmap();
@@ -177,6 +250,48 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col)
return res; return res;
} }
/* set_tile-запись в ЖИВУЮ foretable уровня (порт curr_room_tiles[…] = …).
* В оригинале уровень обычная структура в ОЗУ, и do_pickup (seg006:1671),
* remove_loose (seg007:0EB8), loose_land (seg007:11E8) правят её насовсем.
* У нас уровень в EMM-странице пишем прямо туда, поэтому изменение
* переживает выход из комнаты (BUG-LVLSTATE-1). Пишем БАЙТ ЦЕЛИКОМ:
* старшие биты старого тайла (напр. 0x20 «solid loose») к новому типу
* отношения не имеют. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
{
if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return;
gfx_w0_map(lvl_page);
((uint8_t *)(LVL_DATA_OFF + BP_FG))[(uint16_t)(room - 1) * ROOM_TILES + tilepos]
= tile;
gfx_w0_unmap();
}
/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
* foretable в исходное состояние из эталонной копии. */
void pop_level_reset_tiles(void)
{
uint16_t i;
uint8_t *dst;
const uint8_t *src;
if (!lvl_ok) return;
gfx_w0_map(lvl_page);
dst = (uint8_t *)(LVL_DATA_OFF + BP_FG);
src = (const uint8_t *)LVL_PRISTINE_OFF;
for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i];
gfx_w0_unmap();
}
/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83):
* стражи такая же часть данных уровня, как тайлы, и перезагрузка файла
* возвращает их всех. Убитый до смерти Кида страж после respawn снова жив
* и стоит на исходном тайле; забывается и запомненная поза трупа
* (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */
void pop_level_reset_guards(void)
{
if (!lvl_ok) return;
gstate_init();
}
uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; } uint8_t pop_doorlink1(uint8_t i) { return dl1[i]; }
uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; } uint8_t pop_doorlink2(uint8_t i) { return dl2[i]; }
void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; } void pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }
@@ -243,22 +358,95 @@ static uint8_t start_byte(uint16_t off)
/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат /* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003). Возврат
* 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из * 0 = стража в комнате нет (tile >= 30). X оригинал пересчитывает из
* колонки (pos_guards), поэтому guards_x из файла не читаем. */ * колонки (pos_guards), поэтому guards_x из файла не читаем. */
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
* В оригинале массивы стражей часть структуры уровня в ОЗУ, и движок их
* ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
* позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq. Благодаря
* этому, вернувшись, находишь труп там же, а не воскресшего стража.
* У нас уровень лежит в EMM-странице только на чтение, поэтому держим
* мутабельную копию в W2: 6 байт на комнату. */
#define GS_TILE 0
#define GS_DIR 1
#define GS_X 2
#define GS_SKILL 3
#define GS_SEQL 4
#define GS_SEQH 5
static uint8_t gstate[24 * 6];
static void gstate_init(void)
{
const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
uint8_t r;
gfx_w0_map(lvl_page);
for (r = 0; r < 24; r++) {
uint8_t *g = gstate + (uint16_t)r * 6;
g[GS_TILE] = p[BP_GTILE + r];
g[GS_DIR] = p[BP_GDIR + r];
g[GS_X] = p[BP_GX + r];
g[GS_SKILL] = p[BP_GSKILL + r];
/* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
* комнатах (оригинал чистит их в reset_level_unused_fields).
* Осмысленное значение туда кладёт только leave_guard, поэтому
* стартуем нулями: 0 = «поднимать стандартной стойкой».
* Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
g[GS_SEQL] = 0;
g[GS_SEQH] = 0;
}
gfx_w0_unmap();
}
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты. seq
* сохраняем ТОЛЬКО у мёртвого у живого seq_hi = 0 означает «поднять
* стандартной стойкой», как в оригинале. */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead)
{
uint8_t *g;
if (room < 1 || room > 24) return;
g = gstate + (uint16_t)(room - 1) * 6;
g[GS_TILE] = tile;
g[GS_DIR] = (uint8_t)dir;
g[GS_X] = x;
g[GS_SKILL] = skill;
if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
else { g[GS_SEQH] = 0; }
}
uint16_t pop_guard_state_seq(uint8_t room)
{
const uint8_t *g;
if (room < 1 || room > 24) return 0;
g = gstate + (uint16_t)(room - 1) * 6;
if (g[GS_SEQH] == 0) return 0; /* живой — стандартная стойка */
return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
}
/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
* центре тайла). */
uint8_t pop_guard_state_x(uint8_t room)
{
if (room < 1 || room > 24) return 0;
return gstate[(uint16_t)(room - 1) * 6 + GS_X];
}
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir, uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill) uint8_t *color, uint8_t *skill)
{ {
const uint8_t *p; const uint8_t *g;
uint8_t t; uint8_t t;
if (!lvl_ok || room < 1 || room > 24) return 0; if (!lvl_ok || room < 1 || room > 24) return 0;
gfx_w0_map(lvl_page); /* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
p = (const uint8_t *)LVL_DATA_OFF; * pop_guard_state_save); цвет статичен, его читаем со страницы. */
t = p[BP_GTILE + room - 1]; g = gstate + (uint16_t)(room - 1) * 6;
t = g[GS_TILE];
if (t < 30) { if (t < 30) {
*tile = t; *tile = t;
*dir = (int8_t)p[BP_GDIR + room - 1]; *dir = (int8_t)g[GS_DIR];
*color = (uint8_t)(p[BP_GCOLOR + room - 1] & 0x0F); *skill = g[GS_SKILL];
*skill = p[BP_GSKILL + room - 1]; gfx_w0_map(lvl_page);
} *color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
gfx_w0_unmap(); gfx_w0_unmap();
}
return (uint8_t)(t < 30); return (uint8_t)(t < 30);
} }
+58 -1
View File
@@ -26,6 +26,36 @@
int pop_level_load(const char *path); int pop_level_load(const char *path);
void pop_level_free(void); void pop_level_free(void);
/* ---- Номер уровня и потабличные различия (levels_plan.md §1) -------- */
/* Порт current_level (seg000 load_lev_spr: current_level = next_level =
* level). Уровень 1..15; 0 демо-уровень, в скоуп не входит. Читают
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
extern uint8_t pop_current_level;
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
* pop_current_level. 0 OK, -1 файла нет / не влез (тогда
* pop_current_level и страница НЕ тронуты играем дальше на старом).
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
int pop_level_load_num(uint8_t n);
/* tbl_entry_pose (data.h:848): 1 — падение внутрь (ур. 1), 2 — вбегание
* (ур. 13), 0 разворот на месте. Индекс номер уровня. */
uint8_t pop_level_entry_pose(uint8_t n);
/* tbl_guard_hp (data.h:846) — база HP стража на уровне (get_guard_hp,
* seg002:0044: extrastrength[skill] + tbl_guard_hp[level]). */
uint8_t pop_level_guard_hp(uint8_t n);
/* tbl_guard_type (data.h:844): 0 обычный, 1 толстый, 2 скелет, 3 визирь,
* 4 тень, 1 стражей на уровне НЕТ (уровни 14/15). */
int8_t pop_level_guard_type(uint8_t n);
/* tbl_level_type (data.h:840): 0 = dungeon, 1 = palace. Пока читается
* только для контроля второй тайлсет это levels_plan.md §3. */
uint8_t pop_level_tileset(uint8_t n);
/* Извлечь комнату room (1..24) в массивы приложения (W2): /* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] тайлы комнаты (fg маскирован &0x1F, bg raw); * fg[30], bg[30] тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[6], lcol_bg[3] [0..2] правый столбец (col9) комнаты СЛЕВА * lcol_fg[6], lcol_bg[3] [0..2] правый столбец (col9) комнаты СЛЕВА
@@ -33,7 +63,8 @@ void pop_level_free(void);
* для коллизии на колонке 2 (Kid стоит В шве); * для коллизии на колонке 2 (Kid стоит В шве);
* rcol_fg[6] симметрично (col0 / col1); * rcol_fg[6] симметрично (col0 / col1);
* нет комнаты стена/0; * нет комнаты стена/0;
* below_fg[10] верхний ряд (row0) комнаты СНИЗУ («стена * below_fg[11] [0..9] верхний ряд (row0) комнаты СНИЗУ,
* [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА («стена
* вниз»); нет комнаты стена. * вниз»); нет комнаты стена.
* 0 OK, -1 уровень не загружен / room вне диапазона. */ * 0 OK, -1 уровень не загружен / room вне диапазона. */
int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg, int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
@@ -49,6 +80,20 @@ uint8_t pop_room_link(uint8_t room, uint8_t side);
* тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */ * тип анимируемого тайла в ЛЮБОЙ комнате, не только текущей. */
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos); uint8_t pop_level_tile(uint8_t room, uint8_t tilepos);
/* Записать тайл в ЖИВУЮ foretable уровня (порт curr_room_tiles[tp] = tile:
* do_pickup / remove_loose / loose_land). Изменение персистентно уровень
* лежит в ОЗУ-странице, а не в ПЗУ, то есть переживает выход из комнаты,
* как в оригинале. Отменяется только pop_level_reset_tiles. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile);
/* Рестарт уровня: вернуть ВСЕ тайлы в исходное (порт load_level, который
* play_level зовёт на каждой итерации в т.ч. после смерти Кида). */
void pop_level_reset_tiles(void);
/* Рестарт уровня: вернуть ВСЕХ стражей (тот же load_level + pos_guards).
* Убитый страж после respawn снова жив как в оригинале. */
void pop_level_reset_guards(void);
/* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений). /* Батч-доступ к странице уровня (один map/unmap окна 0 на много чтений).
* Между begin/end только pop_level_tile_raw, без рисования. */ * Между begin/end только pop_level_tile_raw, без рисования. */
void pop_level_access_begin(void); void pop_level_access_begin(void);
@@ -75,6 +120,18 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col);
/* Стартовая позиция уровня (блок @2112): комната / поза / направление. */ /* Стартовая позиция уровня (блок @2112): комната / поза / направление. */
/* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет. /* Страж комнаты: 1 = есть (заполнит tile/dir/color/skill), 0 = нет.
* Порт enter_guard (seg002:0112). */ * Порт enter_guard (seg002:0112). */
/* leave_guard (seg002:02F5): запомнить состояние стража комнаты в ЖИВОЙ
* копии массивов уровня. dead != 0 сохранить и последовательность, тогда
* при возврате в комнату страж поднимется трупом там же, где лёг.
* tile == 30 (или больше) «стража в комнате больше нет». */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead);
/* Последовательность запомненного стража (0 — поднимать стандартной
* стойкой, как у живого). */
uint16_t pop_guard_state_seq(uint8_t room);
uint8_t pop_guard_state_x(uint8_t room);
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir, uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill); uint8_t *color, uint8_t *skill);
+618 -111
View File
@@ -30,6 +30,9 @@
#include "pop_bg.h" #include "pop_bg.h"
#include "pop_state.h" #include "pop_state.h"
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */ #include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
#include "pop_level.h" /* pop_level_start_room — вход в дверь уровня */
#include "pop_guard.h" /* SWORD_2_DRAWN — ветка приземления с мечом */
#include "pop_tune.h" /* POP_ENABLE_JUMP_GRAB и прочие настройки */
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
#define TILE_EMPTY 0 #define TILE_EMPTY 0
@@ -46,6 +49,7 @@
#define TILE_POTION 10 /* зелье */ #define TILE_POTION 10 /* зелье */
#define TILE_SWORD 22 /* меч на полу */ #define TILE_SWORD 22 /* меч на полу */
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */ #define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
#define TILE_LEVELDOOR_LEFT 16 /* ЛЕВАЯ половина двери уровня (на ней modif) */
#define ACT_STAND 0 #define ACT_STAND 0
#define ACT_RUNJUMP 1 #define ACT_RUNJUMP 1
#define ACT_HANGCLIMB 2 #define ACT_HANGCLIMB 2
@@ -66,6 +70,7 @@
#define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */ #define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */
/* K4 — зацеп/вис/подтягивание/спуск */ /* K4 — зацеп/вис/подтягивание/спуск */
#define SEQ_8_GRAB_STRAIGHT 8 /* прыжок вверх и зацеп (прямо) */ #define SEQ_8_GRAB_STRAIGHT 8 /* прыжок вверх и зацеп (прямо) */
#define SEQ_9_GRAB_WHILE_JUMPING 9 /* зацеп ПРЯМО В ПРЫЖКЕ (улучшение SDLPoP) */
#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */ #define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */
#define SEQ_16_JUMP_UP_AND_GRAB 16 /* прыжок вверх и зацеп (нет пола сзади)*/ #define SEQ_16_JUMP_UP_AND_GRAB 16 /* прыжок вверх и зацеп (нет пола сзади)*/
#define SEQ_24_GRAB_FORWARD 24 /* прыжок вверх и зацеп (пол сзади) */ #define SEQ_24_GRAB_FORWARD 24 /* прыжок вверх и зацеп (пол сзади) */
@@ -78,6 +83,8 @@
#define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */ #define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */ #define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
#define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */ #define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */
#define SEQ_63_ACTIVE_AFTER_FALL 63 /* стойка С МЕЧОМ после приземления */
#define SEQ_81_FIGHTFALL 81 /* fightfall: падение из боевой стойки */
#define TILE_FLOOR 1 #define TILE_FLOOR 1
#define FRAME_109_CROUCH 109 #define FRAME_109_CROUCH 109
@@ -114,17 +121,29 @@ uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавше
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */ uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */ uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */ uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
* Смерть/рестарт уровня возвращают ровно его (do_startpos: hitp_max =
* hitp_curr = hitp_beg_lev), а по ПРОХОЖДЕНИИ уровня он подтягивается к
* достигнутому максимуму (hitp_beg_lev = hitp_max) так «жизнь» из большой
* склянки переносится на следующий уровень, а из мёртвых прогонов нет.
* Стартовое значение (custom->start_hitp = 3) ставит main через
* pop_kid_hp_newgame инициализатором нельзя: модуль банкованный. */
uint8_t hitp_beg_lev;
/* Дельта HP Кида за кадр (порт hitp_delta). Её ставит СВОРД-путь боёвки
* (guards.c), а применяет pop_do_delta_hp раз в кадр как в оригинале.
* Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
* проверен до боёвки, и переводить его на дельту без нужды рискованно. */
int8_t hitp_delta;
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */ static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */ static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
/* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для /* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
* которой сохранены флаги перекрытия, и сами флаги (бит0 = левая грань стены, * (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
* бит1 = правая). BUMP_COL_NONE данных нет (смена комнаты/старт). */ * с check_collisions. Сброс при смене комнаты coll_prev_row. */
#define BUMP_COL_NONE ((int8_t)-128) static void pop_coll_invalidate(void);
static int8_t bump_prev_col;
static uint8_t bump_prev_flags;
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого /* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
* соседа (за col0), g_lcol[3..5] = его col8 (колонка 2); g_rcol симметрично * соседа (за col0), g_lcol[3..5] = его col8 (колонка 2); g_rcol симметрично
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах * (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах
@@ -156,7 +175,27 @@ void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }
void pop_map_set_room(uint8_t room) __banked { g_room = room; } void pop_map_set_room(uint8_t room) __banked { g_room = room; }
void pop_kid_hp_reset(void) __banked { hitp_curr = 3; pop_kid_dead = 0; } /* do_startpos (seg003:0160): HP уровня = hitp_beg_lev, а не константа —
* иначе большая склянка теряется на границе уровня. */
void pop_kid_hp_reset(void) __banked
{
hitp_curr = hitp_max = hitp_beg_lev;
hitp_delta = 0;
pop_kid_dead = 0;
}
/* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */
void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; }
/* add_life (seg000:11EC) + set_health_life (seg000:1200): большая склянка
* поднимает ПОТОЛОК HP (до max_hitp_allowed = 10) и доливает до него. */
static void pop_kid_add_life(void)
{
uint8_t hpmax = (uint8_t)(hitp_max + 1);
if (hpmax > POP_MAX_HITP) hpmax = POP_MAX_HITP;
hitp_max = hpmax;
hitp_delta = (int8_t)(hitp_max - hitp_curr);
}
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */ /* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count) static uint8_t take_hp(uint8_t count)
@@ -175,7 +214,7 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и * недействительны (в оригинале prev_coll_room != curr_row_coll_room и
* бампа на кадре входа нет). */ * бампа на кадре входа нет). */
bump_prev_col = BUMP_COL_NONE; pop_coll_invalidate();
} }
static uint8_t get_tile(int8_t col, int8_t row) static uint8_t get_tile(int8_t col, int8_t row)
@@ -202,6 +241,16 @@ static uint8_t get_tile(int8_t col, int8_t row)
return g_curr_tile; return g_curr_tile;
} }
/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для
* случая room == текущая). Нужен лучу видимости стража
* (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и
* стражем, а оба по условию в одной (= отрисованной) комнате. Колонки за
* пределами 0..9 get_tile разрешает через швы здесь не используются. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked
{
return get_tile(col, row);
}
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и /* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
* идёт сквозь них (на шве уходит в соседнюю комнату; внутри комнаты просто * идёт сквозь них (на шве уходит в соседнюю комнату; внутри комнаты просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в * проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
@@ -321,6 +370,11 @@ static void determine_col(void)
Kid.curr_col = get_tile_div_mod_m7(dx_weight()); Kid.curr_col = get_tile_div_mod_m7(dx_weight());
} }
/* determine_col наружу — для pop_load_fram_det_col (pop_kid): порт связки
* load_frame + determine_col (seg006:0144), которую оригинал зовёт перед
* control(). См. разбор там. */
void pop_determine_col(void) __banked { determine_col(); }
/* расстояние до края тайла (для in_wall). */ /* расстояние до края тайла (для in_wall). */
static int distance_to_edge(int xpos) static int distance_to_edge(int xpos)
{ {
@@ -434,7 +488,22 @@ static void land(void)
* приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt, * приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt,
* seg003:0AFC). */ * seg003:0AFC). */
if (Kid.fall_y < 22) { if (Kid.fall_y < 22) {
/* seg005:176 — ВЕТКА С МЕЧОМ, и она не косметика. seq_17_soft_land
* заканчивается ВЕЧНЫМ циклом на кадре 109 (`jmp softland_crouch` в
* seqtbl), из которого выводит только control_crouched(). А
* control() при `Char.sword == sword_2_drawn` до него не доходит:
* управление уходит в control_with_sword, который знает лишь кадр
* 171 (стойка с мечом). То есть Кид, упавший на этаж С ВЫНУТЫМ
* МЕЧОМ, в этот присед садится НАВСЕГДА ровно то, что поймали на
* приёмке уровня 2 (BUG-LAND-SWORD-1). Оригинал такой позы не
* допускает: с мечом приземление даёт боевую стойку. */
if (Kid.sword == SWORD_2_DRAWN) {
seq = SEQ_63_ACTIVE_AFTER_FALL;
} else {
seq = SEQ_17_SOFT_LAND; seq = SEQ_17_SOFT_LAND;
}
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
is_guard_notice = 1;
} else { } else {
uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */ uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */
pop_kid_hurt = 1; pop_kid_hurt = 1;
@@ -447,6 +516,7 @@ static void land(void)
pop_kid_dead = 1; pop_kid_dead = 1;
return; return;
} }
is_guard_notice = 1; /* seg005:195 */
seq = SEQ_20_MEDIUM_LAND; seq = SEQ_20_MEDIUM_LAND;
} }
Kid.fall_x = Kid.fall_y = 0; Kid.fall_x = Kid.fall_y = 0;
@@ -458,11 +528,34 @@ static void land(void)
static void start_fall(void) static void start_fall(void)
{ {
uint8_t frame = Kid.frame, seq, tile; uint8_t frame = Kid.frame, seq, tile;
/* seg006:1044 первым делом убирает меч в ножны: дальше падением рулит
* не боевой диспетчер, а обычный. Без этого Kid летит «с клинком», и
* control_with_sword разбирает кадры падения как боевые. */
Kid.sword = SWORD_0_SHEATHED;
inc_curr_row(); inc_curr_row();
/* start_chompers() — чомперов ещё нет (L3-CHOMP). */
/* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */ /* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */
if (frame == 13) seq = SEQ_19_FALL; if (frame == 13) seq = SEQ_19_FALL;
else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP; else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP;
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP; else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
else if (frame >= 81 && frame < 86) {
/* сорвался, приземляясь после прыжка вверх: сдвиг ВПЕРЁД на 5 */
seq = SEQ_19_FALL;
Kid.x = (uint8_t)char_dx_forward(5);
pop_load_fram_det_col();
}
else if (frame >= 150 && frame < 180) {
/* Кадры С МЕЧОМ (150..179) — отступил в провал. У оригинала это
* ОТДЕЛЬНАЯ последовательность seq_81 (fightfall): падение строго
* вниз (set_fall(0,15)), а не seq_7 с дрейфом fall_x=1 из-за
* дрейфа наш Kid уезжал на ~тайл в сторону за время падения.
* Плюс сдвиг на 5 назад, если стоит у самой кромки лицом влево.
* Ветка стража (seq_82/83) не нужна: start_fall только Kid.
* TODO: droppedout=1 (guard_follows_kid_down, guards.c:296). */
if (Kid.direction < 0 && distance_to_edge_weight() <= 7)
Kid.x = (uint8_t)char_dx_forward(-5);
seq = SEQ_81_FIGHTFALL;
}
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */ else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
kid_set_seq(seq); kid_set_seq(seq);
pop_kid_play(); pop_kid_play();
@@ -537,6 +630,70 @@ static void check_grab(void)
} }
} }
static void make_loose_fall(int pos, uint8_t modifier);
/* check_grab_run_jump — ЗАЦЕП ПРЯМО В ПРЫЖКЕ, при Shift, зажатом ЗАРАНЕЕ.
*
* ВАЖНО: в оригинальной игре этого НЕТ. Это улучшение SDLPoP
* (`USE_JUMP_GRAB` + `fixes->enable_jump_grab`, seg006:1228), и там оно
* выключено, пока не включены fixes-and-enhancements. Ванильное поведение:
* зацепиться можно только в НАЧАЛЕ ПАДЕНИЯ (check_grab, кадры 102..105)
* то есть Shift надо успеть нажать уже в полёте. Пользователь ловил ровно
* это и просил «чтобы работало и при заранее зажатом Shift», поэтому
* улучшение портировано, но за выключателем POP_ENABLE_JUMP_GRAB
* (pop_tune.h): игровая сборка сможет вернуться к оригиналу.
*
* Условия оригинала: действие прыжок, кадр 22..23 (прыжок с места) или
* 39..41 (разбег-прыжок), «вперёд» ОТПУЩЕНО, «вверх» зажато. Дальше та
* же проба, что у check_grab, но персонаж не падает, а сразу ставится на
* кромку и играет seq_9.
*
* Упрощение против SDLPoP: там колонка захвата правится, когда тайл
* оказался в ЧУЖОЙ комнате (curr_room != Char.room) у нас модель
* однокомнатная, get_tile сам резолвит колонки за швом в координатах
* ТЕКУЩЕЙ комнаты, поэтому правка не нужна. Зато кнопка/плита за швом
* (grab_col вне 0..9) не сработают тайл принадлежит соседу, а
* pop_trob_modif/g_fg у нас про свою комнату; такие случаи пропускаем. */
static uint8_t check_grab_run_jump(void)
{
uint8_t frame = Kid.frame;
uint8_t grab_tile;
int8_t grab_col, grab_row;
/* Сам выключатель проверяют ТОЧКИ ВЫЗОВА (иначе SDCC ругается на
* недостижимый код при POP_ENABLE_JUMP_GRAB = 1). */
if (Kid.action != ACT_RUNJUMP) return 0;
if (!((frame >= 22 && frame <= 23) || (frame >= 39 && frame <= 41))) return 0;
if (control_x != POP_CONTROL_RELEASED || control_y != POP_CONTROL_HELD_UP) return 0;
if (!can_grab_front_above()) return 0;
grab_tile = g_curr_tile; /* тайл, за который взялись */
grab_col = g_infrontx;
grab_row = (int8_t)(Kid.curr_row - 1); /* СНЯТЬ до play_seq: он двигает ряд */
Kid.x = (uint8_t)(pop_x_bump[grab_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
Kid.x = (uint8_t)char_dx_forward(Kid.direction < 0 ? -12 : 2);
Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
kid_set_seq(SEQ_9_GRAB_WHILE_JUMPING);
pop_kid_play();
determine_col();
grab_timer = 12;
/* Хвост оригинала: за что схватились — то и сработает. Нужен потому,
* что check_press на следующем кадре уже не поможет: кадр сменится на
* 81 (hangdrop), если Shift отпустят сразу. */
if (grab_col >= 0 && grab_col <= 9 && grab_row >= 0 && grab_row <= 2) {
uint8_t tp = (uint8_t)(grab_row * 10 + grab_col);
if (grab_tile == TILE_OPENER || grab_tile == TILE_CLOSER)
pop_trigger_button(g_room, tp, grab_tile, pop_trob_modif(g_room)[tp]);
else if (grab_tile == TILE_LOOSE) {
is_guard_notice = 1;
make_loose_fall(tp, 1);
}
}
return 1;
}
static void do_fall(void) static void do_fall(void)
{ {
uint8_t nrow = (uint8_t)(Kid.curr_row + 1); uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
@@ -696,6 +853,47 @@ uint8_t pop_jump_up_seq(void) __banked
return jump_up_plain(); return jump_up_plain();
} }
/* run_jump (seg005:0AA8), часть «выровнять по кромке» ------------------ *
* Оригинал НЕ отталкивается откуда попало: перед разбег-прыжком он смотрит
* на 12 тайла вперёд и, если там провал (или пика), подгоняет X так, чтобы
* толчок пришёлся ровно на кромку пола. Иначе прыжок стартует в случайной
* фазе бегового цикла а запаса у seq_4 почти нет: суммарный dx кадров
* 34..44 равен 62 px при ширине тайла 14, то есть ровно 4 колонки и меньше
* половины тайла сверху. Поэтому без выравнивания провал в ТРИ пустых
* тайла (перелёт с колонки 5 на колонку 1) становится непроходимым
* BUG-RJUMP-1, уровень 2, комнаты 1 и 9.
*
* Возврат: 1 прыгать (Kid.x уже подогнан), 0 прыжок ОТМЕНИТЬ. Отмена
* в оригинале НЕ гасит control_up: Кид бежит дальше с зажатой «вверх», и на
* следующем кадре попытка повторяется так игрок ловит нужную фазу, просто
* удерживая клавишу. */
uint8_t pop_run_jump_align(void) __banked
{
int xpos = char_dx_forward(4);
int8_t col = get_tile_div_mod_m7(xpos);
uint8_t tf;
for (tf = 0; tf < 2; tf++) {
uint8_t t;
col = (int8_t)(col + dir_front[Kid.direction + 1]);
t = get_tile(col, Kid.curr_row);
if (t != TILE_SPIKE && tile_is_floor(t)) continue; /* тут пол — дальше */
{
int adj = distance_to_edge(xpos) + TILE_SIZEX * (int)tf - TILE_SIZEX;
/* Оригинал сравнивает БЕЗЗНАКОВО:
* if ((word)adj < (word)-8 || adj >= 2) { if (adj < 128) return; adj = -3; }
* что означает ровно «adj НЕ попал в [-8,-1] прыжка нет».
* Ветка с adj = -3 недостижима: distance_to_edge [0,13] и
* tf {0,1} дают adj [-14,13], а туда нужно adj >= 128. */
if (adj < -8 || adj > -1) return 0;
Kid.x = (uint8_t)char_dx_forward((int8_t)(adj + 4));
}
break;
}
return 1;
}
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига — /* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */ * позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
int pop_wall_ahead(void) __banked int pop_wall_ahead(void) __banked
@@ -761,6 +959,58 @@ uint8_t pop_hang_fall_seq(void) __banked
* либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 = * либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
* приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки * приседать. Меняет Kid.x. Kid развёрнут от края (край сзади) у кромки
* пола + под соседним тайлом есть за что зацепиться спуск на этаж вниз. */ * пола + под соседним тайлом есть за что зацепиться спуск на этаж вниз. */
/* Ветка «войти в дверь уровня» из up_pressed (seg005:0482) вместе с
* go_up_leveldoor (seg005:0574). Дверь рядом и открыта поставить Кида в
* проём, вернуть 1 (последовательность seq_70 ставит вызывающий это его
* слой); иначе 0 и Кид не тронут.
*
* Геометрию персонажа меняем в Kid, а НЕ в Char: окно Char вокруг
* диспетчера возвращает назад ТОЛЬКО curr_seq и sword
* (pop_savekid_state), поэтому во всём порте позицию правит pop_map как
* в pop_down_action и pop_jump_up_seq. Запись в Char.x/Char.direction
* отсюда молча теряется (проверено: Кид уходил из проёма влево).
*
* Порядок проб как в оригинале и с той же семантикой `else if`: берём
* ПЕРВУЮ клетку, в которой стоит дверь (под персонажем за спиной перед
* лицом), и уже её проверяем на открытость а не ищем «первую открытую».
*
* Открытость: `modifier >= 42`, то есть створка доехала до верха. Это
* ветка fix_exit_door, а не ванильный глобал leveldoor_open: с ванильным
* условием в дверь можно войти, пока она ещё ползёт вверх, и персонаж
* проходит сквозь опущенную створку.
*
* Стартовая комната исключена (`level.start_room != drawn_room`): в первой
* комнате уровня дверь та, через которую вошли, и обратно в неё нельзя. */
uint8_t pop_leveldoor_enter(void) __banked
{
int8_t cols[3];
uint8_t i;
if (pop_level_start_room() == g_room) return 0;
cols[0] = Kid.curr_col;
cols[1] = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
cols[2] = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
for (i = 0; i < 3; i++) {
if (get_tile(cols[i], Kid.curr_row) != TILE_LEVELDOOR_LEFT)
continue;
/* дверь найдена — дальше решает только её modif (см. выше) */
if (cols[i] < 0 || cols[i] > 9) return 0; /* дверь в шве: не наша */
if (pop_trob_modif(g_room)[Kid.curr_row * 10 + cols[i]] < 42)
return 0;
/* go_up_leveldoor (seg005:0574): встать РОВНО в проём и развернуться
* ВЛЕВО. Направление тут не косметика: все DX последовательности
* seq_70 ОТРИЦАТЕЛЬНЫЕ (5,4,4,2,3,1,3,1,2), а
* char_dx_forward при взгляде влево меняет им знак то есть именно
* dir_FF_left и гонит персонажа ВПРАВО-ВВЕРХ по лестнице. При
* взгляде вправо тот же seq уводит его влево, ИЗ проёма. */
Kid.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10);
Kid.direction = -1;
return 1;
}
return 0;
}
uint8_t pop_down_action(void) __banked uint8_t pop_down_action(void) __banked
{ {
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) { if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
@@ -793,6 +1043,9 @@ uint8_t pop_down_action(void) __banked
uint8_t pop_have_sword; uint8_t pop_have_sword;
uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */ uint8_t pop_item_taken; /* 0 = нет; иначе tilepos+1 поднятого */
uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */ uint8_t pop_flash_time; /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье). В оригинале это
* flash_color; нам хватает двух значений, которые реально встречаются. */
uint8_t pop_flash_red;
static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */ static int8_t pickup_obj_type; /* -1 = меч (порт pickup_obj_type) */
/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить /* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
@@ -801,6 +1054,13 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos)
{ {
pickup_obj_type = obj_type; pickup_obj_type = obj_type;
g_fg[tilepos] = TILE_FLOOR; g_fg[tilepos] = TILE_FLOOR;
/* curr_room_modif[curr_tilepos] = 0 (seg006:1676): у зелья в модификаторе
* лежал тип+фаза пузырька, у меча счётчик блеска. Тайл стал полом
* мусор в модификаторе не нужен (в исходное его вернёт только рестарт). */
{
uint8_t *m = pop_trob_modif(g_room);
if (m) m[tilepos] = 0;
}
pop_item_taken = (uint8_t)(tilepos + 1); pop_item_taken = (uint8_t)(tilepos + 1);
} }
@@ -816,7 +1076,34 @@ void pop_proc_get_object(void) __banked
* чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */ * чёрное поле экрана. У нас так же (gfx_pal_set 0), см. roomtest. */
pop_flash_time = 8; pop_flash_time = 8;
} }
pickup_obj_type = 0; /* зелья (типы 1..6) не портированы */ else {
/* proc_get_object (seg006:1927), ветка зелий. Типы 3/4/6
* (перо/переворот/открыть все ворота) свойства поздних уровней,
* их портируем вместе с этими уровнями. */
switch (pickup_obj_type) {
case 1: /* здоровье: +1 HP, красная вспышка */
if (hitp_curr != hitp_max) {
hitp_delta = 1;
pop_flash_red = 1;
pop_flash_time = 2;
}
break;
case 2: /* большая склянка: +1 к ПОТОЛКУ HP */
pop_kid_add_life();
pop_flash_red = 1;
pop_flash_time = 4; /* seg006:1878 — вспышка длиннее */
break;
case 5: /* «злое» зелье: −1 HP */
hitp_delta = -1;
pop_flash_red = 1;
pop_flash_time = 2;
break;
default: /* 3 перо / 4 переворот / 6 открыть —
* TODO вместе с их уровнями (7/9) */
break;
}
}
pickup_obj_type = 0;
} }
/* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет /* check_get_item + get_item (seg005:061F/073E). Возврат: 0 — предмета нет
@@ -845,9 +1132,17 @@ uint8_t pop_get_item_action(void) __banked
Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2)); Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
return 1; return 1;
} }
if (t != TILE_SWORD) return 0; /* питьё зелий не портировано */ {
do_pickup(-1, (uint8_t)(Kid.curr_row * 10 + icol)); uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol);
return 2; if (t == TILE_SWORD) {
do_pickup(-1, tp);
return 2; /* поднять меч (seq_91) */
}
/* Зелье: тип лежит в СТАРШИХ битах модификатора (get_item,
* seg005: do_pickup(curr_room_modif[curr_tilepos] >> 3)). */
do_pickup((int8_t)(pop_trob_modif(g_room)[tp] >> 3), tp);
return 3; /* пить (seq_78) */
}
} }
/* ---- Диспетчер ----------------------------------------------------- */ /* ---- Диспетчер ----------------------------------------------------- */
@@ -864,8 +1159,25 @@ static void check_on_floor(void)
static void check_action(void) static void check_action(void)
{ {
uint8_t action = Kid.action, frame = Kid.frame; uint8_t action = Kid.action, frame = Kid.frame;
/* Зацеп прямо в прыжке (улучшение SDLPoP, seg006:0914) — ДО всего
* остального: если получилось, кадр уже сменён на вис. */
if (POP_ENABLE_JUMP_GRAB && action == ACT_RUNJUMP &&
pop_ctrl_shift_held() && check_grab_run_jump())
return;
if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) { if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) {
if (frame == 109 || (frame >= 110 && frame <= 119)) /* ТОЛЬКО кадр 109 (присед) — как в ванильном seg006:909. Кадры
* вставания 110..119 сюда добавляет ОПЦИОНАЛЬНЫЙ FIX_STAND_ON_THIN_AIR,
* и он состоит из ТРЁХ частей: (1) эта ветка, (2) отмена падения,
* если в 2 px позади ещё пол (seg006:1079), (3) ПРАВКА seqtbl
* dx(1)dx(0) и dx(4)dx(3) в standup. У нас seqtbl извлечён из
* данных оригинала, третью часть не применить, а без неё вставание
* уносит весовую точку на 4 px вперёд в стену. Именно половинчатый
* порт этого фикса ронял Кида сквозь пол: комната 5, приземление с
* уроном на щебень (2,7) у стены (2,8), вставание колонка 8 = стена
* start_fall выпадение в комнату 6 (BUG-STANDUP-1). Ванильное
* поведение (кадр 109) от этого свободно; цена «встать на воздухе»
* у самой кромки, ровно как в оригинале 1989 года. */
if (frame == 109)
check_on_floor(); check_on_floor();
} else if (action == ACT_FREEFALL) { } else if (action == ACT_FREEFALL) {
do_fall(); do_fall();
@@ -885,9 +1197,13 @@ static void bumped_fall(void)
if (Kid.action == ACT_FREEFALL) { if (Kid.action == ACT_FREEFALL) {
Kid.fall_x = 0; Kid.fall_x = 0;
} else { } else {
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */ kid_set_seq(SEQ_45_BUMPFALL);
pop_kid_play(); pop_kid_play();
} }
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
* взводить обязаны см. BUG-GUARD-DEAF-1. */
is_guard_notice = 1;
} }
/* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ. /* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ.
@@ -911,7 +1227,23 @@ static void bumped_floor(void)
return; return;
} }
Kid.fall_y = 0; Kid.fall_y = 0;
if (Kid.action == ACT_BUMPED) return; /* уже в отскоке — не рестартить */ /* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара
* ставится ВСЕГДА, даже если персонаж уже в отскоке. Раньше здесь стоял
* наш guard `if (Kid.action == ACT_BUMPED) return;` («не рестартить») и
* он был корнем BUG-STANDUP-1:
* 1) вставание из приседа (medland) двигает Кида на 1 px в стену
* срабатывает бамп;
* 2) bumped_floor ПРИЖИМАЕТ y к полу (181), отменяя dy(2), который
* medland сделал в начале;
* 3) наш guard возвращает управление, medland продолжается и делает
* свои dy(+1), dy(+1) теперь уже ОТ уровня пола, то есть Кид
* оказывается НИЖЕ пола (y = 182);
* 4) следующий бамп видит «персонаж ниже пола» (беззнаковая разность
* заворачивается) и уводит его в bumped_fall seq_45 выпадение
* в комнату снизу.
* У оригинала шага (3) нет: seq_47 обрывает medland, и лишних dy не
* случается. */
if (!Kid.alive) return; /* труп — только выравнивание */
frame = Kid.frame; frame = Kid.frame;
/* Кадры прыжков и начала падения — «жёсткий» удар с приседанием /* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
* (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */ * (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
@@ -919,108 +1251,253 @@ static void bumped_floor(void)
(frame >= 102 && frame < 107)) (frame >= 102 && frame < 107))
kid_set_seq(SEQ_46_HARDBUMP); kid_set_seq(SEQ_46_HARDBUMP);
else else
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */ kid_set_seq(SEQ_47_BUMP);
pop_kid_play(); pop_kid_play();
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
} }
/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять /* ---- check_collisions (seg004:0004) -------------------------------- *
* к грани и отдать исход в bumped_floor/bumped_fall (порт bumped, seg004). * ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
* Это даёт «шаг назад со звуком» при беге/шаге в стену вместо падения. Уже * удара выбирается обходом 90 по переходу флага 01.
* в отскоке (action bumped) только выравниваем, без рестарта сиквенса. *
* В падении/висе не трогаем. */ * ПОЧЕМУ НЕ «колонка переднего края» (как было до 2026-08-03, BUG-COLL-1):
* колонка края попадает на стену лишь пока край внутри её габаритного окна,
* а окно узкое. Для решётки в col9 (x_bump[14]=184, coll_left=191, левая
* грань 191+10=201) это ровно 201..204 четыре пикселя. Беговой кадр
* сдвигает край больше чем на четыре, край перескакивает с 200 сразу на
* 205 (это уже колонка 10, соседняя комната, не стена) бампа нет, а
* check_leave тут же видит char_x_right 201 и уводит Кида СКВОЗЬ
* ЗАКРЫТУЮ РЕШЁТКУ. Флаг колонки 9 в оригинале взводится при любом
* перелёте, потому что сравнивается не колонка края, а КООРДИНАТЫ:
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
* Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал
* сквозь пол после приземления у стены (комн. 5).
*
* ТРИ РЯДА как в оригинале (было: один, и это оказалось BUG-JUMPWALL-1).
* Оригинал каждый кадр считает флаги для рядов row1, row, row+1, а
* move_coll_to_prev (seg004:00DF) в начале СЛЕДУЮЩЕГО кадра выбирает из них
* тот, что соответствует ряду прошлого кадра. Так «прошлые» флаги остаются
* настоящими и на кадре смены ряда.
*
* Наш прежний упрощённый вариант (prev = 3 = «уже перекрывал») подавлял бамп
* ровно на этом кадре а в падении ряд меняется чуть ли не каждый кадр, и
* переход 01 на стене приходится как раз на него. Симптом: недолетевший
* прыжок уходит СКВОЗЬ кладку по свободной траектории (ур. 3 комн. 14).
* Цена трёх рядов 3×14 get_tile на кадр вместо 14; замерено, что это
* дешевле любого из обходных вариантов, а поведение совпадает с оригиналом.
*
* Ряды вне комнаты (1 и 3) считаются штатно: get_tile сам резолвит ряд 1
* через комнату сверху (g_above), а ряд 3 отдаёт WALL то же «через край
* не бампим», что и раньше. */
/* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы
* СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка
* соседа обязана бампить так же, как своя (иначе Kid уходит сквозь
* закрытые ворота шва старый BUG-SEAM-PINGPONG). Оригинал делает то же
* самое: get_row_collision_data идёт от col(char_x_left)1 до
* col(char_x_right)+2 и резолвит комнату через find_room_of_tile, а
* координата грани (coll_tile_left_xpos) остаётся в системе ОТРИСОВАННОЙ
* комнаты как у нас pop_x_bump[col + FIRST_ONSCREEN_COLUMN]. */
#define COLL_C0 (-2) /* первая считаемая колонка */
#define COLL_N 14 /* колонки 2..11 */
#define COLL_IDX(c) ((c) - COLL_C0)
#define COLL_ROW_NONE ((int8_t)-128)
static uint8_t coll_prev[COLL_N], coll_curr[COLL_N];
static uint8_t coll_above[COLL_N], coll_below[COLL_N];
static int8_t coll_prev_row = COLL_ROW_NONE; /* ряд, к которому относится coll_prev */
/* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */
#define BUMP_NONE ((int8_t)-128)
static int8_t bump_col_l; /* bump_col_left_of_wall (Kid лицом ВПРАВО) */
static int8_t bump_col_r; /* bump_col_right_of_wall (Kid лицом ВЛЕВО) */
static int coll_xl, coll_xr; /* char_x_left_coll / char_x_right_coll */
/* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны.
* В оригинале это несовпадение prev_coll_room и curr_row_coll_room
* на кадре входа бампа нет. */
static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; }
/* set_char_collision (seg006:0723), горизонтальная часть: габарит персонажа
* в комнатных координатах. char_x_left = obj_x/2 + 58 (минус полуширина при
* взгляде вправо) это ровно наш char_x_forward_edge для переднего края. */
static void set_char_collision(void)
{
int e = char_x_forward_edge();
int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */
if (Kid.direction < 0) { coll_xl = e; coll_xr = e + wh; }
else { coll_xr = e; coll_xl = e - wh; }
if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; }
}
/* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж
* достал левую грань стены колонки», бит1 = «правую». Оригинал считает
* колонки от col(char_x_left)1 до col(char_x_right)+2 и оставляет прочие
* как есть; мы считаем все четырнадцать они дешёвые (индекс в массив +
* switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */
/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором
* персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент
* ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по
* разнице рядов. Условия дословно оригинальные, включая ±3: ряд может
* «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда
* геометрия колонок та же, что у текущего ряда.
*
* Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого
* ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */
static void move_coll_to_prev(int8_t row)
{
const uint8_t *src;
int8_t i;
if (coll_prev_row == COLL_ROW_NONE) {
for (i = 0; i < COLL_N; i++) coll_prev[i] = 3;
return;
}
if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row)
src = coll_curr; /* ряд не менялся */
else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row)
src = coll_above; /* поднялся на ряд */
else
src = coll_below; /* опустился на ряд */
for (i = 0; i < COLL_N; i++) coll_prev[i] = src[i];
}
static void get_row_collision_data(int8_t row, uint8_t *flags)
{
int8_t col;
for (col = COLL_C0; col < COLL_C0 + COLL_N; col++) {
uint8_t f = 0;
uint8_t wt = wall_type(get_tile(col, row));
if (wt) {
int coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
if (wall_dl[wt] + coll_left < coll_xr) f |= 1;
if (coll_left - wall_dr[wt] + TILE_RIGHTX > coll_xl) f |= 2;
}
flags[COLL_IDX(col)] = f;
}
}
static void check_collisions(void)
{
int8_t col;
bump_col_l = bump_col_r = BUMP_NONE;
/* seg004:0044 — на кадрах разворота коллизии не считаются вовсе. */
if (Kid.action == ACT_TURN) return;
/* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех
* колонках считать по нему нечего. Стену в падении держит
* do_fall/in_wall, выход из комнаты check_leave_below. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; }
set_char_collision();
move_coll_to_prev(Kid.curr_row);
coll_prev_row = Kid.curr_row;
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */
get_row_collision_data(Kid.curr_row, coll_curr);
get_row_collision_data((int8_t)(Kid.curr_row + 1), coll_below);
get_row_collision_data((int8_t)(Kid.curr_row - 1), coll_above);
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
* в которой флаг перешёл 01. */
for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) {
uint8_t p = coll_prev[COLL_IDX(col)], c = coll_curr[COLL_IDX(col)];
if (!(p & 1) && (c & 1)) bump_col_l = col;
if (!(p & 2) && (c & 2)) bump_col_r = col;
}
}
/* is_obstacle (seg004:037E): препятствие ли тайл колонки на самом деле.
* Побочно оставляет тайл в g_curr_tile (как curr_tile2 у оригинала). */
static uint8_t is_obstacle_at_col(int8_t col)
{
uint8_t t = get_tile(col, Kid.curr_row);
if (t == TILE_POTION) return 0; /* зелье не бампит */
if (t == TILE_GATE) return (uint8_t)!gate_passable(col, Kid.curr_row);
if (t == TILE_CHOMPER) /* закрыт только при modif==2 */
return (uint8_t)(col >= 0 && col <= 9 &&
pop_trob_modif(g_room)[Kid.curr_row * 10 + col] == 2);
/* ветка «прыжок сквозь зеркало» (seg004:239) — уровень 4, не портируем */
return 1;
}
/* bumped (seg004:0448). delta_x — сдвиг персонажа В КООРДИНАТАХ (не
* «вперёд»), push_dir куда толкает: <0 влево, >=0 вправо. Чем кончится
* удар, решает тайл, на котором персонаж оказался после отжатия: сквозь
* стену/верх двери он стоит на СОСЕДНЕЙ клетке (в сторону толчка), для
* прочих препятствий (ворота, зеркало, кусаки) на клетке препятствия. */
static void bumped(int delta_x, int8_t push_dir, int8_t tcol)
{
uint8_t ct;
if (Kid.alive >= 0) return; /* труп не бампит */
Kid.x = (uint8_t)((int)Kid.x + delta_x);
determine_col();
ct = g_curr_tile; /* тайл препятствия */
if (push_dir < 0) { /* толчок ВЛЕВО */
if (ct == TILE_WALL)
ct = get_tile((int8_t)(tcol - 1), Kid.curr_row);
} else { /* толчок ВПРАВО */
if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
ct = get_tile((int8_t)(tcol + 1), Kid.curr_row);
}
if (tile_is_floor(ct)) bumped_floor();
else bumped_fall();
determine_col();
}
/* check_bumped_look_right (seg004:030A): Kid смотрит ВПРАВО и достал ЛЕВУЮ
* грань стены отжать влево. */
static void check_bumped_look_right(void)
{
int8_t col = bump_col_l;
uint8_t wt;
int coll_left;
if (Kid.direction != 0) return; /* (меча в руке у нас нет) */
if (!is_obstacle_at_col(col)) return;
/* Прежде чем удариться о стену — попытка зацепиться за её верх
* (seg004:0205, то же улучшение). Не вышло тайл препятствия надо
* перечитать: проба зацепа сбила g_curr_tile. */
if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) {
if (check_grab_run_jump()) return;
if (!is_obstacle_at_col(col)) return;
}
wt = wall_type(g_curr_tile);
if (!wt) return;
coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
bumped(wall_dl[wt] + coll_left - coll_xr, -1, col);
}
/* check_bumped_look_left (seg004:02D2): Kid смотрит ВЛЕВО и достал ПРАВУЮ
* грань стены отжать вправо. */
static void check_bumped_look_left(void)
{
int8_t col = bump_col_r;
uint8_t wt;
int coll_left;
if (Kid.direction >= 0) return;
if (!is_obstacle_at_col(col)) return;
if (POP_ENABLE_JUMP_GRAB && pop_ctrl_shift_held()) { /* seg004:0186 */
if (check_grab_run_jump()) return;
if (!is_obstacle_at_col(col)) return;
}
wt = wall_type(g_curr_tile);
if (!wt) return;
coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
bumped(coll_left - wall_dr[wt] + TILE_RIGHTX - coll_xl, 0, col);
}
/* check_bumped (seg004:029D). */
static void check_bumped(void) static void check_bumped(void)
{ {
uint8_t t, wt; int8_t tcol; int d; /* Гарды РОВНО как в оригинале: вис и подтягивание. ПАДЕНИЕ И ПОЛЁТ НЕ
/* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы: * ИСКЛЮЧЕНЫ bumped_fall (seg004:04E4) специально разбирает случай
* action hang_climb/hang_straight + кадры подъёма 135..148 иначе * actions_4_in_freefall (гасит fall_x и отжимает на 4). Наш прежний
* climb-up у стены прерывается отскоком). FREEFALL наш guard: стену * guard `action == FREEFALL || MIDAIR -> return` и был вторым корнем
* в падении держит do_fall/in_wall. */ * BUG-COLL-1: длинный прыжок в стену (комната 6, между (0,6) и (0,7))
if (Kid.action == ACT_FREEFALL || Kid.action == ACT_HANGCLIMB || * ничем не останавливался, и Кид оказывался ВНУТРИ кладки. */
Kid.action == ACT_HANGSTRAIGHT || Kid.action == ACT_MIDAIR) return; if (Kid.action == ACT_HANGCLIMB || Kid.action == ACT_HANGSTRAIGHT) return;
if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */ if (Kid.frame >= 135 && Kid.frame < 149) return; /* кадры подъёма/спуска */
/* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb), /* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
* т.е. action ещё midair без этого guard'а bump у стены рвёт цепочку * т.е. action ещё midair без этого guard'а bump у стены рвёт цепочку
* hang->hang_fall->seq_11 (правильное приземление). */ * hang->hang_fall->seq_11 (правильное приземление). */
if (Kid.frame >= 87 && Kid.frame < 100) return; if (Kid.frame >= 87 && Kid.frame < 100) return;
/* Вне рядов комнаты (падение мимо пола): get_tile даёт WALL, а if (bump_col_l != BUMP_NONE) check_bumped_look_right();
* dist_from_wall_forward/pop_x_bump[] мусор с большим отриц. сдвигом else if (bump_col_r != BUMP_NONE) check_bumped_look_left();
* underflow X и уход в стену. fell_out ловит do_fall. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) return;
/* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются
* (bump_col_* сбрасываются и функция выходит) bump'а в развороте нет. */
if (Kid.action == ACT_TURN) return;
/* Стена ищется по колонке, в которую упирается ПЕРЕДНИЙ КРАЙ персонажа
* (порт seg004: bump_col_left/right_of_wall колонка, где край пересёк
* грань; НЕ curr_col!). Через curr_col было ДВА расхождения: (1) весовая
* точка кадра заезжает за грань раньше края, curr_col становился 1 на
* шве, ворота оказывались «под персонажем» и гард односторонней стены их
* снимал Kid уходил в соседнюю комнату СКВОЗЬ ОПУЩЕННУЮ РЕШЁТКУ;
* (2) тот же гард был нужен только из-за отсутствия проверки разворота
* (см. выше). Колонка переднего края даёт и то, и другое даром: стоя
* СПРАВА от ворот лицом вправо, край в соседней колонке ворота не
* бампят; заехав краем за их грань лицом влево бампят. */
tcol = get_tile_div_mod_m7(char_front_coll());
t = get_tile(tcol, Kid.curr_row);
wt = wall_type(t);
/* EDGE-триггер (порт check_collisions + get_row_collision_data, seg004:0004).
* Оригинал считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ направления:
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
* и зовёт bumped() ТОЛЬКО когда нужный флаг перешёл 0 -> 1 (взгляд вправо
* смотрит на 0x0F, влево на 0xF0). Без этого проверка «уровнем» ловит
* ЛЮБОЕ перекрытие: Kid, стоящий В проёме опущенной решётки, при развороте
* на месте получал ложный бамп и выпихивался сквозь неё в соседнюю
* комнату. С флагами от габарита разворот их не меняет бампа нет. */
{
uint8_t f = 0, prev, need;
if (wt) {
int coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */
int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */
int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1;
int xl, xr;
if (Kid.direction < 0) { xl = e; xr = e + wh; } /* габарит персонажа */
else { xr = e; xl = e - wh; }
if (kid_cur_flags() & FRAME_THIN) { xl += 4; xr -= 4; }
if (lw < xr) f |= 1;
if (rw > xl) f |= 2;
}
/* прошлый кадр: та же колонка — её флаги; другая — считаем, что не
* перекрывали; «нет данных» (смена комнаты) считаем, что перекрывали
* всё, чтобы на кадре входа бампа не было (в оригинале это даёт
* несовпадение prev_coll_room и curr_row_coll_room). */
prev = (bump_prev_col == BUMP_COL_NONE) ? 3u
: (bump_prev_col == tcol ? bump_prev_flags : 0u);
bump_prev_col = tcol;
bump_prev_flags = f;
if (!wt) return;
need = (Kid.direction < 0) ? 2u : 1u;
if (!(f & need) || (prev & need)) return; /* не перекрыл / уже перекрывал */
}
if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row))
return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */
d = dist_from_wall_forward(t, tcol);
if (d >= 0) return; /* ещё не зашёл за грань */
Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */
determine_col();
/* Хвост bumped (seg004:04E4): чем КОНЧИТСЯ удар, решает тайл, на котором
* персонаж оказался после отжатия от грани. Сквозь стену/верх двери он
* стоит на СОСЕДНЕЙ клетке (в сторону толчка), для прочих препятствий
* (ворота, зеркало, кусаки) на клетке самого препятствия (у ворот пол
* есть, поэтому tile_is_floor их считает полом). */
{
uint8_t ct = t;
if (Kid.direction < 0) { /* смотрит влево => толчок ВПРАВО */
if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
ct = get_tile((int8_t)(tcol + 1), Kid.curr_row);
} else { /* смотрит вправо => толчок ВЛЕВО */
if (ct == TILE_WALL)
ct = get_tile((int8_t)(tcol - 1), Kid.curr_row);
}
if (tile_is_floor(ct)) bumped_floor();
else bumped_fall();
}
determine_col();
} }
/* ---- Loose floors (seg007) ---------------------------------------- * /* ---- Loose floors (seg007) ---------------------------------------- *
@@ -1066,10 +1543,13 @@ static void check_press(void)
int8_t c = Kid.curr_col; int8_t c = Kid.curr_col;
if (g_above && g_link_u && c >= 0 && c <= 9 && if (g_above && g_link_u && c >= 0 && c <= 9 &&
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) && (g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
(int8_t)pop_ceil_modif[c] <= 0) (int8_t)pop_ceil_modif[c] <= 0) {
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */ pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
is_guard_notice = 1; /* seg006:1734 */
}
} else if (get_tile_above_char() == TILE_LOOSE) { } else if (get_tile_above_char() == TILE_LOOSE) {
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1); make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
is_guard_notice = 1; /* seg006:1734 */
} }
return; return;
} }
@@ -1083,8 +1563,10 @@ static void check_press(void)
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
if (t == TILE_OPENER || t == TILE_CLOSER) if (t == TILE_OPENER || t == TILE_CLOSER)
pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]); pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]);
else if (t == TILE_LOOSE) else if (t == TILE_LOOSE) {
make_loose_fall(tp, 1); make_loose_fall(tp, 1);
is_guard_notice = 1; /* seg006:1734 */
}
} }
} }
@@ -1124,7 +1606,8 @@ void pop_check_knock(void) __banked
/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest /* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
* индексируются позицией тайла и не должны утечь из старой комнаты в новую. * индексируются позицией тайла и не должны утечь из старой комнаты в новую.
* Плюс останавливает падающий кусок (pop_bg). */ * Уже ПАДАЮЩИЕ куски при этом не трогаем они привязаны к своей комнате и
* должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */
void pop_loose_reset(void) __banked void pop_loose_reset(void) __banked
{ {
int i; int i;
@@ -1137,7 +1620,7 @@ void pop_loose_reset(void) __banked
pop_ceil_fell = 0; pop_ceil_fell = 0;
pop_debris_at = 0; pop_debris_at = 0;
pop_loose_landed = 0; pop_loose_landed = 0;
pop_loose_mob_reset(); pop_loose_mob_room_changed();
} }
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге /* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
@@ -1281,6 +1764,21 @@ void pop_loose_tick(void) __banked
* check_spike_below колонки берутся get_tile_div_mod БЕЗ 7. * check_spike_below колонки берутся get_tile_div_mod БЕЗ 7.
* *
* Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */ * Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
/* Вторая половина ветки двери уровня из clip_char (seg006:1231):
* obj_clip_right = leveldoor_right ЭКРАННЫЙ X, правее которого спрайт не
* рисуется. 0 = обрезки нет.
*
* Почему обрезка, а не fore-слой: створка и косяк уходят в оригинале
* ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются
* ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
* поднимающегося по лестнице срезать сам спрайт. */
int pop_clip_char_right(void) __banked
{
if (Kid.frame >= 224 && Kid.frame < 229)
return pop_leveldoor_right;
return 0;
}
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
{ {
static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 }; static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
@@ -1290,6 +1788,14 @@ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
int8_t cL, cR, trow, clip_row; int8_t cL, cR, trow, clip_row;
uint8_t t; uint8_t t;
/* Кадры подъёма по лестнице двери уровня (clip_char, seg006:1231):
* персонаж режется по низу поднятой створки. ВНИМАНИЕ: комментарий в
* SDLPoP говорит «frames 217..228», а КОД проверяет
* `>= frame_224_exit_stairs_8`, то есть 224..228 портируем по коду.
* Правую кромку отдаёт pop_clip_char_right (парная половина ветки). */
if (Kid.frame >= 224 && Kid.frame < 229)
return pop_leveldoor_ybottom + 1;
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */ xr = xl + wh; /* char_x_right */
top_y = obj_y - (int)h + 1; /* char_top_y */ top_y = obj_y - (int)h + 1; /* char_top_y */
@@ -1489,6 +1995,7 @@ void pop_phys_tick(void) __banked
fall_accel(); fall_accel();
fall_speed(); fall_speed();
determine_col(); determine_col();
check_collisions(); /* seg004: флаги перекрытия по колонкам ряда */
check_bumped(); /* удержать у стены до check_action (порядок PoP) */ check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action(); check_action();
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */ check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
+40 -1
View File
@@ -30,13 +30,33 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
* Звать в enter_room. */ * Звать в enter_room. */
void pop_map_set_room(uint8_t room) __banked; void pop_map_set_room(uint8_t room) __banked;
/* Тайл-код текущей комнаты по клетке (col 0..9, row 0..2) — порт
* get_tile(room,col,row) для текущей комнаты. Нужен лучу видимости стража
* (guards.c): он идёт по ряду между Кидом и стражем. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked;
/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для
* pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */
void pop_determine_col(void) __banked;
/* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. /* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP.
* pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/
* респавн). */ * респавн). */
extern uint8_t pop_kid_dead; extern uint8_t pop_kid_dead;
extern uint8_t hitp_curr; extern uint8_t hitp_curr;
extern uint8_t hitp_max; /* стартовое HP (для индикатора) */
extern int8_t hitp_delta; /* урон за кадр от меча; применяет pop_do_delta_hp */
void pop_kid_hp_reset(void) __banked; void pop_kid_hp_reset(void) __banked;
/* hitp_beg_lev (seg003): HP, с которым НАЧАТ текущий уровень. Рестарт
* уровня (смерть/выпадение) откатывает HP ровно к нему; пройденный уровень
* подтягивает его к hitp_max (play_level_2, seg003:410) так «жизнь» из
* большой склянки переезжает на следующий уровень. Правит главный цикл. */
extern uint8_t hitp_beg_lev;
#define POP_START_HITP 3 /* custom->start_hitp */
#define POP_MAX_HITP 10 /* custom->max_hitp_allowed */
void pop_kid_hp_newgame(void) __banked; /* начало ИГРЫ: hitp_beg_lev = 3 */
/* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object). /* Предметы (seg005 check_get_item/get_item, seg006 do_pickup/proc_get_object).
* pop_have_sword «меч у Кида» (порт have_sword). pop_item_taken 0 или * pop_have_sword «меч у Кида» (порт have_sword). pop_item_taken 0 или
* tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл * tilepos+1 только что поднятого предмета: вызывающий обязан запечь тайл
@@ -46,7 +66,9 @@ extern uint8_t pop_item_taken;
/* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока /* Вспышка фона (do_flash/set_bg_attr, seg001:0908): счётчик кадров, пока
* НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */ * НУЛЕВАЯ запись палитры подменена ярко-жёлтым. Тикает вызывающий. */
extern uint8_t pop_flash_time; extern uint8_t pop_flash_time;
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч */ /* Цвет вспышки: 0 = ярко-жёлтая (подобран меч), 1 = красная (зелье). */
extern uint8_t pop_flash_red;
uint8_t pop_get_item_action(void) __banked; /* 0 нет / 1 присесть / 2 поднять меч / 3 пить */
void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */ void pop_proc_get_object(void) __banked; /* эффект — из play_seq (опкод GET_ITEM) */
/* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для /* Уход из комнаты ВБОК/ВВЕРХ: 0=нет; 1=left,2=right,3=up (= side+1 для
@@ -113,6 +135,10 @@ void pop_phys_tick(void) __banked;
* Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */ * Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */
int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked; int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked;
/* Парная половина clip_char: ЭКРАННЫЙ X, правее которого спрайт не рисуется
* (правый косяк двери уровня в кадрах подъёма 224..228). 0 = обрезки нет. */
int pop_clip_char_right(void) __banked;
/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после /* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после
* pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */ * pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */
#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */ #define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */
@@ -128,6 +154,13 @@ uint8_t pop_edge_type(void) __banked;
* control_running (чистый стоп у стены). */ * control_running (чистый стоп у стены). */
int pop_wall_ahead(void) __banked; int pop_wall_ahead(void) __banked;
/* Разбег-прыжок: выравнивание по кромке пола (run_jump, seg005:0AA8).
* Смотрит на 12 тайла вперёд; если там провал/пика подгоняет Kid.x под
* толчок ровно с кромки. 1 = прыгать, 0 = ОТМЕНИТЬ прыжок (Кид не в той
* фазе относительно кромки; control_up гасить НЕЛЬЗЯ попытка повторится
* на следующем кадре). Без этого провал в три тайла непроходим. */
uint8_t pop_run_jump_align(void) __banked;
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО /* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x. * прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
* check_jump_up/grab_up (K4.4). control_up гасит caller. */ * check_jump_up/grab_up (K4.4). control_up гасит caller. */
@@ -142,4 +175,10 @@ int8_t pop_hang_wall(void) __banked; /* Shift в висе: 1=у сте
uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */ uint8_t pop_hang_fall_seq(void) __banked; /* seq_23 / seq_11 (сорваться), +Kid.x */
uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */ uint8_t pop_down_action(void) __banked; /* 1=обработано(спуск/отступ), 0=присед */
/* Дверь уровня рядом и ОТКРЫТА ПОЛНОСТЬЮ? → поставить Кида в проём (x +
* разворот влево, go_up_leveldoor) и вернуть 1; иначе 0, Кид не тронут.
* Ветка двери уровня из up_pressed (seg005:0482). Последовательность
* seq_70 ставит вызывающий (pop_ctrl) тайлы наши, управление его. */
uint8_t pop_leveldoor_enter(void) __banked;
#endif #endif
+1
View File
@@ -59,6 +59,7 @@ void pop_redraw_needed(void)
case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break; case POP_RD_LOOSE_GONE: pop_loose_bake_empty(row, col); break;
case POP_RD_FLOOR: pop_floor_bake(row, col); break; case POP_RD_FLOOR: pop_floor_bake(row, col); break;
case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break; case POP_RD_LEVELDOOR: pop_leveldoor_redraw(row, col); break;
case POP_RD_GATE: pop_gate_redraw(row, col); break;
default: break; default: break;
} }
if (--rd_cnt[i]) left++; if (--rd_cnt[i]) left++;
+1
View File
@@ -29,6 +29,7 @@
#define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */ #define POP_RD_LOOSE_GONE 3 /* loose провалился: запечь пусто */
#define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */ #define POP_RD_FLOOR 4 /* пол изменился: кнопка/щебень/поднятый меч */
#define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */ #define POP_RD_LEVELDOOR 5 /* створка двери уровня (ПРАВАЯ половина) */
#define POP_RD_GATE 6 /* решётка ворот (tilepos = САМИ ворота) */
/* Виды перерисовки полосы у потолка (по колонке). */ /* Виды перерисовки полосы у потолка (по колонке). */
#define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */ #define POP_RDA_CEIL 1 /* плита-потолок: дрожащий кадр / покой */
+45
View File
@@ -11,5 +11,50 @@
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки. * Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
*/ */
#include "pop_state.h" #include "pop_state.h"
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_loose_landed; uint8_t pop_loose_landed;
/* ---- Шина управления (seg006 globals) ------------------------------ *
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка
* pop_ctrl (банк 5) пишет с клавиатуры, guards.c (банк 1) подаёт
* синтетический ввод ИИ (autocontrol_*, seg002), pop_map (банк 3) читает
* через pop_ctrl_shift_held. Сегодня оно уцелело бы и в pop_ctrl.c
* (собираем без --bank-data, данные банков остаются в _DATA), но это
* флаг сборки: включат его и шина уедет в страницу банка 5.
*
* control_x/y/shift «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 конечный
* автомат held/released/ignore (авто-повтор). */
int8_t control_x, control_y, control_shift;
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
/* Габариты последней нарисованной двери уровня (порт leveldoor_right /
* leveldoor_ybottom): пишет draw_leveldoor из банка pop_bg, читает
* clip_char из pop_map. См. pop_state.h. */
int pop_leveldoor_right;
int pop_leveldoor_ybottom;
/* ---- Отладочная «пустышка» для брейкпоинтов из БАНКОВ ---------------- *
* Зачем. PC-брейкпоинт видит только логический адрес, а 0xC000+ это
* окно, куда мапятся ВСЕ банки: точка на адресе банковой функции ловит
* заодно чужой код, случайно легший по тому же смещению (проверено:
* точка на start_fall из банка 3 срабатывала на каждом кадре попадала
* в pop_bg из банка 2). Условные брейкпоинты этот отладчик MAME не
* поддерживает («error in assignment expression» на `==`).
*
* Приём (идея пользователя, 2026-08-04): позвать ЭТУ функцию ровно из
* того места банкового кода, которое отлаживаем, и поставить брейкпоинт
* на неё она резидентна в W1, её адрес однозначен. По возврату из неё
* читаем что нужно. Условие «когда именно ловить» пишется обычным `if`
* в C это гибче любых выражений отладчика.
*
* Счётчик нужен, чтобы вызов не выкинул оптимизатор; заодно видно, сколько
* раз точка прошла, если ловим не останавливаясь. */
uint8_t pop_dbg_hits;
void pop_dbg_trap(void) { pop_dbg_hits++; }
+21
View File
@@ -14,4 +14,25 @@
* разбирает логика loose-полов (pop_map, W1/W2). */ * разбирает логика loose-полов (pop_map, W1/W2). */
extern uint8_t pop_loose_landed; extern uint8_t pop_loose_landed;
/* Чит «бессмертие» (I): Кида бьют, но HP не убавляется. Читают пути урона
* (боёвка в банке, пики в pop_map), переключает главный цикл. */
extern uint8_t pop_immortal;
/* Габариты ПОСЛЕДНЕЙ нарисованной двери уровня — порт глобалов
* leveldoor_right / leveldoor_ybottom (seg008 draw_leveldoor). Их ставит
* отрисовка (банк pop_bg), а читает clip_char (pop_map, W1/W2): кадры
* подъёма по лестнице (224..228) обрезаются по правому косяку проёма и по
* низу поднятой створки. Дверь рисуется ПОД персонажем, поэтому перекрыть
* его не может оригинал именно режет спрайт.
* right экранный X правой кромки; ybottom room-space Y (без POP_YOFF).
* 0 = двери в отрисованной комнате нет. */
extern int pop_leveldoor_right;
extern int pop_leveldoor_ybottom;
/* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
* _pop_dbg_trap (резидент W1, адрес однозначен). */
extern uint8_t pop_dbg_hits;
void pop_dbg_trap(void);
#endif #endif
+55 -17
View File
@@ -13,6 +13,8 @@
* пометки, поэтому pop_process_trobs единственная функция модуля, которую * пометки, поэтому pop_process_trobs единственная функция модуля, которую
* нельзя звать из __banked-кода. */ * нельзя звать из __banked-кода. */
#include "pop_bg.h" #include "pop_bg.h"
#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */
#include "pop_tune.h" /* POP_DBG_GATE_HOLD — отладочное растягивание окна */
/* Коды тайлов (fg & 0x1F), которые анимируются. */ /* Коды тайлов (fg & 0x1F), которые анимируются. */
#define TILE_SPIKE 0x02 #define TILE_SPIKE 0x02
@@ -21,6 +23,7 @@
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */ #define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
#define TILE_OPENER 0x0F #define TILE_OPENER 0x0F
#define TILE_POTION 0x0A #define TILE_POTION 0x0A
#define TILE_LOOSE 0x0B /* проваливающийся пол (фаза тряски в modif) */
#define TILE_SWORD 0x16 /* меч-предмет на полу */ #define TILE_SWORD 0x16 /* меч-предмет на полу */
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */ #define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
#define TILE_TORCH 0x13 #define TILE_TORCH 0x13
@@ -47,12 +50,12 @@ static uint8_t trob_drawn[POP_ROOMTILES];
/* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и /* prandom (seg009.c:321, 32-бит LCG) — нужен для стартовой фазы факела и
* выбора следующего кадра пламени. */ * выбора следующего кадра пламени. */
static unsigned long trob_seed; static pop_rnd_t trob_seed;
#define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv)) #define trob_prandom(maxv) pop_prandom(&trob_seed, (maxv))
void pop_trob_reset(void) void pop_trob_reset(void) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0; for (i = 0; i < POP_NROOMS; i++) room_seen[i] = 0;
@@ -61,18 +64,37 @@ void pop_trob_reset(void)
trobs_count = 0; trobs_count = 0;
} }
uint8_t *pop_trob_modif(uint8_t room) uint8_t *pop_trob_modif(uint8_t room) __banked
{ {
if (room < 1 || room > POP_NROOMS) return 0; if (room < 1 || room > POP_NROOMS) return 0;
if (!room_seen[room - 1]) { if (!room_seen[room - 1]) {
uint8_t i; uint8_t i;
pop_level_room_bg(room, room_modif[room - 1]); pop_level_room_bg(room, room_modif[room - 1]);
/* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 — /* load_alter_mod (seg008:198E): модификатор В ФАЙЛЕ уровня и
* старшие биты = тип зелья, младшие 3 = фаза пузырька. */ * модификатор В РАНТАЙМЕ разные величины, оригинал переводит их
* при загрузке. Порт того, что нас касается:
*
* ворота 1 = «открыты» (Table 8 спецификации DAT) -> 188, то есть
* рабочая высота подъёма; ЛЮБОЕ другое значение -> 0
* (закрыты). Без этого перевода ворота, которые в
* уровне открыты, стартуют закрытыми, и кнопка-closer
* рядом с ними теряет смысл (ур. 2, комн. 13).
* loose -> 0: фаза тряски всегда начинается с нуля.
* зелье -> <<3: старшие биты = тип, младшие 3 = фаза пузырька.
*
* Ветка стен (blue-line/связи кладки) НЕ портируется намеренно: у
* нас pop_bg считает связи по типам СОСЕДЕЙ прямо при отрисовке
* (wall_modifier), сохранённый модификатор стены не читается вовсе. */
pop_level_access_begin(); pop_level_access_begin();
for (i = 0; i < POP_ROOMTILES; i++) for (i = 0; i < POP_ROOMTILES; i++) {
if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION) uint8_t *m = &room_modif[room - 1][i];
room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3); switch (pop_level_tile_raw(room, i) & 0x1F) {
case TILE_GATE: *m = (uint8_t)((*m == 1) ? 188 : 0); break;
case TILE_LOOSE: *m = 0; break;
case TILE_POTION: *m = (uint8_t)(*m << 3); break;
default: break;
}
}
pop_level_access_end(); pop_level_access_end();
room_seen[room - 1] = 1; room_seen[room - 1] = 1;
} }
@@ -87,7 +109,7 @@ static int find_trob(uint8_t room, uint8_t tilepos)
return -1; return -1;
} }
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
{ {
int f = find_trob(room, tilepos); int f = find_trob(room, tilepos);
if (f >= 0) { trobs[f].type = type; return; } if (f >= 0) { trobs[f].type = type; return; }
@@ -98,7 +120,7 @@ void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type)
trobs_count++; trobs_count++;
} }
void pop_trob_room_changed(void) void pop_trob_room_changed(void) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0; for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
@@ -109,7 +131,7 @@ void pop_trob_room_changed(void)
* тайлов: факелам случайная стартовая фаза пламени (start_anim_torch, * тайлов: факелам случайная стартовая фаза пламени (start_anim_torch,
* seg007:0847: modif = prandom(8)), зельям случайная фаза пузырька * seg007:0847: modif = prandom(8)), зельям случайная фаза пузырька
* (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */ * (start_anim_potion: младшие 3 бита = prandom(6)+1, старшие = тип). */
void pop_trob_anim_room(uint8_t room) void pop_trob_anim_room(uint8_t room) __banked
{ {
uint8_t *mod = pop_trob_modif(room); uint8_t *mod = pop_trob_modif(room);
uint8_t i, code; uint8_t i, code;
@@ -259,13 +281,20 @@ static void do_trigger_list(uint8_t index, uint8_t button_type)
} }
void pop_trigger_button(uint8_t room, uint8_t tilepos, void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier) uint8_t button_type, uint8_t modifier) __banked
{ {
uint8_t link_timer = get_doorlink_timer(modifier); uint8_t link_timer = get_doorlink_timer(modifier);
if (link_timer != 0x1F) { /* не заклинено */ if (link_timer != 0x1F) { /* не заклинено */
set_doorlink_timer(modifier, 5); /* Оригинал ставит 5; вынесено в pop_tune.h, потому что проверять
if (link_timer < 2) * руками проход в закрывающуюся решётку при таком окне почти
pop_add_trob(room, tilepos, 1); /* кнопка нажимается (+ TODO sound) */ * невозможно (см. BUG-GATE-PASS-1). */
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
if (link_timer < 2) {
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
/* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у
* нас нет, флаг обязан быть (BUG-GUARD-DEAF-1). */
is_guard_notice = 1;
}
do_trigger_list(modifier, button_type); do_trigger_list(modifier, button_type);
} }
} }
@@ -337,7 +366,7 @@ static void animate_door(uint8_t *m, int8_t *type)
*m = mod; *m = mod;
} }
uint8_t pop_trob_room_active(uint8_t room) uint8_t pop_trob_room_active(uint8_t room) __banked
{ {
uint8_t i; uint8_t i;
for (i = 0; i < trobs_count; i++) for (i = 0; i < trobs_count; i++)
@@ -362,7 +391,7 @@ static uint8_t torch_tick;
#define TORCH_ANIM_NOW 1 #define TORCH_ANIM_NOW 1
#endif #endif
void pop_process_trobs(uint8_t cur_room) void pop_process_trobs(uint8_t cur_room) __banked
{ {
uint8_t i, w = 0; uint8_t i, w = 0;
#if TORCH_ANIM_DIV > 1 #if TORCH_ANIM_DIV > 1
@@ -460,6 +489,15 @@ void pop_process_trobs(uint8_t cur_room)
* страницы: живой trob метит каждый кадр по одной. */ * страницы: живой trob метит каждый кадр по одной. */
pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1)); pop_set_redraw(tp, POP_RD_SPIKE, (uint8_t)(type < 0 ? 2 : 1));
} }
if (room == cur_room && code == TILE_GATE) {
/* draw_trob (seg007:01E6), которым заканчивается animate_door:
* пока trob жив решётка едет каждый кадр, помечаем ТЕКУЩУЮ
* страницу; последний кадр (type<0, ворота встали) ОБЕ, иначе
* на второй странице дабл-буфера застынет предыдущее положение
* и решётка дрожит через кадр. Тайлпос САМИ ворота, соседа
* справа выбирает pop_gate_redraw. */
pop_set_redraw(tp, POP_RD_GATE, (uint8_t)(type < 0 ? 2 : 1));
}
if (room == cur_room && code == TILE_LEVELDOOR) { if (room == cur_room && code == TILE_LEVELDOOR) {
/* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там /* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там
* её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе * её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе
+8 -8
View File
@@ -25,41 +25,41 @@
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и /* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
* очистить список trob. */ * очистить список trob. */
void pop_trob_reset(void); void pop_trob_reset(void) __banked;
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт. /* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня * При ПЕРВОМ обращении лениво инициализируется из backtable уровня
* (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне * (pop_level_room_bg). Далее ЖИВЁТ (не перезагружается). room вне
* диапазона 0. */ * диапазона 0. */
uint8_t *pop_trob_modif(uint8_t room); uint8_t *pop_trob_modif(uint8_t room) __banked;
/* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить /* Добавить/обновить trob (room, tilepos, type). Если уже есть — сменить
* type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */ * type (как SDLPoP add_trob/find_trob). type<0 = будет удалён. */
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type); void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked;
/* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать /* Продвинуть все trob на кадр (animate_* по коду тайла из уровня), записать
* modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице. * modif, перерисовать тайлы ТЕКУЩЕЙ (cur_room) комнаты на back-странице.
* Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */ * Звать в основном цикле ПОСЛЕ set_draw_page(back), рядом с loose. */
void pop_process_trobs(uint8_t cur_room); void pop_process_trobs(uint8_t cur_room) __banked;
/* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку /* Кид встал на кнопку (opener/closer): взвести таймер связи + обойти цепочку
* LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button. * LINKLOC, добавив trob целям (ворота). Порт seg007 trigger_button.
* room/tilepos кнопка; button_type её тайл-код; modifier её modif * room/tilepos кнопка; button_type её тайл-код; modifier её modif
* (= индекс в LINKLOC). Звать из check_press (pop_map). */ * (= индекс в LINKLOC). Звать из check_press (pop_map). */
void pop_trigger_button(uint8_t room, uint8_t tilepos, void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier); uint8_t button_type, uint8_t modifier) __banked;
/* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через /* Есть ли активные trob в комнате room? Для перерисовки ворот соседа через
* шов (roomtest не дёргает шов, пока сосед не анимируется). */ * шов (roomtest не дёргает шов, пока сосед не анимируется). */
uint8_t pop_trob_room_active(uint8_t room); uint8_t pop_trob_room_active(uint8_t room) __banked;
/* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты /* Смена комнаты: сбросить отложенные перерисовки «покоя» текущей комнаты
* (trob-состояние в room_modif персистентно и НЕ трогается). Звать в * (trob-состояние в room_modif персистентно и НЕ трогается). Звать в
* enter_room. */ * enter_room. */
void pop_trob_room_changed(void); void pop_trob_room_changed(void) __banked;
/* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы, /* anim_tile_modif (seg000:0B12): завести анимации тайлов комнаты (факелы,
* зелья) со случайной стартовой фазой. Звать в enter_room. */ * зелья) со случайной стартовой фазой. Звать в enter_room. */
void pop_trob_anim_room(uint8_t room); void pop_trob_anim_room(uint8_t room) __banked;
#endif #endif
+66
View File
@@ -0,0 +1,66 @@
/*
* pop_tune.h НАСТРАИВАЕМЫЕ КОНСТАНТЫ движка (аналог custom_options
* SDLPoP) в одном месте.
*
* Зачем отдельный заголовок. У оригинала «особые события» уровней зашиты
* магическими числами прямо в код (`current_level == 3 && Char.room == 7`),
* и SDLPoP вынес их в структуру `custom_options_type` (types.h) с
* загрузкой из SDLPoP.ini и правкой через меню Settings/Mods. Нам нужно
* то же самое по двум причинам:
*
* 1. ОТЛАДКА. Некоторые сцены руками почти не проверить (успеть в
* закрывающуюся решётку), и единственный вменяемый способ временно
* растянуть тайминг. Такие крутилки обязаны жить в одном месте, а не
* прятаться числами по трём модулям.
* 2. МОДЫ. Тот, кто захочет свой набор уровней, меняет ровно эти числа.
*
* Пока это только `#define` ни INI, ни меню нет (см. TASKS.md, запись
* TUNE-1). Но правило уже действует: НОВОЕ «особое событие» или тайминг
* заводится ЗДЕСЬ, а не константой по месту. Имена как у SDLPoP
* (custom->имя), чтобы сверка с оригиналом оставалась механической.
*/
#ifndef POP_TUNE_H
#define POP_TUNE_H
/* ---- Чекпойнт уровня 3 (SDLPoP: checkpoint_*) ----------------------- *
* Оригинал: level3_set_chkp (seg002:0665) взводит флаг, когда Кид уходит
* ВЛЕВО ИЗ комнаты 7 (вызов стоит до goto_other_room, поэтому Char.room там
* ещё старая); do_startpos (seg003:0141) подменяет по
* нему стартовую позицию и убирает loose-плиту, через которую туда
* попадают (иначе после респавна дорога назад была бы уже провалена). */
#define POP_CHKP_LEVEL 3 /* checkpoint_level */
#define POP_CHKP_ROOM 7 /* комната, ИЗ КОТОРОЙ уходят влево (у
* SDLPoP хардкод; это НЕ комната
* назначения см. roomtest.c) */
#define POP_CHKP_RESPAWN_ROOM 2 /* checkpoint_respawn_room */
#define POP_CHKP_RESPAWN_TILEPOS 6 /* checkpoint_respawn_tilepos */
#define POP_CHKP_RESPAWN_DIR (-1) /* checkpoint_respawn_dir: влево */
#define POP_CHKP_CLEAR_ROOM 7 /* checkpoint_clear_tile_room */
#define POP_CHKP_CLEAR_COL 4 /* checkpoint_clear_tile_col */
#define POP_CHKP_CLEAR_ROW 0 /* checkpoint_clear_tile_row */
/* ---- Отладочные крутилки (в оригинале и в SDLPoP их НЕТ) ------------ *
* POP_DBG_GATE_HOLD сколько кадров решётка держится поднятой после
* нажатия кнопки, ПРЕЖДЕ чем пойдёт вниз. В оригинале это таймер связи
* дверей: trigger_button ставит 5, animate_button вычитает по единице, и
* решётка стоит, пока таймер > 1 (то есть ~4 кадра после того, как Кид
* сошёл с кнопки). Проверять руками проход в закрывающуюся решётку при
* таком окне почти невозможно, поэтому значение вынесено сюда: поставь
* 30 и успеешь дойти спокойно. ОБЯЗАН быть 5 в игровой сборке.
*
* ПОТОЛОК 30. Таймер связи пять бит модификатора кнопки (маска 0x1F), а
* значение 31 у оригинала означает «событие заклинено» (`link_timer != 0x1F`
* в trigger_button). Больше 30 ставить нельзя кнопка перестанет
* срабатывать вовсе. */
#define POP_DBG_GATE_HOLD 5
/* ---- Улучшения SDLPoP (в ОРИГИНАЛЕ их нет) -------------------------- *
* POP_ENABLE_JUMP_GRAB зацепиться за кромку можно ПРЯМО В ПРЫЖКЕ, если
* Shift зажат заранее (SDLPoP `enable_jump_grab`, seg006:1228). Ваниль
* разрешает зацеп только в начале падения (`check_grab`, кадры 102..105),
* то есть Shift приходится нажимать уже в полёте это и раздражало на
* приёмке. 0 как в оригинале, 1 как в SDLPoP с включёнными
* enhancements. */
#define POP_ENABLE_JUMP_GRAB 1
#endif /* POP_TUNE_H */
+388 -72
View File
@@ -16,8 +16,10 @@
#include "pop_level.h" #include "pop_level.h"
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_redraw.h" #include "pop_redraw.h"
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_guard.h" #include "pop_guard.h"
#include "pop_cheat.h" #include "pop_cheat.h"
#include "pop_tune.h"
__sfr __at (0xFE) io_border; __sfr __at (0xFE) io_border;
@@ -46,23 +48,58 @@ __sfr __at (0xFE) io_border;
#define PROF(n) ((void)0) #define PROF(n) ((void)0)
#endif #endif
/* Сколько банков грузит crt0_banked (читает его стартовый код). */ /* Сколько банков грузит crt0_banked (читает его стартовый код).
const uint8_t n_banks = 3; * 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP),
* 5 = pop_ctrl (диспетчер управления).
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
* вплотную (16 021 из 16 384), а расти ему ещё есть куда тайлы поздних
* уровней, чомперы, зеркало.
*
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть это не ошибка
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
const uint8_t n_banks = 6;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */ /* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats; uint8_t pop_cheats;
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
*
* При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
* одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание тоже
* 5). Приёмный FIFO SIO 3 байта, а импульс запроса прерывания здесь
* теряется примерно в 44 % случаев, поэтому байты копятся и один из
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
*
* Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько
* вызовов за кадр они попадают туда, где прерывания и так разрешены, и
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя машина
* уходит в перезагрузку.
*
* Что помогает: опрос раз в ~0.5 мс. Столько времени берётся из ожидания
* кадра процессор всё равно крутит там ~42 мс из 60, ничего не делая.
* Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
* kbd_idle ниже); полезной работы это не отнимает. Потолок приёма
* подтверждён отдельно (tests/kbdpoll: 25 нажатий 25, 250 байт из 250). */
/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
static void kbd_idle(void)
{
kbd_raw_poll();
}
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ /* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2 #define SEQ_STAND 2
#define ROOM_1 1 #define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4 #define FIRST_LEVEL 1 /* с какого уровня начинается игра */
* row2, шахта col3). Kid на полу row0 col1, идёт вправо падает в шахту
* пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */
#define START_ROOM 1 /* обход комнат: старт с 1-й, дальше +/- (ROOMNAV) */
#define START_COL 3
#define START_ROW 0
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
@@ -73,6 +110,7 @@ uint8_t pop_cheats;
* Убирается закомментированием ОДНОЙ строки ниже. */ * Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1 #define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы /* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */ * чтобы enter_room переключал комнату без длинного списка параметров. */
@@ -80,7 +118,7 @@ static uint8_t room_fg[30], room_bg[30];
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0), /* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и * [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка 2 / 11). См. pop_level.h. */ * смотрит наружу (колонка 2 / 11). См. pop_level.h. */
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[10]; static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */ static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
@@ -98,6 +136,89 @@ static void update_kid_render_dx(void)
pop_kid_set_render_dx(dx); pop_kid_set_render_dx(dx);
} }
/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) -----------------------
*
* В оригинале никто не «поверх» по определению: и Кид, и страж попадают в
* midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а
* тайлы обходятся в фиксированном порядке redraw_needed_tiles (seg008:1B06)
* идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ тот
* поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или
* выше, его тайл в обходе позже. Совпали тайлы решает sort_curr_objs
* (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже.
*
* Мы рисовали стражКид всегда, то есть Кид был поверх безусловно.
*
* Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 (2 - row). */
#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col))
/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */
static int8_t tile_div_mod(int x)
{
int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */
int c = v / TILE_SIZEX;
if (v % TILE_SIZEX < 0) --c;
return (int8_t)c;
}
/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита.
* ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к
* *_coll, а колонки char_col_left/right считает по этой. */
static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx,
uint8_t flags, uint16_t w)
{
int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */
uint8_t fl = flags;
if (dir < 0) fl ^= 0x80;
/* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */
if (!(fl & 0x80) && e <= 57) e++;
if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */
return e;
}
/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа.
*
* Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил
* сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ
* колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому
* бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров
* бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих:
* enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт
* дал бы перекос в другую сторону. */
static void objtile_at_char(int8_t *row, int8_t *col,
const pop_char_t *ch, int8_t dx, int8_t dy,
uint8_t flags, uint16_t w)
{
int8_t r = ch->curr_row, c = ch->curr_col;
if (ch->action == 1) { /* actions_1_run_jump */
int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy));
if (br == -1) br = 3; /* char_bottom_row */
r = br;
c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w));
if (c < 0) c = 0; /* MAX(char_col_left, 0) */
}
if ((ch->frame >= 135 && ch->frame < 149) ||
ch->action == 2 || ch->action == 3 ||
ch->action == 4 || ch->action == 6) c--;
*row = r; *col = c;
}
/* 1 = страж рисуется ПОСЛЕ Кида (поверх). */
static uint8_t guard_over_kid(void)
{
int8_t kr, kc, gr, gc;
int kk, gk;
if (Guard.charid == 0 || Guard.room != cur_room) return 0;
objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(),
kid_cur_flags(), kid_fp_width());
objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
pop_gframe.flags, pop_guard_fp_width());
kk = OBJ_ORDER_KEY(kr, kc);
gk = OBJ_ORDER_KEY(gr, gc);
if (gk != kk) return (uint8_t)(gk > kk);
/* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */
return (uint8_t)((int)Guard.y + pop_gframe.dy > kid_fp_obj_y());
}
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый /* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта * draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт modifier стабилен * реально изменился. Пока Кид держит кнопку и гейт открыт modifier стабилен
@@ -111,17 +232,15 @@ static uint8_t seam_redraw; /* оставшиеся кадры редроя (
static uint8_t room_label_sig = 0xFF; static uint8_t room_label_sig = 0xFF;
static uint8_t room_label_redraw; static uint8_t room_label_redraw;
/* Персистентные переопределения тайлов уровня: упавший loose-кусок оставил /* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
* щебень (floor debris), а на месте самой плиты пусто (в т.ч. в комнате * куска, поднятый предмет пол) пишутся ПРЯМО В УРОВЕНЬ
* СВЕРХУ: провалившийся потолок = колодец). Минимальный per-room override: * pop_level_set_tile, как в оригинале (curr_room_tiles[] = ). Раньше тут
* живёт между переходами, применяется в enter_room поверх загруженных тайлов * жила таблица переопределений на 8 записей, и девятая правка молча
* (и для ряда 2 верхнего соседа). (Полноценное per-room modifier-состояние * терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* в docs/gates_spikes_plan.md, P0.) */ * комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E #define TILE_DEBRIS 0x0E
#define TILE_EMPTY 0x00 #define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01 #define TILE_FLOOR 0x01
#define OVR_MAX 8
static uint8_t ovr_room[OVR_MAX], ovr_pos[OVR_MAX], ovr_tile[OVR_MAX], ovr_cnt;
/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам. /* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
* flash_on вспышка фона активна (нулевая запись палитры подменена). */ * flash_on вспышка фона активна (нулевая запись палитры подменена). */
@@ -137,36 +256,15 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
gfx_pal_set(1, 0, r, g, b); gfx_pal_set(1, 0, r, g, b);
} }
static void add_tile_override(uint8_t room, uint8_t pos, uint8_t tile)
{
uint8_t i;
for (i = 0; i < ovr_cnt; i++)
if (ovr_room[i] == room && ovr_pos[i] == pos) { ovr_tile[i] = tile; return; }
if (ovr_cnt < OVR_MAX) {
ovr_room[ovr_cnt] = room; ovr_pos[ovr_cnt] = pos;
ovr_tile[ovr_cnt] = tile; ovr_cnt++;
}
}
/* Применить переопределения комнаты room к массиву тайлов: base = tilepos
* первого элемента (0 для полной комнаты, 20 для ряда 2 верхнего соседа). */
static void apply_overrides(uint8_t room, uint8_t *tiles, uint8_t base, uint8_t n)
{
uint8_t i;
for (i = 0; i < ovr_cnt; i++)
if (ovr_room[i] == room && ovr_pos[i] >= base && ovr_pos[i] < base + n)
tiles[ovr_pos[i] - base] = ovr_tile[i];
}
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
* активной страницу 1 вызывающий восстанавливает нужную draw-страницу. */ * активной страницу 1 вызывающий восстанавливает нужную draw-страницу. */
static void enter_room(uint8_t room) static void enter_room(uint8_t room)
{ {
uint8_t pg; uint8_t pg;
pop_guard_leave(); /* запомнить стража уходящей комнаты */
uint8_t *room_mod; uint8_t *room_mod;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
apply_overrides(room, room_fg, 0, 30); /* щебень/пустота от упавших плит */
pop_map_set(room_fg); pop_map_set(room_fg);
pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
pop_room_link(room, 2), pop_room_link(room, 3), pop_room_link(room, 2), pop_room_link(room, 3),
@@ -181,7 +279,6 @@ static void enter_room(uint8_t room)
pop_level_access_begin(); pop_level_access_begin();
for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
pop_level_access_end(); pop_level_access_end();
apply_overrides(up, above_fg, 20, 10); /* провалившийся потолок = колодец */
for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
pop_room_set_above(above_fg, above_mod); pop_room_set_above(above_fg, above_mod);
pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */ pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */
@@ -191,10 +288,16 @@ static void enter_room(uint8_t room)
} }
} }
pop_map_set_room(room); pop_map_set_room(room);
/* Kid.room — как в оригинале: «комната персонажа». Полная модель
* (Kid.room отрисованной у шва) ещё не портирована, но сравнение
* Kid.room == Guard.room нужно уже сейчас: без него луч видимости
* стража никогда не срабатывает. */
Kid.room = room;
pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */
pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */
pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */
pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ pop_guard_enter(room); /* страж комнаты (порт enter_guard) */
pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */
/* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
* per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
* а не по свежему backtable. Фаза P0. */ * а не по свежему backtable. Фаза P0. */
@@ -222,6 +325,119 @@ static void enter_room(uint8_t room)
pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */ pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
} }
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
* (seg003:028A) поставить Кида в стартовую позицию УРОВНЯ. Один вход и
* для запуска, и для рестарта после смерти / выпадения из уровня: в
* оригинале это буквально один и тот же путь (play_level_2 зовёт
* start_level заново), поэтому и у нас не три копии четырёх строк.
*
* Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define:
* start_pos тайл 0..29 (row*10+col), start_dir направление, в котором
* персонаж должен ОКАЗАТЬСЯ.
*
* Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж
* ставится спиной и доворачивается кадрами позы входа. Сама поза из
* tbl_entry_pose (data.h:848 = {0,1,0,,2,}):
* 1 (уровень 1) падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2);
* это оно захлопывает решётку за спиной в первой комнате;
* 2 (уровень 13) вбегание (seq_84_run);
* иначе разворот на месте (seq_5_turn).
* Таблица индексируется НОМЕРОМ УРОВНЯ pop_level_entry_pose(). */
#define SEQ_TURN 5
#define SEQ_FALL 7
#define SEQ_RUN 84
#define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */
#define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */
#define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */
/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ —
* это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью
* (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72).
* Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки:
* его стартовый тайл (комната 5, ряд 1, колонка 3) правая половина
* той самой двери. */
static void find_start_level_door(uint8_t room)
{
uint8_t *m = pop_trob_modif(room);
uint8_t tp;
if (!m) return;
/* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз
* за уровень, а pop_add_trob сам лезет в W0. */
for (tp = 0; tp < 30; tp++) {
if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue;
m[tp] = 43; /* створка поднята полностью */
pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */
}
}
/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7
* уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет
* стартовую позицию. Сбрасывается началом игры и прохождением уровня
* ровно как в оригинале (seg003:37 и seg003:413). */
static uint8_t pop_checkpoint;
static void pop_start_level(void)
{
uint8_t entry_pose = pop_level_entry_pose(pop_current_level);
uint8_t start_room = pop_level_start_room();
uint8_t pos = pop_level_start_pos();
int8_t dir = (int8_t)~pop_level_start_dir();
uint8_t seq = SEQ_TURN;
/* Особое событие: старт с чекпойнта (do_startpos, seg003:0141).
* Числа в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles()
* ниже, иначе рестарт её вернёт. */
uint8_t chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint);
if (chkp) {
start_room = POP_CHKP_RESPAWN_ROOM;
pos = POP_CHKP_RESPAWN_TILEPOS;
dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную
* и разворачивается seq_5 */
}
/* Рестарт уровня = load_level() заново (play_level, seg003:57): в
* исходное возвращаются И модификаторы (пики/ворота), И сами тайлы
* разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и
* убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */
pop_trob_reset();
pop_level_reset_tiles();
pop_level_reset_guards();
pop_guard_reset(); /* start_level: Guard.direction = none */
pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */
/* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в
* комнату-чекпойнт после респавна остался бы непройденным. */
if (chkp)
pop_level_set_tile(POP_CHKP_CLEAR_ROOM,
(uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL),
TILE_EMPTY);
/* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos
* отрабатывает раньше первого draw_room), иначе шов кадр показывал бы
* ещё не тронутое состояние решётки. */
if (entry_pose == ENTRY_POSE_FALL) {
/* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5,
* col2, row0 её же типом и модификатором (в оригинале и то и другое
* берётся из curr_* после get_tile). */
uint8_t tp = 2; /* row0*10 + col2 */
pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]);
seq = SEQ_FALL;
} else if (entry_pose == ENTRY_POSE_RUN) {
seq = SEQ_RUN;
}
/* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть
* открыта уже в первом кадре (в оригинале find_start_level_door стоит
* между do_startpos и draw_level_first). */
find_start_level_door(start_room);
enter_room(start_room);
kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir);
Kid.sword = 0; /* set_start_pos: sword_0_sheathed */
/* play_level (seg003:106): have_sword = level >= have_sword_from_level
* (=2) начиная со второго уровня меч у Кида уже есть. */
pop_have_sword = (uint8_t)(pop_current_level >= 2);
pop_kid_hp_reset();
}
int main(void) int main(void)
{ {
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1 /* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
@@ -232,7 +448,8 @@ int main(void)
#ifdef ROOMNAV #ifdef ROOMNAV
uint8_t nav_prev = 0; uint8_t nav_prev = 0;
#endif #endif
uint8_t cheat_prev = 0; uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
uint8_t xdec_prev = 0, xinc_prev = 0;
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */ if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed"); puts("pop_bg_load failed");
@@ -250,11 +467,13 @@ int main(void)
puts("kid atlas load failed"); puts("kid atlas load failed");
return 1; return 1;
} }
if (pop_level_load("LEVELS\\res2001.bin") != 0 && /* сырой уровень 1 в EMM-страницу */ if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */
pop_level_load("a:\\res2001.bin") != 0) {
puts("level load failed"); puts("level load failed");
return 1; return 1;
} }
pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
pop_checkpoint = 0; /* init_game (seg003:37) */
initgraph(); initgraph();
gfx_set_visible_page(0); gfx_set_visible_page(0);
@@ -268,15 +487,11 @@ int main(void)
* ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */ * ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить /* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
* trob/room_modif (ленивая инициализация из уровня при первом входе). */ * trob/room_modif (ленивая инициализация из уровня при первом входе). */
pop_trob_reset(); /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
enter_room(START_ROOM); * pop_start_level. Управление: / бег, присед, + прыжок с места,
* Shift шаг, ESC выход. Карта коллизии задана внутри enter_room. */
pop_start_level();
gfx_set_draw_page(0); gfx_set_draw_page(0);
/* K2b: Kid стоит на уступе (row1), управляется с клавиатуры.
* / бег, присед, + прыжок с места, Shift шаг (K3). ESC выход.
* Карта коллизии (pop_map_set) уже задана в enter_room. */
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
pop_kid_hp_reset();
kid_draw(); kid_draw();
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(), kid_fp_width(), kid_fp_height(),
@@ -287,6 +502,7 @@ int main(void)
puts("kbd_raw open failed"); puts("kbd_raw open failed");
return 1; return 1;
} }
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
while (!kbd_raw_down(KBD_ESC)) { while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
@@ -309,11 +525,47 @@ int main(void)
* стоит, можно спокойно разбирать позу Kid и окклюзию. */ * стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0; if (kbd_raw_down(0x1E)) frozen = 0;
{ /* Читы (seg000: switch под if (cheats_enabled)). K — убить { /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* стража; по фронту нажатия, чтобы не срабатывало каждый кадр. */ * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K убить стража;
* I бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор);
* Shift+L следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* [ / ] подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
* персонажа в этом порте задаёт не диспетчер (грабли
* L1-EXIT). Колонку не трогаем её пересчитает физика
* следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL); uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
if (pop_cheats && k && !cheat_prev) pop_guard_kill(); if (pop_cheats && k && !cheat_prev) pop_guard_kill();
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k; cheat_prev = k;
immo_prev = i;
sword_prev = s;
nlvl_prev = nl;
xdec_prev = xd;
xinc_prev = xi;
} }
if (frozen) { if (frozen) {
PROF(0); /* чёрный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
@@ -324,8 +576,17 @@ int main(void)
#ifdef ROOMNAV #ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд, /* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) назад. Edge по nav_prev: одно нажатие = одна * '-' (0x7B / 0x4E) назад. Edge по nav_prev: одно нажатие = одна
* комната. pop_trob_reset ДО enter_room каждая комната смотрится в * комната.
* исходном состоянии (пики убраны, ворота закрыты). */ *
* Здесь БЫЛ pop_trob_reset() перед enter_room «чтобы каждая комната
* смотрелась в исходном состоянии». Убран: reset обнуляет room_seen,
* и модификаторы перечитываются из уровня заново, то есть чит
* ОТМАТЫВАЕТ МИР открытые/закрытые ворота, выдвинутые пики, нажатые
* кнопки. Пока ворота с bg=1 ошибочно стартовали закрытыми, откат был
* не виден; после порта load_alter_mod он выглядит как баг «ворота
* снова открылись» (уровень 1, решётка комнаты 5 (0,9), которую
* закрывает стартовый триггер do_startpos). Навигация обязана только
* телепортировать; исходное состояние даёт перезапуск уровня. */
{ {
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) | uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1)); ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
@@ -333,7 +594,6 @@ int main(void)
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF; uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1) r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
: (uint8_t)(r == 1 ? 24 : r - 1); : (uint8_t)(r == 1 ? 24 : r - 1);
pop_trob_reset();
enter_room(r); enter_room(r);
/* Предпочитаем обычный пол; иначе — любой tile_is_floor /* Предпочитаем обычный пол; иначе — любой tile_is_floor
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */ * (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
@@ -356,6 +616,7 @@ int main(void)
back = dbuf ? (uint8_t)(front ^ 1) : 0; back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back); gfx_set_draw_page(back);
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */ pop_ctrl_tick(); /* ввод -> control(): смена seq */
kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */
pop_guard_heal(); /* и прошлый кадр стража */ pop_guard_heal(); /* и прошлый кадр стража */
@@ -364,22 +625,42 @@ int main(void)
kid_tick(); /* play_seq: следующий кадр */ kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */ pop_phys_tick(); /* K3: падение/приземление/стена */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */ pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting();
pop_check_sword_hurt();
pop_do_delta_hp();
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */ if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0; pop_fell_out = 0;
if (down) { /* L2: переход в комнату снизу — продолжает падать */ if (down) { /* L2: переход в комнату снизу — продолжает падать */
enter_room(down); /* координаты Kid уже репроецированы pop_map */ enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */ gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */
} else { /* выпал из уровня (нет комнаты снизу) — респавн */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */ pop_start_level();
enter_room(START_ROOM);
gfx_set_draw_page(back); gfx_set_draw_page(back);
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
pop_kid_hp_reset();
} }
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */ } else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1)); uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
uint8_t left = (uint8_t)(pop_leave_dir == 1);
pop_leave_dir = 0; pop_leave_dir = 0;
/* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал
* зовёт его из leave_room ДО goto_other_room, то есть Char.room
* там ещё СТАРАЯ. Значит условие `Char.room == 7` читается как
* «Кид УХОДИТ ИЗ комнаты 7 влево» (в комнату 2), а не «входит в
* неё». Смысл: чекпойнт даётся за то, что комната 7 пройдена
* НАСКВОЗЬ, и респавн ставится туда, куда Кид только что попал.
*
* Сначала я поставил проверку на комнату НАЗНАЧЕНИЯ и это
* поймал пользователь прогоном в SDLPoP: он умер В комнате 7, но
* влево из неё не выходил, и оригинал вернул его в стартовую
* комнату 9, а плита осталась цела. Заодно тут запоминается
* достигнутый потолок HP: смерть после чекпойнта его не отбирает. */
if (left && pop_current_level == POP_CHKP_LEVEL &&
cur_room == POP_CHKP_ROOM) {
pop_checkpoint = 1;
hitp_beg_lev = hitp_max;
}
if (nb) { if (nb) {
enter_room(nb); enter_room(nb);
gfx_set_draw_page(back); gfx_set_draw_page(back);
@@ -410,16 +691,35 @@ int main(void)
} }
} }
} }
if (pop_next_level != pop_current_level) {
/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
* цикл уровня кончается, когда next_level разошёлся с
* current_level (END_LEVEL после seq_70 в двери уровня либо чит
* Shift+L), и play_level грузит следующий.
*
* HP переносится: hitp_beg_lev = hitp_max ДО загрузки так
* поднятая на уровне «жизнь» (большая склянка) переезжает
* дальше, а урон нет (do_startpos дольёт до потолка). */
hitp_beg_lev = hitp_max;
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else {
/* Всё состояние прошлого уровня сносит сам pop_start_level:
* pop_trob_reset обнуляет room_seen (модификаторы комнат
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
* из неё же, loose чистит enter_room. */
pop_start_level();
}
gfx_set_draw_page(back);
}
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */ if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++; dead_frames++;
if (dead_frames > RESPAWN_SETTLE && if (dead_frames > RESPAWN_SETTLE &&
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) { (kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
dead_frames = 0; dead_frames = 0;
pop_trob_reset(); /* рестарт уровня: пики/ворота в исходное */ pop_start_level();
enter_room(START_ROOM);
gfx_set_draw_page(back); gfx_set_draw_page(back);
kid_init(SEQ_STAND, START_COL, START_ROW, 0);
pop_kid_hp_reset();
} }
} }
if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */ if (pop_loose_fell) { /* упавший кусок → debris в комнате снизу */
@@ -427,26 +727,26 @@ int main(void)
uint8_t fcol = (uint8_t)(fpos % 10); uint8_t fcol = (uint8_t)(fpos % 10);
uint8_t down = pop_room_link(cur_room, LINK_DOWN); uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_loose_fell = 0; pop_loose_fell = 0;
add_tile_override(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */ pop_level_set_tile(cur_room, fpos, TILE_EMPTY); /* сама плита исчезла */
if (down) { if (down) {
int8_t lrow = pop_room_col_landing(down, fcol); int8_t lrow = pop_room_col_landing(down, fcol);
if (lrow >= 0) add_tile_override(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS); if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
} }
} }
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */ if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1); uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
uint8_t up = pop_room_link(cur_room, 2); uint8_t up = pop_room_link(cur_room, 2);
pop_ceil_fell = 0; pop_ceil_fell = 0;
if (up) add_tile_override(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */ if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
} }
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */ if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
add_tile_override(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS); pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
pop_debris_at = 0; pop_debris_at = 0;
} }
if (pop_item_taken) { /* предмет поднят → тайл стал полом */ if (pop_item_taken) { /* предмет поднят → тайл стал полом */
item_pos = (uint8_t)(pop_item_taken - 1); item_pos = (uint8_t)(pop_item_taken - 1);
item_bake = 2; /* запечь на ОБЕИХ страницах */ item_bake = 2; /* запечь на ОБЕИХ страницах */
add_tile_override(cur_room, item_pos, TILE_FLOOR); pop_level_set_tile(cur_room, item_pos, TILE_FLOOR);
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */ pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
pop_item_taken = 0; pop_item_taken = 0;
} }
@@ -463,8 +763,12 @@ int main(void)
* Приоритет как в оригинале: активный flash_time > кадр урона * Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */ * (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) { if (pop_flash_time) {
flash_bg(255, 255, 85); /* color_14_brightyellow */ /* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
* оригинала; нам хватает этих двух). */
if (pop_flash_red) flash_bg(255, 0, 0); /* color_4_red */
else flash_bg(255, 255, 85); /* color_14_brightyellow */
pop_flash_time--; pop_flash_time--;
if (!pop_flash_time) pop_flash_red = 0;
flash_on = 1; flash_on = 1;
} else if (pop_kid_hurt) { } else if (pop_kid_hurt) {
flash_bg(255, 85, 85); /* color_12_brightred */ flash_bg(255, 85, 85); /* color_12_brightred */
@@ -472,15 +776,26 @@ int main(void)
} }
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */ PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_guard_draw(); /* страж — ДО Kid (порядок как в оригинале) */ { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). */
uint8_t g_after = guard_over_kid();
if (!g_after) pop_guard_draw();
PROF(6); /* kid_draw (тот же циан) */ PROF(6); /* kid_draw (тот же циан) */
kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */
if (g_after) pop_guard_draw();
}
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */
kid_draw_splash(); kid_draw_splash();
pop_kid_hurt = 0; pop_kid_hurt = 0;
} }
pop_hp_draw(); /* полоса HP — только при изменении */
PROF(6); /* fore поверх Kid + борта */ PROF(6); /* fore поверх Kid + борта */
/* Окно fore-клипа общее, и pop_guard_draw ставит своё. При «страж
* поверх» он отрабатывает ПОСЛЕ kid_draw, поэтому окно Кида
* восстанавливаем явно иначе передние грани у Кида отсекаются и
* он рисуется поверх столбов. */
kid_fore_clip_restore();
pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
kid_fp_width(), kid_fp_height(), kid_fp_width(), kid_fp_height(),
Kid.direction, Kid.frame, Kid.action); Kid.direction, Kid.frame, Kid.action);
@@ -531,6 +846,7 @@ int main(void)
} }
} }
gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */
pop_ctrl_close(); pop_ctrl_close();
closegraph(); closegraph();
pop_bg_free(); pop_bg_free();
@@ -0,0 +1 @@
build/
@@ -0,0 +1,52 @@
# Модульные тесты движка roomtest под ucsim_z80. Обвязка общая (testkit/),
# здесь — только сами наборы и список модулей, которые в них линкуются.
TESTKIT := $(abspath $(CURDIR)/../../../../testkit)
ENGINE_DIR := $(abspath $(CURDIR)/..)
# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
CODE_LOC := 0x1000
# Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/
# sprinter: под тестом эти функции заглушены (stubs.c), но объявления
# берутся настоящие — иначе разъедутся сигнатуры.
PROJ_ROOT := $(abspath $(TESTKIT)/..)
EXTRA_INC := -I$(PROJ_ROOT)/libc/include -I$(PROJ_ROOT)/libbgi/include -I$(CURDIR)
OBJS_geom := build/eng_pop_geom.rel
OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
build/eng_pop_map.rel build/eng_pop_ctrl.rel \
build/eng_pop_state.rel \
build/stubs.rel build/scene.rel build/kid_blob.rel \
build/bank_stub.rel
# Пробный набор (разведка, без эталонов) линкуется тем же составом.
OBJS_grab := $(OBJS_phys)
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
OBJS_wall := $(OBJS_phys)
include $(TESTKIT)/host-tests.mk
# --- t_phys: блоб таблиц анимации ------------------------------------
KID_BIN := $(abspath $(CURDIR)/../../poc/res/kid/kid_data.bin)
build/kid_blob.c: gen_kid_blob.py $(KID_BIN) | $(BUILD)
@python3 gen_kid_blob.py $(KID_BIN) $@
build/kid_blob.rel: build/kid_blob.c kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/stubs.rel: stubs.c stubs.h kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/scene.rel: scene.c scene.h stubs.h kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/bank_stub.rel: bank_stub.s | $(BUILD)
@$(SDASZ80) -plosgff $@ $<
# Эталоны трасс: пересобрать набор, если их правили (см. шапку t_phys.c).
build/phys.rel: $(wildcard golden/*.h)
@@ -0,0 +1,105 @@
# tests-host — модульные тесты движка roomtest
Прогоняются под `ucsim_z80`, обвязка общая — `testkit/` (там же объяснение,
почему под z80, а не хостовым gcc, и грабли).
```
make host-tests # из корня, вместе с остальными
make -C applications/PoP/roomtest/tests-host # только эти
make -C applications/PoP/roomtest/tests-host t_geom
```
## Что покрыто
| набор | модуль | что проверяет |
|-------|--------|---------------|
| `t_geom` | `pop_geom.c` | геометрия комнаты (`x_bump`, `y_land`, `y_to_row`) и PRNG оригинала |
| `t_phys` | `pop_map.c` + `pop_kid.c` + `pop_ctrl.c` | покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал |
Главный тест в `t_geom``geom_lcg_matches_reference`. LCG оригинала
(`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме
Горнера ради обхода `__mullong`, и заявка «бит-в-бит как в SDLPoP» до
появления теста держалась только на комментарии. Тест сверяет рукописный
asm с наивной 32-битной формулой на 128 шагах — и по возвращаемому
значению, и по обеим половинам сида.
## t_phys: характеризация, а не проверка правильности
Набор существует ради **страховки под рефакторинг**. Эталоны сняты с
текущего билда, то есть консервируют и текущие баги — правильность
по-прежнему проверяется сверкой с `SDLPoP/src/`. Ценность в другом:
ближайший крупный шаг (перенос физики с `Kid.` на `Char.`, чтобы её мог
переиспользовать страж) обязан быть **поведение-сохраняющим**, и «трасса до
== трасса после» ловит ровно тот класс ошибок, который там возможен.
Поэтому эталон **переснимается только осознанно**, и в коммите должно быть
написано, что и почему поменялось. Молча обновлённый эталон обесценивает
весь набор.
```
python3 record_golden.py # переснять все трассы
python3 record_golden.py 7 9 # только сценарии 7 и 9
```
Трасса — по одной записи на кадр: `frame, x, y, dir, col, row, action` и
смещение `curr_seq` от `SEQTBL_BASE`. Кадр гоняется ровно в том порядке,
что и в главном цикле `roomtest.c`:
```
pop_ctrl_tick(); // ввод -> control(): смена последовательности
kid_tick(); // play_seq: следующий кадр
pop_phys_tick(); // падение/приземление/стена
pop_loose_tick(); // досчёт тряски и снятие провалившейся плиты
```
Покадровость здесь принципиальна: порт seg005/seg006 — конечный автомат, и
ошибка почти всегда проявляется не в одной функции, а в расхождении
траектории через несколько кадров. Плюс главный риск переноса на `Char`
не арифметика, а то, **кто владеет окном `Char` внутри кадра**; поймать это
можно только прогоном полного кадра.
Проверка, что харнесс воспроизводит устройство: сценарий `stand` даёт
`frame=15, x=114, y=118, curr_seq=0x19A3` — те же значения, что читаются из
`_Kid` в живом MAME.
### Как это заработало под ucsim
Три шва, каждый закрыт без правок продуктового кода:
1. **Таблицы анимации.** `pop_kid.c` ходит по ним абсолютными адресами от
`KD_DATA_OFF` (0x100) — там, куда на устройстве их приводит маппинг W0.
В тесте `kid_data.bin` просто кладётся по 0x100 (`gen_kid_blob.py`), а
`gfx_w0_map` заглушен. Следствие: код набора линкуется с
`--code-loc 0x1000` (`CODE_LOC` в Makefile), чтобы не налезть на блоб.
2. **Окружение.** `stubs.c` — libc/libbgi (графика, память, файлы), соседние
модули (`pop_bg`/`pop_trob`/`pop_level`/`pop_redraw`) и состояние стража.
Заглушки не пустые там, где это меняет смысл: файловое чтение реально
отдаёт `kid_data.bin` (иначе `pop_kid_data_load` тихо сдаётся, `kdat_ok`
остаётся нулём и `play_seq` не делает ничего — трасса выходит из одного
застывшего кадра), клавиатура отвечает по набору «нажатых» скан-кодов
(так под тест попадают `read_input` и `read_user_control`, а не только
диспетчер), а обращения к соседям **журналируются** (`tk_log`) — можно
проверять «плита отвалилась и попросила перерисовку», а не только
координаты.
3. **`__banked`.** Модули физики помечены как банковые, SDCC генерирует на
них трамплин `___sdcc_bcall_ehl`, которому нужны `set_bank`/`get_bank`.
В тесте память плоская — `bank_stub.s` отдаёт нулевой банк и пустое
переключение, соблюдая контракт по регистрам.
Что харнесс НЕ покрывает и остаётся за MAME: отрисовка и окклюзия, реальные
переключения банков, тайминги кадра, настоящая клавиатура.
## Что нужно, чтобы двинуться дальше
- **Доступ к странице уровня.** `pop_level.c` ходит по абсолютным адресам
(`(uint8_t *)(LVL_DATA_OFF + …)` после `gfx_w0_map`). Для `pop_map` это не
потребовалось — карту комнаты в него ИНЪЕКТИРУЮТ через `pop_map_set`, —
но набор на сам `pop_level`/`pop_trob` в это упрётся. Решение то же, что
для `kid_data`: положить страницу по нужному адресу и поднять `CODE_LOC`.
- **Сценарии из `bug_closed.md`.** У каждой закрытой записи есть симптом и
ожидаемое поведение — готовый список регрессионных кейсов, который стоит
переносить в `t_phys` по мере надобности.
- **BUG-LOOSE-2** (гонка «уйти из комнаты раньше, чем долетит плита») —
через мост MAME воспроизвести не удалось, а на уровне логики это
несколько строк: заспавнить кусок, сменить комнату, тикать до
приземления, проверить щебень.
@@ -0,0 +1,25 @@
;--------------------------------------------------------------------------
; bank_stub.s — set_bank/get_bank для прогона под ucsim.
;
; Модули движка помечены __banked (в продукте они лежат в страницах W3), и
; SDCC генерирует на такие вызовы трамплин ___sdcc_bcall_ehl из своей
; библиотеки. Трамплин обязан найти две пользовательские подпрограммы:
;
; get_bank — вернуть в A номер текущего банка, прочие регистры не трогать
; set_bank — переключить банк на A, прочие регистры не трогать
;
; В тесте банков нет: вся память плоская, все модули слинкованы вместе.
; Поэтому «банк» всегда нулевой, а переключение — пустое. Важно только
; соблюсти контракт по регистрам, иначе трамплин испортит аргументы вызова.
;--------------------------------------------------------------------------
.area _CODE
.globl set_bank
.globl get_bank
get_bank::
ld a, #0x00
ret
set_bank::
ret
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
gen_kid_blob.py kid_data.bin как C-массив для тестов под ucsim.
Зачем. Таблицы кадров и seqtbl живут в EMM-странице, замапленной в W0, и
pop_kid ходит по ним АБСОЛЮТНЫМИ адресами от KD_DATA_OFF (0x100). В ucsim
никакого W0 нет, зато есть ровная 64-КБ память, поэтому тест просто кладёт
файл по 0x100 и глушит gfx_w0_map. Единственное следствие для сборки
код набора линкуется с --code-loc выше конца блоба (см. Makefile).
Файл берётся ровно тот, что уходит на устройство, pop_extract_kid_data.py
делает его из оригинального PRINCE.EXE, так что тест проверяет порт против
НАСТОЯЩИХ данных игры, а не против переписанной от руки таблицы.
"""
import sys
from pathlib import Path
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
data = src.read_bytes()
out = [
"/* Сгенерировано gen_kid_blob.py — не править руками. */",
'#include "kidblob.h"',
"",
f"const unsigned char kid_blob[{len(data)}] = {{",
]
for i in range(0, len(data), 16):
out.append(" " + ",".join(str(b) for b in data[i:i + 16]) + ",")
out += [
"};",
"",
f"const unsigned int kid_blob_size = {len(data)};",
"",
"/* Разложить блоб по KD_DATA_OFF. Без memcpy: тестовый бинарь не",
" * линкуется с libc, а тянуть ради этого z80-рантайм незачем. */",
"void kid_blob_install(void)",
"{",
" unsigned char *d = (unsigned char *)KID_BLOB_ADDR;",
" const unsigned char *s = kid_blob;",
f" unsigned int n = {len(data)};",
" while (n--) *d++ = *s++;",
"}",
"",
]
dst.write_text("\n".join(out))
print(f"gen_kid_blob: {src.name} -> {dst.name} ({len(data)} Б)")
@@ -0,0 +1,22 @@
/* Эталон трассы: crouch. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x6B, 0x81, 0x76, 0x00, 0x04, 0x01, 0x01, 0x3E, 0x06,
0x6C, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x41, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6E, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4A, 0x06,
0x6F, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4B, 0x06,
0x70, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4E, 0x06,
0x71, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4F, 0x06,
0x72, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x52, 0x06,
0x73, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x53, 0x06,
0x74, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x54, 0x06,
0x75, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x57, 0x06,
0x76, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x58, 0x06,
0x77, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x59, 0x06,
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,36 @@
/* Эталон трассы: fall_ledge. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x66, 0x9E, 0x79, 0x00, 0x06, 0x02, 0x03, 0x77, 0x02,
0x67, 0xA0, 0x7F, 0x00, 0x06, 0x02, 0x03, 0x7C, 0x02,
0x68, 0x9F, 0x88, 0x00, 0x06, 0x02, 0x03, 0x81, 0x02,
0x69, 0x9F, 0x94, 0x00, 0x06, 0x02, 0x03, 0x84, 0x02,
0x6A, 0x9E, 0xA6, 0x00, 0x06, 0x02, 0x04, 0xDE, 0x03,
0x6B, 0xA0, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC3, 0x06,
0x6C, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC6, 0x06,
0x6D, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x01, 0xC9, 0x06,
0x6E, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4A, 0x06,
0x6F, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4B, 0x06,
0x70, 0xA5, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4E, 0x06,
@@ -0,0 +1,22 @@
/* Эталон трассы: jump_up. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x43, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x12, 0x04,
0x44, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x13, 0x04,
0x45, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x14, 0x04,
0x46, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x15, 0x04,
0x47, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x16, 0x04,
0x48, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x17, 0x04,
0x49, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x18, 0x04,
0x4A, 0x80, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x19, 0x04,
0x4B, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1A, 0x04,
0x4C, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1B, 0x04,
0x4D, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1C, 0x04,
0x4E, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1D, 0x04,
0x4F, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1E, 0x04,
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x21, 0x04,
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x24, 0x04,
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x25, 0x04,
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x28, 0x04,
0x51, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB8, 0x03,
0x52, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB9, 0x03,
0x53, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x05, 0xBC, 0x03,
@@ -0,0 +1,36 @@
/* Эталон трассы: loose. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
0x09, 0x9E, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xA0, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0xA4, 0x76, 0x00, 0x06, 0x01, 0x01, 0x22, 0x00,
0x0C, 0xA9, 0x76, 0x00, 0x07, 0x01, 0x01, 0x25, 0x00,
0x0D, 0xAB, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0xAE, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2D, 0x00,
0x07, 0xB2, 0x76, 0x00, 0x07, 0x01, 0x01, 0x14, 0x00,
0x08, 0xB7, 0x76, 0x00, 0x08, 0x01, 0x01, 0x17, 0x00,
0x09, 0xB8, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xBA, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1F, 0x00,
@@ -0,0 +1,36 @@
/* Эталон трассы: rjump_gap3. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x08, 0x00,
0x02, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x09, 0x00,
0x03, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0A, 0x00,
0x04, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0B, 0x00,
0x05, 0xB0, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0E, 0x00,
0x06, 0xAD, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x11, 0x00,
0x07, 0xAA, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x14, 0x00,
0x08, 0xA5, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x17, 0x00,
0x09, 0xA4, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xA2, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x9E, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x99, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x97, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x2A, 0x00,
0x22, 0x95, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x64, 0x01,
0x23, 0x90, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x67, 0x01,
0x24, 0x8A, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x6A, 0x01,
0x25, 0x87, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x6D, 0x01,
0x26, 0x82, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x72, 0x01,
0x27, 0x7B, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x75, 0x01,
0x28, 0x6F, 0x73, 0xFF, 0x04, 0x01, 0x01, 0x7A, 0x01,
0x29, 0x67, 0x6A, 0xFF, 0x03, 0x01, 0x01, 0x7F, 0x01,
0x2A, 0x5F, 0x68, 0xFF, 0x02, 0x01, 0x01, 0x84, 0x01,
0x2B, 0x5B, 0x73, 0xFF, 0x01, 0x01, 0x01, 0x89, 0x01,
0x2C, 0x57, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x8E, 0x01,
0x07, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x14, 0x00,
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
0x33, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x94, 0x04,
0x34, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x95, 0x04,
0x0F, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x00, 0x35, 0x00,
0x10, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x28, 0x01,
0x11, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x29, 0x01,
0x12, 0x50, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2C, 0x01,
0x13, 0x4E, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2F, 0x01,
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
@@ -0,0 +1,26 @@
/* Эталон трассы: run_right. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
@@ -0,0 +1,14 @@
/* Эталон трассы: stand. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,22 @@
/* Эталон трассы: step_shift. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x79, 0x72, 0x76, 0x00, 0x03, 0x01, 0x01, 0x6F, 0x05,
0x7A, 0x73, 0x76, 0x00, 0x03, 0x01, 0x01, 0x72, 0x05,
0x7B, 0x74, 0x76, 0x00, 0x03, 0x01, 0x01, 0x75, 0x05,
0x7C, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x78, 0x05,
0x7D, 0x7B, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7B, 0x05,
0x7E, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7E, 0x05,
0x7F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x81, 0x05,
0x80, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x82, 0x05,
0x81, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x83, 0x05,
0x82, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x84, 0x05,
0x83, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x85, 0x05,
0x84, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x86, 0x05,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,18 @@
/* Эталон трассы: turn. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x2D, 0x7A, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD1, 0x01,
0x2E, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD4, 0x01,
0x2F, 0x77, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD7, 0x01,
0x30, 0x78, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xDA, 0x01,
0x01, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x08, 0x00,
0x02, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x09, 0x00,
0x03, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x71, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x6E, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x11, 0x00,
0x07, 0x6B, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x66, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x17, 0x00,
0x09, 0x65, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x63, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x5F, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x5A, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x25, 0x00,
@@ -0,0 +1,18 @@
/*
* kidblob.h таблицы анимации Кида в памяти теста.
*
* KID_BLOB_ADDR обязан совпадать с KD_DATA_OFF из pop_kid.c: продукт
* адресует таблицы именно так, и в этом весь смысл под тестом работает
* тот же код доступа, что на устройстве, просто без маппинга W0.
*/
#ifndef POP_TESTS_KIDBLOB_H
#define POP_TESTS_KIDBLOB_H
#define KID_BLOB_ADDR 0x0100
extern const unsigned char kid_blob[];
extern const unsigned int kid_blob_size;
void kid_blob_install(void);
#endif
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
record_golden.py снять эталонные трассы для t_phys.
Зачем скрипт, а не один прогон. Отчёт tcheck ограничен 768 байтами лога, а
трасс на все сценарии сильно больше. Поэтому набор умеет снимать РОВНО
ОДИН сценарий за прогон (-DSC_RECORD=<n>), а перебор делает этот скрипт:
собирает набор N раз, каждый раз забирает из отчёта имя сценария и его
трассу и пишет golden/<имя>.h.
Эталон фиксирует ТЕКУЩЕЕ поведение. Это осознанно: набор страхует
поведение-сохраняющие правки (перенос физики Kid -> Char), а не доказывает
правильность. Правильность сверка с SDLPoP. Поэтому переснимать эталоны
можно только вместе с внятным объяснением, что и почему поменялось.
Использование:
python3 record_golden.py # снять все сценарии
python3 record_golden.py 7 9 # только эти номера
"""
import re
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
GOLDEN = HERE / "golden"
# Число сценариев = число TC_RUN в t_phys.c: список ведётся там, здесь его
# дублировать незачем.
NSCEN = len(re.findall(r"^\s*TC_RUN\(", (HERE / "t_phys.c").read_text(), re.M))
def record(n: int) -> str:
# make не видит смены -DSC_RECORD (в зависимостях только файлы), поэтому
# объектник набора сносим руками — иначе следующий прогон молча снимет
# трассу предыдущего сценария под новым именем.
for f in ("phys.rel", "phys.ihx"):
(HERE / "build" / f).unlink(missing_ok=True)
subprocess.run(["make", "-s", "build/phys.ihx", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
cwd=HERE, check=True)
p = subprocess.run(["make", "-s", "run-phys", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
cwd=HERE, capture_output=True, text=True)
out = p.stdout
m = re.search(r"^\s*### (\S+)\s*$", out, re.M)
if not m:
raise SystemExit(f"сценарий {n}: в отчёте нет строки '### имя'\n{out}")
name = m.group(1)
rows = re.findall(r"^\s*([0-9A-F]{18})\s*$", out, re.M)
if not rows:
raise SystemExit(f"сценарий {n} ({name}): трасса пустая\n{out}")
lines = [f"/* Эталон трассы: {name}. Снят record_golden.py — не править руками.",
" * Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */"]
for r in rows:
lines.append(" " + ", ".join(f"0x{r[i:i+2]}" for i in range(0, 18, 2)) + ",")
GOLDEN.mkdir(exist_ok=True)
(GOLDEN / f"{name}.h").write_text("\n".join(lines) + "\n")
return f"{name}: {len(rows)} кадров"
def main() -> int:
nums = [int(a) for a in sys.argv[1:]] or list(range(1, NSCEN + 1))
for n in nums:
print(f"[{n}/{NSCEN}] " + record(n))
# Итоговая пересборка в обычном режиме, чтобы каталог не остался с
# объектниками, собранными под SC_RECORD.
subprocess.run(["make", "-s", "clean"], cwd=HERE, check=True)
print("эталоны сняты; проверить: make -C", HERE.name, "t_phys")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,163 @@
/*
* scene.c реализация покадрового раннера (см. scene.h).
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"
#include "pop_geom.h"
#include "pop_guard.h"
#include "kid_data.h"
#include "scene.h"
#include "stubs.h"
#include "kidblob.h"
sc_rec sc_trace[SC_TRACE_MAX];
uint8_t sc_len;
static uint8_t sc_fg[30];
/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */
static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 };
void sc_init(void)
{
kid_blob_install(); /* таблицы туда, куда смотрит pop_kid */
pop_kid_data_load("KID\\KID_DATA.BIN");
pop_kid_hp_newgame();
}
void sc_room(const uint8_t *fg, uint8_t room)
{
uint8_t i;
for (i = 0; i < 30; i++) { sc_fg[i] = fg[i]; tk_modif[i] = 0; }
pop_map_set(sc_fg);
pop_map_set_room(room);
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
* и сверху/снизу тогда решения физики зависят только от sc_fg, и
* трасса не поедет, если когда-нибудь поменяется загрузка кромок. */
pop_map_set_edges(0, 0, 0, 0, sc_edge, sc_edge);
tk_start_room = room;
tk_log_clear();
}
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
pop_kid_hp_reset();
}
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
pop_determine_col(); /* колонку пересчитать под новый X */
pop_kid_hp_reset();
}
void sc_trace_clear(void) { sc_len = 0; }
static void sc_record(void)
{
sc_rec *r;
if (sc_len >= SC_TRACE_MAX) return;
r = &sc_trace[sc_len++];
r->frame = Kid.frame;
r->x = Kid.x;
r->y = Kid.y;
r->dir = Kid.direction;
r->col = Kid.curr_col;
r->row = Kid.curr_row;
r->action = Kid.action;
r->seq = (uint16_t)(Kid.curr_seq - SEQTBL_BASE);
}
uint8_t sc_run(uint8_t keys, uint8_t n)
{
uint8_t i, done = 0;
for (i = 0; i < n; i++) {
tk_keys_clear();
if (keys & SC_L) tk_key_down(KBD_LEFT);
if (keys & SC_R) tk_key_down(KBD_RIGHT);
if (keys & SC_U) tk_key_down(KBD_UP);
if (keys & SC_D) tk_key_down(KBD_DOWN);
if (keys & SC_SHIFT) tk_key_down(KBD_LSHIFT);
/* Порядок ровно как в главном цикле roomtest.c (kid_heal и
* отрисовка выпадают они ничего не решают). pop_loose_tick
* оставлен: он не рисование, а МЕХАНИКА досчитывает тряску и
* убирает провалившуюся плиту из карты, то есть меняет то, по чему
* физика в следующем кадре принимает решения. */
pop_ctrl_tick();
kid_tick();
pop_phys_tick();
pop_loose_tick();
sc_record();
done++;
if (sc_len >= SC_TRACE_MAX) break;
}
return done;
}
/* ---- сверка с эталоном ---------------------------------------------- */
void sc_expect(const uint8_t *golden, uint8_t nrec)
{
uint8_t i;
const uint8_t *g = golden;
tc_check((int16_t)sc_len, (int16_t)nrec, "sc_len", __LINE__);
for (i = 0; i < nrec && i < sc_len; i++, g += SC_REC_BYTES) {
const sc_rec *r = &sc_trace[i];
/* Номер кадра идёт первым полем каждой проверки: в отчёте видно,
* на каком именно кадре трасса разошлась. */
tc_check((int16_t)r->frame, (int16_t)g[0], "frame", i);
tc_check((int16_t)r->x, (int16_t)g[1], "x", i);
tc_check((int16_t)r->y, (int16_t)g[2], "y", i);
tc_check((int16_t)r->dir, (int16_t)(int8_t)g[3], "dir", i);
tc_check((int16_t)r->col, (int16_t)(int8_t)g[4], "col", i);
tc_check((int16_t)r->row, (int16_t)(int8_t)g[5], "row", i);
tc_check((int16_t)r->action, (int16_t)g[6], "action", i);
tc_check((int16_t)r->seq,
(int16_t)((uint16_t)g[7] | ((uint16_t)g[8] << 8)), "seq", i);
}
}
/* ---- снятие эталона -------------------------------------------------- */
static void put(char c)
{
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
}
static void put_byte(uint8_t v)
{
static const char hex[] = "0123456789ABCDEF";
put(hex[v >> 4]); put(hex[v & 15]);
}
void sc_dump(const char *name)
{
uint8_t i;
tc_result.magic = TC_MAGIC;
tc_result.total = 1;
tc_result.failed = 1; /* иначе раннер лог не напечатает */
put('#'); put('#'); put('#'); put(' ');
while (*name) put(*name++);
put('\n');
for (i = 0; i < sc_len; i++) {
const sc_rec *r = &sc_trace[i];
put_byte(r->frame);
put_byte(r->x);
put_byte(r->y);
put_byte((uint8_t)r->dir);
put_byte((uint8_t)r->col);
put_byte((uint8_t)r->row);
put_byte(r->action);
put_byte((uint8_t)r->seq);
put_byte((uint8_t)(r->seq >> 8));
put('\n');
}
}
@@ -0,0 +1,88 @@
/*
* scene.h покадровый сценарный раннер для тестов физики Кида.
*
* Почему покадровый, а не «вызвать функцию и сверить возврат». Порт
* seg005/seg006 это конечный автомат: диспетчер выбирает
* последовательность, интерпретатор проигрывает её кадр за кадром, физика
* между кадрами двигает персонажа и решает, не пора ли падать. Ошибка
* почти всегда проявляется не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ
* через несколько кадров. Плюс главный риск ближайшего рефакторинга
* (Kid -> Char) не арифметика, а то, КТО владеет окном Char внутри кадра;
* поймать это можно только прогоном полного кадра, как в главном цикле:
*
* pop_ctrl_tick(); // ввод -> control(): смена последовательности
* kid_tick(); // play_seq: следующий кадр
* pop_phys_tick(); // падение/приземление/стена
*
* Отсюда формат: сцена (комната + стартовая поза) + скрипт ввода, на
* выходе трасса состояния Кида по кадрам. Сравнение трассы с эталоном
* и есть тест.
*/
#ifndef POP_TESTS_SCENE_H
#define POP_TESTS_SCENE_H
#include <stdint.h>
/* Одна запись трассы: то, чем оперируют решения движка. curr_seq —
* СМЕЩЕНИЕ от SEQTBL_BASE (адрес зависит от базы, смещение нет). */
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t dir;
int8_t col, row;
uint8_t action;
uint16_t seq;
} sc_rec;
#define SC_TRACE_MAX 48
extern sc_rec sc_trace[SC_TRACE_MAX];
extern uint8_t sc_len;
/* Кнопки скрипта ввода — битовая маска на кадр. */
#define SC_L 0x01
#define SC_R 0x02
#define SC_U 0x04
#define SC_D 0x08
#define SC_SHIFT 0x10
/* Инициализация окружения набора: таблицы анимации, обнуление состояния.
* Звать один раз в начале прогона. */
void sc_init(void);
/* Задать комнату: fg[30] — коды тайлов по рядам (row*10+col), room —
* номер (нужен trob'ам). Модификаторы обнуляются; ставить их тест может
* сам через tk_modif. Соседи комнаты стены (изолированная сцена). */
void sc_room(const uint8_t *fg, uint8_t room);
/* Поставить Кида: колонка/ряд задают позицию по центру тайла, как это
* делает do_startpos (seg003) так сцена описывается в терминах карты, а
* не в пикселях. */
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir);
/* То же, но с точным X: тайл задаёт ряд и пол под ногами, X — фазу внутри
* тайла (нужно, когда исход зависит от того, где именно Кид стоит). */
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir);
/* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу.
* Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */
uint8_t sc_run(uint8_t keys, uint8_t n);
/* Сбросить трассу (перед новым сценарием). */
void sc_trace_clear(void);
/* Сверить трассу с эталоном: golden — плоский массив по 8 байт на кадр
* (frame,x,y,dir,col,row,action,seq_lo,seq_hi 9), nrec записей.
* Расхождения уходят в отчёт tcheck с номером кадра и поля. */
void sc_expect(const uint8_t *golden, uint8_t nrec);
#define SC_REC_BYTES 9
/* Снятие эталона. Лог tcheck всего 768 байт — на все сценарии сразу трасс
* не хватит, поэтому за прогон снимается РОВНО ОДИН сценарий (его номер
* задаёт -DSC_RECORD=<n>), а перебором занимается record_golden.py.
* Формат: строка «### имя», дальше по строке на кадр, по два хекс-символа
* на байт. Раннер печатает лог только при провале sc_dump ставит
* failed=1 намеренно. */
void sc_dump(const char *name);
#endif
@@ -0,0 +1,200 @@
/*
* stubs.c окружение движка для прогона под ucsim_z80.
*
* Под тестом крутится НАСТОЯЩАЯ физика (pop_map) с настоящим
* интерпретатором последовательностей (pop_kid): это единственный способ
* проверить порт seg005/seg006, потому что вся его суть в точных
* величинах кадров и seqtbl. А вот всё, что вокруг, в ucsim не существует:
* видеопамять, EMM-страницы, файлы, клавиатура. Здесь они заглушены.
*
* Три категории:
* 1. libc/libbgi (графика, память, файлы, клавиатура) пустышки. Код
* отрисовки из pop_kid.c линкуется, но не вызывается.
* 2. Соседние модули движка (pop_bg/pop_trob/pop_level/pop_redraw)
* минимальные модели: room_modif живёт здесь массивом, пометки
* перерисовки и падающие куски ЖУРНАЛИРУЮТСЯ (тест может проверить,
* что физика их дёрнула), уровень отдаёт стартовую комнату.
* 3. Состояние стража просто данные: страж в тестах физики не тикает,
* но pop_map/pop_ctrl на его поля смотрят.
*
* Клавиатура сделана НЕ пустышкой: `tk_keys` набор «нажатых» скан-кодов,
* и `kbd_raw_down` отвечает по нему. Так под тест попадают и read_input, и
* read_user_control, то есть весь путь ввода, а не только диспетчер.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_bg.h"
#include "pop_map.h"
#include "pop_trob.h"
#include "pop_level.h"
#include "pop_redraw.h"
#include "_pop_draw.h"
#include "stubs.h"
#include "kidblob.h"
/* ---- 1. libc / libbgi ---------------------------------------------- */
uint8_t _gfx_visible_page, _gfx_draw_page, _gfx_bank;
/* ISR-стаб W0-страницы: продукт берёт его АДРЕС и вписывает в страницу.
* Здесь достаточно, чтобы символ существовал. */
void _gfx_w0_isr(void) { }
void gfx_w0_map(uint8_t page) { (void)page; }
void gfx_w0_unmap(void) { }
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
{ (void)x; (void)y; (void)img; (void)flip; }
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t maxw)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; (void)maxw; }
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
int atlas_load(atlas_t *a, const char *path) { (void)a; (void)path; return -1; }
void atlas_free(atlas_t *a) { (void)a; }
const void *atlas_image(const atlas_t *a, uint8_t idx)
{ (void)a; (void)idx; return 0; }
/* EMM: страниц нет, но блок обязан выделиться — иначе pop_kid_data_load
* сдастся на первом же шаге и молча оставит kdat_ok = 0, а play_seq тогда
* просто ничего не делает (и трасса выходит из одного застывшего кадра). */
uint8_t mem_alloc_pages_bios(uint8_t n) { (void)n; return 1; }
void mem_free_block_bios(uint8_t blk) { (void)blk; }
uint8_t mem_get_page_bios(uint8_t blk, uint8_t idx) { (void)blk; (void)idx; return 1; }
/* Файловая система: единственный «файл» — таблицы анимации. Сделано не
* пустышкой, а рабочим чтением, чтобы pop_kid_data_load отработал целиком
* (включая правку ISR-стаба страницы) и сам взвёл свой kdat_ok тест не
* лезет во внутренние статики продукта. Данные, по которым потом ходят
* аксессоры, кладёт kid_blob_install по KID_BLOB_ADDR: на устройстве туда
* же их приводит маппинг W0. */
int open(const char *p, int f) { (void)p; (void)f; return 3; }
int read(int fd, void *b, int n)
{
unsigned char *d = (unsigned char *)b;
const unsigned char *s = kid_blob;
unsigned int i, cnt = kid_blob_size;
if (fd != 3) return -1;
if ((unsigned int)n < cnt) cnt = (unsigned int)n;
for (i = 0; i < cnt; i++) d[i] = s[i];
return (int)cnt;
}
int close(int fd) { (void)fd; return 0; }
/* ---- клавиатура: управляемая тестом --------------------------------- */
uint16_t tk_keys[TK_KEYS_MAX];
int kbd_raw_open(void) { return 0; }
void kbd_raw_close(void) { }
void kbd_raw_sync(void) { }
uint8_t kbd_raw_down(uint16_t code)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++)
if (tk_keys[i] == code) return 1;
return 0;
}
void tk_keys_clear(void)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++) tk_keys[i] = 0;
}
void tk_key_down(uint16_t code)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++)
if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
}
/* ---- 2. соседние модули движка -------------------------------------- */
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
* зелья (тип). Физика читает и пишет его напрямую через pop_trob_modif. */
uint8_t tk_modif[30];
uint8_t tk_start_room;
uint8_t *pop_trob_modif(uint8_t room) __banked { (void)room; return tk_modif; }
uint8_t pop_level_start_room(void) { return tk_start_room; }
/* Журнал: что физика попросила у соседей. Тест сверяет ФАКТ и параметры
* (какой тайл помечен, где заспавнен кусок) заглушка-пустышка этого не
* дала бы, а именно такие баги (BUG-GATE-ANIM-1) мы и ловим. */
tk_log_t tk_log;
static void log_put(uint8_t kind, uint8_t a, uint8_t b, uint8_t c)
{
if (tk_log.n >= TK_LOG_MAX) { tk_log.overflow = 1; return; }
tk_log.e[tk_log.n].kind = kind;
tk_log.e[tk_log.n].a = a;
tk_log.e[tk_log.n].b = b;
tk_log.e[tk_log.n].c = c;
tk_log.n++;
}
void tk_log_clear(void) { tk_log.n = 0; tk_log.overflow = 0; }
uint8_t tk_log_count(uint8_t kind)
{
uint8_t i, c = 0;
for (i = 0; i < tk_log.n; i++) if (tk_log.e[i].kind == kind) c++;
return c;
}
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
{ log_put(TK_TROB, room, tilepos, (uint8_t)type); }
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier) __banked
{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); }
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages)
{ log_put(TK_REDRAW, tilepos, kind, pages); }
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages)
{ log_put(TK_REDRAW_ABOVE, col, kind, pages); }
void pop_loose_mob_spawn(int row, int col) __banked
{ log_put(TK_MOB_SPAWN, (uint8_t)row, (uint8_t)col, 0); }
void pop_loose_mob_tick(void) __banked { }
void pop_loose_mob_room_changed(void) __banked { }
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{ (void)col; (void)y; return 0; }
/* Отрисовочные хвосты, которые физика дёргает напрямую. */
void pop_fore_set_clip(int x, int y, int w, int h) __banked
{ (void)x; (void)y; (void)w; (void)h; }
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{ (void)x; (void)w; (void)sy_top; (void)h; }
void pop_heal_fast(int x, int y, int w, int h)
{ (void)x; (void)y; (void)w; (void)h; }
/* ---- 3. состояние стража -------------------------------------------- */
pop_char_t Guard;
kframe pop_gframe;
int8_t can_guard_see_kid;
int8_t guard_refrac, kid_sword_strike;
int8_t offguard, holding_sword;
/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и
* обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража,
* которого в наборе нет. */
int8_t is_guard_notice;
@@ -0,0 +1,43 @@
/*
* stubs.h интерфейс тестового окружения (см. stubs.c).
*
* Что тест может задавать: «нажатые» клавиши (tk_key_down) и модификаторы
* тайлов комнаты (tk_modif). Что может проверять: журнал обращений
* физики к соседним модулям (tk_log).
*/
#ifndef POP_TESTS_STUBS_H
#define POP_TESTS_STUBS_H
#include <stdint.h>
/* ---- клавиатура ------------------------------------------------------ */
#define TK_KEYS_MAX 6
extern uint16_t tk_keys[TK_KEYS_MAX];
void tk_keys_clear(void);
void tk_key_down(uint16_t code);
/* ---- комната --------------------------------------------------------- */
extern uint8_t tk_modif[30]; /* room_modif текущей комнаты */
extern uint8_t tk_start_room; /* что вернёт pop_level_start_room */
/* ---- журнал обращений ------------------------------------------------ */
#define TK_LOG_MAX 24
enum {
TK_TROB = 1, /* a=room b=tilepos c=type */
TK_BUTTON, /* a=room b=tilepos c=type */
TK_REDRAW, /* a=tilepos b=kind c=pages */
TK_REDRAW_ABOVE, /* a=col b=kind c=pages */
TK_MOB_SPAWN /* a=row b=col */
};
typedef struct { uint8_t kind, a, b, c; } tk_log_e;
typedef struct { uint8_t n, overflow; tk_log_e e[TK_LOG_MAX]; } tk_log_t;
extern tk_log_t tk_log;
void tk_log_clear(void);
/* Сколько записей данного вида в журнале (0 = не было). */
uint8_t tk_log_count(uint8_t kind);
#endif
@@ -0,0 +1,146 @@
/*
* t_geom.c тесты pop_geom.c (геометрия комнаты + PRNG оригинала).
*
* Модуль выбран первым, потому что не тянет за собой ничего: ни графики,
* ни данных уровня, ни клавиатуры только свой заголовок.
*
* Главный тест здесь geom_lcg_matches_reference. В pop_geom.c LCG
* оригинала (s = s*214013 + 2531011) написан на ассемблере по схеме
* Горнера ради обхода __mullong, и заявка «бит-в-бит как в SDLPoP» до сих
* пор держалась только на комментарии. Тест сверяет рукописный asm с
* наивной 32-битной формулой на каждом шаге и по возвращаемому значению,
* и по обеим половинам сида.
*/
#include "tcheck.h"
#include "pop_geom.h"
/* ---- геометрия ------------------------------------------------------ */
TC_TEST(geom_xbump_layout)
{
/* Шаг колонок равномерный и равен TILE_SIZEX — на этом стоит вся
* арифметика колонок (x_bump[col + FIRST_ONSCREEN_COLUMN]). */
uint8_t i;
for (i = 1; i < 20; i++)
TC_EQ(pop_x_bump[i] - pop_x_bump[i - 1], TILE_SIZEX);
/* Центр тайла колонки 0 — та самая величина, которую кладут в Char.x
* do_startpos и pos_guards (seg003). */
TC_EQ(pop_x_bump[0 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 72);
TC_EQ(pop_x_bump[9 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 198);
}
TC_TEST(geom_yland_rows)
{
/* y_land[row + 1]; [0] — ряд «над комнатой». */
TC_EQ(pop_y_land[0], -8);
TC_EQ(pop_y_land[1], 55);
TC_EQ(pop_y_land[2], 118);
TC_EQ(pop_y_land[3], 181);
TC_EQ(pop_y_land[4], 244);
/* Шаг между рядами — TILE_SIZEY. */
TC_EQ(pop_y_land[2] - pop_y_land[1], TILE_SIZEY);
TC_EQ(pop_y_land[3] - pop_y_land[2], TILE_SIZEY);
}
TC_TEST(geom_y_to_row)
{
/* Пол каждого ряда должен давать номер этого ряда. */
TC_EQ(pop_y_to_row(pop_y_land[1]), 0);
TC_EQ(pop_y_to_row(pop_y_land[2]), 1);
TC_EQ(pop_y_to_row(pop_y_land[3]), 2);
/* Ряд над комнатой. */
TC_EQ(pop_y_to_row(pop_y_land[0]), -1);
/* Оборот mod 4: ряд «под комнатой» сворачивается обратно в -1 —
* ровно на это опирается спавн падающего куска у нижней кромки. */
TC_EQ(pop_y_to_row(pop_y_land[4]), -1);
}
/* ---- PRNG: сверка asm-LCG с эталонной формулой ---------------------- */
#if POP_PRANDOM_EXACT
#define LCG_ITERS 128
TC_TEST(geom_lcg_matches_reference)
{
pop_rnd_t s;
unsigned long ref = 12345UL; /* произвольный ненулевой старт */
uint16_t i, got, want;
pop_prandom_set(s, 12345u);
for (i = 0; i < LCG_ITERS; i++) {
got = pop_prandom(&s, 255u);
ref = ref * 214013UL + 2531011UL;
want = (uint16_t)((uint16_t)(ref >> 16) % 256u);
/* Обрываемся на первом расхождении: иначе одна ошибка в умножении
* забила бы весь буфер отчёта однотипными строками. */
if (got != want ||
s.lo != (uint16_t)ref ||
s.hi != (uint16_t)(ref >> 16)) {
TC_EQ(got, want);
TC_EQ(s.lo, (uint16_t)ref);
TC_EQ(s.hi, (uint16_t)(ref >> 16));
break;
}
}
TC_EQ(i, LCG_ITERS); /* прошли всю дистанцию без расхождений */
}
TC_TEST(geom_lcg_seed_zero)
{
/* Ноль — законный сид (кладку сеют номером комнаты + ряд + колонка,
* что вполне даёт 0). У LCG ноль не является неподвижной точкой
* убеждаемся, что asm это воспроизводит. */
pop_rnd_t s;
unsigned long ref = 0UL;
pop_prandom_set(s, 0u);
pop_prandom(&s, 255u);
ref = ref * 214013UL + 2531011UL;
TC_EQ(s.lo, (uint16_t)ref);
TC_EQ(s.hi, (uint16_t)(ref >> 16));
}
#endif /* POP_PRANDOM_EXACT */
TC_TEST(geom_prandom_range)
{
/* Оба генератора обязаны укладываться в 0..maxv, в том числе когда
* maxv+1 НЕ степень двойки (там другая ветка pop_rnd_fit). */
pop_rnd_t s;
uint16_t i, v, seen_hi = 0;
uint16_t out_of_range = 0;
pop_prandom_set(s, 1u);
for (i = 0; i < 200; i++) {
v = pop_prandom(&s, 4u); /* n = 5, не степень двойки */
if (v > 4) out_of_range++;
if (v == 4) seen_hi = 1;
}
TC_EQ(out_of_range, 0);
TC_TRUE(seen_hi); /* верхняя граница достижима */
pop_prandom_set(s, 7u);
out_of_range = 0;
for (i = 0; i < 64; i++) {
v = pop_prandom(&s, 1u); /* n = 2, маска */
if (v > 1) out_of_range++;
}
TC_EQ(out_of_range, 0);
}
int main(void)
{
TC_RUN(geom_xbump_layout);
TC_RUN(geom_yland_rows);
TC_RUN(geom_y_to_row);
#if POP_PRANDOM_EXACT
TC_RUN(geom_lcg_matches_reference);
TC_RUN(geom_lcg_seed_zero);
#endif
TC_RUN(geom_prandom_range);
return 0;
}
@@ -0,0 +1,159 @@
/*
* t_grab.c прыжок с места через провал в три тайла с зацепом за дальнюю
* кромку (уровень 2, комната 9: Кид на краю (1,1) прыгает и должен
* повиснуть на (1,5)).
*
* Зачем отдельный набор. Трюк требует точной фазы и вручную выходит далеко
* не всегда в SDLPoP тоже. Поэтому проверяем не «получилось с первого
* раза», а то, что действительно важно: **окно существует и достижимо**.
* Разница между «сложно» и «невозможно» это разница между «так в
* оригинале» и «баг у нас», и глазами в MAME её не различить.
*
* Два утверждения:
* 1. есть хотя бы одна стартовая X, из которой зацеп удаётся;
* 2. короткий шаг (Shift+вперёд) приводит Кида В ЭТО окно иначе окно
* было бы недостижимо для игрока, что равно его отсутствию.
*
* При провале в отчёт уходит карта исходов по всей ширине плиты:
* G зацепился, L приземлился на дальнюю плиту, . сорвался.
*
* Механику зацепа задаёт check_grab (seg006:0A28): она работает ТОЛЬКО на
* кадрах 102..105 (начало падения) и только при зажатом Shift. mininim,
* для сравнения, разрешает повиснуть прямо в дуге прыжка (kid-jump.c,
* кадры 7..10) но это переработка, а не порт, и источник истины у нас
* SDLPoP.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#define E 0
#define F 1
#define P 3
#define W 20
/* Уровень 2, комната 9, ряд 1: факел(0), кнопка(1), провал(2,3,4),
* колонна(5). Ряд 2 под провалом пуст падение уходит из комнаты. */
static const uint8_t room9[30] = {
E, E, E, E, E, W, W, W, W, W,
F, F, E, E, E, P, F, F, F, P,
W, W, E, E, E, P, F, W, W, W,
};
/* Судить по curr_row нельзя: у улетающего вниз Кида ряд заворачивается
* (0,1,2,0,), и падение выглядит как «остался наверху». Надёжный признак
* ДЕЙСТВИЕ: 2 = вис/подтягивание, 6 = вис у стены. */
#define ACT_STAND 0
#define ACT_RUN_JUMP 1
#define ACT_HANG_CLIMB 2
#define ACT_HANG_STRAIGHT 6
#define X_LO 76
#define X_HI 99
static char probe(uint8_t x, uint8_t k1, uint8_t k2)
{
uint8_t i;
sc_room(room9, 9);
sc_kid_at_x(1, 1, x, 0 /* лицом вправо */);
sc_trace_clear();
sc_run(k1, 4); /* фаза 1: ↑+вперёд — прыжок с места */
sc_run(k2, 26); /* фаза 2: Shift — попытка зацепа */
for (i = 0; i < sc_len; i++) {
uint8_t a = sc_trace[i].action;
if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) return 'G';
}
i = sc_len ? (uint8_t)(sc_len - 1) : 0;
if (sc_trace[i].col >= 5 &&
(sc_trace[i].action == ACT_STAND || sc_trace[i].action == ACT_RUN_JUMP))
return 'L';
return '.';
}
static void put(char c)
{
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
}
static void puts_(const char *s) { while (*s) put(*s++); }
/* Карта исходов — только когда что-то сломалось: по ней сразу видно,
* окно съехало или исчезло. */
static void dump_map(const char *tag, uint8_t k1, uint8_t k2)
{
uint8_t x;
puts_(tag);
for (x = X_LO; x <= X_HI; x++) put(probe(x, k1, k2));
put('\n');
}
/* Куда встаёт Кид после n-го короткого шага от x0. */
static uint8_t step_x(uint8_t x0, uint8_t nsteps)
{
uint8_t n, fx = x0;
sc_room(room9, 9);
sc_kid_at_x(1, 1, x0, 0);
for (n = 0; n < nsteps; n++) {
sc_trace_clear();
sc_run(SC_SHIFT | SC_R, 4); /* шаг = нажатие, не удержание */
sc_run(0, 6); /* отпустить, дать доиграть */
if (sc_len) fx = sc_trace[sc_len - 1].x;
}
return fx;
}
TC_TEST(grab_window_exists)
{
uint8_t x, n = 0, lo = 0, hi = 0;
for (x = X_LO; x <= X_HI; x++)
if (probe(x, SC_U | SC_R, SC_SHIFT) == 'G') {
if (!n) lo = x;
hi = x;
n++;
}
/* Окно обязано существовать: без него трюк непроходим в принципе, а
* уровень 2 без него не пройти. */
TC_TRUE(n > 0);
if (!n) {
dump_map("no-window: ", SC_U | SC_R, SC_SHIFT);
} else {
/* Границы фиксируем: если поедут — это осознанное изменение физики,
* а не случайность. */
TC_EQ(lo, 91);
TC_EQ(hi, 95);
}
}
TC_TEST(grab_window_reachable_by_safe_step)
{
/* Игрок доходит до кромки короткими шагами. Если они не попадают в
* окно, существование окна ничего не стоит. */
TC_EQ(step_x(80, 1), 91);
TC_EQ(step_x(80, 2), 95);
}
TC_TEST(grab_same_for_all_key_patterns)
{
/* Живой игрок держит «вперёд» и жмёт Shift по-разному. Исход не должен
* зависеть от того, отпущена ли стрелка и когда нажат Shift. */
uint8_t x;
for (x = X_LO; x <= X_HI; x++) {
char a = probe(x, SC_U | SC_R, SC_SHIFT);
char b = probe(x, SC_U | SC_R, SC_SHIFT | SC_R);
char c = probe(x, SC_U | SC_R | SC_SHIFT, SC_SHIFT | SC_R);
TC_EQ(b, a);
TC_EQ(c, a);
}
}
int main(void)
{
sc_init();
TC_RUN(grab_window_exists);
TC_RUN(grab_window_reachable_by_safe_step);
TC_RUN(grab_same_for_all_key_patterns);
return 0;
}
@@ -0,0 +1,248 @@
/*
* t_phys.c характеризация физики Кида (pop_map + pop_kid + pop_ctrl).
*
* Назначение набора СТРАХОВКА ПОД РЕФАКТОРИНГ, а не проверка
* правильности. Эталоны здесь сняты с текущего билда, то есть
* консервируют и текущие баги; правильность по-прежнему проверяется
* против SDLPoP. Смысл в другом: перенос физики с `Kid.` на `Char.`
* обязан быть поведение-сохраняющим, и «трасса до == трасса после» ловит
* ровно тот класс ошибок, который там возможен.
*
* Поэтому при осознанном изменении поведения эталон ПЕРЕСНИМАЕТСЯ (см.
* SC_RECORD ниже), а в коммите должно быть написано, почему трасса
* поменялась. Молча обновлённый эталон обесценивает весь набор.
*
* Снятие эталона:
* make -C applications/PoP/roomtest/tests-host t_phys SC_RECORD=1
* Трассы уходят в отчёт tcheck готовыми C-строками вставить в golden_*.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
#define E 0
#define F 1
#define P 3
#define L 11
#define W 20
/* Ровный пол во всю комнату: базовая сцена для ходьбы/бега/разворота. */
static const uint8_t room_flat[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
};
/* Пол с обрывом после колонки 5: падение с ряда 1 на ряд 2. */
static const uint8_t room_ledge[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, E, E, E, E,
W, F, F, F, F, F, F, F, F, W,
};
/* Проваливающаяся плита на (1,4) — сцена под loose-механику. */
static const uint8_t room_loose[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, L, F, F, F, F, F,
W, F, F, F, F, F, F, F, F, W,
};
/* Провал в ТРИ пустых тайла (колонки 2,3,4) — геометрия из BUG-RJUMP-1
* (ур. 2, комнаты 1 и 9). Разбег-прыжок влево с колонки 5 обязан
* доставать до колонки 1; у нас Кид сваливается в провал. */
static const uint8_t room_gap3[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, E, E, E, F, F, F, F, F,
W, F, F, F, F, F, F, F, F, W,
};
/* SC_RECORD=<n> — снять эталон СЦЕНАРИЯ n (нумерация в порядке вызовов в
* main). По одному за прогон: лог tcheck 768 байт, на все трассы сразу
* его не хватает. Перебором занимается record_golden.py. */
#ifndef SC_RECORD
#define SC_RECORD 0
#endif
/* Снять или сверить — одной строкой в каждом сценарии, чтобы режимы не
* разъезжались. Имя сценария = имя файла эталона. */
#if SC_RECORD
static uint8_t sc_idx;
#define SC_CHECK(g, nm) \
do { if (++sc_idx == (SC_RECORD)) sc_dump(nm); } while (0)
#else
#define SC_CHECK(g, nm) sc_expect((g), (uint8_t)(sizeof(g) / SC_REC_BYTES))
#endif
/* ---- эталоны (сняты SC_RECORD=1) ------------------------------------ *
* В режиме снятия эталонов файлов golden/ ещё нет и не должно быть:
* первый прогон их и порождает. */
#if !SC_RECORD
static const uint8_t golden_stand[] = {
#include "golden/stand.h"
};
static const uint8_t golden_run_right[] = {
#include "golden/run_right.h"
};
static const uint8_t golden_step_shift[] = {
#include "golden/step_shift.h"
};
static const uint8_t golden_turn[] = {
#include "golden/turn.h"
};
static const uint8_t golden_crouch[] = {
#include "golden/crouch.h"
};
static const uint8_t golden_jump_up[] = {
#include "golden/jump_up.h"
};
static const uint8_t golden_fall_ledge[] = {
#include "golden/fall_ledge.h"
};
static const uint8_t golden_loose[] = {
#include "golden/loose.h"
};
static const uint8_t golden_rjump_gap3[] = {
#include "golden/rjump_gap3.h"
};
#else
/* SC_RECORD: имена нужны только чтобы SC_CHECK() раскрылся. */
#define golden_stand 0
#define golden_run_right 0
#define golden_step_shift 0
#define golden_turn 0
#define golden_crouch 0
#define golden_jump_up 0
#define golden_fall_ledge 0
#define golden_loose 0
#define golden_rjump_gap3 0
#endif
/* ---- сценарии -------------------------------------------------------- */
TC_TEST(phys_stand_is_stable)
{
/* Никакого ввода: Кид на ровном полу обязан стоять неподвижно. Тест
* дешёвый, но ловит самое обидное «сам поехал» после правки
* геометрии или окна Char. */
sc_room(room_flat, 1);
sc_kid_at(3, 1, -1);
sc_trace_clear();
sc_run(0, 12);
SC_CHECK(golden_stand, "stand");
}
TC_TEST(phys_run_right)
{
sc_room(room_flat, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 24);
SC_CHECK(golden_run_right, "run_right");
}
TC_TEST(phys_step_with_shift)
{
/* Shift + вперёд — осторожный шаг: одна из самых чувствительных к
* distance_to_edge веток. */
sc_room(room_flat, 1);
sc_kid_at(3, 1, 0);
sc_trace_clear();
sc_run(SC_R | SC_SHIFT, 20);
SC_CHECK(golden_step_shift, "step_shift");
}
TC_TEST(phys_turn_around)
{
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
sc_trace_clear();
sc_run(SC_L, 16);
SC_CHECK(golden_turn, "turn");
}
TC_TEST(phys_crouch_and_stand_up)
{
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
sc_trace_clear();
sc_run(SC_D, 8);
sc_run(0, 12);
SC_CHECK(golden_crouch, "crouch");
}
TC_TEST(phys_jump_up_nothing_above)
{
/* Над головой пусто — seq_28; проверяем и смещение по x, которое
* оригинал делает перед прыжком. */
sc_room(room_flat, 1);
sc_kid_at(4, 1, -1);
sc_trace_clear();
sc_run(SC_U, 20);
SC_CHECK(golden_jump_up, "jump_up");
}
TC_TEST(phys_run_off_ledge)
{
/* Бег в обрыв: check_on_floor -> start_fall -> land на ряду 2.
* Сценарий, вокруг которого крутился BUG-FALL-SWORD-1. */
sc_room(room_ledge, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 34);
SC_CHECK(golden_fall_ledge, "fall_ledge");
}
TC_TEST(phys_loose_floor_breaks)
{
/* Пробежка по loose-плите: тайл обязан затрястись и провалиться, а
* физика сообщить об этом соседям (trob + перерисовка). */
sc_room(room_loose, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 34);
SC_CHECK(golden_loose, "loose");
#if !SC_RECORD
/* Журнал заглушек: плита обязана была отвалиться (кусок отрывается —
* pop_loose_mob_spawn) и попросить перерисовку. Это уже не
* характеризация, а содержательная проверка заглушка-пустышка её не
* дала бы. Через trob loose у нас НЕ идёт (только пики/ворота), у него
* свой pop_loose_modif поэтому спрашиваем именно про кусок. */
TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0);
TC_TRUE(tk_log_count(TK_REDRAW) > 0);
#endif
}
TC_TEST(phys_running_jump_over_3tile_gap)
{
/* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок.
* Ожидание оригинала: перелёт провала в три тайла и посадка на (1,1).
* Сейчас эталон фиксирует НАШЕ поведение (падение в провал) когда
* баг починим, трасса поменяется осознанно и эталон переснимется. */
sc_room(room_gap3, 1);
sc_kid_at(8, 1, -1);
sc_trace_clear();
sc_run(SC_L, 8); /* разбег */
sc_run(SC_L | SC_U, 26); /* прыжок и посадка */
SC_CHECK(golden_rjump_gap3, "rjump_gap3");
}
/* ---- точка входа ----------------------------------------------------- */
void main(void)
{
sc_init();
TC_RUN(phys_stand_is_stable);
TC_RUN(phys_run_right);
TC_RUN(phys_step_with_shift);
TC_RUN(phys_turn_around);
TC_RUN(phys_crouch_and_stand_up);
TC_RUN(phys_jump_up_nothing_above);
TC_RUN(phys_run_off_ledge);
TC_RUN(phys_loose_floor_breaks);
TC_RUN(phys_running_jump_over_3tile_gap);
}
@@ -0,0 +1,95 @@
/*
* t_wall.c удар о стену В ВОЗДУХЕ (BUG-JUMPWALL-1).
*
* Наблюдение с приёмки уровня 3 (комната 14): Кид прыгает, не долетает и
* вместо удара о кладку с последующим сползанием ВДОЛЬ неё пролетает
* сквозь стену «по свободной траектории». Класс отказа шире одной
* комнаты, поэтому проверяем его отдельным набором, а не сценарием в
* t_phys: там характеризация (трасса == эталон), здесь утверждение о
* поведении («сквозь кладку нельзя»), которое не должно молча
* переснашиваться вместе с эталонами.
*
* Оригинал: bumped_fall (seg004:04E4) на ударе гасит `Char.fall_x`, то есть
* горизонтальный снос обнуляется и дальше Кид падает вертикально. Кто
* решает, что удар был, check_collisions (seg004:0004) + check_bumped.
*
* Проверка свипом по стартовому X, а не одним прыжком: исход зависит от
* фазы, и «с одной X работает» ничего не доказывает (ровно так и вышло с
* первым сценарием из центра плиты стена держит).
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#define E 0
#define F 1
#define W 20
/* Комната 14 уровня 3 с одной правкой: шахте колонок 6–7 подложено дно
* (ряд 2), потому что соседних комнат у сцены нет, а в игре Кид улетает
* вниз в комнату 13. Остальное как в данных: площадка (0,8), провал
* (0,6)/(0,7), СТЕНА в колонке 5 рядов 12, площадка (0,5) за ней. */
static const uint8_t room14[30] = {
F, F, F, E, E, F, E, E, F, E,
W, W, W, E, E, W, E, E, W, E,
W, W, W, F, F, W, F, F, W, W,
};
/* Плита (0,8) по X: колонка 8 — это [177, 191), плюс шов с пустой (0,9). */
#define X_LO 177
#define X_HI 196
/* Прыгнуть влево со стартовой X. Возврат — код исхода:
* 'L' долетел до площадки (0,5) и стоит на ней (ряд 0) законно;
* '.' не долетел и упал на дно шахты колонок 67 тоже законно;
* цифра колонка, в которой Кид оказался ЛЕВЕЕ стены, то есть прошёл
* сквозь кладку колонки 5 (это и есть баг). */
static char jump_left_from(uint8_t x)
{
sc_room(room14, 14);
sc_kid_at_x(8, 0, x, -1 /* лицом влево */);
sc_trace_clear();
/* Клавиши держим только на сам прыжок: если удерживать их всю сцену,
* Кид, ДОЛЕТЕВШИЙ до (0,5), тут же прыгает с неё второй раз и исход
* говорит уже не о стене, а о втором прыжке. */
sc_run(SC_L | SC_U, 6);
sc_run(0, 34);
if (!sc_len) return '?';
{
int8_t col = sc_trace[sc_len - 1].col;
int8_t row = sc_trace[sc_len - 1].row;
if (row == 0) return 'L'; /* остался на верхнем ряду */
if (col >= 6) return '.'; /* сполз вдоль стены в шахту */
return (char)('0' + (col < 0 ? 0 : col));
}
}
static void put(char c)
{
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
}
static void puts_(const char *s) { while (*s) put(*s++); }
TC_TEST(wall_stops_undershot_jump)
{
uint8_t x, bad = 0;
/* Карта исходов по всей ширине плиты: 'L' — долетел до (0,5), '.' —
* сполз вдоль стены в шахту, цифра просочился сквозь кладку. */
puts_("x=177..196: ");
for (x = X_LO; x <= X_HI; x++) {
char r = jump_left_from(x);
put(r);
if (r != 'L' && r != '.') bad++;
}
put('\n');
TC_EQ(bad, 0);
}
int main(void)
{
sc_init();
TC_RUN(wall_stops_undershot_jump);
return 0;
}
@@ -14,7 +14,7 @@ seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_
import os import os
import re import re
SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src" SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "SDLPoP", "src")
OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), OUT_BIN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "poc", "res", "kid") "..", "poc", "res", "kid")
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
@@ -44,6 +44,18 @@ def parse_frame_table(txt, name="frame_table_kid"):
return frames return frames
def parse_sword_tbl(txt):
"""sword_tbl[] (seg006:2047) -> список (id, x, y). id 255 = клинка нет.
Индекс sword_frame (cur_frame.sword & 0x3F); один и тот же chtab_0 и у
Кида, и у стража."""
m = re.search(r'const sword_table_type sword_tbl\[\]\s*=\s*\{(.*?)\n\};', txt, re.S)
rows = []
for row in re.findall(r'\{([^}]*)\}', m.group(1)):
a, b, c = [int(p.strip(), 0) for p in row.split(',')]
rows.append((a & 0xFF, b & 0xFF, c & 0xFF))
return rows
def parse_byte_array(txt, name): def parse_byte_array(txt, name):
m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S) m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S)
return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))] return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))]
@@ -69,6 +81,7 @@ def main():
gframes = parse_frame_table(seg006, "frame_tbl_guard") gframes = parse_frame_table(seg006, "frame_tbl_guard")
seqtbl = parse_byte_array(seqc, "original_seqtbl") seqtbl = parse_byte_array(seqc, "original_seqtbl")
offs = parse_word_array(seqc, "original_seqtbl_offsets") offs = parse_word_array(seqc, "original_seqtbl_offsets")
swtbl = parse_sword_tbl(seg006)
with open(OUT, "w") as f: with open(OUT, "w") as f:
f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n" f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n"
@@ -102,7 +115,22 @@ def main():
f.write("#define KID_NSEQ %d\n" % len(offs)) f.write("#define KID_NSEQ %d\n" % len(offs))
f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n") f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n")
f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n") f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n")
f.write("#endif\n")
# sword_tbl — макро-инициализаторами, а не готовыми массивами: так
# 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто
# включил заголовок.
f.write("/* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0.\n"
" * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет.\n"
" * Смещения x/y — ЭКРАННЫЕ (применяются после calc_screen_x_coord),\n"
" * x — «вперёд» по направлению персонажа. */\n")
f.write("#define KID_NSWORD %d\n" % len(swtbl))
for i, (nm, fmt) in enumerate((("ID", "0x%02X"), ("DX", "%d"), ("DY", "%d"))):
# dx/dy — знаковые: печатаем со знаком, иначе int8_t = overflow
vals = [r[i] if i == 0 else (r[i] - 256 if r[i] > 127 else r[i])
for r in swtbl]
f.write("#define KID_SWORD_%s_INIT { \\\n" % nm)
f.write(emit_c(vals, 12, fmt).replace("\n", " \\\n") + " \\\n}\n")
f.write("\n#endif\n")
# kid_data.bin: кадры (по 5 байт) + seqtbl подряд # kid_data.bin: кадры (по 5 байт) + seqtbl подряд
os.makedirs(OUT_BIN_DIR, exist_ok=True) os.makedirs(OUT_BIN_DIR, exist_ok=True)
+5 -3
View File
@@ -26,7 +26,10 @@ OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid")
PAL_BASE = 0x70 # слоты палитры Sprinter под Kid PAL_BASE = 0x70 # слоты палитры Sprinter под Kid
SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F
GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F GUARD_PAL_BASE = 0x90 # chtab_5 (страж) -> слоты 0x90..0x9F
SWORD_IDS = tuple(range(20, 28)) # sword_tbl для кадров 229..236 (найти-убрать) # chtab_0 целиком: sword_tbl (seg006:2047) адресует id 0..33 — подъём меча,
# ножны И вся боёвка. Меч в оригинале ОДИН на всех: и Кид, и страж рисуют
# клинок из этого же chtab_0 (add_sword_to_objtable, seg006:1798).
SWORD_IDS = tuple(range(0, 34))
SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ) SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ)
MASK = (1 << SHIFT) - 1 MASK = (1 << SHIFT) - 1
@@ -150,8 +153,7 @@ def main():
names.append(None) names.append(None)
print(f" (kid стр {pg}: пусто)") print(f" (kid стр {pg}: пусто)")
# chtab_0: меч в руке. Пакуем ТОЛЬКО кадры подъёма/ножен (sword_tbl # chtab_0: меч в руке — весь chtab (id 0..33), индекс в атласе = id.
# 35..42 -> id 20..27); индекс в атласе = id - 20 (бой не портирован).
sw = {} sw = {}
for i, id in enumerate(SWORD_IDS): for i, id in enumerate(SWORD_IDS):
w, h, pix = load_sword(id) w, h, pix = load_sword(id)
+71 -9
View File
@@ -5,6 +5,15 @@
## Ближайшее ## Ближайшее
- [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план:
`docs/host-tests-plan.md`. Обвязка уже готова и обкатана
(`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но
обязательно: это основной продукт репозитория, а покрыт он сейчас
только интеграционными тестами в MAME. Начинать с `time/` и
`stdio/` (нулевые швы), затем геометрия `libbgi/common`, затем
фейковые ESTEX/BIOS в тестовом crt0 — они открывают `file/`, `io/`,
`conio/` и проверку гардов (`_fd_guard`).
Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель
поверх неё** (делитель — просто один слот цепи; так снимается EBUSY поверх неё** (делитель — просто один слот цепи; так снимается EBUSY
на сосуществование со своим тиком приложения). на сосуществование со своим тиком приложения).
@@ -52,6 +61,35 @@
## Auto-banking (memory/banking_roadmap.md) ## Auto-banking (memory/banking_roadmap.md)
- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.**
Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…`
у `sprinter-cc` и константой `const uint8_t n_banks = N;` в
приложении (её читает `crt0_banked` при старте, чтобы выделить
EMM-страницы и загрузить в них образы банков). Рассогласование не
даёт ни ошибки компиляции, ни ошибки линковки — только зависание в
рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
уже есть в шапке `runtime/bank.s`).
**Найдено 2026-08-05** на PoP/roomtest: добавили пятый банк
(`--bank 5=pop_ctrl.c`), забыли `n_banks` — игра доходила до конца
загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз
занял заметно больше, чем сама правка: симптом (зависание в чужом
коде) никак не указывает на причину.
Сверять надо не количество, а **максимальный индекс** банка: `--bank`
нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked`
заполняет `_bank_pages[1..n_banks]` подряд.
Варианты, от простого к правильному:
1. **Проверка после линковки** — вытащить значение `_n_banks` из
`.ihx`/`.map` (это `const` в `_CODE`, адрес известен из карты) и
сравнить с `max(N)` по флагам; не совпало — `sprinter-cc` падает
с внятным сообщением. Ничего не ломает в существующих
приложениях, ставится сразу.
2. **Генерировать самим `sprinter-cc`** — он уже так делает, когда
банков нет (`const unsigned char n_banks = 0;`). Правильнее по
сути (единственный источник истины), но требует убрать константу
из всех банковых приложений, иначе дубль символа: `tests/banked`,
`banktest`, `banklocl`, `bankedbg`, `applications/PoP/roomtest`.
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map, проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка, first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
@@ -279,15 +317,39 @@ Quick wins:
- [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest, - [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest,
2026-07-22) — замеры watchpoint-счётчиками: при стабильном 2026-07-22) — замеры watchpoint-счётчиками: при стабильном
удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт: удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт:
E0 74 + E0 F0 74 при FIFO 3) даёт overrun. Подозрение: dev-MAME E0 74 + E0 F0 74 при FIFO 3) даёт overrun.
не эмулирует прерывание SIO на каждый принятый байт (байты **Уточнение 2026-08-01 (по коду драйвера, прежняя гипотеза
вычерпываются только кадровым IRQ 50 Гц) и/или подаёт пачку без НЕВЕРНА):** dev-MAME per-byte INT ДАЁТ —
реальных ~1 мс/байт — на железе per-byte INT должен делать `mame/sources/MAME/src/mame/sinclair/sprinter.cpp`
overrun'ы редкостью. Проверить mame/sources/MAME/src/mame/ `on_kbd_data()` ставит `m_irqs->in_set<1>()` на каждый принятый
sinclair/sprinter.cpp (путь байта клавиатуры → INT) и при байт. Но тут же заводит `m_irq_off_timer` на **32 такта CPU**, и
желании поправить dev-MAME. Recovery-политика уже терпима к `irq_off()` снимает линию — то есть импульс, пришедшийся на наше
частым overrun'ам (селективный wipe, docs/kbd-games.md), но DI-окно, теряется НАСОВСЕМ (байт остаётся в FIFO до следующего
аккорд «держу →, тапнул ↑» остаётся уязвим. IRQ или кадрового 50 Гц). А DI-окна у нас длинные: ядра
акселератора держат `di` на весь блит (`libbgi/bgi256/
_bgi_blit_cols_raw.c`), это сотни микросекунд против 32 тактов.
**Замерено 2026-08-01 (PoP roomtest, счётчики в MAME): обе
«наши» гипотезы отпали.** (а) `kbd_raw_poll()` из главного цикла
не меняет ничего (9/10 с ним и без); (б) снятие `di` в accel-ядрах
УРОНИЛО машину — режим «акселератор при EI» тут недоступен; и сама
длина DI ни при чём (в замороженном кадре без блитов потерь
БОЛЬШЕ). Байт теряется ДО чтения порта: на 49 прочитанных байт
только 28 входов в клавиатурную ветку, т.е. ~44 % импульсов
запроса не обслужены и 3-байтовый FIFO переполняется. Главный
подозреваемый — `m_irq_off_timer` в sprinter.cpp: он ОДИН на два
источника (экран + клавиатура), `irq_off()` гасит обе линии, так
что кадровое прерывание способно обрезать клавиатурный импульс.
**ЗАКРЫТО в рабочем объёме 2026-08-01:** лечится ПЛОТНЫМ опросом —
`kbd_raw_poll` повешен idle-хуком графики (`gfx_set_idle_hook`,
новый API libbgi) на ожидание кадра, где процессор всё равно
простаивает ~2/3 периода. 35 нажатий с зажатым Shift → 35
дошедших make против 9 из 10 без хука. Пользователь на ручной
проверке отмечает, что редкие пропуски всё же ощущаются — остаток
отложен до финальной полировки, следующий шаг описан там же.
Полный протокол — applications/PoP/roomtest/TASKS.md, KBD-1.
Recovery-политика уже терпима к частым overrun'ам (селективный
wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и
«держу Shift, тапаю ←» остаются уязвимы.
## Known quirks (зафиксированы, обходы в libc) ## Known quirks (зафиксированы, обходы в libc)
+129
View File
@@ -0,0 +1,129 @@
# План: модульные тесты libc и libbgi под ucsim_z80
Статус: **не начато**, задача на будущее. Обвязка уже готова и обкатана —
`testkit/` (см. `testkit/README.md`); первый потребитель —
`applications/PoP/roomtest/tests-host/`. Этот документ — про то, как накрыть
тем же способом основной продукт репозитория.
## Что это НЕ заменяет
В `tests/` уже лежат 62 каталога — это **интеграционные** тесты: одна фича =
одна программа, которая пакуется на дискету и гоняется в MAME
(`docs/mame-autotest.md`). Они проверяют, что API работает на живой машине:
ESTEX, BIOS, диск, экран, тайминги.
Модульные тесты их не отменяют, а дополняют с другой стороны:
| | `tests/` (MAME) | `tests-host` (ucsim) |
|---|---|---|
| что проверяет | работает ли на машине | верна ли логика |
| граничные случаи | 1–2 на фичу | десятки, дёшево |
| время прогона | десятки секунд | миллисекунды |
| ловит | железо, тайминги, банки | арифметику, краевые условия, регрессии |
Правило разделения то же, что уже записано для PoP: **что можно проверить
без железа — проверять в ucsim, MAME оставить железу.**
## Три группы модулей
### 1. Чистая логика — тестируется как есть, швов не нужно
Здесь можно начинать в тот же день, когда задачу возьмут в работу.
**libc:**
- `time/``_tm_is_leap`, `_tm_mdays`, `_tm_month_days`, `_tm_year_days`,
`mktime`, `gmtime`, `localtime`, `asctime`, `ctime`. Классическая
календарная арифметика: високосные годы, границы месяцев, переходы через
год, круговой прогон `mktime(gmtime(t)) == t`. Идеальный первый набор —
много краевых случаев и ноль зависимостей.
- `stdio/``dec_print`, `hex8/16/32`, `_scanf_core`, `sscanf`. Формат и
разбор: ширина, знак, переполнение, мусор на входе.
- `string/`, `stdlib/``strlwr`, `strupr`, `max`, `min`.
**libbgi/common** (115 модулей, почти всё mode-agnostic):
- `_bgi_isqrt`, `_bgi_trig` — сверить с эталонной формулой на всём
диапазоне, ровно как сделано для LCG в `t_geom` у PoP;
- `_bgi_lineseg`, `_bgi_styled_line`, `_bgi_poly_edge`, `_bgi_arc_draw`
геометрия и отсечение: линия целиком вне окна, по диагонали через угол,
вырожденная в точку;
- `_spr_ysort` — порядок сортировки спрайтов;
- `_bgi_hspan`, `_bgi_fill_span` — заливка: краевые span'ы, нулевая ширина.
### 2. Нужны швы — но швы дешёвые
**Фейковый ESTEX и BIOS прямо в тестовом crt0.** Оба вызываются через
`rst #0x10` и `rst #0x08`, то есть через фиксированные векторы в первых
байтах памяти. В тестовом бинаре эти адреса наши: можно положить туда
обработчик, который эмулирует крошечную файловую систему в ОЗУ и текстовый
экран в буфере. Это открывает:
- `file/` (32 модуля) и `io/` (21) — `fopen`/`fread`/`fwrite`/`fseek`,
буферизация (`docs/file-buffering-design.md`), поведение на EOF и
ошибках, `errno`;
- `conio/` (38) — вывод в буфер вместо экрана, проверка атрибутов,
скроллинга, границ окна.
Отдельно ценно: **гарды**, которые обязаны быть в обеих сборках. `_fd_guard`
(девятый `OPEN` вешает DSS) — это ровно тот случай, где нужен тест, а не
вера в комментарий: открыть восемь, убедиться, что девятый вернул ошибку и
не дошёл до ESTEX.
**Кадровый буфер в ОЗУ для libbgi.** Если рисующие ядра умеют писать в
обычный буфер, а не только в видеопамять, растеризацию можно проверять
снимком: нарисовать фигуру, сравнить с эталонным массивом. Начинать с
маленьких (8×8, 16×16) — эталон читаемый прямо в исходнике.
### 3. Только MAME
Банки и W-окна, EMM, реальный ESTEX/DSS, клавиатурный трамплин и IM2,
CBL-звук (`cbl/`), мышь (`mouse/`), `mem/` (это банки и страницы, а не
куча), тайминги и бюджет кадра, ускоритель.
## Обе сборки: fast и safe
Библиотеки собираются в двух вариантах (`-D*_NOCHECK` вырезает
валидацию параметров). Наборы стоит гонять **против обоих**:
- в safe — что валидация ловит мусорные аргументы и ставит `errno`;
- в fast — что вырезание валидации не поменяло поведение на корректных
входах.
Это дешёвая параметризация Makefile (тот же набор, два `OBJS_*`), и она
пресекает целый класс расхождений между вариантами.
## Фазы
1. **Календарь и формат.** `time/`, `stdio/`. Нулевые швы, максимальная
плотность краевых случаев. Цель — обкатать поток работы на libc.
2. **Геометрия libbgi.** `common/`: isqrt, тригонометрия, отсечение линий,
рёбра полигонов. Тоже без швов.
3. **Фейковые ESTEX/BIOS в crt0.** Открывает `file/`, `io/`, `conio/` и
гарды. Самый крупный кусок работы и самая большая отдача.
4. **Растр libbgi по снимкам** — если окажется, что ядра можно нацелить на
буфер в ОЗУ без правок продукта; иначе отложить.
5. **Параметризация fast/safe** — после того, как наборов станет заметно.
## Критерии
- Тест не считается написанным, пока не проверен мутацией: сломать
проверяемое место, убедиться, что набор краснеет. Пустые тесты хуже
отсутствующих.
- Каждый закрытый баг libc/libbgi получает регрессионный кейс, если он
ловится без железа.
- `make host-tests` остаётся быстрым: если суммарно перевалит за несколько
секунд, делить на быстрый и полный прогон.
## Организация
Наборы кладутся рядом с кодом, обвязка общая:
```
libc/tests-host/ наборы по областям: t_time.c, t_stdio.c, …
libbgi/tests-host/ t_geom.c, t_raster.c, …
```
Каждому — `Makefile` на пять строк (`TESTKIT`, `ENGINE_DIR`, `OBJS_*`,
`include`), и добавить каталог в `HOST_TEST_DIRS` корневого `Makefile`.
Подробности — `testkit/README.md`.
+34
View File
@@ -313,6 +313,7 @@ Set 2: `0xF0` — префикс отпускания, `0xE0` — префикс
| `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit | | `void kbd_raw_close(void)` | выключить, вернуть клавиатуру DSS; идемпотентно; висит на atexit |
| `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 | | `uint8_t kbd_raw_down(uint16_t code)` | зажата ли code ПРЯМО СЕЙЧАС (0/1); code вне 0..511 — 0 |
| `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) | | `void kbd_raw_sync(void)` | звать РАЗ В КАДР до опроса: recovery после Rx-overrun SIO — сбрасывает held-состояние всех клавиш КРОМЕ модификаторов (те не перечитываются typematic'ом; docs/kbd-games.md) |
| `uint8_t kbd_raw_poll(void)` | вычерпать FIFO ОПРОСОМ, не дожидаясь прерывания: 0 — было пусто (~40 тактов), 1 — что-то декодировано. Звать МЕЖДУ фазами кадра, после тяжёлых блитов — не раз в кадр (см. ниже, «почему одного прерывания мало») |
| `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) | | `KBD_EXT` | ИЛИ-флаг кода: клавиша была расширенной (0xE0-префикс на проводе) |
| `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно | | `KBD_UP/DOWN/LEFT/RIGHT/SPACE/ENTER/ESC/LSHIFT/RSHIFT` | позиционные коды PS/2 Set 2 — LEFT/ESC/UP подтверждены (см. ниже); остальные — по стандарту, не перепроверены поштучно |
| `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 | | `KBD_LCTRL/LALT/RCTRL/RALT` | коды модификаторов (R* — расширенные, с KBD_EXT); добавлены 2026-07-22 |
@@ -335,6 +336,39 @@ git как урок: при повторных «нет эффекта» на с
брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться брейкпоинтом/watchpoint'ом на конкретный адрес кода, не полагаться
только на визуальный снимок с произвольным таймингом. только на визуальный снимок с произвольным таймингом.
**ПОЧЕМУ ОДНОГО ПРЕРЫВАНИЯ МАЛО (`kbd_raw_poll`, 2026-08-01).** Приёмный
FIFO SIO — 3 байта, и рассчитывать на «каждый байт разбудит нас» нельзя:
запрос прерывания клавиатуры держится единицы микросекунд (в dev-MAME —
32 такта CPU, `sprinter.cpp` `on_kbd_data``irq_off_timer`), а ядра
акселератора держат `DI` на весь блит — сотни микросекунд. Импульс,
попавший в такое окно, теряется насовсем; байт лежит в FIFO до следующего
прерывания (следующий байт либо кадровое, 50 Гц). Трафик же идёт пачками:
тап стрелки = 5 байт, а при УДЕРЖИВАЕМОМ Shift PS/2 обрамляет расширенный
код «фиктивным шифтом» (`E0 F0 12``E0 12`) — по 5 байт и на нажатие, и
на отпускание. Три ячейки такую пачку не держат: потерянный make = «нажатие
не сработало», потерянный break = залипание. Отсюда `kbd_raw_poll()`
вычерпывание тем же декодером, но из главного цикла. Тело идёт под `DI`
(чтение порта 0x18 деструктивно, а read-modify-write карты гонится с
трамплином) и БЕЗУСЛОВНО делает `EI` на выходе — из ISR звать нельзя.
**Как её звать (замеры 2026-08-01, PoP roomtest — читать до применения!).**
Расстановка «несколько вызовов за кадр, после тяжёлых фаз» **не даёт
ничего**: 9 дошедших make-байт из 10 нажатий и с ней, и без неё — такие
вызовы попадают ровно в участки с разрешёнными прерываниями и лишь
дублируют трамплин. Там же измерено, что длина DI-окон графики на потери
НЕ влияет (в кадре вообще без блитов потерь больше), а теряется байт ДО
чтения порта: примерно 44 % импульсов запроса прерывания не обслуживаются,
и трёхбайтовый FIFO переполняется.
**Работает только ПЛОТНЫЙ опрос — порядка раза в 0.5 мс.** Штатный способ
взять эту частоту даром — повесить функцию idle-хуком графики:
`gfx_set_idle_hook()` (`<gfx.h>`) зовёт её, пока `gfx_wait_vsync` крутит
опрос луча, а это ~2/3 периода кадра. Проверено: 35 нажатий стрелки с
зажатым Shift → 35 дошедших make против 9 из 10 без хука. Остаточные
редкие потери возможны (пачка целиком внутри DI-окна одного accel-прохода);
полный протокол и что делать дальше — `applications/PoP/roomtest/TASKS.md`,
задача KBD-1.
**ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/ **ГЛАВНОЕ СЛЕДСТВИЕ:** пока `kbd_raw_open()` активен, `kbhit/getch/getkey/
kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт kbd_mod_state` НЕ получают новых событий (в т.ч. CTRLKEY подряд отдаёт
то же самое, что было на момент открытия — резидентный обработчик DSS, то же самое, что было на момент открытия — резидентный обработчик DSS,
+7 -13
View File
@@ -4,7 +4,6 @@ argv 3431
assrtest 3847 assrtest 3847
atlas 9193 atlas 9193
attrprob 4087 attrprob 4087
balls 6031
banked 1056 banked 1056
bankedbg 1067 bankedbg 1067
banklocl 4683 banklocl 4683
@@ -15,9 +14,9 @@ bios_text 4461
blitperf 5860 blitperf 5860
blitw 4939 blitw 4939
cat 913 cat 913
cblstream 6431 cblstream 6464
cbltest 6580 cbltest 6613
cblwav 6648 cblwav 6681
conio 4605 conio 4605
conio2 3929 conio2 3929
dec_test 860 dec_test 860
@@ -27,30 +26,25 @@ fdmax 6023
filetest 10547 filetest 10547
fpsdiv 4870 fpsdiv 4870
gets 509 gets 509
gfx_dbuf 5112 gfx_dbuf 5134
gfx_demo 4117 gfx_demo 4117
gfxbanks 6044 gfxbanks 6044
hello 4167 hello 4167
hello2 4244 hello2 4244
irqtest 6647 irqtest 6680
kbdpoll 1219
kbdraw 4911 kbdraw 4911
ls 4835 ls 4835
malloc 4461 malloc 4461
mdview 18878
mdview2 27031
mem_test 4555 mem_test 4555
mouse 4382 mouse 4382
openenv 6124 openenv 6124
palfile 5406 palfile 5406
ptime 5744 ptime 5744
rpgprof 12082 rt_test 5187
rpgwalk 10309
rt_test 5154
scroll 2974
seek 4188 seek 4188
simple 955 simple 955
solidt 11531 solidt 11531
space 11272
spranim 9431 spranim 9431
spriteclip 4119 spriteclip 4119
sprites 6791 sprites 6791
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
4 assrtest
5 atlas
6 attrprob
balls
7 banked
8 bankedbg
9 banklocl
14 blitperf
15 blitw
16 cat
17 cblstream
18 cbltest
19 cblwav
20 conio
21 conio2
22 dec_test
26 filetest
27 fpsdiv
28 gets
29 gfx_dbuf
30 gfx_demo
31 gfxbanks
32 hello
33 hello2
34 irqtest
35 kbdpoll
36 kbdraw
37 ls
38 malloc
mdview
mdview2
39 mem_test
40 mouse
41 openenv
42 palfile
43 ptime
44 rpgprof rt_test
rpgwalk
rt_test
scroll
45 seek
46 simple
47 solidt
space
48 spranim
49 spriteclip
50 sprites
+8 -5
View File
@@ -175,11 +175,14 @@ void gfx_scroll_v(uint8_t src_page, uint8_t dst_page,
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4). спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md` 2. **ОЗУ-копия per-page или общая.** Ранний план дабл-буфера PoP исходил из
пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется «теневая копия одна — общая»; **практикой это опровергнуто**: у каждой
по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных** страницы СВОЯ теневая ОЗУ-копия, heal берёт фон из копии той страницы, в
положений камеры во время скролла → пинг-понг (§8) сломается. Проверить: которую рисуем (`applications/PoP/roomtest/CLAUDE.md`, раздел
записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих. «Дабл-буфер»; `roomtest.c enter_room` рисует фон в обе страницы по
отдельности именно поэтому). Значит пинг-понг (§8) в принципе рабочий,
но перед опорой на него — подтвердить на своём коде: записать разный фон
в page0 и page1 банком `0x50`, сверить чтение обеих.
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между 3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
fill и flush одного burst'а. Доковый straight-copy этого не проверяет. fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
+5
View File
@@ -347,6 +347,11 @@ void _bgi_poly_edge(int x0, int y0, int x1, int y1);
extern void _cbl_port_ref(void); extern void _cbl_port_ref(void);
extern void _cbl_port_unref(void); extern void _cbl_port_unref(void);
/* Idle-хук ожидания кадра (common/_gfx_idle_state.c, ставится
* gfx_set_idle_hook). NULL = нет; зовётся из лучевого цикла
* gfx_wait_vsync. */
extern void (*_gfx_idle_hook)(void);
/* ---- FPS-делитель (frame pacing) --------------------------------- * /* ---- FPS-делитель (frame pacing) --------------------------------- *
* Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные * Фоновый счётчик кадров через цепочку кадровых IRQ (libc/irq). Данные
* в common/_gfx_fps_state.c; при _gfx_fps_div<=1 путь не активен и * в common/_gfx_fps_state.c; при _gfx_fps_div<=1 путь не активен и
+10
View File
@@ -0,0 +1,10 @@
/*
* _gfx_idle_state указатель на idle-хук, который gfx_wait_vsync зовёт,
* пока ждёт луч. Отдельный data-модуль: на него ссылаются и установщик
* (gfx_set_idle_hook), и сам цикл ожидания, а тянуть один из-за другого
* в программу, которая хук не ставит, ни к чему. Без инициализации
* (crt0 зануляет _DATA NULL = хука нет).
*/
#include "../_bgi.h"
void (*_gfx_idle_hook)(void);
+4 -43
View File
@@ -9,9 +9,9 @@
* сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт * сверху для column-major бесплатна: колонка = непрерывный кусок ОЗУ, старт
* просто сдвигается на skip байт, длина accel-блока уменьшается. * просто сдвигается на skip байт, длина accel-блока уменьшается.
* *
* Здесь же живёт полное тело блита колонками gfx_blit_cols() тонкая * Тело (вместе с клип-логикой) в gfx_blit_cols_part_w.c: этот вызов =
* обёртка (skip=0, rows=0), чтобы не дублировать клип-логику. * «без ограничения ширины». Подробности зеркала/клипа см. шапку
* Подробности зеркала/клипа см. шапку gfx_blit_cols.c. * gfx_blit_cols.c.
*/ */
#include <stdint.h> #include <stdint.h>
#include "../_bgi.h" #include "../_bgi.h"
@@ -19,44 +19,5 @@
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip, void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows) int skip, int rows)
{ {
const uint8_t *p = (const uint8_t *)img; gfx_blit_cols_part_w(x, y, img, flip, skip, rows, 0);
int full_w = p[0] | (p[1] << 8);
int full_h = p[2] | (p[3] << 8);
const uint8_t *px = p + 4;
int w = full_w, h;
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
int sy; /* пропуск строк сверху колонки */
int cx = 0; /* пропуск колонок слева экрана */
const uint8_t *src;
uint8_t *dst;
if (full_w <= 0 || full_h <= 0)
return;
if (skip < 0) skip = 0;
if (skip >= full_h)
return; /* обрезано целиком */
sy = skip;
h = full_h - skip;
if (rows > 0 && rows < h)
h = rows;
/* Клип по экрану — только если хоть один край вылезает. */
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
if (x < 0) { cx = -x; w += x; x = 0; }
if (y < 0) { sy -= y; h += y; y = 0; }
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
if (w <= 0 || h <= 0) return;
}
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. */
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
if (cx)
src += cx * sstride;
src += sy;
_bgi_begin();
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
_bgi_end();
} }
+49
View File
@@ -0,0 +1,49 @@
/*
* gfx_blit_cols_part_noclip блит спрайта column-major БЕЗ клипа по экрану,
* с обрезкой сверху (как gfx_blit_cols_part). Пара к gfx_blit_cols_part
* ровно так же, как gfx_blit_part_noclip к gfx_blit_part.
*
* Зачем: у персонажей PoP спрайт почти всегда целиком на экране, а общий
* gfx_blit_cols_part платит за проверку четырёх границ, пересчёт w/h/cx/sy и
* 16-битную арифметику независимо от того, вылезает край или нет замер в
* MAME (roomtest, страж + клинок): 5 622 такта подготовки против 13 596 на
* сам accel-проход. Здесь та же колонка уходит в _bgi_blit_cols_raw без
* подготовки.
*
* Требования (проверяет ВЫЗЫВАЮЩИЙ, кода проверок здесь нет):
* - прямоугольник назначения целиком на экране;
* - ширина и высота кадра <= 255, y <= 255;
* - skip < высоты кадра.
* Не выполняется зовите gfx_blit_cols_part.
*
* Флип, как и в gfx_blit_cols_part, бесплатный: подаём src с последней
* колонки и отрицательный страйд accel сам кладёт её в левый x.
*
* W3-скобка ставится ЗДЕСЬ (код libbgi живёт в W1): из приложения,
* собранного с --w3, её вызывать нельзя после _bgi_begin окно W3 занято
* видеобанком и код вызывающего исчезает из адресного пространства.
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows)
{
const uint8_t *p = (const uint8_t *)img;
uint8_t w = p[0]; /* ширина кадра (< 256) */
uint8_t fh = p[2]; /* высота кадра (< 256) */
const uint8_t *src = p + 4;
uint8_t h;
if (w == 0 || fh == 0 || skip >= fh) return;
h = (uint8_t)(fh - skip);
if (rows && rows < h) h = rows;
if (flip) src += (uint16_t)(w - 1) * fh; /* последняя колонка -> левый x */
src += skip;
_bgi_begin();
_bgi_blit_cols_raw(src, (uint8_t *)(_gfx_addr_base + (uint16_t)x),
w, h, flip ? -(int)fh : (int)fh, (uint8_t)y);
_bgi_end();
}
+70
View File
@@ -0,0 +1,70 @@
/*
* gfx_blit_cols_part_w блит спрайта column-major с обрезкой СВЕРХУ
* (skip/rows) И ОГРАНИЧЕНИЕМ ШИРИНЫ (maxw): рисуются только первые maxw
* колонок футпринта, считая от x. maxw = 0 «вся ширина кадра».
*
* Здесь живёт ПОЛНОЕ тело блита колонками; gfx_blit_cols_part (maxw=0) и
* gfx_blit_cols (ещё и skip=rows=0) тонкие обёртки, чтобы клип-логика не
* дублировалась. Подробности зеркала/клипа см. шапку gfx_blit_cols.c.
*
* Зачем ограничение ширины. Обрезка СПРАВА для column-major это ровно
* уменьшение числа колонок, то есть внутри уже есть (так же клипается край
* экрана: `w = _bgi_maxx + 1 - x`), наружу просто не было выведено.
* Понадобилось для порта clip_char в PoP: персонаж, поднимающийся по
* лестнице за дверью уровня, обязан уходить за правый косяк проёма
* (obj_clip_right), а дверь рисуется ПОД ним и перекрыть его не может.
*
* Важно: ограничение работает и при flip. При зеркале источник читается с
* последней колонки к первой (sstride < 0), но первые w нарисованных
* колонок в любом случае ложатся в ЛЕВУЮ часть футпринта [x, x+w) то
* есть уменьшение w всегда срезает справа, как и требуется.
*/
#include <stdint.h>
#include "../_bgi.h"
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t maxw)
{
const uint8_t *p = (const uint8_t *)img;
int full_w = p[0] | (p[1] << 8);
int full_h = p[2] | (p[3] << 8);
const uint8_t *px = p + 4;
int w = full_w, h;
int sstride = flip ? -full_h : full_h; /* шаг между колонками в ОЗУ */
int sy; /* пропуск строк сверху колонки */
int cx = 0; /* пропуск колонок слева экрана */
const uint8_t *src;
uint8_t *dst;
if (full_w <= 0 || full_h <= 0)
return;
if (maxw && (int)maxw < w)
w = (int)maxw; /* обрезка справа (см. шапку) */
if (skip < 0) skip = 0;
if (skip >= full_h)
return; /* обрезано целиком */
sy = skip;
h = full_h - skip;
if (rows > 0 && rows < h)
h = rows;
/* Клип по экрану — только если хоть один край вылезает. */
if (x < 0 || y < 0 || x + w > _bgi_maxx + 1 || y + h > _bgi_maxy + 1) {
if (x < 0) { cx = -x; w += x; x = 0; }
if (y < 0) { sy -= y; h += y; y = 0; }
if (x + w > _bgi_maxx + 1) w = _bgi_maxx + 1 - x;
if (y + h > _bgi_maxy + 1) h = _bgi_maxy + 1 - y;
if (w <= 0 || h <= 0) return;
}
/* Первая ВИДИМАЯ колонка (с учётом флипа) + первая видимая строка. */
src = flip ? (px + (uint16_t)(full_w - 1) * (uint16_t)full_h) : px;
if (cx)
src += cx * sstride;
src += sy;
_bgi_begin();
dst = (uint8_t *)(_gfx_addr_base + (uint16_t)x);
_bgi_blit_cols_raw(src, dst, (uint8_t)w, (uint8_t)h, sstride, (uint8_t)y);
_bgi_end();
}
+27
View File
@@ -0,0 +1,27 @@
/*
* gfx_set_idle_hook что вызывать, пока gfx_wait_vsync ждёт луч.
*
* ЗАЧЕМ. Ожидание кадра это самый длинный простой в кадре: у игры с
* пейсингом «3 растровых кадра на логический тик» процессор проводит там
* ~42 мс из 60, крутя `in a,(#0xFE)` и больше ничего не делая. Хук
* позволяет занять это время полезным опросом, не отнимая ни такта у
* полезной работы.
*
* Первый потребитель (и повод завести) клавиатура: приёмный FIFO SIO
* всего 3 байта, а одно нажатие расширенной клавиши при зажатом Shift
* даёт пачку из 5 байт («fake shift»); если импульс запроса прерывания
* потерян, байты копятся и один теряется. Лечится вычерпыванием по
* опросу (`kbd_raw_poll` из <kbd_raw.h>) но только ПЛОТНЫМ, раз в
* ~0.5 мс, а не пару раз за кадр. Замер: applications/PoP/roomtest/
* TASKS.md, задача KBD-1.
*
* ТРЕБОВАНИЯ К ХУКУ: он зовётся в тесном цикле, поэтому обязан быть
* дешёвым и НЕ рисовать (окно W3 в этот момент принадлежит вызывающему,
* а не графике) и не ждать сам. fn = 0 снять хук.
*/
#include "../_bgi.h"
void gfx_set_idle_hook(void (*fn)(void))
{
_gfx_idle_hook = fn;
}
+22
View File
@@ -62,6 +62,7 @@ static void gfx_wait_vsync_beam(void)
;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) --- ;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) ---
ld bc, #0 ; ~65536 попыток на фазу таймаут ld bc, #0 ; ~65536 попыток на фазу таймаут
_gwv_wait1: _gwv_wait1:
call _gwv_idle ; занять простой (см. gfx_set_idle_hook)
in a, (#0xFE) in a, (#0xFE)
bit 5, a bit 5, a
jr nz, _gwv_have1 jr nz, _gwv_have1
@@ -75,6 +76,7 @@ static void gfx_wait_vsync_beam(void)
;; --- фаза 2: дождаться bit5==0 (сам переход начало кадра) --- ;; --- фаза 2: дождаться bit5==0 (сам переход начало кадра) ---
ld bc, #0 ld bc, #0
_gwv_wait0: _gwv_wait0:
call _gwv_idle
in a, (#0xFE) in a, (#0xFE)
bit 5, a bit 5, a
jr z, _gwv_done jr z, _gwv_done
@@ -83,6 +85,26 @@ static void gfx_wait_vsync_beam(void)
or c or c
jr nz, _gwv_wait0 jr nz, _gwv_wait0
;; таймаут и здесь переходим к тому же фолбэку ;; таймаут и здесь переходим к тому же фолбэку
jr _gwv_fallback
;; --- вызов idle-хука ------------------------------------- *
;; BC (счётчик таймаута) сохраняем: хук обычная C-функция и
;; клобберит всё, кроме IX. Косвенный вызов через push адреса
;; возврата + jp (hl): `call (hl)` в Z80 нет. Цена холостого
;; прохода около двух десятков тактов в цикле, который и так
;; сжигает время впустую.
_gwv_idle:
ld hl, (__gfx_idle_hook)
ld a, h
or l
ret z ; хук не поставлен
push bc
ld de, #_gwv_idle_ret
push de ; адрес возврата для jp (hl)
jp (hl)
_gwv_idle_ret:
pop bc
ret
_gwv_fallback: _gwv_fallback:
ei ei

Some files were not shown because too many files have changed in this diff Show More