SprPoP: финал больше не убивает программу — прямой вызов в чужой банк
Пройденная игра доходила до таблицы рекордов и умирала: программа
исчезала, машина следом вставала намертво (di;halt на 0x0000) либо уходила
в reset. Одинаково из Flex Navigator и из голого DSS.
КОРЕНЬ. pop_ui.h объявлял группу pop_text_*_mapped БЕЗ __banked. Пока
pop_hof.c лежал в банке 9 рядом с pop_ui.c, прямой call был верен; после
переноса pop_hof/pop_config/pop_pal в банк 10 тот же call стал уходить в
пустой хвост чужого банка. Процессор полз по 0xFF до 0x0000, где ловушка
DSS ставит B=0x27 и сворачивает процесс — подмена страниц W1/W2/W3,
которую было видно на трупе, оказалась уборкой, а не причиной.
Точную инструкцию (call $E503 = _pop_text_map банка 9) дала трассировка
MAME на узком участке: trace включалась брейкпоинтом на входе в
pop_hof_show и выключалась на процедуре завершения процесса DSS (0x1E56).
ЧТО СДЕЛАНО
* pop_ui.h/.c — группа text_*_mapped помечена __banked.
* toolchain/check_bank_calls.py — две проверки банкового кода:
1) прямой call в чужой банк (доказательна, ВАЛИТ сборку — проверено
намеренной поломкой);
2) указатель на данные своего банка, отданный в чужой (эвристика по
форме кода, только предупреждает).
Встроена в app.mk, запускается сразу после линковки.
* pop_hof.c — курсор ввода строится на стеке: литерал "_" лежал в _BANK10
и после пометки __banked уезжал из-под ног чужому банку, заливая экран
знаками вопроса.
* libc: kbd_raw_keypad_as_ext() — kbd_raw_sync переносит голые коды
нумпада в EXT-половину карты. Лечит залипание стрелок (потерянный
префикс E0 сажал make в PLAIN как код нумпада, и снять его было нечем),
заодно нумпад стал управлением: 7/8/9, 4/6, 2 и 5 = вниз.
* pop_pace.c — цикл ожидания луча зовёт тот же idle-хук, что и
gfx_wait_vsync: без этого F10 в геймплее не работал вовсе.
* pop_hof.c — Esc в таблице рекордов отменяет запись (расхождение с
оригиналом записано в docs/impl_diff.md).
* Экран версии показывается только через Menu/Settings/About: стартовый
показ и Ctrl+V убраны, мёртвый код снят.
* sprpop_cold.c — pop_start_level зовёт pop_hp_invalidate: после Ctrl+A с
выросшим за уровень максимумом полоса HP моргала между страницами.
Разбор всех четырёх багов — в applications/PoP/roomtest/BUGS_CLOSED.md
(FINAL-BANKCALL, FINAL-HOF-GARBAGE, KBD-ARROW-PHANTOM, F10-GAMEPLAY),
правило про банки — в applications/SprPoP/CLAUDE.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,32 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||
|
||||
## Банки: две мины, которых компилятор не видит
|
||||
|
||||
Обе стреляют молча, обе поймали нас 2026-08-27 (разбор — в
|
||||
`../PoP/roomtest/BUGS_CLOSED.md`, поиск «FINAL-BANKCALL»).
|
||||
|
||||
**1. Прямой вызов в чужой банк.** Трамплин SDCC выбирает по `__banked`
|
||||
в ОБЪЯВЛЕНИИ, а не по тому, где функция лежит. Функция без пометки
|
||||
зовётся прямым `call` — верно ровно пока вызывающий и вызываемый в одном
|
||||
банке. Перенесли модуль ради разгрузки банка — и тот же `call` уходит в
|
||||
пустой хвост чужого банка, процессор ползёт по 0xFF до 0x0000, DSS убивает
|
||||
процесс. Ловит `toolchain/check_bank_calls.py`, она встроена в сборку и
|
||||
ВАЛИТ её (проверено намеренной поломкой).
|
||||
|
||||
**2. Указатель на данные своего банка, отданный в чужой.** Обратная
|
||||
сторона: пока обе стороны в одном банке, указатель на литерал или
|
||||
const-таблицу работает; пометили функцию `__banked` — и трамплин на время
|
||||
вызова переключает W3, а указатель показывает уже в чужой банк. Так экран
|
||||
таблицы рекордов залило знаками вопроса: в `pop_text_draw_mapped` (банк 9)
|
||||
уходил литерал `"_"` из банка 10.
|
||||
|
||||
**Правило:** всё, что уходит указателем в другой банк, обязано лежать в
|
||||
W2 — стек, `_DATA` или копия. `const`-таблицы и строковые литералы
|
||||
банкового модуля наружу отдавать нельзя (то же, что уже записано про
|
||||
`pop_tile.c`). Вторая проверка скрипта предупреждает о явных случаях, но
|
||||
она эвристическая и гарантией не является.
|
||||
|
||||
## Порядок слоёв в кадре (важно для окклюзии)
|
||||
|
||||
`sprpop.c` каждый тик рисует в СКРЫТУЮ страницу: `pop_char_heal` (стереть
|
||||
|
||||
@@ -647,3 +647,40 @@ Enter/Shift — либо оставить как есть уже осознан
|
||||
отпущено ВСЁ, что игрок держал в момент смерти (иначе зажатая при падении
|
||||
стрелка перезапускает уровень мгновенно), и не раньше `RESPAWN_SETTLE`
|
||||
кадров — труп должен успеть лечь.
|
||||
|
||||
---
|
||||
|
||||
## Таблица рекордов: Esc отменяет запись, а оригинал уйти не даёт
|
||||
|
||||
**Как в оригинале.** `show_hof` вставляет результат в таблицу ДО ввода
|
||||
имени и крутит ввод, пока тот не вернёт положительную длину:
|
||||
|
||||
```c
|
||||
/* SDLPoP seg001.c:624 */
|
||||
while (input_str(&rect, hof[hof_index].name, 24, "", 0, 4, color, bgcolor) <= 0);
|
||||
/* seg009.c input_str: Esc -> return -1; Enter -> return length */
|
||||
```
|
||||
|
||||
То есть **уйти нельзя ни по Esc, ни с пустым именем**: и −1, и 0 просто
|
||||
начинают ввод заново. Запись в таблице остаётся в любом случае.
|
||||
|
||||
**Как у нас.** Enter записывает имя (пустое подставляется как `PLAYER`),
|
||||
**Esc отказывается от записи целиком**: таблица перечитывается с диска
|
||||
(`hof_load`), `hof_save` не вызывается, экран показывается уже без строки
|
||||
ввода.
|
||||
|
||||
**Зачем.** Решение пользователя, 2026-08-27. Оригинальное поведение
|
||||
означает, что случайно добравшийся до финала игрок обязан вписать имя,
|
||||
чтобы вообще уйти с экрана, — на клавиатуре без цифрового блока и с нашим
|
||||
raw-каналом это выглядит как зависание, а не как требование.
|
||||
|
||||
**Чем платим.** Расхождение видно игроку: в оригинале таблица после
|
||||
победы пополняется всегда. Плюс отмена стоит одного лишнего чтения
|
||||
POP.HOF — откатывать сдвиг строк, который сделал `hof_insert_current`,
|
||||
дешевле перечитыванием файла, чем обратным сдвигом (экран холодный,
|
||||
50 мс там никому не мешают).
|
||||
|
||||
**Что проверять при регрессе.** После Esc: таблица на экране прежняя (без
|
||||
новой строки), POP.HOF на диске не изменился, следующий показ таблицы —
|
||||
между credits и attract-demo — даёт тот же список. После Enter с пустым
|
||||
именем строка называется `PLAYER` и сохраняется.
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
* Ctrl+R: Return to intro.
|
||||
* Ctrl+S: Sound on/off.
|
||||
* Ctrl+M: Music on/off.
|
||||
* Ctrl+V: Show version of SprPoP.
|
||||
* Ctrl+Q / F10: Quit game.
|
||||
|
||||
* F6: Quicksave: Save the exact state of the game.
|
||||
|
||||
@@ -665,6 +665,12 @@ void pop_control(void) __banked
|
||||
* «откуда взялся прыжок» останется одним. */
|
||||
#define KBD_HOME (KBD_EXT | 0x6C)
|
||||
#define KBD_PGUP (KBD_EXT | 0x7D)
|
||||
/* Центр нумпада (KP5) — тоже «вниз», как просит раскладка оригинала: там
|
||||
* весь блок 7/8/9-4/6-1/2/3 был управлением, и середина работала как
|
||||
* приседание. Своей навигационной клавиши у KP5 нет, поэтому после
|
||||
* kbd_raw_keypad_as_ext его голый код 0x73 переезжает в EXT-половину как
|
||||
* есть — читаем оттуда и добавляем к стрелке «вниз». */
|
||||
#define KBD_KP5 (KBD_EXT | 0x73)
|
||||
|
||||
static void read_input(void)
|
||||
{
|
||||
@@ -673,7 +679,7 @@ static void read_input(void)
|
||||
uint8_t l = (uint8_t)(kbd_raw_down(KBD_LEFT) || home);
|
||||
uint8_t r = (uint8_t)(kbd_raw_down(KBD_RIGHT) || pgup);
|
||||
uint8_t u = (uint8_t)(kbd_raw_down(KBD_UP) || home || pgup);
|
||||
uint8_t d = kbd_raw_down(KBD_DOWN);
|
||||
uint8_t d = (uint8_t)(kbd_raw_down(KBD_DOWN) || kbd_raw_down(KBD_KP5));
|
||||
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
|
||||
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
|
||||
control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
|
||||
@@ -781,6 +787,16 @@ int pop_ctrl_init(void) __banked
|
||||
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
|
||||
control_shift2 = CONTROL_RELEASED;
|
||||
ctrl1_forward = ctrl1_backward = ctrl1_up = ctrl1_down = ctrl1_shift2 = CONTROL_RELEASED;
|
||||
/* НУМПАД НАМ НЕ НУЖЕН, И ЭТО ЛЕЧИТ ЗАЛИПАНИЕ СТРЕЛОК. Управление и
|
||||
* меню читают только расширенные коды (KBD_LEFT = KBD_EXT|0x6B и т.д.).
|
||||
* А при переполнении FIFO SIO теряется префикс E0, и make стрелки
|
||||
* садится в PLAIN-половину карты как код нумпада — снять его нечем,
|
||||
* потому что break придёт уже с префиксом. Дальше kbd_raw_any_down()
|
||||
* навсегда «да», и всякое ожидание «пока ничего не нажато» виснет
|
||||
* насмерть (BUG KBD-STUCK-WAIT; поймано на 14-м уровне, где почти весь
|
||||
* путь идёт удержанием Left). Гасит биты kbd_raw_sync(), а его
|
||||
* pop_ctrl_tick зовёт каждый кадр. */
|
||||
kbd_raw_keypad_as_ext(1);
|
||||
return kbd_raw_open();
|
||||
}
|
||||
|
||||
|
||||
@@ -343,8 +343,22 @@ static void hof_edit_draw(uint8_t row, const char *name, uint8_t cursor)
|
||||
x += pop_text_width_mapped(POP_TEXT_BIG_DARK, name) + 1;
|
||||
}
|
||||
/* draw_text_cursor(): подчерк той же гарнитуры сразу за текстом,
|
||||
* мигающий сменой цвета на фоновый — у нас проще перерисовать. */
|
||||
if (cursor) pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_");
|
||||
* мигающий сменой цвета на фоновый — у нас проще перерисовать.
|
||||
*
|
||||
* СТРОКА СТРОИТСЯ НА СТЕКЕ, И ЭТО НЕ ПРИДИРКА (поймано в MAME
|
||||
* 2026-08-27). Литерал "_" лёг бы в _BANK10, то есть в окно W3, а
|
||||
* pop_text_draw_mapped живёт в банке 9: трамплин на время вызова
|
||||
* переключает W3, и указатель показывает уже в ЧУЖОЙ банк. Функция
|
||||
* честно читает оттуда байты до первого нуля и рисует их знаками
|
||||
* вопроса — экран заливало мусором на каждое мигание курсора. Стек
|
||||
* лежит в W2, он виден из любого банка. */
|
||||
if (cursor) {
|
||||
char cur[2];
|
||||
|
||||
cur[0] = '_';
|
||||
cur[1] = 0;
|
||||
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, cur);
|
||||
}
|
||||
pop_text_unmap();
|
||||
}
|
||||
|
||||
@@ -409,8 +423,8 @@ static const uint8_t hof_key_code[30] = {
|
||||
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
|
||||
};
|
||||
|
||||
/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
|
||||
* перерисовать строку. */
|
||||
/* Возвращает 1 — ввод завершён (Enter), 2 — отменён (Esc); 0 — продолжаем.
|
||||
* *changed — надо ли перерисовать строку. */
|
||||
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
uint8_t *previous, uint8_t *changed)
|
||||
{
|
||||
@@ -432,7 +446,7 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
*changed = 1;
|
||||
} else if (i == 28 || i == 29) {
|
||||
previous[i] = down;
|
||||
return 1;
|
||||
return (uint8_t)(i == 28 ? 1 : 2); /* Enter : Esc */
|
||||
}
|
||||
}
|
||||
previous[i] = down;
|
||||
@@ -446,7 +460,8 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
* input_str) — в кадрах луча это HOF_BLINK. */
|
||||
#define HOF_BLINK POP_T60(6)
|
||||
|
||||
static void hof_edit(uint8_t row)
|
||||
/* 1 — игрок отказался вписывать имя (Esc). */
|
||||
static uint8_t hof_edit(uint8_t row)
|
||||
{
|
||||
uint8_t length = 0, previous[30], i, changed = 1;
|
||||
uint8_t cursor = 1, phase = 0;
|
||||
@@ -459,10 +474,14 @@ static void hof_edit(uint8_t row)
|
||||
for (i = 0; i < sizeof(previous); i++)
|
||||
previous[i] = kbd_raw_down(hof_key_code[i]);
|
||||
for (;;) {
|
||||
uint8_t done;
|
||||
|
||||
kbd_raw_sync();
|
||||
if (hof_name_tick(entry, &length, previous, &changed)) {
|
||||
done = hof_name_tick(entry, &length, previous, &changed);
|
||||
if (done == 2) return 1; /* Esc — запись не нужна */
|
||||
if (done) {
|
||||
if (!length) strcpy(entry->name, "PLAYER");
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
if (changed) { cursor = 1; phase = 0; }
|
||||
if (++phase >= HOF_BLINK) {
|
||||
@@ -496,8 +515,13 @@ void pop_hof_show(void) __banked
|
||||
if (hof_page_build(page, row) == 0) {
|
||||
pop_screen_present(page);
|
||||
(void)pop_pal_fade_in_skip(HOF_FADE);
|
||||
hof_edit(row);
|
||||
hof_save();
|
||||
/* ОТКАЗ ПО Esc — РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (docs/impl_diff.md).
|
||||
* SDLPoP крутит `while (input_str(...) <= 0)`, то есть уйти без
|
||||
* имени нельзя вовсе. Мы разрешаем: тогда таблицу перечитываем
|
||||
* с диска (hof_insert_current уже сдвинул строки, и откатывать
|
||||
* их дешевле всего именно так) и НЕ сохраняем. */
|
||||
if (hof_edit(row)) hof_load();
|
||||
else hof_save();
|
||||
/* Имя вписано: строку показываем уже обычным золотым текстом,
|
||||
* без полосы (restore_peel + show_hof_text оригинала). */
|
||||
page = pop_screen_begin();
|
||||
|
||||
@@ -653,53 +653,6 @@ static void menu_erase(uint8_t entry_page)
|
||||
}
|
||||
}
|
||||
|
||||
static void build_info_draw(void)
|
||||
{
|
||||
uint8_t back = pop_screen_begin();
|
||||
int top = UI_CENTER_TOP(ABOUT_H + BUILD_KEYS_GAP);
|
||||
|
||||
pop_screen_fill(BLACK);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_text_map();
|
||||
about_text(top);
|
||||
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2,
|
||||
top + ABOUT_H + BUILD_KEYS_GAP, UI_KEYS_TEXT);
|
||||
pop_text_unmap();
|
||||
pop_screen_present(back);
|
||||
}
|
||||
|
||||
/* В SprPoP ещё нет автомата TITLE (это FG1/FG6), поэтому build screen
|
||||
* показывается перед первым игровым кадром и после выхода возвращает ровно
|
||||
* ту же сцену. Настройка уже прочитана в pop_boot до initgraph. */
|
||||
void pop_build_info_show(void) __banked
|
||||
{
|
||||
uint8_t entry_page, saved_page, saved_bank, frames;
|
||||
struct fillsettingstype saved_fill;
|
||||
|
||||
if (!pop_ui_ready() || !pop_settings.show_build_info) return;
|
||||
entry_page = gfx_get_visible_page();
|
||||
saved_page = gfx_get_draw_page();
|
||||
saved_bank = gfx_get_bank();
|
||||
getfillsettings(&saved_fill);
|
||||
/* Первый реальный пользователь FG5 fade: старая игровая палитра
|
||||
* запоминается в pop_ui, затем full-screen появляется из чёрного. */
|
||||
pop_pal_fade_out(4);
|
||||
build_info_draw();
|
||||
pop_pal_fade_in(4);
|
||||
for (frames = 0; frames < 150; frames++) { /* около трёх секунд */
|
||||
if (kbd_raw_down(KBD_ESC) || kbd_raw_down(KBD_ENTER) ||
|
||||
kbd_raw_down(KBD_SPACE))
|
||||
break;
|
||||
pop_wait_edge();
|
||||
}
|
||||
ui_wait_release();
|
||||
pop_pal_fade_out(4);
|
||||
menu_erase(entry_page);
|
||||
pop_pal_fade_in(4);
|
||||
setfillstyle(saved_fill.pattern, saved_fill.color);
|
||||
gfx_set_bank(saved_bank);
|
||||
gfx_set_draw_page(saved_page);
|
||||
}
|
||||
|
||||
int pop_menu_init(void) __banked
|
||||
{
|
||||
|
||||
@@ -40,6 +40,5 @@ int8_t pop_menu_process(void) __banked;
|
||||
|
||||
/* Экран Sprinter build, если он включён в POP.CFG. Вызывается после
|
||||
* открытия raw-клавиатуры и до первого игрового кадра. */
|
||||
void pop_build_info_show(void) __banked;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <stdint.h>
|
||||
#include <gfx.h>
|
||||
#include <kbd_raw.h>
|
||||
#include "pop_state.h" /* pop_idle — общий idle-хук */
|
||||
#include "pop_pace.h"
|
||||
|
||||
uint8_t pop_speed_mode; /* crt0 зануляет _DATA → NORMAL */
|
||||
@@ -77,7 +78,11 @@ void pop_wait_edge(void)
|
||||
t = pop_frame_tick;
|
||||
do {
|
||||
pop_beam_sample();
|
||||
kbd_raw_poll(); /* KBD-1: опрос обязан быть плотным */
|
||||
/* ТОТ ЖЕ ХУК, что висит на gfx_wait_vsync, а не голый kbd_raw_poll:
|
||||
* кроме плотного опроса FIFO (KBD-1) он проверяет F10, и без этого
|
||||
* выход из игры в геймплее не работал — пейсинг по лучу через
|
||||
* gfx_wait_vsync не ходит. */
|
||||
pop_idle();
|
||||
} while (pop_frame_tick == t);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
* отрыв куска, снимает сам обход по факту пустой таблицы. */
|
||||
/* F10 нажата — выйти из программы; см. pop_state.c. */
|
||||
extern uint8_t pop_quit_req;
|
||||
|
||||
/* Общий idle-хук: вычерпать клавиатурный FIFO и проверить F10. Живёт в
|
||||
* sprpop.c (резидент), висит на gfx_wait_vsync и вызывается из цикла
|
||||
* ожидания луча — обе точки ожидания обязаны звать ОДНО И ТО ЖЕ, иначе
|
||||
* выход работает не везде. */
|
||||
void pop_idle(void);
|
||||
extern uint8_t pop_mob_busy;
|
||||
|
||||
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
|
||||
|
||||
@@ -71,17 +71,17 @@ uint8_t pop_ui_ready(void)
|
||||
return font_ready;
|
||||
}
|
||||
|
||||
void pop_text_map(void)
|
||||
void pop_text_map(void) __banked
|
||||
{
|
||||
gfx_w0_map(font_at.page);
|
||||
}
|
||||
|
||||
void pop_text_unmap(void)
|
||||
void pop_text_unmap(void) __banked
|
||||
{
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s)
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s) __banked
|
||||
{
|
||||
const pop_font_desc_t *desc = &font_desc[font];
|
||||
uint8_t width = 0;
|
||||
@@ -95,7 +95,7 @@ uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s)
|
||||
}
|
||||
|
||||
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
|
||||
const char *s)
|
||||
const char *s) __banked
|
||||
{
|
||||
const pop_font_desc_t *desc = &font_desc[font];
|
||||
|
||||
@@ -115,14 +115,14 @@ void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
|
||||
}
|
||||
|
||||
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
|
||||
const char *s)
|
||||
const char *s) __banked
|
||||
{
|
||||
pop_text_draw_mapped(font, center - pop_text_width_mapped(font, s) / 2,
|
||||
baseline, s);
|
||||
}
|
||||
|
||||
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
|
||||
const char *s)
|
||||
const char *s) __banked
|
||||
{
|
||||
pop_text_draw_mapped(font, right - pop_text_width_mapped(font, s),
|
||||
baseline, s);
|
||||
|
||||
@@ -27,16 +27,25 @@ void pop_ui_free(void) __banked;
|
||||
uint8_t pop_ui_ready(void);
|
||||
|
||||
/* Группа text_*_mapped требует активного pop_text_map() и не вызывает
|
||||
* BIOS/ESTEX. Для прозрачных букв установить GFX_BANK_SPRITE до рисования. */
|
||||
void pop_text_map(void);
|
||||
void pop_text_unmap(void);
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s);
|
||||
* BIOS/ESTEX. Для прозрачных букв установить GFX_BANK_SPRITE до рисования.
|
||||
*
|
||||
* ВСЯ ГРУППА __banked, И ЭТО ОБЯЗАТЕЛЬНО (крэш финала, 2026-08-27). Живёт
|
||||
* она в pop_ui.c, то есть в банке 9, а зовут её и из ДРУГИХ банков — HOF
|
||||
* лежит в банке 10. Трамплин SDCC выбирает по объявлению, а не по тому,
|
||||
* где функция реально лежит (memory sdcc_banked_call_rules): без пометки
|
||||
* получается прямой `call` по адресу банка 9, который в чужом банке
|
||||
* попадает в пустой хвост (0xFF) — процессор ползёт по нему до 0x0000, и
|
||||
* DSS убивает процесс с кодом 0x27. Собиралось это МОЛЧА, поэтому рядом
|
||||
* появилась toolchain/check_bank_calls.py — она такое ловит на сборке. */
|
||||
void pop_text_map(void) __banked;
|
||||
void pop_text_unmap(void) __banked;
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s) __banked;
|
||||
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
|
||||
const char *s);
|
||||
const char *s) __banked;
|
||||
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
|
||||
const char *s);
|
||||
const char *s) __banked;
|
||||
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
|
||||
const char *s);
|
||||
const char *s) __banked;
|
||||
|
||||
/* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте
|
||||
* FONT.ATL. dim_step: 0 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними
|
||||
|
||||
@@ -125,7 +125,10 @@ uint8_t pop_menu_request;
|
||||
* проявление полосами, меню, игра. Одна точка вместо десятка. */
|
||||
#define KBD_KEY_F10 0x09
|
||||
|
||||
static void kbd_idle(void)
|
||||
/* НЕ static: этот же хук зовёт цикл ожидания луча (pop_wait_edge), иначе
|
||||
* в геймплее F10 не работал бы вовсе — пейсинг по лучу не ходит через
|
||||
* gfx_wait_vsync, к которому хук подвешен (поймано 2026-08-27). */
|
||||
void pop_idle(void)
|
||||
{
|
||||
kbd_raw_poll();
|
||||
if (kbd_raw_down(KBD_KEY_F10)) pop_quit_req = 1;
|
||||
@@ -267,14 +270,15 @@ int main(void)
|
||||
puts("kbd_raw open failed");
|
||||
return 1;
|
||||
}
|
||||
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||
gfx_set_idle_hook(pop_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||
if (!pop_app_dispatch(POP_APP_EV_BOOT_READY)) {
|
||||
pop_shutdown();
|
||||
return 1;
|
||||
}
|
||||
/* MS8: CFG уже применён в pop_boot до первого показа. Экран build
|
||||
* показывается только по явной пользовательской настройке. */
|
||||
pop_build_info_show();
|
||||
/* Экран версии при запуске БОЛЬШЕ НЕ ПОКАЗЫВАЕТСЯ (решение пользователя
|
||||
* 2026-08-27): единственная его точка — пункт ABOUT в меню паузы.
|
||||
* Само состояние BUILD_INFO в автомате осталось: через него проходит
|
||||
* маршрут BOOT -> TITLE, и убирать его ради одной пустой фазы незачем. */
|
||||
if (!pop_app_dispatch(POP_APP_EV_BUILD_DONE)) {
|
||||
pop_shutdown();
|
||||
return 1;
|
||||
|
||||
@@ -99,7 +99,7 @@ static void find_start_level_door(uint8_t room)
|
||||
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;
|
||||
static uint8_t restart_prev, version_prev;
|
||||
static uint8_t restart_prev;
|
||||
static uint8_t intro_prev, showtime_prev, timer_prev;
|
||||
uint8_t e = (uint8_t)(kbd_raw_down(KBD_MENU) || kbd_raw_down(KBD_MENU_ALT));
|
||||
uint8_t ctrl = (uint8_t)(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL));
|
||||
@@ -142,14 +142,6 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
if (r && !restart_prev) *restart_level = 1;
|
||||
restart_prev = r;
|
||||
}
|
||||
/* Ctrl+V — версия сборки. Функция уже есть, до сих пор её показывал
|
||||
* только старт программы. */
|
||||
{
|
||||
uint8_t v = (uint8_t)(ctrl && kbd_raw_down(KBD_VERSION));
|
||||
|
||||
if (v && !version_prev) pop_build_info_show();
|
||||
version_prev = v;
|
||||
}
|
||||
/* Ctrl+R — вернуться в заставку. Тот же переход, что «Restart Game»
|
||||
* пунктом меню: автомат сам разложит title/intro с первого экрана. */
|
||||
{
|
||||
@@ -431,6 +423,13 @@ void pop_start_level(void) __banked
|
||||
* гасит заставка «LEVEL N», которую этот же переход и пропускает. */
|
||||
if (pop_seamless) { pop_seamless = 0; pop_kid_hp_carry(); }
|
||||
else pop_kid_hp_reset();
|
||||
/* ПОЛОСУ HP — НА ОБЕИХ СТРАНИЦАХ. Старт уровня перерисовал фон, то есть
|
||||
* стёр её, а сама она обновляется только при ИЗМЕНЕНИИ чисел. После
|
||||
* Ctrl+A с выросшим за уровень максимумом (большой кувшин, чит Shift+T)
|
||||
* это давало моргание: одна страница показывала старые деления, вторая
|
||||
* новые (поймано пользователем 2026-08-27). invalidate ровно для этого
|
||||
* и заведён — см. его шапку в pop_cdraw.c. */
|
||||
pop_hp_invalidate();
|
||||
}
|
||||
|
||||
/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- *
|
||||
|
||||
@@ -110,7 +110,6 @@ int8_t pop_level_change(uint8_t back) __banked;
|
||||
* из keys.txt. */
|
||||
#define KBD_QUIT 0x15 /* Q — вместе с Ctrl */
|
||||
#define KBD_RESTART 0x1C /* A — Ctrl+A, рестарт уровня */
|
||||
#define KBD_VERSION 0x2A /* V — Ctrl+V, показать версию */
|
||||
#define KBD_INTRO 0x2D /* R — Ctrl+R, вернуться в заставку */
|
||||
#define KBD_SHOWTIME 0x29 /* Space — сколько осталось (seg000:612) */
|
||||
#define KBD_TIMER 0x2C /* T — постоянный показ таймера. БЕЗ модификаторов:
|
||||
|
||||
Reference in New Issue
Block a user