Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faec9a7d3a | |||
| 5d912f0f3f | |||
| 4086dde1f1 | |||
| 0b15ed7b8f |
@@ -930,8 +930,34 @@ load_intro у оригинала и так начинается с ожидан
|
||||
«ступень на каждый кадр» звал пересчёт сто раз и растягивал fade до
|
||||
десяти с лишним секунд.
|
||||
|
||||
**Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM —
|
||||
в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен
|
||||
КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те,
|
||||
что насос уже прошёл. Это отдельная задача — курсор насоса придётся
|
||||
научить заворачиваться, а загрузчик — держать дистанцию от него.
|
||||
## 8. ПОТОКОВЫЙ ТРЕК: ФИНАЛЬНАЯ ТЕМА (2026-08-26)
|
||||
|
||||
`won` (56) — 115 с, 1,2 МБ, 78 страниц EMM. В память он не влезает ни при
|
||||
каком бюджете, поэтому играется КОЛЬЦОМ из шести страниц (96 КБ = 9 с): насос
|
||||
идёт по кругу, а `pop_music_service` дочитывает файл в те страницы, которые
|
||||
насос уже прошёл.
|
||||
|
||||
**Кто кого догоняет.** Страница звучит 1,5 с, а читается 33 мс — запас
|
||||
сорокакратный. Дистанция считается без отдельных счётчиков: страница ровно
|
||||
128 блоков насоса, поэтому проигранных страниц = (всего блоков − осталось)
|
||||
/ 128. Пока прочитано меньше, чем проиграно плюс размер кольца, в кольце
|
||||
есть свободный слот. Файл читается ПОСЛЕДОВАТЕЛЬНО, без `lseek`.
|
||||
|
||||
**Что пришлось учесть.**
|
||||
|
||||
* Насос заворачивает страницу только при `pop_mus_ring != 0`; конец трека
|
||||
по-прежнему определяет `left`. Обычный трек этой ветки не касается.
|
||||
* Кольцо обязано сниматься при любом обычном запуске (`pop_music_play`,
|
||||
`_stream`, `_load_begin`): иначе следующий трек играет по кругу первых
|
||||
шести страниц — поймано на титрах сразу после победы.
|
||||
* Живые сцены комнаты принцессы открывают CBL сами (`cut_begin`):
|
||||
`pop_ending_show` глушит звук первым действием, и «arrived to princess»
|
||||
(26) не звучал вовсе.
|
||||
* Тема дослушивается до конца (прерывается клавишей), как `while
|
||||
(check_sound_playing() && !key_test_quit())` в seg001:637. У оригинала
|
||||
между титрами и этим ожиданием стоит ввод имени в таблицу рекордов —
|
||||
когда он появится у нас, ожидание переедет за него (задача HOF-ENTRY).
|
||||
|
||||
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
||||
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
||||
за размер кольца — то есть подкачка успевает.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -214,7 +214,7 @@ SND_DATA := $(SND_ARC)
|
||||
# игра 24 25 28 29 30 32 33 37 39 41 43
|
||||
# Нет только 56 (won, 115 с / 1,2 МБ) — финалу нужен стриминг с диска.
|
||||
MUS_DIR := $(CURDIR)/../poc/res/music
|
||||
MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55
|
||||
MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55 56
|
||||
MUS_DATA := $(foreach i,$(MUS_IDS),$(MUS_DIR)/m$(i).bin)
|
||||
|
||||
# UI: два исходных шрифта SDLPoP в одном EMM-атласе. Файл короче страницы:
|
||||
|
||||
@@ -78,7 +78,10 @@
|
||||
это максимум из встречавшихся; 12/22/24 — skill 2/3/3).
|
||||
|
||||
<a id="l9-invert"></a>
|
||||
### L9-INVERT. Зелье ПЕРЕВОРОТА (уровень 9, комнаты 7 и 10)
|
||||
### L9-INVERT. Зелье ПЕРЕВОРОТА — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Подробный план реализации — [`../docs/l9_invert_plan.md`](../docs/l9_invert_plan.md)**
|
||||
> (задачи libbgi, задачи приложения, порядок работ, риски, открытые вопросы).
|
||||
@@ -379,7 +382,10 @@ x = 106; разбор — [`../docs/perf_l11_room15.md`](../docs/perf_l11_room15
|
||||
(6 126 тактов фиксированной накладной на любой блит, независимо от размера).
|
||||
|
||||
|
||||
### <a id="l12-shadow"></a>L12-SHADOW. Уровень 12: встреча с тенью, бой, слияние
|
||||
### <a id="l12-shadow"></a>L12-SHADOW. Уровень 12: тень, бой, слияние — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Статус 2026-08-13: код написан, хост-тесты зелёные (45 проверок,
|
||||
> `tests-host/t_shadow.c`), ЖИВОЙ ПРОВЕРКИ В MAME ЕЩЁ НЕ БЫЛО.** Для неё
|
||||
@@ -497,7 +503,10 @@ POP_RD_LOOSE, 1)` — полную перерисовку тайла, десят
|
||||
берётся вместе с остальной шлифовкой переходов.
|
||||
|
||||
<a id="l13-jaffar"></a>
|
||||
### L13-JAFFAR. Уровень 13: Джафар, победа, выход, падающая гряда
|
||||
### L13-JAFFAR. Уровень 13: Джафар, победа, выход — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Статус 2026-08-13: код написан, хост-тесты зелёные (44 проверки,
|
||||
> `tests-host/t_jaffar.c`), ЖИВОЙ ПРОВЕРКИ В MAME ЕЩЁ НЕ БЫЛО.**
|
||||
@@ -550,7 +559,10 @@ POP_RD_LOOSE, 1)` — полную перерисовку тайла, десят
|
||||
6. комната 23 — уровень молча становится 13-м, HP НЕ сбрасывается.
|
||||
|
||||
|
||||
### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ (уровень 7, комната 1)
|
||||
### <a id="l7-feather"></a>L7-FEATHER. Зелье МЕДЛЕННОГО ПАДЕНИЯ — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> Проверено пользователем в живой игре.
|
||||
|
||||
|
||||
> **Статус 2026-08-12: код написан, ждёт живой проверки в MAME.** Сделаны все
|
||||
> шесть шагов ниже плюс синие пузырьки зелья «−HP» (тип 5) — они всплыли по
|
||||
@@ -1188,7 +1200,15 @@ N навык M» — отдельная надобность, если пона
|
||||
|
||||
**Оговорка по памяти** — та же, что в TUNE-1: парсер холодный, в банк.
|
||||
|
||||
### <a id="snd"></a>SND. Звук — эффекты через CBL, музыка на AY
|
||||
### <a id="snd"></a>SND. Звук — эффекты через CBL, музыка на AY — ЗАКРЫТА ИНАЧЕ 2026-08-26
|
||||
|
||||
> **Решение пересмотрено.** Музыка пошла НЕ на AY, а тем же PCM через CBL:
|
||||
> записи DOS-версии укладываются в формат железа без единого преобразования
|
||||
> в рантайме (`../docs/sound_plan.md` §4). Озвучены заставка, сцены между
|
||||
> уровнями и игровые джинглы — 21 трек. Единственный неозвученный кусок —
|
||||
> финал `won`, см. [MUS-WON](#mus-won).
|
||||
|
||||
<details><summary>исходная постановка (путь A на AY)</summary>
|
||||
|
||||
Полный разбор с замерами — [`../docs/sound_plan.md`](../docs/sound_plan.md).
|
||||
Коротко, почему это оказалось дешевле, чем выглядело:
|
||||
@@ -1206,6 +1226,40 @@ N навык M» — отдельная надобность, если пона
|
||||
|
||||
Фазы З1..З5 и список «проверить артефактом до кодинга» — в доке.
|
||||
|
||||
</details>
|
||||
|
||||
### <a id="mob-spr-emm"></a>MOB-SPR-EMM. Композит падающего куска — в EMM (1284 Б W2)
|
||||
|
||||
`mob_spr` (`pop_room.c`) — самый крупный буфер в W2: 4 + 64*20 байт под
|
||||
композит падающей плиты. Это половина нынешнего дефицита W2.
|
||||
|
||||
**В БАНК ЕГО ПЕРЕНЕСТИ НЕЛЬЗЯ** (разобрано 2026-08-26). Цепочка рисования
|
||||
— `pop_room` (банк 7) -> `pop_mem_b` (банк 5) -> `blit_b_clip` (резидент), и
|
||||
трамплин на `pop_mem_b` переключает W3. Указатель на данные банка 7 после
|
||||
этого показывает на страницу банка 5: блит нарисует мусор, причём молча.
|
||||
`--bank-data` вдобавок действует на весь модуль, а `mobs` из того же файла
|
||||
читают снаружи.
|
||||
|
||||
**ЧТО РАБОТАЕТ — EMM-страница с маппингом в W0.** W0 не переключают ни
|
||||
трамплин (он трогает только W3), ни звуковой ISR (насос живёт в W3), значит
|
||||
адрес `0x0000+off` валиден и в банке 7, и в банке 5, и в резиденте — ровно
|
||||
как у атласных спрайтов, которые тем же блитом и рисуются. `blit_b_clip`
|
||||
окно не трогает, маппингом занимается вызывающий.
|
||||
|
||||
**Цена.** `mob_spr_build` читает три части через `gfx_w0_map(a->page)`, а
|
||||
приёмник должен быть в том же окне — держать оба нельзя. Сборку придётся
|
||||
вести построчно: строка части в 64-байтовый буфер на стеке, перемаппить
|
||||
окно, записать. Вызывается это лишь на смену тайлсета. Плюс
|
||||
`gfx_w0_map`/`unmap` вокруг каждого куска — около 1350 тактов, при шести
|
||||
кусках меньше 2 % кадра.
|
||||
|
||||
**Чем подтверждать.** Падающие плиты в комнате 1.2 и каскад уровня 13:
|
||||
ошибка проявится не крашем, а мусором на месте куска.
|
||||
|
||||
**Дешёвая половинчатая мера** (если 1284 Б не нужны целиком): буфер объявлен
|
||||
с запасом 64x20, фактический композит около 58x19 — точные константы дают
|
||||
~180 байт без единого риска.
|
||||
|
||||
### <a id="pal-arc"></a>PAL-ARC. Палитры — в архив своей группы (полишинг)
|
||||
|
||||
Файлов палитр в игре шесть: `KID\kid.pal` (1024 Б), `PV\story.pal`,
|
||||
@@ -1238,7 +1292,24 @@ N навык M» — отдельная надобность, если пона
|
||||
нужны в разное время: `kid.pal` — каждому уровню, заставочные — только
|
||||
заставке.
|
||||
|
||||
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг с диска
|
||||
### <a id="hof-entry"></a>HOF-ENTRY. Ввод имени в таблицу рекордов
|
||||
|
||||
У оригинала после титров победы игрок вписывает своё имя в Hall of Fame
|
||||
(`input_str` в `end_sequence`, seg001:614), и победная тема играет всё это
|
||||
время — она и рассчитана на то, что человек сидит и печатает.
|
||||
|
||||
У нас показ таблицы есть, ввода нет. Из-за этого тема сейчас
|
||||
дослушивается в конце `pop_ending_show`; когда ввод появится, ожидание
|
||||
надо перенести ЗА таблицу — как в оригинале.
|
||||
|
||||
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> **Сделано.** Кольцо из шести страниц (96 КБ = 9 с), насос заворачивается
|
||||
> по `pop_mus_ring`, подкачка в `pop_music_service` по дистанции
|
||||
> «проиграно / прочитано». Разбор и грабли — `../docs/sound_plan.md` §8.
|
||||
> Проверено в MAME на сборке `LEVEL=14`.
|
||||
|
||||
<details><summary>исходная постановка</summary>
|
||||
|
||||
Трек 56 (`won`, 115 с) — это 1,2 МБ, 78 страниц EMM. Ни в `POP_MUS_PAGES`
|
||||
(20), ни в разумный бюджет памяти он не влезает, поэтому единственный
|
||||
@@ -1254,7 +1325,24 @@ N навык M» — отдельная надобность, если пона
|
||||
**Заодно.** Тот же механизм снимает ограничение `POP_MUS_PAGES` со всех
|
||||
треков: сейчас каждый должен помещаться в память целиком.
|
||||
|
||||
### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1, как звук и музыка
|
||||
</details>
|
||||
|
||||
### <a id="arc-atlases"></a>ARC-ATLASES. Атласы — в архивы PBA1 — ЗАКРЫТА 2026-08-26
|
||||
|
||||
> **Сделано.** Восемь архивов: kid (58 атласов), pv (78), shadow (32), bg
|
||||
> (25), title (20), guard (10), vizier (5), skel (4). На образе было 232
|
||||
> файла, стало 8 архивов плюс шесть палитр, `kid.ani` и таблицы уровней.
|
||||
> Индексы печатает упаковщик (`ARC_<ГРУППА>_<ФАЙЛ>`), серии код проверяет
|
||||
> статически. `atlas_load` разделён: `atlas_attach` принимает уже
|
||||
> прочитанную страницу, поэтому чтение из архива не дублирует проверку
|
||||
> магии. Грабля, стоившая отладки: имя архива обязано лежать в том же
|
||||
> банке, что и код, который его читает, — строковый литерал из чужого банка
|
||||
> после переключения W3 показывает на другие данные; поэтому наружу отдаётся
|
||||
> НОМЕР архива (`pop_arc_open_id`).
|
||||
>
|
||||
> Остаток на будущее — палитры, см. [PAL-ARC](#pal-arc).
|
||||
|
||||
<details><summary>исходная постановка</summary>
|
||||
|
||||
**Что сделано.** Формат PBA1 и читатель готовы и работают на звуке:
|
||||
`toolchain/pop_pack_arc.py` собирает архив (заголовок 512 Б, записи по 4
|
||||
@@ -1276,6 +1364,8 @@ N навык M» — отдельная надобность, если пона
|
||||
меряется по MAME), число файлов на образе, и что каждая сцена по-прежнему
|
||||
собирается при неполном диске (пути ошибок в `atlas_load`).
|
||||
|
||||
</details>
|
||||
|
||||
### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2
|
||||
|
||||
`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча
|
||||
|
||||
@@ -51,7 +51,7 @@ void pop_settings_defaults(void) __banked
|
||||
pop_settings.show_build_info = 0;
|
||||
pop_settings.enhancement_flags = 0;
|
||||
pop_settings.time_limit_enabled = 1;
|
||||
pop_settings.show_debug_bar = 1; /* roomtest — dev-сборка */
|
||||
pop_settings.show_debug_bar = 0; /* по умолчанию чистый экран */
|
||||
}
|
||||
|
||||
/* Применить модель к настоящим владельцам runtime-состояния. Sound нельзя
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "pop_timer.h"
|
||||
#include "pop_pal.h"
|
||||
#include "pop_ui.h"
|
||||
#include "pop_music.h" /* подкачка потоковой темы победы */
|
||||
#include "pop_hof.h"
|
||||
|
||||
#define HOF_COUNT 5
|
||||
@@ -260,10 +261,15 @@ static void hof_draw(uint8_t edit_row)
|
||||
pop_screen_present(page);
|
||||
}
|
||||
|
||||
/* Ожидания на экране рекордов держат ПОТОКОВУЮ музыку: победная тема
|
||||
* (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
|
||||
* игрок смотрит таблицу — иначе насос догонит загрузчик и пойдёт по
|
||||
* кругу старыми данными. */
|
||||
static void hof_wait_release(void)
|
||||
{
|
||||
while (kbd_raw_any_down()) {
|
||||
kbd_raw_sync();
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
}
|
||||
@@ -340,6 +346,7 @@ void pop_hof_show(void) __banked
|
||||
hof_wait_release();
|
||||
while (!kbd_raw_any_down()) {
|
||||
kbd_raw_sync();
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
hof_wait_release();
|
||||
@@ -363,6 +370,7 @@ void pop_hof_show_title(void) __banked
|
||||
hof_wait_release();
|
||||
while (!kbd_raw_any_down()) {
|
||||
kbd_raw_sync();
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
hof_wait_release();
|
||||
|
||||
@@ -721,6 +721,7 @@ static int intro_pv_animated(void)
|
||||
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
|
||||
uint8_t sand_frame = 0;
|
||||
uint8_t flash_done = 0;
|
||||
uint8_t faded_in = 0;
|
||||
uint8_t flash;
|
||||
intro_skip_t skip;
|
||||
int result = -1;
|
||||
@@ -753,7 +754,9 @@ static int intro_pv_animated(void)
|
||||
if (bad) return -1;
|
||||
}
|
||||
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
/* Проявление — ПОСЛЕ первого нарисованного кадра (см. ниже в цикле):
|
||||
* иначе две секунды fade идут по пустой комнате, а принцесса возникает
|
||||
* уже на свету. */
|
||||
/* Актёрские атласы в EMM; дальше по сцене диск трогает только подкачка
|
||||
* следующей реплики — по одной странице за кадр (pop_music_load_step).
|
||||
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
||||
@@ -838,11 +841,9 @@ static int intro_pv_animated(void)
|
||||
torch_right, sand_frame, flash,
|
||||
render_tick);
|
||||
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
||||
pop_music_service(); /* заявки на музыку из сцены (эффект без оцифровки) */
|
||||
/* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра:
|
||||
* 33 мс диска укладываются в паузу логического кадра (133 мс) и
|
||||
* анимации не стоят. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||||
* целиком, вместе с принцессой и факелами. */
|
||||
if (!faded_in) { faded_in = 1; pop_pal_fade_in(INTRO_FADE); }
|
||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
|
||||
* 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425
|
||||
* повторяет это с точностью 0,07 % (30 мс на всю сцену) без
|
||||
@@ -858,8 +859,21 @@ static int intro_pv_animated(void)
|
||||
snd_done += (uint8_t)(now - snd_prev);
|
||||
snd_prev = now;
|
||||
if (snd_done >= snd_want) break;
|
||||
gfx_wait_vsync();
|
||||
/* Страницу следующей реплики читаем ЗДЕСЬ, в паузе кадра:
|
||||
* 33 мс диска укладываются в ожидание (кадр сцены — 133 мс),
|
||||
* и подкачка не добавляется к длительности кадра, а
|
||||
* растворяется в ней. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
else gfx_wait_vsync();
|
||||
}
|
||||
/* ДОЛГ НЕ ДОГОНЯЕМ. Подкачка следующей реплики (страница — 33 мс)
|
||||
* и любой тяжёлый кадр оставляют snd_done впереди snd_want, и
|
||||
* дальше цикл гнал кадры без единого ожидания: пламя факелов
|
||||
* мелькало, а события соседних тиков (створка ворот и дверь
|
||||
* покоев) слипались в один кадр — заявка перезаписывала заявку, и
|
||||
* один из звуков пропадал. Тот же принцип, что у pop_pace_end:
|
||||
* якорь ставится ПО ФАКТУ, пропущенное время не наверстывается. */
|
||||
if (snd_done > snd_want) snd_want = snd_done;
|
||||
} else {
|
||||
uint16_t target = POP_T60(tick);
|
||||
|
||||
@@ -940,7 +954,6 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||
}
|
||||
|
||||
pop_screen_present(1);
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
result = 0;
|
||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||
intro_skip_begin(&skip);
|
||||
@@ -968,6 +981,9 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||
/* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */
|
||||
for (hold = 1; hold < 5; hold++) gfx_wait_vsync();
|
||||
|
||||
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
|
||||
* пустой комнате, а часы и пламя возникают уже на свету. */
|
||||
if (frame == 0) pop_pal_fade_in(INTRO_FADE);
|
||||
torch_left = (uint8_t)((torch_left + 1) % 9);
|
||||
torch_right = (uint8_t)((torch_right + 1) % 9);
|
||||
sand = (uint8_t)((sand + 1) % 3);
|
||||
@@ -1307,6 +1323,10 @@ static int cut_begin(uint8_t hg_state)
|
||||
intro_commit_palette();
|
||||
if (intro_pv_draw_base() != 0) return -1;
|
||||
if (cut_load() != 0) return -1;
|
||||
/* Атласы сцены прочитаны — можно открывать вывод. Без этого сцена шла
|
||||
* молча: pop_ending_show глушит CBL первым действием, а включить его
|
||||
* обратно было некому, и «arrived to princess» (26) не звучал вовсе. */
|
||||
(void)pop_sfx_start();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1332,8 +1352,13 @@ static int cut_scene_8(void)
|
||||
cut_kid_init(CHARID_24_MOUSE, 144, 167, 106);
|
||||
/* princess_crouching (seg001:024D): сидит и гладит мышь. */
|
||||
cut_shad_init(131, 169, 110);
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
aborted = cut_run(20, 1);
|
||||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||||
* время на экране была бы пустая комната: персонажи появляются только с
|
||||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||||
* картинка проявляется целиком, вместе с актёрами. */
|
||||
aborted = cut_run(1, 1);
|
||||
if (!aborted) pop_pal_fade_in(INTRO_FADE);
|
||||
if (!aborted) aborted = cut_run(19, 1);
|
||||
if (!aborted) {
|
||||
cut_kid_seq(107); /* мышь встаёт и уходит */
|
||||
aborted = cut_run(20, 1);
|
||||
@@ -1356,10 +1381,15 @@ static int cut_scene_9(void)
|
||||
/* princess_stand (seg001:026A) — поза PV1, лицом вправо. */
|
||||
cut_shad_init(144, 169, 94);
|
||||
Guard.direction = 0;
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||||
* время на экране была бы пустая комната: персонажи появляются только с
|
||||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||||
* картинка проявляется целиком, вместе с актёрами. */
|
||||
aborted = cut_run(1, 0); /* мышь ещё за кадром */
|
||||
if (!aborted) pop_pal_fade_in(INTRO_FADE);
|
||||
/* init_mouse_go (seg001:022A): мышь вбегает справа. */
|
||||
cut_kid_init(CHARID_24_MOUSE, 199, 167, 105);
|
||||
aborted = cut_run(5, 1);
|
||||
if (!aborted) aborted = cut_run(5, 1);
|
||||
if (!aborted) {
|
||||
cut_shad_seq(112); /* принцесса приседает */
|
||||
aborted = cut_run(9, 1);
|
||||
@@ -1383,8 +1413,13 @@ static int cut_scene_12_short(void)
|
||||
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);
|
||||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||||
* время на экране была бы пустая комната: персонажи появляются только с
|
||||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||||
* картинка проявляется целиком, вместе с актёрами. */
|
||||
aborted = cut_run(1, 0);
|
||||
if (!aborted) pop_pal_fade_in(INTRO_FADE);
|
||||
if (!aborted) aborted = cut_run(1, 0);
|
||||
if (!aborted) {
|
||||
cut_shad_seq(98); /* princess turn around [PV1] */
|
||||
aborted = cut_run(24, 0);
|
||||
@@ -1406,9 +1441,14 @@ static int cut_ending(void)
|
||||
/* init_ending_princess (seg001:0251) + init_ending_kid (seg001:0287). */
|
||||
cut_shad_init(136, 164, 109);
|
||||
cut_kid_init(CHARID_0_KID, 198, 164, 1);
|
||||
pop_pal_fade_in(INTRO_FADE);
|
||||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||||
* время на экране была бы пустая комната: персонажи появляются только с
|
||||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||||
* картинка проявляется целиком, вместе с актёрами. */
|
||||
aborted = cut_run(1, 1);
|
||||
if (!aborted) pop_pal_fade_in(INTRO_FADE);
|
||||
pop_sfx_play(26); /* «arrived to princess» */
|
||||
aborted = cut_run(8, 1);
|
||||
if (!aborted) aborted = cut_run(7, 1);
|
||||
if (!aborted) {
|
||||
cut_shad_seq(108); /* поворот и объятие */
|
||||
aborted = cut_run(5, 1);
|
||||
@@ -1464,6 +1504,7 @@ static uint8_t intro_run(const pop_cs_cmd_t *script)
|
||||
} else if (ev == POP_CS_EVENT_FADE_IN) pop_pal_fade_in(INTRO_FADE);
|
||||
else if (ev == POP_CS_EVENT_FADE_OUT) pop_pal_fade_out(INTRO_FADE);
|
||||
else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg);
|
||||
pop_music_service(); /* подкачка потокового трека (финал) */
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
|
||||
@@ -1694,7 +1735,35 @@ void pop_ending_show(void) __banked
|
||||
/* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по
|
||||
* скрипту идёт только текст HAIL, поэтому статический сценарий ниже
|
||||
* начинается с него; при нехватке ресурсов отыгрывается целиком. */
|
||||
if (cut_ending() == 0) intro_run(ending_hail_script);
|
||||
else intro_run(ending_script);
|
||||
if (cut_ending() == 0) {
|
||||
/* ПОБЕДНАЯ ТЕМА (56) — потоком с диска: 115 секунд и 1,2 МБ в память
|
||||
* не влезают никак. Оригинал пускает её здесь же, сразу после
|
||||
* встречи с принцессой и до экрана HAIL (seg001:583). Дальше
|
||||
* страницы дочитывает pop_music_service из циклов сцен. */
|
||||
(void)pop_sfx_start();
|
||||
(void)pop_music_ring_start(POP_MUS_WON);
|
||||
intro_run(ending_hail_script);
|
||||
/* ДОСЛУШАТЬ. Оригинал не обрывает победную тему переходом в меню:
|
||||
* `while (check_sound_playing() && !key_test_quit())` (seg001:637),
|
||||
* и лишь потом гасит экран. Клавишей прервать можно.
|
||||
*
|
||||
* У ОРИГИНАЛА между титрами и этим ожиданием стоит таблица рекордов
|
||||
* с вводом имени, и тема играет сквозь неё. Наша таблица пока без
|
||||
* ввода, поэтому ждём здесь; когда ввод появится — ожидание
|
||||
* переедет за него (задача HOF-ENTRY). */
|
||||
{
|
||||
intro_skip_t tail;
|
||||
|
||||
intro_skip_begin(&tail);
|
||||
while (pop_music_busy() && !intro_skip_requested(&tail)) {
|
||||
pop_music_service(); /* поток дочитывается по ходу */
|
||||
pop_wait_edge();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
intro_run(ending_script);
|
||||
}
|
||||
pop_music_free();
|
||||
pop_sfx_pause();
|
||||
intro_restore_game_palette();
|
||||
}
|
||||
|
||||
@@ -256,6 +256,11 @@ static void menu_draw(uint8_t sel)
|
||||
for (i = 0; i < MENU_N_ITEMS; i++)
|
||||
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2,
|
||||
MENU_FIRST_BASE + (int)i * MENU_STEP, menu_text[i]);
|
||||
/* Полосу сначала чистим: там могло висеть сообщение игры («60 MINUTES
|
||||
* LEFT», «QUICKSAVE»), и две надписи ложились друг на друга. */
|
||||
pop_text_unmap();
|
||||
pop_status_wipe();
|
||||
pop_text_map();
|
||||
pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, MENU_PAUSED_BASE,
|
||||
"GAME PAUSED");
|
||||
pop_text_unmap();
|
||||
@@ -687,7 +692,11 @@ int8_t pop_menu_process(void) __banked
|
||||
getfillsettings(&saved_fill);
|
||||
pop_sfx_pause(); /* snapshot/dim + первый full redraw */
|
||||
pop_pal_snapshot();
|
||||
pop_pal_menu_dim(2); /* 50%, диапазон UI остаётся ярким */
|
||||
/* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не
|
||||
* числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы
|
||||
* теперь затемнение на одну шестнадцатую — фон под меню почти не гас
|
||||
* (поймано пользователем 2026-08-26). */
|
||||
pop_pal_menu_dim(POP_PAL_DIM_BLACK / 2);
|
||||
menu_draw(sel);
|
||||
|
||||
for (;;) {
|
||||
|
||||
@@ -62,6 +62,34 @@ static void slot_release(uint8_t n)
|
||||
slot_blocks[n] = 0;
|
||||
}
|
||||
|
||||
/* --- ПОТОКОВЫЙ ТРЕК: КОЛЬЦО СТРАНИЦ ---------------------------------- *
|
||||
*
|
||||
* Финальная тема (`won`, 115 с) — это 1,2 МБ и 78 страниц EMM. Держать её
|
||||
* в памяти нельзя ни при каком бюджете, поэтому она играется из НЕСКОЛЬКИХ
|
||||
* страниц по кругу: насос идёт по кольцу (pop_mus_ring), а загрузчик
|
||||
* дочитывает файл в те страницы, которые насос уже прошёл.
|
||||
*
|
||||
* Кто кого догоняет. Страница звучит 1,5 с, а читается 33 мс — запас
|
||||
* сорокакратный, так что «не успеть» можно только застряв на секунду.
|
||||
* Дистанцию считаем без отдельных счётчиков: страница ровно 128 блоков
|
||||
* насоса, поэтому проигранных страниц = (всего блоков - осталось) / 128.
|
||||
* Пока прочитано меньше, чем проиграно плюс размер кольца, — в кольце есть
|
||||
* свободный слот, и следующую страницу можно читать.
|
||||
*
|
||||
* Файл читаем ПОСЛЕДОВАТЕЛЬНО, без lseek: страницы идут по порядку, и fd
|
||||
* сам стоит там, где надо. */
|
||||
#define RING_PAGES 6 /* 96 КБ = 9 с звучания, запас с лихвой */
|
||||
|
||||
static uint8_t ring_on; /* идёт потоковый трек */
|
||||
static uint8_t ring_blk; /* EMM-блок кольца */
|
||||
static uint8_t ring_fd_open;
|
||||
static int ring_fd;
|
||||
static uint8_t ring_file_pages; /* страниц в файле */
|
||||
static uint8_t ring_loaded; /* сколько уже прочитано */
|
||||
static uint16_t ring_total; /* длина трека в блоках насоса */
|
||||
|
||||
void pop_music_ring_stop(void) __banked;
|
||||
|
||||
void pop_music_stop(void) __banked
|
||||
{
|
||||
pop_mus_left = 0;
|
||||
@@ -69,6 +97,7 @@ void pop_music_stop(void) __banked
|
||||
|
||||
void pop_music_free(void) __banked
|
||||
{
|
||||
if (ring_on) pop_music_ring_stop();
|
||||
if (ld_busy) { close(ld_fd); ld_busy = 0; }
|
||||
pop_mus_left = 0;
|
||||
slot_release(0);
|
||||
@@ -96,6 +125,7 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
char name[12];
|
||||
uint8_t pages, n;
|
||||
|
||||
if (ring_on) pop_music_ring_stop(); /* потоковый трек и слоты не дружат */
|
||||
if (ld_busy) load_abort();
|
||||
if (id >= POP_MUS_COUNT) return -1;
|
||||
pages = pop_mus_tbl[id].pages;
|
||||
@@ -174,6 +204,7 @@ int8_t pop_music_stream(uint8_t id) __banked
|
||||
{
|
||||
uint8_t i, n;
|
||||
|
||||
if (ring_on) pop_music_ring_stop(); /* см. pop_music_play */
|
||||
if (pop_music_load_begin(id) != 0) return -1;
|
||||
if (pop_music_load_step() < 0) return -1; /* первая страница */
|
||||
n = ld_slot;
|
||||
@@ -197,6 +228,10 @@ void pop_music_play(void) __banked
|
||||
uint8_t n, i;
|
||||
|
||||
if (!mus_ready) return;
|
||||
/* СНЯТЬ КОЛЬЦО. Если до этого играл потоковый трек (финал), насос всё
|
||||
* ещё заворачивал бы страницы по кругу шести — обычный трек звучал бы
|
||||
* своей первой третью без конца (поймано на титрах после победы). */
|
||||
if (ring_on) pop_music_ring_stop();
|
||||
n = (uint8_t)(mus_ready - 1);
|
||||
/* Насос обязан замолчать ДО подмены таблицы страниц: иначе он успеет
|
||||
* взять порцию по новому индексу из старого курсора. */
|
||||
@@ -215,6 +250,84 @@ void pop_music_play(void) __banked
|
||||
pop_mus_left = slot_blocks[n];
|
||||
}
|
||||
|
||||
|
||||
static void ring_close_file(void)
|
||||
{
|
||||
if (ring_fd_open) { close(ring_fd); ring_fd_open = 0; }
|
||||
}
|
||||
|
||||
void pop_music_ring_stop(void) __banked
|
||||
{
|
||||
pop_mus_left = 0;
|
||||
pop_mus_ring = 0;
|
||||
ring_close_file();
|
||||
if (ring_blk) { mem_free_block(ring_blk); ring_blk = 0; }
|
||||
ring_on = 0;
|
||||
}
|
||||
|
||||
int8_t pop_music_ring_start(uint8_t id) __banked
|
||||
{
|
||||
char name[12];
|
||||
uint8_t i, page;
|
||||
|
||||
if (id >= POP_MUS_COUNT || !pop_mus_tbl[id].pages) return -1;
|
||||
pop_music_free(); /* обычные слоты кольцу не нужны */
|
||||
pop_music_ring_stop();
|
||||
mus_name(name, id);
|
||||
ring_fd = open(name, O_RDONLY);
|
||||
if (ring_fd < 0) return -1;
|
||||
ring_fd_open = 1;
|
||||
ring_blk = mem_alloc_pages(RING_PAGES);
|
||||
if (!ring_blk) { ring_close_file(); return -1; }
|
||||
ring_file_pages = pop_mus_tbl[id].pages;
|
||||
ring_total = pop_mus_tbl[id].blocks;
|
||||
/* Заполняем кольцо целиком: старт с полным буфером даёт девять секунд
|
||||
* форы, и первая же тяжёлая сцена финала не догонит насос. */
|
||||
for (i = 0; i < RING_PAGES; i++) {
|
||||
page = mem_get_page(ring_blk, i);
|
||||
pop_mus_page[i] = page;
|
||||
if (!page ||
|
||||
(i < ring_file_pages && bank_read_page(ring_fd, page, 0, 16384) < 0)) {
|
||||
pop_music_ring_stop();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
ring_loaded = (uint8_t)(ring_file_pages < RING_PAGES ? ring_file_pages
|
||||
: RING_PAGES);
|
||||
if (ring_loaded >= ring_file_pages) ring_close_file();
|
||||
ring_on = 1;
|
||||
pop_mus_id = id;
|
||||
pop_mus_pg = 0;
|
||||
pop_mus_ptr = 0;
|
||||
pop_mus_ring = RING_PAGES; /* насос пойдёт по кругу */
|
||||
pop_mus_left = ring_total;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Один шаг подкачки: страница файла в слот, который насос уже прошёл. */
|
||||
static void ring_step(void)
|
||||
{
|
||||
uint16_t played;
|
||||
uint8_t played_pg, page;
|
||||
|
||||
if (!ring_on) return;
|
||||
if (!pop_mus_left) { /* трек доиграл — отпустить память */
|
||||
pop_music_ring_stop();
|
||||
return;
|
||||
}
|
||||
if (ring_loaded >= ring_file_pages) return; /* файл прочитан весь */
|
||||
played = (uint16_t)(ring_total - pop_mus_left);
|
||||
played_pg = (uint8_t)(played >> 7); /* 128 блоков на страницу */
|
||||
if ((uint8_t)(ring_loaded - played_pg) >= RING_PAGES) return; /* полно */
|
||||
page = pop_mus_page[ring_loaded % RING_PAGES];
|
||||
if (!page || bank_read_page(ring_fd, page, 0, 16384) < 0) {
|
||||
pop_music_ring_stop();
|
||||
return;
|
||||
}
|
||||
ring_loaded++;
|
||||
if (ring_loaded >= ring_file_pages) ring_close_file();
|
||||
}
|
||||
|
||||
void pop_music_service(void) __banked
|
||||
{
|
||||
/* Один вызов на кадр из ЛЮБОГО цикла — игрового, интро, катсцены.
|
||||
@@ -225,6 +338,7 @@ void pop_music_service(void) __banked
|
||||
* демо проигрывает записанные нажатия, и любой музыкальный повод —
|
||||
* подобранный меч, зелье — зазвучал бы там, где оригинал играет одни
|
||||
* эффекты. */
|
||||
if (ring_on) { ring_step(); return; } /* потоковый трек — свой путь */
|
||||
if (!pop_mus_want || pop_app_state == POP_APP_DEMO) {
|
||||
/* Выключили на ходу: оборвать поток и замолчать. Эффекты при этом
|
||||
* продолжают звучать — у них свой тумблер. */
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#define POP_MUS_STORY_2 50 /* принцесса ждёт */
|
||||
#define POP_MUS_STORY_3 53 /* входит Джафар */
|
||||
#define POP_MUS_STORY_4 52 /* Джафар уходит */
|
||||
#define POP_MUS_WON 56 /* победа: 115 с, только потоком */
|
||||
|
||||
/* Курсор проигрывания — читает НАСОС (pop_sfx.c, резидент), пишет
|
||||
* загрузчик. Отдельный от эффектов: они звучат поверх музыки, а не
|
||||
@@ -45,6 +46,8 @@ extern volatile uint16_t pop_mus_left; /* порций до конца, 0
|
||||
/* Номер играющего трека (нумерация оригинала). Нужен менеджеру звука:
|
||||
* музыка участвует в таблицах приоритета наравне с эффектами. */
|
||||
extern uint8_t pop_mus_id;
|
||||
/* Размер кольца страниц для потокового трека; 0 — обычный режим. */
|
||||
extern uint8_t pop_mus_ring;
|
||||
|
||||
/* ДВА СЛОТА. Заставочные сцены идут цепочкой треков (титры -> история ->
|
||||
* PV из трёх реплик), и загрузка каждого — это полсекунды-секунда диска.
|
||||
@@ -88,6 +91,14 @@ void pop_music_stop(void) __banked;
|
||||
* стрима. Всё чтение с диска живёт здесь. */
|
||||
void pop_music_service(void) __banked;
|
||||
|
||||
/* ПОТОКОВЫЙ ТРЕК ИЗ КОЛЬЦА СТРАНИЦ — для того, что в память не влезает.
|
||||
* Финал `won` весит 1,2 МБ (78 страниц); играется из шести по кругу, а
|
||||
* дочитывает его pop_music_service, пока трек звучит.
|
||||
* 0 — играет, -1 — нет файла/памяти. Останавливает pop_music_ring_stop
|
||||
* (её же зовёт pop_music_free). */
|
||||
int8_t pop_music_ring_start(uint8_t id) __banked;
|
||||
void pop_music_ring_stop(void) __banked;
|
||||
|
||||
/* Трек ещё звучит? 0 — доиграл или не запускался. */
|
||||
uint8_t pop_music_busy(void) __banked;
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define POP_MUS_COUNT 56
|
||||
#define POP_MUS_COUNT 57
|
||||
#define POP_MUS_BLOCK 128
|
||||
#define POP_MUS_MAX_PAGES 20
|
||||
#define POP_MUS_MAX_PAGES 78
|
||||
|
||||
typedef struct {
|
||||
uint8_t pages; /* 0 — трека нет */
|
||||
@@ -78,6 +78,7 @@ static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
|
||||
{ 11, 1403 }, /* 53 story_3_Jaffar_enters */
|
||||
{ 17, 2064 }, /* 54 intro_theme */
|
||||
{ 11, 1350 }, /* 55 story_1_absence */
|
||||
{ 78, 9865 }, /* 56 won */
|
||||
};
|
||||
|
||||
#endif /* POP_MUSIC_TBL_H */
|
||||
|
||||
@@ -55,6 +55,10 @@ uint8_t pop_mus_id;
|
||||
/* Тик насоса: +1 на каждый отданный CBL блок, то есть 85,4 Гц. */
|
||||
volatile uint8_t pop_snd_tick;
|
||||
|
||||
/* Размер КОЛЬЦА страниц для потоковой музыки, 0 — обычный трек целиком в
|
||||
* памяти. Читает насос, ставит загрузчик (pop_music.c). */
|
||||
uint8_t pop_mus_ring;
|
||||
|
||||
/* Заявка на музыкальный трек: номер в нумерации оригинала, 0 — нет заявки
|
||||
* (нулевого трека не бывает). Ставит pop_sfx_play, снимает главный цикл. */
|
||||
uint8_t pop_mus_req;
|
||||
@@ -141,8 +145,18 @@ int pop_sfx_fill(uint16_t n)
|
||||
pop_mus_ptr += n;
|
||||
if (pop_mus_ptr >= 0x4000) {
|
||||
pop_mus_ptr = 0;
|
||||
if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) pop_mus_pg++;
|
||||
else pop_mus_left = 0;
|
||||
/* КОЛЬЦО. Финальная тема — 1,2 МБ, в память целиком не влезает
|
||||
* никак, поэтому она играется из нескольких страниц по кругу, а
|
||||
* загрузчик дочитывает в те, что мы уже прошли (pop_music.c).
|
||||
* Конец трека определяет left, а не номер страницы. */
|
||||
if (pop_mus_ring) {
|
||||
if ((uint8_t)(pop_mus_pg + 1) < pop_mus_ring) pop_mus_pg++;
|
||||
else pop_mus_pg = 0;
|
||||
} else if ((uint8_t)(pop_mus_pg + 1) < POP_MUS_PAGES) {
|
||||
pop_mus_pg++;
|
||||
} else {
|
||||
pop_mus_left = 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -219,6 +219,19 @@ static void build_text(char *buf)
|
||||
}
|
||||
}
|
||||
|
||||
/* Стереть полосу сообщения на ТЕКУЩЕЙ странице. Нужна меню: оно пишет в ту
|
||||
* же полосу своё «GAME PAUSED», а под ним могло остаться сообщение игры
|
||||
* («60 MINUTES LEFT», «QUICKSAVE») — две надписи ложились друг на друга. */
|
||||
void pop_status_wipe(void) __banked
|
||||
{
|
||||
uint8_t saved;
|
||||
|
||||
if (!pop_ui_ready()) return;
|
||||
saved = gfx_get_bank();
|
||||
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
|
||||
gfx_set_bank(saved);
|
||||
}
|
||||
|
||||
static void status_draw(void)
|
||||
{
|
||||
char buf[18];
|
||||
|
||||
@@ -81,4 +81,7 @@ uint8_t pop_dead_prompt(uint16_t frames) __banked;
|
||||
* затёрли чужим содержимым (вход в комнату, выход из меню). */
|
||||
void pop_status_invalidate(void) __banked;
|
||||
|
||||
/* Стереть полосу сообщения на текущей странице (меню пишет туда своё). */
|
||||
void pop_status_wipe(void) __banked;
|
||||
|
||||
#endif /* POP_STATUS_H */
|
||||
|
||||
@@ -128,11 +128,6 @@ static void kbd_idle(void)
|
||||
#define SEQ_STAND 2
|
||||
#define ROOM_1 1
|
||||
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
||||
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
|
||||
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
|
||||
#define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */
|
||||
#define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc,
|
||||
* пока меню не готово рисовать — см. план MS4) */
|
||||
|
||||
|
||||
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
|
||||
@@ -417,63 +412,20 @@ int main(void)
|
||||
* QuickSave/QuickLoad идут тем же механизмом, что F6/F9 (запрос
|
||||
* подхватит блок ниже в этой же границе кадра), рестарты исполняются
|
||||
* после вычисления back-страницы (как респавн), QUIT рвёт цикл. */
|
||||
/* Меню, F10 и QuickSave/Load — целиком в банке 8 (pop_frame_ui).
|
||||
* Всё это события «раз в партию», а кода на них было полсотни строк
|
||||
* в резиденте; наружу остаются флаги, которые нужны самому циклу. */
|
||||
if (!is_demo) {
|
||||
uint8_t e = kbd_raw_down(KBD_MENU);
|
||||
uint8_t menu_opened;
|
||||
int8_t mrc = POP_MENU_NONE;
|
||||
if (e && !esc_prev) pop_menu_request = 1;
|
||||
esc_prev = e;
|
||||
menu_opened = pop_menu_request;
|
||||
if (menu_opened) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_PAUSE)) {
|
||||
failed = 1; quit = 1; break;
|
||||
}
|
||||
mrc = pop_menu_process();
|
||||
}
|
||||
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_BACKUP)
|
||||
pop_qsave_request_load_backup();
|
||||
else if (mrc == POP_MENU_RESTART_LEVEL) menu_restart = 1;
|
||||
else if (mrc == POP_MENU_RESTART_GAME) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_RESTART_INTRO)) failed = 1;
|
||||
uint8_t want_restart = 0, dead_reset = 0;
|
||||
int8_t urc = pop_frame_ui(&want_restart, &dead_reset);
|
||||
|
||||
if (want_restart) menu_restart = 1;
|
||||
if (dead_reset) dead_frames = 0;
|
||||
if (urc) {
|
||||
if (urc < 0) failed = 1;
|
||||
if (urc != 1) quit = 1; /* 1 — restart game, без quit */
|
||||
break;
|
||||
}
|
||||
else if (mrc == POP_MENU_QUIT) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_QUIT)) 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
|
||||
* теперь открывает меню; уйдёт вместе с отладкой). */
|
||||
if (!is_demo && kbd_raw_down(KBD_QUIT)) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
/* F6/F9 только ставят запрос; файловая операция выполняется здесь,
|
||||
* на границе кадров — вне окон Char и до heal/tick/draw. */
|
||||
if (!is_demo) {
|
||||
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
|
||||
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
||||
int8_t qrc;
|
||||
if (s && !qs_save_prev) pop_qsave_request_save();
|
||||
if (l && !qs_load_prev) pop_qsave_request_load();
|
||||
qs_save_prev = s; qs_load_prev = l;
|
||||
qrc = pop_qsave_process();
|
||||
if (qrc > 0) dead_frames = 0;
|
||||
}
|
||||
if (frozen) {
|
||||
PROF(0); /* чёрный: ждём vsync */
|
||||
@@ -761,66 +713,12 @@ int main(void)
|
||||
* или доиграли запись — показать текущий кадр и затем вернуться в
|
||||
* title. В частности, не вызываем level_switch(0->1). */
|
||||
if (is_demo && (kid_room == 24 || pop_kid_dead)) demo_finished = 1;
|
||||
if (!is_demo && pop_next_level != pop_current_level) {
|
||||
pop_pre_cutscene_t pre = pop_flow_pre_cutscene(pop_next_level,
|
||||
pop_timer_minutes);
|
||||
/* Смена уровня целиком — в банке (pop_level_switch): грузится
|
||||
* файл, спрайты соперника и тайлсет. До 2/4/6/8/9/12 таблица
|
||||
* flow запускает отдельное состояние CUTSCENE, как play_level()
|
||||
* SDLPoP: следующий уровень не трогаем до завершения/skip. */
|
||||
if (pre != POP_PRE_NONE) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
pop_pre_cutscene_show(pre);
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
/* Катсцена оставляет экран чёрным. Новый уровень будет
|
||||
* открыт одним применением его готовой палитры. */
|
||||
reveal_pending = 1;
|
||||
} else {
|
||||
/* CBL глушим ДО первой же операции с палитрой. Палитра идёт
|
||||
* через BIOS ($A4) с запрещёнными прерываниями: насос не
|
||||
* успевает долить блок, железо крутит хвост своего буфера —
|
||||
* на слух скрежет. Раньше pop_sfx_pause() звался только
|
||||
* внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и
|
||||
* переход без катсцены (в т.ч. телепорт Shift+L) скрежетал
|
||||
* почти всегда — поймано пользователем 2026-08-25. Ветка с
|
||||
* катсценой свободна от этого: pop_pre_cutscene_show глушит
|
||||
* звук первым действием. */
|
||||
pop_sfx_pause();
|
||||
/* Без fade: старый кадр гасим сразу и обязательно даём
|
||||
* показать один чёрный растр ДО диска/перерисовки. */
|
||||
pop_pal_apply(4);
|
||||
pop_wait_edge();
|
||||
reveal_pending = 1;
|
||||
if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pop_level_switch() != 0 ||
|
||||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
if (reveal_pending) {
|
||||
/* Обе страницы и snapshot уже принадлежат новому уровню:
|
||||
* включить его корректную палитру одним переходом. */
|
||||
pop_pal_apply(0);
|
||||
reveal_pending = 0;
|
||||
}
|
||||
/* level_switch оставляет CBL закрытым: palette/fade идут через
|
||||
* BIOS и при открытом буфере давали скрежет на первом кадре. */
|
||||
(void)pop_sfx_start();
|
||||
gfx_set_draw_page(back);
|
||||
/* Смена уровня целиком уехала в банк 8 (pop_level_change): это
|
||||
* событие раз в несколько минут, а кода на нём было под сотню строк
|
||||
* в самом горячем месте программы — резидент W1 у нас в дефиците. */
|
||||
if (!is_demo && pop_next_level != pop_current_level &&
|
||||
pop_level_change(back) != 0) {
|
||||
failed = 1; quit = 1; break;
|
||||
}
|
||||
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
|
||||
dead_frames++;
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
|
||||
#include "pop_settings.h" /* pop_settings_defaults — стартовый профиль VANILLA */
|
||||
#include "pop_cfg.h" /* pop_cfg_load — POP.CFG поверх дефолтов */
|
||||
#include "pop_flow.h" /* таблица level-flow: какая сцена перед уровнем */
|
||||
#include "pop_intro.h" /* pop_pre_cutscene_show — сцена перед уровнем */
|
||||
#include "pop_app.h" /* pop_app_dispatch — состояния приложения */
|
||||
#include "pop_pal.h" /* pop_pal_apply — гашение перед загрузкой */
|
||||
#include "pop_timer.h" /* pop_timer_minutes — выбор сцены «времени мало» */
|
||||
#include "pop_ui.h" /* POP_PAL_DIM_BLACK */
|
||||
#include "pop_menu.h" /* пауза-меню: коды выбора */
|
||||
#include "pop_qsave.h" /* QuickSave/QuickLoad по F6/F9 */
|
||||
#include "pop_status.h" /* pop_status_invalidate после меню */
|
||||
#include <kbd_raw.h>
|
||||
#include "pop_status.h" /* статус-строка и отладочная полоса — банк 9 */
|
||||
#include "pop_menu.h" /* pop_menu_init/free — атлас UI в bank 9 */
|
||||
#include "pop_ui.h" /* ранняя чёрная палитра + snapshot стартового экрана */
|
||||
@@ -73,6 +83,134 @@ static void find_start_level_door(uint8_t room)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* МЕНЮ, F10 И QUICKSAVE/LOAD — граница кадра, но не горячий путь.
|
||||
*
|
||||
* Всё это события «раз в партию», а в главном цикле они занимали полсотни
|
||||
* строк резидента W1. Здесь же держим и edge-состояния клавиш: снаружи они
|
||||
* никому не нужны.
|
||||
*
|
||||
* restart_level — «пользователь выбрал Restart Level» (главный цикл сделает
|
||||
* это сам, после вычисления back-страницы, как респавн);
|
||||
* dead_reset — «QuickLoad удался, счётчик кадров смерти сбросить».
|
||||
* Возврат: 0 — продолжаем кадр, 1 — выйти из игрового цикла (Restart Game),
|
||||
* 2 — выйти и завершить работу (Quit), -1 — сбой диспетчера. */
|
||||
int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
{
|
||||
static uint8_t esc_prev, qs_save_prev, qs_load_prev;
|
||||
uint8_t e = kbd_raw_down(KBD_MENU);
|
||||
uint8_t menu_opened;
|
||||
int8_t mrc = POP_MENU_NONE;
|
||||
|
||||
if (e && !esc_prev) pop_menu_request = 1;
|
||||
esc_prev = e;
|
||||
menu_opened = pop_menu_request;
|
||||
if (menu_opened) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_PAUSE)) return -1;
|
||||
mrc = pop_menu_process();
|
||||
}
|
||||
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_BACKUP)
|
||||
pop_qsave_request_load_backup();
|
||||
else if (mrc == POP_MENU_RESTART_LEVEL) *restart_level = 1;
|
||||
else if (mrc == POP_MENU_RESTART_GAME)
|
||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_RESTART_INTRO) ? 1 : -1);
|
||||
else if (mrc == POP_MENU_QUIT)
|
||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
|
||||
if (menu_opened) {
|
||||
/* Меню переписало обе страницы целиком. Снимки слотов об этом не
|
||||
* знают и продолжают считать, что персонаж на странице уже
|
||||
* нарисован, — стоящий Кид оставался бы невидимым до первого своего
|
||||
* движения. */
|
||||
pop_cd_forget();
|
||||
pop_status_invalidate();
|
||||
if (!pop_app_dispatch(POP_APP_EV_RESUME)) return -1;
|
||||
}
|
||||
/* F10 — отладочный немедленный выход (раньше был на Esc, но Esc теперь
|
||||
* открывает меню; уйдёт вместе с отладкой). */
|
||||
if (kbd_raw_down(KBD_QUIT))
|
||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
|
||||
/* F6/F9 только ставят запрос; файловая операция выполняется здесь, на
|
||||
* границе кадров — вне окон Char и до heal/tick/draw. */
|
||||
{
|
||||
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
|
||||
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
||||
|
||||
if (s && !qs_save_prev) pop_qsave_request_save();
|
||||
if (l && !qs_load_prev) pop_qsave_request_load();
|
||||
qs_save_prev = s; qs_load_prev = l;
|
||||
if (pop_qsave_process() > 0) *dead_reset = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* СМЕНА УРОВНЯ — целиком здесь, а не в главном цикле.
|
||||
*
|
||||
* Событие редкое (раз в несколько минут игры), а кода на нём была без
|
||||
* малого сотня строк — и все они лежали в _main, то есть в резиденте W1,
|
||||
* где у нас счёт на десятки байт. Наружу остаётся одно условие и один
|
||||
* вызов.
|
||||
*
|
||||
* back — страница, в которую главный цикл рисует кадр: после загрузки
|
||||
* уровня её надо вернуть себе (level_switch показывает свою).
|
||||
* Возврат: 0 — уровень сменён, -1 — сбой, главный цикл завершает работу. */
|
||||
int8_t pop_level_change(uint8_t back) __banked
|
||||
{
|
||||
uint8_t reveal_pending = 0;
|
||||
pop_pre_cutscene_t pre = pop_flow_pre_cutscene(pop_next_level,
|
||||
pop_timer_minutes);
|
||||
|
||||
/* Загрузка файла уровня, спрайтов соперника и тайлсета — в
|
||||
* pop_level_switch ниже. До 2/4/6/8/9/12 таблица
|
||||
* flow запускает отдельное состояние CUTSCENE, как play_level()
|
||||
* SDLPoP: следующий уровень не трогаем до завершения/skip. */
|
||||
if (pre != POP_PRE_NONE) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED))
|
||||
return -1;
|
||||
pop_pre_cutscene_show(pre);
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED))
|
||||
return -1;
|
||||
/* Катсцена оставляет экран чёрным. Новый уровень будет
|
||||
* открыт одним применением его готовой палитры. */
|
||||
reveal_pending = 1;
|
||||
} else {
|
||||
/* CBL глушим ДО первой же операции с палитрой. Палитра идёт
|
||||
* через BIOS ($A4) с запрещёнными прерываниями: насос не
|
||||
* успевает долить блок, железо крутит хвост своего буфера —
|
||||
* на слух скрежет. Раньше pop_sfx_pause() звался только
|
||||
* внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и
|
||||
* переход без катсцены (в т.ч. телепорт Shift+L) скрежетал
|
||||
* почти всегда — поймано пользователем 2026-08-25. Ветка с
|
||||
* катсценой свободна от этого: pop_pre_cutscene_show глушит
|
||||
* звук первым действием. */
|
||||
pop_sfx_pause();
|
||||
/* Без fade: старый кадр гасим сразу и обязательно даём
|
||||
* показать один чёрный растр ДО диска/перерисовки. */
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
pop_wait_edge();
|
||||
reveal_pending = 1;
|
||||
if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED))
|
||||
return -1;
|
||||
}
|
||||
if (pop_level_switch() != 0 ||
|
||||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY))
|
||||
return -1;
|
||||
if (reveal_pending) {
|
||||
/* Обе страницы и snapshot уже принадлежат новому уровню:
|
||||
* включить его корректную палитру одним переходом. */
|
||||
pop_pal_apply(0);
|
||||
reveal_pending = 0;
|
||||
}
|
||||
/* level_switch оставляет CBL закрытым: palette/fade идут через
|
||||
* BIOS и при открытом буфере давали скрежет на первом кадре. */
|
||||
(void)pop_sfx_start();
|
||||
gfx_set_draw_page(back);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
|
||||
* (seg003:028A).
|
||||
*
|
||||
@@ -937,7 +1075,7 @@ int pop_boot(void) __banked
|
||||
* понадобится как источник обычного fade-in. Для build-info экран
|
||||
* остаётся штатный яркий игровой фон. */
|
||||
if (!pop_settings.show_build_info) {
|
||||
pop_pal_apply(4);
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
}
|
||||
|
||||
/* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново
|
||||
@@ -1091,7 +1229,7 @@ int pop_new_game_load(void) __banked
|
||||
/* Title мог завершиться на яркой title-палитре. Сначала погасить её,
|
||||
* затем восстановить полный game snapshot; level_switch собирает обе
|
||||
* страницы уже невидимо, и первый игровой кадр проявляется. */
|
||||
pop_pal_apply(4);
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
gfx_set_draw_page(0);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
pop_screen_fill_game();
|
||||
@@ -1099,7 +1237,7 @@ int pop_new_game_load(void) __banked
|
||||
pop_screen_fill_game();
|
||||
gfx_set_visible_page(1);
|
||||
pop_pal_game_load();
|
||||
pop_pal_apply(4);
|
||||
pop_pal_apply(POP_PAL_DIM_BLACK);
|
||||
if (pop_level_switch() != 0) return -1;
|
||||
/* Первый уровень появляется сразу, без fade, но только после того как
|
||||
* обе страницы и его собственная палитра полностью готовы. */
|
||||
|
||||
@@ -93,6 +93,23 @@ int pop_boot(void) __banked;
|
||||
* файла нет — молча остаться на текущем. -1 — фатально. */
|
||||
int pop_level_switch(void) __banked;
|
||||
|
||||
/* Смена уровня целиком (катсцена, гашение, загрузка, палитра, звук).
|
||||
* back — страница кадра главного цикла. 0 — ок, -1 — сбой. */
|
||||
int8_t pop_level_change(uint8_t back) __banked;
|
||||
|
||||
/* Клавиши служебного слоя. Живут здесь, а не в roomtest.c: сам обработчик
|
||||
* (pop_frame_ui) переехал в банк, а главный цикл о них больше не знает. */
|
||||
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
|
||||
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
|
||||
#define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */
|
||||
#define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc,
|
||||
* пока меню не готово рисовать — см. план MS4) */
|
||||
|
||||
/* Меню, F10 и QuickSave/Load на границе кадра. restart_level/dead_reset —
|
||||
* флаги для главного цикла. 0 — продолжаем, 1 — Restart Game, 2 — Quit,
|
||||
* -1 — сбой. */
|
||||
int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked;
|
||||
|
||||
/* Чистый старт настоящей игры после Enter/Esc на title/intro/credits либо
|
||||
* клавиши в attract-demo. Ждёт отпускания клавиши, сбрасывает состояние
|
||||
* новой игры, загружает FIRST_LEVEL и проявляет его первый кадр. */
|
||||
|
||||
Reference in New Issue
Block a user