diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 6b8a95e..241fd91 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -14,14 +14,21 @@ MEMORY ?= huge # small-раскладка + банки кода в W # банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и # логику, которая рисует. См. docs/layout_plan_v2.md §2, memory # sdcc_banked_call_rules. -EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=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 — см. там. +EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c $(PROF_FLAGS) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте # MAME = время фазы в растровых строках. PROF ?= 1 PROF_FLAGS := -DPROF_BORDER=$(PROF) -EXTRA_SRCS := pop_state.c pop_draw.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_trob.c pop_redraw.c pop_geom.c pop_guard.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md index 1ccfcfc..0836e44 100644 --- a/applications/PoP/roomtest/TASKS.md +++ b/applications/PoP/roomtest/TASKS.md @@ -44,8 +44,8 @@ | 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 | -| — | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | отладка (BUG-GATE-PASS-1) | -| — | [MEM-BANK5](#mem-bank5) | новый банк кода: W1/W2 осталось 38 Б кучи | берётся по факту нехватки места | +| ✔ | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** | +| ✔ | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б | --- diff --git a/applications/PoP/roomtest/pop_cheat.h b/applications/PoP/roomtest/pop_cheat.h index 8589a6d..8acb53f 100644 --- a/applications/PoP/roomtest/pop_cheat.h +++ b/applications/PoP/roomtest/pop_cheat.h @@ -6,7 +6,8 @@ * просто глобальный флаг: main ставит его при старте (на время разработки — * ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл. * - * Реализовано: K — убить стража (seg000:786). Остальные читы оригинала + * Реализовано: K — убить стража (seg000:786), I/S/Shift+L (наши отладочные), + * [ / ] — подгонка Кида по X (seg000:1828). Остальные читы оригинала * (время, перо, воскрешение, переход по комнатам) пока не портированы. */ #ifndef POP_CHEAT_H @@ -20,6 +21,8 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ #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 diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index ba4208a..7922b79 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -17,6 +17,7 @@ */ #include #include +#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */ #include "pop_kid.h" #include "pop_map.h" #include "pop_guard.h" /* charid, состояние меча, seq стража */ @@ -73,15 +74,15 @@ #define FRAME_109_CROUCH 109 /* ---- Состояние управления (seg006 globals) ------------------------ * - * control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно: - * X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный - * автомат held/released/ignore (авто-повтор); ctrl1_* — их память между - * кадрами (rest/save). */ -int8_t control_x, control_y, control_shift; -/* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт - * СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же - * control(), что и у игрока. Поэтому шина общая. */ -int8_t control_forward, control_backward, control_up, control_down, control_shift2; + * Сами control_* ЖИВУТ В pop_state.c, а не здесь. Модуль уехал в БАНК 5, + * а шину пишут с трёх сторон (этот банк, guards.c из банка 1) и читают из + * банка 3 (pop_map) — это ровно тот случай, для которого pop_state и + * заведён. Сегодня оно работало бы и отсюда: банки собираются БЕЗ + * --bank-data, поэтому их писучие данные остаются в общем _DATA. Но это + * флаг сборки, а не свойство кода: включат --bank-data ради экономии + * W1/W2 — и шина молча уедет в страницу банка. + * + * ctrl1_* остаются здесь: их трогает только этот модуль. */ static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2; /* ---- seqtbl_offset_char: сменить последовательность Kid ------------ * @@ -549,7 +550,7 @@ static void control_with_sword(void) } /* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */ -void pop_control(void) +void pop_control(void) __banked { uint8_t f = Char.frame; /* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0). @@ -674,7 +675,7 @@ static void save_ctrl_1(void) } /* ---- Публичный API ------------------------------------------------ */ -int pop_ctrl_init(void) +int pop_ctrl_init(void) __banked { control_forward = control_backward = control_up = control_down = CONTROL_RELEASED; control_shift2 = CONTROL_RELEASED; @@ -682,17 +683,17 @@ int pop_ctrl_init(void) return kbd_raw_open(); } -void pop_ctrl_close(void) +void pop_ctrl_close(void) __banked { 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); } -void pop_ctrl_tick(void) +void pop_ctrl_tick(void) __banked { kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */ /* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж diff --git a/applications/PoP/roomtest/pop_ctrl.h b/applications/PoP/roomtest/pop_ctrl.h index 58e2d3f..0b5f550 100644 --- a/applications/PoP/roomtest/pop_ctrl.h +++ b/applications/PoP/roomtest/pop_ctrl.h @@ -18,19 +18,26 @@ #include +/* МОДУЛЬ ЖИВЁТ В БАНКЕ 5 (--bank 5=pop_ctrl.c), поэтому всё, что зовут + * снаружи, помечено __banked. Зовут с трёх сторон: главный цикл + * (резидент), guards.c (банк 1) и pop_map.c (банк 3) — банк→банк + * работает, трамплин сохраняет страницу окна на стеке. Диспетчер + * дёргается один раз за кадр на персонажа, так что цена перехода + * незаметна. Сама шина control_* лежит в pop_state.c (см. ниже). */ + /* Открыть raw-канал клавиатуры и обнулить память авто-повтора. * 0 — OK, -1 — ошибка (EBUSY и пр., см. kbd_raw_open). */ -int pop_ctrl_init(void); -void pop_ctrl_close(void); +int pop_ctrl_init(void) __banked; +void pop_ctrl_close(void) __banked; /* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать * диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД * kid_tick() (play_seq продолжит с новой последовательности). */ -void pop_ctrl_tick(void); +void pop_ctrl_tick(void) __banked; /* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map): * зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */ -int8_t pop_ctrl_shift_held(void); +int8_t pop_ctrl_shift_held(void) __banked; /* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ---- * В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего @@ -42,6 +49,9 @@ int8_t pop_ctrl_shift_held(void); /* Оси ЭТОГО кадра (абсолютные): 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 @@ -53,6 +63,6 @@ extern int8_t control_forward, control_backward, control_up, control_down, /* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через * pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж — из * своего тика напрямую. */ -void pop_control(void); +void pop_control(void) __banked; #endif diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c index 85c401e..e0343a9 100644 --- a/applications/PoP/roomtest/pop_state.c +++ b/applications/PoP/roomtest/pop_state.c @@ -11,9 +11,24 @@ * Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки. */ #include "pop_state.h" +#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */ 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; diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 62d928e..d5a6b58 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -49,11 +49,18 @@ __sfr __at (0xFE) io_border; /* Сколько банков грузит crt0_banked (читает его стартовый код). * 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map - * (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP). + * (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP), + * 5 = pop_ctrl (диспетчер управления). * Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ * вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних - * уровней, чомперы, зеркало. */ -const uint8_t n_banks = 4; + * уровней, чомперы, зеркало. + * + * ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть — это не ошибка + * сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и + * останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку + * runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра + * доходила до конца загрузки ресурсов и вставала намертво в DSS. */ +const uint8_t n_banks = 5; /* Режим читов (pop_cheat.h): на время разработки включаем в main. */ uint8_t pop_cheats; @@ -419,6 +426,7 @@ int main(void) uint8_t nav_prev = 0; #endif 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) */ puts("pop_bg_load failed"); @@ -505,21 +513,35 @@ int main(void) * тестировать уровни 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 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 && 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; immo_prev = i; sword_prev = s; nlvl_prev = nl; + xdec_prev = xd; + xinc_prev = xi; } if (frozen) { PROF(0); /* чёрный: ждём vsync */ @@ -530,8 +552,17 @@ int main(void) #ifdef ROOMNAV /* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд, * '-' (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)) | ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1)); @@ -539,7 +570,6 @@ int main(void) uint8_t r = cur_room, i, c, pos = 20, any = 0xFF; r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1) : (uint8_t)(r == 1 ? 24 : r - 1); - pop_trob_reset(); enter_room(r); /* Предпочитаем обычный пол; иначе — любой tile_is_floor * (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */ diff --git a/docs/TODO.md b/docs/TODO.md index 8e94ac5..4e37620 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -61,6 +61,35 @@ ## 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`) — когда проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map, first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,