Files
Sprinter-SDCC/applications/PoP/roomtest/pop_sfx.c
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00

205 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_sfx.c — ГОРЯЧАЯ часть звуковых эффектов: насос в прерывании CBL,
* запуск/останов и таблица звуков. Зачем всё это и в каком виде лежат
* данные — в шапке pop_sfx.h и ../docs/sound_plan.md.
*
* ПОЧЕМУ МОДУЛЬ РЕЗИДЕНТНЫЙ (в EXTRA_SRCS, а не в банке). Три причины,
* и каждой хватило бы одной:
* - callback насоса зовётся ИЗ ПРЕРЫВАНИЯ; трамплин перед вызовом
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
* Файловое чтение страниц выполняет общий резидентный bank_load_file().
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
*/
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <cbl.h>
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
#include "pop_sfx.h"
#include "_pop_sfx.h"
/* Физические страницы набора; заполняет холодная половина. */
uint8_t pop_snd_page[POP_SND_PAGES];
uint8_t pop_snd_ok;
/* ХОЧЕТ ЛИ ЗВУКА ПОЛЬЗОВАТЕЛЬ (Ctrl+S) — отдельно от pop_snd_ok.
*
* Разделять обязательно: pop_snd_ok значит «CBL сейчас открыт», и его
* гасит служебная пауза на время загрузки уровня. Если бы Ctrl+S правил
* только его, то первая же смена уровня позвала бы pop_sfx_start() и
* вернула звук, который пользователь выключил. Поэтому start уважает
* этот флаг, а pause его не трогает.
*
* Живёт в резиденте, а не в банке: метку в борте рисует отладочный ярлык
* каждый кадр, и тащить ради одного байта трамплин незачем. */
uint8_t pop_snd_want;
/* Курсор проигрывания. volatile: пишет главный цикл, читает прерывание.
* Всё в 8/16 бит — 32-битной арифметики на Z80 избегаем (см. memory
* avoid_32bit_arith_z80); поэтому и в таблице смещение уже разобрано на
* страницу и адрес в окне. */
static volatile uint8_t sfx_pg; /* индекс в pop_snd_page[] */
static volatile uint16_t sfx_ptr; /* адрес в окне W0: 0x0000..0x3FFF */
static volatile uint16_t sfx_left; /* сколько байт осталось (кратно 128) */
/* ---- насос ---------------------------------------------------------- *
* Зовётся из ISR CBL раз в ~11,7 мс за очередными 128 байтами.
*
* ОКНО W0 БЕРЁМ ВЗАЙМЫ. Фон в этот момент может держать в W0 страницу
* атласа (pop_blit_b делает map ... blit ... unmap), поэтому сохраняем
* порт, подставляем свою страницу и возвращаем как было. Для фона
* подмена невидима: прерывание для него атомарно. Тот же приём, что у
* трамплина с W1 (_irq_tramp.c).
*
* Заплатки ISR-стаба в 0x38/0x66 нашим страницам НЕ нужны (в отличие от
* страницы уровня, pop_level_read_file): пока страница замаплена, мы
* внутри прерывания и IFF сброшен — прерваться некому.
*
* ТИШИНУ ЛЬЁМ СВОЮ. Когда эффект кончился, отдавать в CBL всё равно надо:
* иначе железо крутит по кругу хвост своего буфера и это слышно как
* жужжание. Готовый блок 0x80 лежит первым в наборе (упаковщик), поэтому
* libc-шный CBL_UNDERRUN_SILENCE с его malloc нам не нужен — а куча в
* тесном резиденте W2 отказала бы и уронила весь звук. */
int pop_sfx_fill(uint16_t n)
{
uint8_t saved, pg;
uint16_t ptr;
if (sfx_left) { pg = sfx_pg; ptr = sfx_ptr; }
else { pg = POP_SND_SILENCE_PAGE; ptr = POP_SND_SILENCE_OFF; }
saved = _io_page_w0;
_io_page_w0 = pop_snd_page[pg];
cbl_push_otir((const void *)ptr, n);
_io_page_w0 = saved;
if (!sfx_left) return 1; /* играли тишину — курсор не двигаем */
sfx_ptr += n;
if (sfx_ptr >= 0x4000) { /* граница страницы — ровно по блоку */
sfx_ptr = 0;
/* Переход на следующую страницу нужен ровно одному эффекту — тому,
* что сам ДЛИННЕЕ страницы (упаковщик с 2026-08-25 кладёт остальные
* целиком в одну). Гард обязателен: без него ошибка в длине увела
* бы насос за pop_snd_page[] и он читал бы чужую память — на слух
* это «играет не тот звук», причём каждый раз другой. */
if ((uint8_t)(sfx_pg + 1) < POP_SND_PAGES) sfx_pg++;
else sfx_left = n; /* обрываем */
}
sfx_left -= n;
return 1;
}
/* ---- ОЧЕРЕДЬ И ПРИОРИТЕТЫ (порт seg000:12C5 + seg000:1304) ------------ *
*
* Оригинал НЕ перебивает играющий звук чем попало, и это слышно. Модель
* ровно такая:
*
* play_sound(id) — только НОМИНИРУЕТ кандидата на кадр; из нескольких
* за кадр остаётся ВАЖНЕЙШИЙ (меньше prio = важнее,
* при равенстве побеждает последний — сравнение <=);
* play_next_sound() — раз в кадр решает, запускать ли: можно, если ничего
* не играет, ЛИБО текущий звук помечен «перебиваемым»
* И новый не менее важен. Иначе номинант ВЫБРАСЫВАЕТСЯ
* (next_sound = -1 безусловно), а не встаёт в очередь.
*
* Отсюда то, что слышно в игре: `gate_closing_fast` (6) неперебиваем и
* доигрывает до звонкого конца, а приземление Кида в этот момент пропадает
* совсем; челюсти (47, prio 0x10) всегда важнее решётки (4, prio 0x32), и
* решётка звучит только в паузах между укусами.
*
* Таблицы — из SDLPoP (sound_prio_table seg000:1528, sound_interruptible
* data.h:433) с правками fix_sound_priorities(): в config.h SDLPoP
* FIX_SOUND_PRIORITIES определён безусловно, то есть сравниваем мы себя
* именно с исправленным вариантом (10 -> 0x0D, 48 -> 0x15, 49 перебиваем).
*/
/* Приоритет: МЕНЬШЕ значит ВАЖНЕЕ. Музыкальные id тут тоже есть — они
* никогда не номинируются (нет оцифровки), но индексация должна совпадать
* с оригиналом, иначе таблицу не сверить глазами. */
static const uint8_t snd_prio[POP_SND_COUNT] = {
0x14, 0x1E, 0x23, 0x66, 0x32, 0x37, 0x30, 0x30, 0x4B, 0x50, /* 0.. 9 */
0x0D, 0x12, 0x0C, 0x0B, 0x69, 0x6E, 0x73, 0x78, 0x7D, 0x82, /* 10..19 */
0x91, 0x96, 0x9B, 0xA0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x13, /* 20..29 */
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, /* 30..39 */
0x01, 0x01, 0x01, 0x01, 0x87, 0x8C, 0x0F, 0x10, 0x15, 0x16, /* 40..49 */
0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01 /* 50..56 */
};
/* «Играющий звук можно перебить» — битовая карта (бит id&7 байта id>>3):
* 57 флагов уложены в 8 байт, потому что в резиденте W1/W2 каждая сотня
* байт на счету, а читаем мы их раз в кадр. */
static const uint8_t snd_intr[8] = {
/* 0.. 7 */ (uint8_t)(0<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 1<<7),
/* 8..15 */ (uint8_t)(1<<0 | 1<<1 | 1<<2 | 1<<3 | 1<<4 | 1<<5 | 0<<6 | 0<<7),
/*16..23 */ (uint8_t)(1<<0 | 1<<1 | 0<<2 | 1<<3 | 1<<4 | 1<<5 | 1<<6 | 1<<7),
/*24..31 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 1<<5 | 0<<6 | 0<<7),
/*32..39 */ 0,
/*40..47 */ (uint8_t)(0<<0 | 0<<1 | 0<<2 | 0<<3 | 0<<4 | 0<<5 | 0<<6 | 1<<7),
/*48..55 */ 0,
/*56.. */ 0
};
/* Створка двери уровня (15) — единственная запись, которую оригинал правит
* НА ХОДУ (seg007:442/464): при закрытии её перебивать можно, при открытии
* нельзя (иначе съезд створки обрывается и «конца» не слышно). Держим
* отдельным байтом, чтобы таблица осталась в ПЗУ. */
uint8_t pop_sfx_slide_intr;
/* Номинант кадра, ЗАПИСАННЫЙ КАК id+1: ноль значит «никого». Так сделано
* ради crt0 — он зануляет _DATA, а «нет номинанта» у оригинала это -1;
* со смещением на единицу начальное состояние получается даром. */
static uint8_t snd_next1;
static uint8_t snd_curr; /* что играет — для prio/intr */
static uint8_t snd_interruptible(uint8_t id)
{
if (id == 15) return pop_sfx_slide_intr;
return (uint8_t)(snd_intr[id >> 3] & (uint8_t)(1 << (id & 7)));
}
/* ---- запуск / останов ------------------------------------------------ */
void pop_sfx_play(uint8_t id)
{
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
/* Нет оцифровки (музыка) — не номинируем вовсе: порт проверки
* `if (NULL == sound_pointers[id]) return;`. Иначе музыкальный id
* выигрывал бы конкурс и глушил живой эффект. */
if (pop_snd_tbl[id].len == 0) return;
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
snd_next1 = (uint8_t)(id + 1);
}
void pop_sfx_tick(void)
{
uint8_t id, pg;
uint16_t off, len;
if (!snd_next1) return;
id = (uint8_t)(snd_next1 - 1);
snd_next1 = 0; /* номинант живёт ровно кадр */
if (sfx_left != 0 && /* что-то играет... */
(!snd_interruptible(snd_curr) || snd_prio[id] > snd_prio[snd_curr]))
return; /* ...и уступать не обязано */
snd_curr = id;
pg = pop_snd_tbl[id].page;
off = pop_snd_tbl[id].off;
len = pop_snd_tbl[id].len;
/* Курсор из трёх полей, а читает его прерывание — обновляем под DI,
* иначе насос может застать наполовину обновлённое состояние и уехать
* в чужие данные. */
IRQ_DISABLE();
sfx_pg = pg;
sfx_ptr = off;
sfx_left = len;
IRQ_ENABLE();
}
void pop_sfx_stop(void)
{
IRQ_DISABLE();
sfx_left = 0;
IRQ_ENABLE();
}