Таблица рекордов: оба показа оригинала, ввод имени, переходы полосами

pop_hof.c был написан, но никуда не вызывался: после финала автомат уходил
мимо него в title, а на титрах экран получался чёрным. Теперь это хвост
end_sequence и шаг show_title между credits и attract-demo, как в оригинале.

Два корня пустого экрана:

- story.pal — 256 записей, и запись 0x3F (цвет глифов шрифта) в ней чёрная:
  текст рисовался, но был не виден. Генератор кладёт туда золотой 0xB7;
- полосовой переход копирует страницу акселератором, а тот читает ОЗУ-копию,
  куда прозрачный блит текста не пишет. Строки рисуются после перехода,
  прямо в видимую страницу.

Попутно:

- s5 в архиве PV — фон таблицы (рамка story + логотип на y=24, HOF_POP);
- отдельный индекс палитры под фон текстовой рамки: финал красит его в
  #800000, титры оставляют #100060 (load_title_images(bgcolor)). Прежний
  ремап в индекс 9 подменить было нельзя — им нарисована сама титульная
  картинка;
- второй набор глифов в font.atl цветом 0x3E: у SDLPoP шрифт маска и
  show_hof_text рисует текст дважды разным цветом, у нас цвет запечён в
  пиксели. Каталог SPA1 держит счётчик в байте, поэтому набор обрезан по '_';
- общий pop_screen_present_ltr(): им теперь пользуются story-переход интро,
  титры, таблица и титульная картинка финала. Заодно вернулся пропущенный
  шаг show_title — экран с логотипом и Jordan Mechner между «свадьбой» и
  титрами (BUGS_CLOSED#cutscene-ltr).

Формат POP.HOF — 6 записей, имя до 15 символов, версия 2. Проверено в MAME
на сборке LEVEL=14: HAIL → титульная картинка → таблица с полосой ввода и
временем справа → ввод имени → титульная картинка; на следующем запуске
таблица показывается между credits и demo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 16:41:41 +03:00
parent faec9a7d3a
commit a3c5c600da
52 changed files with 740 additions and 275 deletions
+243 -92
View File
@@ -1,6 +1,13 @@
/*
* pop_hof.c — persistent Hall of Fame, банк 9 (FG12).
*
* Порт двух показов оригинала: между credits и attract-demo (seg000:2060)
* и после победы, с вводом имени (end_sequence, seg001:5F1). Экран тот
* же самый — рамка story с логотипом PRINCE OF PERSIA (STORY_FRAME +
* HOF_POP), поэтому фон приходит готовой композицией из архива PV, а
* палитру выбирает вызывающая сцена: обе фазы идут на story.pal, и
* отличаются только цветом фона рамки (см. pop_pal_story_load).
*
* POP.HOF — небольшой самодостаточный формат: magic, version, fixed records
* и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры:
* decode просто начинает с пустой таблицы. Ввод — raw-клавиатура, как весь
@@ -19,19 +26,44 @@
#include "pop_timer.h"
#include "pop_pal.h"
#include "pop_ui.h"
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
#include "pop_intro.h" /* фон экрана: рамка story + логотип */
#include "pop_title.h" /* титульная картинка после ввода имени */
#include "pop_music.h" /* подкачка потоковой темы победы */
#include "pop_hof.h"
#define HOF_COUNT 5
#define HOF_NAME_SIZE 10 /* девять букв + '\0' */
#define HOF_REC_SIZE 14
/* MAX_HOF_COUNT оригинала — шесть строк. */
#define HOF_COUNT 6
#define HOF_NAME_SIZE 16 /* пятнадцать букв + '\0' */
#define HOF_REC_SIZE 20
#define HOF_HEADER_SIZE 6
#define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2)
#define HOF_BASE 0x100
#define HOF_VERSION 1
#define HOF_FADE 4
#define HOF_VERSION 2
#define HOF_FADE POP_FADE_FRAMES
#define HOF_NO_ROW 0xFF
/* Геометрия hof_rects[] (seg001:0D92) в координатах 320x200: строка i
* занимает y = 84 + 14*i .. +12, имя от x=72, время по правому краю 248.
* Полоса ввода — та же строка, сдвинутая offset4_rect_add(-4,-1,-40,-1). */
#define HOF_ROW_TOP 84
#define HOF_ROW_STEP 14
#define HOF_ROW_H 12
#define HOF_LEFT 72
#define HOF_RIGHT 248
#define HOF_EDIT_LEFT (HOF_LEFT - 4)
#define HOF_EDIT_RIGHT (HOF_RIGHT - 40)
/* Крупный шрифт: глиф 7 строк, строка 12 — текст центрируется по вертикали.
* baseline = верх строки + 2 + POP_FONT_BIG_ASCENT. */
#define HOF_BASELINE(i) (POP_YOFF + HOF_ROW_TOP + (int)(i) * HOF_ROW_STEP + 9)
/* Золотой цвет полосы ввода: 0xB7 оригинала = седьмой цвет палитры title,
* а у нас story.pal сделана из неё же. */
#define HOF_GOLD 7
/* Паузы оригинала в тиках 60 Гц: показ таблицы на титрах и пауза после
* ввода имени (seg000:2065, seg001:628). */
#define HOF_TITLE_TICKS POP_T60(0xF0)
#define HOF_AFTER_TICKS POP_T60(120)
typedef struct {
char name[HOF_NAME_SIZE];
uint8_t minutes;
@@ -136,7 +168,7 @@ static void hof_load(void)
for (i = 0; i < HOF_SIZE; i++)
hof_buf[i] = ((const uint8_t *)HOF_BASE)[i];
gfx_w0_unmap();
if (!hof_decode(hof_buf, HOF_SIZE)) hof_clear();
if (!hof_decode(hof_buf, (uint8_t)HOF_SIZE)) hof_clear();
}
mem_free_block(blk);
}
@@ -209,66 +241,158 @@ static void hof_dec(uint8_t value, char *out)
*out = 0;
}
/* «59:43» — те же min-1 : tick/12, что печатает show_hof (seg001:0E94). */
static void hof_result_text(const hof_entry_t *entry, char *out)
{
uint8_t seconds;
if (!entry->limited) {
strcpy(out, "UNLIMITED");
return;
}
hof_dec(entry->minutes, out);
/* min == 0 у оригинала означает истёкшее время и печатается как 0:00. */
hof_dec(entry->minutes ? (uint8_t)(entry->minutes - 1) : 0, out);
while (*out) out++;
*out++ = ' ';
*out++ = 'M';
*out++ = 'I';
*out++ = 'N';
seconds = entry->minutes ? (uint8_t)(entry->ticks / 12) : 0;
*out++ = ':';
*out++ = (char)('0' + seconds / 10);
*out++ = (char)('0' + seconds % 10);
*out = 0;
}
static void hof_draw(uint8_t edit_row)
/* show_hof_text(): сначала тень со сдвигом (1,1), затем сам текст. У нас
* цвет запечён в глифы, поэтому «другой цвет» — это второй набор
* (POP_TEXT_BIG_DARK), а не параметр рисования. */
static void hof_text_left(int x, int baseline, const char *s)
{
uint8_t page, i;
page = pop_screen_begin();
pop_screen_fill(BLACK);
gfx_set_bank(GFX_BANK_SPRITE);
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, 160, 58, "HALL OF FAME");
pop_text_draw_mapped(POP_TEXT_SMALL, 64, 84, "NAME");
pop_text_right_mapped(POP_TEXT_SMALL, 255, 84, "TIME");
for (i = 0; i < HOF_COUNT; i++) {
char rank[3], value[12];
const hof_entry_t *entry = &hof_table[i];
int y = 106 + (int)i * 20;
rank[0] = (char)('1' + i); rank[1] = '.'; rank[2] = 0;
pop_text_draw_mapped(POP_TEXT_SMALL, 46, y, rank);
if (entry->name[0])
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, entry->name);
else if (i == edit_row)
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "_");
else
pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "---");
if (entry->name[0]) {
hof_result_text(entry, value);
pop_text_right_mapped(POP_TEXT_SMALL, 255, y, value);
}
}
if (edit_row != HOF_NO_ROW)
pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "TYPE NAME - ENTER");
else
pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "PRESS ANY KEY");
pop_text_unmap();
pop_screen_present(page);
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x + 1, baseline + 1, s);
pop_text_draw_mapped(POP_TEXT_BIG, x, baseline, s);
}
/* Ожидания на экране рекордов держат ПОТОКОВУЮ музыку: победная тема
* (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
* игрок смотрит таблицу — иначе насос догонит загрузчик и пойдёт по
* кругу старыми данными. */
static void hof_wait_release(void)
static void hof_text_right(int right, int baseline, const char *s)
{
while (kbd_raw_any_down()) {
kbd_raw_sync();
pop_text_right_mapped(POP_TEXT_BIG_DARK, right + 1, baseline + 1, s);
pop_text_right_mapped(POP_TEXT_BIG, right, baseline, s);
}
/* Золотая полоса под ввод — draw_rect(&rect, bgcolor) оригинала. */
static void hof_edit_bar(uint8_t row)
{
int top = HOF_BASELINE(row) - 10;
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, HOF_GOLD);
bar(HOF_EDIT_LEFT, top, HOF_EDIT_RIGHT, top + HOF_ROW_H - 1);
}
/* Строки таблицы в ТЕКУЩУЮ draw-страницу. Отделены от фона потому, что
* полосовой переход НЕ ПЕРЕНОСИТ их: gfx_copy_page работает акселератором,
* а тот читает ОЗУ-копию страницы, куда прозрачный (спрайтовый) блит не
* пишет. Поэтому на титрах строки рисуются уже после перехода, прямо в
* видимую страницу. */
static void hof_draw_rows(uint8_t edit_row)
{
uint8_t i;
gfx_set_bank(GFX_BANK_SPRITE);
pop_text_map();
for (i = 0; i < HOF_COUNT; i++) {
char value[12];
const hof_entry_t *entry = &hof_table[i];
int baseline = HOF_BASELINE(i);
/* Строка, которую сейчас вписывают, уже содержит результат: имени
* ещё нет, а время справа оригинал показывает сразу (show_hof
* рисует все hof_count строк, включая только что вставленную). */
if (!entry->name[0] && i != edit_row) continue;
if (entry->name[0]) hof_text_left(HOF_LEFT, baseline, entry->name);
hof_result_text(entry, value);
hof_text_right(HOF_RIGHT, baseline, value);
}
pop_text_unmap();
}
/* Готовая страница: фон из архива, полоса ввода и строки. -1 — фон не
* прочитан, и тогда таблица просто не показывается: ни игра, ни title от
* этого не зависят. */
static int hof_page_build(uint8_t page, uint8_t edit_row)
{
if (pop_intro_hof_bg(page) != 0) return -1;
gfx_set_draw_page(page);
if (edit_row != HOF_NO_ROW) hof_edit_bar(edit_row);
hof_draw_rows(edit_row);
return 0;
}
/* Имя пишется ПРЯМО НА ЭКРАН, как input_str оригинала
* (current_target_surface = onscreen_surface_): страницы при вводе не
* меняются местами, поэтому дорисовывать фон второй страницы не нужно. */
static void hof_edit_draw(uint8_t row, const char *name, uint8_t cursor)
{
int x = HOF_LEFT, baseline = HOF_BASELINE(row);
gfx_set_draw_page(gfx_get_visible_page());
hof_edit_bar(row);
gfx_set_bank(GFX_BANK_SPRITE);
pop_text_map();
/* Тёмный набор: на золотой полосе оригинал пишет цветом 0xBE. */
if (name[0]) {
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, name);
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, "_");
pop_text_unmap();
}
/* ---- ожидания -------------------------------------------------------- *
*
* Любое ожидание этого экрана держит ПОТОКОВУЮ музыку: победная тема
* (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
* игрок смотрит таблицу и печатает имя — иначе насос догонит загрузчика и
* пойдёт по кругу старыми данными.
*
* Прерывание — по НОВОМУ нажатию (детектор фронта, как в сценах intro):
* клавиша, которой закончили предыдущий экран, в момент входа ещё зажата,
* и считать её за «пропустить» нельзя. */
typedef struct { uint8_t armed; } hof_skip_t;
static void hof_skip_begin(hof_skip_t *sk)
{
kbd_raw_sync();
sk->armed = (uint8_t)!kbd_raw_any_down();
}
static uint8_t hof_skip_requested(hof_skip_t *sk)
{
kbd_raw_sync();
if (!kbd_raw_any_down()) { sk->armed = 1; return 0; }
return sk->armed;
}
/* Возврат 1 — ожидание прервано нажатием. */
static uint8_t hof_wait_frames(uint16_t frames)
{
hof_skip_t skip;
hof_skip_begin(&skip);
while (frames--) {
if (hof_skip_requested(&skip)) return 1;
pop_music_service();
gfx_wait_vsync();
}
return 0;
}
/* `while (check_sound_playing() && !key_test_quit())` (seg001:637): финал
* дослушивает победную тему и прерывается любой клавишей. */
static void hof_wait_music(void)
{
hof_skip_t skip;
hof_skip_begin(&skip);
while (pop_music_busy() && !hof_skip_requested(&skip)) {
pop_music_service();
gfx_wait_vsync();
}
@@ -283,8 +407,10 @@ static const uint8_t hof_key_code[30] = {
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
};
/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
* перерисовать строку. */
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
uint8_t *previous)
uint8_t *previous, uint8_t *changed)
{
uint8_t i;
@@ -294,12 +420,16 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
if (i < 26 && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = (char)('A' + i);
entry->name[*length] = 0;
*changed = 1;
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = ' ';
entry->name[*length] = 0;
*changed = 1;
} else if (i == 27 && *length) {
entry->name[--(*length)] = 0;
*changed = 1;
} else if (i == 28 || i == 29) {
previous[i] = down;
return 1;
}
}
@@ -308,22 +438,42 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
return 0;
}
/* Ввод имени в строку row. Пустое имя оригинал не отвергает, но пустая
* строка в таблице выглядела бы как свободное место — подставляем PLAYER. */
/* Курсор мигает каждые шесть тиков 60 Гц (start_timer(timer_0, 6) в
* input_str) — в кадрах луча это HOF_BLINK. */
#define HOF_BLINK POP_T60(6)
static void hof_edit(uint8_t row)
{
uint8_t length = 0, previous[30], i;
uint8_t length = 0, previous[30], i, changed = 1;
uint8_t cursor = 1, phase = 0;
hof_entry_t *entry = &hof_table[row];
for (i = 0; i < sizeof(previous); i++) previous[i] = 0;
hof_wait_release();
/* Стартовое состояние клавиш: зажатое с прошлого экрана не считается
* нажатием, поэтому первый sync только заполняет previous[]. */
kbd_raw_sync();
for (i = 0; i < sizeof(previous); i++)
previous[i] = kbd_raw_down(hof_key_code[i]);
for (;;) {
kbd_raw_sync();
if (hof_name_tick(entry, &length, previous)) {
if (hof_name_tick(entry, &length, previous, &changed)) {
if (!length) strcpy(entry->name, "PLAYER");
hof_save();
hof_wait_release();
return;
}
hof_draw(row);
if (changed) { cursor = 1; phase = 0; }
if (++phase >= HOF_BLINK) {
phase = 0;
cursor = (uint8_t)!cursor;
changed = 1;
}
if (changed) {
hof_edit_draw(row, entry->name, cursor);
changed = 0;
}
pop_music_service();
gfx_wait_vsync();
}
}
@@ -331,48 +481,49 @@ void pop_hof_show(void) __banked
{
uint8_t row;
pop_sfx_pause();
hof_load();
row = hof_insert_current();
pop_pal_story_load();
pop_pal_apply(POP_PAL_DIM_BLACK);
hof_draw(row);
pop_pal_fade_in(HOF_FADE);
if (row != HOF_NO_ROW) {
hof_edit(row);
} else {
hof_wait_release();
while (!kbd_raw_any_down()) {
kbd_raw_sync();
pop_music_service();
gfx_wait_vsync();
uint8_t page;
/* fade_out_2 гасит титульную картинку перед таблицей (seg001:601).
* Палитру не меняем: story.pal с красным фоном рамки уже стоит от
* финального HAIL, и снимок для fade сделан ею же. */
pop_pal_fade_out(HOF_FADE);
page = pop_screen_begin();
if (hof_page_build(page, row) == 0) {
pop_screen_present(page);
pop_pal_fade_in(HOF_FADE);
hof_edit(row);
hof_save();
/* Имя вписано: строку показываем уже обычным золотым текстом,
* без полосы (restore_peel + show_hof_text оригинала). */
page = pop_screen_begin();
if (hof_page_build(page, HOF_NO_ROW) == 0)
pop_screen_present(page);
hof_wait_frames(HOF_AFTER_TICKS);
(void)pop_title_main_ltr();
}
hof_wait_release();
}
hof_wait_music();
pop_pal_fade_out(HOF_FADE);
pop_music_free();
pop_sfx_pause();
}
void pop_hof_show_title(void) __banked
uint8_t pop_hof_show_title(void) __banked
{
/* Один keypress должен пропускать всю цепочку title/intro, а не внезапно
* останавливаться на уже известной таблице. */
if (kbd_raw_any_down()) return;
uint8_t page;
hof_load();
if (!hof_has_entries()) return;
pop_sfx_pause();
pop_pal_story_load();
pop_pal_apply(POP_PAL_DIM_BLACK);
hof_draw(HOF_NO_ROW);
pop_pal_fade_in(HOF_FADE);
hof_wait_release();
while (!kbd_raw_any_down()) {
kbd_raw_sync();
pop_music_service();
gfx_wait_vsync();
}
hof_wait_release();
pop_pal_fade_out(HOF_FADE);
if (!hof_has_entries()) return 0;
page = pop_screen_begin();
/* Ни fade, ни смены палитры: экран проявляется полосами прямо поверх
* credits, на той же story.pal (seg000:2063). Переход несёт ТОЛЬКО
* фон — строки дорисовываются следом в видимую страницу. */
if (pop_intro_hof_bg(page) != 0) return 0;
pop_screen_present_ltr();
gfx_set_draw_page(gfx_get_visible_page());
hof_draw_rows(HOF_NO_ROW);
return hof_wait_frames(HOF_TITLE_TICKS);
}