/* * roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК * (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на * уровень или только по отладочной клавише; кадровый путь остался в * roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h. * * Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого * модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие * __banked-модули через трамплины. */ #include #include #include "roomtest_cold.h" #include "pop_bg.h" /* pop_loose_mob_reset — банк 7, зовём через трамплин */ #include "pop_kid.h" #include "pop_map.h" #include "pop_level.h" #include "pop_trob.h" #include "pop_guard.h" #include "pop_state.h" #include "pop_geom.h" #include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */ #include /* gfx_copy_page */ #include "pop_tune.h" #include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */ #include "pop_shadow.h" /* запечённый набор Тени */ #include "pop_sfx.h" /* звуковые эффекты через CBL */ #include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */ #include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */ #include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */ #include "pop_settings.h" /* pop_settings_defaults — стартовый профиль VANILLA */ #include "pop_cfg.h" /* pop_cfg_load — POP.CFG поверх дефолтов */ #include "pop_status.h" /* статус-строка и отладочная полоса — банк 9 */ #include "pop_menu.h" /* pop_menu_init/free — атлас UI в bank 9 */ #include "pop_ui.h" /* ранняя чёрная палитра + snapshot стартового экрана */ #include "pop_pal.h" /* единый lifecycle game/title/level палитр */ #include "pop_timer.h" /* global 60-minute timer — core in bank 9 */ #include /* puts/printf — сообщения об ошибках загрузки */ #include #define SEQ_STAND 2 #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 */ #define TILE_EMPTY 0 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ /* Во время смены уровня enter_room должен только СОБРАТЬ новую страницу. * Физический flip делает pop_level_switch после окончательной палитры. */ static uint8_t level_prepare; static uint8_t level_prepared_page; /* 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) */ } } /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A). * * Позиция берётся ИЗ ДАННЫХ уровня (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(). */ void pop_start_level(void) __banked { uint8_t entry_pose, start_room, pos, seq = SEQ_TURN, chkp; int8_t dir; pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */ /* upside_down тоже сбрасывается стартом уровня (seg003:38/188). Если * картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной * стороной, поэтому переворачивать ничего не надо — только снять флаги. */ pop_upside = 0; pop_upside_want = 0; pop_upside_dirty = 0; entry_pose = pop_level_entry_pose(pop_current_level); start_room = pop_level_start_room(); pos = pop_level_start_pos(); dir = (int8_t)~pop_level_start_dir(); /* Особое событие: старт с чекпойнта (do_startpos, seg003:0141). * Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles() * ниже, иначе рестарт её вернёт. */ 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 */ } #ifdef DBG_START_ROOM /* ОТЛАДОЧНЫЙ СТАРТ: сразу в целевую комнату оптимизации, минуя проход * уровня. Задаётся сборкой (make ROOM=15 POS=2), по умолчанию — сцена * 11/15 из docs/perf_l11_room15.md: Кид в (0,2), справа чомпер под * факелом, дальше второй факел и страж. * * Ставится ПОСЛЕ чекпойнта намеренно: отладочная позиция должна * перебивать и его тоже, иначе рестарт уровня уводил бы Кида из * измеряемой комнаты. Направление и позу входа не трогаем — они из * данных уровня, как у обычного старта. */ /* ...НО ТОЛЬКО НА СВОЁМ УРОВНЕ. pop_start_level зовётся ещё и на * СМЕНЕ уровня, и без этой проверки отладочная позиция перехватывала * переход: на 6-м Кид проваливается в комнате 1, а на 7-м обязан * появиться в комнате 17 и продолжить падение вниз (спецсобытие * «вход падением», см. ниже). Вместо этого он оказывался в * DBG_START_ROOM/POS, спецсобытие не срабатывало вовсе, и переход 6->7 * выглядел сломанным (поймано пользователем 2026-08-20 на сборке * make LEVEL=6 ROOM=1 POS=18). * * Рестарт ТОГО ЖЕ уровня (смерть, чекпойнт) отладочную позицию * сохраняет — ради этого она и перебивает чекпойнт выше. */ if (pop_current_level == FIRST_LEVEL) { start_room = DBG_START_ROOM; pos = DBG_START_POS; } #endif /* Рестарт уровня = 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 */ /* united_with_shadow = 0 (seg003:95) — состояние ПЕРСОНАЖА уровня, а не * комнаты: pop_guard_reset зовётся и при каждом входе в комнату (из * pop_guard_enter), поэтому гасить слияние надо здесь. */ pop_united_shadow = 0; pop_shadow_init = 0; pop_guard_notice_timer = 0; /* фора Джафара — не переживает рестарт */ pop_nav_hold = 0; /* старт уровня снимает и держатель чита */ pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ pop_loose_forget(); /* ...и фазы плит — уровень восстановлен целиком */ /* «Особое событие: убрать 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], 0 /* seg003:170 — playsound=0 */); 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); #ifdef DBG_START_ROOM /* ...и поправить x: kid_init ставит `x_bump[col] + TILE_SIZEX`, а это * ЛЕВАЯ ГРАНИЦА СЛЕДУЮЩЕЙ колонки (порт set_start_pos — в данных уровня * стартовые позиции подобраны под такую договорённость). Отладочный * старт называет тайл, В КОТОРОМ Кид должен оказаться, поэтому сдвигаем * его внутрь названного: с границы физика относит Кида к колонке правее, * и в 11/15 он стартовал прямо в челюстях (найдено пользователем * 2026-08-19). * * Отступ ровно 2, а не «половина тайла»: колонку определяет не сам x, а * ВЕСОВАЯ ТОЧКА кадра (determine_col -> dx_weight: dx позы минус * weight-биты, с учётом направления), поэтому геометрически ровной * середины тут нет. Число снято замером живой сцены: x = 98 при позе * стойки даёт curr_col = 2, x = 94 — уже 1. */ if (pop_current_level == FIRST_LEVEL) { /* см. условие выше */ Kid.x = (uint8_t)(pop_x_bump[(pos % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX - 2); Kid.curr_col = (int8_t)(pos % 10); } #endif /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м * уровне Кид ставится в комнату 17, а экран тут же переводится на * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — * он влетает в неё сверху, продолжая падение с шестого уровня. */ if (pop_current_level == FALLING_ENTRY_LEVEL && start_room == FALLING_ENTRY_ROOM) { uint8_t below = pop_room_link(start_room, LINK_DOWN); if (below) { Kid.y = (uint8_t)(Kid.y - 189); Kid.curr_row = pop_y_to_row((int16_t)Kid.y); enter_room(below); } } Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ /* Остальные одноразовые флаги того же set_start_pos (seg003:028A). Они * НЕ косметика: droppedout переживал границу уровня и на следующем * уровне заставлял стража идти «за упавшим Кидом» вниз * (guard_follows_kid_down) — на 4-м уровне в комнате 22 страж по этому * флагу шёл прямо в челюсти, хотя Кида он даже не видел. Взводится он * на ПРОШЛОМ уровне: Кид сорвался с уступа в боевой позе (кадры * 150..179, pop_map.c start_fall) — обычная концовка боя со скелетом на * 3-м. Поймано пользователем 2026-08-24 (переход 3 -> 4 ногами, через * дверь уровня; читом Shift+L старт чистый, и баг не воспроизводился). */ pop_droppedout = 0; offguard = 0; knock = 0; /* play_level (seg003:106): have_sword = level >= have_sword_from_level * (=2); исключение — demo level 0, где меч уже выдан. */ pop_have_sword = (uint8_t)(pop_current_level == 0 || pop_current_level >= 2); /* seamless (seg003:158): пришли БЕЗ двери уровня, прямо на бегу — HP не * трогаем, оно переезжает как есть. Флаг одноразовый: у оригинала его * гасит заставка «LEVEL N», которую этот же переход и пропускает. */ if (pop_seamless) { pop_seamless = 0; pop_kid_hp_carry(); } else pop_kid_hp_reset(); } /* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- * * * Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки, * включая недостижимые обычным путём. * * Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната * смотрелась в исходном состоянии». Убран: reset обнуляет room_seen, и * модификаторы перечитываются из уровня заново, то есть чит ОТМАТЫВАЕТ МИР * — открытые/закрытые ворота, выдвинутые пики, нажатые кнопки. Навигация * обязана только телепортировать; исходное состояние даёт перезапуск. */ uint8_t pop_dbg_roomnav(uint8_t dir) __banked { uint8_t r, i, c, pos = 20, any = 0xFF; /* Кид МЁРТВ: сначала честный рестарт уровня, потом телепорт. Навигация * только переставляет Кида (kid_init + сброс HP), то есть «оживляет» его * мимо start_level — а тот обязан вернуть тайлы, модификаторы и ОЧИСТИТЬ * слот соперника (seg003:100: Guard.direction = dir_56_none). Без этого * после смерти оставался живой скелет рядом с вернувшейся кучей костей. */ if (pop_kid_dead) pop_start_level(); r = cur_room; r = (dir == 1) ? (uint8_t)(r == 24 ? 1 : r + 1) : (uint8_t)(r == 1 ? 24 : r - 1); enter_room(r); /* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — сначала * НИЖНИЙ ряд (2), потом 1, и только потом 0 (пользователь 2026-08-11). * Раньше брался первый пол подряд, то есть почти всегда (0,0) — верхний * левый угол: это и «прилёт» в проём/ворота, и, на уровне 5, посадка * вплотную к тени спецсобытия, которая ждёт в колонке −1 ряда 0. Вместо * сцены кражи зелья начиналась схватка, которой в этой комнате быть не * должно. Снизу вверх нужно и самой навигации: в верхнем ряду пола чаще * нет, и Кид проваливается из комнаты сразу после телепорта. * * ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12): её проходят * СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за кромки), и с нижнего ряда * этот спуск не проверить вовсе. Пол в ряду 0 там есть, так что Кид не * проваливается. * * В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его там нет — * любой проходимый tile_is_floor (seg001: всё, кроме пустоты, верха * колонны, верха двери, стены и «половинок» 26..29). */ { int8_t row, step = -1, first = 2; if (pop_current_level == GRAB_DOWN_LEVEL && r == GRAB_DOWN_ROOM) { first = 0; step = 1; } for (row = first; row >= 0 && row <= 2 && any == 0xFF; row = (int8_t)(row + step)) { uint8_t fallback = 0xFF; for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) { c = (uint8_t)(room_fg[i] & 0x1F); if (c == 1) { any = i; break; } if (fallback == 0xFF && c && c != 9 && c != 12 && c != 20 && !(c >= 26 && c <= 29)) fallback = i; } if (any == 0xFF) any = fallback; } } if (any != 0xFF) pos = any; kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0); pop_kid_hp_reset(); /* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом заклинивал * управление намертво: pop_control при Kid.sword == SWORD_2_DRAWN уходит * в control_with_sword, а там единственный выход — ветка «убрать меч» на * кадре FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр * обычный, стража в новой комнате нет, swordfight не запускается — и * диспетчер каждый кадр не делает НИЧЕГО. Это болезнь ЧИТА: в оригинале * телепорта между комнатами нет, попасть в режим боя без соперника * нечем. Меч в инвентаре (pop_have_sword) НЕ теряем — только состояние * схватки. */ Kid.sword = SWORD_0_SHEATHED; holding_sword = 0; offguard = 0; guard_refrac = 0; /* ПРОВАЛ НА СЛЕДУЮЩИЙ УРОВЕНЬ ПО ЧИТУ (та же болезнь, что выше). * Уровень 12 кончается ФАКТОМ попадания в комнату 23 — двери у него нет * (pop_seamless, seg000:0900). А навигация перебирает комнаты ПО * НОМЕРУ, то есть любой обход рано или поздно наступает на 23-ю, и * уровень молча меняется на 13-й: комнату 23 двенадцатого уровня читом * не посмотреть в принципе, а прогон «съезжает» на соседний уровень * незаметно для проверяющего (поймано 2026-08-13 на первом же прогоне). * Держим триггер, пока Кид не сменит комнату НОГАМИ. */ pop_nav_hold = 1; return 1; } /* ---- ЧИТЫ (seg000: switch под if (cheats_enabled)) -------------------- * * Переехали из главного цикла в банк 8: дёргаются раз в кадр, то есть * один трамплин на кадр, а места в резиденте W1/W2 не осталось (звук, * атлас Тени и пейсинг съели остаток; см. TASKS_OPEN, MEM). * * Флаги «фронта» стали статиками модуля — раньше они были локалями main. * Смысл прежний: одно нажатие = одно срабатывание. */ static uint8_t cheat_prev, immo_prev, sound_prev, nlvl_prev; static uint8_t upsd_prev, xdec_prev, xinc_prev, cfg_prev; static uint8_t time_sub_prev, time_add_prev; void pop_cheat_tick(void) __banked { /* Читы (seg000: switch под if (cheats_enabled)). Все — по * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр. * K — убить стража; * I — бессмертие Кида (наш отладочный, в оригинале его нет: * иначе боёвку проверять получается ровно три раза); * Ctrl+S — вкл/выкл звук (порт Ctrl+S из SDLPoP, seg000:657). * НЕ чит: работает и без pop_cheats, как в оригинале. * Ctrl обязателен — голая S у нас свободна, но менять * пользовательскую раскладку оригинала незачем; * Shift+L — следующий уровень (seg000:698, ветка next_level = * current_level + 1). Единственный вменяемый способ * тестировать уровни 2..14. NB: L без Shift — отладочный * «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с * Shift шаг тоже пройдёт, но уровень тут же сменится, так * что конфликта на практике нет; * F7 / F8 — отнять / добавить минуту. Это точный аналог * keypad −/+ SDLPoP, но +/- заняты обходом комнат; * [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (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 = (uint8_t)(kbd_raw_down(KBD_SOUND_TOGGLE) && (kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL))); uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) && (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))); uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE); uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC); uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC); uint8_t ts = kbd_raw_down(KBD_CHEAT_TIME_SUB); uint8_t ta = kbd_raw_down(KBD_CHEAT_TIME_ADD); /* C — записать POP.CFG сейчас (отладка MS2, до экрана Settings: * там сохранять будет Apply/выход). */ uint8_t cf = kbd_raw_down(0x21); /* C (PS/2 set 2) */ if (pop_cheats && k && !cheat_prev) pop_guard_kill(); /* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и * мелкий урон» -> 0. Что означают уровни — pop_state.h. */ if (pop_cheats && i && !immo_prev) pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1); if (s && !sound_prev) { pop_sfx_toggle(); /* не под pop_cheats */ pop_settings.sound_enabled = pop_snd_want; /* Порт seg000:657: оригинал отвечает строкой в статус-полосе. */ pop_status_msg = (uint8_t)(pop_snd_want ? POP_MSG_SOUND_ON : POP_MSG_SOUND_OFF); } if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL) pop_next_level = (uint8_t)(pop_current_level + 1); /* U — переворот экрана (seg000:0793 — чит и в оригинале). * Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */ if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */ pop_upside_want = (uint8_t)!pop_upside_want; pop_upside_dirty = 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); if (pop_cheats && ts && !time_sub_prev) pop_timer_cheat_subtract(); if (pop_cheats && ta && !time_add_prev) pop_timer_cheat_add(); if (pop_cheats && cf && !cfg_prev) (void)pop_cfg_save(); cheat_prev = k; immo_prev = i; sound_prev = s; nlvl_prev = nl; upsd_prev = up; xdec_prev = xd; xinc_prev = xi; time_sub_prev = ts; time_add_prev = ta; cfg_prev = cf; } /* Отладочная строка в верхнем борте переехала в pop_status.c (банк 9): * она теперь ТЕКСТОВАЯ, а шрифт живёт там же. Здесь не осталось ничего — * см. заголовок pop_status.h про то, почему заявка сообщения стоит один * байт, а весь рендер сидит в банке. */ /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */ /* side — куда ушёл КИД (0 left, 1 right, 2 up, 3 down); 0xFF = «не переход * за Кидом» (старт уровня, респавн, чит-навигация). От него зависит ровно * одно: пойдёт ли страж следом (pop_guard_follow, порт exit_room+ * follow_guard) или останется в прежней комнате. */ void enter_room_side(uint8_t room, uint8_t side) __banked { uint8_t pg; /* Страж идёт за Кидом? — решаем ДО того, как что-либо загрузим: функции * нужны и старая комната (Guard.room), и целевая. Пошёл — ни leave, ни * enter звать нельзя: он уже перенесён и живёт дальше как есть. */ uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0); if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */ /* ...и так же запомнить НЕДОСЧИТАННЫЕ loose: пока карта ещё описывает * СТАРУЮ комнату, фаза уезжает в персистентный room_modif + trob, и * плита доваливается заочно (см. pop_loose_leave_room / animate_loose). */ pop_loose_leave_room(); uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_map_set(room_fg); /* Соседей слева/справа карта забирает из уровня САМА (целиком, см. * pop_map_set_edges) — отдельной копии в приложении нет. */ pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_room_link(room, 2), pop_room_link(room, 3)); /* Боковой переход: колонки сдвинулись ровно на 10 (x ∓140), флаги * перекрытия при этом инвариантны — историю перенумеровываем, а не * теряем. Без этого решётка шва не бампит на кадре входа и Кид с * разбега проходит сквозь закрытые ворота (BUG-GATE-PASS-1). */ if (side == 0) pop_coll_shift(+10); /* ушёл ВЛЕВО: col += 10 */ else if (side == 1) pop_coll_shift(-10); /* ушёл ВПРАВО: col −= 10 */ pop_room_set_below(below_fg); /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика * уровня, mod — живой room_modif (ленивая инициализация). */ { uint8_t up = pop_room_link(room, 2), c; if (up) { uint8_t *um = pop_trob_modif(up); pop_level_access_begin(); for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); pop_level_access_end(); for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; pop_room_set_above(above_fg, above_mod); pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */ } else { pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */ pop_map_set_above(0); } } pop_map_set_room(room); /* Kid.room — как в оригинале: «комната персонажа». Полная модель * (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение * Kid.room == Guard.room нужно уже сейчас: без него луч видимости * стража никогда не срабатывает. */ Kid.room = room; pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_bake_forget(); /* и слоты «первая запечка была на странице N» */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */ pop_check_fall_flo(); /* спецсобытие ур.13: гряда плит сверху */ /* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»- * уровнях забирает слот соперника себе: вернула 1 — стража поднимать * НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда * не пускает). Порядок ровно как в оригинале, не «сначала страж, потом * заменим». */ if (!follow && !pop_check_shadow(room)) pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ * per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * а не по свежему backtable. Фаза P0. */ room_mod = pop_trob_modif(room); /* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот, * видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6). * pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */ { uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1); if (lft) { uint8_t *m = pop_trob_modif(lft), i; for (i = 0; i < 3; i++) { lcol_bg[i] = m[9 + i * 10]; /* шов рисуется в обе страницы ниже — синхр. сигнатуру */ seam_sig[i] = seam_row_sig(m, lcol_fg, i); } } if (rgt) { uint8_t *m = pop_trob_modif(rgt); rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; } } seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */ /* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и * обычно тут же показывается флипом; вторая страница дабл-буфера получает её * КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти * полумиллиона тактов на вторую отрисовку). Побочный эффект важнее * тактов: игрок больше не видит, КАК рисуются тайлы — только готовый * кадр. * * Копия идёт банком 0x50, то есть обновляет и VRAM, и ОЗУ-копию * страницы-приёмника — иначе heal на ней восстанавливал бы фон прошлой * комнаты (инвариант gfx_copy_page, регресс tests/pageflip). * * Видимая страница здесь МЕНЯЕТСЯ, поэтому главный цикл перечитывает её * в начале каждого кадра (front = gfx_get_visible_page()), а не ведёт * свой счёт. */ { gfx_rect_t full; pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */ gfx_set_draw_page(pg); pop_screen_fill_field(); pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg); if (level_prepare) { /* Межуровневый staging: пока pop_pal_level_load последовательно * пишет KID.PAL и env/wall, видима нулевая чёрная страница. * Показывать эту комнату разрешено только после финальной * палитры — иначе level 4 успевает мелькнуть цветами level 3. */ level_prepared_page = pg; } else { gfx_set_visible_page(pg); /* показать готовое */ gfx_set_draw_page((uint8_t)(pg ^ 1)); full.x = 0; full.y = 0; full.w = 320; full.h = 256; gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */ } } cur_room = room; kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */ pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */ } /* Вход в комнату БЕЗ перехода за Кидом: старт уровня, респавн, чит-обход. * Страж такой смены не «видит» — он остаётся в своей комнате. */ void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); } void pop_qsave_restore_room(uint8_t room) __banked { uint8_t i, pg, *room_mod; gfx_rect_t full; uint8_t lft, rgt, up; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_map_set(room_fg); lft = pop_room_link(room, 0); rgt = pop_room_link(room, 1); up = pop_room_link(room, 2); pop_map_set_edges(lft, rgt, up, pop_room_link(room, 3)); pop_room_set_below(below_fg); if (up) { uint8_t *um = pop_trob_modif(up); pop_level_access_begin(); for (i = 0; i < 10; i++) above_fg[i] = pop_level_tile_raw(up, (uint8_t)(20 + i)); pop_level_access_end(); for (i = 0; i < 10; i++) above_mod[i] = um[20 + i]; pop_room_set_above(above_fg, above_mod); pop_map_set_above(above_fg); } else { pop_room_set_above(0, 0); pop_map_set_above(0); } pop_map_set_room(room); pop_loose_reset(); pop_trob_room_changed(); pop_bake_forget(); pop_loose_mob_room_changed(); room_mod = pop_trob_modif(room); if (lft) { uint8_t *m = pop_trob_modif(lft); for (i = 0; i < 3; i++) { lcol_bg[i] = m[9 + i * 10]; seam_sig[i] = seam_row_sig(m, lcol_fg, i); } } if (rgt) { uint8_t *m = pop_trob_modif(rgt); rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; } seam_redraw = seam_rows = 0; pg = (uint8_t)(gfx_get_visible_page() ^ 1); gfx_set_draw_page(pg); pop_screen_fill_field(); pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg); gfx_set_visible_page(pg); gfx_set_draw_page((uint8_t)(pg ^ 1)); full.x = 0; full.y = 0; full.w = 320; full.h = 256; gfx_copy_page(&full, GFX_COPY_DIRECT); cur_room = room; kid_room = Kid.room; /* Обе страницы теперь содержат только свежий фон. Старые габариты * персонажей относятся к кадру до QuickLoad и не должны запускать heal * поверх восстановленной комнаты. */ pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0; pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0; pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0; pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0; pop_cd_init(); pop_hp_invalidate(); } /* 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 */ /* ---- Порядок «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; } /* ВРЕМЕННО: трасса kidobj — порт отладочного вывода, добавленного * пользователем в SDLPoP add_kid_to_objtable (seg008:1679): * * DBG kidobj tilepos=%d frame=%d act=%d col=%d cols=%d..%d rows=%d..%d * * Считаем ровно то же, что set_char_collision (seg006:0723) + * set_objtile_at_char (seg006:13F3): габарит НЕутоньшённый (THIN правит * только *_coll, а колонки берутся по «толстой» границе), колонки * клампятся в 0..9, нижний ряд −1 читается как 3. Звать ПОСЛЕ отрисовки * Кида: ширину/высоту кадра (fpw/fph) заполняет она. * * Вывод забирает брейкпоинт MAME на резидентном pop_dbg_trap — сама * функция банковая, её адрес в W3 неоднозначен. */ void pop_dbg_kidobj(void) __banked { int8_t dx = kid_cur_dx(), dy = kid_cur_dy(); uint8_t fl = kid_cur_flags(); uint16_t w = pop_cd[POP_CH_KID].fpw; uint16_t h = pop_cd[POP_CH_KID].fph; int xl = char_x_left_of(Kid.x, Kid.direction, dx, fl, w); int xr = xl + (int)((w + 1) >> 1); /* + char_width_half */ int oy = (int)Kid.y + dy; int ty = oy - (int)h + 1; int8_t r, c, tr, br, cl, cr; objtile_at_char(&r, &c, &Kid, dx, dy, fl, w); br = pop_y_to_row((int16_t)oy); if (br == -1) br = 3; if (ty >= 192) ty = 0; tr = pop_y_to_row((int16_t)ty); cl = tile_div_mod(xl); if (cl < 0) cl = 0; cr = tile_div_mod(xr); if (cr > 9) cr = 9; pop_dbg_obj[0] = (int8_t)(r * 10 + c); pop_dbg_obj[1] = (int8_t)Kid.frame; pop_dbg_obj[2] = (int8_t)Kid.action; pop_dbg_obj[3] = Kid.curr_col; pop_dbg_obj[4] = cl; pop_dbg_obj[5] = cr; pop_dbg_obj[6] = tr; pop_dbg_obj[7] = br; pop_dbg_obj[8] = (int8_t)cur_room; pop_dbg_trap(); } /* 1 = страж рисуется ПОСЛЕ Кида (поверх). __banked: зовётся раз в кадр, * то есть один трамплин — а 500 Б кода в резиденте дороже. */ uint8_t guard_over_kid(void) __banked { 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(), pop_cd[POP_CH_KID].fpw); objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy, pop_gframe.flags, pop_cd[POP_CH_OPP].fpw); 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 > pop_cd[POP_CH_KID].fpy); } /* ---- Переворот уже нарисованного (зелье инверсии, уровень 9) --------- * * * Порт need_redraw_because_flipped (seg000:15E9 + 939/946). Оригинал * держит готовый offscreen и переворачивает его на выводе; у нас страниц * ровно две, рабочей третьей нет, поэтому переворачиваем то, что уже * нарисовано, — построчной копией акселератора (gfx_copy_page). * * Почему это законно и почему heal после не ломается: accel ЧИТАЕТ * ОЗУ-копию, значит копируется ЧИСТЫЙ ФОН без персонажей, а пишет банком * 0x50 — и в видео, и в копию приёмника. После двух проходов обе страницы * дабл-буфера содержат перевёрнутый фон в обеих плоскостях, и heal * восстанавливает уже его (memory accel_port_y_between_triggers, * регресс — tests/pageflip). * * Порядок: перевернуть в СКРЫТУЮ, показать её, скопировать оттуда во * вторую (теперь скрытую) — прямой копией. Так игрок видит результат * сразу, а не после второго прохода. * * Переворачиваем ТОЛЬКО игровое поле (POP_PLAYFIELD_H): полоса HP лежит * ниже него и в оригинале тоже не переворачивается. */ void pop_flip_screen(void) __banked { gfx_rect_t area; uint8_t front = gfx_get_visible_page(); uint8_t back = (uint8_t)(front ^ 1); uint8_t i; area.x = 0; area.y = POP_YOFF; area.w = 320; area.h = POP_PLAYFIELD_H; /* СНАЧАЛА убрать из фона то, что в нём запечено, но фоном не является: * пламя факелов (pop_torch_wipe объясняет, почему оно вообще там). * Отражённое вместе с фоном, оно оседало в зеркальных позициях, где его * никто не перерисовывает — по комнате оставались лишние языки огня * (нашёл пользователь на глотке зелья). * * Чистим ОБЕ страницы: источник отражения — видимая, а вторая получит * прямую копию уже чистой картинки. Дешевле полной перерисовки комнаты * на порядок: 0..4 тайла против 6.2 млн тактов (14.5 кадра, замер MAME * 2026-08-12) — а копия отражения стоит всего 2.1 кадра. */ /* ВНИМАНИЕ на систему координат: pop_upside к этому моменту УЖЕ * переключён (главный цикл делает это перед вызовом), а картинка на * экране нарисована ещё в СТАРОЙ. Чистить её надо по старым * координатам, иначе чёрный бар ложится в зеркальное место — на экране * оставался чёрный квадрат, а пламя так и не стиралось. */ pop_upside = (uint8_t)!pop_upside; /* временно: система ЭКРАНА */ gfx_set_draw_page(front); for (i = 0; i < 30; i++) { uint8_t t = room_fg[i]; if (t == 19 || t == 30) /* факел / факел с щебнем */ pop_torch_wipe(i / 10, i % 10); } pop_upside = (uint8_t)!pop_upside; /* вернуть новое состояние */ gfx_set_draw_page(back); gfx_copy_page(&area, GFX_COPY_VFLIP); /* источник — видимая страница */ gfx_set_visible_page(back); gfx_set_draw_page(front); gfx_copy_page(&area, GFX_COPY_DIRECT); /* вторую страницу — прямой копией */ /* Всё, что рисуется поверх фона, надо положить заново: прошлые * прямоугольники персонажей относятся к СТАРОЙ (неперевёрнутой) * картинке, и heal по ним стёр бы не то место. */ pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0; pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0; pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0; pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0; POP_CD_TOUCH_ALL(); /* фон трогали целиком */ pop_hp_invalidate(); /* полосу HP перерисовать */ } /* ---- Запуск, смена уровня, завершение ------------------------------- * * Три куска главного цикла, которые отрабатывают раз за игру (или раз на * уровень). В резиденте от них оставались только вызовы — сам код здесь. */ int pop_boot(void) __banked { /* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */ pop_settings_defaults(); pop_cfg_load(); pop_timer_new_game(); /* default 60:00, Settings may freeze it */ /* Экран Sprinter хранит VRAM и палитры между программами. Перейти в * графику надо ДО долгих чтений, но первой же операцией погасить ОБЕ * палитры: тогда ни старый экран, ни частично собранный первый уровень * не могут мелькнуть перед первым title fade-in. */ /* PIC-палитры графики живут и в text mode. Предзапись чёрного сокращает * окно между аппаратным SETVMOD и первой строкой нашего кода, в котором * иначе успевает мелькнуть VRAM предыдущей программы. */ pop_pal_black(); initgraph_black(); /* initgraph_black уже обнулил VRAM до SETVMOD. Повторная заливка после * него фиксирует и shadow-копии, откуда затем работают heal/page-copy. */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); gfx_set_draw_page(0); bar(0, 0, 319, 255); gfx_set_draw_page(1); bar(0, 0, 319, 255); gfx_set_visible_page(0); pop_pal_black(); /* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после * initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к * этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */ pop_current_level = FIRST_LEVEL; if (pop_bg_load(pop_level_type()) != 0) { puts("pop_bg_load failed"); return -1; } /* pop_bg_load установил только свои слоты палитры. До конца загрузки * всё равно держим экран чёрным. */ pop_pal_black(); pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */ /* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */ pop_guard_reset(); if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */ printf("kid_data.bin load failed\n"); return -1; } pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */ pop_settings_apply(1); /* CFG -> speed/cheats/CBL */ if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ puts("kid atlas load failed"); return -1; } if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ puts("level load failed"); 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) */ /* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной * палитре и поэтому не открывает игроку старое содержимое VRAM. */ if (pop_menu_init() != 0) { puts("menu font atlas load failed"); return -1; } /* kid.pal + динамические slots tileset/Shadow — один логический * источник. pop_pal_game_load делает снимок ПОСЛЕ всех доплат. */ pop_pal_game_load(); /* Сохраняем ИМЕННО игровую палитру: после стартового title/intro она * понадобится как источник обычного fade-in. Для build-info экран * остаётся штатный яркий игровой фон. */ if (!pop_settings.show_build_info) { pop_pal_apply(4); } /* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново * выполняет pop_level_switch. Если нарисовать её сейчас в обе страницы, * она становится сохранённой VRAM и может мелькнуть перед первым title * при следующем запуске приложения. Единственное исключение — явно * включённый build-info: его фон по контракту является игровой сценой. */ if (pop_settings.show_build_info) { pop_start_level(); if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all(); gfx_set_draw_page(0); pop_char_draw(POP_CH_KID); pop_char_fore(POP_CH_KID); } /* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы, * уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск * надолго, насос не успевает долить блок, и на слух это скрежет * (pop_sfx.h). Парная к pop_sfx_pause в pop_level_switch. */ pop_sfx_start(); return 0; } /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): цикл * уровня кончается, когда next_level разошёлся с current_level (END_LEVEL * после seq_70 в двери уровня либо чит Shift+L), и play_level грузит * следующий. Возврат -1 — фатально (не удалось сменить набор фона). */ int pop_level_switch(void) __banked { uint8_t old_page; gfx_rect_t full; /* Граница FG0: повреждённый snapshot или старый внешний вызов не должен * даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING * добавит автомат приложения (FG11); пока просто оставляем уровень. */ if (pop_next_level > POP_LEVEL_LAST) { pop_next_level = pop_current_level; return 0; } /* Порт 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) */ pop_sfx_pause(); /* загрузка уровня — ESTEX надолго, звук глушим */ if (pop_level_load_num(pop_next_level) != 0) { pop_next_level = pop_current_level; /* файла нет — остаёмся */ } else { /* С этого места видим только индекс 0. Это одновременно * скрывает дисковую подготовку и даёт безопасный экран, * пока loaders физически меняют отдельные диапазоны palette. */ pop_pal_black(); old_page = gfx_get_visible_page(); gfx_set_bank(GFX_BANK_NORMAL); /* Обнуляем ОБЕ страницы, а не только software-visible: * аппаратный SCREEN_SWITCH и сохранённое BGI-состояние не * должны оставлять старые sprites на соседней странице. */ gfx_set_draw_page(0); pop_screen_fill_game(); gfx_set_draw_page(1); pop_screen_fill_game(); pop_wait_edge(); /* Набор спрайтов соперника у каждого уровня СВОЙ * (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и * кадров меньше, чем у стража. Без перезагрузки соперник * рисуется чужим атласом: часть индексов уходит за его * пределы, и персонаж просто не виден. */ pop_guard_free(); pop_guard_load(); /* guard loader применяет свои цветовые слоты физически; * пока остальной уровень читается, экран обязан быть чёрным. */ pop_pal_black(); /* Зеркальные копии атласов сделаны из СТАРЫХ страниц — * после перезагрузки спрайтов они мусор (pop_vflip.h). */ pop_vflip_reset(); /* Тайлсет: подземелье <-> дворец (порт tbl_envir_ki, * seg000:1108). Внутри — no-op, если тип не менялся; * при смене выгружает старые атласы и правит 32 записи * палитры. Только на границе уровня, не в кадре. */ if (pop_bg_load(pop_level_type()) != 0) { puts("bg set load failed"); return -1; } /* Смена tileset тоже пишет env/wall slots. Не открывать их * до готовности обеих страниц нового уровня. */ pop_pal_black(); /* Всё состояние прошлого уровня сносит сам pop_start_level: * pop_trob_reset обнуляет room_seen (модификаторы комнат * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают * из неё же, loose чистит enter_room. */ level_prepare = 1; pop_start_level(); level_prepare = 0; /* pop_bg_load мог заменить только env/wall, а до этого * экран жил после fade на чёрной палитре заставки. Полный * game source обязателен даже для dungeon->dungeon: иначе * fade_in восстановит старый (либо уже чёрный) snapshot. * Яркость caller выберет сам: gameplay может показать кадр * сразу, lifecycle title/demo — сначала apply(4)+fade_in. */ /* Уровень с зельем инверсии — заранее поднять зеркальный * набор персонажей (pop_vflip.h): 34 страницы читаются ~1.5 с, * и делать это после открытия палитры либо в момент глотка * нельзя. Звать можно откуда угодно — функция идемпотентна. */ if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all(); /* Последнее действие подготовки: собрать полный logical * source новой палитры. Пока он собирается физическими * записями, видимая old_page заполнена нулём. */ pop_pal_level_load(1); /* Атомарный commit на границе растра: впервые показать * готовую комнату уже с окончательной палитрой уровня. * Вторую страницу синхронизировать аппаратной копией. */ pop_wait_edge(); gfx_set_visible_page(level_prepared_page); gfx_set_draw_page(old_page); full.x = 0; full.y = 0; full.w = 320; full.h = 256; gfx_copy_page(&full, GFX_COPY_DIRECT); /* Уровень готов и показан — заявить «LEVEL N» (порт * show_level из draw_level_first, seg003:222). */ pop_status_level(); } /* CBL намеренно НЕ открываем здесь. После загрузки caller ещё * может делать BIOS fade/palette либо восстанавливать QuickSave; * открытый буфер за это время успевает повторить старую половину * и даёт скрежет. Caller запускает звук последним действием. */ return 0; } /* Общий холодный маршрут title/intro/credits/demo -> настоящая игра. * Держим его в банке 8: это разовый lifecycle-код, а не часть кадрового * пути, и перенос возвращает резиденту W1 место под стек/heap. */ int pop_new_game_load(void) __banked { /* Клавиша, которой пропустили заставку/demo, не должна стать первым * движением Кида или тут же открыть pause menu на уровне. */ while (kbd_raw_any_down()) { kbd_raw_sync(); gfx_wait_vsync(); } pop_kid_hp_newgame(); hitp_max = POP_START_HITP; pop_timer_new_game(); pop_checkpoint = 0; pop_next_level = FIRST_LEVEL; /* Title мог завершиться на яркой title-палитре. Сначала погасить её, * затем восстановить полный game snapshot; level_switch собирает обе * страницы уже невидимо, и первый игровой кадр проявляется. */ pop_pal_apply(4); gfx_set_draw_page(0); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill_game(); gfx_set_draw_page(1); pop_screen_fill_game(); gfx_set_visible_page(1); pop_pal_game_load(); pop_pal_apply(4); if (pop_level_switch() != 0) return -1; /* Первый уровень появляется сразу, без fade, но только после того как * обе страницы и его собственная палитра полностью готовы. */ pop_pal_apply(0); (void)pop_sfx_start(); return 0; } void pop_shutdown(void) __banked { gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */ /* ЗВУК ГАСИМ ПЕРВЫМ, до closegraph и освобождения страниц. Дальше идут * ESTEX-вызовы (mem_free_block на каждый атлас), а они уходят надолго — * насос не успевает долить очередной блок, и железо доигрывает по кругу * несвежую половину буфера. Замер записи MAME: на выходе по ESC ровно * 11 мс шума на полной громкости, то есть ОДИН пропущенный блок (128 * сэмплов = 11,7 мс). Та же причина, по которой звук глушится на время * загрузки уровня (pop_sfx.h). */ pop_sfx_close(); pop_ctrl_close(); closegraph(); pop_bg_free(); pop_kid_free(); pop_shadow_free(); pop_menu_free(); pop_guard_free(); pop_kid_data_free(); pop_level_free(); }