0db4f94707
ДЛИТЕЛЬНОСТЬ. fade_in_1/fade_out_1 у оригинала — 64 шага палитры по два тика, то есть 128 тиков = 2,13 с каждый; сцена перед уровнем 2 идёт с ними около семи секунд. Наши четыре кадра укладывались в восемь сотых секунды, и сцена выходила втрое короче. INTRO_FADE и TITLE_FADE теперь POP_T60(128). Ступеней яркости стало восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, 1/8, 0 — каждая парой сдвигов, умножения на Z80 не нужно): растянуть четыре ступени на две секунды значило бы получить четыре скачка яркости. МУЗЫКА. Прошлый коммит отдавал трек доигрывать уже в игре — так делает оригинал. На слух вышло хуже: музыка спотыкается, потому что загрузка уровня не даёт насосу долить блок вовремя (у DOS-версии такой проблемы нет). Дослушиваем под чёрным экраном, до отрисовки уровня — тем более что следующий load_intro у оригинала всё равно начинается с ожидания тишины (seg001:681). Пропуск сцены обрывает и музыку. Заодно: WAIT перед PV пересчитан (584 вместо 707 — наш fade больше не короче), а тема титров пускается ПОСЛЕ fade_in, как в show_title. Host-тесты: 15 наборов. Проверено в MAME: плавное появление титров. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
371 lines
11 KiB
C
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(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();
|
|
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(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();
|
|
gfx_wait_vsync();
|
|
}
|
|
hof_wait_release();
|
|
pop_pal_fade_out(HOF_FADE);
|
|
}
|