Звуковые эффекты PoP: оцифровка оригинала через CBL

Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота
железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 —
размер блока запроса CBL, поэтому ни один блок не пересекает границу
страницы и проигрыватель не знает слова «стык».

Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих
мест физики и из play_seq.  Тишину льём свою (первый блок набора), а не
через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная.
Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от
pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и
железо крутит хвост буфера — на слух скрежет.

Разведены все места play_sound() SDLPoP, у которых есть оцифровка
(id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и
провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч,
зеркало, скелет, зелье.  Таблица соответствий — docs/sound_plan.md §8.
Музыкальные id остаются с нулевой длиной до фазы музыки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 15:16:43 +03:00
parent 24ced6058f
commit 8ea4c32e51
25 changed files with 561 additions and 137 deletions
+50
View File
@@ -333,3 +333,53 @@ IBM PC 1989 года: один квадратный голос. Он у нас
Если позже захочется богаче — материал уже будет разобран, и можно
разложить те же мелодии на три канала AY (бас/мелодия/арпеджио), не трогая
ни данные, ни секвенсор.
---
## 8. Разводка вызовов по коду (сделано 2026-08-20)
Портированы ВСЕ места `play_sound()` SDLPoP, у которых есть оцифровка
(id 0..23, 44..49, 51 — остальные id это музыка, у них в
`pop_sound_tbl.h` длина 0, и вызов просто глушит текущий эффект).
| id | что | где у нас | оригинал |
|----|-----|-----------|----------|
| 0 | разбился насмерть | `pop_map.c` land | seg005 |
| 1 | крик падения | `pop_map.c` do_fall | seg005:39 |
| 2 | плита рухнула | `pop_room.c` (обе ветки посадки) | seg007 |
| 3 | кнопка нажата | `pop_trob.c` | seg007 |
| 4/5/6/7 | ворота: закрываются / открываются / рухнули / стоп | `pop_trob.c` | seg007 |
| 8 | удар о стену | `pop_map.c` bumped_fall/bumped_floor + seqtbl | seg004/seg006 |
| 9 | зацеп за карниз | `pop_map.c` check_grab | seg006 |
| 11 | свист клинка мимо | `guards.c` check_hurting | seg002:0DAE |
| 12/13 | ранен соперник / Кид | `guards.c` hurt_by_sword | seg002:0C1F |
| 13 | Кид ранен зельем | `pop_map.c` ветка «злого» зелья | seg006:1894 |
| 14/15 | дверь уровня: закрывается / едет | `pop_trob.c` | seg007 |
| 16 | средняя посадка; толчок о стража | `pop_map.c` land / bump_into_opponent | seg005/seg003:0654 |
| 17 | мягкая посадка | `pop_map.c` land | seg005 |
| 18 | пьёт | seqtbl (SEQ_SOUND) | seg006 |
| 19 | вынул меч | `pop_ctrl.c` | seg005:945 |
| 20/21/22 | дрожит плита | `pop_map.c` loose_shake | seg007:0E55 |
| 23 | шаг | seqtbl (SEQ_SOUND) | seg006 |
| 44 | скелет оживает | `guards.c` pop_check_skel | seg002:106D |
| 45 | прыжок в зеркало | `pop_map.c` jump_through_mirror | seg003:0617 |
| 46 | сожрал чомпер | `pop_map.c` | seg004 |
| 47 | чомпер щёлкнул | `pop_trob.c` (кадр 2) | seg007 |
| 48 | напоролся на пики | `pop_map.c` | seg005 |
| 49 | пики пошли | `pop_map.c` start_anim_spike | seg007:08F6 |
Что осталось не разведено — только МУЗЫКА (25 презентация, 26 объятия,
27/35/40 заставки, 29 встреча Джафара, 30/33 зелья, 32/41 конец уровня,
36 время вышло, 37 победа, 43 смерть Джафара, 50/52/53 сюжетные вставки)
и 51 (дверь принцессы, тоже из заставки). Их черёд — фаза «музыка».
**Квирк, за которым следить.** Звук ворот у оригинала звучит не всегда, а
по условию видимости (`play_door_sound_if_visible`, seg007:1250): либо
ворота в комнате слева и стоят в 9-й колонке, либо ворота в НАРИСОВАННОЙ
комнате и колонка не 9-я. У нас это параметр `audible` у `animate_door`.
**Тряска плиты — свой домен prandom.** Оригинал берёт номер сэмпла (20/21/22)
из общего генератора и вдобавок «сжигает» один бросок ради совместимости с
DOS-версией; у нас последовательности разведены по доменам
(`impl_diff.md`), поэтому у тряски свой сид, а холостой бросок не делаем —
на розыгрыши физики и кладки это не влияет.
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
Binary file not shown.
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
+12 -3
View File
@@ -35,7 +35,7 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 3000
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c $(PROF_FLAGS)
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
@@ -57,7 +57,7 @@ POS ?= 2
ifneq ($(strip $(ROOM)),)
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
endif
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid
@@ -120,8 +120,13 @@ SHADOW_DIR := $(CURDIR)/../poc/res/shadow
SHADOW_DATA := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(SHADOW_DIR)/sk$(n).atl) \
$(foreach n,0 1 2 3,$(SHADOW_DIR)/sf$(n).atl)
# ЗВУКОВЫЕ ЭФФЕКТЫ — оригинальная оцифровка, приведённая к частоте CBL
# (10 937,5 Гц, 8 бит моно). 8 страниц по 16 КБ; см. ../docs/sound_plan.md.
SND_DIR := $(CURDIR)/../poc/res/sound
SND_DATA := $(foreach n,0 1 2 3 4 5 6 7,$(SND_DIR)/s$(n).bin)
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
$(VIZIER_DATA) $(SHADOW_DATA) $(LVL_DATA)
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
@@ -139,6 +144,7 @@ hdd: $(EXAMPLE).exe
$(foreach f,$(SKEL_DATA),SKEL:$(f)) \
$(foreach f,$(VIZIER_DATA),VIZIER:$(f)) \
$(foreach f,$(SHADOW_DATA),SHADOW:$(f)) \
$(foreach f,$(SND_DATA),SND:$(f)) \
$(foreach f,$(LVL_DATA),LEVELS:$(f))
TC := $(PROJ_ROOT)/applications/PoP/toolchain
@@ -156,6 +162,9 @@ $(VIZIER_DATA): $(TC)/pop_pack_guard.py
$(SHADOW_DATA) pop_shadow_atlas.h: $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py
cd $(TC) && python3 pop_pack_shadow.py
$(SND_DATA) pop_sound_tbl.h: $(TC)/pop_pack_sound.py
cd $(TC) && python3 pop_pack_sound.py
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
cd $(TC) && python3 pop_pack_kid.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
+26
View File
@@ -0,0 +1,26 @@
/*
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
* публичный — pop_sfx.h). Горячая половина живёт в резиденте
* (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они таблицу
* звуков, массив физических страниц и насос.
*/
#ifndef _POP_SFX_INTERNAL_H
#define _POP_SFX_INTERNAL_H
#include <stdint.h>
#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
extern uint8_t pop_snd_page[POP_SND_PAGES];
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
extern uint8_t pop_snd_ok;
/* Насос: отдать n байт в CBL. Резидентный — его адрес отдаётся в
* cbl_open() холодной половиной. */
int pop_sfx_fill(uint16_t n);
/* Прочитать файл целиком в ФИЗИЧЕСКУЮ страницу (через W3). Резидентный:
* банковый код, переключив W3, вырезал бы сам себя. */
int pop_page_read(const char *path, uint8_t phys, uint16_t want);
#endif
+7 -1
View File
@@ -16,6 +16,7 @@
* дальше работает общий pop_control().
*/
#include "pop_guard.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
@@ -189,6 +190,7 @@ void pop_check_skel(void) __banked
Char.charid = CHARID_4_SKELETON;
Char.fall_x = Char.fall_y = 0;
pop_char_set_seq(SEQ_88_SKEL_WAKE_UP);
pop_sfx_play(44); /* seg002:106D — скелет оживает */
play_seq();
pop_saveshad();
@@ -989,7 +991,9 @@ static void hurt_by_sword(void)
}
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
play_seq(); /* TODO: звук 12/13 */
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
play_seq();
}
/* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника.
@@ -1010,6 +1014,8 @@ static void check_hurting(void)
(of != FRAME_161_PARRY && of != FRAME_150_PARRY)) {
/* Соперник НЕ парирует. */
if (cf == FRAME_154_POKING) {
/* seg002:0DAE — свист клинка мимо цели. */
pop_sfx_play(11);
min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12);
distance = pop_char_opp_dist();
if (distance >= (int16_t)min_range && distance < 29)
+3 -1
View File
@@ -20,6 +20,7 @@
#include "pop_state.h"
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
@@ -405,7 +406,8 @@ static void draw_sword(void)
uint8_t seq_id = SEQ_55_DRAW_SWORD;
control_forward = control_shift2 = release_arrows();
if (Char.charid == CHARID_0_KID) {
offguard = 0; /* TODO: play_sound(19) — звука пока нет */
offguard = 0;
pop_sfx_play(19); /* seg005:945 — меч из ножен */
} else if (Char.charid != CHARID_1_SHADOW) {
seq_id = SEQ_90_EN_GARDE; /* соперник встаёт сразу в стойку */
}
+21 -8
View File
@@ -10,6 +10,7 @@
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_sfx.h" /* звуковые эффекты: опкод SOUND в play_seq */
#include "pop_trob.h" /* pop_start_chompers — смена ряда будит чомперы */
#include "pop_guard.h" /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h" /* POP_YOFF — вертикальное центрирование */
@@ -323,14 +324,26 @@ void play_seq(void)
case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
knock = -1;
break;
case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт
* ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя.
* Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0
* назван silent потому, что НЕ звучит, а стражи его всё
* равно замечают (стоит, например, в seq доставания меча).
* DRINK(3)/LEVEL(4) — не шум. См. BUG-GUARD-DEAF-1. */
if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1;
Char.curr_seq++;
case 0xF2: /* SOUND (seg006:0627). Аргумент — НЕ номер звука игры, а
* одно из пяти событий; отображение взято оттуда же:
* 0 SILENT — не звучит, но стражи Кида ЗАМЕЧАЮТ
* (стоит, например, в seq доставания меча);
* 1 FOOTSTEP — звук 23, и шум;
* 2 BUMP — звук 8, и шум;
* 3 DRINK — звук 18, не шум;
* 4 LEVEL — МУЗЫКА конца уровня (32 на 4-м, иначе 41,
* на 13/15 молчок). Оцифровки у неё нет,
* это путь A на AY — пока тишина.
* Второй смысл опкода («Кид нашумел») терять нельзя даже
* без звука, см. BUG-GUARD-DEAF-1. */
{
uint8_t ev = SEQ(Char.curr_seq);
Char.curr_seq++;
if (ev < 3) is_guard_notice = 1;
if (ev == 1) pop_sfx_play(23);
else if (ev == 2) pop_sfx_play(8);
else if (ev == 3) pop_sfx_play(18);
}
break;
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
* В оригинале это `++next_level`, а главный цикл
+64 -15
View File
@@ -21,6 +21,7 @@
#include "pop_map.h"
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
#include "pop_sfx.h" /* звуковые эффекты (sound_plan.md) */
#include "pop_geom.h" /* общая геометрия (x_bump/y_land/y_to_row) */
#include "pop_tile.h" /* POP_TILE_DIV/MOD — деление на ширину тайла таблицей */
#include "pop_redraw.h" /* пометки перерисовки (порт set_redraw_*) */
@@ -143,6 +144,11 @@ uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заб
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
/* is_screaming (seg005:38): крик падения звучит ОДИН раз за полёт, а не
* каждый кадр. Сбрасывается там же, где у оригинала: приземление (land),
* зацеп за карниз (check_grab) и постановка на старт (set_start_pos). */
static uint8_t is_screaming;
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max; /* стартовое HP Кида (для индикатора) */
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
@@ -666,6 +672,7 @@ static void land(void)
* control_crouched(), а при `sword == sword_2_drawn` управление уходит в
* control_with_sword и до него не доходит — Кид садится в присед
* НАВСЕГДА (BUG-LAND-SWORD-1). */
is_screaming = 0; /* seg005:116 */
if (Char.fall_y < 22) {
soft_land:
if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) {
@@ -674,8 +681,8 @@ static void land(void)
} else {
seq = SEQ_17_SOFT_LAND;
}
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
if (Char.charid == CHARID_0_KID) is_guard_notice = 1;
/* seg005:185 — мягкое приземление слышно И стражам, и игроку. */
if (Char.charid == CHARID_0_KID) { pop_sfx_play(17); is_guard_notice = 1; }
} else {
uint8_t deadly = (uint8_t)(Char.fall_y >= 33); /* СНЯТЬ до обнуления */
if (!deadly && Char.charid == CHARID_1_SHADOW) goto soft_land; /* seg005:189 */
@@ -683,13 +690,14 @@ static void land(void)
Char.fall_x = Char.fall_y = 0;
if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */
pop_take_hp(100);
if (Char.charid == CHARID_0_KID) pop_sfx_play(0); /* разбился */
pop_char_set_seq(SEQ_22_CRUSHED);
play_seq();
determine_col();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
return;
}
if (Char.charid == CHARID_0_KID) is_guard_notice = 1; /* seg005:195 */
if (Char.charid == CHARID_0_KID) { pop_sfx_play(16); is_guard_notice = 1; } /* seg005:195 */
seq = SEQ_20_MEDIUM_LAND;
}
Char.fall_x = Char.fall_y = 0;
@@ -811,7 +819,9 @@ static void check_grab(void)
pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
play_seq();
determine_col();
grab_timer = 12; /* (TODO: sound grab) */
grab_timer = 12;
pop_sfx_play(9); /* seg006 check_grab */
is_screaming = 0; /* seg006:1219 */
}
}
@@ -882,6 +892,10 @@ static uint8_t check_grab_run_jump(void)
static void do_fall(void)
{
uint8_t nrow = (uint8_t)(Char.curr_row + 1);
if (!is_screaming && Char.fall_y >= 31) {
pop_sfx_play(1); /* seg005:39 — крик падения */
is_screaming = 1;
}
if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */
if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) {
check_grab(); /* ещё летит — попытка зацепа */
@@ -1355,6 +1369,8 @@ void pop_proc_get_object(void) __banked
* ставили вспышку руками, то есть красили экран дважды.
* Спецсобытие уровня зелий: там синие склянки забирают ПОЛОВИНУ
* запаса, а не одну единицу. */
pop_sfx_stop(); /* seg006:1893 — stop_sounds */
pop_sfx_play(13);
if (pop_current_level == POP_POTIONS_LEVEL)
hitp_delta = (int8_t)(-((hitp_max + 1) >> 1));
else
@@ -1508,8 +1524,9 @@ static void bumped_fall(void)
play_seq();
}
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
* стену слышен и в свободном падении. Флаг «Кид нашумел» взводится
* вместе со звуком — см. BUG-GUARD-DEAF-1. */
pop_sfx_play(8);
is_guard_notice = 1;
}
@@ -1560,7 +1577,8 @@ static void bumped_floor(void)
else
pop_char_set_seq(SEQ_47_BUMP);
play_seq();
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
pop_sfx_play(8); /* bumped_sound (seg004:05F1) */
is_guard_notice = 1;
}
/* bump_into_opponent (seg003:08AA). Зовётся из play_kid_frame (seg000:1238)
@@ -1593,6 +1611,7 @@ static void bump_into_opponent(void)
if (distance > 15) return;
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
pop_sfx_play(16); /* seg003:0654 — толчок о стража */
pop_char_set_seq(SEQ_47_BUMP);
play_seq();
}
@@ -2367,6 +2386,28 @@ static void check_press(void)
}
}
/* ---- Звук дрожащей плиты (loose_shake, seg007:0E55) ------------------ *
* Три сэмпла 20/21/22 вперемешку, но НЕ на каждый кадр фазы: гейт-таблица
* loose_sound (data:2734) даёт звук на фазах 1,2,3,5,8, а на остальных
* молчит — иначе тряска трещит без пауз. Повтор подряд одного и того же
* сэмпла оригинал запрещает (цикл do/while), иначе слышно «заедание».
*
* Сид СВОЙ, а не общий с trob/боёвкой: домены prandom у нас разведены
* (../docs/impl_diff.md), и звук не должен сдвигать чужие розыгрыши. */
static const uint8_t loose_sound[12] = { 0,1,1,1,0,1,0,0,1,0,0,0 };
static pop_rnd_t loose_seed;
static uint8_t last_loose_snd;
static void loose_shake(uint8_t force, uint8_t modif)
{
uint8_t id;
if (!force && !loose_sound[modif & 0x7F]) return;
do { id = (uint8_t)(pop_prandom(&loose_seed, 2) + 20); }
while (id == last_loose_snd);
last_loose_snd = id;
pop_sfx_play(id);
}
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
* приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
* modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick). Трясём
@@ -2718,11 +2759,13 @@ void pop_loose_tick(void) __banked
/* ждём молча */
} else if (m >= 0x84) { /* конец тряски: сброс + покой */
*lm = 0;
loose_shake(1, 0); /* последний удар — без гейта */
/* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
* правая грань loose живёт в соседе). ОБЕ страницы —
* иначе на одной застывает дрожащий кадр → мерцание. */
pop_set_redraw(pos, POP_RD_LOOSE, 2);
} else {
loose_shake(0, m);
pop_set_redraw(pos, POP_RD_LOOSE, 1); /* дрожащий кадр */
}
} else if (m >= 11 && pop_loose_fell) {
@@ -2753,6 +2796,7 @@ void pop_loose_tick(void) __banked
pop_loose_mob_spawn(row, col); /* отрыв падающего куска */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению */
} else { /* отсчёт 1..10 — дрожащий кадр */
loose_shake(0, m);
pop_set_redraw(pos, POP_RD_LOOSE, 1);
}
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
@@ -2774,9 +2818,10 @@ void pop_loose_tick(void) __banked
* цикл, а не соседний. */
if (pop_current_level == POP_LOOSE_TILES_LEVEL) {
/* ждём молча */
} else if (m >= 0x84) { *lm = 0;
} else if (m >= 0x84) { *lm = 0; loose_shake(1, 0);
pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
else pop_set_redraw_above(col, POP_RDA_CEIL, 1);
else { loose_shake(0, m);
pop_set_redraw_above(col, POP_RDA_CEIL, 1); }
} else if (m >= 11 && pop_ceil_fell) {
*lm = 10; /* сигнал занят — кадр подождём */
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
@@ -2786,6 +2831,7 @@ void pop_loose_tick(void) __banked
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
} else {
loose_shake(0, m);
pop_set_redraw_above(col, POP_RDA_CEIL, 1); /* отсчёт 1..10 */
}
if (*lm) found = 1; /* фаза ещё жива — гейт держим */
@@ -3005,9 +3051,10 @@ static void start_anim_spike(uint8_t tilepos)
if (!mod) return;
old = (int8_t)mod[tilepos];
if (old <= 0) {
if (old == 0)
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
else if ((uint8_t)old != 0xFF)
if (old == 0) {
pop_add_trob(g_room, tilepos, 1);
pop_sfx_play(49); /* seg007:08F6 — пики пошли */
} else if ((uint8_t)old != 0xFF)
mod[tilepos] = 0x8F;
}
}
@@ -3032,7 +3079,8 @@ static void spiked(uint8_t tilepos)
Char.x = (uint8_t)(pop_x_bump[Char.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Char.x = (uint8_t)char_dx_forward(8);
Char.fall_y = 0;
pop_take_hp(100); /* (TODO: sound_48_spiked) */
pop_sfx_play(48);
pop_take_hp(100);
pop_char_set_seq(SEQ_51_SPIKED);
play_seq();
determine_col();
@@ -3104,7 +3152,8 @@ static void chomped(uint8_t tilepos, int8_t col)
Char.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX);
Char.x = (uint8_t)char_dx_forward(7 - !Char.direction);
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
pop_take_hp(100); /* (TODO: sound_46_chomped) */
pop_sfx_play(46);
pop_take_hp(100);
pop_char_set_seq(SEQ_54_CHOMPED);
play_seq();
if (Char.charid == CHARID_0_KID) pop_kid_dead = 1;
@@ -3312,7 +3361,7 @@ static void jump_through_mirror(void)
(void)mirror_image();
pop_jumped_mirror = 0;
Char.charid = CHARID_1_SHADOW;
/* play_sound(45) — звука в порте пока нет */
pop_sfx_play(45); /* seg003:0617 — прыжок в зеркало */
pop_saveshad();
guardhp_max = guardhp_curr = hitp_max;
hitp_curr = 1;
+3
View File
@@ -17,6 +17,7 @@
#include <fcntl.h>
#include <unistd.h>
#include "pop_bg.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */
#include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */
@@ -1493,6 +1494,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* КНОПКА под куском обязаны сработать — сигналим главному циклу
* комнатой и тайлом (loose_land, seg007:11E8, работает с
* curmob.room, а не с отрисованной). */
pop_sfx_play(2); /* tile crashing, seg007:1041 */
pop_loose_exit_room = mt_m->room;
pop_loose_exit = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
/* clean = 2 даже для чужой комнаты: пока кусок летел, он мог
@@ -1504,6 +1506,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg)
* подержим кусок на границе и приземлим следующим кадром */
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
} else {
pop_sfx_play(2); /* tile crashing, seg007:1041 */
mt_m->y = MOB_Y_BOUND[mt_m->row + 1];
pop_loose_landed = (uint8_t)(mt_m->row * 10 + mt_m->col + 1);
mt_m->active = 0; mt_m->clean = 2; /* кусок исчез — на его месте debris */
+121
View File
@@ -0,0 +1,121 @@
/*
* pop_sfx.c — ГОРЯЧАЯ часть звуковых эффектов: насос в прерывании CBL,
* запуск/останов и таблица звуков. Зачем всё это и в каком виде лежат
* данные — в шапке pop_sfx.h и ../docs/sound_plan.md.
*
* ПОЧЕМУ МОДУЛЬ РЕЗИДЕНТНЫЙ (в EXTRA_SRCS, а не в банке). Три причины,
* и каждой хватило бы одной:
* - callback насоса зовётся ИЗ ПРЕРЫВАНИЯ; трамплин перед вызовом
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
* - pop_page_read маппит W3 на время чтения, а банковый код сам живёт
* в W3 и вырезал бы себя из-под ног (та же причина, по которой
* резидентен pop_level_read_file, см. _pop_level.h).
* Холодная половина (аллокация страниц, 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;
/* Курсор проигрывания. 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;
sfx_pg++;
}
sfx_left -= n;
return 1;
}
/* ---- запуск / останов ------------------------------------------------ */
void pop_sfx_play(uint8_t id)
{
uint8_t pg;
uint16_t off, len;
if (!pop_snd_ok || id >= POP_SND_COUNT) return;
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; /* len == 0 — просто глушим */
IRQ_ENABLE();
}
void pop_sfx_stop(void)
{
IRQ_DISABLE();
sfx_left = 0;
IRQ_ENABLE();
}
/* ---- чтение файла в ФИЗИЧЕСКУЮ страницу ------------------------------ *
* Резидентно по причине из шапки: маппим W3, читаем, возвращаем окно.
* Ровно то же делает atlas_load, поэтому вызов из банка безопасен.
* 0 — прочитано ожидаемое число байт, -1 — нет. */
int pop_page_read(const char *path, uint8_t phys, uint16_t want)
{
uint8_t saved_w3;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
saved_w3 = _io_page_w3;
sprinter_page_w3(phys);
n = read(fd, (uint8_t *)0xC000, want);
_io_page_w3 = saved_w3;
close(fd);
return (n == (int)want) ? 0 : -1;
}
+12 -3
View File
@@ -21,11 +21,20 @@
#include <stdint.h>
/* Поднять звук: занять 8 EMM-страниц, вычитать в них набор, открыть CBL.
* 0 — OK, -1 — не получилось (тогда pop_sfx_play молча ничего не делает,
* игра идёт без звука). Звать один раз при старте. */
/* Поднять набор: занять 8 EMM-страниц и вычитать в них данные. ВЫВОД НЕ
* ВКЛЮЧАЕТ — см. pop_sfx_start. 0 — OK, -1 — не получилось (тогда всё
* дальнейшее молча ничего не делает, игра идёт без звука). */
int pop_sfx_init(void) __banked;
/* Включить вывод (открыть CBL). Звать ПОСЛЕ всей загрузки: пока идёт
* чтение файлов, ESTEX уходит в диск надолго, насос не успевает долить
* блок, и железо крутит хвост своего буфера — на слух сплошной скрежет.
* Идемпотентно. */
int pop_sfx_start(void) __banked;
/* Заглушить вывод на время загрузки (закрывает CBL). Парная к start. */
void pop_sfx_pause(void) __banked;
/* Закрыть CBL и отпустить страницы; звать на выходе. */
void pop_sfx_close(void) __banked;
+76
View File
@@ -0,0 +1,76 @@
/*
* pop_sfx_cold.c — ХОЛОДНАЯ половина звука: поднять набор и открыть CBL.
* Отрабатывает один раз за запуск, поэтому живёт в банке 8; горячая
* половина (насос, запуск эффекта) — в резиденте, см. pop_sfx.c.
*/
#include <stdint.h>
#include <sprinter_mem.h>
#include <cbl.h>
#include "pop_sfx.h"
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
#include "_pop_sfx.h"
static uint8_t snd_blk; /* блок EMM под весь набор */
int pop_sfx_init(void) __banked
{
uint8_t i, blk;
if (pop_snd_ok) return 0;
/* Одним блоком: страницы блока идут подряд по логическому индексу, и
* таблица звуков адресует их именно так (page = смещение >> 14). */
blk = mem_alloc_pages(POP_SND_PAGES);
if (!blk) return -1;
for (i = 0; i < POP_SND_PAGES; i++) {
pop_snd_page[i] = mem_get_page(blk, i);
if (!pop_snd_page[i] ||
pop_page_read(pop_snd_files[i], pop_snd_page[i], 16384) != 0) {
mem_free_block(blk);
return -1;
}
}
snd_blk = blk;
return 0;
}
/* ЗАПУСТИТЬ ВЫВОД — ОТДЕЛЬНО ОТ ЗАГРУЗКИ, и это не косметика.
*
* Пока идёт чтение файлов, CBL держать открытым НЕЛЬЗЯ: ESTEX уходит в
* диск надолго, насос не успевает долить очередные 128 байт, и железо
* крутит по кругу хвост своего 256-байтового буфера. На слух это
* сплошной скрежет (поймано пользователем на старте, 2026-08-20).
* Поэтому загрузка страниц и открытие CBL разведены: грузим молча,
* включаем звук последним действием, а на время загрузки уровня глушим
* (pop_sfx_pause).
*
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
* UNDERRUN_APP, а не SILENCE: тишину насос льёт СВОЮ (первый блок
* набора), и просить у libc буфер через malloc не надо — куча в
* резиденте W2 тесная, и её отказ ронял бы весь звук. */
int pop_sfx_start(void) __banked
{
if (pop_snd_ok) return 0;
if (!snd_blk) return -1; /* набор не загружен */
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, pop_sfx_fill) != 0)
return -1;
pop_snd_ok = 1;
return 0;
}
void pop_sfx_pause(void) __banked
{
if (!pop_snd_ok) return;
pop_sfx_stop();
cbl_close();
pop_snd_ok = 0;
}
void pop_sfx_close(void) __banked
{
pop_sfx_pause();
if (snd_blk) { mem_free_block(snd_blk); snd_blk = 0; }
}
+40 -31
View File
@@ -20,35 +20,38 @@
#define POP_SND_PAGES 8
#define POP_SND_COUNT 57
#define POP_SND_BLOCK 128
/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */
#define POP_SND_SILENCE_PAGE 0
#define POP_SND_SILENCE_OFF 0x0000
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
{ 0, 0x0000, 1664 }, /* 0 */
{ 0, 0x0680, 12032 }, /* 1 */
{ 0, 0x3580, 6272 }, /* 2 */
{ 1, 0x0E00, 1280 }, /* 3 */
{ 1, 0x1300, 1152 }, /* 4 */
{ 1, 0x1780, 1408 }, /* 5 */
{ 1, 0x1D00, 5504 }, /* 6 */
{ 1, 0x3280, 896 }, /* 7 */
{ 1, 0x3600, 1664 }, /* 8 */
{ 1, 0x3C80, 2816 }, /* 9 */
{ 2, 0x0780, 4992 }, /* 10 */
{ 2, 0x1B00, 1280 }, /* 11 */
{ 2, 0x2000, 1280 }, /* 12 */
{ 2, 0x2500, 3840 }, /* 13 */
{ 2, 0x3400, 7552 }, /* 14 */
{ 3, 0x1180, 17664 }, /* 15 */
{ 4, 0x1680, 4736 }, /* 16 */
{ 4, 0x2900, 4224 }, /* 17 */
{ 4, 0x3980, 6912 }, /* 18 */
{ 5, 0x1480, 2816 }, /* 19 */
{ 5, 0x1F80, 1408 }, /* 20 */
{ 5, 0x2500, 1152 }, /* 21 */
{ 5, 0x2980, 1280 }, /* 22 */
{ 5, 0x2E80, 1408 }, /* 23 */
{ 0, 0x0080, 1664 }, /* 0 */
{ 0, 0x0700, 12032 }, /* 1 */
{ 0, 0x3600, 6272 }, /* 2 */
{ 1, 0x0E80, 1280 }, /* 3 */
{ 1, 0x1380, 1152 }, /* 4 */
{ 1, 0x1800, 1408 }, /* 5 */
{ 1, 0x1D80, 5504 }, /* 6 */
{ 1, 0x3300, 896 }, /* 7 */
{ 1, 0x3680, 1664 }, /* 8 */
{ 1, 0x3D00, 2816 }, /* 9 */
{ 2, 0x0800, 4992 }, /* 10 */
{ 2, 0x1B80, 1280 }, /* 11 */
{ 2, 0x2080, 1280 }, /* 12 */
{ 2, 0x2580, 3840 }, /* 13 */
{ 2, 0x3480, 7552 }, /* 14 */
{ 3, 0x1200, 17664 }, /* 15 */
{ 4, 0x1700, 4736 }, /* 16 */
{ 4, 0x2980, 4224 }, /* 17 */
{ 4, 0x3A00, 6912 }, /* 18 */
{ 5, 0x1500, 2816 }, /* 19 */
{ 5, 0x2000, 1408 }, /* 20 */
{ 5, 0x2580, 1152 }, /* 21 */
{ 5, 0x2A00, 1280 }, /* 22 */
{ 5, 0x2F00, 1408 }, /* 23 */
{ 0, 0x0000, 0 }, /* 24 */
{ 0, 0x0000, 0 }, /* 25 */
{ 0, 0x0000, 0 }, /* 26 */
@@ -69,14 +72,14 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
{ 0, 0x0000, 0 }, /* 41 */
{ 0, 0x0000, 0 }, /* 42 */
{ 0, 0x0000, 0 }, /* 43 */
{ 5, 0x3400, 2688 }, /* 44 */
{ 5, 0x3E80, 4224 }, /* 45 */
{ 6, 0x0F00, 4608 }, /* 46 */
{ 6, 0x2100, 5120 }, /* 47 */
{ 6, 0x3500, 5120 }, /* 48 */
{ 7, 0x0900, 4480 }, /* 49 */
{ 5, 0x3480, 2688 }, /* 44 */
{ 5, 0x3F00, 4224 }, /* 45 */
{ 6, 0x0F80, 4608 }, /* 46 */
{ 6, 0x2180, 5120 }, /* 47 */
{ 6, 0x3580, 5120 }, /* 48 */
{ 7, 0x0980, 4480 }, /* 49 */
{ 0, 0x0000, 0 }, /* 50 */
{ 7, 0x1A80, 4864 }, /* 51 */
{ 7, 0x1B00, 4864 }, /* 51 */
{ 0, 0x0000, 0 }, /* 52 */
{ 0, 0x0000, 0 }, /* 53 */
{ 0, 0x0000, 0 }, /* 54 */
@@ -84,6 +87,11 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
{ 0, 0x0000, 0 }, /* 56 */
};
/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная
* половина): иначе копия строк уезжает в каждый включивший заголовок
* модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же
* приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */
#ifdef POP_SND_FILES
static const char *const pop_snd_files[POP_SND_PAGES] = {
"SND\\s0.bin",
"SND\\s1.bin",
@@ -94,5 +102,6 @@ static const char *const pop_snd_files[POP_SND_PAGES] = {
"SND\\s6.bin",
"SND\\s7.bin",
};
#endif
#endif
+21 -10
View File
@@ -4,6 +4,7 @@
* См. pop_trob.h.
*/
#include "pop_trob.h"
#include "pop_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
#include "pop_state.h" /* pop_leveldoor_open */
#include "pop_level.h"
#include "pop_geom.h"
@@ -265,6 +266,7 @@ static void animate_chomper(uint8_t *m, int8_t *type,
frame = (uint8_t)((*m & 0x7F) + 1);
if (frame > POP_CHOMPER_SPEED) frame = 1;
*m = (uint8_t)(blood | frame);
if (frame == 2) pop_sfx_play(47); /* щелчок челюстей, seg007:297 */
if ((room != drawn_room || (int8_t)TP_ROW[tp] != Kid.curr_row ||
(Kid.alive >= 0 && blood == 0)) && frame >= 6)
*type = -1;
@@ -463,8 +465,9 @@ void pop_trigger_button(uint8_t room, uint8_t tilepos,
set_doorlink_timer(modifier, POP_DBG_GATE_HOLD);
if (link_timer < 2) {
pop_add_trob(room, tilepos, 1); /* кнопка нажимается */
/* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у
* нас нет, флаг обязан быть (BUG-GUARD-DEAF-1). */
/* seg007:766: щелчок кнопки — шум, и страж оборачивается
* (флаг обязателен даже без звука, BUG-GUARD-DEAF-1). */
pop_sfx_play(3);
is_guard_notice = 1;
}
do_trigger_list(modifier, button_type);
@@ -523,10 +526,13 @@ static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
uint8_t sp;
if (*type < 7) (*type)++;
sp = leveldoor_close_speeds[*type - 3];
if (sp >= *m) { *m = 0; *type = -1; }
else *m = (uint8_t)(*m - sp);
if (sp >= *m) { *m = 0; *type = -1; pop_sfx_play(14); } /* seg007:437 */
else {
*m = (uint8_t)(*m - sp);
if (*type == 4) pop_sfx_play(15); /* seg007:443 */
}
} else {
if (*m < 43) (*m)++;
if (*m < 43) { (*m)++; pop_sfx_play(15); } /* seg007:465 */
if (*m >= 43) {
*m = 43; *type = -1;
/* Условие оригинала — переход ИЗ 0 или 2, то есть ровно один раз
@@ -546,7 +552,7 @@ static void animate_leveldoor(uint8_t *m, int8_t *type, uint8_t cur_room)
static const uint8_t gate_close_speeds[9] = {0, 0, 0, 20, 40, 60, 80, 100, 120};
static const int8_t door_delta[3] = {-1, 4, 4};
static void animate_door(uint8_t *m, int8_t *type)
static void animate_door(uint8_t *m, int8_t *type, uint8_t audible)
{
int8_t anim_type = *type;
uint8_t mod = *m;
@@ -555,18 +561,23 @@ static void animate_door(uint8_t *m, int8_t *type)
if (anim_type < 8) { anim_type++; *type = anim_type; }
{
int new_mod = (int)mod - gate_close_speeds[anim_type];
if (new_mod < 0) { mod = 0; *type = -1; }
if (new_mod < 0) { mod = 0; *type = -1; pop_sfx_play(6); }
else mod = (uint8_t)new_mod;
}
} else if (mod != 0xFF) {
mod = (uint8_t)(mod + door_delta[anim_type]);
if (anim_type == 0) { /* закрытие */
if (mod == 0) *type = -1; /* gate_stop */
/* seg007:376: скрип закрывающейся решётки — раз в четыре
* шага модификатора и ТОЛЬКО если комната на экране
* (play_door_sound_if_visible), иначе на 13-м уровне
* скрипели бы решётки из соседних комнат. */
else if (mod < 188 && (mod & 3) == 3 && audible) pop_sfx_play(4);
} else { /* открытие */
if (mod >= 188) {
if (anim_type < 2) { mod = 238; *type = 0; } /* открыт → потом закрывать */
if (anim_type < 2) { mod = 238; *type = 0; pop_sfx_play(7); }
else { mod = 0xFF; *type = -1; } /* насовсем */
}
} else if ((mod & 7) == 0) pop_sfx_play(5); /* seg007:386 */
}
} else {
*type = -1;
@@ -672,7 +683,7 @@ void pop_process_trobs(uint8_t cur_room) __banked
animate_button(mod[tp], &type); /* mod[tp] = индекс LINKLOC кнопки */
break;
case TILE_GATE:
animate_door(&mod[tp], &type);
animate_door(&mod[tp], &type, (uint8_t)(room == cur_room));
break;
case TILE_LOOSE:
animate_loose(&mod[tp], &type, room, tp, cur_room);
+3 -53
View File
@@ -282,59 +282,9 @@ int main(void)
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K — убить стража;
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор);
* Shift+L — следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift — отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
* персонажа в этом порте задаёт не диспетчер (грабли
* L1-EXIT). Колонку не трогаем — её пересчитает физика
* следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
if (pop_cheats && i && !immo_prev)
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
* Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */
if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */
pop_upside_want = (uint8_t)!pop_upside_want;
pop_upside_dirty = 1;
}
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k;
immo_prev = i;
sword_prev = s;
nlvl_prev = nl;
upsd_prev = up;
xdec_prev = xd;
xinc_prev = xi;
}
/* Читы В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
pop_cheat_tick();
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
/* Заморозка — тоже полноценный логический кадр: иначе якорь
+73
View File
@@ -24,6 +24,7 @@
#include "pop_tune.h"
#include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */
#include "pop_shadow.h" /* запечённый набор Тени */
#include "pop_sfx.h" /* звуковые эффекты через CBL */
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
@@ -308,6 +309,74 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
return 1;
}
/* ---- ЧИТЫ (seg000: switch под if (cheats_enabled)) -------------------- *
* Переехали из главного цикла в банк 8: дёргаются раз в кадр, то есть
* один трамплин на кадр, а места в резиденте W1/W2 не осталось (звук,
* атлас Тени и пейсинг съели остаток; см. TASKS_OPEN, MEM).
*
* Флаги «фронта» стали статиками модуля — раньше они были локалями main.
* Смысл прежний: одно нажатие = одно срабатывание.
*/
static uint8_t cheat_prev, immo_prev, sword_prev, nlvl_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev;
void pop_cheat_tick(void) __banked
{
/* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K — убить стража;
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор);
* Shift+L — следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift — отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
* персонажа в этом порте задаёт не диспетчер (грабли
* L1-EXIT). Колонку не трогаем — её пересчитает физика
* следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
if (pop_cheats && i && !immo_prev)
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
* Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */
if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */
pop_upside_want = (uint8_t)!pop_upside_want;
pop_upside_dirty = 1;
}
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k;
immo_prev = i;
sword_prev = s;
nlvl_prev = nl;
upsd_prev = up;
xdec_prev = xd;
xinc_prev = xi;
}
/* ---- ОТЛАДКА: состояние прогона в верхнем борте ---------------------- *
*
* Четыре блока ПАЛОЧКАМИ, каждый своим цветом (outtextxy не годится: текст
@@ -766,6 +835,7 @@ int pop_boot(void) __banked
pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed");
return -1;
@@ -830,6 +900,7 @@ int pop_level_switch(void) __banked
* дальше, а урон — нет (do_startpos дольёт до потолка). */
hitp_beg_lev = hitp_max;
pop_checkpoint = 0; /* уровень пройден (seg003:413) */
pop_sfx_pause(); /* загрузка уровня — ESTEX надолго, звук глушим */
if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else {
@@ -861,6 +932,7 @@ int pop_level_switch(void) __banked
* и делать это в момент глотка нельзя. Звать можно откуда
* угодно — функция идемпотентна. */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
pop_sfx_start(); /* ...и обратно, когда всё прочитано */
}
return 0;
}
@@ -872,6 +944,7 @@ void pop_shutdown(void) __banked
closegraph();
pop_bg_free();
pop_kid_free();
pop_sfx_close();
pop_shadow_free();
pop_guard_free();
pop_kid_data_free();
@@ -66,6 +66,10 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked;
* звать можно каждый кадр. */
void pop_dbg_room_label(uint8_t room) __banked;
/* Читы по фронту нажатия (K/I/S/Shift+L/U/[/]). В банке: раз в кадр,
* а резидент W1/W2 переполнен — см. шапку функции. */
void pop_cheat_tick(void) __banked;
/* Кто рисуется ПОЗЖЕ — Кид или соперник (порядок обхода тайлов оригинала,
* set_objtile_at_char + sort_curr_objs). 1 = соперник поверх. Зовётся раз
* в кадр, поэтому живёт в банке: трамплин на кадр дешевле полукилобайта
+15 -2
View File
@@ -117,7 +117,12 @@ def main():
print(f" звук {sid}: {rate} Гц -> {RATE:g} Гц, {len(samples)} -> {len(pcm)} сэмплов")
# Раскладка: начало каждого звука кратно BLOCK, страницы по PAGE.
blob = bytearray()
# ПЕРВЫЙ БЛОК — ТИШИНА. Он нужен насосу: когда эффект кончился, в CBL
# всё равно надо что-то отдавать, иначе железо доигрывает по кругу хвост
# своего буфера (жужжание). Держим тишину СВОЮ, а не просим у libc
# (CBL_UNDERRUN_SILENCE делает malloc), потому что куча в резиденте W2
# тесная и её отказ ронял бы весь звук.
blob = bytearray([0x80] * BLOCK)
table = {}
for sid in sorted(sounds):
pad = (-len(blob)) % BLOCK
@@ -158,6 +163,9 @@ def main():
"#define POP_SND_PAGES %d" % npages,
"#define POP_SND_COUNT %d" % NSOUND,
"#define POP_SND_BLOCK %d" % BLOCK,
"/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */",
"#define POP_SND_SILENCE_PAGE 0",
"#define POP_SND_SILENCE_OFF 0x0000",
"",
"typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;",
"",
@@ -170,10 +178,15 @@ def main():
lines += [
"};",
"",
"/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная",
" * половина): иначе копия строк уезжает в каждый включивший заголовок",
" * модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же",
" * приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */",
"#ifdef POP_SND_FILES",
"static const char *const pop_snd_files[POP_SND_PAGES] = {",
]
lines += [' "SND\\\\s%d.bin",' % i for i in range(npages)]
lines += ["};", "", "#endif", ""]
lines += ["};", "#endif", "", "#endif", ""]
with open(HDR, "w") as f:
f.write("\n".join(lines))