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