Files
Sprinter-SDCC/applications/SprPoP/src/sprpop_cold.c
T
snark13 ea8efdb0fd SprPoP: обобщить HDD-сборку и очистить метаданные
Добавить общий каталог назначения для HDD и удалить локальную копию упаковщика.\n\nУбрать устаревшие generated-имена ресурсов, выводить число страниц Kid из kid.arc и ограничить звуковую таблицу горячим модулем.\n\nЗафиксировать планы runtime-индексов музыки и PCM-эффектов.
2026-08-30 16:16:06 +03:00

1409 lines
90 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.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК
* (--bank 8=sprpop_cold.c, окно W3). Здесь всё, что отрабатывает раз на
* уровень или только по отладочной клавише; кадровый путь остался в
* sprpop.c. Зачем разделили и что осталось снаружи — см. sprpop_cold.h.
*
* Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого
* модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие
* __banked-модули через трамплины.
*/
#include <graphics.h>
#include <gfx.h>
#include "sprpop_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 <sprite.h> /* 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_music.h" /* pop_music_busy/free — ждём трек заставки */
#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_flow.h" /* таблица level-flow: какая сцена перед уровнем */
#include "pop_intro.h" /* pop_pre_cutscene_show — сцена перед уровнем */
#include "pop_app.h" /* pop_app_dispatch — состояния приложения */
#include "pop_pal.h" /* pop_pal_apply — гашение перед загрузкой */
#include "pop_timer.h" /* pop_timer_minutes — выбор сцены «времени мало» */
#include "pop_ui.h" /* POP_PAL_DIM_BLACK */
#include "pop_menu.h" /* пауза-меню: коды выбора */
#include "pop_qsave.h" /* QuickSave/QuickLoad по F6/F9 */
#include "pop_status.h" /* pop_status_invalidate после меню */
#include <kbd_raw.h>
#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 <stdio.h> /* puts/printf — сообщения об ошибках загрузки */
#include <kbd_raw.h>
#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) */
}
}
/* МЕНЮ, F10 И QUICKSAVE/LOAD — граница кадра, но не горячий путь.
*
* Всё это события «раз в партию», а в главном цикле они занимали полсотни
* строк резидента W1. Здесь же держим и edge-состояния клавиш: снаружи они
* никому не нужны.
*
* restart_level — «пользователь выбрал Restart Level» (главный цикл сделает
* это сам, после вычисления back-страницы, как респавн);
* dead_reset — «QuickLoad удался, счётчик кадров смерти сбросить».
* Возврат: 0 — продолжаем кадр, 1 — выйти из игрового цикла (Restart Game),
* 2 — выйти и завершить работу (Quit), -1 — сбой диспетчера. */
int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
{
static uint8_t esc_prev, qs_save_prev, qs_load_prev;
static uint8_t restart_prev;
static uint8_t intro_prev, showtime_prev, timer_prev;
uint8_t e = (uint8_t)(kbd_raw_down(KBD_MENU) || kbd_raw_down(KBD_MENU_ALT));
uint8_t ctrl = (uint8_t)(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL));
uint8_t menu_opened;
int8_t mrc = POP_MENU_NONE;
if (e && !esc_prev) pop_menu_request = 1;
esc_prev = e;
menu_opened = pop_menu_request;
if (menu_opened) {
if (!pop_app_dispatch(POP_APP_EV_PAUSE)) return -1;
mrc = pop_menu_process();
}
if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save();
else if (mrc == POP_MENU_QUICKLOAD) pop_qsave_request_load();
else if (mrc == POP_MENU_QUICKLOAD_BACKUP)
pop_qsave_request_load_backup();
else if (mrc == POP_MENU_RESTART_LEVEL) *restart_level = 1;
else if (mrc == POP_MENU_RESTART_GAME)
return (int8_t)(pop_app_dispatch(POP_APP_EV_RESTART_INTRO) ? 1 : -1);
else if (mrc == POP_MENU_QUIT)
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
if (menu_opened) {
/* Меню переписало обе страницы целиком. Снимки слотов об этом не
* знают и продолжают считать, что персонаж на странице уже
* нарисован, — стоящий Кид оставался бы невидимым до первого своего
* движения. */
pop_cd_forget();
pop_status_invalidate();
if (!pop_app_dispatch(POP_APP_EV_RESUME)) return -1;
}
/* Ctrl+Q — выход (keys.txt). Вторая клавиша выхода, F10, ловится в
* kbd_idle (sprpop.c) и работает везде, включая заставку. */
if (ctrl && kbd_raw_down(KBD_QUIT))
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
/* Ctrl+A — рестарт уровня: тот же путь, что и пунктом меню. */
{
uint8_t r = (uint8_t)(ctrl && kbd_raw_down(KBD_RESTART));
if (r && !restart_prev) *restart_level = 1;
restart_prev = r;
}
/* Ctrl+R — вернуться в заставку. Тот же переход, что «Restart Game»
* пунктом меню: автомат сам разложит title/intro с первого экрана. */
{
uint8_t ri = (uint8_t)(ctrl && kbd_raw_down(KBD_INTRO));
uint8_t was = intro_prev;
intro_prev = ri;
if (ri && !was)
return (int8_t)(pop_app_dispatch(POP_APP_EV_RESTART_INTRO) ? 1 : -1);
}
/* Space — «сколько осталось» (seg000:612). Печатать самим не надо:
* ставим тот же флаг, которым пользуется автоматическое объявление
* минут, и строку соберёт time_msg() из pop_status. 2 — перебить
* текущее сообщение немедленно, как делает оригинал по этой клавише. */
{
uint8_t sh = kbd_raw_down(KBD_SHOWTIME);
if (sh && !showtime_prev) pop_show_time = 2;
showtime_prev = sh;
}
/* T — постоянный показ таймера. Без модификаторов: Shift+T отдан
* «добавить HP» (фаза C плана keys_plan.md), и без этой проверки один
* тап сработал бы обеими функциями сразу. */
{
uint8_t tk = (uint8_t)(kbd_raw_down(KBD_TIMER) && !ctrl &&
!(kbd_raw_down(KBD_LSHIFT) ||
kbd_raw_down(KBD_RSHIFT)));
if (tk && !timer_prev) pop_show_timer ^= 1;
timer_prev = tk;
}
/* F6/F9 только ставят запрос; файловая операция выполняется здесь, на
* границе кадров — вне окон Char и до heal/tick/draw. */
{
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
if (s && !qs_save_prev) pop_qsave_request_save();
if (l && !qs_load_prev) pop_qsave_request_load();
qs_save_prev = s; qs_load_prev = l;
if (pop_qsave_process() > 0) *dead_reset = 1;
}
return 0;
}
/* СМЕНА УРОВНЯ — целиком здесь, а не в главном цикле.
*
* Событие редкое (раз в несколько минут игры), а кода на нём была без
* малого сотня строк — и все они лежали в _main, то есть в резиденте W1,
* где у нас счёт на десятки байт. Наружу остаётся одно условие и один
* вызов.
*
* back — страница, в которую главный цикл рисует кадр: после загрузки
* уровня её надо вернуть себе (level_switch показывает свою).
* Возврат: 0 — уровень сменён, -1 — сбой, главный цикл завершает работу. */
int8_t pop_level_change(uint8_t back) __banked
{
uint8_t reveal_pending = 0;
pop_pre_cutscene_t pre = pop_flow_pre_cutscene(pop_next_level,
pop_timer_minutes);
/* Загрузка файла уровня, спрайтов соперника и тайлсета — в
* pop_level_switch ниже. До 2/4/6/8/9/12 таблица
* flow запускает отдельное состояние CUTSCENE, как play_level()
* SDLPoP: следующий уровень не трогаем до завершения/skip. */
if (pre != POP_PRE_NONE) {
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED))
return -1;
pop_pre_cutscene_show(pre);
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED))
return -1;
/* Катсцена оставляет экран чёрным. Новый уровень будет
* открыт одним применением его готовой палитры. */
reveal_pending = 1;
} else {
/* CBL глушим ДО первой же операции с палитрой. Палитра идёт
* через BIOS ($A4) с запрещёнными прерываниями: насос не
* успевает долить блок, железо крутит хвост своего буфера —
* на слух скрежет. Раньше pop_sfx_pause() звался только
* внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и
* переход без катсцены (в т.ч. телепорт Shift+L) скрежетал
* почти всегда — поймано пользователем 2026-08-25. Ветка с
* катсценой свободна от этого: pop_pre_cutscene_show глушит
* звук первым действием. */
/* Глушение снято 2026-08-28 (замер: загрузка уровня насос не
* морит — 500 вызовов подряд, максимум 245 832 такта при
* дедлайне 251 000). Прежнее обоснование — скрежет на палитре
* через BIOS ($A4 с запрещёнными прерываниями), пойманный на
* переходе без катсцены 2026-08-25. Если он вернётся, это
* единственное место, куда его надо возвращать. */
/* Без fade: старый кадр гасим сразу и обязательно даём
* показать один чёрный растр ДО диска/перерисовки. */
pop_pal_apply(POP_PAL_DIM_BLACK);
pop_wait_edge();
reveal_pending = 1;
if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED))
return -1;
}
if (pop_level_switch() != 0 ||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY))
return -1;
if (reveal_pending) {
/* Обе страницы и snapshot уже принадлежат новому уровню:
* включить его корректную палитру одним переходом. */
pop_pal_apply(0);
reveal_pending = 0;
}
/* level_switch оставляет CBL закрытым: palette/fade идут через
* BIOS и при открытом буфере давали скрежет на первом кадре. */
(void)pop_sfx_start();
gfx_set_draw_page(back);
return 0;
}
/* 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_music_level_start(); /* одноразовые темы уровня — заново */
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();
/* ПОЛОСУ HP — НА ОБЕИХ СТРАНИЦАХ. Старт уровня перерисовал фон, то есть
* стёр её, а сама она обновляется только при ИЗМЕНЕНИИ чисел. После
* Ctrl+A с выросшим за уровень максимумом (большой кувшин, чит Shift+T)
* это давало моргание: одна страница показывала старые деления, вторая
* новые (поймано пользователем 2026-08-27). invalidate ровно для этого
* и заведён — см. его шапку в pop_cdraw.c. */
pop_hp_invalidate();
}
/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- *
*
* Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки,
* включая недостижимые обычным путём.
*
* Здесь БЫЛ 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, music_prev, nlvl_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev, cfg_prev;
static uint8_t time_sub_prev, time_add_prev;
static uint8_t hp1_prev, hpmax_prev, feather_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);
/* Ctrl+I — бессмертие, Shift+I — переворот: скан-код один. */
uint8_t i = (uint8_t)(kbd_raw_down(KBD_CHEAT_IMMO) &&
(kbd_raw_down(KBD_LCTRL) ||
kbd_raw_down(KBD_RCTRL)));
uint8_t s = (uint8_t)(kbd_raw_down(KBD_SOUND_TOGGLE) &&
(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL)));
uint8_t m = (uint8_t)(kbd_raw_down(KBD_MUSIC_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 = (uint8_t)(kbd_raw_down(KBD_CHEAT_UPSIDE) &&
(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);
/* ±минута — на голых +/-, как в оригинале. С Ctrl те же
* клавиши означают обход комнат (sprpop.c), поэтому здесь Ctrl
* обязан быть ОТПУЩЕН, иначе сработает и то и другое. */
uint8_t nomod = (uint8_t)!(kbd_raw_down(KBD_LCTRL) ||
kbd_raw_down(KBD_RCTRL));
uint8_t ts = (uint8_t)(nomod && (kbd_raw_down(KBD_TIME_SUB_MAIN) ||
kbd_raw_down(KBD_TIME_SUB_NUM)));
uint8_t ta = (uint8_t)(nomod && (kbd_raw_down(KBD_TIME_ADD_MAIN) ||
kbd_raw_down(KBD_TIME_ADD_NUM)));
/* C — записать POP.CFG сейчас (отладка MS2, до экрана Settings:
* там сохранять будет Apply/выход). */
uint8_t cf = kbd_raw_down(0x21); /* C (PS/2 set 2) */
/* ЧИТЫ ЗДОРОВЬЯ И ПЕРА (seg000:838, :845, :795).
*
* Делают то же, что соответствующие зелья, но НЕ через них:
* оригинал в этих ветках не зовёт stop_sounds, хотя ветка
* зелья зовёт (seg006:1871). Повторяем чит, а не зелье.
* Исключение — перо: там stop_sounds сидит внутри самого
* feather_fall(), поэтому остаётся.
*
* Shift+S делит скан-код с Ctrl+S (звук), поэтому Ctrl обязан
* быть отпущен; Shift+T — с голым T (таймер), но тот сам
* требует отпущенного Shift. */
uint8_t shift = (uint8_t)(kbd_raw_down(KBD_LSHIFT) ||
kbd_raw_down(KBD_RSHIFT));
uint8_t h1 = (uint8_t)(shift && nomod &&
kbd_raw_down(KBD_SOUND_TOGGLE));
uint8_t hm = (uint8_t)(shift && kbd_raw_down(KBD_TIMER));
uint8_t ff = (uint8_t)(shift && kbd_raw_down(KBD_KEY_W));
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
/* Shift+S — одна единица HP, как малое красное зелье. */
if (pop_cheats && h1 && !hp1_prev && hitp_curr != hitp_max) {
pop_sfx_play(33);
hitp_delta = 1;
pop_flash_color = POP_FLASH_RED;
pop_flash_time = 2;
}
/* Shift+T — поднять ПОТОЛОК HP, как большая склянка. Оригинал
* здесь условия «не полон» не ставит: потолок растёт всегда. */
if (pop_cheats && hm && !hpmax_prev) {
pop_sfx_play(30);
pop_add_life();
pop_flash_color = POP_FLASH_RED;
pop_flash_time = 4;
}
/* Shift+W — медленное падение, как зелье пера. */
if (pop_cheats && ff && !feather_prev) {
pop_feather = POP_FEATHER_FRAMES;
pop_sfx_stop();
pop_sfx_play(39);
pop_flash_color = POP_FLASH_GREEN;
pop_flash_time = 3;
}
hp1_prev = h1; hpmax_prev = hm; feather_prev = ff;
/* Бессмертие по кругу: 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 (m && !music_prev) {
pop_mus_want ^= 1; /* не под pop_cheats */
pop_settings.music_enabled = pop_mus_want;
pop_status_msg = (uint8_t)(pop_mus_want ? POP_MSG_MUSIC_ON
: POP_MSG_MUSIC_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;
music_prev = m;
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_forget();
pop_cd_init();
pop_hp_invalidate();
/* Полоса состояния тоже переписана заново — «QUICKLOAD» обязан лечь
* поверх свежего фона на ОБЕИХ страницах, иначе он виден через кадр и
* мигает. */
pop_status_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_forget();
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.ani") != 0) { /* кадры+seqtbl в EMM-странице */
puts("kid.ani 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(KID_PAGES) != 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(POP_PAL_DIM_BLACK);
}
/* Стартовая комната до 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) */
/* ЗАМЕР 2026-08-28: глушение снято ВРЕМЕННО, чтобы измерить
* зазоры насоса на загрузке уровня (дедлайн 251 000 тактов).
* Если зазоры укладываются — снятие остаётся, и трек заставки
* доигрывает поверх загрузки, как в оригинале (seg001:690). */
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 на границе растра: впервые показать
* готовую комнату уже с окончательной палитрой уровня.
* Вторую страницу синхронизировать аппаратной копией. */
/* ЖДЁМ КОНЦА ТРЕКА ЗАСТАВКИ НА ЧЁРНОМ ЭКРАНЕ — порт
* seg003:107:
* while (check_sound_playing() && !do_paused()) idle();
* stop_sounds();
* draw_level_first();
* Порядок оригинала важен: загрузка уровня идёт ПОД
* звучащий трек, и только потом чернота до его конца. Тогда
* общее время черноты = max(трек, загрузка), а не их сумма,
* и трек не обрывается на полуслове. Раньше у нас было
* наоборот: pre_cut_finish досиживал трек, потом глушил
* насос и грузил уровень.
*
* ЗАМЕР 2026-08-28: 500 подряд вызовов насоса через всю
* загрузку — максимальный зазор 245 832 такта при дедлайне
* 251 000, ни одной пропущенной порции. Поэтому глушение на
* загрузке снято и музыка её переживает.
*
* Счётчик — страховка от вечного ожидания (потоковый трек,
* который никогда не кончится): ~20 с и идём дальше. */
{
uint16_t mus_guard = 1000;
while (pop_music_busy() && mus_guard) {
mus_guard--;
pop_wait_edge();
}
pop_music_free(); /* порт stop_sounds() */
}
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(POP_PAL_DIM_BLACK);
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(POP_PAL_DIM_BLACK);
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();
}