Files
Sprinter-SDCC/applications/PoP/roomtest/roomtest_cold.c
T
snark13 4a939e42f8 Катсцены: тайминги, часы и «время вышло» — по оригиналу; тумблер музыки
ТАЙМИНГИ. Треки заставок между уровнями длиннее самих сцен (27 — 10,7 с
против 2,6 с картинки), и оригинал их не обрывает: load_intro гасит экран
и возвращает управление, а звук продолжает играть поверх загрузки уровня
и первых секунд игры (seg001:690). Мы глушили CBL на выходе — музыка
обрывалась на полуслове. Больше не глушим.

«ВРЕМЯ ВЫШЛО». Оригинал (seg001:04D3) не зовёт ни одного init_*: на
экране пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка
нет). Мы показывали готовую композицию с принцессой и полными часами —
противоположное по смыслу. Теперь это живая сцена без персонажей,
102 кадра, и трек 36 дослушивается под чёрным экраном, как в оригинале.

ПЕСОЧНЫЕ ЧАСЫ. Сцена «времени мало» (cutscene_12) шла статической
композицией, куда упаковщик запёк res953 — ПОЛНЫЕ часы. Отсюда и
наблюдение «чем меньше времени, тем полнее часы»: картинка была одна и та
же независимо от таймера. Сделана живой (cut_scene_12_short: принцесса
стоит правее, через два кадра оборачивается), часы берутся от
pre_hourglass_state. Заодно: при state 7 струйка песка не рисуется.

МУЗЫКА — СВОЙ ТУМБЛЕР. Settings -> MUSIC, Ctrl+M и статус в debug bar.
Флаг pop_mus_want отдельный от звукового: музыка у нас поток с диска, и
выключают её по другим причинам, чем эффекты; выключение обрывает текущий
трек, эффекты продолжают звучать. Строка-заглушка GAMEPLAY PROFILE
(«VANILLA ONLY») уступила место MUSIC — на десятую строку экрана не
хватает. Подписи тумблеров в debug bar сокращены до S:/M:/I:.

БАГИ. После меню персонаж оставался невидимым до первого движения:
снимок слота продолжал утверждать, что кадр на странице уже нарисован.
Общая pop_cd_forget() теперь зовётся после меню, QuickLoad и переворота
экрана. «QUICKLOAD» мигал через кадр — pop_status_show_now снимал бит
видимой страницы, а её успевало переписать восстановление комнаты.

Host-тесты: 15 наборов. Проверено в MAME: debug bar, выход из меню.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 09:28:36 +03:00

1132 lines
73 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.
/*
* roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК
* (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на
* уровень или только по отладочной клавише; кадровый путь остался в
* roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h.
*
* Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого
* модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие
* __banked-модули через трамплины.
*/
#include <graphics.h>
#include <gfx.h>
#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 <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_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 <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) */
}
}
/* 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();
}
/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- *
*
* Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки,
* включая недостижимые обычным путём.
*
* Здесь БЫЛ 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;
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 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 = 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 (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-странице */
printf("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(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();
}