diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index bb74c28..3a6135f 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -1206,6 +1206,38 @@ N навык M» — отдельная надобность, если пона Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке. +### MOB-SPR-EMM. Композит падающего куска — в EMM (1284 Б W2) + +`mob_spr` (`pop_room.c`) — самый крупный буфер в W2: 4 + 64*20 байт под +композит падающей плиты. Это половина нынешнего дефицита W2. + +**В БАНК ЕГО ПЕРЕНЕСТИ НЕЛЬЗЯ** (разобрано 2026-08-26). Цепочка рисования +— `pop_room` (банк 7) -> `pop_mem_b` (банк 5) -> `blit_b_clip` (резидент), и +трамплин на `pop_mem_b` переключает W3. Указатель на данные банка 7 после +этого показывает на страницу банка 5: блит нарисует мусор, причём молча. +`--bank-data` вдобавок действует на весь модуль, а `mobs` из того же файла +читают снаружи. + +**ЧТО РАБОТАЕТ — EMM-страница с маппингом в W0.** W0 не переключают ни +трамплин (он трогает только W3), ни звуковой ISR (насос живёт в W3), значит +адрес `0x0000+off` валиден и в банке 7, и в банке 5, и в резиденте — ровно +как у атласных спрайтов, которые тем же блитом и рисуются. `blit_b_clip` +окно не трогает, маппингом занимается вызывающий. + +**Цена.** `mob_spr_build` читает три части через `gfx_w0_map(a->page)`, а +приёмник должен быть в том же окне — держать оба нельзя. Сборку придётся +вести построчно: строка части в 64-байтовый буфер на стеке, перемаппить +окно, записать. Вызывается это лишь на смену тайлсета. Плюс +`gfx_w0_map`/`unmap` вокруг каждого куска — около 1350 тактов, при шести +кусках меньше 2 % кадра. + +**Чем подтверждать.** Падающие плиты в комнате 1.2 и каскад уровня 13: +ошибка проявится не крашем, а мусором на месте куска. + +**Дешёвая половинчатая мера** (если 1284 Б не нужны целиком): буфер объявлен +с запасом 64x20, фактический композит около 58x19 — точные константы дают +~180 байт без единого риска. + ### PAL-ARC. Палитры — в архив своей группы (полишинг) Файлов палитр в игре шесть: `KID\kid.pal` (1024 Б), `PV\story.pal`, diff --git a/applications/PoP/roomtest/pop_config.c b/applications/PoP/roomtest/pop_config.c index ba9d117..e5a2d3f 100644 --- a/applications/PoP/roomtest/pop_config.c +++ b/applications/PoP/roomtest/pop_config.c @@ -51,7 +51,7 @@ void pop_settings_defaults(void) __banked pop_settings.show_build_info = 0; pop_settings.enhancement_flags = 0; pop_settings.time_limit_enabled = 1; - pop_settings.show_debug_bar = 1; /* roomtest — dev-сборка */ + pop_settings.show_debug_bar = 0; /* по умолчанию чистый экран */ } /* Применить модель к настоящим владельцам runtime-состояния. Sound нельзя diff --git a/applications/PoP/roomtest/pop_intro.c b/applications/PoP/roomtest/pop_intro.c index 8c1c0de..6a565b6 100644 --- a/applications/PoP/roomtest/pop_intro.c +++ b/applications/PoP/roomtest/pop_intro.c @@ -721,6 +721,7 @@ static int intro_pv_animated(void) uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 }; uint8_t sand_frame = 0; uint8_t flash_done = 0; + uint8_t faded_in = 0; uint8_t flash; intro_skip_t skip; int result = -1; @@ -753,7 +754,9 @@ static int intro_pv_animated(void) if (bad) return -1; } - pop_pal_fade_in(INTRO_FADE); + /* Проявление — ПОСЛЕ первого нарисованного кадра (см. ниже в цикле): + * иначе две секунды fade идут по пустой комнате, а принцесса возникает + * уже на свету. */ /* Актёрские атласы в EMM; дальше по сцене диск трогает только подкачка * следующей реплики — по одной странице за кадр (pop_music_load_step). * Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI @@ -838,6 +841,9 @@ static int intro_pv_animated(void) torch_right, sand_frame, flash, render_tick); pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */ + /* Первый кадр собран под чёрной палитрой — теперь проявляем его + * целиком, вместе с принцессой и факелами. */ + if (!faded_in) { faded_in = 1; pop_pal_fade_in(INTRO_FADE); } pop_music_service(); /* заявки на музыку из сцены (эффект без оцифровки) */ /* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра: * 33 мс диска укладываются в паузу логического кадра (133 мс) и @@ -940,7 +946,6 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames) } pop_screen_present(1); - pop_pal_fade_in(INTRO_FADE); result = 0; full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; intro_skip_begin(&skip); @@ -968,6 +973,9 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames) /* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */ for (hold = 1; hold < 5; hold++) gfx_wait_vsync(); + /* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по + * пустой комнате, а часы и пламя возникают уже на свету. */ + if (frame == 0) pop_pal_fade_in(INTRO_FADE); torch_left = (uint8_t)((torch_left + 1) % 9); torch_right = (uint8_t)((torch_right + 1) % 9); sand = (uint8_t)((sand + 1) % 3); @@ -1332,8 +1340,13 @@ static int cut_scene_8(void) cut_kid_init(CHARID_24_MOUSE, 144, 167, 106); /* princess_crouching (seg001:024D): сидит и гладит мышь. */ cut_shad_init(131, 169, 110); - pop_pal_fade_in(INTRO_FADE); - aborted = cut_run(20, 1); + /* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это + * время на экране была бы пустая комната: персонажи появляются только с + * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда + * картинка проявляется целиком, вместе с актёрами. */ + aborted = cut_run(1, 1); + if (!aborted) pop_pal_fade_in(INTRO_FADE); + if (!aborted) aborted = cut_run(19, 1); if (!aborted) { cut_kid_seq(107); /* мышь встаёт и уходит */ aborted = cut_run(20, 1); @@ -1356,10 +1369,15 @@ static int cut_scene_9(void) /* princess_stand (seg001:026A) — поза PV1, лицом вправо. */ cut_shad_init(144, 169, 94); Guard.direction = 0; - pop_pal_fade_in(INTRO_FADE); + /* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это + * время на экране была бы пустая комната: персонажи появляются только с + * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда + * картинка проявляется целиком, вместе с актёрами. */ + aborted = cut_run(1, 0); /* мышь ещё за кадром */ + if (!aborted) pop_pal_fade_in(INTRO_FADE); /* init_mouse_go (seg001:022A): мышь вбегает справа. */ cut_kid_init(CHARID_24_MOUSE, 199, 167, 105); - aborted = cut_run(5, 1); + if (!aborted) aborted = cut_run(5, 1); if (!aborted) { cut_shad_seq(112); /* принцесса приседает */ aborted = cut_run(9, 1); @@ -1383,8 +1401,13 @@ static int cut_scene_12_short(void) if (cut_begin(pre_hourglass_state()) != 0) return -1; /* init_princess_x156 (seg001:0230) = init_princess + x = 156. */ cut_shad_init(156, 166, 94); - pop_pal_fade_in(INTRO_FADE); - aborted = cut_run(2, 0); + /* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это + * время на экране была бы пустая комната: персонажи появляются только с + * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда + * картинка проявляется целиком, вместе с актёрами. */ + aborted = cut_run(1, 0); + if (!aborted) pop_pal_fade_in(INTRO_FADE); + if (!aborted) aborted = cut_run(1, 0); if (!aborted) { cut_shad_seq(98); /* princess turn around [PV1] */ aborted = cut_run(24, 0); @@ -1406,9 +1429,14 @@ static int cut_ending(void) /* init_ending_princess (seg001:0251) + init_ending_kid (seg001:0287). */ cut_shad_init(136, 164, 109); cut_kid_init(CHARID_0_KID, 198, 164, 1); - pop_pal_fade_in(INTRO_FADE); + /* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это + * время на экране была бы пустая комната: персонажи появляются только с + * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда + * картинка проявляется целиком, вместе с актёрами. */ + aborted = cut_run(1, 1); + if (!aborted) pop_pal_fade_in(INTRO_FADE); pop_sfx_play(26); /* «arrived to princess» */ - aborted = cut_run(8, 1); + if (!aborted) aborted = cut_run(7, 1); if (!aborted) { cut_shad_seq(108); /* поворот и объятие */ aborted = cut_run(5, 1); diff --git a/applications/PoP/roomtest/pop_menu.c b/applications/PoP/roomtest/pop_menu.c index 919182d..6753805 100644 --- a/applications/PoP/roomtest/pop_menu.c +++ b/applications/PoP/roomtest/pop_menu.c @@ -687,7 +687,11 @@ int8_t pop_menu_process(void) __banked getfillsettings(&saved_fill); pop_sfx_pause(); /* snapshot/dim + первый full redraw */ pop_pal_snapshot(); - pop_pal_menu_dim(2); /* 50%, диапазон UI остаётся ярким */ + /* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не + * числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы + * теперь затемнение на одну шестнадцатую — фон под меню почти не гас + * (поймано пользователем 2026-08-26). */ + pop_pal_menu_dim(POP_PAL_DIM_BLACK / 2); menu_draw(sel); for (;;) { diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 0e38647..ebecd64 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -128,11 +128,6 @@ static void kbd_idle(void) #define SEQ_STAND 2 #define ROOM_1 1 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ -#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */ -#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */ -#define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */ -#define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc, - * пока меню не готово рисовать — см. план MS4) */ /* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного @@ -417,63 +412,20 @@ int main(void) * QuickSave/QuickLoad идут тем же механизмом, что F6/F9 (запрос * подхватит блок ниже в этой же границе кадра), рестарты исполняются * после вычисления back-страницы (как респавн), QUIT рвёт цикл. */ + /* Меню, F10 и QuickSave/Load — целиком в банке 8 (pop_frame_ui). + * Всё это события «раз в партию», а кода на них было полсотни строк + * в резиденте; наружу остаются флаги, которые нужны самому циклу. */ if (!is_demo) { - uint8_t e = kbd_raw_down(KBD_MENU); - 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)) { - failed = 1; quit = 1; break; - } - 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) menu_restart = 1; - else if (mrc == POP_MENU_RESTART_GAME) { - if (!pop_app_dispatch(POP_APP_EV_RESTART_INTRO)) failed = 1; + uint8_t want_restart = 0, dead_reset = 0; + int8_t urc = pop_frame_ui(&want_restart, &dead_reset); + + if (want_restart) menu_restart = 1; + if (dead_reset) dead_frames = 0; + if (urc) { + if (urc < 0) failed = 1; + if (urc != 1) quit = 1; /* 1 — restart game, без quit */ break; } - else if (mrc == POP_MENU_QUIT) { - if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1; - quit = 1; - break; - } - if (menu_opened) { - /* Меню переписало обе страницы целиком. Снимки слотов об - * этом не знают и продолжают считать, что персонаж на - * странице уже нарисован, — стоящий Кид оставался бы - * невидимым до первого своего движения. */ - pop_cd_forget(); - pop_status_invalidate(); - if (!pop_app_dispatch(POP_APP_EV_RESUME)) { - failed = 1; quit = 1; break; - } - } - } - /* F10 — отладочный немедленный выход (раньше был на Esc, но Esc - * теперь открывает меню; уйдёт вместе с отладкой). */ - if (!is_demo && kbd_raw_down(KBD_QUIT)) { - if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1; - quit = 1; - break; - } - /* F6/F9 только ставят запрос; файловая операция выполняется здесь, - * на границе кадров — вне окон Char и до heal/tick/draw. */ - if (!is_demo) { - uint8_t s = kbd_raw_down(KBD_QUICKSAVE); - uint8_t l = kbd_raw_down(KBD_QUICKLOAD); - int8_t qrc; - 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; - qrc = pop_qsave_process(); - if (qrc > 0) dead_frames = 0; } if (frozen) { PROF(0); /* чёрный: ждём vsync */ @@ -761,66 +713,12 @@ int main(void) * или доиграли запись — показать текущий кадр и затем вернуться в * title. В частности, не вызываем level_switch(0->1). */ if (is_demo && (kid_room == 24 || pop_kid_dead)) demo_finished = 1; - if (!is_demo && pop_next_level != pop_current_level) { - 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)) { - failed = 1; - quit = 1; - break; - } - pop_pre_cutscene_show(pre); - if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) { - failed = 1; - quit = 1; - break; - } - /* Катсцена оставляет экран чёрным. Новый уровень будет - * открыт одним применением его готовой палитры. */ - reveal_pending = 1; - } else { - /* CBL глушим ДО первой же операции с палитрой. Палитра идёт - * через BIOS ($A4) с запрещёнными прерываниями: насос не - * успевает долить блок, железо крутит хвост своего буфера — - * на слух скрежет. Раньше pop_sfx_pause() звался только - * внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и - * переход без катсцены (в т.ч. телепорт Shift+L) скрежетал - * почти всегда — поймано пользователем 2026-08-25. Ветка с - * катсценой свободна от этого: pop_pre_cutscene_show глушит - * звук первым действием. */ - pop_sfx_pause(); - /* Без fade: старый кадр гасим сразу и обязательно даём - * показать один чёрный растр ДО диска/перерисовки. */ - pop_pal_apply(4); - pop_wait_edge(); - reveal_pending = 1; - if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED)) { - failed = 1; - quit = 1; - break; - } - } - if (pop_level_switch() != 0 || - !pop_app_dispatch(POP_APP_EV_LEVEL_READY)) { - failed = 1; - quit = 1; - break; - } - 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); + /* Смена уровня целиком уехала в банк 8 (pop_level_change): это + * событие раз в несколько минут, а кода на нём было под сотню строк + * в самом горячем месте программы — резидент W1 у нас в дефиците. */ + if (!is_demo && pop_next_level != pop_current_level && + pop_level_change(back) != 0) { + failed = 1; quit = 1; break; } if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */ dead_frames++; diff --git a/applications/PoP/roomtest/roomtest_cold.c b/applications/PoP/roomtest/roomtest_cold.c index b4cda1d..e48fbeb 100644 --- a/applications/PoP/roomtest/roomtest_cold.c +++ b/applications/PoP/roomtest/roomtest_cold.c @@ -30,6 +30,16 @@ #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 #include "pop_status.h" /* статус-строка и отладочная полоса — банк 9 */ #include "pop_menu.h" /* pop_menu_init/free — атлас UI в bank 9 */ #include "pop_ui.h" /* ранняя чёрная палитра + snapshot стартового экрана */ @@ -73,6 +83,134 @@ static void find_start_level_door(uint8_t room) } } + + +/* МЕНЮ, 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; + uint8_t e = kbd_raw_down(KBD_MENU); + 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; + } + /* F10 — отладочный немедленный выход (раньше был на Esc, но Esc теперь + * открывает меню; уйдёт вместе с отладкой). */ + if (kbd_raw_down(KBD_QUIT)) + return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1); + /* 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 глушит + * звук первым действием. */ + pop_sfx_pause(); + /* Без 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). * @@ -937,7 +1075,7 @@ int pop_boot(void) __banked * понадобится как источник обычного fade-in. Для build-info экран * остаётся штатный яркий игровой фон. */ if (!pop_settings.show_build_info) { - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); } /* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново @@ -1091,7 +1229,7 @@ int pop_new_game_load(void) __banked /* Title мог завершиться на яркой title-палитре. Сначала погасить её, * затем восстановить полный game snapshot; level_switch собирает обе * страницы уже невидимо, и первый игровой кадр проявляется. */ - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); gfx_set_draw_page(0); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill_game(); @@ -1099,7 +1237,7 @@ int pop_new_game_load(void) __banked pop_screen_fill_game(); gfx_set_visible_page(1); pop_pal_game_load(); - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); if (pop_level_switch() != 0) return -1; /* Первый уровень появляется сразу, без fade, но только после того как * обе страницы и его собственная палитра полностью готовы. */ diff --git a/applications/PoP/roomtest/roomtest_cold.h b/applications/PoP/roomtest/roomtest_cold.h index 870e13e..d1fe99b 100644 --- a/applications/PoP/roomtest/roomtest_cold.h +++ b/applications/PoP/roomtest/roomtest_cold.h @@ -93,6 +93,23 @@ int pop_boot(void) __banked; * файла нет — молча остаться на текущем. -1 — фатально. */ int pop_level_switch(void) __banked; +/* Смена уровня целиком (катсцена, гашение, загрузка, палитра, звук). + * back — страница кадра главного цикла. 0 — ок, -1 — сбой. */ +int8_t pop_level_change(uint8_t back) __banked; + +/* Клавиши служебного слоя. Живут здесь, а не в roomtest.c: сам обработчик + * (pop_frame_ui) переехал в банк, а главный цикл о них больше не знает. */ +#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */ +#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */ +#define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */ +#define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc, + * пока меню не готово рисовать — см. план MS4) */ + +/* Меню, F10 и QuickSave/Load на границе кадра. restart_level/dead_reset — + * флаги для главного цикла. 0 — продолжаем, 1 — Restart Game, 2 — Quit, + * -1 — сбой. */ +int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked; + /* Чистый старт настоящей игры после Enter/Esc на title/intro/credits либо * клавиши в attract-demo. Ждёт отпускания клавиши, сбрасывает состояние * новой игры, загружает FIRST_LEVEL и проявляет его первый кадр. */