Files
Sprinter-SDCC/applications/PoP/roomtest/pop_status.c
T
snark13 5756101d29 Статус-строка: 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>
2026-08-25 14:13:48 +03:00

367 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_status.c — текст в двух служебных полосах экрана, БАНК 9.
*
* Нижняя полоса (та же, где деления HP) — статус-строка оригинала;
* верхняя — наша отладочная. Обе живут ВНЕ игрового поля, поэтому фон у
* них POP_COL_OUTSIDE, а не цвет 0: вспышки (урон, зелья, молния) сделаны
* подменой нулевого цвета палитры, и полосы не должны мигать вместе с
* игрой (pop_ui.h).
*
* ЦЕНА ВЫВОДА — главное ограничение. Блит одного глифа стоит ~4,6 тыс.
* тактов почти независимо от размера (см. memory blit_cost_model): цена
* вызова, а не пикселей. Полная отладочная строка — это полсотни глифов,
* то есть ПОЛКАДРА. Отсюда три правила, на которых держится модуль:
*
* 1. Всё change-driven. Пока показанное не изменилось, не рисуем вовсе
* — за кадр остаётся только сравнение нескольких байт.
* 2. Отладочная строка перерисовывается ПО ПОЛЯМ. Смена комнаты — это
* две цифры (~9 тыс. тактов, 2% кадра), а не вся строка; подписи
* («Level», «, Room», …) не меняются никогда и рисуются лишь при
* полной инвалидации, которая случается на старте уровня, где кадр и
* так тяжёлый.
* 3. Пробелы не блитятся (pop_ui.c): их глиф целиком прозрачен, и на
* строке из полусотни символов это девять сэкономленных блитов.
*
* ГРЯЗЬ СЧИТАЕТСЯ ПО СТРАНИЦАМ, а не счётчиком «осталось два кадра». У
* каждой страницы дабл-буфера своя видео-ОЗУ, и если поля меняются в
* разных кадрах вперемешку, общий счётчик их путает: маска на страницу
* всегда описывает, что именно этой странице ещё должны.
*
* Рисуем прозрачным банком GFX_BANK_TRANSPARENT (не SPRITE): 0xFF шрифта
* не пишется, но ОЗУ-копия обновляется — и текст переживает полностраничные
* копии (вход в комнату, выход из меню), которые берут картинку из тени.
*
* Данных в W2 — 11 байт, и семь из них просто переехали сюда из прежнего
* ярлыка комнаты в roomtest_cold.c. Кода в резиденте не появляется вовсе:
* заявка сообщения — присваивание одного байта, всё остальное здесь.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include "pop_status.h"
#include "pop_ui.h"
#include "pop_font.h"
#include "pop_bg.h" /* POP_YOFF */
#include "pop_cdraw.h" /* POP_HP_Y — полосу делим с делениями жизней */
#include "pop_settings.h"
#include "pop_state.h" /* pop_immortal */
#include "pop_level.h" /* pop_current_level */
#include "pop_pace.h" /* pop_speed_mode */
#include "pop_sfx.h" /* pop_snd_want */
/* ---- нижняя полоса: статус-строка -------------------------------------- */
/* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */
#define MSG_TICKS 24
/* Базовая линия та же, что у GAME PAUSED (pop_menu.c): строка ниже делений. */
#define MSG_BASE (POP_YOFF + POP_HP_Y + 8)
#define MSG_TOP (MSG_BASE - POP_FONT_BIG_ASCENT - 2)
#define MSG_BOTTOM (MSG_BASE)
/* POP_MSG_LEVEL здесь пустой: его строка собирается с номером уровня. */
static const char * const status_text[POP_MSG_N] = {
"",
"QUICKSAVE",
"NO QUICKSAVE",
"QUICKLOAD",
"NO QUICKLOAD",
"SOUND ON",
"SOUND OFF",
""
};
uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */
uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */
static uint8_t st_shown; /* что реально нарисовано */
static uint8_t st_dirty; /* бит на страницу дабл-буфера */
static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL */
/* ---- верхняя полоса: отладочная строка --------------------------------- *
*
* Level ##, Room ##, Speed: ..., Sound: ..., Immortal #
*
* Раскладка фиксированная: у каждого поля свой левый край и своя зона
* стирания, поэтому изменение одного значения не трогает соседей. Ширины
* посчитаны по самому длинному значению (FASTEST, OFF, две цифры). */
#define DBG_BASE 6 /* базовая линия малого шрифта */
#define DBG_TOP 0
/* На строку НИЖЕ базовой линии: у малого шрифта 'p' высотой 7 (и ',' — 6)
* свисает под неё, и при стирании ровно до базовой линии от хвоста «p» в
* «Speed:» оставалась одинокая точка. */
#define DBG_BOTTOM 7
#define DBG_LEFT 8
#define DBG_RIGHT 254
/* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля
* начинается сразу за ней. Так запятая всегда стоит вплотную к значению —
* при выравнивании влево она отрывалась бы на всю ширину зоны («Level 1 ,
* Room»). Дырка слева от значения при этом крошечная: варианты близки по
* ширине (NORMAL 30 против FASTEST 32, ON 9 против OFF 14). */
#define X_LBL_LEVEL 10 /* "Level" 10..30 */
#define R_VAL_LEVEL 44 /* зона 31..43 */
#define X_LBL_ROOM 44 /* ", Room" 44..70 */
#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: цифре нужен зазор в пробел */
/* Ширина зоны значения — под самый длинный вариант плюс воздух. */
#define W_VAL_NUM 13 /* две цифры */
#define W_VAL_SPEED 35 /* FASTEST */
#define W_VAL_SOUND 17 /* OFF */
#define W_VAL_IMM 9 /* одна цифра */
enum { /* биты маски «что перерисовать» */
DBG_F_LEVEL = 0x01,
DBG_F_ROOM = 0x02,
DBG_F_SPEED = 0x04,
DBG_F_SOUND = 0x08,
DBG_F_IMM = 0x10,
DBG_F_ALL = 0x1F /* + подписи: см. 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_dirty[2]; /* маска по странице; DBG_F_ALL => и подписи */
static uint8_t dbg_on; /* показанное состояние тумблера Settings */
/* ---- общее ------------------------------------------------------------- */
static void wipe(int left, int top, int right, int bottom)
{
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
bar(left, top, right, bottom);
}
/* Число 0..99 БЕЗ ведущего нуля и без ведущего пробела: значение
* выравнивается вправо, а пробел в шрифте имеет ненулевую ширину и сдвинул
* бы цифру влево от границы зоны. Деления нет намеренно: у SDCC z80 один
* «/» стоит ~5,4 тыс. тактов (memory sdcc_z80_division_hoisting), здесь же
* максимум девять вычитаний. */
static void dec2(uint8_t v, char *buf)
{
uint8_t t = 0;
while (v >= 10) { v = (uint8_t)(v - 10); t++; }
if (t) { *buf++ = (char)('0' + t); }
buf[0] = (char)('0' + v);
buf[1] = 0;
}
/* ---- нижняя полоса ----------------------------------------------------- */
/* «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)
{
char buf[9];
const char *text;
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
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);
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, (POP_STATUS_L + POP_STATUS_R) / 2,
MSG_BASE, text);
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 — её ширина. */
static void dbg_value(int right, uint8_t w, const char *s)
{
wipe(right - w, DBG_TOP, right - 1, DBG_BOTTOM);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_right_mapped(POP_TEXT_SMALL, right - 1, DBG_BASE, s);
pop_text_unmap();
}
/* Подписи неизменны — рисуем их только при полной перерисовке полосы. */
static void dbg_labels(void)
{
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
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_unmap();
}
static void dbg_draw(uint8_t mask)
{
char buf[3];
/* Полная перерисовка стирает всю полосу разом — значит зоны значений
* стирать по второму разу не надо, но нарисовать их всё равно нужно. */
if (mask == DBG_F_ALL) dbg_labels();
if (mask & DBG_F_LEVEL) {
dec2(dbg_lvl, buf);
dbg_value(R_VAL_LEVEL, W_VAL_NUM, buf);
}
if (mask & DBG_F_ROOM) {
dec2(dbg_room, buf);
dbg_value(R_VAL_ROOM, W_VAL_NUM, buf);
}
if (mask & DBG_F_SPEED)
dbg_value(R_VAL_SPEED, W_VAL_SPEED,
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_IMM) {
buf[0] = (char)('0' + (dbg_imm < 10 ? dbg_imm : 9));
buf[1] = 0;
dbg_value(R_VAL_IMM, W_VAL_IMM, buf);
}
}
/* Снимок ДО сравнения — обход sdcc_z80_cmp_store_a_bug: `if (n != s) s = n;`
* SDCC z80 собирает неверно (SUB затирает A, в снимок уезжает разность). */
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 mask = 0;
dbg_lvl = pop_current_level;
dbg_room = room;
dbg_spd = pop_speed_mode;
dbg_snd = pop_snd_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_imm != prev_imm) mask |= DBG_F_IMM;
if (mask) {
dbg_dirty[0] |= mask;
dbg_dirty[1] |= mask;
}
}
/* ---- кадровая точка ---------------------------------------------------- */
void pop_status_invalidate(void) __banked
{
st_dirty = 3;
dbg_dirty[0] = DBG_F_ALL;
dbg_dirty[1] = DBG_F_ALL;
}
void pop_status_frame(uint8_t room) __banked
{
uint8_t page, bit, saved;
uint8_t want = pop_settings.show_debug_bar;
if (!pop_ui_ready()) return;
page = (uint8_t)(gfx_get_draw_page() & 1);
bit = (uint8_t)(1 << page);
saved = gfx_get_bank();
/* --- нижняя полоса: заявка, таймер, перерисовка --- */
if (pop_status_msg != st_shown) {
st_shown = pop_status_msg;
pop_status_ticks = (uint8_t)(st_shown ? MSG_TICKS : 0);
st_dirty = 3;
} else if (pop_status_ticks) {
pop_status_ticks--;
if (!pop_status_ticks) {
/* Оригинал по истечении зовёт erase_bottom_text(1) — снимаем и
* заявку, иначе то же сообщение никогда не покажется снова. */
pop_status_msg = 0;
st_shown = 0;
st_dirty = 3;
}
}
if (st_dirty & bit) {
status_draw();
st_dirty = (uint8_t)(st_dirty & ~bit);
}
/* --- верхняя полоса --- */
if (want != dbg_on) {
dbg_on = want;
/* Выключили — полосу надо стереть на ОБЕИХ страницах, иначе её
* остаток висел бы до следующей смены комнаты. */
dbg_dirty[0] = DBG_F_ALL;
dbg_dirty[1] = DBG_F_ALL;
}
if (!want) {
if (dbg_dirty[page]) {
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
dbg_dirty[page] = 0;
}
} else {
dbg_scan(room);
if (dbg_dirty[page]) {
dbg_draw(dbg_dirty[page]);
dbg_dirty[page] = 0;
}
}
gfx_set_bank(saved);
}