Катсцены: тайминги, часы и «время вышло» — по оригиналу; тумблер музыки

ТАЙМИНГИ. Треки заставок между уровнями длиннее самих сцен (27 — 10,7 с
против 2,6 с картинки), и оригинал их не обрывает: load_intro гасит экран
и возвращает управление, а звук продолжает играть поверх загрузки уровня
и первых секунд игры (seg001:690). Мы глушили CBL на выходе — музыка
обрывалась на полуслове. Больше не глушим.

«ВРЕМЯ ВЫШЛО». Оригинал (seg001:04D3) не зовёт ни одного init_*: на
экране пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка
нет). Мы показывали готовую композицию с принцессой и полными часами —
противоположное по смыслу. Теперь это живая сцена без персонажей,
102 кадра, и трек 36 дослушивается под чёрным экраном, как в оригинале.

ПЕСОЧНЫЕ ЧАСЫ. Сцена «времени мало» (cutscene_12) шла статической
композицией, куда упаковщик запёк res953 — ПОЛНЫЕ часы. Отсюда и
наблюдение «чем меньше времени, тем полнее часы»: картинка была одна и та
же независимо от таймера. Сделана живой (cut_scene_12_short: принцесса
стоит правее, через два кадра оборачивается), часы берутся от
pre_hourglass_state. Заодно: при state 7 струйка песка не рисуется.

МУЗЫКА — СВОЙ ТУМБЛЕР. Settings -> MUSIC, Ctrl+M и статус в debug bar.
Флаг pop_mus_want отдельный от звукового: музыка у нас поток с диска, и
выключают её по другим причинам, чем эффекты; выключение обрывает текущий
трек, эффекты продолжают звучать. Строка-заглушка GAMEPLAY PROFILE
(«VANILLA ONLY») уступила место MUSIC — на десятую строку экрана не
хватает. Подписи тумблеров в debug bar сокращены до S:/M:/I:.

БАГИ. После меню персонаж оставался невидимым до первого движения:
снимок слота продолжал утверждать, что кадр на странице уже нарисован.
Общая pop_cd_forget() теперь зовётся после меню, QuickLoad и переворота
экрана. «QUICKLOAD» мигал через кадр — pop_status_show_now снимал бит
видимой страницы, а её успевало переписать восстановление комнаты.

Host-тесты: 15 наборов. Проверено в MAME: debug bar, выход из меню.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:28:36 +03:00
parent 0086ac80c2
commit 4a939e42f8
17 changed files with 257 additions and 78 deletions
+32
View File
@@ -1206,6 +1206,38 @@ N навык M» — отдельная надобность, если пона
Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке.
### <a id="pal-arc"></a>PAL-ARC. Палитры — в архив своей группы (полишинг)
Файлов палитр в игре шесть: `KID\kid.pal` (1024 Б), `PV\story.pal`,
`PV\pv.pal` (по 1024), `BG\pop_tile.pal`, `BG\pal_tile.pal` (по 128),
`TITLE\title.pal` (64). Всё, что лежит в `poc/res/tiles/` (36 файлов) и
`bg/pop_bg.pal`, на образ не попадает и кодом не читается — осадок ранних
экспериментов, его стоит просто убрать.
**Что предлагается.** Класть палитру в архив ТОЙ ЖЕ группы, где её
графика: `kid.pal``kid.arc`, оба `*_tile.pal``bg.arc`, `title.pal`
`title.arc`, `story`/`pv.pal``pv.arc`. Тогда правило одно на все
ресурсы («файл лежит в архиве своей группы»), и уходят последние шесть
открытий: `pop_bg_load` берёт тайлсет и палитру за одно открытие, заставка
так же. Отдельно ценно, что `kid.pal` перестанет читаться своим `open` на
КАЖДОЙ загрузке уровня (её перезаливает `pop_pal_game_load`).
**Чего это стоит.** Элемент архива читается только в EMM-страницу,
поэтому нужна маленькая функция «палитра из страницы»: `bank_read` по 256
байт в буфер и четыре `gfx_pal_load`. Буфер уже есть — `pop_pal_buf` по
0x4000.
**Что теряется.** Возможность подменить палитру файлом рядом с игрой
(`pal_file_load` сейчас откатывается на `a:\kid.pal`). Решить явно:
либо файл остаётся приоритетным и архив — запасным путём, либо fallback
выбрасывается.
**Почему НЕ отдельная папка `PAL\` и не один общий атлас палитр.** Папка
не уменьшает числа файлов и рвёт связь «палитра ↔ её графика». Общий
атлас экономил бы открытия только при загрузке всех палитр разом, а они
нужны в разное время: `kid.pal` — каждому уровню, заставочные — только
заставке.
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг с диска
Трек 56 (`won`, 115 с) — это 1,2 МБ, 78 страниц EMM. Ни в `POP_MUS_PAGES`
+4 -3
View File
@@ -14,11 +14,12 @@
#define POP_CFG_VERSION 1
#define POP_CFG_HEADER_SIZE 7
/* Ранний MS0-файл имел семь полей. Его продолжаем принимать: отсутствующие
* поля берут исторический дефолт (лимит времени ON, отладочная полоса ON),
* а новая запись содержит все девять. */
* поля берут исторический дефолт (лимит времени ON, отладочная полоса ON,
* музыка ON), а новая запись содержит все десять. */
#define POP_CFG_LEGACY_PAYLOAD_SIZE 7
#define POP_CFG_TIMELIMIT_PAYLOAD_SIZE 8
#define POP_CFG_PAYLOAD_SIZE 9
#define POP_CFG_DEBUGBAR_PAYLOAD_SIZE 9
#define POP_CFG_PAYLOAD_SIZE 10
#define POP_CFG_TAIL_SIZE 2
/* CFG остаётся маленьким. Верхняя граница не даёт ручному мусору занять
* стек W2, но оставляет запас для будущих полей версии 1. */
+19
View File
@@ -420,6 +420,25 @@ static void cd_bbox(uint8_t who, uint8_t p, int *x0, int *y0, int *x1, int *y1)
}
}
/* ЗАБЫТЬ, ЧТО НАРИСОВАНО, на ОБЕИХ страницах.
*
* Звать после всего, что переписало экран мимо кадрового конвейера: меню,
* QuickLoad, переворот картинки. Иначе снимок слота продолжает утверждать,
* что нужный кадр персонажа на странице уже есть, и cd_quiet честно
* пропускает отрисовку — персонаж исчезает до первого своего движения
* (поймано пользователем на выходе из меню; рядом с факелом бага не видно
* ровно потому, что анимация фона метит прямоугольник Кида и пропуск
* снимается сама собой). */
void pop_cd_forget(void) __banked
{
uint8_t i;
for (i = 0; i < POP_CH_N; i++) {
pop_cd[i].valid[0] = pop_cd[i].valid[1] = 0;
pop_cd[i].ovalid[0] = pop_cd[i].ovalid[1] = 0;
}
}
/* «Тихий» слот: спрайт этой страницы цел (heal его не трогал), фон в его
* прямоугольнике не трогали, и все входы отрисовки те же, что в момент,
* когда этот кадр на эту страницу и рисовали. valid[p] здесь ключевой: он
+5
View File
@@ -79,6 +79,11 @@ typedef struct {
extern pop_cdraw_t pop_cd[POP_CH_N];
/* Забыть, что нарисовано на обеих страницах: после меню, QuickLoad и любой
* другой перерисовки экрана мимо кадрового конвейера. Без этого пропуск
* перерисовки «тихого» персонажа оставляет его невидимым. */
void pop_cd_forget(void) __banked;
/* Стереть прошлый кадр персонажа (спрайт + накладные) на ТЕКУЩЕЙ странице. */
void pop_char_heal(uint8_t who) __banked;
+3
View File
@@ -27,6 +27,9 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ
/* Ctrl+S (PS/2 set 2: S = 0x1B) — вкл/выкл звук; порт Ctrl+S из SDLPoP
* (seg000:657). С Ctrl, а не голая S: у оригинала клавиша именно такая. */
#define KBD_SOUND_TOGGLE 0x1B
/* Ctrl+M — тумблер МУЗЫКИ, парный к Ctrl+S. Отдельный потому, что музыка
* у нас поток с диска: её выключают по другим причинам, чем эффекты. */
#define KBD_MUSIC_TOGGLE 0x3A /* M (PS/2 set 2) */
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
+12 -4
View File
@@ -44,6 +44,7 @@ static uint8_t cfg_checksum(const uint8_t *data, uint16_t size)
void pop_settings_defaults(void) __banked
{
pop_settings.sound_enabled = 1;
pop_settings.music_enabled = 1;
pop_settings.speed_mode = POP_SPEED_NORMAL;
pop_settings.gameplay_profile = POP_PROFILE_VANILLA;
pop_settings.cheats_enabled = 1; /* пока roomtest — dev-сборка */
@@ -66,6 +67,9 @@ void pop_settings_apply(uint8_t sound_ready) __banked
* Файл должен отражать фактически применимое состояние. */
pop_settings.sound_enabled = pop_snd_want;
}
/* Музыке набор эффектов не нужен: она читается с диска отдельно,
* поэтому применяется всегда, а не по sound_ready. */
pop_mus_want = pop_settings.music_enabled;
}
void pop_cfg_encode(uint8_t *buf)
@@ -84,6 +88,7 @@ void pop_cfg_encode(uint8_t *buf)
buf[13] = (uint8_t)(pop_settings.enhancement_flags >> 8);
buf[14] = pop_settings.time_limit_enabled;
buf[15] = pop_settings.show_debug_bar;
buf[16] = pop_settings.music_enabled;
sum = cfg_checksum(buf + POP_CFG_HEADER_SIZE, POP_CFG_PAYLOAD_SIZE);
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
@@ -113,7 +118,8 @@ uint8_t pop_cfg_decode(const uint8_t *buf, uint16_t size)
buf[9] > POP_PROFILE_ENHANCED || buf[10] > 1 || buf[11] > 1)
return 0;
if (payload_size >= POP_CFG_TIMELIMIT_PAYLOAD_SIZE && buf[14] > 1) return 0;
if (payload_size >= POP_CFG_PAYLOAD_SIZE && buf[15] > 1) return 0;
if (payload_size >= POP_CFG_DEBUGBAR_PAYLOAD_SIZE && buf[15] > 1) return 0;
if (payload_size >= POP_CFG_PAYLOAD_SIZE && buf[16] > 1) return 0;
flags = (uint16_t)(buf[12] | ((uint16_t)buf[13] << 8));
pop_settings.sound_enabled = buf[7];
pop_settings.speed_mode = buf[8];
@@ -124,12 +130,14 @@ uint8_t pop_cfg_decode(const uint8_t *buf, uint16_t size)
pop_settings.enhancement_flags = flags;
/* Старый POP.CFG создавался до настройки таймера (и до тумблера
* отладочной полосы): отсутствующее поле означает исторический дефолт
* ограничение 60 минут и включённую полосу, а не случайное значение в
* памяти. */
* ограничение 60 минут, включённую полосу и музыку, а не случайное
* значение в памяти. */
pop_settings.time_limit_enabled =
(payload_size >= POP_CFG_TIMELIMIT_PAYLOAD_SIZE) ? buf[14] : 1;
pop_settings.show_debug_bar =
(payload_size >= POP_CFG_PAYLOAD_SIZE) ? buf[15] : 1;
(payload_size >= POP_CFG_DEBUGBAR_PAYLOAD_SIZE) ? buf[15] : 1;
pop_settings.music_enabled =
(payload_size >= POP_CFG_PAYLOAD_SIZE) ? buf[16] : 1;
return 1;
}
+84 -27
View File
@@ -892,13 +892,19 @@ static uint8_t pre_hourglass_state(void)
* Статична только поза Princess; draw_princess_room_bg оригинала
* каждый логический кадр заново обновляет факелы, звёзды, корпус часов и
* одну из трёх фаз сыплющегося песка. */
static int pre_princess_animated(pop_pre_cutscene_t scene)
/* Живая комната принцессы БЕЗ актёрской механики: готовая композиция (или
* чистый фон, если picture == PV_ROOM_BARE), поверх неё пламя факелов,
* звёзды в окне и песочные часы заданного состояния. На этом держатся и
* сцены перед уровнями, и экран «время вышло» разница только в картинке,
* состоянии часов и длине.
*
* hg_state: 0 часов нет, 7 время вышло (пустые, без струйки песка). */
#define PV_ROOM_BARE 0xFF
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
{
atlas_t hourglass, torch;
gfx_rect_t full;
uint8_t picture = (uint8_t)(scene == POP_PRE_4 ?
INTRO_PV_LYING : INTRO_PV_STAND);
uint8_t state = pre_hourglass_state();
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
intro_skip_t skip;
@@ -908,8 +914,12 @@ static int pre_princess_animated(pop_pre_cutscene_t scene)
intro_commit_palette();
/* Обе shadow-страницы содержат чистую статическую сцену. Динамика ниже
* рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */
if (intro_draw_page(picture, 0) != 0 ||
intro_draw_page(picture, 1) != 0) return -1;
if (picture == PV_ROOM_BARE) {
if (intro_pv_draw_base() != 0) return -1;
} else if (intro_draw_page(picture, 0) != 0 ||
intro_draw_page(picture, 1) != 0) {
return -1;
}
{
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
pop_arc_t arc;
@@ -927,7 +937,7 @@ static int pre_princess_animated(pop_pre_cutscene_t scene)
result = 0;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
intro_skip_begin(&skip);
for (frame = 0; frame < 26; frame++) {
for (frame = 0; frame < frames; frame++) {
uint8_t back, hold, star;
if (intro_skip_requested(&skip)) { result = 1; break; }
@@ -938,8 +948,13 @@ static int pre_princess_animated(pop_pre_cutscene_t scene)
pv_blit_floor(&torch, torch_left, 93, 116);
pv_blit_floor(&torch, torch_right, 211, 116);
pv_draw_stars(star_phase);
pv_blit_floor(&hourglass, state, 152, 168);
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
if (state) {
pv_blit_floor(&hourglass, state, 152, 168);
/* Струйка песка есть, пока часы идут: при state 7 время уже
* вышло (draw_hourglass, sandflow = -1). */
if (state < 7)
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
}
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
gfx_wait_vsync();
gfx_set_visible_page(back);
@@ -1042,7 +1057,7 @@ static void intro_restore_game_palette(void)
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
#define CUT_PV2_SPLIT 17
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен
* (cutscene_frame_time SDLPoP; см. pre_princess_animated). */
* (cutscene_frame_time SDLPoP; см. pre_room_animated). */
#define CUT_FRAME_VSYNC 5
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
@@ -1237,10 +1252,13 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
pv_draw_stars(cut_star_phase);
/* hourglass_state == 0 — часов в комнате нет вовсе (reset_cutscene
* SDLPoP ставит state=0 и sandflow=-1). Так идёт финальная встреча
* с принцессой: время уже не считается, это победа. */
* с принцессой: время уже не считается, это победа.
* state 7 время ВЫШЛО: часы стоят пустыми, и струйки песка в них
* тоже нет (draw_hourglass: `if (hourglass_state >= 7) return;`). */
if (cut_hg_state) {
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168);
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164);
if (cut_hg_state < 7)
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164);
}
cut_blit_princess(&Guard, &pr_fr);
if (kid_visible) cut_blit_kid(&Kid, &kid_fr);
@@ -1342,6 +1360,27 @@ static int cut_scene_9(void)
return 0;
}
/* cutscene_12 (seg001:0525), ветка «времени мало»: принцесса стоит правее
* обычного, через два кадра оборачивается и всё. Живой её делает та же
* машинка, что сцены 8/9: часы здесь обязаны показывать ОСТАВШЕЕСЯ время,
* а не запечённую в композицию картинку. */
static int cut_scene_12_short(void)
{
uint8_t aborted;
if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* init_princess_x156 (seg001:0230) = init_princess + x = 156. */
cut_shad_init(156, 166, 94);
pop_pal_fade_in(INTRO_FADE);
aborted = cut_run(2, 0);
if (!aborted) {
cut_shad_seq(98); /* princess turn around [PV1] */
aborted = cut_run(24, 0);
}
cut_end(aborted);
return 0;
}
/* end_sequence_anim (seg001:041C): Кид вбегает к принцессе, она
* поворачивается и обнимает его; следом появляется мышь. */
static int cut_ending(void)
@@ -1522,7 +1561,15 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
const pop_cs_cmd_t *script = 0;
pop_sfx_pause();
/* Музыка сцены — как в оригинале: 27 для 2/4/6/12 (seg001:401),
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
* сцен (27 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
* load_intro просто гасит экран и возвращает управление, а звук
* продолжает играть уже поверх загрузки уровня и первых секунд игры
* (seg001:690). Мы глушили CBL на выходе музыка обрывалась на
* полуслове. Теперь не глушим: доиграет сама, а следующая загрузка
* трека всё равно освободит слот.
*
* Какой именно трек как в оригинале: 27 для 2/4/6/12 (seg001:401),
* 35 для 8/9 (seg001:298), 40 для короткой «времени мало» (seg001:388).
* Экран в этот момент чёрный, поэтому читаем трек целиком: пауза на
* диск здесь никому не мешает. */
@@ -1537,20 +1584,22 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
}
}
if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) {
if (pre_princess_animated(scene) >= 0) {
pop_music_free();
pop_sfx_pause();
if (pre_room_animated((uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING
: INTRO_PV_STAND),
pre_hourglass_state(), 26) >= 0) {
intro_restore_game_palette();
return;
}
/* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */
}
if (scene == POP_PRE_12_SHORT && cut_scene_12_short() == 0) {
intro_restore_game_palette();
return;
}
/* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */
if (scene == POP_PRE_8 || scene == POP_PRE_9) {
int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9();
if (done == 0) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return;
}
@@ -1560,27 +1609,35 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
else if (scene == POP_PRE_8) script = pre_8;
else if (scene == POP_PRE_9) script = pre_9;
else if (scene == POP_PRE_12_SHORT) script = pre_12_short;
if (!script) {
pop_music_free();
pop_sfx_pause();
return;
}
if (!script) return;
intro_run(script);
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
}
void pop_time_expired_show(void) __banked
{
pop_sfx_pause();
/* time_expired (seg001:365) начинается с play_sound(sound_36). */
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
if (pop_music_load(36) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
intro_run(time_expired_script);
/* ПРИНЦЕССЫ ЗДЕСЬ НЕТ. Оригинал не зовёт ни одного init_*: экран —
* это пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка
* нет). Мы показывали готовую композицию с принцессой и полными
* часами то есть ровно противоположное по смыслу.
* Длина 2 + 100 кадров cutscene, как в оригинале. */
if (pre_room_animated(PV_ROOM_BARE, 7, 102) < 0)
intro_run(time_expired_script); /* нет ресурсов — старый путь */
/* Оригинал ждёт КОНЦА ТРЕКА уже под чёрным экраном (seg001:374). */
{
intro_skip_t tail;
intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail))
pop_wait_edge();
}
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
+21 -20
View File
@@ -87,13 +87,13 @@
9 * SET_STEP + SEL_BOT_DY + 1)
#define SET_TITLE_BASE (UI_CENTER_TOP(SET_BLOCK_H) + POP_FONT_BIG_ASCENT)
#define SET_HEAD1 (SET_TITLE_BASE + SET_HEAD_GAP)
#define SET_HEAD2 (SET_HEAD1 + 3 * SET_STEP + SET_HEAD_GAP)
#define SET_HEAD3 (SET_HEAD2 + 4 * SET_STEP + SET_HEAD_GAP)
#define SET_HEAD2 (SET_HEAD1 + 4 * SET_STEP + SET_HEAD_GAP)
#define SET_HEAD3 (SET_HEAD2 + 3 * SET_STEP + SET_HEAD_GAP)
/* CONTROLS: число строк зависит от того, включены ли читы, поэтому верх
* блока считается в рантайме. */
#define CTRL_TITLE_GAP 20
#define CTRL_N_BASE 6 /* общие клавиши */
#define CTRL_N_BASE 7 /* общие клавиши */
#define CTRL_N_CHEAT 5 /* дополнительные строки читов */
#define CTRL_COL_KEY 45
#define CTRL_COL_DESC 145
@@ -267,9 +267,9 @@ static void menu_draw(uint8_t sel)
* значение помещается (на голом литерале 218 молчит). */
static const uint8_t settings_y[SETTINGS_N_ITEMS] = {
(uint8_t)(SET_HEAD1 + 1 * SET_STEP), (uint8_t)(SET_HEAD1 + 2 * SET_STEP),
(uint8_t)(SET_HEAD1 + 3 * SET_STEP),
(uint8_t)(SET_HEAD1 + 3 * SET_STEP), (uint8_t)(SET_HEAD1 + 4 * SET_STEP),
(uint8_t)(SET_HEAD2 + 1 * SET_STEP), (uint8_t)(SET_HEAD2 + 2 * SET_STEP),
(uint8_t)(SET_HEAD2 + 3 * SET_STEP), (uint8_t)(SET_HEAD2 + 4 * SET_STEP),
(uint8_t)(SET_HEAD2 + 3 * SET_STEP),
(uint8_t)(SET_HEAD3 + 1 * SET_STEP), (uint8_t)(SET_HEAD3 + 2 * SET_STEP)
};
@@ -293,15 +293,15 @@ static void settings_draw(uint8_t sel)
"SETTINGS");
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD1, "GENERAL");
settings_line(0, "SOUND", pop_settings.sound_enabled ? "ON" : "OFF", sel == 0);
settings_line(1, "DEBUG BAR",
pop_settings.show_debug_bar ? "ON" : "OFF", sel == 1);
settings_line(2, "RESTORE DEFAULTS...", 0, sel == 2);
settings_line(1, "MUSIC", pop_settings.music_enabled ? "ON" : "OFF", sel == 1);
settings_line(2, "DEBUG BAR",
pop_settings.show_debug_bar ? "ON" : "OFF", sel == 2);
settings_line(3, "RESTORE DEFAULTS...", 0, sel == 3);
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD2, "GAMEPLAY");
settings_line(3, "TIME LIMIT",
settings_line(4, "TIME LIMIT",
pop_settings.time_limit_enabled ? "60 MIN" : "UNLIMITED",
sel == 3);
settings_line(4, "SPEED", speed_text[pop_settings.speed_mode], sel == 4);
settings_line(5, "GAMEPLAY PROFILE", "VANILLA", sel == 5);
sel == 4);
settings_line(5, "SPEED", speed_text[pop_settings.speed_mode], sel == 5);
settings_line(6, "CHEATS", pop_settings.cheats_enabled ? "ON" : "OFF", sel == 6);
pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD3, "CONTROLS");
settings_line(7, "SHOW KEY BINDINGS", 0, sel == 7);
@@ -313,12 +313,13 @@ static void settings_draw(uint8_t sel)
/* Две колонки «клавиша — что делает». Первые CTRL_N_BASE строк общие, за
* ними идут читовые: их показываем, только когда читы включены. */
static const char * const ctrl_key[CTRL_N_BASE + CTRL_N_CHEAT] = {
"ARROWS", "SHIFT", "ESC", "F6 / F9", "CTRL+S", "P",
"ARROWS", "SHIFT", "ESC", "F6 / F9", "CTRL+S", "CTRL+M", "P",
"K", "I", "SHIFT+L", "U", "F7 / F8"
};
static const char * const ctrl_desc[CTRL_N_BASE + CTRL_N_CHEAT] = {
"MOVE / JUMP", "ACTION", "PAUSE MENU", "SAVE / LOAD", "SOUND", "SPEED",
"MOVE / JUMP", "ACTION", "PAUSE MENU", "SAVE / LOAD", "SOUND", "MUSIC",
"SPEED",
"KILL GUARD", "IMMORTAL", "NEXT LEVEL", "FLIP SCREEN", "TIME - / +"
};
@@ -521,25 +522,25 @@ static uint8_t settings_process(void)
/* Left/right принадлежат только числовой настройке скорости;
* остальные строки меняются Enter либо Space, чтобы стрелкой не
* открыть случайно Controls и не сбросить параметры. */
if ((ent && !ent_prev) || (sel == 4 && delta)) {
if ((ent && !ent_prev) || (sel == 5 && delta)) {
if (sel == 0) {
pop_settings.sound_enabled ^= 1; change = 1;
} else if (sel == 1) {
pop_settings.show_debug_bar ^= 1; change = 1;
pop_settings.music_enabled ^= 1; change = 1;
} else if (sel == 2) {
pop_settings.show_debug_bar ^= 1; change = 1;
} else if (sel == 3) {
if (ui_confirm("RESTORE DEFAULTS?")) {
pop_settings_defaults(); change = 1;
}
redraw = 1; sound = MENU_SND_SELECT;
} else if (sel == 3) {
pop_settings.time_limit_enabled ^= 1; change = 1;
} else if (sel == 4) {
pop_settings.time_limit_enabled ^= 1; change = 1;
} else if (sel == 5) {
uint8_t speed = pop_settings.speed_mode;
if (delta < 0) speed = (uint8_t)(speed ? speed - 1 : POP_SPEED_FASTEST);
else speed = (uint8_t)(speed == POP_SPEED_FASTEST ? POP_SPEED_NORMAL : speed + 1);
pop_settings.speed_mode = speed; change = 1;
} else if (sel == 5) {
ui_notice("VANILLA ONLY"); redraw = 1; sound = MENU_SND_SELECT;
} else if (sel == 6) {
pop_settings.cheats_enabled ^= 1; change = 1;
} else if (sel == 7) {
+7
View File
@@ -219,6 +219,13 @@ void pop_music_service(void) __banked
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
* Заявку кладёт pop_sfx_play (звук без оцифровки = музыка оригинала),
* а диск трогаем только здесь. */
if (!pop_mus_want) {
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
* продолжают звучать у них свой тумблер. */
if (pop_mus_req) pop_mus_req = 0;
if (ld_busy || pop_mus_left) pop_music_free();
return;
}
if (pop_mus_req) {
uint8_t id = pop_mus_req;
+1
View File
@@ -19,6 +19,7 @@ typedef enum {
typedef struct {
uint8_t sound_enabled; /* 0/1 — зеркало звука (Ctrl+S / pop_sfx) */
uint8_t music_enabled; /* 0/1 — зеркало музыки (Ctrl+M / pop_mus_want) */
uint8_t speed_mode; /* POP_SPEED_* (pop_pace.h), 0..2 */
uint8_t gameplay_profile; /* pop_gameplay_profile_t */
uint8_t cheats_enabled; /* 0/1 — активны ли чит-клавиши */
+7 -1
View File
@@ -59,6 +59,12 @@ volatile uint8_t pop_snd_tick;
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
uint8_t pop_mus_req;
/* ХОЧЕТ ЛИ ПОЛЬЗОВАТЕЛЬ МУЗЫКУ (Settings / Ctrl+M) — отдельно от звука.
* Оригинал такого разделения не знал (там один тумблер на всё), но у нас
* музыка это ПОТОК С ДИСКА: кому мешает подкачка или просто не нужен
* саундтрек, гасит её, не теряя эффектов. */
uint8_t pop_mus_want;
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) */
@@ -237,7 +243,7 @@ void pop_sfx_play(uint8_t id)
* В конкурсе эффектов такой id не участвует: он не глушит живой звук
* (порт проверки `if (NULL == sound_pointers[id]) return;`). */
if (pop_snd_tbl[id].len == 0) {
pop_mus_req = id;
if (pop_mus_want) pop_mus_req = id;
return;
}
if (!snd_next1 || snd_prio[id] <= snd_prio[snd_next1 - 1])
+5
View File
@@ -65,6 +65,11 @@ extern volatile uint8_t pop_snd_tick;
* недопустимо. 0 заявки нет. Снимать обязан тот, кто обработал. */
extern uint8_t pop_mus_req;
/* Тумблер МУЗЫКИ (Settings / Ctrl+M), независимый от тумблера эффектов:
* музыка у нас поток с диска, и выключают её по другим причинам.
* 0 заявки не принимаются, играющий трек глушится (pop_music_service). */
extern uint8_t pop_mus_want;
/* Заявить эффект с номером id (0..56 — нумерация оригинала, см.
* sound_plan.md). НЕ запускает его немедленно и НЕ перебивает играющий:
* это порт play_sound (seg000:12C5), который лишь НОМИНИРУЕТ кандидата на
+28 -11
View File
@@ -73,6 +73,8 @@ static const char * const status_text[POP_MSG_N] = {
"NO QUICKLOAD",
"SOUND ON",
"SOUND OFF",
"MUSIC ON",
"MUSIC OFF",
"", /* LEVEL N — собирается с номером */
"", /* N MINUTES LEFT */
"", /* N SECONDS LEFT */
@@ -113,10 +115,15 @@ static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL
#define R_VAL_ROOM 84 /* зона 71..83 */
#define X_LBL_SPEED 84 /* ", Speed:" 84..116 */
#define R_VAL_SPEED 152 /* зона 117..151 */
#define X_LBL_SOUND 152 /* ", Sound:" 152..184 */
#define R_VAL_SOUND 202 /* зона 185..201 */
#define X_LBL_IMM 202 /* ", Immortal" 202..243 */
#define R_VAL_IMM 253 /* зона 244..252: цифре нужен зазор в пробел */
/* Три тумблера подряд — звук, музыка, бессмертие. Подписи сокращены до
* буквы с двоеточием: полные слова («Sound», «Immortal») занимали столько,
* что на музыку места уже не оставалось, а смысл и так читается. */
#define X_LBL_SOUND 152 /* ", S:" 152..168 */
#define R_VAL_SOUND 185 /* зона 169..184 */
#define X_LBL_MUS 185 /* ", M:" 185..201 */
#define R_VAL_MUS 218 /* зона 202..217 */
#define X_LBL_IMM 218 /* ", I:" 218..234 */
#define R_VAL_IMM 245 /* зона 235..244: цифре нужен зазор в пробел */
/* Оставшееся время ##:## — отдельным полем у правого края, подписи не
* требует: формат читается сам. */
#define R_VAL_TIME 310 /* зона 284..309 */
@@ -125,7 +132,7 @@ static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL
/* Ширина зоны значения — под самый длинный вариант плюс воздух. */
#define W_VAL_NUM 13 /* две цифры */
#define W_VAL_SPEED 35 /* FASTEST */
#define W_VAL_SOUND 17 /* OFF */
#define W_VAL_SOUND 16 /* OFF */
#define W_VAL_IMM 9 /* одна цифра */
enum { /* биты маски «что перерисовать» */
@@ -135,12 +142,13 @@ enum { /* биты маски «что перерис
DBG_F_SOUND = 0x08,
DBG_F_IMM = 0x10,
DBG_F_TIME = 0x20,
DBG_F_ALL = 0x3F /* + подписи: см. dbg_labels */
DBG_F_MUS = 0x40,
DBG_F_ALL = 0x7F /* + подписи: см. dbg_labels */
};
static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" };
static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_imm;
static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_mus, dbg_imm;
static uint8_t dbg_tmin, dbg_tsec; /* показанное время ##:## */
static uint8_t dbg_phase; /* кадры до пересчёта секунд */
static uint8_t dbg_dirty[2]; /* маска по странице; DBG_F_ALL => и подписи */
@@ -308,7 +316,11 @@ void pop_status_show_now(uint8_t msg) __banked
saved_bank = gfx_get_bank();
gfx_set_draw_page(visible);
status_draw();
st_dirty = (uint8_t)(st_dirty & ~(1 << visible));
/* Бит видимой страницы НЕ снимаем. За время дисковой операции ту же
* страницу успевает переписать восстановление комнаты, и снятый бит
* означал бы «здесь уже нарисовано» надпись оставалась ровно на одной
* странице и мигала через кадр (поймано на QUICKLOAD). Лишняя
* перерисовка строки стоит дешевле мигания. */
gfx_set_draw_page(saved_page);
gfx_set_bank(saved_bank);
}
@@ -334,8 +346,9 @@ static void dbg_labels(void)
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_LEVEL, DBG_BASE, "Level");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_ROOM, DBG_BASE, ", Room");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SPEED, DBG_BASE, ", Speed:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SOUND, DBG_BASE, ", Sound:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_IMM, DBG_BASE, ", Immortal");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SOUND, DBG_BASE, ", S:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_MUS, DBG_BASE, ", M:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_IMM, DBG_BASE, ", I:");
pop_text_unmap();
}
@@ -359,6 +372,8 @@ static void dbg_draw(uint8_t mask)
speed_text[dbg_spd < 3 ? dbg_spd : 0]);
if (mask & DBG_F_SOUND)
dbg_value(R_VAL_SOUND, W_VAL_SOUND, dbg_snd ? "ON" : "OFF");
if (mask & DBG_F_MUS)
dbg_value(R_VAL_MUS, W_VAL_SOUND, dbg_mus ? "ON" : "OFF");
if (mask & DBG_F_IMM) {
buf[0] = (char)('0' + (dbg_imm < 10 ? dbg_imm : 9));
buf[1] = 0;
@@ -377,7 +392,7 @@ static void dbg_draw(uint8_t mask)
static void dbg_scan(uint8_t room)
{
uint8_t prev_lvl = dbg_lvl, prev_room = dbg_room, prev_spd = dbg_spd;
uint8_t prev_snd = dbg_snd, prev_imm = dbg_imm;
uint8_t prev_snd = dbg_snd, prev_mus = dbg_mus, prev_imm = dbg_imm;
uint8_t prev_tmin = dbg_tmin, prev_tsec = dbg_tsec;
uint8_t mask = 0;
@@ -402,11 +417,13 @@ static void dbg_scan(uint8_t room)
dbg_room = room;
dbg_spd = pop_speed_mode;
dbg_snd = pop_snd_want;
dbg_mus = pop_mus_want;
dbg_imm = pop_immortal;
if (dbg_lvl != prev_lvl) mask |= DBG_F_LEVEL;
if (dbg_room != prev_room) mask |= DBG_F_ROOM;
if (dbg_spd != prev_spd) mask |= DBG_F_SPEED;
if (dbg_snd != prev_snd) mask |= DBG_F_SOUND;
if (dbg_mus != prev_mus) mask |= DBG_F_MUS;
if (dbg_imm != prev_imm) mask |= DBG_F_IMM;
if (dbg_tmin != prev_tmin || dbg_tsec != prev_tsec) mask |= DBG_F_TIME;
if (mask) {
+2
View File
@@ -26,6 +26,8 @@ enum {
POP_MSG_NO_QUICKLOAD, /* F9 нечего грузить / битый слот */
POP_MSG_SOUND_ON, /* Ctrl+S */
POP_MSG_SOUND_OFF,
POP_MSG_MUSIC_ON, /* Ctrl+M */
POP_MSG_MUSIC_OFF,
POP_MSG_LEVEL, /* «LEVEL N» — строка собирается с номером */
POP_MSG_MINUTES, /* «N MINUTES LEFT» */
POP_MSG_SECONDS, /* «N SECONDS LEFT» / «1 SECOND LEFT» */
+10 -2
View File
@@ -444,8 +444,16 @@ int main(void)
quit = 1;
break;
}
if (menu_opened && !pop_app_dispatch(POP_APP_EV_RESUME)) {
failed = 1; quit = 1; break;
if (menu_opened) {
/* Меню переписало обе страницы целиком. Снимки слотов об
* этом не знают и продолжают считать, что персонаж на
* странице уже нарисован, стоящий Кид оставался бы
* невидимым до первого своего движения. */
pop_cd_forget();
pop_status_invalidate();
if (!pop_app_dispatch(POP_APP_EV_RESUME)) {
failed = 1; quit = 1; break;
}
}
}
/* F10 — отладочный немедленный выход (раньше был на Esc, но Esc
+16 -9
View File
@@ -344,7 +344,7 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
* Флаги «фронта» стали статиками модуля раньше они были локалями main.
* Смысл прежний: одно нажатие = одно срабатывание.
*/
static uint8_t cheat_prev, immo_prev, sound_prev, nlvl_prev;
static uint8_t cheat_prev, immo_prev, sound_prev, music_prev, nlvl_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev, cfg_prev;
static uint8_t time_sub_prev, time_add_prev;
@@ -379,6 +379,8 @@ void pop_cheat_tick(void) __banked
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = (uint8_t)(kbd_raw_down(KBD_SOUND_TOGGLE) &&
(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL)));
uint8_t m = (uint8_t)(kbd_raw_down(KBD_MUSIC_TOGGLE) &&
(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL)));
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);
@@ -401,6 +403,12 @@ void pop_cheat_tick(void) __banked
pop_status_msg = (uint8_t)(pop_snd_want ? POP_MSG_SOUND_ON
: POP_MSG_SOUND_OFF);
}
if (m && !music_prev) {
pop_mus_want ^= 1; /* не под pop_cheats */
pop_settings.music_enabled = pop_mus_want;
pop_status_msg = (uint8_t)(pop_mus_want ? POP_MSG_MUSIC_ON
: POP_MSG_MUSIC_OFF);
}
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
@@ -417,6 +425,7 @@ void pop_cheat_tick(void) __banked
cheat_prev = k;
immo_prev = i;
sound_prev = s;
music_prev = m;
nlvl_prev = nl;
upsd_prev = up;
xdec_prev = xd;
@@ -617,12 +626,13 @@ void pop_qsave_restore_room(uint8_t room) __banked
/* Обе страницы теперь содержат только свежий фон. Старые габариты
* персонажей относятся к кадру до QuickLoad и не должны запускать heal
* поверх восстановленной комнаты. */
pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0;
pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0;
pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0;
pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0;
pop_cd_forget();
pop_cd_init();
pop_hp_invalidate();
/* Полоса состояния тоже переписана заново — «QUICKLOAD» обязан лечь
* поверх свежего фона на ОБЕИХ страницах, иначе он виден через кадр и
* мигает. */
pop_status_invalidate();
}
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
@@ -843,10 +853,7 @@ void pop_flip_screen(void) __banked
/* Всё, что рисуется поверх фона, надо положить заново: прошлые
* прямоугольники персонажей относятся к СТАРОЙ (неперевёрнутой)
* картинке, и heal по ним стёр бы не то место. */
pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0;
pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0;
pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0;
pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0;
pop_cd_forget();
POP_CD_TOUCH_ALL(); /* фон трогали целиком */
pop_hp_invalidate(); /* полосу HP перерисовать */
}
+1 -1
View File
@@ -14,7 +14,7 @@
/* Runtime-владельцы нужны только для линковки pop_settings_apply(); codec
* их не вызывает. */
uint8_t pop_speed_mode, pop_cheats, pop_snd_want;
uint8_t pop_speed_mode, pop_cheats, pop_snd_want, pop_mus_want;
void pop_sfx_toggle(void) __banked { pop_snd_want = (uint8_t)!pop_snd_want; }
static void set_all(uint8_t snd, uint8_t spd, uint8_t prof, uint8_t cheats,