Files
Sprinter-SDCC/applications/PoP/roomtest/pop_hof.c
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00

371 lines
11 KiB
C

/*
* pop_hof.c — persistent Hall of Fame, банк 9 (FG12).
*
* 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_hof.h"
#define HOF_COUNT 5
#define HOF_NAME_SIZE 10 /* девять букв + '\0' */
#define HOF_REC_SIZE 14
#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_NO_ROW 0xFF
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, 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;
}
static void hof_result_text(const hof_entry_t *entry, char *out)
{
if (!entry->limited) {
strcpy(out, "UNLIMITED");
return;
}
hof_dec(entry->minutes, out);
while (*out) out++;
*out++ = ' ';
*out++ = 'M';
*out++ = 'I';
*out++ = 'N';
*out = 0;
}
static void hof_draw(uint8_t edit_row)
{
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);
}
static void hof_wait_release(void)
{
while (kbd_raw_any_down()) {
kbd_raw_sync();
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
};
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
uint8_t *previous)
{
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;
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = ' ';
entry->name[*length] = 0;
} else if (i == 27 && *length) {
entry->name[--(*length)] = 0;
} else if (i == 28 || i == 29) {
return 1;
}
}
previous[i] = down;
}
return 0;
}
static void hof_edit(uint8_t row)
{
uint8_t length = 0, previous[30], i;
hof_entry_t *entry = &hof_table[row];
for (i = 0; i < sizeof(previous); i++) previous[i] = 0;
hof_wait_release();
for (;;) {
kbd_raw_sync();
if (hof_name_tick(entry, &length, previous)) {
if (!length) strcpy(entry->name, "PLAYER");
hof_save();
hof_wait_release();
return;
}
hof_draw(row);
}
}
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(4);
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();
gfx_wait_vsync();
}
hof_wait_release();
}
pop_pal_fade_out(HOF_FADE);
}
void pop_hof_show_title(void) __banked
{
/* Один keypress должен пропускать всю цепочку title/intro, а не внезапно
* останавливаться на уже известной таблице. */
if (kbd_raw_any_down()) return;
hof_load();
if (!hof_has_entries()) return;
pop_sfx_pause();
pop_pal_story_load();
pop_pal_apply(4);
hof_draw(HOF_NO_ROW);
pop_pal_fade_in(HOF_FADE);
hof_wait_release();
while (!kbd_raw_any_down()) {
kbd_raw_sync();
gfx_wait_vsync();
}
hof_wait_release();
pop_pal_fade_out(HOF_FADE);
}