Музыка звучала там, где оригинал играет эффекты

Три места, все — наши заявки на треки, поставленные без условий оригинала.

Вступление первого уровня (25) оказалось не «звуком при приседе», а
автоматом (seg005:02EB): пока need_level1_music не ноль, control_crouched
НЕ читает управление — Кид сидит, играет тема, и только когда она смолкла,
он встаёт. Мы играли трек при любом первом приседе, и тема догоняла игрока
посреди уровня: пробежал, спрыгнул, присел — заиграла. Теперь автомат
портирован целиком, а «ещё звучит» спрашивается у курсора насоса
pop_mus_left (если музыка выключена, курсор нулевой и Кид просто встаёт).

Демо-уровень: оригинал молчит музыкой и там, где играет её в игре. Смерть
Кида — прямое условие `current_level != 0 && != 15` (seg006:1366), убитый
страж — отдельная ветка «беги из комнаты» без звука (seg006:1929). Плюс
общий гейт в pop_music_service: заставочная демка озвучена одними
эффектами, и любой новый музыкальный повод (меч, зелье) звучал бы там, где
оригинал их не играет.

Гейт живёт в банке, а не в главном цикле: в резиденте W1 оставалось
27 байт.

Host-тесты: 15 наборов. Проверено в MAME: на старте первого уровня тема 25
отыгрывает целиком, курсор доходит до нуля.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 10:41:07 +03:00
parent 06a1772011
commit e5179af9d8
5 changed files with 66 additions and 16 deletions
+9
View File
@@ -869,6 +869,7 @@ EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_mu
|------|---------|-------------| |------|---------|-------------|
| 24 / 28 / 32 | смерть: обычная / в бою / от руки тени | `ctrl_kid_death` | | 24 / 28 / 32 | смерть: обычная / в бою / от руки тени | `ctrl_kid_death` |
| 25 | вступление 1-го уровня (Кид сидит), тень 6-го | `control_crouched`, `guards.c` | | 25 | вступление 1-го уровня (Кид сидит), тень 6-го | `control_crouched`, `guards.c` |
| — | НА ДЕМО-УРОВНЕ музыки нет вовсе: там одни эффекты | гейт в `pop_music_service` |
| 27 / 35 / 40 | сцены перед 2/4/6/12, 8/9, «времени мало» | `pop_pre_cutscene_show` | | 27 / 35 / 40 | сцены перед 2/4/6/12, 8/9, «времени мало» | `pop_pre_cutscene_show` |
| 29 | встреча с Джафаром | `pop_meet_jaffar` | | 29 | встреча с Джафаром | `pop_meet_jaffar` |
| 30 / 33 | большая склянка / малая | `pop_proc_get_object` | | 30 / 33 | большая склянка / малая | `pop_proc_get_object` |
@@ -878,6 +879,14 @@ EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_mu
| 41 / 32 | конец уровня / конец 4-го (тень) | опкод SND_LEVEL в `play_seq` | | 41 / 32 | конец уровня / конец 4-го (тень) | опкод SND_LEVEL в `play_seq` |
| 26 | встреча с принцессой | `cut_ending` | | 26 | встреча с принцессой | `cut_ending` |
**Вступление первого уровня — автомат, а не «звук при приседе»** (seg005:02EB).
Пока `need_level1_music` не ноль, `control_crouched` НЕ ЧИТАЕТ управление:
Кид сидит, тема играет, и лишь когда она смолкла, он может встать. Наша
первая версия просто играла трек при первом приседе — и тема догоняла
игрока посреди уровня (пробежал, спрыгнул, присел — заиграла). Признак
«ещё звучит» берём у курсора насоса `pop_mus_left`: он резидентный, и если
музыка выключена, курсор остаётся нулём — Кид просто встаёт.
Темы, которые звучат один раз за заход на уровень (вступление 1-го, тень Темы, которые звучат один раз за заход на уровень (вступление 1-го, тень
6-го), сбрасывает `pop_music_level_start()` из `pop_start_level`. Оригинал 6-го), сбрасывает `pop_music_level_start()` из `pop_start_level`. Оригинал
для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это
+4
View File
@@ -442,6 +442,10 @@ void pop_jaffar_exit(void) __banked
* Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */ * Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */
static void on_guard_killed(void) static void on_guard_killed(void)
{ {
/* Демо-уровень (0) у оригинала уходит в свою ветку — «беги из комнаты»,
* без единого звука (seg006:1929). Заставочная демка озвучена только
* эффектами, музыки в ней нет вовсе. */
if (pop_current_level == 0) return;
if (pop_current_level != JAFFAR_LEVEL) { if (pop_current_level != JAFFAR_LEVEL) {
/* seg006:1940 — победный трек за стража; за тень его нет: она не /* seg006:1940 — победный трек за стража; за тень его нет: она не
* «убита», а слилась с Кидом. */ * «убита», а слилась с Кидом. */
+39 -11
View File
@@ -21,6 +21,7 @@
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */ #include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */ #include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_music.h" /* pop_mus_left — «тема ещё звучит» */
#include "pop_map.h" #include "pop_map.h"
#include "pop_level.h" /* pop_current_level — вступительная тема 1-го */ #include "pop_level.h" /* pop_current_level — вступительная тема 1-го */
#include "pop_guard.h" /* charid, состояние меча, seq стража */ #include "pop_guard.h" /* charid, состояние меча, seq стража */
@@ -255,24 +256,45 @@ static void safe_step(void)
} }
} }
/* Вступительная тема уровня 1 играется один раз за заход на уровень; /* Автомат вступительной темы уровня 1 (порт need_level1_music):
* сбрасывает флаг pop_ctrl_level_start (её зовёт запуск уровня). */ * 0 — не нужна, 1 — пора пустить, 2 — играет, ждём конца. */
static uint8_t lvl1_music; static uint8_t lvl1_music;
void pop_ctrl_level_start(void) __banked { lvl1_music = 0; } /* seg003:45 — need_level1_music = (level == 1). Ставит старт уровня. */
void pop_ctrl_level_start(void) __banked
{
lvl1_music = (uint8_t)(pop_current_level == 1);
}
/* ---- control_* обработчики состояний ------------------------------ */ /* ---- control_* обработчики состояний ------------------------------ */
static void control_crouched(void) static void control_crouched(void)
{ {
/* «Presentation» (seg005:02EB): на первом уровне Кид начинает игру /* ВСТУПЛЕНИЕ ПЕРВОГО УРОВНЯ (seg005:02EB) — это не «звук при приседе», а
* сидя, и пока он сидит, звучит вступительная тема. Оригинал ждёт * маленький автомат, и Кид на его время ОБЕЗДВИЖЕН:
* тишины (`if (!check_sound_playing())`) — у нас достаточно одного * 1 — ничего не звучит: пустить тему, перейти в 2;
* запуска: заявку разберёт главный цикл, а насос сам придержит музыку, * 2 — ждать, пока она доиграет, и только тогда отпустить управление.
* пока доигрывает эффект. */ * Оттого Кид и сидит в начале игры, пока не смолкнет музыка.
if (pop_current_level == 1 && !lvl1_music) { *
lvl1_music = 1; * Я сперва сделал проще — «сыграть при первом приседе», — и тема
* догоняла игрока посреди уровня: пробежал, спрыгнул, присел — заиграла
* (поймано пользователем).
*
* Ждём по pop_mus_left: это курсор насоса, он резидентный и правдиво
* отвечает «трек ещё звучит». Если музыка выключена или заявку никто
* не обработал, курсор так и останется нулём — Кид просто встанет, а
* не сядет навсегда. */
if (lvl1_music && pop_current_level == 1) {
if (!pop_mus_left) {
if (lvl1_music == 1) {
pop_sfx_play(25); pop_sfx_play(25);
lvl1_music = 2;
} else {
lvl1_music = 0;
} }
}
return; /* управление на это время не читаем */
}
lvl1_music = 0;
/* Shift в приседе — поднять предмет (seg005 control_crouched). */ /* Shift в приседе — поднять предмет (seg005 control_crouched). */
if (control_shift2 == CONTROL_HELD && get_item_action()) return; if (control_shift2 == CONTROL_HELD && get_item_action()) return;
if (control_y != CONTROL_HELD_DOWN) { if (control_y != CONTROL_HELD_DOWN) {
@@ -730,11 +752,17 @@ static void ctrl_kid_death(void)
/* play_death_music (seg006:15B5): траурный трек зависит от того, /* play_death_music (seg006:15B5): траурный трек зависит от того,
* КАК умер Кид — в бою, от руки тени или просто так. Оригинал ждёт * КАК умер Кид — в бою, от руки тени или просто так. Оригинал ждёт
* шести тиков, чтобы доиграл звук удара; у нас ждать не нужно — * шести тиков, чтобы доиграл звук удара; у нас ждать не нужно —
* насос держит курсор музыки, пока звучит эффект. */ * насос держит курсор музыки, пока звучит эффект.
*
* НА ДЕМО-УРОВНЕ МУЗЫКИ НЕТ — и это буквальное условие оригинала
* (seg006:1366: `current_level != 0 && current_level != 15`).
* Заставочная демка озвучена одними эффектами. */
if (pop_current_level != 0 && pop_current_level != 15) {
if (Guard.charid == CHARID_1_SHADOW) pop_sfx_play(32); if (Guard.charid == CHARID_1_SHADOW) pop_sfx_play(32);
else if (holding_sword) pop_sfx_play(28); else if (holding_sword) pop_sfx_play(28);
else pop_sfx_play(24); else pop_sfx_play(24);
} }
}
} }
/* ---- Публичный API ------------------------------------------------ */ /* ---- Публичный API ------------------------------------------------ */
+7 -1
View File
@@ -16,6 +16,7 @@
#include "pop_music.h" #include "pop_music.h"
#include "pop_music_tbl.h" #include "pop_music_tbl.h"
#include "pop_sfx.h" #include "pop_sfx.h"
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
uint8_t pop_mus_page[POP_MUS_PAGES]; uint8_t pop_mus_page[POP_MUS_PAGES];
@@ -219,7 +220,12 @@ void pop_music_service(void) __banked
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены. /* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
* Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала), * Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала),
* а диск трогаем только здесь. */ * а диск трогаем только здесь. */
if (!pop_mus_want) { /* В ЗАСТАВОЧНОЙ ДЕМКЕ МУЗЫКИ НЕТ. Точечные условия оригинала (смерть,
* убитый страж) расставлены по своим местам, но гейт нужен и общий:
* демо проигрывает записанные нажатия, и любой музыкальный повод —
* подобранный меч, зелье — зазвучал бы там, где оригинал играет одни
* эффекты. */
if (!pop_mus_want || pop_app_state == POP_APP_DEMO) {
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом /* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
* продолжают звучать — у них свой тумблер. */ * продолжают звучать — у них свой тумблер. */
if (pop_mus_req) pop_mus_req = 0; if (pop_mus_req) pop_mus_req = 0;
@@ -156,6 +156,9 @@ void tk_key_down(uint16_t code)
* проверяют их игровую логику, поэтому здесь достаточно no-op контрактов. */ * проверяют их игровую логику, поэтому здесь достаточно no-op контрактов. */
void pop_beam_sample(void) __preserves_regs(b, c, d, e) { } void pop_beam_sample(void) __preserves_regs(b, c, d, e) { }
void pop_sfx_play(uint8_t id) { (void)id; } void pop_sfx_play(uint8_t id) { (void)id; }
/* Курсор насоса: в тестах звука нет вовсе, значит «ничего не звучит» — и
* автомат вступительной темы первого уровня сразу отпускает управление. */
volatile uint16_t pop_mus_left;
void pop_sfx_stop(void) { } void pop_sfx_stop(void) { }
/* ---- 2. соседние модули движка -------------------------------------- */ /* ---- 2. соседние модули движка -------------------------------------- */