Музыка: озвучена вся заставка, тайминги сведены со шкалой оригинала

Добавлены треки 54 (тема титров), 50/53/52 (три реплики сцены с
принцессой и Джафаром) — заставка звучит целиком, как в оригинале.

pop_music: два слота EMM. Реплики идут встык, паузы под загрузку нет,
поэтому следующая читается в НЕ играющий слот, а play подменяет
резидентную таблицу страниц. Своей копии таблицы слот не держит — её
хранит блок EMM. Плюс постраничная загрузка (load_begin/load_step):
страница стоит 33 мс, четверть логического кадра сцены, и подкачка
посреди анимации не видна.

Тайминги: все длительности сцен — в тиках оригинала (60 Гц), перевод в
кадры луча делает POP_T60. Пока сцены были немыми, разбег в 20 % был
незаметен; с музыкой реплика кончалась раньше картинки. Переведены
титры, история, хвост после PV и пейсинг самой PV-сцены.

Паузы, которые оригинал отмеряет концом сэмпла, теперь равны реальной
длине наших записей: конец m50 на тике 846, вход Джафара 1046, уход
2073, конец сцены 2500 (было 1959). Fade перед PV сдвинут так, чтобы
полная темнота наступала в тот же момент трека, что у SDLPoP: их fade
длиннее нашего на 123 тика, наш полосовой переход длиннее на 16.

Финальная сцена: убраны песочные часы — end_sequence_anim не трогает
hourglass_state, а reset_cutscene его обнулил. Уровень пройден, время
больше не идёт.

Проверено в MAME: цепочка 54 -> 55 -> 50 -> 53 -> 52 отыгрывается
целиком, интро доходит до демо-режима. Host-тесты: 15 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 20:17:38 +03:00
parent 16d3262340
commit d86adc54c4
13 changed files with 477 additions and 140 deletions
+174 -74
View File
@@ -17,6 +17,7 @@
#include "pop_music.h"
#include "pop_cutscene.h"
#include "pop_intro.h"
#include "pop_pace.h" /* pop_wait_edge */
#include "pop_pal.h"
#include "pop_timer.h"
#include "pop_ui.h"
@@ -25,33 +26,42 @@
#define INTRO_PARTS 5
#define INTRO_FADE 4
#define PV_ANIM_TICKS 1959
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422). Музыка сюжета
* (50/52/53) в наборе пустая — её у нас нет; эти два ЕСТЬ и в оригинале
* звучат именно здесь: створка ворот закрывается, затем открывается дверь
* покоев, из которой входит Джафар. */
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот
* закрывается, затем открывается дверь покоев, из которой входит Джафар. */
#define PV_SND_GATE_CLOSING 4
#define PV_SND_DOOR_OPENING 51
/* Шкала ниже повторяет порядок pv_scene() SDLPoP. Обычный кадр cutscene
* длится 8 тиков, после начала заклинания — 7. Длинные речевые паузы здесь
* заданы явно: в DOS они заканчиваются по окончании CBL-сэмпла. */
/* Шкала ниже повторяет порядок pv_scene() SDLPoP и живёт в ЕГО тиках
* (60 Гц): обычный кадр cutscene длится 8 тиков, до первой смены темпа 6,
* после начала заклинания 7. В кадры луча переводит POP_T60.
*
* ДЛИННЫЕ ПАУЗЫ ЗДЕСЬ — ЭТО РЕПЛИКИ. Оригинал их не отмеряет, а ждёт
* окончания сэмпла (`do { frame(1) } while (check_sound_playing())`), и
* пока музыки у нас не было, паузы стояли укороченными «на глаз». Теперь
* они равны реальной длине наших записей (pop_music_tbl.h, тики = блоки *
* 128 / 10937,5 * 60): m50 — 831, m53 — 985 тиков. */
enum {
PV_WAIT_END = 360,
PV_GATE_END = 408,
PV_DOOR_END = 432,
PV_MUS_2_START = 12, /* frame(2), затем «принцесса ждёт» (m50) */
PV_MUS_3_LOAD = 60, /* подкачка следующей реплики, m50 звучит */
PV_M50_END = 846, /* 12 + 834: конец m50, кадр становится 8 */
PV_WAIT_END = 886, /* +frame(5) -> створка ворот */
PV_GATE_END = 934,
PV_DOOR_END = 958,
PV_TURN_START = PV_DOOR_END,
PV_WALK1_START = 472,
PV_DIALOG1_START = 520,
PV_WALK2_START = 696,
PV_DIALOG2_START = 936,
PV_RAISE_START = 1216,
PV_STEPBACK_START = 1223,
PV_MAGIC_START = 1342,
PV_EXIT_START = 1490,
PV_GLASS_DONE = 1700,
PV_SLUMP_START = 1763
PV_WALK1_START = 998,
PV_DIALOG1_START = 1046, /* Джафар входит — реплика m53 */
PV_MUS_4_LOAD = 1100, /* подкачка m52, Джафар стоит на месте */
PV_WALK2_START = 1222,
PV_DIALOG2_START = 1462,
PV_RAISE_START = 1742,
PV_STEPBACK_START = 1749,
PV_MAGIC_START = 1868,
PV_EXIT_START = 2031, /* 1046 + 985: конец m53 */
PV_MUS_4_START = 2073, /* +frame(6) -> «Джафар уходит» (m52) */
PV_GLASS_DONE = 2241,
PV_SLUMP_START = 2304,
PV_ANIM_TICKS = 2500
};
enum {
@@ -103,10 +113,18 @@ static const char * const pv_base_part[INTRO_PARTS] = {
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */
/* ПОЧЕМУ НЕ 0x258, КАК В ОРИГИНАЛЕ. Точка отсчёта у обоих — пуск m55;
* совпасть должен МОМЕНТ ПОЛНОЙ ТЕМНОТЫ, а дороги к нему разные:
* 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. */
static const pop_cs_cmd_t intro_before_pv[] = {
/* absence собирается прямо поверх последнего title-кадра полосовым
* transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */
{ POP_CS_WAIT, 0x258 }, /* story 1: In the absence */
{ POP_CS_WAIT, POP_T60(707) }, /* story 1: In the absence */
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
@@ -130,13 +148,20 @@ static const pop_cs_cmd_t intro_pv_fallback[] = {
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t intro_after_pv[] = {
/* Хвост show_title() разрезан надвое: между «свадьбой» и титрами оригинал
* ЖДЁТ окончания последней реплики Джафара (seg000:2051), которая начала
* звучать ещё в PV и продолжается поверх смены картинки. */
static const pop_cs_cmd_t intro_marry[] = {
{ POP_CS_PALETTE, INTRO_PAL_STORY },
{ POP_CS_SHOW, INTRO_STORY_MARRY },
{ POP_CS_FADE_IN, 0 },
{ POP_CS_WAIT, 0x78 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t intro_credits[] = {
{ POP_CS_WAIT, POP_T60(0x78) },
{ POP_CS_SHOW, INTRO_STORY_CREDITS },
{ POP_CS_WAIT, 0x168 },
{ POP_CS_WAIT, POP_T60(0x168) },
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
@@ -592,17 +617,18 @@ static void pv_blit_princess(const atlas_t *princess, const pv_actor_t *actor)
166, (uint8_t)!actor->left);
}
/* Внутренняя шкала сценария пока остаётся в исходных тиках 60 Гц: так
* сохраняются точки начала реплик и последовательностей. Внешне каждый
* такой логический кадр показывается четыре физических кадра Sprinter. */
/* Длина логического кадра в тиках 60 Гц — cutscene_frame_time оригинала.
* Сколько это кадров луча, решает пейсинг в intro_pv_animated. */
static uint8_t pv_seq_period(uint16_t tick)
{
if (tick < PV_WAIT_END) return 6;
if (tick < PV_M50_END) return 6;
if (tick < PV_RAISE_START) return 8;
return 7;
}
static void intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
/* Рисует кадр и меняет страницу; возвращает, сколько кадров луча на это
* ушло (вспышка живёт на физической шкале и стоит одиннадцать). */
static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
const atlas_t *j1, const atlas_t *j2,
const atlas_t *hourglass, const atlas_t *torch,
const pv_actor_t *p, const pv_actor_t *j,
@@ -653,14 +679,9 @@ static void intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
pop_pal_flash_white(0);
gfx_wait_vsync();
}
} else {
uint8_t hold;
/* Номинальные 50/4 = 12,5 FPS. Если сама сборка кадра пересекла
* лишний фронт, фактическая частота естественно станет ниже; точный
* anchor-based pacing оставлен отдельным TODO. */
for (hold = 1; hold < 4; hold++) gfx_wait_vsync();
return 11;
}
return 1;
}
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
@@ -674,6 +695,7 @@ static int intro_pv_animated(void)
uint16_t tick;
uint16_t frame_end;
uint16_t render_tick;
uint16_t beam = 0; /* потрачено кадров луча с начала сцены */
uint8_t anim_step = 0;
uint8_t torch_left = 1;
uint8_t torch_right = 6;
@@ -713,10 +735,10 @@ static int intro_pv_animated(void)
}
pop_pal_fade_in(INTRO_FADE);
/* Все атласы сцены уже в EMM: дальше файлового ввода-вывода нет, и
* насос CBL можно открывать (иначе ESTEX не даёт долить блок — см.
* контракт pop_sfx_start). Полноэкранная accel-копия кадра насосу не
* мешает: leaf режет DI бандами по 16 строк. */
/* Актёрские атласы в EMM; дальше по сцене диск трогает только подкачка
* следующей реплики — по одной странице за кадр (pop_music_load_step).
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
* бандами по 16 строк. */
(void)pop_sfx_start();
result = 0;
princess_actor.x = 120;
@@ -754,6 +776,16 @@ static int intro_pv_animated(void)
* очередного сэмпла, — заявляем эффект ровно на них. */
if (tick == PV_WAIT_END) pop_sfx_play(PV_SND_GATE_CLOSING);
else if (tick == PV_GATE_END) pop_sfx_play(PV_SND_DOOR_OPENING);
/* Три реплики сцены. Каждая следующая читается с диска, пока
* звучит предыдущая (второй слот pop_music), поэтому здесь
* остаётся только пуск — мгновенный. */
else if (tick == PV_MUS_2_START) pop_music_play();
else if (tick == PV_MUS_3_LOAD)
(void)pop_music_load_begin(POP_MUS_STORY_3);
else if (tick == PV_DIALOG1_START) pop_music_play();
else if (tick == PV_MUS_4_LOAD)
(void)pop_music_load_begin(POP_MUS_STORY_4);
else if (tick == PV_MUS_4_START) pop_music_play();
++tick;
} while (tick < frame_end);
@@ -768,13 +800,30 @@ static int intro_pv_animated(void)
++anim_step;
}
render_tick = (uint16_t)(tick - 1);
intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2,
&hourglass, &torch, &princess_actor,
&jaffar_actor, star_phase, torch_left,
torch_right, sand_frame, flash, render_tick);
beam += intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2,
&hourglass, &torch, &princess_actor,
&jaffar_actor, star_phase, torch_left,
torch_right, sand_frame, flash,
render_tick);
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
/* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра:
* 33 мс диска укладываются в паузу логического кадра (133 мс) и
* анимации не стоят. */
if (pop_music_loading()) (void)pop_music_load_step();
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
* 1/60 с, наш экран идёт на 1/50: без пересчёта сцена растянулась
* бы на пятую часть, и реплики разъехались бы с картинкой. */
{
uint16_t target = POP_T60(tick);
while (beam < target) { gfx_wait_vsync(); beam++; }
}
}
pop_sfx_pause(); /* дальше снова только загрузка файлов */
/* CBL НЕ ГЛУШИМ: последняя реплика Джафара звучит поверх смены сцены —
* ровно как в оригинале, где show_title ждёт её окончания уже на
* экране «свадьбы» (seg000:2051). Глушит вызывающий, когда музыка
* действительно доиграла. */
if (result) { pop_music_free(); pop_sfx_pause(); }
if (!result) pop_pal_fade_out(INTRO_FADE);
free_torch:
@@ -872,14 +921,24 @@ free_hourglass_pre:
* видимый title узкими вертикальными полосами слева направо. У оригинала
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не
* удваивая число полных копий 320x256 в ограниченном bank 9. */
/* Собрать «In the absence» в СКРЫТУЮ страницу. Отделено от показа: между
* ними успевает загрузиться трек, а на экране всё это время держится
* последний кадр титров — ровно как у оригинала, который рисует историю в
* offscreen и лишь потом пускает transition_ltr. */
static int intro_story_prepare(void)
{
uint8_t target = (uint8_t)(gfx_get_visible_page() ^ 1);
return intro_draw_page(INTRO_STORY_ABSENCE, target);
}
/* Полосовой показ подготовленной страницы (transition_ltr оригинала). */
static int intro_story_transition(void)
{
gfx_rect_t stripe;
uint8_t visible = gfx_get_visible_page();
uint8_t target = (uint8_t)(visible ^ 1);
int x;
if (intro_draw_page(INTRO_STORY_ABSENCE, target) != 0) return -1;
gfx_set_draw_page(visible); /* source gfx_copy_page = target */
stripe.y = 0;
stripe.h = GFX_HEIGHT;
@@ -1119,8 +1178,13 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
pv_draw_stars(cut_star_phase);
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168);
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164);
/* hourglass_state == 0 — часов в комнате нет вовсе (reset_cutscene
* SDLPoP ставит state=0 и sandflow=-1). Так идёт финальная встреча
* с принцессой: время уже не считается, это победа. */
if (cut_hg_state) {
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168);
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164);
}
cut_blit_princess(&Guard, &pr_fr);
if (kid_visible) cut_blit_kid(&Kid, &kid_fr);
/* ПОСЛЕ персонажей: передняя колонна — передний слой комнаты, она
@@ -1144,16 +1208,17 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
}
/* Общая обвязка всех трёх сцен: чистый фон комнаты принцессы в обе
* страницы, PV-палитра, актёрские атласы. Возврат -1 — ресурсов нет
* страницы, PV-палитра, актёрские атласы. hg_state — кадр песочных часов
* (0 = часов нет). Возврат -1 — ресурсов нет
* (неполный HDD), и caller откатывается на статический сценарий. */
static int cut_begin(void)
static int cut_begin(uint8_t hg_state)
{
uint8_t i;
intro_skip_begin(&cut_skip);
cut_torch_l = 1; cut_torch_r = 6; cut_sand = 0;
for (i = 0; i < 6; i++) cut_star_phase[i] = 1;
cut_hg_state = pre_hourglass_state();
cut_hg_state = hg_state;
intro_prepare_palette(INTRO_PAL_PV);
intro_commit_palette();
if (intro_pv_draw_base() != 0) return -1;
@@ -1173,7 +1238,7 @@ static int cut_scene_8(void)
{
uint8_t aborted;
if (cut_begin() != 0) return -1;
if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* init_mouse_cu8 (seg001:0212) = init_mouse_go + x=144 + seq_106. */
cut_kid_init(CHARID_24_MOUSE, 144, 167, 106);
/* princess_crouching (seg001:024D): сидит и гладит мышь. */
@@ -1198,7 +1263,7 @@ static int cut_scene_9(void)
{
uint8_t aborted;
if (cut_begin() != 0) return -1;
if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* princess_stand (seg001:026A) — поза PV1, лицом вправо. */
cut_shad_init(144, 169, 94);
Guard.direction = 0;
@@ -1224,7 +1289,10 @@ static int cut_ending(void)
{
uint8_t aborted;
if (cut_begin() != 0) return -1;
/* end_sequence_anim (seg001:041C) НЕ трогает hourglass_state, а
* reset_cutscene перед сценой обнулил его: песочных часов в финале нет.
* Это и логично — уровень пройден, время больше не идёт. */
if (cut_begin(0) != 0) return -1;
/* 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);
@@ -1296,31 +1364,42 @@ uint8_t pop_intro_show(void) __banked
{
int pv_result;
/* Title уже заглушил CBL; оставляем это явным lifecycle-правилом, если
* intro позднее будет вызвано отдельно (например, из debug-маршрута). */
pop_sfx_pause();
if (intro_story_transition() != 0) {
/* МУЗЫКА ПРИХОДИТ СЮДА ЗВУЧАЩЕЙ. На титрах играет тема (m54), и
* обрывать её нельзя: у оригинала заставка и история — одна звуковая
* дорожка, где следующая реплика начинается там, где кончилась
* предыдущая. Поэтому здесь ни pop_sfx_pause, ни pop_sfx_start:
* CBL уже открыт титрами. */
if (intro_story_prepare() != 0) {
/* Ошибка файла не должна оставить старый title без выхода: менее
* эффектный, но корректный fallback остаётся обычным fade-in. */
pop_music_free();
pop_sfx_pause();
intro_prepare_palette(INTRO_PAL_STORY);
if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0)
pop_pal_fade_in(INTRO_FADE);
}
/* Первый экран истории — единственное место, где музыка уже звучит.
* Грузим ПОСЛЕ того, как картинка собрана: чтение кусков идёт через
* ESTEX, и при открытом CBL насос не успел бы долить блок (скрежет,
* см. pop_sfx_start). Порядок обязателен: load -> start -> play. */
if (pop_music_load(POP_MUS_STORY_1) == 0) {
(void)pop_sfx_start();
} else {
/* Картинка собрана и ждёт в скрытой странице; читаем следующую
* реплику во ВТОРОЙ слот — тема титров при этом продолжает
* звучать из первого. */
(void)pop_music_load(POP_MUS_STORY_1);
/* Тему дослушиваем до конца, как seg000:2027, и только потом
* пускаем историю: пуск и полосовой переход идут встык. */
{
intro_skip_t head;
intro_skip_begin(&head);
while (pop_music_busy() && !intro_skip_requested(&head))
pop_wait_edge();
}
(void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */
pop_music_play();
(void)intro_story_transition();
}
if (intro_run(intro_before_pv)) {
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
* загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше,
* чем играет буфер, и железо начинает крутить свои 256 байт по
* кругу — на слух ровно тот скрежет, что ловится нажатием клавиши
* на первом экране истории. В обычной ветке ниже pause уже есть,
* а здесь его не было. */
* на первом экране истории. */
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
@@ -1329,26 +1408,47 @@ uint8_t pop_intro_show(void) __banked
/* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на
* переходе к PV: fade уводит картинку, а музыка продолжается, и лишь
* когда она смолкла, начинается сцена с принцессой (у SDLPoP это
* общий приём — `while (check_sound_playing())`, seg000:2027).
* Мы освобождали страницы сразу после fade, и трек обрывался на
* полуслове — на слух это и был скрежет в конце затемнения. */
* общий приём — `while (check_sound_playing())`, seg000:2027). */
{
intro_skip_t tail;
intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail)) pop_wait_edge();
}
pop_music_free();
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
* дальше внутри сцены остаётся только пуск, а следующие две реплики
* подкачиваются по странице за кадр. */
pop_sfx_pause();
(void)pop_music_load(POP_MUS_STORY_2);
pv_result = intro_pv_animated();
if (pv_result > 0) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
if (pv_result < 0 && intro_run(intro_pv_fallback)) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
if (intro_run(intro_after_pv)) {
/* «Свадьба» показывается ПОВЕРХ звучащей реплики Джафара: она началась
* в PV и переживает и fade-out сцены, и fade-in этого экрана. */
if (intro_run(intro_marry)) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
{
intro_skip_t marry;
intro_skip_begin(&marry);
while (pop_music_busy() && !intro_skip_requested(&marry))
pop_wait_edge();
}
pop_music_free();
pop_sfx_pause();
if (intro_run(intro_credits)) {
intro_restore_game_palette();
return 1;
}