Статус-строка: LEVEL N и лейбл QS/QL до дисковой операции

* pop_status_level() — порт show_level (seg008:25A8): «LEVEL N» на старте
  уровня.  Демо-уровень 0 и номера от 14 молчат, тринадцатый показывается
  двенадцатым, бесшовный переход 12->13 пропускается и гасит флаг за собой.
  Первое сообщение с ПАРАМЕТРОМ: строка собирается вручную (dec2, без
  printf и без деления), номер хранится снимком st_arg — иначе вторая
  страница дабл-буфера нарисовала бы другое число.
* pop_status_show_now() — печать немедленно, в ВИДИМУЮ страницу.
  QuickSave/QuickLoad заявляют лейбл ПЕРВЫМ действием, до mem_alloc/ESTEX:
  запись снимка занимает доли секунды, и раньше игрок видел сначала
  необъяснённый фриз, а надпись — уже после него.  Отказ переписывает
  строку на NO QUICKSAVE/NO QUICKLOAD обычной заявкой.
  Расхождение с оригиналом (он печатает по результату) — impl_diff.md.

Проверено в MAME: watchpoint ловит заявку внутри pop_status_show_now,
step_out возвращает в pop_qsave_process — и QUICKSAVE уже на экране, диск
ещё не тронут.  Бюджет прежний: _CODE 23981, куча 266 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 14:13:48 +03:00
parent 47a4b084c4
commit 5756101d29
6 changed files with 130 additions and 19 deletions
+26
View File
@@ -577,3 +577,29 @@ seg008.c:1600). XOR идёт по 24-битному RGB того, что УЖЕ
кайма станет резать глаз — вариантов два: запечь второй набор под светлый кайма станет резать глаз — вариантов два: запечь второй набор под светлый
фон (ещё 32 страницы EMM) или считать эту кайму прозрачной (силуэт станет фон (ещё 32 страницы EMM) или считать эту кайму прозрачной (силуэт станет
на пиксель уже). Оба хуже нынешнего; трогать только по факту жалобы. на пиксель уже). Оба хуже нынешнего; трогать только по факту жалобы.
## QuickSave/QuickLoad: лейбл печатается ДО дисковой операции, а не после
**Как в оригинале.** SDLPoP печатает `QUICKSAVE` / `NO QUICKSAVE` (и пару
для загрузки) уже ПО РЕЗУЛЬТАТУ операции — `process_quicksave` (seg000:497)
сначала делает save/load, потом зовёт `display_text_bottom` и ставит
`text_time_total = 24`. На PC это незаметно: файл пишется мгновенно.
**У нас.** `pop_qsave_process` заявляет строку ПЕРВЫМ действием, ещё до
`mem_alloc_pages`/ESTEX, через `pop_status_show_now()` — та печатает её
немедленно в ВИДИМУЮ страницу, не дожидаясь конца кадра. Отказ уже потом
переписывает строку на `NO QUICKSAVE`/`NO QUICKLOAD` обычной заявкой.
**Зачем.** Запись снимка на диск занимает доли секунды, и всё это время
игра стоит. При порядке оригинала игрок видел сначала необъяснённый фриз,
и только по его окончании — надпись, объясняющую то, что уже прошло.
Решение пользователя, 2026-08-25.
**Чем платим.** Строка успевает мигнуть даже там, где операция потом не
удалась: сначала `QUICKSAVE`, следом `NO QUICKSAVE`. На практике отказ —
редкость (нет места/диска), и «заявка → отказ» читается не хуже.
**Что проверять при регрессе.** Что после неудачной операции на экране
остаётся именно `NO QUICKSAVE`/`NO QUICKLOAD`, а не первая строка: отказ
идёт обычной заявкой и печатается кадровым проходом, то есть на кадр позже.
+13 -8
View File
@@ -139,18 +139,23 @@ display_text_bottom: draw_rect(чёрным) + show_text(halign_center, valign_b
## 4. Что из этого касается нашего порта ## 4. Что из этого касается нашего порта
Реализовано: Реализовано (`pop_status.c`, таймер 24 тика как у оригинала):
* `GAME PAUSED`в строке HP, без таймера (`pop_menu.c`). * `GAME PAUSED` — без таймера, рисует само меню (`pop_menu.c`);
* `QUICKSAVE` / `NO QUICKSAVE`, `QUICKLOAD` / `NO QUICKLOAD` — заявка стоит
в `pop_qsave_process`, то есть в единственном месте, где известно, что
именно делали. Лейбл печатается ДО дисковой операции — осознанное
расхождение, см. `impl_diff.md`;
* `SOUND ON` / `SOUND OFF` — Ctrl+S;
* `LEVEL %d` — порт `show_level()` целиком: демо-уровень 0 и номера от 14
молчат, тринадцатый показывается двенадцатым, бесшовный переход 12→13
пропускается и гасит флаг за собой.
Есть у нас, но сообщение пока НЕ печатается (показывается иначе или не Пока НЕ печатается:
показывается вовсе):
* `LEVEL %d` и вся группа времени — таймер уже есть (`pop_timer.c`), текста нет; * вся группа времени — таймер есть (`pop_timer.c`), текста нет;
* `QUICKSAVE`/`QUICKLOAD` — у нас вместо строки экран-уведомление (`ui_notice`);
* `SOUND ON/OFF` — у нас индикатор в отладочной полосе, а не текст;
* `Press Button to Continue` — у нас авто-респавн по таймеру/стрелке вверх; * `Press Button to Continue` — у нас авто-респавн по таймеру/стрелке вверх;
* номера комнат (`C`/Shift+`C`) — у нас палочки в отладочной полосе. * номера комнат (`C`/Shift+`C`) — у нас отдельная отладочная строка.
Нам не нужно: copy protection (уровень 15 исключён из порта — см. Нам не нужно: copy protection (уровень 15 исключён из порта — см.
`full_game_plan.md`), joystick-режимы, replay, скриншоты. `full_game_plan.md`), joystick-режимы, replay, скриншоты.
+10 -8
View File
@@ -225,6 +225,11 @@ int8_t pop_qsave_process(void) __banked
int8_t rc=-1; int8_t rc=-1;
pop_qsave_request = 0; pop_qsave_request = 0;
if (!request) return 0; if (!request) return 0;
/* Лейбл — ДО дисковой работы: она длится доли секунды, игра на это
* время замирает, и строка обязана объяснить фриз, а не подводить под
* ним итог. Не удалось — ниже перепишем на NO QUICKSAVE/QUICKLOAD. */
pop_status_show_now((uint8_t)(request == QS_SAVE ? POP_MSG_QUICKSAVE
: POP_MSG_QUICKLOAD));
blk=mem_alloc_pages(1); blk=mem_alloc_pages(1);
if (!blk) return -1; if (!blk) return -1;
page=mem_get_page(blk,0); page=mem_get_page(blk,0);
@@ -238,14 +243,11 @@ int8_t pop_qsave_process(void) __banked
} }
pop_sfx_start(); pop_sfx_start();
mem_free_block(blk); mem_free_block(blk);
/* Сообщение заявляем ЗДЕСЬ, а не у вызывающего: только тут известно, /* Успех ничего не меняет: нужная строка висит с начала операции.
* что именно делали (F6/F9 и оба пункта меню приходят одним запросом). * Отказ переписывает её на NO QUICKSAVE/NO QUICKLOAD — обычной
* Порт seg000:497/514 — те же четыре строки, что и в оригинале. */ * заявкой, кадр её подхватит (порт seg000:497/514). */
if (request == QS_SAVE) if (rc <= 0)
pop_status_msg = (uint8_t)(rc > 0 ? POP_MSG_QUICKSAVE pop_status_msg = (uint8_t)(request == QS_SAVE ? POP_MSG_NO_QUICKSAVE
: POP_MSG_NO_QUICKSAVE);
else
pop_status_msg = (uint8_t)(rc > 0 ? POP_MSG_QUICKLOAD
: POP_MSG_NO_QUICKLOAD); : POP_MSG_NO_QUICKLOAD);
return rc; return rc;
} }
+65 -2
View File
@@ -58,6 +58,7 @@
#define MSG_TOP (MSG_BASE - POP_FONT_BIG_ASCENT - 2) #define MSG_TOP (MSG_BASE - POP_FONT_BIG_ASCENT - 2)
#define MSG_BOTTOM (MSG_BASE) #define MSG_BOTTOM (MSG_BASE)
/* POP_MSG_LEVEL здесь пустой: его строка собирается с номером уровня. */
static const char * const status_text[POP_MSG_N] = { static const char * const status_text[POP_MSG_N] = {
"", "",
"QUICKSAVE", "QUICKSAVE",
@@ -65,13 +66,15 @@ static const char * const status_text[POP_MSG_N] = {
"QUICKLOAD", "QUICKLOAD",
"NO QUICKLOAD", "NO QUICKLOAD",
"SOUND ON", "SOUND ON",
"SOUND OFF" "SOUND OFF",
""
}; };
uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */ uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */
uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */ uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */
static uint8_t st_shown; /* что реально нарисовано */ static uint8_t st_shown; /* что реально нарисовано */
static uint8_t st_dirty; /* бит на страницу дабл-буфера */ static uint8_t st_dirty; /* бит на страницу дабл-буфера */
static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL */
/* ---- верхняя полоса: отладочная строка --------------------------------- * /* ---- верхняя полоса: отладочная строка --------------------------------- *
* *
@@ -151,17 +154,77 @@ static void dec2(uint8_t v, char *buf)
/* ---- нижняя полоса ----------------------------------------------------- */ /* ---- нижняя полоса ----------------------------------------------------- */
/* «LEVEL N» собирается вручную: printf сюда не тянем, а номер — единственное
* переменное место во всём наборе сообщений. */
static void level_text(char *buf)
{
buf[0] = 'L'; buf[1] = 'E'; buf[2] = 'V'; buf[3] = 'E'; buf[4] = 'L';
buf[5] = ' ';
dec2(st_arg, buf + 6);
}
static void status_draw(void) static void status_draw(void)
{ {
char buf[9];
const char *text;
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM); wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
if (!st_shown) return; if (!st_shown) return;
if (st_shown == POP_MSG_LEVEL) { level_text(buf); text = buf; }
else { text = status_text[st_shown]; }
gfx_set_bank(GFX_BANK_TRANSPARENT); gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map(); pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, (POP_STATUS_L + POP_STATUS_R) / 2, pop_text_center_mapped(POP_TEXT_BIG, (POP_STATUS_L + POP_STATUS_R) / 2,
MSG_BASE, status_text[st_shown]); MSG_BASE, text);
pop_text_unmap(); pop_text_unmap();
} }
/* Порт show_level (seg008:25A8). Демо-уровень 0 и номера от 14 не
* показываются вовсе, тринадцатый представляется двенадцатым, а бесшовный
* переход 12->13 молчит — и, как в оригинале, гасит флаг за собой. */
void pop_status_level(void) __banked
{
uint8_t n = pop_current_level;
uint8_t quiet = pop_seamless;
pop_seamless = 0;
if (quiet || n == 0 || n >= POP_LEVEL_LAST) return;
if (n == 13) n = 12;
st_arg = n;
pop_status_msg = POP_MSG_LEVEL;
}
/* Показать сообщение НЕМЕДЛЕННО, не дожидаясь конца кадра.
*
* Обычный путь (заявка в pop_status_msg + pop_status_frame) печатает строку
* там же, где рисуется кадр, — то есть уже ПОСЛЕ длинной операции. На
* QuickSave это выглядело так: игра замирает на полсекунды, и только потом
* появляется «QUICKSAVE», когда объяснять фриз уже поздно. Здесь строка
* выкладывается сразу, до дисковой работы.
*
* Рисуем в ВИДИМУЮ страницу: игрок смотрит именно на неё, а до следующего
* флипа кадра ещё вся операция. Скрытой странице сообщение остаётся
* должным — её закроет обычный кадровый проход. */
void pop_status_show_now(uint8_t msg) __banked
{
uint8_t visible, saved_page, saved_bank;
pop_status_msg = msg;
if (!pop_ui_ready()) return; /* шрифта нет — останется заявкой */
st_shown = msg;
pop_status_ticks = (uint8_t)(msg ? MSG_TICKS : 0);
st_dirty = 3;
visible = (uint8_t)(gfx_get_visible_page() & 1);
saved_page = gfx_get_draw_page();
saved_bank = gfx_get_bank();
gfx_set_draw_page(visible);
status_draw();
st_dirty = (uint8_t)(st_dirty & ~(1 << visible));
gfx_set_draw_page(saved_page);
gfx_set_bank(saved_bank);
}
/* ---- верхняя полоса ---------------------------------------------------- */ /* ---- верхняя полоса ---------------------------------------------------- */
/* right — граница зоны (первый столбец СЛЕДУЮЩЕЙ подписи), w — её ширина. */ /* right — граница зоны (первый столбец СЛЕДУЮЩЕЙ подписи), w — её ширина. */
+12
View File
@@ -26,6 +26,7 @@ enum {
POP_MSG_NO_QUICKLOAD, /* F9 нечего грузить / битый слот */ POP_MSG_NO_QUICKLOAD, /* F9 нечего грузить / битый слот */
POP_MSG_SOUND_ON, /* Ctrl+S */ POP_MSG_SOUND_ON, /* Ctrl+S */
POP_MSG_SOUND_OFF, POP_MSG_SOUND_OFF,
POP_MSG_LEVEL, /* «LEVEL N» — строка собирается с номером */
POP_MSG_N POP_MSG_N
}; };
@@ -34,6 +35,17 @@ enum {
* только если строка уже погасла (как у оригинала: он тоже сравнивает). */ * только если строка уже погасла (как у оригинала: он тоже сравнивает). */
extern uint8_t pop_status_msg; extern uint8_t pop_status_msg;
/* Показать сообщение НЕМЕДЛЕННО, до конца кадра: строка выкладывается в
* видимую страницу прямо сейчас. Нужна там, где сразу за вызовом идёт
* длинная операция (QuickSave/QuickLoad): обычная заявка напечаталась бы
* уже ПОСЛЕ фриза, когда объяснять его поздно. */
void pop_status_show_now(uint8_t msg) __banked;
/* Порт show_level (seg008:25A8): заявить «LEVEL N» на старте уровня.
* Сама решает, показывать ли (демо-уровень и финальные номера молчат) —
* вызывающему условий знать не нужно. */
void pop_status_level(void) __banked;
/* Сколько логических кадров сообщение ещё висит; 0 — не висит. Читает /* Сколько логических кадров сообщение ещё висит; 0 — не висит. Читает
* pop_hp_draw (банк 4), чтобы не затирать чужой текст своим фоном. */ * pop_hp_draw (банк 4), чтобы не затирать чужой текст своим фоном. */
extern uint8_t pop_status_ticks; extern uint8_t pop_status_ticks;
@@ -1052,6 +1052,9 @@ int pop_level_switch(void) __banked
gfx_set_draw_page(old_page); gfx_set_draw_page(old_page);
full.x = 0; full.y = 0; full.w = 320; full.h = 256; full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT); gfx_copy_page(&full, GFX_COPY_DIRECT);
/* Уровень готов и показан — заявить «LEVEL N» (порт
* show_level из draw_level_first, seg003:222). */
pop_status_level();
} }
/* CBL намеренно НЕ открываем здесь. После загрузки caller ещё /* CBL намеренно НЕ открываем здесь. После загрузки caller ещё
* может делать BIOS fade/palette либо восстанавливать QuickSave; * может делать BIOS fade/palette либо восстанавливать QuickSave;