/* * 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 #include #include #include #include #include #include #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); }