SprPoP: звук мигания, равномерные часы, полоса HP после рестарта, надпись ждёт мелодию
Четыре правки прогона 2026-08-31. Две ПРОВЕРЕНЫ пользователем в MAME (звук мигания, порядок «мелодия -> надпись»), две ждут проверки — образ собран. ЗВУК МИГАНИЯ (проверено). Упаковщик научился синтезировать ноты PC- спикера в обычный сэмпл: заголовок с темпом, тройки «частота + длина», меандр на нашей частоте вывода. Берём из нот ТОЛЬКО номера, которых нет ни в оцифровке, ни среди мелодий, — иначе синтез перекрыл бы музыку, которую мы играем из MUS/. На поставке SDLPoP это ровно один номер: 38, сигнал под мигание «Press Button»; 31, 34 и 42 там пустые заглушки. Громкость по слуховой проверке снижена вдвое (44 -> 22): на полном размахе сигнал перекрикивал игру. EXE не меняется — раскладка читается с диска, набор занял те же 9 страниц. НАДПИСЬ ЖДЁТ МЕЛОДИЮ (проверено). Порядок оригинала: ветка мёртвого (seg006:1351) на седьмом шаге выходит, пока звук играет, и «Press Button» появляется только после музыки смерти. Чтобы ожидание не было принудительным, три быстрых пути (Ctrl+A, обе быстрые загрузки, пункты меню) музыку глушат — оригинал при Ctrl+A делает то же (seg000:0617). Обычная кнопка во время мелодии не действует: она ответ НА надпись. ЧАСЫ (ждёт проверки). Тик стоит столько кадров ЛУЧА, сколько их в кадре режима NORMAL, поэтому FAST/FASTEST больше не ускоряют время. Считаем ФАКТИЧЕСКИ прошедшие кадры луча, а не ожидаемый делитель: логический кадр не всегда укладывается в бюджет, и часы «по делителю» шли рывками (первый прогон это показал — «несколько секунд быстро, потом притормаживание»). Вклад одного вызова ограничен, иначе пауза и меню прыгнули бы вперёд. Четыре новых теста: NORMAL не сдвинулся ни на тик (и вне боя, и в бою), FAST и FASTEST держат реальное время. ПОЛОСА HP ПОСЛЕ Ctrl+A (ждёт проверки). Корень: счётчик считает СТРАНИЦЫ, а тратился по КАДРАМ — между двумя вызовами переворота может не быть, и оба прохода уходили в одну страницу, вторая оставалась с делениями прошлого боя. Теперь проход тратится только при смене gfx_get_draw_page(). Плюс полная чистка всей ширины при инвалидации: старая полоса могла заходить под статус-текст, где щадящая чистка её не трогала; текст сразу перезапрашивается. Все 16 наборов host-тестов зелёные, check_bank_calls чист. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -39,7 +39,7 @@
|
|||||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||||||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||||||
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные (2026-08-31) |
|
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные (2026-08-31) |
|
||||||
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | открыт: разбор есть (2026-08-31) |
|
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1423,6 +1423,15 @@ Ctrl+A у нас — РЕСТАРТ УРОВНЯ (`sprpop_cold.c`, тот же
|
|||||||
середину, там и остаётся. Полоса стража рисуется справа налево от 314 и
|
середину, там и остаётся. Полоса стража рисуется справа налево от 314 и
|
||||||
при большом запасе HP заходит именно в эту незачищаемую зону.
|
при большом запасе HP заходит именно в эту незачищаемую зону.
|
||||||
|
|
||||||
|
**КОРЕНЬ (2026-08-31).** Счётчик считает СТРАНИЦЫ, но тратился по КАДРАМ,
|
||||||
|
а это не одно и то же: между двумя вызовами отрисовки переворота может не
|
||||||
|
быть, и оба прохода уходили в одну страницу — вторая оставалась с
|
||||||
|
делениями прошлого боя. Фикс: проход тратится только когда
|
||||||
|
`gfx_get_draw_page()` отличается от страницы прошлого прохода (первый
|
||||||
|
проход идёт всегда). Полная чистка всей ширины при инвалидации добавлена
|
||||||
|
там же — старая полоса могла заходить под статус-текст, где щадящая чистка
|
||||||
|
её не трогала; текст сразу перезапрашивается, чтобы не пропал.
|
||||||
|
|
||||||
**Что проверить при взятии в работу.**
|
**Что проверить при взятии в работу.**
|
||||||
|
|
||||||
1. Значения `POP_STATUS_L`/`POP_STATUS_R` против реальной ширины полос:
|
1. Значения `POP_STATUS_L`/`POP_STATUS_R` против реальной ширины полос:
|
||||||
|
|||||||
@@ -1009,7 +1009,7 @@ tp/10 у факелов таблицей, пустой слот соперник
|
|||||||
|
|
||||||
## P1 — берётся в любой момент
|
## P1 — берётся в любой момент
|
||||||
|
|
||||||
### <a id="time-speed-modes"></a>TIME-SPEED. Часы бегут быстрее в FAST/FASTEST
|
### <a id="time-speed-modes"></a>TIME-SPEED. Часы бегут быстрее в FAST/FASTEST — СДЕЛАНО 2026-08-31, остался вариант 3 (RTC)
|
||||||
|
|
||||||
**Наблюдение (пользователь, 2026-08-31):** «при режиме FAST/FASTEST время
|
**Наблюдение (пользователь, 2026-08-31):** «при режиме FAST/FASTEST время
|
||||||
начинает бежать быстрее — похоже, время мы считаем в наших логических
|
начинает бежать быстрее — похоже, время мы считаем в наших логических
|
||||||
@@ -1040,12 +1040,37 @@ tp/10 у факелов таблицей, пустой слот соперник
|
|||||||
минутой в любом режиме, а в бою часы по-прежнему замедляются, как в
|
минутой в любом режиме, а в бою часы по-прежнему замедляются, как в
|
||||||
оригинале. Цена — счётчик-накопитель в `pop_timer_tick`.
|
оригинале. Цена — счётчик-накопитель в `pop_timer_tick`.
|
||||||
|
|
||||||
**Решать пользователю; по умолчанию — вариант 1** (режимы скорости
|
3. **Часы от RTC** (идея пользователя, 2026-08-31). Брать время из
|
||||||
отладочные, а вариант 2 разводит наш таймер с оригинальным на уровне
|
часов реального времени, а не считать кадры вовсе.
|
||||||
механики).
|
|
||||||
|
**Почему третий вариант интереснее, чем кажется.** Погрешность есть уже
|
||||||
|
СЕЙЧАС и без всяких режимов: логический кадр NORMAL — 4 кадра луча, а это
|
||||||
|
81,93 мс, то есть 12,2 кадра в секунду вместо ровных 12. Оригинальная
|
||||||
|
минута из 720 тиков проходит у нас за 58,99 с — почти на секунду быстрее.
|
||||||
|
За час игры набегает около минуты. Ни один из первых двух вариантов этого
|
||||||
|
не лечит: они выравнивают режимы между собой, но обе шкалы остаются
|
||||||
|
привязанными к лучу, а луч не кратен игровой секунде.
|
||||||
|
|
||||||
|
RTC (`ESTEX $21 SYSTIME`, memory `sprinter_systime_dow`) даёт абсолютную
|
||||||
|
шкалу и снимает накопление полностью. Подводные камни, которые надо
|
||||||
|
решить при взятии в работу:
|
||||||
|
|
||||||
|
- вызов ESTEX стоит дорого и клобберит регистры — читать раз в тик, не в
|
||||||
|
кадре, и не из горячего пути (memory `estex_bios_abi`);
|
||||||
|
- разрешение RTC — секунда, а тик игры — 1/12 секунды: нужен гибрид
|
||||||
|
«кадры внутри секунды, синхронизация по RTC на границе», иначе часы
|
||||||
|
задёргаются;
|
||||||
|
- пауза, меню и загрузка НЕ должны съедать игровое время — при часах от
|
||||||
|
RTC это перестаёт получаться само собой и требует явного вычитания;
|
||||||
|
- быстрая загрузка/сохранение обязаны сохранять смещение, а не абсолютное
|
||||||
|
время.
|
||||||
|
|
||||||
|
**Решать пользователю.** Порядок по цене: вариант 1 (ничего), вариант 2
|
||||||
|
(счётчик кадров луча, лечит только разбег режимов), вариант 3 (RTC, лечит
|
||||||
|
и накопление — но требует разобраться с паузами).
|
||||||
|
|
||||||
|
|
||||||
### <a id="snd-speaker-38"></a>SND-SPEAKER-38. Звук мигания надписи: синтезировать ноты спикера в PCM
|
### <a id="snd-speaker-38"></a>SND-SPEAKER-38. Звук мигания надписи — СДЕЛАНО и ПРОВЕРЕНО 2026-08-31
|
||||||
|
|
||||||
**Постановка (пользователь, 2026-08-31):** «когда надпись Press Button
|
**Постановка (пользователь, 2026-08-31):** «когда надпись Press Button
|
||||||
начинает мигать — в SDLPoP воспроизводится звук на каждое моргание, у нас
|
начинает мигать — в SDLPoP воспроизводится звук на каждое моргание, у нас
|
||||||
|
|||||||
@@ -989,11 +989,24 @@ void pop_mirror_draw(int clip_top) __banked
|
|||||||
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
||||||
* sdcc_z80_cmp_store_a_bug). */
|
* sdcc_z80_cmp_store_a_bug). */
|
||||||
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
||||||
|
/* Сколько страниц ещё стереть ЦЕЛИКОМ. Обычное обновление щадит зону
|
||||||
|
* статус-текста (иначе он мигал бы на каждом изменении жизней), но после
|
||||||
|
* рестарта уровня в этой зоне остаются деления ПРОШЛОГО боя: полоса стража
|
||||||
|
* при большом запасе HP заходит под текст, и щадящая чистка их не трогает.
|
||||||
|
* Симптом — «после гибели и Ctrl+A на одной из страниц осталась полоса по
|
||||||
|
* результатам боя» (BUGS_OPEN, HP-BAR-RESTART). */
|
||||||
|
static uint8_t hp_wipe; /* сколько страниц ещё стереть */
|
||||||
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
||||||
|
/* Страница, в которую лёг прошлый проход. Счётчики выше считают СТРАНИЦЫ,
|
||||||
|
* но кадр и страница — не одно и то же: между двумя вызовами переворота
|
||||||
|
* может не быть, и тогда оба прохода уходили в ОДНУ страницу, а вторая
|
||||||
|
* оставалась с делениями прошлого боя. Ровно этим полоса и переживала
|
||||||
|
* Ctrl+A (BUGS_OPEN, HP-BAR-RESTART). */
|
||||||
|
static uint8_t hp_page_prev = 0xFF;
|
||||||
|
|
||||||
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
||||||
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
||||||
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
|
void pop_hp_invalidate(void) __banked { hp_todo = 2; hp_wipe = 2; }
|
||||||
|
|
||||||
void pop_hp_draw(void) __banked
|
void pop_hp_draw(void) __banked
|
||||||
{
|
{
|
||||||
@@ -1010,6 +1023,13 @@ void pop_hp_draw(void) __banked
|
|||||||
if (changed) hp_todo = 2;
|
if (changed) hp_todo = 2;
|
||||||
}
|
}
|
||||||
if (hp_todo == 0) return;
|
if (hp_todo == 0) return;
|
||||||
|
{ /* Пока страница та же, что у прошлого прохода, — ждём переворота:
|
||||||
|
* иначе потратим оба прохода на одну страницу. Первый проход
|
||||||
|
* (hp_todo == 2) идёт всегда, ему сравнивать не с чем. */
|
||||||
|
uint8_t pg = gfx_get_draw_page();
|
||||||
|
if (hp_todo < 2 && pg == hp_page_prev) return;
|
||||||
|
hp_page_prev = pg;
|
||||||
|
}
|
||||||
hp_todo--;
|
hp_todo--;
|
||||||
|
|
||||||
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
|
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
|
||||||
@@ -1021,7 +1041,14 @@ void pop_hp_draw(void) __banked
|
|||||||
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
|
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
|
||||||
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
|
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
|
||||||
* так что чистить края по отдельности достаточно. */
|
* так что чистить края по отдельности достаточно. */
|
||||||
if (pop_status_ticks) {
|
if (hp_wipe) {
|
||||||
|
/* Полная чистка обеих страниц: старая полоса могла заходить под
|
||||||
|
* текст. Текст при этом стирается тоже, поэтому сразу просим
|
||||||
|
* перерисовать и его — иначе строка уровня пропала бы. */
|
||||||
|
hp_wipe--;
|
||||||
|
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||||
|
if (pop_status_ticks) pop_status_invalidate();
|
||||||
|
} else if (pop_status_ticks) {
|
||||||
bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6);
|
bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6);
|
||||||
bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ extern uint8_t pop_speed_mode; /* POP_SPEED_*; дефолт — NORMAL */
|
|||||||
|
|
||||||
/* Делитель для текущего режима. fight = «у Кида вынут меч». */
|
/* Делитель для текущего режима. fight = «у Кида вынут меч». */
|
||||||
uint8_t pop_pace_n(uint8_t fight);
|
uint8_t pop_pace_n(uint8_t fight);
|
||||||
|
/* ЭТАЛОН ХОДА ЧАСОВ — делитель РЕЖИМА NORMAL для того же признака боя.
|
||||||
|
* Игровое время меряется им, а не фактическим темпом: иначе FAST/FASTEST
|
||||||
|
* ускоряли бы и часы (минута проходила за две трети минуты). При NORMAL
|
||||||
|
* фактический делитель равен эталону, поэтому его ход не меняется вовсе —
|
||||||
|
* включая замедление в бою, которое есть и в оригинале. */
|
||||||
|
#define POP_PACE_BASE(fight) ((uint8_t)((fight) ? 5 : 4))
|
||||||
|
|
||||||
/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы
|
/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы
|
||||||
* ожидания — перечитывать обязаны каждый оборот. */
|
* ожидания — перечитывать обязаны каждый оборот. */
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
#include <gfx.h>
|
#include <gfx.h>
|
||||||
#include <kbd_raw.h>
|
#include <kbd_raw.h>
|
||||||
#include "pop_status.h"
|
#include "pop_status.h"
|
||||||
|
#include "pop_music.h" /* ждём конец музыки смерти и глушим её по ответу */
|
||||||
#include "pop_ui.h"
|
#include "pop_ui.h"
|
||||||
#include "pop_font.h"
|
#include "pop_font.h"
|
||||||
#include "pop_bg.h" /* POP_YOFF */
|
#include "pop_bg.h" /* POP_YOFF */
|
||||||
@@ -461,14 +462,25 @@ static void dbg_scan(uint8_t room)
|
|||||||
#define DEAD_BLINK 72 /* последние 72 тика строка мигает */
|
#define DEAD_BLINK 72 /* последние 72 тика строка мигает */
|
||||||
#define SND_BLINK 38 /* sound_38_blink на каждом появлении */
|
#define SND_BLINK 38 /* sound_38_blink на каждом появлении */
|
||||||
|
|
||||||
|
/* Кадр, с которого пошёл отсчёт надписи. НЕ равен DEAD_SETTLE: оригинал
|
||||||
|
* не показывает «Press Button», пока звучит музыка смерти — ветка мёртвого
|
||||||
|
* (seg006:1351) на седьмом шаге просто выходит, если звук ещё играет, и
|
||||||
|
* надпись появляется только после него. Поэтому момент старта отсчёта
|
||||||
|
* заранее не известен и запоминается здесь. */
|
||||||
|
static uint16_t dead_base;
|
||||||
static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */
|
static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */
|
||||||
|
|
||||||
uint8_t pop_dead_prompt(uint16_t frames) __banked
|
uint8_t pop_dead_prompt(uint16_t frames) __banked
|
||||||
{
|
{
|
||||||
uint16_t rem;
|
uint16_t rem;
|
||||||
|
|
||||||
if (frames < DEAD_SETTLE) return 0;
|
if (frames < DEAD_SETTLE) { dead_base = 0; return 0; }
|
||||||
if (frames == DEAD_SETTLE) {
|
if (dead_base == 0) {
|
||||||
|
/* Ждём, пока домолчит музыка смерти — порядок оригинала. Пока она
|
||||||
|
* играет, надписи нет и отсчёт 288 не идёт; прервать ожидание можно
|
||||||
|
* Ctrl+A или быстрой загрузкой, они музыку глушат. */
|
||||||
|
if (pop_music_active()) return 0;
|
||||||
|
dead_base = frames;
|
||||||
msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD);
|
msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD);
|
||||||
pop_show_time = 0; /* иначе поверх ляжет время (seg006:1365) */
|
pop_show_time = 0; /* иначе поверх ляжет время (seg006:1365) */
|
||||||
dead_armed = 0;
|
dead_armed = 0;
|
||||||
@@ -477,7 +489,7 @@ uint8_t pop_dead_prompt(uint16_t frames) __banked
|
|||||||
|
|
||||||
/* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288.
|
/* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288.
|
||||||
* Кадры смерти считает вызывающий, так что хватает вычитания. */
|
* Кадры смерти считает вызывающий, так что хватает вычитания. */
|
||||||
rem = (uint16_t)(frames - DEAD_SETTLE);
|
rem = (uint16_t)(frames - dead_base);
|
||||||
rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem);
|
rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem);
|
||||||
if (rem == 0) {
|
if (rem == 0) {
|
||||||
/* Игрок промолчал все 24 секунды — оригинал зовёт start_game(),
|
/* Игрок промолчал все 24 секунды — оригинал зовёт start_game(),
|
||||||
@@ -519,6 +531,8 @@ uint8_t pop_dead_prompt(uint16_t frames) __banked
|
|||||||
if (!any) return 0;
|
if (!any) return 0;
|
||||||
}
|
}
|
||||||
dead_armed = 0;
|
dead_armed = 0;
|
||||||
|
dead_base = 0;
|
||||||
|
pop_music_stop(); /* игрок ответил — доигрывать не заставляем */
|
||||||
msg_set(POP_MSG_NONE, 0, 0);
|
msg_set(POP_MSG_NONE, 0, 0);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,35 @@ uint8_t pop_timer_minutes;
|
|||||||
uint16_t pop_timer_ticks;
|
uint16_t pop_timer_ticks;
|
||||||
uint8_t pop_show_time;
|
uint8_t pop_show_time;
|
||||||
|
|
||||||
|
/* Накопленные кадры луча, ещё не сложившиеся в тик. Живёт между кадрами:
|
||||||
|
* при FAST логический кадр короче эталона, и остаток переносится вперёд. */
|
||||||
|
static uint8_t tick_acc;
|
||||||
|
|
||||||
void pop_timer_new_game(void) __banked
|
void pop_timer_new_game(void) __banked
|
||||||
{
|
{
|
||||||
pop_timer_minutes = POP_TIMER_START_MINUTES;
|
pop_timer_minutes = POP_TIMER_START_MINUTES;
|
||||||
pop_timer_ticks = POP_TIMER_START_TICKS;
|
pop_timer_ticks = POP_TIMER_START_TICKS;
|
||||||
pop_show_time = 0;
|
pop_show_time = 0;
|
||||||
|
tick_acc = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked
|
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run,
|
||||||
|
uint8_t spent, uint8_t base) __banked
|
||||||
{
|
{
|
||||||
if (!enabled || !may_run || pop_timer_minutes == 0) return 0;
|
if (!enabled || !may_run || pop_timer_minutes == 0) return 0;
|
||||||
|
/* ХОД ЧАСОВ ОТВЯЗАН ОТ ТЕМПА. Тик стоит `base` кадров луча — столько,
|
||||||
|
* сколько их в кадре режима NORMAL. При NORMAL spent == base, и тик
|
||||||
|
* приходится ровно на кадр, как было всегда; в быстрых режимах кадр
|
||||||
|
* короче, остаток копится, и за то же РЕАЛЬНОЕ время выходит столько
|
||||||
|
* же тиков. Цикла не нужно: spent никогда не больше base (быстрые
|
||||||
|
* режимы кадр только УКОРАЧИВАЮТ) — значит не больше тика за вызов. */
|
||||||
|
/* Пауза, меню и загрузка сюда не заходят вовсе, поэтому за время их
|
||||||
|
* работы кадры луча накапливаются мимо нас. Ограничиваем вклад одного
|
||||||
|
* вызова: иначе после меню часы прыгнули бы вперёд на всю паузу. */
|
||||||
|
if (spent > (uint8_t)(base + base)) spent = base;
|
||||||
|
tick_acc = (uint8_t)(tick_acc + spent);
|
||||||
|
if (tick_acc < base) return 0;
|
||||||
|
tick_acc = (uint8_t)(tick_acc - base);
|
||||||
|
|
||||||
--pop_timer_ticks;
|
--pop_timer_ticks;
|
||||||
if (pop_timer_ticks != 0) {
|
if (pop_timer_ticks != 0) {
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ void pop_timer_new_game(void) __banked;
|
|||||||
* may_run — правила текущей сцены/уровня. Возвращает 1 ОДИН раз, когда
|
* may_run — правила текущей сцены/уровня. Возвращает 1 ОДИН раз, когда
|
||||||
* отсчёт дошёл до нуля; на паузе, HDD/QuickSave и при выключенном лимите
|
* отсчёт дошёл до нуля; на паузе, HDD/QuickSave и при выключенном лимите
|
||||||
* вызывать можно — состояние останется неизменным. */
|
* вызывать можно — состояние останется неизменным. */
|
||||||
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked;
|
/* spent — сколько кадров ЛУЧА стоил этот логический кадр (pop_pace_n),
|
||||||
|
* base — сколько их было бы при NORMAL (POP_PACE_BASE). Тик отсчитывается
|
||||||
|
* по base, поэтому режим скорости на ход часов не влияет. */
|
||||||
|
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run,
|
||||||
|
uint8_t spent, uint8_t base) __banked;
|
||||||
|
|
||||||
/* Читы, повторяющие +/- SDLPoP: минус не даёт искусственно поставить 0,
|
/* Читы, повторяющие +/- SDLPoP: минус не даёт искусственно поставить 0,
|
||||||
* плюс добавляет минуту. Они меняют счётчик и при выключенном лимите —
|
* плюс добавляет минуту. Они меняют счётчик и при выключенном лимите —
|
||||||
|
|||||||
@@ -240,6 +240,21 @@ static uint8_t item_pos, item_bake, flash_on;
|
|||||||
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
|
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
|
||||||
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
|
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
|
||||||
* (см. build/obj/SprPoP.asm) и ассемблер падает. */
|
* (см. build/obj/SprPoP.asm) и ассемблер падает. */
|
||||||
|
/* Сколько кадров ЛУЧА прошло с прошлого игрового кадра. Часы считают
|
||||||
|
* именно их, а не ожидаемый делитель темпа: логический кадр не всегда
|
||||||
|
* укладывается в свой бюджет (дорогая сцена, загрузка звука), и часы,
|
||||||
|
* считавшие «по делителю», шли рывками — несколько секунд быстро, потом
|
||||||
|
* притормаживание (наблюдение пользователя 2026-08-31). Луч же идёт
|
||||||
|
* ровно, поэтому по нему время течёт равномерно. */
|
||||||
|
static uint8_t timer_beam_delta(void)
|
||||||
|
{
|
||||||
|
static uint8_t prev;
|
||||||
|
uint8_t now = pop_frame_tick;
|
||||||
|
uint8_t d = (uint8_t)(now - prev);
|
||||||
|
prev = now;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
|
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
|
||||||
{
|
{
|
||||||
gfx_pal_set(0, 0, r, g, b);
|
gfx_pal_set(0, 0, r, g, b);
|
||||||
@@ -608,7 +623,11 @@ int main(void)
|
|||||||
if (!is_demo && pop_timer_tick(pop_settings.time_limit_enabled,
|
if (!is_demo && pop_timer_tick(pop_settings.time_limit_enabled,
|
||||||
(uint8_t)(Kid.alive < 0 &&
|
(uint8_t)(Kid.alive < 0 &&
|
||||||
(pop_current_level < 13 ||
|
(pop_current_level < 13 ||
|
||||||
(pop_current_level == 13 && pop_leveldoor_open == 0)))) &&
|
(pop_current_level == 13 && pop_leveldoor_open == 0))),
|
||||||
|
/* ХОД ЧАСОВ — по эталону NORMAL, а не по фактическому
|
||||||
|
* темпу: иначе FAST/FASTEST ускоряли бы и время. */
|
||||||
|
timer_beam_delta(),
|
||||||
|
POP_PACE_BASE(Kid.sword == SWORD_2_DRAWN)) &&
|
||||||
pop_current_level < 13) {
|
pop_current_level < 13) {
|
||||||
/* Время ИДЁТ и на уровне Джафара (до открытия двери), но
|
/* Время ИДЁТ и на уровне Джафара (до открытия двери), но
|
||||||
* КОНЧИТЬСЯ игра там уже не может: expired() у оригинала стоит
|
* КОНЧИТЬСЯ игра там уже не может: expired() у оригинала стоит
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
|||||||
mrc = pop_menu_process();
|
mrc = pop_menu_process();
|
||||||
}
|
}
|
||||||
if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save();
|
if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save();
|
||||||
else if (mrc == POP_MENU_QUICKLOAD) pop_qsave_request_load();
|
else if (mrc == POP_MENU_QUICKLOAD) { pop_music_stop(); pop_qsave_request_load(); }
|
||||||
else if (mrc == POP_MENU_QUICKLOAD_BACKUP)
|
else if (mrc == POP_MENU_QUICKLOAD_BACKUP)
|
||||||
pop_qsave_request_load_backup();
|
{ pop_music_stop(); pop_qsave_request_load_backup(); }
|
||||||
else if (mrc == POP_MENU_RESTART_LEVEL) *restart_level = 1;
|
else if (mrc == POP_MENU_RESTART_LEVEL) { pop_music_stop(); *restart_level = 1; }
|
||||||
else if (mrc == POP_MENU_RESTART_GAME)
|
else if (mrc == POP_MENU_RESTART_GAME)
|
||||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_RESTART_INTRO) ? 1 : -1);
|
return (int8_t)(pop_app_dispatch(POP_APP_EV_RESTART_INTRO) ? 1 : -1);
|
||||||
else if (mrc == POP_MENU_QUIT)
|
else if (mrc == POP_MENU_QUIT)
|
||||||
@@ -137,10 +137,15 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
|||||||
if (ctrl && kbd_raw_down(KBD_QUIT))
|
if (ctrl && kbd_raw_down(KBD_QUIT))
|
||||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
|
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
|
||||||
/* Ctrl+A — рестарт уровня: тот же путь, что и пунктом меню. */
|
/* Ctrl+A — рестарт уровня: тот же путь, что и пунктом меню. */
|
||||||
|
/* БЫСТРЫЙ ВЫХОД ИЗ СМЕРТИ. Надпись «Press Button» у оригинала ждёт,
|
||||||
|
* пока домолчит музыка смерти (seg006:1351), — так же и у нас. Чтобы
|
||||||
|
* ожидание не было принудительным, три быстрых пути (Ctrl+A, обе
|
||||||
|
* быстрые загрузки и пункты меню) музыку ГЛУШАТ: оригинал при Ctrl+A
|
||||||
|
* делает то же самое (seg000:0617, stop_sounds). */
|
||||||
{
|
{
|
||||||
uint8_t r = (uint8_t)(ctrl && kbd_raw_down(KBD_RESTART));
|
uint8_t r = (uint8_t)(ctrl && kbd_raw_down(KBD_RESTART));
|
||||||
|
|
||||||
if (r && !restart_prev) *restart_level = 1;
|
if (r && !restart_prev) { pop_music_stop(); *restart_level = 1; }
|
||||||
restart_prev = r;
|
restart_prev = r;
|
||||||
}
|
}
|
||||||
/* Ctrl+R — вернуться в заставку. Тот же переход, что «Restart Game»
|
/* Ctrl+R — вернуться в заставку. Тот же переход, что «Restart Game»
|
||||||
@@ -181,7 +186,7 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
|||||||
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
||||||
|
|
||||||
if (s && !qs_save_prev) pop_qsave_request_save();
|
if (s && !qs_save_prev) pop_qsave_request_save();
|
||||||
if (l && !qs_load_prev) pop_qsave_request_load();
|
if (l && !qs_load_prev) { pop_music_stop(); pop_qsave_request_load(); }
|
||||||
qs_save_prev = s; qs_load_prev = l;
|
qs_save_prev = s; qs_load_prev = l;
|
||||||
if (pop_qsave_process() > 0) *dead_reset = 1;
|
if (pop_qsave_process() > 0) *dead_reset = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,17 @@ TC_TEST(timer_starts_and_only_runs_when_allowed)
|
|||||||
pop_timer_new_game();
|
pop_timer_new_game();
|
||||||
TC_EQ(pop_timer_minutes, POP_TIMER_START_MINUTES);
|
TC_EQ(pop_timer_minutes, POP_TIMER_START_MINUTES);
|
||||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||||
TC_FALSE(pop_timer_tick(0, 1)); /* Settings: unlimited */
|
TC_FALSE(pop_timer_tick(0, 1, 4, 4)); /* Settings: unlimited */
|
||||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||||
TC_FALSE(pop_timer_tick(1, 0)); /* pause/HDD/cutscene */
|
TC_FALSE(pop_timer_tick(1, 0, 4, 4)); /* pause/HDD/cutscene */
|
||||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||||
|
|
||||||
pop_timer_minutes = 2;
|
pop_timer_minutes = 2;
|
||||||
pop_timer_ticks = 2;
|
pop_timer_ticks = 2;
|
||||||
TC_FALSE(pop_timer_tick(1, 1));
|
TC_FALSE(pop_timer_tick(1, 1, 4, 4));
|
||||||
TC_EQ(pop_timer_minutes, 2);
|
TC_EQ(pop_timer_minutes, 2);
|
||||||
TC_EQ(pop_timer_ticks, 1);
|
TC_EQ(pop_timer_ticks, 1);
|
||||||
TC_FALSE(pop_timer_tick(1, 1));
|
TC_FALSE(pop_timer_tick(1, 1, 4, 4));
|
||||||
TC_EQ(pop_timer_minutes, 1);
|
TC_EQ(pop_timer_minutes, 1);
|
||||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||||
}
|
}
|
||||||
@@ -54,11 +54,11 @@ TC_TEST(timer_expiry_is_one_shot)
|
|||||||
{
|
{
|
||||||
pop_timer_minutes = 1;
|
pop_timer_minutes = 1;
|
||||||
pop_timer_ticks = 1;
|
pop_timer_ticks = 1;
|
||||||
TC_TRUE(pop_timer_tick(1, 1));
|
TC_TRUE(pop_timer_tick(1, 1, 4, 4));
|
||||||
TC_EQ(pop_timer_minutes, 0);
|
TC_EQ(pop_timer_minutes, 0);
|
||||||
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
|
||||||
TC_FALSE(pop_timer_tick(1, 1));
|
TC_FALSE(pop_timer_tick(1, 1, 4, 4));
|
||||||
TC_FALSE(pop_timer_tick(0, 1));
|
TC_FALSE(pop_timer_tick(0, 1, 4, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
TC_TEST(timer_cheats_match_sdlpop_rules)
|
TC_TEST(timer_cheats_match_sdlpop_rules)
|
||||||
@@ -107,10 +107,63 @@ TC_TEST(timer_qsave_restores_exact_state_and_rejects_bad_tick)
|
|||||||
TC_EQ(pop_timer_ticks, 444);
|
TC_EQ(pop_timer_ticks, 444);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- ХОД ЧАСОВ НЕ ЗАВИСИТ ОТ РЕЖИМА СКОРОСТИ ------------------------- *
|
||||||
|
*
|
||||||
|
* Тик стоит `base` кадров луча — столько, сколько их в кадре режима
|
||||||
|
* NORMAL. Быстрые режимы только УКОРАЧИВАЮТ кадр (spent < base), поэтому
|
||||||
|
* за одно и то же реальное время выходит одинаковое число тиков.
|
||||||
|
* Наблюдение пользователя, из которого выросла правка: «при FAST/FASTEST
|
||||||
|
* время начинает бежать быстрее». */
|
||||||
|
|
||||||
|
/* Сколько тиков насчитается за `frames` логических кадров темпа spent/base. */
|
||||||
|
static uint16_t ticks_over(uint8_t frames, uint8_t spent, uint8_t base)
|
||||||
|
{
|
||||||
|
uint16_t before;
|
||||||
|
uint8_t i;
|
||||||
|
pop_timer_new_game();
|
||||||
|
before = pop_timer_ticks;
|
||||||
|
for (i = 0; i < frames; i++) pop_timer_tick(1, 1, spent, base);
|
||||||
|
return (uint16_t)(before - pop_timer_ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(timer_normal_ticks_every_frame)
|
||||||
|
{
|
||||||
|
/* NORMAL вне боя: кадр равен эталону — тик на каждом кадре, как было до
|
||||||
|
* правки. Страховка, что NORMAL не изменился ни на тик. */
|
||||||
|
TC_EQ(ticks_over(12, 4, 4), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(timer_normal_fight_ticks_every_frame)
|
||||||
|
{
|
||||||
|
/* NORMAL в бою: кадр длиннее (5), но и эталон 5 — часы по-прежнему идут
|
||||||
|
* по кадру. В РЕАЛЬНОМ времени это медленнее, и ровно так же ведёт себя
|
||||||
|
* оригинал, поэтому трогать нечего. */
|
||||||
|
TC_EQ(ticks_over(12, 5, 5), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(timer_fast_keeps_real_time)
|
||||||
|
{
|
||||||
|
/* FAST вне боя: кадр 3 при эталоне 4. За 12 кадров (36 кадров луча) —
|
||||||
|
* 9 тиков, ровно столько же, сколько NORMAL насчитает за те же 36 кадров
|
||||||
|
* луча. До правки было бы 12: часы бежали на треть быстрее. */
|
||||||
|
TC_EQ(ticks_over(12, 3, 4), 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
TC_TEST(timer_fastest_fight_keeps_real_time)
|
||||||
|
{
|
||||||
|
/* FASTEST в бою — самый короткий кадр против самого длинного эталона: 3 против
|
||||||
|
* 5. За 20 кадров ждём 12 тиков. */
|
||||||
|
TC_EQ(ticks_over(20, 3, 5), 12);
|
||||||
|
}
|
||||||
|
|
||||||
void main(void)
|
void main(void)
|
||||||
{
|
{
|
||||||
TC_RUN(timer_starts_and_only_runs_when_allowed);
|
TC_RUN(timer_starts_and_only_runs_when_allowed);
|
||||||
TC_RUN(timer_expiry_is_one_shot);
|
TC_RUN(timer_expiry_is_one_shot);
|
||||||
TC_RUN(timer_cheats_match_sdlpop_rules);
|
TC_RUN(timer_cheats_match_sdlpop_rules);
|
||||||
TC_RUN(timer_qsave_restores_exact_state_and_rejects_bad_tick);
|
TC_RUN(timer_qsave_restores_exact_state_and_rejects_bad_tick);
|
||||||
|
TC_RUN(timer_normal_ticks_every_frame);
|
||||||
|
TC_RUN(timer_normal_fight_ticks_every_frame);
|
||||||
|
TC_RUN(timer_fast_keeps_real_time);
|
||||||
|
TC_RUN(timer_fastest_fight_keeps_real_time);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,35 @@ SND_SOURCES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Ноты PC-СПИКЕРА и МЕЛОДИИ — два других набора того же движка. Нужны нам
|
||||||
|
# по разным причинам: мелодии мы играем отдельно (MUS/), а из нот спикера
|
||||||
|
# добираем те номера, которых нет НИ в оцифровке, НИ среди мелодий — сейчас
|
||||||
|
# это ровно звук 38 (мигание надписи «Press Button»). В поставке SDLPoP
|
||||||
|
# наборы лежат распакованными каталогами res1XXXX.bin, в дистрибутиве DOS —
|
||||||
|
# контейнерами .DAT; поддерживаем оба вида.
|
||||||
|
SND_MIDI = {
|
||||||
|
"sdlpop": (SDLPOP_DATA, "MIDISND%d.DAT", 2),
|
||||||
|
"msdos": (MSDOS, "midisnd%d.dat", 2),
|
||||||
|
}
|
||||||
|
SND_SPEAKER = {
|
||||||
|
"sdlpop": (SDLPOP_DATA, "IBM_SND%d", 2),
|
||||||
|
"msdos": (MSDOS, "ibm_snd%d.dat", 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def snd_extra(kind, name="sdlpop"):
|
||||||
|
"""(каталог, шаблон, сколько контейнеров) для набора нот или мелодий.
|
||||||
|
|
||||||
|
kind — "midi" или "speaker". Набора может не быть вовсе (у пользователя
|
||||||
|
нет MSDOS, поставка неполная) — это НЕ ошибка: вызывающий просто
|
||||||
|
пропускает добор. Возвращает None, если источник неизвестен.
|
||||||
|
"""
|
||||||
|
table = {"midi": SND_MIDI, "speaker": SND_SPEAKER}.get(kind)
|
||||||
|
if table is None:
|
||||||
|
raise SystemExit("неизвестный набор %r; есть: midi, speaker" % kind)
|
||||||
|
return table.get(name)
|
||||||
|
|
||||||
|
|
||||||
def snd_src(name="sdlpop"):
|
def snd_src(name="sdlpop"):
|
||||||
"""(каталог, шаблон имени) контейнеров оцифровки для источника."""
|
"""(каталог, шаблон имени) контейнеров оцифровки для источника."""
|
||||||
if name not in SND_SOURCES:
|
if name not in SND_SOURCES:
|
||||||
|
|||||||
@@ -108,6 +108,69 @@ def resample(src, src_rate, dst_rate):
|
|||||||
return bytes(out)
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
def load_bundle(dat_dir, pattern, count):
|
||||||
|
"""id -> тело ресурса из набора любого вида.
|
||||||
|
|
||||||
|
Наборы приезжают в двух видах: контейнером `.DAT` (дистрибутив DOS) и
|
||||||
|
распакованным каталогом `res1XXXX.bin` (поставка SDLPoP). Тело в обоих
|
||||||
|
случаях начинается с байта типа, так что дальше разбор общий.
|
||||||
|
"""
|
||||||
|
out = {}
|
||||||
|
for i in range(1, count + 1):
|
||||||
|
p = dat_dir / (pattern % i)
|
||||||
|
if p.is_dir():
|
||||||
|
for f in sorted(p.glob("res1*.bin")):
|
||||||
|
try:
|
||||||
|
rid = int(f.stem[3:]) - 10000
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
out[rid] = f.read_bytes()
|
||||||
|
elif p.is_file():
|
||||||
|
out.update(dat_resources(p))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def speaker(body):
|
||||||
|
"""(темп, [(частота Гц, длина в долях), ...]) или None — если это не ноты.
|
||||||
|
|
||||||
|
Формат — канон Princed, «Internal PC Speaker»: заголовок 3 байта, из них
|
||||||
|
байт 1 — доли на ДВЕ секунды; дальше тройки «частота (2 байта) + длина
|
||||||
|
(1 байт)»; нулевая частота = пауза, 1 и 2 — маркеры; в конце маркер
|
||||||
|
`12 00`, который сам отсекается недобором до целой тройки.
|
||||||
|
"""
|
||||||
|
if not body or (body[0] & 7) != 0 or len(body) < 6:
|
||||||
|
return None
|
||||||
|
bps = body[1] or 1
|
||||||
|
notes = []
|
||||||
|
i = 3
|
||||||
|
while i + 3 <= len(body):
|
||||||
|
notes.append((struct.unpack("<H", body[i:i + 2])[0], body[i + 2]))
|
||||||
|
i += 3
|
||||||
|
return bps, notes
|
||||||
|
|
||||||
|
|
||||||
|
def speaker_pcm(bps, notes, rate, amp=22):
|
||||||
|
"""Меандр по нотам -> те же 8 бит без знака, что и оцифровка.
|
||||||
|
|
||||||
|
Амплитуда НАМЕРЕННО маленькая: сигналы спикера короткие и резкие, и на
|
||||||
|
полном размахе они колют ухо рядом с оцифровкой, которая заметно тише.
|
||||||
|
Значение снижено вдвое по слуховой проверке в MAME (решение пользователя
|
||||||
|
2026-08-31): на 44 сигнал мигания перекрикивал игру.
|
||||||
|
"""
|
||||||
|
out = bytearray()
|
||||||
|
for freq, beats in notes:
|
||||||
|
n = int(round(beats * (2.0 / bps) * rate))
|
||||||
|
if n <= 0:
|
||||||
|
continue
|
||||||
|
if freq <= 2: # 0 — пауза, 1 и 2 — маркеры
|
||||||
|
out += bytes([0x80]) * n
|
||||||
|
continue
|
||||||
|
period = rate / freq
|
||||||
|
for i in range(n):
|
||||||
|
out.append(0x80 + amp if (i % period) < period / 2 else 0x80 - amp)
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
||||||
ap.add_argument("--source", default="sdlpop", choices=sorted(P.SND_SOURCES),
|
ap.add_argument("--source", default="sdlpop", choices=sorted(P.SND_SOURCES),
|
||||||
@@ -151,6 +214,33 @@ def main():
|
|||||||
if rate != 11000:
|
if rate != 11000:
|
||||||
print(f" звук {sid}: {rate} Гц -> {RATE:g} Гц, {len(samples)} -> {len(pcm)} сэмплов")
|
print(f" звук {sid}: {rate} Гц -> {RATE:g} Гц, {len(samples)} -> {len(pcm)} сэмплов")
|
||||||
|
|
||||||
|
# ДОБОР ИЗ НОТ PC-СПИКЕРА (docs/TASKS_OPEN.md, SND-SPEAKER-38).
|
||||||
|
# У оригинала три параллельных набора звука, и номера разложены по ним
|
||||||
|
# не подряд: оцифровка, мелодии и ноты спикера. Берём из нот ТОЛЬКО те
|
||||||
|
# номера, которых нет ни там, ни там — иначе синтез перекрыл бы собой
|
||||||
|
# мелодии, которые мы играем отдельно, из MUS/. На поставке SDLPoP это
|
||||||
|
# ровно один номер: 38, сигнал под мигание надписи «Press Button»
|
||||||
|
# (31, 34 и 42 в наборе есть, но они пустые заглушки без нот).
|
||||||
|
midi_cfg = P.snd_extra("midi", args.source)
|
||||||
|
spk_cfg = P.snd_extra("speaker", args.source)
|
||||||
|
midi_ids = set(load_bundle(*midi_cfg)) if midi_cfg else set()
|
||||||
|
added = []
|
||||||
|
for sid, body in sorted((load_bundle(*spk_cfg) if spk_cfg else {}).items()):
|
||||||
|
if sid in sounds or sid in midi_ids:
|
||||||
|
continue
|
||||||
|
sp = speaker(body)
|
||||||
|
if sp is None or not sp[1]:
|
||||||
|
continue
|
||||||
|
pcm = speaker_pcm(sp[0], sp[1], RATE)
|
||||||
|
if not pcm:
|
||||||
|
continue
|
||||||
|
sounds[sid] = pcm
|
||||||
|
added.append("%d (%d нот, %d Б)" % (sid, len(sp[1]), len(pcm)))
|
||||||
|
if added:
|
||||||
|
print(" из нот спикера: " + ", ".join(added))
|
||||||
|
elif spk_cfg:
|
||||||
|
print(" из нот спикера: добирать нечего")
|
||||||
|
|
||||||
# Раскладка: начало каждого звука кратно BLOCK, страницы по PAGE.
|
# Раскладка: начало каждого звука кратно BLOCK, страницы по PAGE.
|
||||||
# ПЕРВЫЙ БЛОК — ТИШИНА. Он нужен насосу: когда эффект кончился, в CBL
|
# ПЕРВЫЙ БЛОК — ТИШИНА. Он нужен насосу: когда эффект кончился, в CBL
|
||||||
# всё равно надо что-то отдавать, иначе железо доигрывает по кругу хвост
|
# всё равно надо что-то отдавать, иначе железо доигрывает по кругу хвост
|
||||||
|
|||||||
Reference in New Issue
Block a user