From 0db4f947076f2319202186d5895aa5f0656eef97 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 26 Aug 2026 09:47:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D1=82=D1=81=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B:=20fade=20=D0=B4=D0=BB=D0=B8=D0=BD=D0=BE=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BA=20=D0=B2=20=D0=BE=D1=80=D0=B8=D0=B3=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5,=20=D0=BC=D1=83=D0=B7=D1=8B=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=81=D0=BB=D1=83=D1=88=D0=B8=D0=B2=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B4=D0=BE=20=D1=83=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BD=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ДЛИТЕЛЬНОСТЬ. fade_in_1/fade_out_1 у оригинала — 64 шага палитры по два тика, то есть 128 тиков = 2,13 с каждый; сцена перед уровнем 2 идёт с ними около семи секунд. Наши четыре кадра укладывались в восемь сотых секунды, и сцена выходила втрое короче. INTRO_FADE и TITLE_FADE теперь POP_T60(128). Ступеней яркости стало восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, 1/8, 0 — каждая парой сдвигов, умножения на Z80 не нужно): растянуть четыре ступени на две секунды значило бы получить четыре скачка яркости. МУЗЫКА. Прошлый коммит отдавал трек доигрывать уже в игре — так делает оригинал. На слух вышло хуже: музыка спотыкается, потому что загрузка уровня не даёт насосу долить блок вовремя (у DOS-версии такой проблемы нет). Дослушиваем под чёрным экраном, до отрисовки уровня — тем более что следующий load_intro у оригинала всё равно начинается с ожидания тишины (seg001:681). Пропуск сцены обрывает и музыку. Заодно: WAIT перед PV пересчитан (584 вместо 707 — наш fade больше не короче), а тема титров пускается ПОСЛЕ fade_in, как в show_title. Host-тесты: 15 наборов. Проверено в MAME: плавное появление титров. Co-Authored-By: Claude Opus 5 --- applications/PoP/docs/sound_plan.md | 20 +++++++ applications/PoP/roomtest/pop_hof.c | 4 +- applications/PoP/roomtest/pop_intro.c | 76 ++++++++++++++++++++------- applications/PoP/roomtest/pop_title.c | 25 +++++---- applications/PoP/roomtest/pop_ui.c | 44 ++++++++++------ applications/PoP/roomtest/pop_ui.h | 7 ++- 6 files changed, 128 insertions(+), 48 deletions(-) diff --git a/applications/PoP/docs/sound_plan.md b/applications/PoP/docs/sound_plan.md index 8459038..78f122e 100644 --- a/applications/PoP/docs/sound_plan.md +++ b/applications/PoP/docs/sound_plan.md @@ -883,6 +883,26 @@ EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_mu для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это есть свои два байта. +**ГДЕ КОНЧАЕТСЯ МУЗЫКА СЦЕНЫ** (уточнено 2026-08-26). Сначала мы отдали +трек «доигрывать в игре»: у оригинала load_intro после сцены просто гасит +экран и возвращает управление. На слух оказалось хуже, чем в оригинале — +музыка спотыкается: загрузка уровня (ESTEX плюс сборка комнаты) не даёт +насосу долить блок вовремя. У DOS-версии этой проблемы нет, там звук +живёт своей жизнью на аппаратуре. + +Поэтому дослушиваем ПОД ЧЁРНЫМ ЭКРАНОМ, до отрисовки уровня: следующий +load_intro у оригинала и так начинается с ожидания тишины (seg001:681), то +есть к новому уровню трек в любом случае смолкает. Пропуск сцены обрывает +и музыку — игрок нажал клавишу, чтобы идти дальше. + +**ДЛИТЕЛЬНОСТЬ FADE.** fade_in_1/fade_out_1 — это 64 шага палитры по два +тика, 128 тиков = 2,13 с; сцена перед уровнем 2 занимает с ними около семи +секунд. Наши четыре ступени укладывались в восемь сотых секунды, и сцена +выходила втрое короче. Теперь `INTRO_FADE = POP_T60(128)`, а ступеней в +`pop_ui_palette_dim` восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, +1/8, 0 — каждая парой сдвигов, без умножения): на четырёх растянутых +ступенях затемнение выглядело бы скачками. + **Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM — в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те, diff --git a/applications/PoP/roomtest/pop_hof.c b/applications/PoP/roomtest/pop_hof.c index 97d92fa..62222c0 100644 --- a/applications/PoP/roomtest/pop_hof.c +++ b/applications/PoP/roomtest/pop_hof.c @@ -330,7 +330,7 @@ void pop_hof_show(void) __banked row = hof_insert_current(); pop_pal_story_load(); - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); hof_draw(row); pop_pal_fade_in(HOF_FADE); @@ -357,7 +357,7 @@ void pop_hof_show_title(void) __banked if (!hof_has_entries()) return; pop_sfx_pause(); pop_pal_story_load(); - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); hof_draw(HOF_NO_ROW); pop_pal_fade_in(HOF_FADE); hof_wait_release(); diff --git a/applications/PoP/roomtest/pop_intro.c b/applications/PoP/roomtest/pop_intro.c index 9aa4a7f..8c24b1a 100644 --- a/applications/PoP/roomtest/pop_intro.c +++ b/applications/PoP/roomtest/pop_intro.c @@ -27,7 +27,13 @@ #include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */ #define INTRO_PARTS 5 -#define INTRO_FADE 4 +/* ДЛИТЕЛЬНОСТЬ FADE. У оригинала это 64 шага палитры по два тика — + * 128 тиков, то есть 2,13 с (fade_in_1/fade_out_1 -> fade_*_frame -> + * do_simple_wait(1) при wait_time = 2). Наши четыре кадра укладывались в + * восемь сотых секунды, и вся сцена выходила втрое короче оригинальной. + * Число здесь — кадры луча, ступени яркости внутри распределяет + * pop_ui_fade_*. */ +#define INTRO_FADE POP_T60(128) /* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот * закрывается, затем открывается дверь покоев, из которой входит Джафар. */ @@ -104,13 +110,14 @@ enum { * SDLPoP: transition_ltr 80 тиков + pop_wait 0x258 + fade_out_2, а тот * гасит палитру за 0x40 шагов по 2 тика = 128 тиков; * у нас: полосовой переход 80 кадров луча (= 96 тиков) + WAIT + fade - * всего в четыре ступени палитры (= 5 тиков). - * Отсюда 80 + 600 + 128 - 96 - 5 = 707. Дальше и там и там экран уже - * чёрный, а трек доигрывает — этой паузой сцена и стыкуется с PV. */ + * той же длины (INTRO_FADE = 128 тиков). + * Теперь, когда наш fade идёт те же 128 тиков, остаётся поправка на один + * лишь переход: 80 + 600 + 128 - 96 - 128 = 584. Дальше и там и там экран + * уже чёрный, а трек доигрывает — этой паузой сцена и стыкуется с PV. */ static const pop_cs_cmd_t intro_before_pv[] = { /* absence собирается прямо поверх последнего title-кадра полосовым * transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */ - { POP_CS_WAIT, POP_T60(707) }, /* story 1: In the absence */ + { POP_CS_WAIT, POP_T60(584) }, /* story 1: In the absence */ { POP_CS_FADE_OUT, 0 }, { POP_CS_END, 0 } }; @@ -328,7 +335,7 @@ static void intro_prepare_palette(uint8_t pal) { intro_pending_pal = pal; intro_pal_pending = 1; - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); intro_black_pages(); } @@ -336,7 +343,7 @@ static void intro_commit_palette(void) { if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load(); else pop_pal_story_load(); - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); intro_pal_pending = 0; } @@ -1032,7 +1039,7 @@ static void intro_restore_game_palette(void) * переходом в demo. */ intro_black_pages(); pop_pal_game_load(); - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); } /* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= * @@ -1303,8 +1310,13 @@ static int cut_begin(uint8_t hg_state) return 0; } +/* Прервали ли последнюю живую сцену клавишей — нужно вызывающему: по + * пропуску музыку обрываем, а не дослушиваем. */ +static uint8_t cut_aborted; + static void cut_end(uint8_t aborted) { + cut_aborted = aborted; if (!aborted) pop_pal_fade_out(INTRO_FADE); cut_free(); } @@ -1556,6 +1568,31 @@ uint8_t pop_intro_show(void) __banked return 0; } +/* Хвост любой сцены перед уровнем. + * + * ДОСЛУШИВАЕМ ПОД ЧЁРНЫМ ЭКРАНОМ, а не «пусть играет в игре». Оригинал + * так и делает: следующий load_intro начинается с ожидания тишины + * (seg001:681), и к отрисовке уровня трек уже смолк. Мы сначала отдали + * музыку в игру — и она там спотыкалась: загрузка уровня (ESTEX + сборка + * комнаты) не даёт насосу долить блок вовремя. Ждать здесь дешевле, чем + * слушать заикание. + * + * Пропуск сцены обрывает и музыку: игрок нажал клавишу, чтобы идти + * дальше, а не чтобы досидеть до конца трека. */ +static void pre_cut_finish(uint8_t aborted) +{ + if (!aborted) { + intro_skip_t tail; + + intro_skip_begin(&tail); + while (pop_music_busy() && !intro_skip_requested(&tail)) + pop_wait_edge(); + } + pop_music_free(); + pop_sfx_pause(); + intro_restore_game_palette(); +} + void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked { const pop_cs_cmd_t *script = 0; @@ -1584,23 +1621,24 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked } } if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) { - if (pre_room_animated((uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING - : INTRO_PV_STAND), - pre_hourglass_state(), 26) >= 0) { - intro_restore_game_palette(); + int r = pre_room_animated((uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING + : INTRO_PV_STAND), + pre_hourglass_state(), 26); + if (r >= 0) { + pre_cut_finish((uint8_t)(r > 0)); return; } /* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */ } if (scene == POP_PRE_12_SHORT && cut_scene_12_short() == 0) { - intro_restore_game_palette(); + pre_cut_finish(cut_aborted); return; } /* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */ if (scene == POP_PRE_8 || scene == POP_PRE_9) { int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9(); if (done == 0) { - intro_restore_game_palette(); + pre_cut_finish(cut_aborted); return; } } @@ -1609,10 +1647,12 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked else if (scene == POP_PRE_8) script = pre_8; else if (scene == POP_PRE_9) script = pre_9; else if (scene == POP_PRE_12_SHORT) script = pre_12_short; - if (!script) return; + if (!script) { + pre_cut_finish(1); + return; + } - intro_run(script); - intro_restore_game_palette(); + pre_cut_finish(intro_run(script)); } void pop_time_expired_show(void) __banked @@ -1650,7 +1690,7 @@ void pop_ending_show(void) __banked * начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка * ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает» * перед встречей. */ - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); /* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по * скрипту идёт только текст HAIL, поэтому статический сценарий ниже * начинается с него; при нехватке ресурсов отыгрывается целиком. */ diff --git a/applications/PoP/roomtest/pop_title.c b/applications/PoP/roomtest/pop_title.c index d51a88f..5ca7b39 100644 --- a/applications/PoP/roomtest/pop_title.c +++ b/applications/PoP/roomtest/pop_title.c @@ -26,7 +26,7 @@ #define TITLE_H 200 #define TITLE_Y POP_YOFF #define TITLE_PARTS 5 -#define TITLE_FADE 4 +#define TITLE_FADE POP_T60(128) /* 64 шага по 2 тика, как fade_*_1 */ enum { TITLE_MAIN = 0, @@ -106,13 +106,13 @@ static uint8_t title_wait(uint16_t ticks) static void title_load_palette_dimmed(void) { pop_pal_title_load(); - pop_pal_apply(4); + pop_pal_apply(POP_PAL_DIM_BLACK); } uint8_t pop_title_show(void) __banked { uint8_t step; - uint8_t skipped = 0; + uint8_t skipped = 0, music_ok = 0; int prepared; uint8_t cold_start = (uint8_t)(!title_shown && !pop_settings.show_build_info); @@ -134,18 +134,21 @@ uint8_t pop_title_show(void) __banked pop_screen_present((uint8_t)prepared); title_shown = 1; - /* Тема заставки. Порядок как на первом экране истории: сначала диск, - * потом открытие CBL, потом пуск — иначе первый же блок насос отдал бы - * из ещё не дочитанной страницы. */ - if (pop_music_load(POP_MUS_INTRO) == 0) { - (void)pop_sfx_start(); - pop_music_play(); - } + /* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже — + * после fade_in, ровно как show_title: fade_in_2, затем + * play_sound(sound_54). */ + music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0); for (step = 0; step < sizeof(title_scene); step++) { if (step && title_draw(title_scene[step], 1) < 0) break; - if (step == 0) pop_pal_fade_in(TITLE_FADE); + if (step == 0) { + pop_pal_fade_in(TITLE_FADE); + if (music_ok) { + (void)pop_sfx_start(); + pop_music_play(); + } + } if (title_wait(title_wait_ticks[step])) { skipped = 1; break; diff --git a/applications/PoP/roomtest/pop_ui.c b/applications/PoP/roomtest/pop_ui.c index 8a3bbc8..0b2ea24 100644 --- a/applications/PoP/roomtest/pop_ui.c +++ b/applications/PoP/roomtest/pop_ui.c @@ -152,13 +152,35 @@ void pop_ui_palette_snapshot(void) __banked /* Из одного исходного снимка приготовить обе экранные палитры. Умножения * нет: шкала 1/4, 1/2, 3/4 на 8-битном значении не должна тащить делитель * в постоянно вызываемый (во время fade) путь. */ +/* ВОСЕМЬ СТУПЕНЕЙ, а не четыре. Fade у оригинала идёт 64 шагами по два + * тика — 2,13 с плавного затухания; наши четыре ступени укладывались в + * доли секунды и выглядели как моргание, а растянуть их по времени значило + * получить четыре заметных скачка яркости. Восемь ступеней собираются + * пáрой сдвигов каждая (умножения на Z80 не нужно), а по времени их + * распределяет вызывающий (pop_ui_fade_*). + * + * dim_step: 0 — исходная, 8 — чёрная. */ +static uint8_t dim_value(uint8_t v, uint8_t step) +{ + switch (step) { + case 1: return (uint8_t)(v - (v >> 3)); /* 7/8 */ + case 2: return (uint8_t)((v >> 1) + (v >> 2)); /* 3/4 */ + case 3: return (uint8_t)((v >> 1) + (v >> 3)); /* 5/8 */ + case 4: return (uint8_t)(v >> 1); /* 1/2 */ + case 5: return (uint8_t)((v >> 2) + (v >> 3)); /* 3/8 */ + case 6: return (uint8_t)(v >> 2); /* 1/4 */ + case 7: return (uint8_t)(v >> 3); /* 1/8 */ + default: return 0; /* 8 и выше */ + } +} + void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked { uint8_t chunk, i, start; uint8_t *buf = pop_pal_buf; if (!font_ready) return; - if (dim_step > 4) dim_step = 4; + if (dim_step > POP_PAL_DIM_BLACK) dim_step = POP_PAL_DIM_BLACK; for (chunk = 0; chunk < 4; chunk++) { start = (uint8_t)(chunk << 6); snapshot_chunk(chunk, buf, 0); @@ -167,19 +189,9 @@ void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked uint8_t idx = (uint8_t)(start + i); uint16_t p = (uint16_t)i * 4u; if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue; - if (dim_step == 1) { - buf[p] = (uint8_t)((buf[p] >> 1) + (buf[p] >> 2)); - buf[p + 1] = (uint8_t)((buf[p + 1] >> 1) + - (buf[p + 1] >> 2)); - buf[p + 2] = (uint8_t)((buf[p + 2] >> 1) + - (buf[p + 2] >> 2)); - } else if (dim_step == 2) { - buf[p] >>= 1; buf[p + 1] >>= 1; buf[p + 2] >>= 1; - } else if (dim_step == 3) { - buf[p] >>= 2; buf[p + 1] >>= 2; buf[p + 2] >>= 2; - } else { - buf[p] = buf[p + 1] = buf[p + 2] = 0; - } + buf[p] = dim_value(buf[p], dim_step); + buf[p + 1] = dim_value(buf[p + 1], dim_step); + buf[p + 2] = dim_value(buf[p + 2], dim_step); } } gfx_pal_load(0, start, 64, buf); @@ -214,7 +226,7 @@ void pop_ui_fade_out(uint8_t steps) __banked if (!steps) return; pop_ui_palette_snapshot(); for (i = 1; i <= steps; i++) { - uint8_t step = (uint8_t)((uint16_t)i * 4u / steps); + uint8_t step = (uint8_t)((uint16_t)i * POP_PAL_DIM_BLACK / steps); pop_ui_palette_dim(step, 0); gfx_wait_vsync(); } @@ -225,7 +237,7 @@ void pop_ui_fade_in(uint8_t steps) __banked uint8_t i; if (!steps) { pop_ui_palette_restore(); return; } for (i = steps; i > 0; i--) { - uint8_t step = (uint8_t)((uint16_t)(i - 1) * 4u / steps); + uint8_t step = (uint8_t)((uint16_t)(i - 1) * POP_PAL_DIM_BLACK / steps); pop_ui_palette_dim(step, 0); gfx_wait_vsync(); } diff --git a/applications/PoP/roomtest/pop_ui.h b/applications/PoP/roomtest/pop_ui.h index 11cad31..ab99e8f 100644 --- a/applications/PoP/roomtest/pop_ui.h +++ b/applications/PoP/roomtest/pop_ui.h @@ -34,9 +34,14 @@ void pop_text_right_mapped(pop_text_font_t font, int right, int baseline, const char *s); /* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте - * FONT.ATL. dim_step: 0 — исходная, 1 — ~75%, 2 — 50%, 3 — 25%, 4 — чёрная. + * FONT.ATL. dim_step: 0 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними + * восемь равных ступеней (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, 1/8). Восемь, а не + * четыре, потому что fade оригинала длится 2,13 с: на четырёх ступенях это + * четыре заметных скачка яркости. * keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */ void pop_ui_palette_snapshot(void) __banked; +#define POP_PAL_DIM_BLACK 8 + void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked; void pop_ui_palette_restore(void) __banked;