Files
Sprinter-SDCC/applications/SprPoP/src/sprpop_cold.h
T
snark13 4e43890fce SprPoP: финал больше не убивает программу — прямой вызов в чужой банк
Пройденная игра доходила до таблицы рекордов и умирала: программа
исчезала, машина следом вставала намертво (di;halt на 0x0000) либо уходила
в reset.  Одинаково из Flex Navigator и из голого DSS.

КОРЕНЬ.  pop_ui.h объявлял группу pop_text_*_mapped БЕЗ __banked.  Пока
pop_hof.c лежал в банке 9 рядом с pop_ui.c, прямой call был верен; после
переноса pop_hof/pop_config/pop_pal в банк 10 тот же call стал уходить в
пустой хвост чужого банка.  Процессор полз по 0xFF до 0x0000, где ловушка
DSS ставит B=0x27 и сворачивает процесс — подмена страниц W1/W2/W3,
которую было видно на трупе, оказалась уборкой, а не причиной.

Точную инструкцию (call $E503 = _pop_text_map банка 9) дала трассировка
MAME на узком участке: trace включалась брейкпоинтом на входе в
pop_hof_show и выключалась на процедуре завершения процесса DSS (0x1E56).

ЧТО СДЕЛАНО

* pop_ui.h/.c — группа text_*_mapped помечена __banked.
* toolchain/check_bank_calls.py — две проверки банкового кода:
  1) прямой call в чужой банк (доказательна, ВАЛИТ сборку — проверено
     намеренной поломкой);
  2) указатель на данные своего банка, отданный в чужой (эвристика по
     форме кода, только предупреждает).
  Встроена в app.mk, запускается сразу после линковки.
* pop_hof.c — курсор ввода строится на стеке: литерал "_" лежал в _BANK10
  и после пометки __banked уезжал из-под ног чужому банку, заливая экран
  знаками вопроса.
* libc: kbd_raw_keypad_as_ext() — kbd_raw_sync переносит голые коды
  нумпада в EXT-половину карты.  Лечит залипание стрелок (потерянный
  префикс E0 сажал make в PLAIN как код нумпада, и снять его было нечем),
  заодно нумпад стал управлением: 7/8/9, 4/6, 2 и 5 = вниз.
* pop_pace.c — цикл ожидания луча зовёт тот же idle-хук, что и
  gfx_wait_vsync: без этого F10 в геймплее не работал вовсе.
* pop_hof.c — Esc в таблице рекордов отменяет запись (расхождение с
  оригиналом записано в docs/impl_diff.md).
* Экран версии показывается только через Menu/Settings/About: стартовый
  показ и Ctrl+V убраны, мёртвый код снят.
* sprpop_cold.c — pop_start_level зовёт pop_hp_invalidate: после Ctrl+A с
  выросшим за уровень максимумом полоса HP моргала между страницами.

Разбор всех четырёх багов — в applications/PoP/roomtest/BUGS_CLOSED.md
(FINAL-BANKCALL, FINAL-HOF-GARBAGE, KBD-ARROW-PHANTOM, F10-GAMEPLAY),
правило про банки — в applications/SprPoP/CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 22:11:01 +03:00

132 lines
9.6 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* sprpop_cold.h — контракт между главным циклом (резидент W1/W2) и его
* ХОЛОДНОЙ половиной, вынесенной в банк (sprpop_cold.c).
*
* Зачем. В режиме huge под кучу остаётся то, что не занял _CODE+_DATA, и к
* уровню 8 запас упал ниже килобайта. Главный цикл при этом наполовину
* состоит из кода, который отрабатывает РАЗ НА УРОВЕНЬ (старт/рестарт) или
* вообще только по отладочной клавише (обход комнат, лейбл номера) — держать
* его в дефицитном окне незачем. Кадровый путь (tick/draw/флип) остался в
* резиденте целиком: цена выноса — один трамплин на вызов, а он тут ноль раз
* за обычный кадр.
*
* Что ОСТАЛОСЬ в sprpop.c и почему: enter_room_side (её зовут и холодная
* половина, и переходы между комнатами в кадре падения) и рабочие массивы
* комнаты — они же и есть то состояние, ради которого функция вообще
* существует; банковый модуль читает их как обычные глобалы (данные банков
* линкуются в общий _DATA, --bank-data мы не включаем).
*/
#ifndef ROOMTEST_COLD_H
#define ROOMTEST_COLD_H
#include <stdint.h>
/* ---- то, что холодная половина берёт у главного цикла ---------------- */
/* Рабочие массивы комнаты: заполняет вход в комнату (банк), читают и он, и
* главный цикл (шов, straddle, чит-навигация). Данные банковых модулей
* линкуются в общий _DATA, поэтому обе половины видят их как обычные
* глобалы — в банк уехал только КОД. */
extern uint8_t room_fg[30], room_bg[30];
extern uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
extern uint8_t above_fg[10], above_mod[10];
extern uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (straddle) */
extern uint8_t seam_sig[3]; /* последняя нарисованная openness в шве */
extern uint8_t seam_rows, seam_redraw;
/* Сигнатура ряда шва: openness ворот соседа либо «кнопка нажата». Живёт в
* резиденте — её зовёт и кадровый путь главного цикла, и вход в комнату. */
uint8_t seam_row_sig(const uint8_t *m, const uint8_t *lfg, uint8_t r);
extern uint8_t cur_room; /* её номер (drawn_room) */
extern uint8_t pop_checkpoint; /* checkpoint (seg000:270) — ставит уход */
/* влево из комнаты 7 уровня 3 */
/* Вход в комнату. side — куда ушёл КИД (0 L, 1 R, 2 U, 3 D); 0xFF = «не
* переход за Кидом» (старт уровня, респавн, чит-навигация): от этого
* зависит, пойдёт ли страж следом. enter_room — обёртка с 0xFF.
* Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */
void enter_room_side(uint8_t room, uint8_t side) __banked;
void enter_room(uint8_t room) __banked;
/* Пересобрать только производные кэши и обе страницы после QuickLoad.
* Игровые таймеры, trobs, mobs и персонажей не изменяет. */
void pop_qsave_restore_room(uint8_t room) __banked;
/* ---- холодная половина ----------------------------------------------- */
/* start_level (seg003:0055) + do_startpos + set_start_pos: поставить Кида в
* стартовую позицию уровня. Один вход и для запуска, и для рестарта после
* смерти / выпадения из уровня — в оригинале это буквально один путь. */
void pop_start_level(void) __banked;
/* ОТЛАДОЧНЫЙ обход комнат по номеру ('+' / ''). dir: 1 — следующая,
* 2 — предыдущая. Ставит Кида на подходящий пол новой комнаты и выводит
* его из боя. Возвращает 1, если комната сменилась (вызывающий обязан
* вернуть себе draw-страницу и обнулить счётчик кадров смерти). */
uint8_t pop_dbg_roomnav(uint8_t dir) __banked;
/* Читы по фронту нажатия (K/I/S/Shift+L/U/[/]/F7/F8). В банке: раз в кадр,
* а резидент W1/W2 переполнен — см. шапку функции. */
void pop_cheat_tick(void) __banked;
/* Кто рисуется ПОЗЖЕ — Кид или соперник (порядок обхода тайлов оригинала,
* set_objtile_at_char + sort_curr_objs). 1 = соперник поверх. Зовётся раз
* в кадр, поэтому живёт в банке: трамплин на кадр дешевле полукилобайта
* резидента. */
uint8_t guard_over_kid(void) __banked;
/* Перевернуть уже нарисованное (зелье инверсии, уровень 9): две копии
* акселератором + инвалидация слотов персонажей и полосы HP. Звать РАЗ на
* переключение, по флагу pop_upside_dirty. */
void pop_flip_screen(void) __banked;
/* ---- Запуск / смена уровня / завершение ------------------------------ *
* Три куска главного цикла с частотой «раз за игру» или «раз на уровень».
* В резиденте от них остались только вызовы. */
/* Загрузить ресурсы, поднять графику и палитру, поставить Кида на старт и
* нарисовать первый экран. 0 — OK, -1 — фатально (сообщение уже выведено). */
int pop_boot(void) __banked;
/* Перейти на pop_next_level: файл уровня, спрайты соперника, тайлсет,
* старт. На время I/O закрывает CBL и оставляет его закрытым: caller обязан
* вызвать pop_sfx_start ПОСЛЕ palette/fade/восстановления snapshot. Если
* файла нет — молча остаться на текущем. -1 — фатально. */
int pop_level_switch(void) __banked;
/* Смена уровня целиком (катсцена, гашение, загрузка, палитра, звук).
* back — страница кадра главного цикла. 0 — ок, -1 — сбой. */
int8_t pop_level_change(uint8_t back) __banked;
/* Клавиши служебного слоя. Живут здесь, а не в sprpop.c: сам обработчик
* (pop_frame_ui) переехал в банк, а главный цикл о них больше не знает. */
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
/* МЕНЮ — Backspace, как в оригинале (keys.txt); Esc оставлен дублёром,
* потому что у SDLPoP он тоже открывает меню по умолчанию. */
#define KBD_MENU 0x66 /* Backspace */
#define KBD_MENU_ALT 0x76 /* Esc — дублёр */
/* F10 больше НЕ здесь: он ловится в kbd_idle (sprpop.c) и работает везде, а
* не только в игровом цикле. Тут остаётся Ctrl+Q — вторая клавиша выхода
* из keys.txt. */
#define KBD_QUIT 0x15 /* Q — вместе с Ctrl */
#define KBD_RESTART 0x1C /* A — Ctrl+A, рестарт уровня */
#define KBD_INTRO 0x2D /* R — Ctrl+R, вернуться в заставку */
#define KBD_SHOWTIME 0x29 /* Space — сколько осталось (seg000:612) */
#define KBD_TIMER 0x2C /* T — постоянный показ таймера. БЕЗ модификаторов:
* Shift+T в keys.txt — «добавить HP» (фаза C) */
/* Меню, F10 и QuickSave/Load на границе кадра. restart_level/dead_reset —
* флаги для главного цикла. 0 — продолжаем, 1 — Restart Game, 2 — Quit,
* -1 — сбой. */
int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked;
/* Чистый старт настоящей игры после Enter/Esc на title/intro/credits либо
* клавиши в attract-demo. Ждёт отпускания клавиши, сбрасывает состояние
* новой игры, загружает FIRST_LEVEL и проявляет его первый кадр. */
int pop_new_game_load(void) __banked;
/* Снять idle-хук, закрыть ввод и графику, отдать все ресурсы. */
void pop_shutdown(void) __banked;
#endif /* ROOMTEST_COLD_H */