Files
Sprinter-SDCC/applications/PoP/roomtest/pop_hof.c
T
snark13 4b74478d19 Таблица рекордов на титрах проявляется собранной
Строки дорисовывались уже после полосового перехода: он копирует страницу
акселератором, а тот читает ОЗУ-копию, куда GFX_BANK_SPRITE (NOSHADOW +
TRANSPARENT) не пишет. Банк GFX_BANK_TRANSPARENT даёт ту же прозрачность
0xFF, но обновляет и копию, поэтому страницу можно собрать целиком до
показа — как offscreen оригинала (draw_full_image + show_hof, и только
потом transition_ltr).

Проверено в MAME: на промежуточных кадрах перехода строка рекорда видна в
уже проявившейся части экрана вместе с логотипом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:18:58 +03:00

531 lines
20 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_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-клавиатура, как весь
* PoP; он не открывает DSS-console и не конфликтует с активным raw hook.
*/
#include <stdint.h>
#include <string.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_bg.h"
#include "pop_settings.h"
#include "pop_sfx.h"
#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"
/* 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 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;
uint16_t ticks;
uint8_t limited;
} hof_entry_t;
static hof_entry_t hof_table[HOF_COUNT];
static uint8_t hof_buf[HOF_SIZE];
static uint8_t hof_xor(const uint8_t *p, uint8_t n)
{
uint8_t sum = 0;
while (n--) sum ^= *p++;
return sum;
}
static void hof_clear_entry(hof_entry_t *entry)
{
uint8_t i;
for (i = 0; i < HOF_NAME_SIZE; i++) entry->name[i] = 0;
entry->minutes = 0;
entry->ticks = 0;
entry->limited = 0;
}
static void hof_clear(void)
{
uint8_t i;
for (i = 0; i < HOF_COUNT; i++) hof_clear_entry(&hof_table[i]);
}
static uint8_t hof_decode(const uint8_t *src, uint8_t size)
{
uint8_t i, j, off, sum;
if (size != HOF_SIZE || src[0] != 'P' || src[1] != 'H' ||
src[2] != 'O' || src[3] != 'F' || src[4] != HOF_VERSION ||
src[5] != HOF_COUNT)
return 0;
sum = hof_xor(src, (uint8_t)(HOF_SIZE - 2));
if (src[HOF_SIZE - 2] != sum || src[HOF_SIZE - 1] != (uint8_t)~sum)
return 0;
off = HOF_HEADER_SIZE;
for (i = 0; i < HOF_COUNT; i++) {
hof_entry_t *entry = &hof_table[i];
for (j = 0; j < HOF_NAME_SIZE - 1; j++) {
uint8_t ch = src[off++];
/* Имя намеренно ASCII: это также отбрасывает мусор из чужого
* формата, не позволяя строке уйти за свою границу. */
if (ch && (ch < ' ' || ch > 'Z')) return 0;
entry->name[j] = (char)ch;
}
entry->name[HOF_NAME_SIZE - 1] = 0;
entry->minutes = src[off++];
entry->ticks = (uint16_t)(src[off] | ((uint16_t)src[off + 1] << 8));
off += 2;
entry->limited = src[off++];
if (entry->limited > 1 ||
(entry->limited && (entry->ticks == 0 ||
entry->ticks > POP_TIMER_START_TICKS)))
return 0;
}
return 1;
}
static void hof_encode(uint8_t *dst)
{
uint8_t i, j, off, sum;
dst[0] = 'P'; dst[1] = 'H'; dst[2] = 'O'; dst[3] = 'F';
dst[4] = HOF_VERSION;
dst[5] = HOF_COUNT;
off = HOF_HEADER_SIZE;
for (i = 0; i < HOF_COUNT; i++) {
const hof_entry_t *entry = &hof_table[i];
for (j = 0; j < HOF_NAME_SIZE - 1; j++) dst[off++] = entry->name[j];
dst[off++] = entry->minutes;
dst[off++] = (uint8_t)entry->ticks;
dst[off++] = (uint8_t)(entry->ticks >> 8);
dst[off++] = entry->limited;
}
sum = hof_xor(dst, (uint8_t)(HOF_SIZE - 2));
dst[HOF_SIZE - 2] = sum;
dst[HOF_SIZE - 1] = (uint8_t)~sum;
}
static void hof_load(void)
{
uint8_t blk, page, i;
int got;
hof_clear();
blk = mem_alloc_pages(1);
if (!blk) return;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
got = bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
if (got == HOF_SIZE) {
gfx_w0_map(page);
for (i = 0; i < HOF_SIZE; i++)
hof_buf[i] = ((const uint8_t *)HOF_BASE)[i];
gfx_w0_unmap();
if (!hof_decode(hof_buf, (uint8_t)HOF_SIZE)) hof_clear();
}
mem_free_block(blk);
}
static void hof_save(void)
{
uint8_t blk, page, i;
blk = mem_alloc_pages(1);
if (!blk) return;
page = mem_get_page(blk, 0);
gfx_w0_page_prepare(page);
hof_encode(hof_buf);
gfx_w0_map(page);
for (i = 0; i < HOF_SIZE; i++) ((uint8_t *)HOF_BASE)[i] = hof_buf[i];
gfx_w0_unmap();
/* Неудача записи не меняет только что показанную таблицу: следующее
* открытие разумно начнёт с прежнего файла или с пустого списка. */
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
mem_free_block(blk);
}
static uint8_t hof_better(const hof_entry_t *entry)
{
uint8_t limited = pop_settings.time_limit_enabled;
if (!entry->name[0]) return 1;
/* Unlimited сохраняется и выводится, но не может вытеснить ограниченный
* таймером результат: у него нет сравнимого времени. */
if (limited != entry->limited) return limited;
if (pop_timer_minutes != entry->minutes)
return (uint8_t)(pop_timer_minutes > entry->minutes);
return (uint8_t)(pop_timer_ticks > entry->ticks);
}
static uint8_t hof_insert_current(void)
{
uint8_t i, j;
for (i = 0; i < HOF_COUNT; i++) {
if (!hof_better(&hof_table[i])) continue;
for (j = HOF_COUNT - 1; j > i; j--) hof_table[j] = hof_table[j - 1];
hof_clear_entry(&hof_table[i]);
hof_table[i].minutes = pop_timer_minutes;
hof_table[i].ticks = pop_timer_ticks;
hof_table[i].limited = pop_settings.time_limit_enabled;
return i;
}
return HOF_NO_ROW;
}
static uint8_t hof_has_entries(void)
{
uint8_t i;
for (i = 0; i < HOF_COUNT; i++)
if (hof_table[i].name[0]) return 1;
return 0;
}
static void hof_dec(uint8_t value, char *out)
{
if (value >= 100) {
*out++ = (char)('0' + value / 100);
value %= 100;
*out++ = (char)('0' + value / 10);
} else if (value >= 10) {
*out++ = (char)('0' + value / 10);
}
*out++ = (char)('0' + value % 10);
*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;
}
/* min == 0 у оригинала означает истёкшее время и печатается как 0:00. */
hof_dec(entry->minutes ? (uint8_t)(entry->minutes - 1) : 0, out);
while (*out) out++;
seconds = entry->minutes ? (uint8_t)(entry->ticks / 12) : 0;
*out++ = ':';
*out++ = (char)('0' + seconds / 10);
*out++ = (char)('0' + seconds % 10);
*out = 0;
}
/* show_hof_text(): сначала тень со сдвигом (1,1), затем сам текст. У нас
* цвет запечён в глифы, поэтому «другой цвет» — это второй набор
* (POP_TEXT_BIG_DARK), а не параметр рисования. */
static void hof_text_left(int x, int baseline, const char *s)
{
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x + 1, baseline + 1, s);
pop_text_draw_mapped(POP_TEXT_BIG, x, baseline, s);
}
static void hof_text_right(int right, int baseline, const char *s)
{
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-страницу.
*
* БАНК ЗДЕСЬ TRANSPARENT, А НЕ SPRITE. Полосовой переход копирует страницу
* акселератором, а тот читает ОЗУ-копию (gfx_copy_page); SPRITE — это
* NOSHADOW + TRANSPARENT, то есть в копию он не пишет, и на титрах
* переезжал бы один фон без строк. TRANSPARENT даёт ту же прозрачность
* 0xFF, но обновляет и копию, поэтому таблица проявляется уже собранной. */
static void hof_draw_rows(uint8_t edit_row)
{
uint8_t i;
gfx_set_bank(GFX_BANK_TRANSPARENT);
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_TRANSPARENT);
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();
}
}
/* Set 2 make-коды A..Z, затем space/backspace/enter/esc. Храним edge для
* каждой клавиши, поэтому зажатая буква не заполняет имя автоповтором. */
static const uint8_t hof_key_code[30] = {
0x1C, 0x32, 0x21, 0x23, 0x24, 0x2B, 0x34, 0x33, 0x43,
0x3B, 0x42, 0x4B, 0x3A, 0x31, 0x44, 0x4D, 0x15, 0x2D,
0x1B, 0x2C, 0x3C, 0x2A, 0x1D, 0x22, 0x35, 0x1A,
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 *changed)
{
uint8_t i;
for (i = 0; i < 30; i++) {
uint8_t down = kbd_raw_down(hof_key_code[i]);
if (down && !previous[i]) {
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;
}
}
previous[i] = down;
}
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, 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;
/* Стартовое состояние клавиш: зажатое с прошлого экрана не считается
* нажатием, поэтому первый 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, &changed)) {
if (!length) strcpy(entry->name, "PLAYER");
return;
}
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();
}
}
void pop_hof_show(void) __banked
{
uint8_t row;
hof_load();
row = hof_insert_current();
if (row != HOF_NO_ROW) {
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_music();
pop_pal_fade_out(HOF_FADE);
pop_music_free();
pop_sfx_pause();
}
uint8_t pop_hof_show_title(void) __banked
{
uint8_t page;
hof_load();
if (!hof_has_entries()) return 0;
page = pop_screen_begin();
/* Страница собирается ЦЕЛИКОМ до показа — как offscreen оригинала:
* draw_full_image + show_hof, и только потом transition_ltr
* (seg000:2063). Ни fade, ни смены палитры: экран проявляется прямо
* поверх credits, на той же story.pal. */
if (hof_page_build(page, HOF_NO_ROW) != 0) return 0;
pop_screen_present_ltr();
return hof_wait_frames(HOF_TITLE_TICKS);
}