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

Добавлены треки 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
+57 -3
View File
@@ -707,9 +707,9 @@ DOS-версии** — `applications/PoP/PoP1_DOS_music` (flac/mp3/ogg/ogg_MT-32
### Что сделано ### Что сделано
`toolchain/pop_pack_music.py` -> куски `MUS\m<id>_<nn>.bin` по 16 КБ `toolchain/pop_pack_music.py` -> один файл `MUS\m<id>.bin` на трек
(bank_load_file читает файл только целиком и только в одну страницу) + (читается порциями по 16 КБ из одного открытого fd) + каталог
каталог `pop_music_tbl.h`. Длина трека хранится **порциями по 128 байт**, `pop_music_tbl.h`. Длина трека хранится **порциями по 128 байт**,
а не байтами: 169 КБ в uint16 не влезает, 1350 блоков — легко. а не байтами: 169 КБ в uint16 не влезает, 1350 блоков — легко.
`pop_music.c` (банк 9) грузит трек в EMM и ставит курсор; насос `pop_music.c` (банк 9) грузит трек в EMM и ставит курсор; насос
@@ -791,3 +791,57 @@ _io_page_w3 = saved;
Бонус: в W3 нет стаба восстановления окна, который в W0 занимал начало Бонус: в W3 нет стаба восстановления окна, который в W0 занимал начало
страницы, — звуковые страницы можно использовать целиком. страницы, — звуковые страницы можно использовать целиком.
## 6. ВСЯ ЗАСТАВКА ОЗВУЧЕНА (2026-08-25)
К `story_1_absence` добавлены остальные четыре трека заставки:
**54** intro_theme (титры), **50** story_2_princess, **53**
story_3_Jaffar_enters, **52** story_4_Jaffar_leaves (сцена с принцессой).
`MUS_IDS` в Makefile — 50 52 53 54 55, всего 1056 КБ на образе.
### Два слота вместо одного
Реплики оригинала идут ВСТЫК: следующая начинается там, где кончилась
предыдущая, паузы под загрузку нет. Поэтому `pop_music` держит два слота
EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_music_play`
подменяет резидентную таблицу страниц и отпускает прошлый слот. Своей
копии таблицы слот не хранит — её и так держит блок EMM, `mem_get_page`
отдаёт номер по индексу (иначе −40 байт W2 у игры, а там их нет).
Плюс **постраничная загрузка**: `pop_music_load_begin` / `_load_step`
читают по одной странице за вызов. Страница стоит 33 мс — четверть
логического кадра заставки (133 мс), поэтому подкачка следующей реплики
прямо посреди анимации не видна. Кто может позволить себе паузу (титры,
чёрный экран между сценами) — зовёт прежний `pop_music_load`.
### Тайминги приведены к шкале оригинала
Все длительности сцен взяты из SDLPoP в его тиках (60 Гц), а ждём мы
кадрами луча (~50 Гц). Пока сцены были немыми, разбег в 20 % не был
виден; с музыкой он слышен сразу — реплика кончается раньше картинки.
Введён `POP_T60(t)` (pop_cutscene.h), и на него переведены титры,
`intro_before_pv`, хвост после PV и пейсинг самой PV-сцены (счётчик
потраченных кадров луча против `POP_T60(tick)`, вместо прежних жёстких
четырёх кадров на логический).
**Паузы-реплики.** Там, где оригинал ждёт конца сэмпла, у нас теперь
стоит реальная длина нашей записи: m50 — 831 тик, m53 — 985. Отсюда
новая шкала PV: конец m50 на 846, вход Джафара (m53) на 1046, уход
(m52) на 2073, конец сцены 2500 тиков (было 1959).
**Fade перед PV** (вопрос пользователя: наш fade короче, 4 ступени против
64). Совпасть должен момент полной темноты, считая от пуска m55:
у SDLPoP это 80 (transition) + 600 (wait) + 128 (fade_out_2: 0x40 шагов
по 2 тика); у нас переход занимает 80 кадров луча = 96 тиков, а fade —
5 тиков. Отсюда `WAIT = 80 + 600 + 128 96 5 = 707` тиков. Дальше и
там и там экран уже чёрный, а трек доигрывает: этой паузой заставка и
стыкуется с PV.
**Музыка переживает смену сцен.** m54 звучит с титров и до первого
экрана истории (`pop_intro_show` больше не глушит CBL на входе), m52
начинается в PV и доигрывает уже на экране «свадьбы» — как seg000:2051.
Проверено в MAME: цепочка 54 → 55 → 50 → 53 → 52 отыгрывается целиком,
курсор `pop_mus_id` меняется ровно на своих кадрах, интро доходит до
демо-режима. Слуховая проверка (нет ли хрипа от диска при играющей
музыке) — за пользователем.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -5
View File
@@ -180,12 +180,13 @@ SND_PARTS := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(SND_DIR)/s$(n).bin)
SND_ARC := $(SND_DIR)/snd.arc SND_ARC := $(SND_DIR)/snd.arc
SND_DATA := $(SND_ARC) SND_DATA := $(SND_ARC)
# МУЗЫКА: записи DOS-версии, приведённые к формату CBL и порезанные на # МУЗЫКА: записи DOS-версии, приведённые к формату CBL
# страницы EMM (toolchain/pop_pack_music.py). Пока в игре звучит один # (toolchain/pop_pack_music.py). Сейчас озвучена вся заставка:
# трек — story_1_absence на первом экране intro; список расширяется # 54 — тема титров, 55 — «In the absence», 50/53/52 — три реплики сцены
# добавлением номеров в MUS_IDS, имена кусков задаёт упаковщик. # с принцессой и Джафаром. Игровые джинглы (смерть, зелья) пойдут
# отдельной задачей: там музыка спорит с эффектами за единственный CBL.
MUS_DIR := $(CURDIR)/../poc/res/music MUS_DIR := $(CURDIR)/../poc/res/music
MUS_IDS := 55 MUS_IDS := 50 52 53 54 55
MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin) MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin)
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы: # UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
+22
View File
@@ -1206,6 +1206,28 @@ N навык M» — отдельная надобность, если пона
Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке. Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке.
### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1, как звук и музыка
**Что сделано.** Формат PBA1 и читатель готовы и работают на звуке:
`toolchain/pop_pack_arc.py` собирает архив (заголовок 512 Б, записи по 4
байта — offset в секторах, size в байтах), `pop_arc.c` открывает его ОДИН
раз на группу и читает элементы прямо в EMM-страницы. Набор эффектов
уехал в `SND\snd.arc` целиком.
**Что осталось.** Перевести остальные группы: `BG\` (25 файлов), `KID\`
(60), `GUARD/SKEL/VIZIER/SHADOW` (~50), `TITLE\` (21), `PV\` (75),
`LEVELS\` (15). Каждая группа — свой архив, потому что таблицу держит
вызывающий на стеке (4 байта на запись, у PV это 300 Б).
**Зачем.** Замер 2026-08-25: `open` 51,4 мс против 32,6 мс на чтение 16 КБ.
Стартовая пачка ресурсов — около 142 файлов, то есть **7,3 с чистого
`open`**, который архивы убирают почти целиком. Второй мотив назван
пользователем прямо: «не нравится огромное кол-во файлов» в корне диска.
**Чем подтверждать.** Время от запуска до первого кадра уровня (сейчас
меряется по MAME), число файлов на образе, и что каждая сцена по-прежнему
собирается при неполном диске (пути ошибок в `atlas_load`).
### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2 ### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2
`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча `pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча
+7
View File
@@ -10,6 +10,13 @@
#include <stdint.h> #include <stdint.h>
/* ТИК ОРИГИНАЛА -> НАШ КАДР. Все длительности сцен взяты из SDLPoP в его
* тиках (BASE_FPS = 60), а ждём мы кадрами луча (50 Гц). Без пересчёта
* сцена идёт на 20 % дольше — незаметно, пока она немая, и сразу слышно,
* когда под неё играет музыка: реплика кончается раньше картинки.
* Аргумент — константа сценария, до 2500, так что 32-бит не возникает. */
#define POP_T60(t) ((uint16_t)((uint16_t)(t) * 5u / 6u))
typedef enum { typedef enum {
POP_CS_SHOW = 1, /* arg: номер готового кадра */ POP_CS_SHOW = 1, /* arg: номер готового кадра */
POP_CS_WAIT, /* arg: число логических кадров */ POP_CS_WAIT, /* arg: число логических кадров */
+174 -74
View File
@@ -17,6 +17,7 @@
#include "pop_music.h" #include "pop_music.h"
#include "pop_cutscene.h" #include "pop_cutscene.h"
#include "pop_intro.h" #include "pop_intro.h"
#include "pop_pace.h" /* pop_wait_edge */
#include "pop_pal.h" #include "pop_pal.h"
#include "pop_timer.h" #include "pop_timer.h"
#include "pop_ui.h" #include "pop_ui.h"
@@ -25,33 +26,42 @@
#define INTRO_PARTS 5 #define INTRO_PARTS 5
#define INTRO_FADE 4 #define INTRO_FADE 4
#define PV_ANIM_TICKS 1959
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422). Музыка сюжета /* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот
* (50/52/53) в наборе пустая — её у нас нет; эти два ЕСТЬ и в оригинале * закрывается, затем открывается дверь покоев, из которой входит Джафар. */
* звучат именно здесь: створка ворот закрывается, затем открывается дверь
* покоев, из которой входит Джафар. */
#define PV_SND_GATE_CLOSING 4 #define PV_SND_GATE_CLOSING 4
#define PV_SND_DOOR_OPENING 51 #define PV_SND_DOOR_OPENING 51
/* Шкала ниже повторяет порядок pv_scene() SDLPoP. Обычный кадр cutscene /* Шкала ниже повторяет порядок pv_scene() SDLPoP и живёт в ЕГО тиках
* длится 8 тиков, после начала заклинания — 7. Длинные речевые паузы здесь * (60 Гц): обычный кадр cutscene длится 8 тиков, до первой смены темпа 6,
* заданы явно: в DOS они заканчиваются по окончании CBL-сэмпла. */ * после начала заклинания 7. В кадры луча переводит POP_T60.
*
* ДЛИННЫЕ ПАУЗЫ ЗДЕСЬ — ЭТО РЕПЛИКИ. Оригинал их не отмеряет, а ждёт
* окончания сэмпла (`do { frame(1) } while (check_sound_playing())`), и
* пока музыки у нас не было, паузы стояли укороченными «на глаз». Теперь
* они равны реальной длине наших записей (pop_music_tbl.h, тики = блоки *
* 128 / 10937,5 * 60): m50 — 831, m53 — 985 тиков. */
enum { enum {
PV_WAIT_END = 360, PV_MUS_2_START = 12, /* frame(2), затем «принцесса ждёт» (m50) */
PV_GATE_END = 408, PV_MUS_3_LOAD = 60, /* подкачка следующей реплики, m50 звучит */
PV_DOOR_END = 432, 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_TURN_START = PV_DOOR_END,
PV_WALK1_START = 472, PV_WALK1_START = 998,
PV_DIALOG1_START = 520, PV_DIALOG1_START = 1046, /* Джафар входит — реплика m53 */
PV_WALK2_START = 696, PV_MUS_4_LOAD = 1100, /* подкачка m52, Джафар стоит на месте */
PV_DIALOG2_START = 936, PV_WALK2_START = 1222,
PV_RAISE_START = 1216, PV_DIALOG2_START = 1462,
PV_STEPBACK_START = 1223, PV_RAISE_START = 1742,
PV_MAGIC_START = 1342, PV_STEPBACK_START = 1749,
PV_EXIT_START = 1490, PV_MAGIC_START = 1868,
PV_GLASS_DONE = 1700, PV_EXIT_START = 2031, /* 1046 + 985: конец m53 */
PV_SLUMP_START = 1763 PV_MUS_4_START = 2073, /* +frame(6) -> «Джафар уходит» (m52) */
PV_GLASS_DONE = 2241,
PV_SLUMP_START = 2304,
PV_ANIM_TICKS = 2500
}; };
enum { enum {
@@ -103,10 +113,18 @@ static const char * const pv_base_part[INTRO_PARTS] = {
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным /* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */ * покадровым 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[] = { static const pop_cs_cmd_t intro_before_pv[] = {
/* absence собирается прямо поверх последнего title-кадра полосовым /* absence собирается прямо поверх последнего title-кадра полосовым
* transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */ * 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_FADE_OUT, 0 },
{ POP_CS_END, 0 } { POP_CS_END, 0 }
}; };
@@ -130,13 +148,20 @@ static const pop_cs_cmd_t intro_pv_fallback[] = {
{ POP_CS_END, 0 } { 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_PALETTE, INTRO_PAL_STORY },
{ POP_CS_SHOW, INTRO_STORY_MARRY }, { POP_CS_SHOW, INTRO_STORY_MARRY },
{ POP_CS_FADE_IN, 0 }, { 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_SHOW, INTRO_STORY_CREDITS },
{ POP_CS_WAIT, 0x168 }, { POP_CS_WAIT, POP_T60(0x168) },
{ POP_CS_FADE_OUT, 0 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 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); 166, (uint8_t)!actor->left);
} }
/* Внутренняя шкала сценария пока остаётся в исходных тиках 60 Гц: так /* Длина логического кадра в тиках 60 Гц — cutscene_frame_time оригинала.
* сохраняются точки начала реплик и последовательностей. Внешне каждый * Сколько это кадров луча, решает пейсинг в intro_pv_animated. */
* такой логический кадр показывается четыре физических кадра Sprinter. */
static uint8_t pv_seq_period(uint16_t tick) 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; if (tick < PV_RAISE_START) return 8;
return 7; 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 *j1, const atlas_t *j2,
const atlas_t *hourglass, const atlas_t *torch, const atlas_t *hourglass, const atlas_t *torch,
const pv_actor_t *p, const pv_actor_t *j, 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); pop_pal_flash_white(0);
gfx_wait_vsync(); gfx_wait_vsync();
} }
} else { return 11;
uint8_t hold;
/* Номинальные 50/4 = 12,5 FPS. Если сама сборка кадра пересекла
* лишний фронт, фактическая частота естественно станет ниже; точный
* anchor-based pacing оставлен отдельным TODO. */
for (hold = 1; hold < 4; hold++) gfx_wait_vsync();
} }
return 1;
} }
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот, /* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
@@ -674,6 +695,7 @@ static int intro_pv_animated(void)
uint16_t tick; uint16_t tick;
uint16_t frame_end; uint16_t frame_end;
uint16_t render_tick; uint16_t render_tick;
uint16_t beam = 0; /* потрачено кадров луча с начала сцены */
uint8_t anim_step = 0; uint8_t anim_step = 0;
uint8_t torch_left = 1; uint8_t torch_left = 1;
uint8_t torch_right = 6; uint8_t torch_right = 6;
@@ -713,10 +735,10 @@ static int intro_pv_animated(void)
} }
pop_pal_fade_in(INTRO_FADE); pop_pal_fade_in(INTRO_FADE);
/* Все атласы сцены уже в EMM: дальше файлового ввода-вывода нет, и /* Актёрские атласы в EMM; дальше по сцене диск трогает только подкачка
* насос CBL можно открывать (иначе ESTEX не даёт долить блок — см. * следующей реплики — по одной странице за кадр (pop_music_load_step).
* контракт pop_sfx_start). Полноэкранная accel-копия кадра насосу не * Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
* мешает: leaf режет DI бандами по 16 строк. */ * бандами по 16 строк. */
(void)pop_sfx_start(); (void)pop_sfx_start();
result = 0; result = 0;
princess_actor.x = 120; 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); 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); 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; ++tick;
} while (tick < frame_end); } while (tick < frame_end);
@@ -768,13 +800,30 @@ static int intro_pv_animated(void)
++anim_step; ++anim_step;
} }
render_tick = (uint16_t)(tick - 1); render_tick = (uint16_t)(tick - 1);
intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2, beam += intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2,
&hourglass, &torch, &princess_actor, &hourglass, &torch, &princess_actor,
&jaffar_actor, star_phase, torch_left, &jaffar_actor, star_phase, torch_left,
torch_right, sand_frame, flash, render_tick); torch_right, sand_frame, flash,
render_tick);
pop_sfx_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); if (!result) pop_pal_fade_out(INTRO_FADE);
free_torch: free_torch:
@@ -872,14 +921,24 @@ free_hourglass_pre:
* видимый title узкими вертикальными полосами слева направо. У оригинала * видимый title узкими вертикальными полосами слева направо. У оригинала
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не * 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не
* удваивая число полных копий 320x256 в ограниченном bank 9. */ * удваивая число полных копий 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) static int intro_story_transition(void)
{ {
gfx_rect_t stripe; gfx_rect_t stripe;
uint8_t visible = gfx_get_visible_page(); uint8_t visible = gfx_get_visible_page();
uint8_t target = (uint8_t)(visible ^ 1);
int x; int x;
if (intro_draw_page(INTRO_STORY_ABSENCE, target) != 0) return -1;
gfx_set_draw_page(visible); /* source gfx_copy_page = target */ gfx_set_draw_page(visible); /* source gfx_copy_page = target */
stripe.y = 0; stripe.y = 0;
stripe.h = GFX_HEIGHT; 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_l, 93, 116);
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116); pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
pv_draw_stars(cut_star_phase); pv_draw_stars(cut_star_phase);
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168); /* hourglass_state == 0 — часов в комнате нет вовсе (reset_cutscene
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164); * 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); cut_blit_princess(&Guard, &pr_fr);
if (kid_visible) cut_blit_kid(&Kid, &kid_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 откатывается на статический сценарий. */ * (неполный HDD), и caller откатывается на статический сценарий. */
static int cut_begin(void) static int cut_begin(uint8_t hg_state)
{ {
uint8_t i; uint8_t i;
intro_skip_begin(&cut_skip); intro_skip_begin(&cut_skip);
cut_torch_l = 1; cut_torch_r = 6; cut_sand = 0; cut_torch_l = 1; cut_torch_r = 6; cut_sand = 0;
for (i = 0; i < 6; i++) cut_star_phase[i] = 1; 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_prepare_palette(INTRO_PAL_PV);
intro_commit_palette(); intro_commit_palette();
if (intro_pv_draw_base() != 0) return -1; if (intro_pv_draw_base() != 0) return -1;
@@ -1173,7 +1238,7 @@ static int cut_scene_8(void)
{ {
uint8_t aborted; 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. */ /* init_mouse_cu8 (seg001:0212) = init_mouse_go + x=144 + seq_106. */
cut_kid_init(CHARID_24_MOUSE, 144, 167, 106); cut_kid_init(CHARID_24_MOUSE, 144, 167, 106);
/* princess_crouching (seg001:024D): сидит и гладит мышь. */ /* princess_crouching (seg001:024D): сидит и гладит мышь. */
@@ -1198,7 +1263,7 @@ static int cut_scene_9(void)
{ {
uint8_t aborted; uint8_t aborted;
if (cut_begin() != 0) return -1; if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* princess_stand (seg001:026A) — поза PV1, лицом вправо. */ /* princess_stand (seg001:026A) — поза PV1, лицом вправо. */
cut_shad_init(144, 169, 94); cut_shad_init(144, 169, 94);
Guard.direction = 0; Guard.direction = 0;
@@ -1224,7 +1289,10 @@ static int cut_ending(void)
{ {
uint8_t aborted; 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). */ /* init_ending_princess (seg001:0251) + init_ending_kid (seg001:0287). */
cut_shad_init(136, 164, 109); cut_shad_init(136, 164, 109);
cut_kid_init(CHARID_0_KID, 198, 164, 1); cut_kid_init(CHARID_0_KID, 198, 164, 1);
@@ -1296,31 +1364,42 @@ uint8_t pop_intro_show(void) __banked
{ {
int pv_result; int pv_result;
/* Title уже заглушил CBL; оставляем это явным lifecycle-правилом, если /* МУЗЫКА ПРИХОДИТ СЮДА ЗВУЧАЩЕЙ. На титрах играет тема (m54), и
* intro позднее будет вызвано отдельно (например, из debug-маршрута). */ * обрывать её нельзя: у оригинала заставка и история — одна звуковая
pop_sfx_pause(); * дорожка, где следующая реплика начинается там, где кончилась
if (intro_story_transition() != 0) { * предыдущая. Поэтому здесь ни pop_sfx_pause, ни pop_sfx_start:
* CBL уже открыт титрами. */
if (intro_story_prepare() != 0) {
/* Ошибка файла не должна оставить старый title без выхода: менее /* Ошибка файла не должна оставить старый title без выхода: менее
* эффектный, но корректный fallback остаётся обычным fade-in. */ * эффектный, но корректный fallback остаётся обычным fade-in. */
pop_music_free();
pop_sfx_pause();
intro_prepare_palette(INTRO_PAL_STORY); intro_prepare_palette(INTRO_PAL_STORY);
if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0) if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0)
pop_pal_fade_in(INTRO_FADE); pop_pal_fade_in(INTRO_FADE);
} } else {
/* Первый экран истории — единственное место, где музыка уже звучит. /* Картинка собрана и ждёт в скрытой странице; читаем следующую
* Грузим ПОСЛЕ того, как картинка собрана: чтение кусков идёт через * реплику во ВТОРОЙ слот — тема титров при этом продолжает
* ESTEX, и при открытом CBL насос не успел бы долить блок (скрежет, * звучать из первого. */
* см. pop_sfx_start). Порядок обязателен: load -> start -> play. */ (void)pop_music_load(POP_MUS_STORY_1);
if (pop_music_load(POP_MUS_STORY_1) == 0) { /* Тему дослушиваем до конца, как seg000:2027, и только потом
(void)pop_sfx_start(); * пускаем историю: пуск и полосовой переход идут встык. */
{
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(); pop_music_play();
(void)intro_story_transition();
} }
if (intro_run(intro_before_pv)) { if (intro_run(intro_before_pv)) {
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт /* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
* загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше, * загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше,
* чем играет буфер, и железо начинает крутить свои 256 байт по * чем играет буфер, и железо начинает крутить свои 256 байт по
* кругу — на слух ровно тот скрежет, что ловится нажатием клавиши * кругу — на слух ровно тот скрежет, что ловится нажатием клавиши
* на первом экране истории. В обычной ветке ниже pause уже есть, * на первом экране истории. */
* а здесь его не было. */
pop_music_free(); pop_music_free();
pop_sfx_pause(); pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
@@ -1329,26 +1408,47 @@ uint8_t pop_intro_show(void) __banked
/* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на /* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на
* переходе к PV: fade уводит картинку, а музыка продолжается, и лишь * переходе к PV: fade уводит картинку, а музыка продолжается, и лишь
* когда она смолкла, начинается сцена с принцессой (у SDLPoP это * когда она смолкла, начинается сцена с принцессой (у SDLPoP это
* общий приём — `while (check_sound_playing())`, seg000:2027). * общий приём — `while (check_sound_playing())`, seg000:2027). */
* Мы освобождали страницы сразу после fade, и трек обрывался на
* полуслове — на слух это и был скрежет в конце затемнения. */
{ {
intro_skip_t tail; intro_skip_t tail;
intro_skip_begin(&tail); intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail)) pop_wait_edge(); while (pop_music_busy() && !intro_skip_requested(&tail)) pop_wait_edge();
} }
pop_music_free(); /* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
* дальше внутри сцены остаётся только пуск, а следующие две реплики
* подкачиваются по странице за кадр. */
pop_sfx_pause(); pop_sfx_pause();
(void)pop_music_load(POP_MUS_STORY_2);
pv_result = intro_pv_animated(); pv_result = intro_pv_animated();
if (pv_result > 0) { if (pv_result > 0) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
return 1; return 1;
} }
if (pv_result < 0 && intro_run(intro_pv_fallback)) { if (pv_result < 0 && intro_run(intro_pv_fallback)) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette(); intro_restore_game_palette();
return 1; 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(); intro_restore_game_palette();
return 1; return 1;
} }
+122 -40
View File
@@ -18,10 +18,18 @@
#include "pop_sfx.h" #include "pop_sfx.h"
uint8_t pop_mus_page[POP_MUS_PAGES]; uint8_t pop_mus_page[POP_MUS_PAGES];
static uint8_t mus_blk; /* блок EMM под трек */
static uint8_t mus_pages; /* сколько страниц реально занято */ /* Два слота: играющий и подгружаемый. Своей таблицы страниц слот НЕ
static uint16_t mus_blocks; /* длина трека в порциях по 128 Б */ * держит — её и так хранит блок EMM, и mem_get_page отдаёт номер по
static uint8_t mus_id; /* что загружено — для менеджера звука */ * индексу. Резидентный pop_mus_page[] заполняется на pop_music_play, для
* одного слота: 40 байт W2 у игры лишних нет. */
#define MUS_SLOTS 2
static uint8_t slot_blk[MUS_SLOTS]; /* блок EMM, 0 — слот пуст */
static uint8_t slot_pages[MUS_SLOTS];
static uint16_t slot_blocks[MUS_SLOTS];
static uint8_t slot_id[MUS_SLOTS];
static uint8_t mus_cur; /* слот, что играет/играл */
static uint8_t mus_ready; /* слот+1, готовый к запуску */
/* "MUS\mNN.bin" — ровно 8.3, как требует DSS. */ /* "MUS\mNN.bin" — ровно 8.3, как требует DSS. */
static void mus_name(char *buf, uint8_t id) static void mus_name(char *buf, uint8_t id)
@@ -34,6 +42,25 @@ static void mus_name(char *buf, uint8_t id)
buf[11] = 0; buf[11] = 0;
} }
/* ld_busy отдельно от ld_fd: инициализаторов у статиков в банке нет
* (crt0 зануляет _DATA), а «нет открытого файла» — это не ноль. */
static uint8_t ld_busy;
static int ld_fd;
static uint8_t ld_slot;
static uint8_t ld_next; /* следующая страница к чтению */
static uint8_t ld_pages;
static uint8_t ld_id;
static void slot_release(uint8_t n)
{
if (slot_blk[n]) {
mem_free_block(slot_blk[n]);
slot_blk[n] = 0;
}
slot_pages[n] = 0;
slot_blocks[n] = 0;
}
void pop_music_stop(void) __banked void pop_music_stop(void) __banked
{ {
pop_mus_left = 0; pop_mus_left = 0;
@@ -41,63 +68,118 @@ void pop_music_stop(void) __banked
void pop_music_free(void) __banked void pop_music_free(void) __banked
{ {
if (ld_busy) { close(ld_fd); ld_busy = 0; }
pop_mus_left = 0; pop_mus_left = 0;
if (mus_blk) { slot_release(0);
mem_free_block(mus_blk); slot_release(1);
mus_blk = 0; mus_ready = 0;
}
mus_pages = 0;
mus_blocks = 0;
} }
int8_t pop_music_load(uint8_t id) __banked /* --- загрузка ---------------------------------------------------------
*
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
* Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя-
* ется в анимации. Кто может позволить себе паузу (титры, чёрный экран
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
static void load_abort(void)
{
if (ld_busy) close(ld_fd);
ld_busy = 0;
slot_release(ld_slot);
}
int8_t pop_music_load_begin(uint8_t id) __banked
{ {
char name[12]; char name[12];
uint8_t i, pages; uint8_t pages, n;
int fd;
if (ld_busy) load_abort();
if (id >= POP_MUS_COUNT) return -1; if (id >= POP_MUS_COUNT) return -1;
pages = pop_mus_tbl[id].pages; pages = pop_mus_tbl[id].pages;
if (!pages || pages > POP_MUS_PAGES) return -1; if (!pages || pages > POP_MUS_PAGES) return -1;
pop_music_free(); /* Грузим ВСЕГДА в не текущий слот: играющий трек читает свои страницы
* из прерывания, и отобрать их у него нельзя. */
n = (uint8_t)(mus_cur ^ 1);
slot_release(n);
mus_name(name, id); mus_name(name, id);
/* Трек лежит ОДНИМ файлом и читается порциями по странице: `open` стоит /* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
* дороже самого чтения (51,4 мс против 32,6 мс на 16 КБ), и платить его * страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
* на каждую страницу — чистые потери. Тот же путь понадобится финалу * на 16 КБ), и платить его на каждую страницу — чистые потери. */
* `won`, который в память целиком не влезает. */ ld_fd = open(name, O_RDONLY);
fd = open(name, O_RDONLY); if (ld_fd < 0) return -1;
if (fd < 0) return -1; slot_blk[n] = mem_alloc_pages(pages);
mus_blk = mem_alloc_pages(pages); if (!slot_blk[n]) { close(ld_fd); return -1; }
if (!mus_blk) { close(fd); return -1; } ld_busy = 1;
for (i = 0; i < pages; i++) { ld_slot = n;
pop_mus_page[i] = mem_get_page(mus_blk, i); ld_next = 0;
/* Последняя страница короче — читаем сколько есть; ошибку ловим по ld_pages = pages;
* отрицательному коду, а не по «не 16384». */ ld_id = id;
if (!pop_mus_page[i] ||
bank_read_page(fd, pop_mus_page[i], 0, 16384) < 0) {
close(fd);
pop_music_free();
return -1;
}
}
close(fd);
mus_pages = pages;
mus_blocks = pop_mus_tbl[id].blocks;
mus_id = id;
return 0; return 0;
} }
int8_t pop_music_load_step(void) __banked
{
uint8_t p;
if (!ld_busy) return 1; /* нечего грузить */
p = mem_get_page(slot_blk[ld_slot], ld_next);
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
* отрицательному коду, а не по «не 16384». */
if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) {
load_abort();
return -1;
}
if (++ld_next < ld_pages) return 0;
close(ld_fd);
ld_busy = 0;
slot_pages[ld_slot] = ld_pages;
slot_blocks[ld_slot] = pop_mus_tbl[ld_id].blocks;
slot_id[ld_slot] = ld_id;
mus_ready = (uint8_t)(ld_slot + 1);
return 1;
}
uint8_t pop_music_loading(void) __banked
{
return ld_busy;
}
int8_t pop_music_load(uint8_t id) __banked
{
int8_t r;
if (pop_music_load_begin(id) != 0) return -1;
do {
r = pop_music_load_step();
} while (r == 0);
return (r < 0) ? -1 : 0;
}
void pop_music_play(void) __banked void pop_music_play(void) __banked
{ {
if (!mus_pages) return; uint8_t n, i;
if (!mus_ready) return;
n = (uint8_t)(mus_ready - 1);
/* Насос обязан замолчать ДО подмены таблицы страниц: иначе он успеет
* взять порцию по новому индексу из старого курсора. */
pop_mus_left = 0;
for (i = 0; i < slot_pages[n]; i++)
pop_mus_page[i] = mem_get_page(slot_blk[n], i);
if (n != mus_cur) slot_release(mus_cur);
mus_cur = n;
mus_ready = 0;
/* Порядок важен: сначала позиция, потом длина. Насос стартует ровно /* Порядок важен: сначала позиция, потом длина. Насос стартует ровно
* с той порции, где left стал ненулевым, и промежуточное состояние * с той порции, где left стал ненулевым, и промежуточное состояние
* «играем, но курсор от прошлого трека» не возникает. */ * «играем, но курсор от прошлого трека» не возникает. */
pop_mus_id = mus_id; /* до left: менеджер спросит уже готовое */ pop_mus_id = slot_id[n]; /* до left: менеджер спросит готовое */
pop_mus_pg = 0; pop_mus_pg = 0;
pop_mus_ptr = 0; pop_mus_ptr = 0;
pop_mus_left = mus_blocks; pop_mus_left = slot_blocks[n];
} }
uint8_t pop_music_busy(void) __banked uint8_t pop_music_busy(void) __banked
+31 -9
View File
@@ -14,9 +14,9 @@
* Музыка ПО ХОДУ игры (джинглы смерти, зелья) — отдельный вопрос, её * Музыка ПО ХОДУ игры (джинглы смерти, зелья) — отдельный вопрос, её
* придётся либо гасить эффектами, либо уводить на AY. * придётся либо гасить эффектами, либо уводить на AY.
* *
* ФОРМАТ на диске — пачка кусков m<id>_<nn>.bin по 16 КБ (bank_load_file * ФОРМАТ на диске — один файл MUS\m<id>.bin на трек, читается порциями по
* читает файл только целиком и только в одну страницу). Каталог с * 16 КБ из одного открытого fd (bank_read_page). Каталог с длинами
* длинами генерирует toolchain/pop_pack_music.py в pop_music_tbl.h. * генерирует toolchain/pop_pack_music.py в pop_music_tbl.h.
*/ */
#ifndef POP_MUSIC_H #ifndef POP_MUSIC_H
#define POP_MUSIC_H #define POP_MUSIC_H
@@ -29,7 +29,11 @@
#define POP_MUS_PAGES 20 #define POP_MUS_PAGES 20
/* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */ /* Номера треков — нумерация оригинала (SDLPoP data/music/names.txt). */
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */ #define POP_MUS_INTRO 54 /* тема заставки, звучит на титрах */
#define POP_MUS_STORY_1 55 /* «In the absence of the Sultan...» */
#define POP_MUS_STORY_2 50 /* принцесса ждёт */
#define POP_MUS_STORY_3 53 /* входит Джафар */
#define POP_MUS_STORY_4 52 /* Джафар уходит */
/* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет /* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет
* загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не * загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не
@@ -42,16 +46,34 @@ extern volatile uint16_t pop_mus_left; /* порций до конца, 0
* музыка участвует в таблицах приоритета наравне с эффектами. */ * музыка участвует в таблицах приоритета наравне с эффектами. */
extern uint8_t pop_mus_id; extern uint8_t pop_mus_id;
/* Загрузить трек в EMM (НЕ запускает). Звать там, где допустима пауза на /* ДВА СЛОТА. Заставочные сцены идут цепочкой треков (титры -> история ->
* диск: CBL в это время обязан быть закрыт, иначе насос не успевает долить * PV из трёх реплик), и загрузка каждого — это полсекунды-секунда диска.
* блок и железо повторяет старый — на слух скрежет (pop_sfx.h). * Прятать её в паузу между репликами негде: пауз в оригинале нет, там
* следующая реплика начинается сразу. Поэтому слотов два: пока звучит
* один, следующий читается во второй, и переключение стоит копирования
* двадцати байт таблицы страниц.
*
* Загрузить трек в свободный слот (НЕ запускает; играющий трек не трогает).
* 0 — готово, -1 — нет трека/памяти/файла. */ * 0 — готово, -1 — нет трека/памяти/файла. */
int8_t pop_music_load(uint8_t id) __banked; int8_t pop_music_load(uint8_t id) __banked;
/* Отпустить страницы. Останавливает проигрывание, если оно шло. */ /* ТА ЖЕ ЗАГРУЗКА ПО ОДНОЙ СТРАНИЦЕ ЗА ВЫЗОВ — для тех, кому нельзя
* замереть на секунду: страница стоит 33 мс, то есть четверть кадра
* заставочной сцены, и подкачка следующей реплики не видна.
* begin — открыть файл и занять страницы (0 / -1);
* step — прочитать очередную: 0 — ещё есть, 1 — трек готов к play,
* -1 — ошибка (слот освобождён);
* loading — идёт ли чтение (звать step имеет смысл, пока 1).
* Оборвать начатое чтение можно pop_music_free. */
int8_t pop_music_load_begin(uint8_t id) __banked;
int8_t pop_music_load_step(void) __banked;
uint8_t pop_music_loading(void) __banked;
/* Отпустить ОБА слота. Останавливает проигрывание, если оно шло. */
void pop_music_free(void) __banked; void pop_music_free(void) __banked;
/* Пустить загруженный трек с начала / оборвать. */ /* Пустить последний загруженный трек с начала; слот предыдущего при этом
* освобождается. Без парного load ничего не делает. */
void pop_music_play(void) __banked; void pop_music_play(void) __banked;
void pop_music_stop(void) __banked; void pop_music_stop(void) __banked;
+5 -5
View File
@@ -14,7 +14,7 @@
#define POP_MUS_COUNT 56 #define POP_MUS_COUNT 56
#define POP_MUS_BLOCK 128 #define POP_MUS_BLOCK 128
#define POP_MUS_MAX_PAGES 11 #define POP_MUS_MAX_PAGES 20
typedef struct { typedef struct {
uint8_t pages; /* 0 — трека нет */ uint8_t pages; /* 0 — трека нет */
@@ -72,11 +72,11 @@ static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
{ 0, 0 }, /* 47 */ { 0, 0 }, /* 47 */
{ 0, 0 }, /* 48 */ { 0, 0 }, /* 48 */
{ 0, 0 }, /* 49 */ { 0, 0 }, /* 49 */
{ 0, 0 }, /* 50 */ { 10, 1184 }, /* 50 story_2_princess */
{ 0, 0 }, /* 51 */ { 0, 0 }, /* 51 */
{ 0, 0 }, /* 52 */ { 20, 2443 }, /* 52 story_4_Jaffar_leaves */
{ 0, 0 }, /* 53 */ { 11, 1403 }, /* 53 story_3_Jaffar_enters */
{ 0, 0 }, /* 54 */ { 17, 2064 }, /* 54 intro_theme */
{ 11, 1350 }, /* 55 story_1_absence */ { 11, 1350 }, /* 55 story_1_absence */
}; };
+36 -4
View File
@@ -13,6 +13,8 @@
#include <sprite.h> #include <sprite.h>
#include "pop_bg.h" /* POP_YOFF: title занимает 200px по центру 256px */ #include "pop_bg.h" /* POP_YOFF: title занимает 200px по центру 256px */
#include "pop_sfx.h" #include "pop_sfx.h"
#include "pop_music.h"
#include "pop_cutscene.h" /* POP_T60 */
#include "pop_settings.h" #include "pop_settings.h"
#include "pop_pal.h" #include "pop_pal.h"
#include "pop_title.h" #include "pop_title.h"
@@ -42,12 +44,19 @@ static const char * const title_part[4][TITLE_PARTS] = {
/* Последовательность и интервалы буквально из show_title(): /* Последовательность и интервалы буквально из show_title():
* main --0x82--> presents --0xCD--> main --0x41--> game * main --0x82--> presents --0xCD--> main --0x41--> game
* --0x10E--> main --0xEB--> logo+Mechner. * --0x10E--> main --0xEB--> logo+Mechner.
* SDLPoP BASE_FPS = 60, поэтому один счёт = один Sprinter vsync. */ * Счёт — в тиках SDLPoP (60 Гц), в кадры луча переводит POP_T60: под
* заставку играет тема, и разбегание на 20 % было бы слышно.
*
* Последний интервал у оригинала не фиксирован: показав логотип, он ждёт
* ОКОНЧАНИЯ ТЕМЫ (`while (check_sound_playing())`, seg000:2027) и лишь
* потом уходит в историю. 0xEB здесь — нижняя граница на случай, когда
* музыки нет (нет файла, выключен звук). */
static const uint8_t title_scene[] = { static const uint8_t title_scene[] = {
TITLE_MAIN, TITLE_PRESENTS, TITLE_MAIN, TITLE_GAME, TITLE_MAIN, TITLE_LOGO TITLE_MAIN, TITLE_PRESENTS, TITLE_MAIN, TITLE_GAME, TITLE_MAIN, TITLE_LOGO
}; };
static const uint16_t title_wait_ticks[] = { static const uint16_t title_wait_ticks[] = {
0x82, 0xCD, 0x41, 0x10E, 0xEB, 0xEB POP_T60(0x82), POP_T60(0xCD), POP_T60(0x41),
POP_T60(0x10E), POP_T60(0xEB), POP_T60(0xEB)
}; };
static uint8_t title_shown; static uint8_t title_shown;
@@ -100,8 +109,8 @@ uint8_t pop_title_show(void) __banked
uint8_t cold_start = (uint8_t)(!title_shown && uint8_t cold_start = (uint8_t)(!title_shown &&
!pop_settings.show_build_info); !pop_settings.show_build_info);
/* До музыки title дойдём в FG8; игровые SFX не должны звучать поверх /* Игровые SFX не должны тянуться сюда из прерванной партии: заставка
* немой заставки и не должны обслуживаться во время дисковых чтений. */ * начинается с тишины, а через несколько строк её займёт тема титров. */
pop_sfx_pause(); pop_sfx_pause();
if (!cold_start) pop_pal_fade_out(TITLE_FADE); if (!cold_start) pop_pal_fade_out(TITLE_FADE);
@@ -117,6 +126,14 @@ uint8_t pop_title_show(void) __banked
pop_screen_present((uint8_t)prepared); pop_screen_present((uint8_t)prepared);
title_shown = 1; title_shown = 1;
/* Тема заставки. Порядок как на первом экране истории: сначала диск,
* потом открытие CBL, потом пуск — иначе первый же блок насос отдал бы
* из ещё не дочитанной страницы. */
if (pop_music_load(POP_MUS_INTRO) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
for (step = 0; step < sizeof(title_scene); step++) { for (step = 0; step < sizeof(title_scene); step++) {
if (step && title_draw(title_scene[step], 1) < 0) if (step && title_draw(title_scene[step], 1) < 0)
break; break;
@@ -125,6 +142,21 @@ uint8_t pop_title_show(void) __banked
skipped = 1; skipped = 1;
break; break;
} }
/* Логотип держим до конца темы — так оригинал стыкует заставку с
* историей: музыка не обрывается, а сменяется следующей репликой. */
if (step + 1 == sizeof(title_scene)) {
while (pop_music_busy()) {
if (title_skip_pressed()) { skipped = 1; break; }
gfx_wait_vsync();
}
}
}
/* Уходим В ИГРУ — заставочная музыка не должна тянуться в уровень.
* Дальше по сценарию (intro) трек, наоборот, ОСТАЁТСЯ звучать: там
* следующая реплика подгружается во второй слот, пока играет эта. */
if (skipped) {
pop_music_free();
pop_sfx_pause();
} }
return skipped; return skipped;
} }