/* * 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 #include #include #include #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_app.h" /* start_game после 288 тиков молчания */ #include "pop_state.h" /* pop_immortal */ #include "pop_level.h" /* pop_current_level */ #include "pop_pace.h" /* pop_speed_mode */ #include "pop_timer.h" /* оставшееся время и флаг «пора показать» */ #include "pop_sfx.h" /* pop_snd_want */ /* ---- нижняя полоса: статус-строка -------------------------------------- */ /* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */ #define MSG_TICKS 24 /* «Висит, пока не снимут»: у оригинала эту роль играет особое значение * text_time_total (288 у «Press Button», 1188 у copy protection). */ #define MSG_HOLD 0xFF /* Базовая линия та же, что у 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", "MUSIC ON", "MUSIC OFF", "", /* LEVEL N — собирается с номером */ "", /* N MINUTES LEFT */ "", /* N SECONDS LEFT */ "TIME HAS EXPIRED!", "Press Button to Continue" }; 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 310 /* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля * начинается сразу за ней. Так запятая всегда стоит вплотную к значению — * при выравнивании влево она отрывалась бы на всю ширину зоны («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 */ /* Три тумблера подряд — звук, музыка, бессмертие. Подписи сокращены до * буквы с двоеточием: полные слова («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 */ #define W_VAL_TIME 26 /* Ширина зоны значения — под самый длинный вариант плюс воздух. */ #define W_VAL_NUM 13 /* две цифры */ #define W_VAL_SPEED 35 /* FASTEST */ #define W_VAL_SOUND 16 /* 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_TIME = 0x20, 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_mus, dbg_imm; static uint8_t dbg_tmin, dbg_tsec; /* показанное время ##:## */ static uint8_t dbg_phase; /* кадры до пересчёта секунд */ 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; } /* То же, но ВСЕГДА две цифры: часы-минуты без ведущего нуля не читаются. */ static void dec2z(uint8_t v, char *buf) { uint8_t t = 0; while (v >= 10) { v = (uint8_t)(v - 10); t++; } buf[0] = (char)('0' + t); buf[1] = (char)('0' + v); buf[2] = 0; } /* ---- нижняя полоса ----------------------------------------------------- */ /* Строки с числом собираются вручную: printf сюда не тянем, а число — * единственное переменное место во всём наборе сообщений. */ static char *put(char *buf, const char *s) { while (*s) *buf++ = *s++; return buf; } /* Число ВПЕРЕДИ («12 MINUTES LEFT»), как у оригинала. */ static void num_text(char *buf, const char *suffix) { dec2(st_arg, buf); while (*buf) buf++; *put(buf, suffix) = 0; } static void build_text(char *buf) { if (st_shown == POP_MSG_LEVEL) { *put(put(buf, "LEVEL "), "") = 0; dec2(st_arg, buf + 6); } else if (st_shown == POP_MSG_MINUTES) { num_text(buf, " MINUTES LEFT"); } else if (st_arg == 1) { *put(buf, "1 SECOND LEFT") = 0; } else { num_text(buf, " SECONDS LEFT"); } } /* Стереть полосу сообщения на ТЕКУЩЕЙ странице. Нужна меню: оно пишет в ту * же полосу своё «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]; const char *text; wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM); if (!st_shown) return; if (st_shown >= POP_MSG_LEVEL && st_shown <= POP_MSG_SECONDS) { build_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(); } /* Заявка сообщения С ЧИСЛОМ. Обычная (байтовая) заявка сравнивает только * номер сообщения, а здесь меняться может одно лишь число — «12 SECONDS * LEFT» и «11 SECONDS LEFT» это один и тот же POP_MSG_SECONDS, и по номеру * их не различить. Поэтому ставим состояние сразу и сами взводим грязь. */ static void msg_set(uint8_t msg, uint8_t arg, uint8_t ticks) { pop_status_msg = msg; st_shown = msg; st_arg = arg; pop_status_ticks = ticks; st_dirty = 3; } /* Порт show_level (seg008:25A8). Демо-уровень 0 и номера от 14 не * показываются вовсе, тринадцатый представляется двенадцатым, а бесшовный * переход 12->13 молчит — и, как в оригинале, гасит флаг за собой. * Следом оригинал просит показать и время: за «LEVEL 1» идёт «60 MINUTES * LEFT», когда первая строка отвисит своё. */ 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; msg_set(POP_MSG_LEVEL, n, MSG_TICKS); pop_show_time = 1; } /* Вторая половина show_time (seg008:1812): собственно печать. Флаг * pop_show_time == 2 означает «перебить текущую строку немедленно» — * так оригинал ведёт себя в последнюю минуту и после читов времени. */ static void time_msg(void) { if (!pop_show_time) return; if (pop_show_time == 2) pop_status_ticks = 0; else if (pop_status_ticks) return; /* ждём, пока освободится строка */ pop_show_time = 0; if (pop_timer_minutes == 0) { msg_set(POP_MSG_EXPIRED, 0, MSG_TICKS); } else if (pop_timer_minutes == 1) { /* Секунда — 12 тиков; +1 потому, что ticks уже уменьшен. */ uint8_t sec = (uint8_t)((pop_timer_ticks + 1) / 12); msg_set(POP_MSG_SECONDS, sec, (uint8_t)(sec == 1 ? 12 : MSG_TICKS)); } else { msg_set(POP_MSG_MINUTES, pop_timer_minutes, MSG_TICKS); } } /* Показать сообщение НЕМЕДЛЕННО, не дожидаясь конца кадра. * * Обычный путь (заявка в 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(); /* Бит видимой страницы НЕ снимаем. За время дисковой операции ту же * страницу успевает переписать восстановление комнаты, и снятый бит * означал бы «здесь уже нарисовано» — надпись оставалась ровно на одной * странице и мигала через кадр (поймано на QUICKLOAD). Лишняя * перерисовка строки стоит дешевле мигания. */ 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, ", 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(); } static void dbg_draw(uint8_t mask) { char buf[6]; /* Полная перерисовка стирает всю полосу разом — значит зоны значений * стирать по второму разу не надо, но нарисовать их всё равно нужно. */ 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_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; dbg_value(R_VAL_IMM, W_VAL_IMM, buf); } if (mask & DBG_F_TIME) { dec2z(dbg_tmin, buf); buf[2] = ':'; dec2z(dbg_tsec, buf + 3); dbg_value(R_VAL_TIME, W_VAL_TIME, 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_mus = dbg_mus, prev_imm = dbg_imm; uint8_t prev_tmin = dbg_tmin, prev_tsec = dbg_tsec; uint8_t mask = 0; /* Часы-минуты: у оригинала rem_min — это НОМЕР идущей минуты, а не * остаток целых минут (старт 60 при rem_tick 719 = «почти 60:00»), * поэтому на табло уходит minutes-1 и секунды из тиков. * * Секунды — единственное деление на этом пути, поэтому считаем их не * каждый кадр, а раз в двенадцать (тик секунды у SDLPoP) и вдобавок * сразу, если минуты изменились (чит времени, загрузка снимка). */ { uint8_t mm = (uint8_t)(pop_timer_minutes ? pop_timer_minutes - 1 : 0); if (dbg_phase) dbg_phase--; if (!dbg_phase || dbg_tmin != mm) { dbg_phase = 12; dbg_tsec = (uint8_t)(pop_timer_minutes ? pop_timer_ticks / 12 : 0); } dbg_tmin = mm; } dbg_lvl = pop_current_level; 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) { dbg_dirty[0] |= mask; dbg_dirty[1] |= mask; } } /* ---- смерть Кида ------------------------------------------------------- * * * Порт play_kid (seg006:1383): вместо авто-респавна по таймеру уровень * перезапускает сам игрок кнопкой, а строка висит, пока он этого не сделал * (MSG_HOLD). Авто-рестарт оригинала по истечении 288 тиков нам не нужен: * он там начинает игру заново, а мы возвращаем на чекпойнт. */ #define DEAD_SETTLE 7 /* Kid.alive > 6: труп успел лечь */ #define DEAD_TICKS 288 /* text_time_total у «Press Button» */ #define DEAD_BLINK 72 /* последние 72 тика строка мигает */ #define SND_BLINK 38 /* sound_38_blink на каждом появлении */ static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */ uint8_t pop_dead_prompt(uint16_t frames) __banked { uint16_t rem; if (frames < DEAD_SETTLE) return 0; if (frames == DEAD_SETTLE) { msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD); pop_show_time = 0; /* иначе поверх ляжет время (seg006:1365) */ dead_armed = 0; return 0; } /* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288. * Кадры смерти считает вызывающий, так что хватает вычитания. */ rem = (uint16_t)(frames - DEAD_SETTLE); rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem); if (rem == 0) { /* Игрок промолчал все 24 секунды — оригинал зовёт start_game(), * то есть уходит в title, а НЕ перезапускает уровень. Событие * шлём отсюда: pop_app живёт в этом же банке, и резиденту остаётся * один break вместо вызова с проверкой. */ msg_set(POP_MSG_NONE, 0, 0); (void)pop_app_dispatch(POP_APP_EV_RESTART_INTRO); return 2; } /* Мигание последних шести секунд: 4 тика видно, 8 нет; на кадре * появления оригинал перепечатывает строку и играет звук 38. */ if (rem < DEAD_BLINK) { uint8_t phase = (uint8_t)(rem % 12); if (phase > 3) { if (st_shown) msg_set(POP_MSG_NONE, 0, 0); } else if (phase == 3 && !st_shown) { msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD); pop_sfx_play(SND_BLINK); } } /* Рестарт уровня — Enter или Shift (seg000:584 подменяет их на Ctrl+A), * а не любая клавиша. Esc сюда не попадает: он открывает меню и * разобран раньше в этом же кадре. * * Отпускания оригинал не ждёт, а мы ждём: Shift у нас — клавиша * действия, и зажатый в момент смерти он перезапустил бы уровень * мгновенно, не дав прочитать строку. */ { uint8_t any = (uint8_t)(kbd_raw_down(KBD_ENTER) || kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)); if (!dead_armed) { dead_armed = (uint8_t)!any; return 0; } if (!any) return 0; } dead_armed = 0; msg_set(POP_MSG_NONE, 0, 0); return 1; } /* ---- кадровая точка ---------------------------------------------------- */ 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(); /* --- нижняя полоса: заявка, таймер, перерисовка --- */ time_msg(); if (pop_status_msg != st_shown) { st_shown = pop_status_msg; pop_status_ticks = (uint8_t)(!st_shown ? 0 : (st_shown == POP_MSG_PRESS_BUTTON ? MSG_HOLD : MSG_TICKS)); st_dirty = 3; } else if (pop_status_ticks && pop_status_ticks != MSG_HOLD) { 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); }