From 6bd5baf7c29bbeb6ef7c1ca1f8eec4c1d27d89fa Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sun, 9 Aug 2026 16:09:08 +0900 Subject: [PATCH] Implement game specific commands for Gakuen KING --- src/sys/ags.h | 11 +- src/sys/ags_draw.cpp | 69 +- src/sys/ags_window.cpp | 9 +- src/sys/nact.cpp | 15 +- src/sys/nact.h | 8 +- src/sys/nact_gakuenking.cpp | 1899 +++++++++++++++++++++-- src/sys/nact_input.cpp | 5 +- src/sys/scenario.h | 1 + test/CMakeLists.txt | 21 +- test/README.md | 4 +- test/scenario/gakuen_king/ADISK.DAT | Bin 0 -> 3840 bytes test/scenario/gakuen_king/AMAP.DAT | Bin 0 -> 40960 bytes test/scenario/gakuen_king/make_amap.py | 45 + test/scenario/gakuen_king/map.adv | 135 ++ test/scenario/gakuen_king/saveload.adv | 81 + test/scenario/gakuen_king/sys3c.cfg | 5 + test/scenario/gakuen_king/test.adv | 15 + test/scenario/gakuen_king/test.hed | 3 + test/scenario/gakuen_king/variables.txt | 768 +++++++++ 19 files changed, 2933 insertions(+), 161 deletions(-) create mode 100644 test/scenario/gakuen_king/ADISK.DAT create mode 100644 test/scenario/gakuen_king/AMAP.DAT create mode 100644 test/scenario/gakuen_king/make_amap.py create mode 100644 test/scenario/gakuen_king/map.adv create mode 100644 test/scenario/gakuen_king/saveload.adv create mode 100644 test/scenario/gakuen_king/sys3c.cfg create mode 100644 test/scenario/gakuen_king/test.adv create mode 100644 test/scenario/gakuen_king/test.hed create mode 100644 test/scenario/gakuen_king/variables.txt diff --git a/src/sys/ags.h b/src/sys/ags.h index 7950180..ff3c466 100644 --- a/src/sys/ags.h +++ b/src/sys/ags.h @@ -109,6 +109,11 @@ public: void update_screen(); void load_cg(int page, int transparent); + // Decodes a CG without drawing it, but still updates the palette. + CG load_cg_surface(int page, int transparent = -1); + // Returns an ACG page bytes, without decoding it. + std::vector load_cg_data(int page) { return acg.load(page); } + void blit_cg(int dest, CG& cg, const SDL_Rect* src, int dx, int dy); void set_cg_file(const char *file_name); void load_censor_list(const char* fname); @@ -121,7 +126,11 @@ public: void fade_out(int duration_ms, bool white); void fade_in(int duration_ms); - void copy(int sx, int sy, int ex, int ey, int dx, int dy); + void copy(int sx, int sy, int ex, int ey, int dx, int dy) { + copy_screen(src_screen, dest_screen, sx, sy, ex, ey, dx, dy); + } + void copy_screen(int src, int dest, int sx, int sy, int ex, int ey, int dx, int dy, + int transparent_color = -1); void gcopy(int gsc, int gde, int glx, int gly, int gsw); void paint(int x, int y, uint8 color); void draw_box(int index); diff --git a/src/sys/ags_draw.cpp b/src/sys/ags_draw.cpp index 0a9eadf..9c583dd 100644 --- a/src/sys/ags_draw.cpp +++ b/src/sys/ags_draw.cpp @@ -31,15 +31,15 @@ void mosaic(SDL_Surface* sf) { } // namespace -void AGS::load_cg(int page, int transparent) +CG AGS::load_cg_surface(int page, int transparent) { if (bmp_prefix) { WARNING("not implemented"); - return; + return CG(); } std::vector data = acg.load(page); if (data.empty()) - return; + return CG(); bool set_palette = extract_palette && !ignore_palette.count(page); CG cg; @@ -92,24 +92,12 @@ void AGS::load_cg(int page, int transparent) break; } - // J command - if (cg_dest.has_value()) { - cg.x = cg_dest->x; - cg.y = cg_dest->y; - cg_dest = std::nullopt; + if (cg.surface()) { + SDL_SetSurfacePalette(cg.surface(), screen_palette); + if (censor_list.count(page)) + mosaic(cg.surface()); } - if (extract_cg && cg.surface()) { - SDL_SetSurfacePalette(cg.surface(), screen_palette); - if (censor_list.count(page)) { - mosaic(cg.surface()); - } - SDL_Rect dstrect = { cg.x, cg.y, cg.width(), cg.height() }; - SDL_BlitSurface(cg.surface(), NULL, hBmpScreen[dest_screen], &dstrect); - if (dest_screen == 0) { - draw_screen(dstrect.x, dstrect.y, dstrect.w, dstrect.h); - } - } if (set_palette) { dirty_rect = {0, 0, screen_width, screen_height}; #ifdef ENABLE_DEBUGGER @@ -117,20 +105,53 @@ void AGS::load_cg(int page, int transparent) g_debugger->on_palette_change(); #endif } + return cg; } -void AGS::copy(int sx, int sy, int ex, int ey, int dx, int dy) +void AGS::blit_cg(int dest, CG& cg, const SDL_Rect* src, int dx, int dy) +{ + if (!cg.surface()) + return; + SDL_Rect dstrect = { dx, dy, src ? src->w : cg.width(), src ? src->h : cg.height() }; + SDL_BlitSurface(cg.surface(), src, hBmpScreen[dest], &dstrect); + if (dest == 0) { + draw_screen(dstrect.x, dstrect.y, dstrect.w, dstrect.h); + } +} + +void AGS::load_cg(int page, int transparent) +{ + CG cg = load_cg_surface(page, transparent); + if (!cg) + return; + + // J command + if (cg_dest.has_value()) { + cg.x = cg_dest->x; + cg.y = cg_dest->y; + cg_dest = std::nullopt; + } + + if (extract_cg) + blit_cg(dest_screen, cg, NULL, cg.x, cg.y); +} + +void AGS::copy_screen(int src, int dest, int sx, int sy, int ex, int ey, int dx, int dy, int transparent_color) { int width = ex - sx + 1; int height = ey - sy + 1; SDL_Rect srcrect = {sx, sy, width, height}; SDL_Rect destrect = {dx, dy, width, height}; - SDL_BlitSurface(hBmpScreen[src_screen], &srcrect, hBmpScreen[dest_screen], &destrect); + SDL_Surface* src_surface = hBmpScreen[src]; + if (transparent_color >= 0) + SDL_SetColorKey(src_surface, SDL_TRUE, transparent_color); + SDL_BlitSurface(src_surface, &srcrect, hBmpScreen[dest], &destrect); + if (transparent_color >= 0) + SDL_SetColorKey(src_surface, SDL_FALSE, 0); - if(dest_screen == 0) { - draw_screen(dx, dy, ex - sx + 1, ey - sy + 1); - } + if (dest == 0) + draw_screen(dx, dy, width, height); } void AGS::gcopy(int gsc, int gde, int glx, int gly, int gsw) diff --git a/src/sys/ags_window.cpp b/src/sys/ags_window.cpp index 7959920..5029b70 100644 --- a/src/sys/ags_window.cpp +++ b/src/sys/ags_window.cpp @@ -104,7 +104,14 @@ void AGS::init_windows() case 3: text_w[i].reset(24, 310, 183, 391, false, false); break; case 4: text_w[i].reset(464, 310, 623, 391, false, false); break; } - menu_w[i].reset(416, 24, 591, 248, true, true); + switch (i) { + default: + case 0: menu_w[i].reset(416, 24, 591, 248, true, true); break; + case 1: menu_w[i].reset(32, 16, 191, 216, true, true); break; + case 2: menu_w[i].reset(448, 16, 607, 216, true, true); break; + case 3: menu_w[i].reset(32, 223, 191, 383, true, true); break; + case 4: menu_w[i].reset(448, 223, 607, 383, true, true); break; + } break; default: text_w[i].reset(8, 311, 623, 391, true, false); diff --git a/src/sys/nact.cpp b/src/sys/nact.cpp index 8c5f8a2..a7971a7 100644 --- a/src/sys/nact.cpp +++ b/src/sys/nact.cpp @@ -513,18 +513,15 @@ void NACT::message(uint8_t first_byte) void NACT::text_wait() { + if (msgskip->skipping()) + return; + Uint32 dwTime = SDL_GetTicks() + text_wait_time; - for(;;) { - if(terminate) { - return; - } - RND = get_key(); - if(RND && wait_keydown) { + while (!terminate) { + if (get_key(false) && wait_keydown) break; - } - if(dwTime <= SDL_GetTicks()) { + if (dwTime <= SDL_GetTicks()) break; - } sys_sleep(16); } } diff --git a/src/sys/nact.h b/src/sys/nact.h index 766c18e..5749f8f 100644 --- a/src/sys/nact.h +++ b/src/sys/nact.h @@ -22,11 +22,11 @@ #define MAX_VERB 128 #define MAX_OBJ 256 -#define MAX_VAR 512 +#define MAX_VAR 768 // Gakuen KING uses up to VAR737. #define MAX_STRVAR 10 #define TRACE(fmt, ...) if (config.trace) trace(fmt "\n", ##__VA_ARGS__) -#define TRACE_UNIMPLEMENTED(fmt, ...) trace("Unimplemented command " fmt "\n", ##__VA_ARGS__) +#define TRACE_UNIMPLEMENTED(fmt, ...) trace("Unimplemented command at %d:%04x " fmt "\n", sco.page(), sco.cmd_addr(), ##__VA_ARGS__) class AGS; class MAKO; @@ -80,7 +80,7 @@ protected: void cmd_set_verbobj2(); virtual void cmd_open_verb() = 0; - void cmd_a(); + virtual void cmd_a(); virtual void cmd_b() { sco.unknown_command('B'); } virtual void cmd_d() { sco.unknown_command('D'); } virtual void cmd_e() { sco.unknown_command('E'); } @@ -146,7 +146,7 @@ protected: int menu_select(int num_items); void wait_after_open_menu(); - uint8 get_key(); + uint8 get_key(bool notify_texthook = true); void wait_key_release(uint8_t mask = 0xff); void get_cursor(int* x, int* y); diff --git a/src/sys/nact_gakuenking.cpp b/src/sys/nact_gakuenking.cpp index 7a48fd3..13584bb 100644 --- a/src/sys/nact_gakuenking.cpp +++ b/src/sys/nact_gakuenking.cpp @@ -1,100 +1,94 @@ #include +#include +#include +#include +#include +#include #include "nact.h" #include "ags.h" +#include "fileio.h" #include "mako.h" +#define D01 var[1] +#define D02 var[2] +#define D03 var[3] +#define D04 var[4] +#define VAR_CHAR_LEVEL var[57] +#define VAR_LOYALTY var[58] +#define VAR_TROOP_COUNT var[59] +#define VAR_TROOP_COUNT_FULL var[60] +#define VAR_TROOP_LIMIT var[61] +#define VAR_UNASSIGNED_TROOPS var[62] +#define VAR_ATTACK_POWER var[63] +#define VAR_DEFENSE_POWER var[64] +#define VAR_SPECIAL_MOVE_GAUGE var[65] +#define VAR_JAPANESE_SCORE var[67] +#define VAR_MATH_SCORE var[68] +#define VAR_ENGLISH_SCORE var[69] +#define VAR_PE_SCORE var[70] +#define VAR_MONEY var[71] +#define VAR_ENEMY_TROOP_COUNT var[72] +#define VAR_ENEMY_TROOP_COUNT_FULL var[73] +#define VAR_ENEMY_ATTACK_POWER var[74] +#define VAR_ENEMY_DEFENSE_POWER var[75] +#define VAR_BATTLE_DRAW_X8 var[76] +#define VAR_BATTLE_DRAW_Y var[77] +#define VAR_BATTLE_DRAW_CG var[78] + namespace { class NACT_GakuenKing : public NACT_Sys3 { public: NACT_GakuenKing(const Config& config, const GameId& game_id) - : NACT_Sys3(config, game_id) {} - - void cmd_b() override { - int p1 = cali(); - int p2 = cali(); - TRACE_UNIMPLEMENTED("B %d,%d:", p1, p2); + : NACT_Sys3(config, game_id) { + text_wait_time = 0; // Y 13 and Y 14 set this + ags->set_box(1, 0, 16, 8, 367, 295); + ags->set_box(2, 0, 40, 0, 599, 298); + load_executable_data(); + amap.open("AMAP.DAT"); + map_base.fill(0); + map_layer_z.fill(CELL_EMPTY); + map_layer_w.fill(CELL_EMPTY); + for (auto& record : map_explored) + record.fill(0); } - void cmd_i() override { - int p1 = cali(); - int p2 = cali(); - int p3 = cali(); - - switch (p1) { - case 1: - if (p3 == 255) { - for (int i = 0; i < 63; i++) { - grid_select_data[i] = p2; - } - } else if (1 <= p3 && p3 <= 63) { - grid_select_data[p3 - 1] = p2; - } - break; - case 2: - switch (p2) { - case 1: - RND = grid_select(376, 8, 32, 32, 7, 9, grid_prev_selection); - if (RND != GRID_SELECT_CANCELED) - grid_prev_selection = RND; - wait_key_release(); - break; - default: - TRACE_UNIMPLEMENTED("I %d,%d,%d:", p1, p2, p3); - return; - } - break; - default: - TRACE_UNIMPLEMENTED("I %d,%d,%d:", p1, p2, p3); - return; - } - TRACE("I %d,%d,%d:", p1, p2, p3); +private: + void cmd_a() override { + NACT::cmd_a(); + map_overview_rebuild_pending = true; } - void cmd_n() override { - int p1 = cali(); - int p2 = cali(); - TRACE_UNIMPLEMENTED("N %d,%d:", p1, p2); - } - - void cmd_p() override { - int p1 = cali(); - TRACE("P %d:", p1); - ags->text.font_color = p1; - } - - void cmd_t() override { - int p1 = cali(); - int p2 = cali(); - int p3 = cali(); - TRACE_UNIMPLEMENTED("T %d,%d,%d:", p1, p2, p3); - } - - void cmd_w() override { - int p1 = cali(); - int p2 = cali(); - int p3 = cali(); - TRACE_UNIMPLEMENTED("W %d,%d,%d:", p1, p2, p3); - } + // --- Y command dispatch ----------------------------------------------- void cmd_y() override { int cmd = cali(); - int param = cali(); + // Y 82 takes a variable reference, not a value. + int param = (cmd == 82) ? cali2() : cali(); TRACE("Y %d,%d:", cmd, param); switch (cmd) { - case 14: - case 26: - case 80: - case 81: - case 82: - TRACE_UNIMPLEMENTED("Y %d,%d:", cmd, param); + // --- Gakuen KING specific --- + case 5: // CG cache preload - no cache in this engine + case 6: // CG cache release - no cache in this engine break; case 9: for (int page = 1; page <= 99; page++) mako->set_cd_track(page, param ? page : 0); break; + case 11: + draw_status_panel(param); + break; + case 12: + run_troop_allocation(); + break; + case 13: + set_text_wait(param == 2 ? 3 : 0); + break; + case 14: + set_text_wait(param == 0 ? 0 : param * 16 / 10); + break; case 20: // audio device check exec_y(14, param); break; @@ -106,70 +100,1757 @@ public: ags->open_text_window(text_window, false); } break; + case 26: + if (0 <= param && param <= 4) + menu_window = param + 1; + ags->menu.back_color = (param == 0) ? 1 : 0; + break; + case 50: + map_overview_rebuild_pending = true; + map_sprite_cg_page = param; + map_sprite_cg = load_map_sheet(param); + break; + case 51: + case 52: + case 53: + { + int bank = cmd - 51; + map_overview_rebuild_pending = true; + map_tile_cg_page[bank] = param; + map_tile_cg[bank] = load_map_sheet(param); + } + break; + case 55: + switch_map_area(param); + break; + case 56: + map_base_visible_view = map_base_visible_overview = !!param; + break; + case 57: + map_overview_visible = map_marker_visible = !!param; + reset_map_overview(); + break; + case 58: + map_out_of_bounds_tile = param & 0xff; + break; + case 59: + explored_record().fill(param ? 0xff : 0x00); + break; + case 80: + update_battle_screen(param); + break; + case 81: + run_staff_roll(); + break; + case 82: + load_saved_variable(param); + break; case 83: sco.label_stack_clear(); sco.page_stack_clear(); break; + case 84: + pixel_dissolve(param); + break; + case 111: + load_cg_to_work_screen(param); + break; + case 112: + map_overview_rebuild_pending = true; + break; case 118: ags->text.pos.x = param; break; case 119: ags->text.pos.y = param; break; - default: + + // --- Shared with generic System3 --- + case 1: // clear message window + case 2: // clear variable group + case 3: // wait with timeout + case 4: // random number + case 7: // clear a registered box + case 8: // select string variable slot + case 16: // stop BGM + case 31: // select drawing screen + case 40: // fade in + case 41: // fade out + case 42: // fade in + case 43: // fade out + case 46: // CG / palette flags + case 60: // screen scroll position + case 101: // select PCM slot + case 102: // register PCM + case 103: // play PCM + case 239: // save file timestamp + case 240: // halfwidth drawing mode + case 253: // page break mark + case 255: // quit exec_y(cmd, param); break; + + default: + TRACE_UNIMPLEMENTED("Y %d,%d:", cmd, param); + break; } } - void cmd_z() override { - int p1 = cali(); - int p2 = cali(); - int p3 = cali(); - TRACE_UNIMPLEMENTED("Z %d,%d,%d:", p1, p2, p3); + // --- data read out of GAKUEN.COM -------------------------------------- + + static constexpr const char* EXECUTABLE_NAME = "GAKUEN.COM"; + + void load_executable_data() { + auto fio = FILEIO::open(EXECUTABLE_NAME, FILEIO_READ_BINARY); + if (!fio) { + WARNING("Cannot open %s", EXECUTABLE_NAME); + return; + } + std::vector exe(CREDIT_DATA_END); + if (!fio->read(exe.data(), exe.size())) { + WARNING("%s: too short", EXECUTABLE_NAME); + return; + } + load_digit_font(exe); + load_credit_text(exe); } -private: - const uint16_t GRID_SELECT_CANCELED = 80; - uint16_t grid_select_data[63] = {}; - uint16_t grid_prev_selection = 0; + // --- timing ----------------------------------------------------------- + + static int ticks_to_ms(int ticks) { return ticks * 1000 / 60; } + + void wait_ticks(int ticks) { + uint32_t deadline = SDL_GetTicks() + ticks_to_ms(ticks); + while (!terminate && SDL_GetTicks() < deadline) + sys_sleep(16); + } + + // --- screens and CG loading ------------------------------------------- + + // The second VRAM page for offscreen work. + static constexpr int WORK_SCREEN = 1; + + // Palette index the color keyed blits treat as transparent. + static constexpr int TRANSPARENT_COLOR = 7; + + // Y 111 + void load_cg_to_work_screen(int page) { + int saved = ags->dest_screen; + ags->dest_screen = WORK_SCREEN; + ags->load_cg(page, -1); + ags->dest_screen = saved; + } + + // --- text ------------------------------------------------------------- + + void cmd_p() override { + int p1 = cali(); + TRACE("P %d:", p1); + ags->text.font_color = p1; + } + + // The per-character wait of Y 13 and Y 14, in 1/100 s units. + void set_text_wait(int centiseconds) { + text_wait_time = centiseconds * 10; + } + + // --- map cells -------------------------------------------------------- + + // The map is 100x100 cells. Cell data lives in three parallel planes: the + // 16-bit base map loaded from AMAP.DAT, and the two 8-bit overlay layers + // written by Z and W, where 0xff means "empty". + static constexpr int MAP_WIDTH = 100; + static constexpr int MAP_HEIGHT = 100; + static constexpr int MAP_CELLS = MAP_WIDTH * MAP_HEIGHT; + static constexpr uint8_t CELL_EMPTY = 0xff; + + // The exploration bitmap is one bit per cell, 14 bytes per row, MSB first. + static constexpr int EXPLORED_ROW_BYTES = 14; + static constexpr int EXPLORED_RECORD_BYTES = EXPLORED_ROW_BYTES * MAP_HEIGHT; + // One record per map area, plus an extra record standing in for map ids + // outside that range (Y 55,255). + static constexpr int NR_EXPLORED_RECORDS = 60; + static constexpr int EXPLORED_SCRATCH = NR_EXPLORED_RECORDS; + + Dri amap; + std::array map_base; + std::array map_layer_z; + std::array map_layer_w; + std::array, NR_EXPLORED_RECORDS + 1> map_explored; + + int map_current_area = 0; + int amap_loaded_page = -1; + + // Writes the base map cell at (x, y) from D01-D04. + void cmd_b() override { + uint16_t x = cali(); + uint16_t y = cali(); + TRACE("B %d,%d:", x, y); + + int index = cell_index(x, y); + if (index < 0) + return; + uint8_t high = ((D01 & 1) << 6) | ((D02 & 1) << 5) | (D04 & 0x1f); + map_base[index] = (high << 8) | (D03 & 0xff); + } + + // Reads the map cell at (x, y) into D01-D04, W and Z overlays taking + // precedence over the base map. + void cmd_n() override { + uint16_t x = cali(); + uint16_t y = cali(); + TRACE("N %d,%d:", x, y); + + int index = cell_index(x, y); + if (index < 0) + return; + if (map_layer_w[index] != CELL_EMPTY) { + D01 = 1; + D02 = 1; + D03 = map_layer_w[index]; + D04 = 64; + } else if (map_layer_z[index] != CELL_EMPTY) { + D01 = 1; + D02 = 1; + D03 = map_layer_z[index]; + D04 = 32; + } else { + uint16_t cell = map_base[index]; + D01 = (cell >> 14) & 1; + D02 = (cell >> 13) & 1; + D03 = cell & 0xff; + D04 = cell & 0x1f; + } + } + + void cmd_w() override { + uint16_t x = cali(); + uint16_t y = cali(); + uint16_t value = cali(); + TRACE("W %d,%d,%d:", x, y, value); + write_map_layer(map_layer_w, x, y, value); + } + + void cmd_z() override { + uint16_t x = cali(); + uint16_t y = cali(); + uint16_t value = cali(); + TRACE("Z %d,%d,%d:", x, y, value); + write_map_layer(map_layer_z, x, y, value); + } + + int cell_index(uint16_t x, uint16_t y) { + uint16_t index = static_cast(y * MAP_WIDTH + x); + if (index >= MAP_CELLS) { + WARNING("map cell (%d,%d) is out of range", x, y); + return -1; + } + return index; + } + + // The overlay layers always block; the base map blocks on bit 13. + bool cell_is_passable(int x, int y) { + int index = cell_index(static_cast(x), static_cast(y)); + if (index < 0) + return false; + if (map_layer_w[index] != CELL_EMPTY || map_layer_z[index] != CELL_EMPTY) + return false; + return !((map_base[index] >> 13) & 1); + } + + void write_map_layer(std::array& layer, uint16_t x, uint16_t y, uint16_t value) { + // 254 clears the whole layer + if (value == 254) { + layer.fill(CELL_EMPTY); + return; + } + int index = cell_index(x, y); + if (index < 0) + return; + layer[index] = value & 0xff; + } + + void clear_map_layers() { + map_layer_z.fill(CELL_EMPTY); + map_layer_w.fill(CELL_EMPTY); + } + + void load_amap_page(int page) { + std::vector data = amap.load(page); + if (data.size() < MAP_CELLS * 2) + ERROR("AMAP page %d is missing or too short (%zu bytes)", page, data.size()); + for (int i = 0; i < MAP_CELLS; i++) + map_base[i] = data[i * 2] | (data[i * 2 + 1] << 8); + amap_loaded_page = page; + } + + std::array& explored_record() { + int record = map_current_area; + if (record < 0 || record >= NR_EXPLORED_RECORDS) + record = EXPLORED_SCRATCH; + return map_explored[record]; + } + + bool is_explored(int x, int y) { + return explored_record()[y * EXPLORED_ROW_BYTES + x / 8] & (0x80 >> (x % 8)); + } + + void set_explored(int x, int y) { + explored_record()[y * EXPLORED_ROW_BYTES + x / 8] |= 0x80 >> (x % 8); + } + + // Y 55 + void switch_map_area(int map_id) { + map_overview_rebuild_pending = true; + map_current_area = map_id; + path.clear(); + + if (map_id == 0) { + // Fill the direction key panel area with color 1. + ags->box_fill(0, 16, 304, 103, 391, 1); + reset_map_overview(); + } else if (map_id == 255) { + for (auto& record : map_explored) + record.fill(0); + } else { + if (map_id != amap_loaded_page) + load_amap_page(map_id); + clear_map_layers(); + map_out_of_bounds_tile = CELL_EMPTY; + reset_map_overview(); + } + } + + // --- map view --------------------------------------------------------- + + // J draws 11x9 cells of 32x32 pixels into (16,8)-(367,295). + static constexpr int VIEW_COLS = 11; + static constexpr int VIEW_ROWS = 9; + static constexpr int VIEW_X = 16; + static constexpr int VIEW_Y = 8; + static constexpr int TILE_SIZE = 32; + + // A CG loaded by Y 50-53 holds an 8-wide grid of 32x32 tiles, + // followed by an 8-wide grid of the matching 2x2 overview patterns. + static constexpr int OVERVIEW_PATTERN_Y = 256; + static constexpr int SHEET_COLS = 8; + static constexpr int TILES_PER_BANK = 64; + static constexpr int NR_TILE_BANKS = 3; + static constexpr int NR_SPRITE_FRAMES = 8; + // T ...,255 leaves no sprite to draw. + static constexpr uint8_t SPRITE_HIDDEN = 0xff; + + uint16_t map_view_x = 0; + uint16_t map_view_y = 0; + uint16_t map_sprite_x = 0; + uint16_t map_sprite_y = 0; + uint16_t map_sprite_cell = 0; + uint8_t map_sprite_frame = SPRITE_HIDDEN; + uint8_t map_out_of_bounds_tile = CELL_EMPTY; + bool map_base_visible_view = true; + bool map_overview_rebuild_pending = false; + // The J draw wait and the marker blink period. GAKUEN.COM sets this to + // 1-10 with the number keys, but we do not support that. + int map_animation_ticks = 9; + + // The sheets loaded by Y 50-53, kept whole rather than split into per-tile images. + CG map_sprite_cg; + CG map_tile_cg[NR_TILE_BANKS]; + int map_sprite_cg_page = 0; + int map_tile_cg_page[NR_TILE_BANKS] = {}; + + void cmd_j() override { + map_view_x = cali(); + map_view_y = cali(); + TRACE("J %d,%d:", map_view_x, map_view_y); + draw_map_view(); + } + + // T x,y,direction: puts the sprite on map cell (x,y) and picks its + // animation frame from the direction; 255 hides it. The next J draws it. + void cmd_t() override { + uint16_t x = cali(); + uint16_t y = cali(); + uint16_t direction = cali(); // 1, 2, 4, or 8 + TRACE("T %d,%d,%d:", x, y, direction); + + // Even base frame per direction 1-8. Repeating a direction alternates + // between the even and odd frame; the parity is shared by all + // directions and restarts even after 255. + static const uint8_t direction_frames[8] = {0, 2, 0, 4, 0, 0, 0, 6}; + + map_sprite_x = x; + map_sprite_y = y; + map_sprite_cell = static_cast(y * MAP_WIDTH + x); + if (direction == 0 || direction == 255) { + map_sprite_frame = static_cast(direction); + } else if (direction >= 9) { + map_sprite_frame = 0; + } else { + uint8_t frame = direction_frames[direction - 1]; + if ((map_sprite_frame & 1) == 0) + frame++; + map_sprite_frame = frame; + } + } + + void draw_map_view() { + if (map_overview_rebuild_pending) + map_marker_saved_color_stale = true; + + for (int row = 0; row < VIEW_ROWS; row++) { + for (int col = 0; col < VIEW_COLS; col++) { + int cell_x = map_view_x - 5 + col; + int cell_y = map_view_y - 4 + row; + bool inside = 0 <= cell_x && cell_x < MAP_WIDTH && + 0 <= cell_y && cell_y < MAP_HEIGHT; + int offset = cell_y * MAP_WIDTH + cell_x; + + uint8_t base = inside ? (map_base[offset] & 0xff) : map_out_of_bounds_tile; + uint8_t layer_z = inside ? map_layer_z[offset] : CELL_EMPTY; + uint8_t layer_w = inside ? map_layer_w[offset] : CELL_EMPTY; + uint8_t sprite = (inside && offset == map_sprite_cell) + ? map_sprite_frame : SPRITE_HIDDEN; + + int px = VIEW_X + col * TILE_SIZE; + int py = VIEW_Y + row * TILE_SIZE; + if (!map_base_visible_view) + base = CELL_EMPTY; + if (base == CELL_EMPTY) { + ags->box_fill(0, px, py, px + TILE_SIZE - 1, py + TILE_SIZE - 1, 0); + } else { + draw_map_tile(base, px, py, false); + } + if (layer_z != CELL_EMPTY) + draw_map_tile(layer_z, px, py, false); + if (layer_w != CELL_EMPTY) + draw_map_tile(layer_w, px, py, true); + if (sprite != SPRITE_HIDDEN) + draw_map_sprite(sprite, px, py); + } + } + + update_map_overview(); + + // Movement and animation speed: every map redraw blocks for this long. + wait_ticks(map_animation_ticks); + + map_overview_rebuild_pending = false; + } + + CG load_map_sheet(int page) { + if (page == 0) + return CG(); + CG cg = ags->load_cg_surface(page, TRANSPARENT_COLOR); + if (!cg) + ERROR("map resource error: CG page %d not available", page); + return cg; + } + + void draw_map_tile(uint8_t tile, int px, int py, bool transparent) { + int bank = tile / TILES_PER_BANK; + if (bank >= NR_TILE_BANKS || !map_tile_cg[bank]) { + WARNING("map tile %d has no loaded bank", tile); + return; + } + int index = tile % TILES_PER_BANK; + blit_sheet(map_tile_cg[bank], + (index % SHEET_COLS) * TILE_SIZE, + (index / SHEET_COLS) * TILE_SIZE, + TILE_SIZE, TILE_SIZE, px, py, transparent); + } + + void draw_map_sprite(uint8_t frame, int px, int py) { + if (frame >= NR_SPRITE_FRAMES || !map_sprite_cg) { + WARNING("map sprite frame %d has no loaded CG", frame); + return; + } + blit_sheet(map_sprite_cg, frame * TILE_SIZE, 0, + TILE_SIZE, TILE_SIZE, px, py, true); + } + + void blit_sheet(CG& cg, int sheet_x, int sheet_y, + int width, int height, int dx, int dy, bool transparent) { + SDL_Rect src = { sheet_x, sheet_y, width, height }; + if (src.x + width > cg.width() || src.y + height > cg.height()) { + WARNING("map resource error: (%d,%d)+%dx%d outside CG %dx%d", + sheet_x, sheet_y, width, height, cg.width(), cg.height()); + return; + } + SDL_SetColorKey(cg.surface(), transparent ? SDL_TRUE : SDL_FALSE, + TRANSPARENT_COLOR); + ags->blit_cg(0, cg, &src, dx, dy); + } + + // --- overview map ----------------------------------------------------- + + // The overview map draws the whole map as 2x2 pixels per cell into + // (392,24)-(591,223). + static constexpr int OVERVIEW_X = 392; + static constexpr int OVERVIEW_Y = 24; + static constexpr int OVERVIEW_CELL = 2; + static constexpr int OVERVIEW_W = MAP_WIDTH * OVERVIEW_CELL; + static constexpr int OVERVIEW_H = MAP_HEIGHT * OVERVIEW_CELL; + + bool map_base_visible_overview = true; + bool map_overview_visible = true; + bool map_marker_visible = true; + + // Current-position marker + uint16_t map_marker_x = 0; + uint16_t map_marker_y = 0; + uint16_t map_marker_prev_x = 0; + uint16_t map_marker_prev_y = 0; + uint8_t map_marker_saved_color = 0; + // Set when the overview is about to be redrawn under the marker, so the + // saved color no longer belongs to the screen. + bool map_marker_saved_color_stale = false; + uint8_t map_marker_blink_color = 0; + uint32_t map_marker_blink_deadline = 0; + + void reset_map_overview() { + map_overview_rebuild_pending = true; + ags->box_fill(0, OVERVIEW_X, OVERVIEW_Y, + OVERVIEW_X + OVERVIEW_W - 1, OVERVIEW_Y + OVERVIEW_H - 1, 0); + } + + void update_map_overview() { + if (!map_overview_visible) + return; + if (map_overview_rebuild_pending) + rebuild_map_overview(); + + map_marker_x = map_view_x; + map_marker_y = map_view_y; + + for (int row = 0; row < VIEW_ROWS; row++) { + uint16_t cell_y = static_cast(map_view_y - 4 + row); + if (cell_y >= MAP_HEIGHT) + continue; + for (int col = 0; col < VIEW_COLS; col++) { + uint16_t cell_x = static_cast(map_view_x - 5 + col); + if (cell_x >= MAP_WIDTH) + continue; + int index = cell_y * MAP_WIDTH + cell_x; + uint8_t tile = map_layer_w[index]; + if (tile == CELL_EMPTY) { + // With the base layer hidden, a cell without a W tile is + // neither drawn nor recorded as explored. + if (!map_base_visible_overview) + continue; + tile = map_layer_z[index]; + if (tile == CELL_EMPTY) + tile = map_base[index] & 0xff; + } + draw_map_overview_cell(cell_x, cell_y, tile); + } + } + } + + void rebuild_map_overview() { + ags->box_fill(0, OVERVIEW_X, OVERVIEW_Y, + OVERVIEW_X + OVERVIEW_W - 1, OVERVIEW_Y + OVERVIEW_H - 1, 0); + for (int y = 0; y < MAP_HEIGHT; y++) { + for (int x = 0; x < MAP_WIDTH; x++) { + if (!is_explored(x, y)) + continue; + int index = y * MAP_WIDTH + x; + // The full rebuild always falls back to the base layer, + // regardless of map_base_visible_overview. + uint8_t tile = map_layer_w[index]; + if (tile == CELL_EMPTY) + tile = map_layer_z[index]; + if (tile == CELL_EMPTY) + tile = map_base[index] & 0xff; + draw_map_overview_cell(x, y, tile); + } + } + } + + void draw_map_overview_cell(int x, int y, uint8_t tile) { + set_explored(x, y); + int bank = tile / TILES_PER_BANK; + if (bank >= NR_TILE_BANKS || !map_tile_cg[bank]) + return; + int index = tile % TILES_PER_BANK; + blit_sheet(map_tile_cg[bank], + (index % SHEET_COLS) * OVERVIEW_CELL, + OVERVIEW_PATTERN_Y + (index / SHEET_COLS) * OVERVIEW_CELL, + OVERVIEW_CELL, OVERVIEW_CELL, + OVERVIEW_X + x * OVERVIEW_CELL, OVERVIEW_Y + y * OVERVIEW_CELL, false); + } + + void update_map_marker() { + if (!map_marker_visible) + return; + if (map_marker_x >= MAP_WIDTH || map_marker_y >= MAP_HEIGHT) + return; + + int x = OVERVIEW_X + map_marker_x * OVERVIEW_CELL; + int y = OVERVIEW_Y + map_marker_y * OVERVIEW_CELL; + if (map_marker_prev_x != map_marker_x || map_marker_prev_y != map_marker_y) { + if (!map_marker_saved_color_stale) { + int px = OVERVIEW_X + map_marker_prev_x * OVERVIEW_CELL; + int py = OVERVIEW_Y + map_marker_prev_y * OVERVIEW_CELL; + ags->box_fill(0, px, py, px + OVERVIEW_CELL - 1, py + OVERVIEW_CELL - 1, + map_marker_saved_color); + } + map_marker_prev_x = map_marker_x; + map_marker_prev_y = map_marker_y; + map_marker_saved_color = ags->get_pixel(0, x, y); + map_marker_saved_color_stale = false; + } + + uint32_t now = SDL_GetTicks(); + if (now >= map_marker_blink_deadline) { + map_marker_blink_deadline = now + ticks_to_ms(map_animation_ticks + 10); + map_marker_blink_color ^= 0x0f; + ags->box_fill(0, x, y, x + OVERVIEW_CELL - 1, y + OVERVIEW_CELL - 1, map_marker_blink_color); + } + } + + // --- direction key input ---------------------------------------------- + + // The direction key panel occupies (16,304)-(103,391) on screen. + static constexpr int KEY_PANEL_X = 16; + static constexpr int KEY_PANEL_Y = 304; + static constexpr int KEY_PANEL_SIZE = 88; + static constexpr int KEY_PANEL_ALT_SRC_X = 264; + static constexpr int PATH_MAX_STEPS = 20; + + struct Neighbor { int dx, dy; uint8_t dir; }; + static constexpr Neighbor neighbor_offsets[4] = { + {0, -1, 1}, {0, 1, 2}, {-1, 0, 4}, {1, 0, 8}, + }; + + int key_panel_src_x = 0; + uint8_t key_panel_direction = 0; + int key_last_mode = -1; + uint8_t key_saved_facing = 0; + // Last direction key seen by K, used as the result of K 2. + uint8_t key_facing = 1; + bool key_menu_from_panel = false; + bool key_from_mouse = false; + std::vector path; + + // Unlike the generic System3 K, the result goes to D01, and the direction + // keys are latched into a facing that survives across calls. + void cmd_k() override { + uint8_t mode = sco.getd(); + TRACE("K %d:", mode); + + if (mode != 1 && mode != 2) + WARNING("K %d: unsupported mode", mode); + bool menu = mode != 1; + + // K 2 highlights a second copy of the panel in the work screen. + key_panel_src_x = menu ? KEY_PANEL_ALT_SRC_X : 0; + if (mode != key_last_mode) { + key_last_mode = mode; + key_panel_direction = 0; + key_facing = 0; + draw_key_panel(); + // K 1 resumes the facing left by the previous call; K 2 starts + // facing up. + resolve_direction(menu ? 1 : key_saved_facing); + } + if (menu) + path.clear(); + + if (!key_from_mouse) + wait_key_release(0xf0); + key_from_mouse = false; + + if (menu) + run_direction_menu(); + else + run_move_input(); + + if (!key_from_mouse) + wait_key_release(0xf0); + } + + void run_move_input() { + if (uint8_t dir = next_path_step()) { + take_step(dir); + key_from_mouse = true; + return; + } + while (!terminate) { + uint8_t key = get_key(); + if (key & 0x0f) { + take_step(key); + return; + } + if (key & 0xc0) { + D01 = key; + return; + } + if (key & 0x30) { + int x, y; + get_cursor(&x, &y); // pumps the events SDL_GetMouseState needs + uint32_t buttons = SDL_GetMouseState(NULL, NULL); + if (!buttons) + D01 = (key & 0x10) ? 0x10 : 0x20; // Enter or the cancel key + else + move_input_click(buttons & SDL_BUTTON_LMASK, x, y); + return; + } + update_map_marker(); + sys_sleep(16); + } + } + + // Maps a click during K 1 to a result in D01. The original captures the + // pointer and steps in the facing direction. This port cannot capture it, + // so a click walks to the cell it lands on, like the System3.5 port. + void move_input_click(bool left, int x, int y) { + bool in_view = VIEW_X <= x && x < VIEW_X + VIEW_COLS * TILE_SIZE && + VIEW_Y <= y && y < VIEW_Y + VIEW_ROWS * TILE_SIZE; + if (in_key_panel(x, y)) { + D01 = 0x20; + key_menu_from_panel = true; + } else if (!in_view) { + D01 = left ? 0x10 : 0x40; + } else if (!left) { + D01 = 0x20; + } else if (build_path(map_view_x - VIEW_COLS / 2 + (x - VIEW_X) / TILE_SIZE, + map_view_y - VIEW_ROWS / 2 + (y - VIEW_Y) / TILE_SIZE)) { + take_step(next_path_step()); + key_from_mouse = true; + } else { + int dx = x - (VIEW_X + VIEW_COLS * TILE_SIZE / 2); + int dy = y - (VIEW_Y + VIEW_ROWS * TILE_SIZE / 2); + if (std::abs(dx) > std::abs(dy)) + D01 = dx < 0 ? 4 : 8; + else + D01 = dy < 0 ? 1 : 2; + } + } + + void take_step(uint8_t key) { + D01 = resolve_direction(key) & 0x0f; + key_saved_facing = key_facing; + } + + // K 2. The pointer picks a direction on the panel by hovering; the + // left button or Enter confirms it. + void run_direction_menu() { + int saved_x = -1, saved_y = -1; + if (key_menu_from_panel) { + key_menu_from_panel = false; + get_cursor(&saved_x, &saved_y); + set_cursor(KEY_PANEL_X + KEY_PANEL_SIZE / 2, KEY_PANEL_Y + KEY_PANEL_SIZE / 2); + } + while (!terminate) { + if (uint8_t hover = key_panel_hover_direction()) + resolve_direction(hover); + uint8_t key = resolve_direction(get_key()); + if (key & 0x20) { + D01 = 0x20; + break; + } + if (key & 0x10) { + D01 = key_facing; + break; + } + update_map_marker(); + sys_sleep(16); + } + if (saved_x >= 0) + set_cursor(saved_x, saved_y); + } + + // False when no walk of PATH_MAX_STEPS or fewer reaches cell (x,y). + bool build_path(int x, int y) { + path.clear(); + int target = cell_at(x, y); + int start = cell_at(map_sprite_x, map_sprite_y); + if (target < 0 || start < 0) + return false; + + // Distance from the player, biased by one so that 0 means unvisited. + std::array dist = {}; + std::vector queue{static_cast(start)}; + dist[start] = 1; + for (size_t i = 0; i < queue.size() && !dist[target]; i++) { + if (dist[queue[i]] > PATH_MAX_STEPS) + break; + for (auto& n : neighbor_offsets) { + int nx = queue[i] % MAP_WIDTH + n.dx; + int ny = queue[i] / MAP_WIDTH + n.dy; + int index = cell_at(nx, ny); + if (index < 0 || dist[index] || !cell_is_passable(nx, ny)) + continue; + dist[index] = dist[queue[i]] + 1; + queue.push_back(static_cast(index)); + } + } + + if (dist[target] < 2) + return false; + for (int d = dist[target]; d > 1; d--) { + path.push_back(static_cast(cell_at(x, y))); + for (auto& n : neighbor_offsets) { + int prev = cell_at(x - n.dx, y - n.dy); + if (prev >= 0 && dist[prev] == d - 1) { + x -= n.dx; + y -= n.dy; + break; + } + } + } + return true; + } + + // -1 when the cell is off the map. + int cell_at(int x, int y) { + if (x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT) + return -1; + return y * MAP_WIDTH + x; + } + + uint8_t next_path_step() { + if (path.empty()) + return 0; + int dx = path.back() % MAP_WIDTH - map_sprite_x; + int dy = path.back() / MAP_WIDTH - map_sprite_y; + path.pop_back(); + for (auto& n : neighbor_offsets) { + if (n.dx == dx && n.dy == dy) + return n.dir; + } + path.clear(); + return 0; + } + + // Latches the facing, updates the panel highlight, and replaces the + // low nibble of the key with the single direction the move will use. + uint8_t resolve_direction(uint8_t key) { + uint8_t dir = key & 0x0f; + if (!dir) + return key; + if (dir == 1 || dir == 2 || dir == 4 || dir == 8) { + key_facing = dir; + } else { + // Several directions at once: prefer the newly pressed one if the + // cell it leads to can be entered, otherwise keep the old facing. + uint8_t other = dir ^ key_facing; + int x = map_sprite_x; + int y = map_sprite_y; + switch (other) { + case 1: y--; break; + case 2: y++; break; + case 4: x--; break; + case 8: x++; break; + default: other = 0; break; + } + dir = (other && cell_is_passable(x, y)) ? other : (dir ^ other); + key = (key & 0xf0) | dir; + } + if (dir != key_panel_direction) { + key_panel_direction = dir; + draw_key_panel(); + } + return key; + } + + // The panel base and the four highlighted variants sit side by side in + // the work screen. + void draw_key_panel() { + int x = key_panel_src_x; + ags->copy_screen(WORK_SCREEN, 0, x + 104, 304, x + 191, 391, KEY_PANEL_X, KEY_PANEL_Y); + switch (key_panel_direction) { + case 1: + ags->copy_screen(WORK_SCREEN, 0, x + 192, 304, x + 279, 343, KEY_PANEL_X, KEY_PANEL_Y); + break; + case 2: + ags->copy_screen(WORK_SCREEN, 0, x + 192, 352, x + 279, 391, KEY_PANEL_X, KEY_PANEL_Y + 48); + break; + case 4: + ags->copy_screen(WORK_SCREEN, 0, x + 280, 304, x + 319, 391, KEY_PANEL_X, KEY_PANEL_Y); + break; + case 8: + ags->copy_screen(WORK_SCREEN, 0, x + 328, 304, x + 367, 391, KEY_PANEL_X + 48, KEY_PANEL_Y); + break; + default: + break; + } + } + + bool in_key_panel(int x, int y) { + return KEY_PANEL_X <= x && x < KEY_PANEL_X + KEY_PANEL_SIZE && + KEY_PANEL_Y <= y && y < KEY_PANEL_Y + KEY_PANEL_SIZE; + } + + // The original reads relative mouse movement with the pointer captured at + // the screen centre, which this port cannot do. The pointer picks a + // direction from the panel instead: the four triangles cut by its diagonals. + uint8_t key_panel_hover_direction() { + int x, y; + get_cursor(&x, &y); + if (!in_key_panel(x, y)) + return 0; + int u = x - KEY_PANEL_X; + int v = y - KEY_PANEL_Y; + if (u >= v) + return (u + v <= KEY_PANEL_SIZE) ? 1 : 8; + return (u + v > KEY_PANEL_SIZE) ? 2 : 4; + } + + // --- numbers ---------------------------------------------------------- + + // Y 11 / Y 12 draw numbers with an 8x16 font stored in GAKUEN.COM. + static constexpr int DIGIT_WIDTH = 8; + static constexpr int DIGIT_HEIGHT = 16; + static constexpr uint8_t DIGIT_BACK_COLOR = 15; + static constexpr int DIGIT_FONT_OFFSET = 0x0c98; + + uint8_t digit_font[10][DIGIT_HEIGHT] = {}; + + void load_digit_font(const std::vector& exe) { + memcpy(digit_font, &exe[DIGIT_FONT_OFFSET], sizeof(digit_font)); + } + + void draw_digit(int digit, int x, int y, uint8_t color) { + ags->box_fill(0, x, y, x + DIGIT_WIDTH - 1, y + DIGIT_HEIGHT - 1, DIGIT_BACK_COLOR); + for (int row = 0; row < DIGIT_HEIGHT; row++) { + uint8_t bits = digit_font[digit][row]; + for (int col = 0; col < DIGIT_WIDTH; col++) { + if (bits & (0x80 >> col)) + ags->set_pixel(0, x + col, y + row, color); + } + } + } + + void draw_number(uint16_t value, int digits, int x, int y, uint8_t color = 0) { + if (digits == 3 && value > 999) + value = 999; + const uint8_t all[5] = { + static_cast(value / 10000), + static_cast(value / 1000 % 10), + static_cast(value / 100 % 10), + static_cast(value / 10 % 10), + static_cast(value % 10), + }; + const uint8_t* d = all + 5 - digits; + int i = 0; + while (i < digits && d[i] == 0) { + // Leading zeros are blanked. + ags->box_fill(0, x + i * DIGIT_WIDTH, y, + x + i * DIGIT_WIDTH + DIGIT_WIDTH - 1, y + DIGIT_HEIGHT - 1, + DIGIT_BACK_COLOR); + i++; + } + if (i == digits) { + // Zero still shows one digit, in the last cell. + draw_digit(0, x + (digits - 1) * DIGIT_WIDTH, y, color); + return; + } + for (; i < digits; i++) + draw_digit(d[i], x + i * DIGIT_WIDTH, y, color); + } + + // --- Y 11 status panel ------------------------------------------------ + + void draw_status_panel(int mode) { + if (mode == 0) { + draw_number(VAR_CHAR_LEVEL, 3, 432, 248); + draw_number(VAR_TROOP_COUNT, 3, 544, 248); + draw_number(VAR_TROOP_COUNT_FULL, 3, 584, 248); + draw_number(VAR_MONEY, 5, 464, 272); + draw_number(VAR_SPECIAL_MOVE_GAUGE, 2, 594, 272); + } else if (mode == 1) { + draw_number(VAR_CHAR_LEVEL, 3, 72, 120); + // 1000 marks this field as absent. + if (VAR_LOYALTY != 1000) + draw_number(VAR_LOYALTY, 3, 88, 168); + draw_number(VAR_TROOP_COUNT, 3, 256, 96); + draw_number(VAR_TROOP_COUNT_FULL, 3, 296, 96); + draw_number(VAR_TROOP_LIMIT, 3, 304, 128); + draw_number(VAR_ATTACK_POWER, 3, 304, 152); + draw_number(VAR_DEFENSE_POWER, 3, 304, 176); + draw_number(VAR_SPECIAL_MOVE_GAUGE, 2, 312, 200); + draw_number(VAR_JAPANESE_SCORE, 2, 88, 200); + draw_number(VAR_MATH_SCORE, 2, 88, 224); + draw_number(VAR_ENGLISH_SCORE, 2, 88, 248); + draw_number(VAR_PE_SCORE, 2, 88, 272); + } else { + // Redraw the direction key panel from the work screen. + ags->copy_screen(WORK_SCREEN, 0, 104, 304, 191, 391, KEY_PANEL_X, KEY_PANEL_Y); + } + } + + // --- Y 12 troop allocation -------------------------------------------- + + static constexpr int TROOP_BUTTON_WIDTH = 24; + static constexpr int TROOP_BUTTON_HEIGHT = 20; + // Button images on the work screen, by state. + static constexpr int TROOP_BUTTON_NORMAL_X = 480; + static constexpr int TROOP_BUTTON_HOVER_X = 504; + static constexpr int TROOP_BUTTON_PRESSED_X = 528; + + struct TroopButton { + int x, y; + int step; + + // The hit rectangle is one pixel inside the image at top and bottom. + bool contains(int px, int py) const { + return x <= px && px < x + TROOP_BUTTON_WIDTH && + y < py && py < y + TROOP_BUTTON_HEIGHT - 1; + } + }; + static constexpr TroopButton troop_buttons[6] = { + {24, 212, 100}, {56, 212, 10}, {88, 212, 1}, + {24, 264, -100}, {56, 264, -10}, {88, 264, -1}, + }; + + void run_troop_allocation() { + draw_status_panel(1); + + load_cg_to_work_screen(230); + // Save the part of the display CG 208 overwrites. + ags->copy_screen(0, WORK_SCREEN, 16, 192, 367, 294, 0, 48); + ags->load_cg(208, -1); + + VAR_TROOP_COUNT_FULL = VAR_TROOP_COUNT; + draw_troop_counts(); + + set_cursor(99, 223); + const TroopButton* hover = nullptr; - // I command - uint16_t grid_select(int left_x, int top_y, int xsize, int ysize, int nr_cols, int nr_rows, int initial_index) { - int x = initial_index % nr_cols; - int y = initial_index / nr_cols; - set_cursor(x * xsize + xsize / 2 + left_x, y * ysize + ysize / 2 + top_y); while (!terminate) { int mx, my; get_cursor(&mx, &my); - x = (mx - left_x) / xsize; - y = (my - top_y) / ysize; - int key = get_key(); - if (key & 0xf) { - if (key & 1) y--; // up - if (key & 2) y++; // down - if (key & 4) x--; // left - if (key & 8) x++; // right - x = std::clamp(x, 0, nr_cols - 1); - y = std::clamp(y, 0, nr_rows - 1); - set_cursor(x * xsize + xsize / 2 + left_x, y * ysize + ysize / 2 + top_y); - wait_key_release(); - } else if (key == 16) { // enter - wait_key_release(); - if (0 <= x && x < nr_cols && 0 <= y && y < nr_rows) { - int sel = nr_cols * y + x; - if (grid_select_data[sel]) - return sel + 1; - } - } else if (key == 32) { // space - wait_key_release(); - return GRID_SELECT_CANCELED; - } else { + uint8_t key = get_key(); + + const TroopButton* under_cursor = troop_button_at(mx, my); + if (under_cursor != hover) { + if (hover) + draw_troop_button(TROOP_BUTTON_NORMAL_X, *hover); + if (under_cursor) + draw_troop_button(TROOP_BUTTON_HOVER_X, *under_cursor); + hover = under_cursor; + } + + if (!key) { sys_sleep(16); + continue; + } + if (key & 0x20) { + wait_key_release(0x20); + break; + } + if (!(key & 0x10)) { + // Direction keys jump the pointer between the six buttons. + if (key & 1) my -= 48; + if (key & 2) my += 48; + if (key & 4) mx -= 32; + if (key & 8) mx += 32; + set_cursor(std::clamp(mx, 35, 99), std::clamp(my, 223, 271)); + wait_key_release(0x0f); + continue; + } + + if (!hover) + continue; + draw_troop_button(TROOP_BUTTON_PRESSED_X, *hover); + apply_troop_change(*hover); + draw_troop_counts(); + wait_ticks(10); + draw_troop_button(TROOP_BUTTON_HOVER_X, *hover); + wait_key_release(0x10); + } + + // Restore the saved area. + ags->copy_screen(WORK_SCREEN, 0, 0, 48, 351, 150, 16, 192); + } + + const TroopButton* troop_button_at(int x, int y) { + for (const TroopButton& b : troop_buttons) { + if (b.contains(x, y)) + return &b; + } + return nullptr; + } + + void draw_troop_button(int image_x, const TroopButton& b) { + // The subtract buttons use the lower row of images. + int image_y = b.step < 0 ? 24 : 4; + ags->copy_screen(WORK_SCREEN, 0, image_x, image_y, + image_x + TROOP_BUTTON_WIDTH - 1, + image_y + TROOP_BUTTON_HEIGHT - 1, b.x, b.y); + } + + void draw_troop_counts() { + draw_number(VAR_TROOP_COUNT, 3, 256, 96); + draw_number(VAR_TROOP_COUNT_FULL, 3, 296, 96); + draw_troop_pool(); + } + + // The unassigned count uses 48x48 digits from the top of the work screen, + // with no leading-zero suppression. + void draw_troop_pool() { + uint16_t value = VAR_UNASSIGNED_TROOPS; + uint16_t divisor = 10000; + for (int i = 0; i < 5; i++) { + int digit = value / divisor; + value %= divisor; + divisor /= 10; + ags->copy_screen(WORK_SCREEN, 0, digit * 48, 0, digit * 48 + 47, 47, + 120 + i * 48, 232); + } + } + + void apply_troop_change(const TroopButton& b) { + uint16_t step = b.step > 0 ? b.step : -b.step; + if (b.step > 0) { + if (VAR_UNASSIGNED_TROOPS < step) + step = VAR_UNASSIGNED_TROOPS; + if (static_cast(VAR_TROOP_COUNT_FULL + step) >= VAR_TROOP_LIMIT) + step = VAR_TROOP_LIMIT - VAR_TROOP_COUNT_FULL; + VAR_TROOP_COUNT_FULL += step; + VAR_UNASSIGNED_TROOPS -= step; + } else { + if (VAR_TROOP_COUNT_FULL < step) + step = VAR_TROOP_COUNT_FULL; + VAR_TROOP_COUNT_FULL -= step; + VAR_UNASSIGNED_TROOPS += step; + } + // A unit never drops below one student. + if (VAR_TROOP_COUNT_FULL == 0) { + VAR_TROOP_COUNT_FULL = 1; + VAR_UNASSIGNED_TROOPS--; + } + VAR_TROOP_COUNT = VAR_TROOP_COUNT_FULL; + } + + // --- I grid selection ------------------------------------------------- + + // I 2 composes its overlays over the background CG on the work screen and + // then copies (368,0)-(631,303) to the display. + static constexpr int GRID_X = 376; + static constexpr int GRID_COPY_X1 = 368; + static constexpr int GRID_COPY_X2 = 631; + static constexpr int GRID_COPY_Y2 = 303; + static constexpr int NR_GRID_ITEMS = 63; + static constexpr uint16_t GRID_SELECT_CANCELED = 80; + + uint8_t grid_item_state[NR_GRID_ITEMS] = {}; + // The two layouts remember their cursor separately. + int grid_cursor_x[2] = {392, 432}; + int grid_cursor_y[2] = {23, 22}; + + // I mode,arg,value + void cmd_i() override { + int p1 = cali(); + int p2 = cali(); + int p3 = cali(); + TRACE("I %d,%d,%d:", p1, p2, p3); + + switch (p1) { + case 1: + if (p3 == 255) { + for (int i = 0; i < NR_GRID_ITEMS; i++) + grid_item_state[i] = p2 & 0xff; + } else if (1 <= p3 && p3 <= NR_GRID_ITEMS) { + grid_item_state[p3 - 1] = p2 & 0xff; + } + break; + case 2: + RND = run_grid_selection(p2, p3); + break; + default: + break; + } + } + + uint16_t run_grid_selection(int layout, int cg_page) { + const bool wide = (layout == 2); + const int cols = wide ? 2 : 7; + const int rows = 9; + const int cell_w = wide ? 112 : 32; + const int nr_items = wide ? 18 : NR_GRID_ITEMS; + + load_cg_to_work_screen(cg_page); + for (int i = 0; i < nr_items; i++) { + uint8_t state = grid_item_state[i]; + if (state & 0xfd) + continue; // only states 0 and 2 draw anything + int sx = GRID_X + (i % cols) * cell_w; + int sy = (wide ? 8 : 7) + (i / cols) * 32; + int ex = sx + cell_w - 1; + int ey = sy + (wide ? 31 : 32); + if (state == 0) { + ags->box_fill(WORK_SCREEN, sx, sy, ex, ey, 0); + } else { + for (int n = 0; n < 3; n++) + ags->box_line(WORK_SCREEN, sx + n, sy + n, ex - n, ey - n, 15); } } + ags->copy_screen(WORK_SCREEN, 0, GRID_COPY_X1, 0, GRID_COPY_X2, GRID_COPY_Y2, + GRID_COPY_X1, 0); + + int& cx = grid_cursor_x[wide ? 1 : 0]; + int& cy = grid_cursor_y[wide ? 1 : 0]; + set_cursor(cx, cy); + + while (!terminate) { + uint8_t key = get_key(); + if (!key) { + sys_sleep(16); + continue; + } + if (!(key & 0xb0)) { + if (key & 1 && cy >= (wide ? 54 : 55)) cy -= 32; + if (key & 2 && cy < (wide ? 247 : 248)) cy += 32; + if (key & 4 && cx >= (wide ? 544 : 424)) cx -= cell_w; + if (key & 8 && cx < (wide ? 433 : 553)) cx += cell_w; + set_cursor(cx, cy); + wait_key_release(0x0f); + continue; + } + if (key & 0xa0) { + wait_key_release(); + return GRID_SELECT_CANCELED; + } + // The item comes from where the pointer actually is. The original + // confines it to the grid with the mouse driver; here the pointer + // can leave the grid and the selection is clamped instead. + int mx, my; + get_cursor(&mx, &my); + int col = std::clamp((mx - GRID_X) / cell_w, 0, cols - 1); + int row = std::clamp((my - 7 - (wide ? 1 : 0)) / 32, 0, rows - 1); + cx = col * cell_w + (wide ? 432 : 392); + cy = row * 32 + (wide ? 22 : 23); + set_cursor(cx, cy); + wait_key_release(); + // The state table is not consulted: state 0 returns its number + // too, and the scenario rejects it. + return static_cast(row * cols + col + 1); + } return GRID_SELECT_CANCELED; } + + // --- Y 80 battle screen ----------------------------------------------- + + // Y 80 composes the battle screen on the work screen: four 160x240 frames side + // by side at the top, the sheet of 16x16 parts at y=240, and the left and right + // templates at y=280. Y 80,3-6 send one frame to the display. + static constexpr int BATTLE_FRAME_W = 160; + static constexpr int BATTLE_FRAME_H = 240; + static constexpr int BATTLE_PART_SIZE = 16; + static constexpr int BATTLE_PART_SHEET_Y = 240; + static constexpr int BATTLE_PARTS_PER_ROW = 40; + static constexpr int BATTLE_TEMPLATE_Y = 280; + static constexpr int BATTLE_TEMPLATE_H = 120; + static constexpr int BATTLE_DEST_Y = 32; + static constexpr int BATTLE_LEFT_DEST_X = 152; + static constexpr int BATTLE_RIGHT_DEST_X = 328; + static constexpr int BATTLE_BACK_COLOR = 1; + static constexpr uint8_t CASUALTY_DIGIT_COLOR = 5; + + void update_battle_screen(int mode) { + // Make the next K redraw its mode specific panel. + key_last_mode = 0; + if (mode >= 3) + draw_battle_status(); + + int part_x = VAR_BATTLE_DRAW_X8 * 8; + int part_y = VAR_BATTLE_DRAW_Y; + switch (mode) { + case 0: + ags->cg_dest = SDL_Point{part_x, part_y}; + load_cg_to_work_screen(VAR_BATTLE_DRAW_CG); + break; + case 1: + ags->box_fill(WORK_SCREEN, 0, 0, 4 * BATTLE_FRAME_W - 1, BATTLE_FRAME_H - 1, + BATTLE_BACK_COLOR); + break; + case 2: + { + // The sheet holds parts 0-39 in one row and 40 and up in the + // next. A part number past the sheet is not range checked. + int part = VAR_BATTLE_DRAW_CG; + int row = (part >= BATTLE_PARTS_PER_ROW) ? 1 : 0; + if (row) + part -= BATTLE_PARTS_PER_ROW; + int sx = part * BATTLE_PART_SIZE; + int sy = BATTLE_PART_SHEET_Y + row * BATTLE_PART_SIZE; + ags->copy_screen(WORK_SCREEN, WORK_SCREEN, + sx, sy, sx + BATTLE_PART_SIZE - 1, sy + BATTLE_PART_SIZE - 1, + part_x, part_y, TRANSPARENT_COLOR); + } + break; + case 3: + case 4: + case 5: + case 6: + { + int frame = mode - 3; + int sx = frame * BATTLE_FRAME_W; + int dx = (frame < 2) ? BATTLE_LEFT_DEST_X : BATTLE_RIGHT_DEST_X; + ags->copy_screen(WORK_SCREEN, 0, sx, 0, sx + BATTLE_FRAME_W - 1, + BATTLE_FRAME_H - 1, dx, BATTLE_DEST_Y); + } + break; + case 7: + case 8: + build_battle_frames(mode == 7 ? 0 : 2 * BATTLE_FRAME_W); + break; + default: + break; + } + } + + // The template is stored as two 160x120 halves next to each other; joining + // them gives one 160x240 frame, which is written to both of the pair's + // slots so the two animation frames start out identical. + void build_battle_frames(int base_x) { + for (int half = 0; half < 2; half++) { + int sx = base_x + half * BATTLE_FRAME_W; + for (int frame = 0; frame < 2; frame++) { + ags->copy_screen(WORK_SCREEN, WORK_SCREEN, + sx, BATTLE_TEMPLATE_Y, + sx + BATTLE_FRAME_W - 1, BATTLE_TEMPLATE_Y + BATTLE_TEMPLATE_H - 1, + base_x + frame * BATTLE_FRAME_W, half * BATTLE_TEMPLATE_H); + } + } + } + + void draw_battle_status() { + draw_number(VAR_CHAR_LEVEL, 3, 80, 144); + draw_number(VAR_TROOP_COUNT, 3, 24, 184); + draw_number(VAR_TROOP_COUNT_FULL - VAR_TROOP_COUNT, 3, 80, 184, CASUALTY_DIGIT_COLOR); + draw_number(VAR_ATTACK_POWER, 3, 96, 208); + draw_number(VAR_DEFENSE_POWER, 3, 96, 232); + draw_number(VAR_SPECIAL_MOVE_GAUGE, 2, 64, 272); + draw_number(VAR_ENEMY_TROOP_COUNT, 3, 520, 184); + draw_number(VAR_ENEMY_TROOP_COUNT_FULL - VAR_ENEMY_TROOP_COUNT, 3, 576, 184, CASUALTY_DIGIT_COLOR); + draw_number(VAR_ENEMY_ATTACK_POWER, 3, 592, 208); + draw_number(VAR_ENEMY_DEFENSE_POWER, 3, 592, 232); + } + + // --- Y 84 pixel dissolve ---------------------------------------------- + + // Y 84 takes its order from a table of 6606 positions in CG archive page 197, + // packed as x + y * 402 behind a 16-byte header. + static constexpr int DISSOLVE_TABLE_PAGE = 197; + static constexpr int DISSOLVE_TABLE_OFFSET = 16; + static constexpr int NR_DISSOLVE_POINTS = 6606; + static constexpr int DISSOLVE_STRIDE = 402; + static constexpr int DISSOLVE_X = 124; + static constexpr int DISSOLVE_Y = 254; + + // The CG is loaded out of sight and then revealed one pixel at a time in + // the order the table gives. + void pixel_dissolve(int page) { + load_cg_to_work_screen(page); + + std::vector table = ags->load_cg_data(DISSOLVE_TABLE_PAGE); + if (static_cast(table.size()) < DISSOLVE_TABLE_OFFSET + NR_DISSOLVE_POINTS * 2) { + WARNING("Y 84: dissolve table page %d is missing or too short (%zu bytes)", + DISSOLVE_TABLE_PAGE, table.size()); + return; + } + + for (int i = 0; i < NR_DISSOLVE_POINTS && !terminate; i++) { + int offset = DISSOLVE_TABLE_OFFSET + i * 2; + int value = table[offset] | (table[offset + 1] << 8); + int x = DISSOLVE_X + value % DISSOLVE_STRIDE; + int y = DISSOLVE_Y + value / DISSOLVE_STRIDE; + ags->copy_screen(WORK_SCREEN, 0, x, y, x, y, x, y); + if ((i & 0x2f) == 0x2f) + wait_ticks(1); + } + } + + // --- Y 81 staff roll -------------------------------------------------- + + // Y 81 builds the credits in a 312-pixel wide column on the work screen and + // scrolls a 74-pixel window of it through the band at y=314 on the display. + static constexpr int CREDIT_X1 = 160; + static constexpr int CREDIT_X2 = 471; + static constexpr int CREDIT_BAND_Y = 314; + static constexpr int CREDIT_BAND_H = 74; + static constexpr int CREDIT_DATA_OFFSET = 0x4896; + static constexpr int CREDIT_DATA_END = 0x4b95; + + // In the credit stream, a control byte of 2 or more starts a line at + // x = control * 8, 1 inserts a 2-pixel gap, and 0 leaves a blank line. + struct CreditEntry { + uint8_t control; + std::string text; + }; + // Each group fills the column, then scrolls it past the band. + struct CreditGroup { + int nr_entries; + int scroll_steps; + }; + + static constexpr CreditGroup credit_groups[] = { + {15, 292}, {13, 256}, {15, 309}, {13, 256}, + {15, 292}, {13, 256}, {14, 274}, {1, 45}, + }; + + std::string credit_title; + std::string credit_subtitle; + std::vector credit_entries; + + void load_credit_text(const std::vector& exe) { + size_t pos = CREDIT_DATA_OFFSET; + bool truncated = false; + auto read_string = [&]() -> std::string { + size_t start = pos; + while (pos < exe.size() && exe[pos]) + pos++; + if (pos == exe.size()) { + truncated = true; + return {}; + } + return std::string(reinterpret_cast(&exe[start]), pos++ - start); + }; + std::string title = read_string(); + std::string subtitle = read_string(); + std::vector entries; + for (const CreditGroup& group : credit_groups) { + for (int i = 0; i < group.nr_entries && !truncated; i++) { + if (pos == exe.size()) { + truncated = true; + break; + } + uint8_t control = exe[pos++]; + entries.push_back({control, control >= 2 ? read_string() : std::string()}); + } + } + // The data is intact only if the last entry ends exactly at + // CREDIT_DATA_END. + if (truncated || pos != exe.size()) { + WARNING("%s: unexpected staff roll data", EXECUTABLE_NAME); + return; + } + credit_title = std::move(title); + credit_subtitle = std::move(subtitle); + credit_entries = std::move(entries); + } + + void run_staff_roll() { + constexpr int TOP_Y = 72; + constexpr int LINE_HEIGHT = 18; + constexpr int TITLE_STEPS = 127; + + if (credit_entries.empty()) + return; + int saved_size = ags->text.font_size; + uint8_t saved_color = ags->text.font_color; + ags->text.font_size = 16; + ags->text.font_color = 15; + + clear_credit_column(); + draw_credit_text(168, TOP_Y, credit_title, false); + draw_credit_text(256, TOP_Y + 36, credit_subtitle, true); + scroll_credits(TITLE_STEPS); + + size_t index = 0; + for (const CreditGroup& group : credit_groups) { + clear_credit_column(); + int y = TOP_Y; + for (int i = 0; i < group.nr_entries; i++) { + const CreditEntry& entry = credit_entries[index++]; + if (entry.control == 1) { + y += 2; + continue; + } + if (entry.control >= 2) + draw_credit_text(entry.control * 8, y, entry.text, false); + y += LINE_HEIGHT; + } + scroll_credits(group.scroll_steps); + if (terminate) + break; + } + + ags->text.font_size = saved_size; + ags->text.font_color = saved_color; + } + + void clear_credit_column() { + ags->box_fill(WORK_SCREEN, CREDIT_X1, 0, CREDIT_X2, 399, 0); + } + + void draw_credit_text(int x, int y, std::string_view text, bool hankaku) { + int saved_screen = ags->dest_screen; + bool saved_hankaku = ags->draw_hankaku; + ags->dest_screen = WORK_SCREEN; + ags->draw_hankaku = hankaku; + ags->text.pos.x = x; + ags->text.pos.y = y; + ags->draw_text(text); + ags->draw_hankaku = saved_hankaku; + ags->dest_screen = saved_screen; + } + + void scroll_credits(int steps) { + constexpr int band_rows[] = {315, 317, 384, 386}; + + for (int step = 0; step < steps && !terminate; step++) { + ags->copy_screen(WORK_SCREEN, 0, CREDIT_X1, step, CREDIT_X2, + step + CREDIT_BAND_H - 1, CREDIT_X1, CREDIT_BAND_Y); + for (int row : band_rows) + ags->box_fill(0, CREDIT_X1, row, CREDIT_X2, row, 0); + wait_ticks(get_key() ? 1 : 4); + } + } + + // --- save and load ---------------------------------------------------- + + // A save file is a 5005-byte resident block followed by the exploration + // bitmaps of all 60 areas, 1400 bytes each. Everything not listed below is + // written as zero. + static constexpr int SAVE_BLOCK_BYTES = 0x138d; + static constexpr int SAVE_TOTAL_BYTES = + SAVE_BLOCK_BYTES + NR_EXPLORED_RECORDS * EXPLORED_RECORD_BYTES; + static constexpr int SAVE_HEADER_BYTES = 112; + static constexpr int SAVE_OFF_PAGE = 0x070; // 1-based scenario page + static constexpr int SAVE_OFF_MUSIC = 0x078; // page read by play_music + static constexpr int SAVE_OFF_ADDR = 0x07c; // offset within the page + static constexpr int SAVE_OFF_MAP_AREA = 0x080; + static constexpr int SAVE_OFF_SPRITE_CG = 0x084; // Y 50 + static constexpr int SAVE_OFF_TILE_CG = 0x088; // Y 51-53, 4 apart + static constexpr int SAVE_OFF_VAR = 0x094; // RND is index 0 + static constexpr int NR_SAVED_VARS = 768; // the text state follows + static constexpr int SAVE_OFF_TEXT_STATE = 0x694; + static constexpr int SAVE_OFF_LABEL_DEPTH = 0x6a6; + static constexpr int SAVE_OFF_LABEL_NEXT = 0x6a8; // address of the next push + static constexpr int SAVE_OFF_LABEL_FRAMES = 0x6ac; + static constexpr int LABEL_FRAME_BYTES = 4; + static constexpr int NR_LABEL_FRAMES = 32; + static constexpr int LABEL_STACK_ADDR = 0x6096; + static constexpr int SAVE_OFF_PAGE_DEPTH = 0x72c; + static constexpr int SAVE_OFF_PAGE_NEXT = 0x72e; + static constexpr int SAVE_OFF_PAGE_FRAMES = 0x732; + static constexpr int PAGE_FRAME_BYTES = 16; + static constexpr int NR_PAGE_FRAMES = 64; + static constexpr int PAGE_STACK_ADDR = 0x611c; + // Frame layout of the page stack: page, return offset, scenario_body_offset, + // scenario_header_word, one word each 4 bytes apart. + static constexpr int PAGE_FRAME_OFF_ADDR = 4; + static constexpr int PAGE_FRAME_OFF_BODY = 8; + static constexpr int SCENARIO_BODY_OFFSET = 2; + static constexpr int SAVE_OFF_VIEW_X = 0xb32; + static constexpr int SAVE_OFF_VIEW_Y = 0xb34; + static constexpr int SAVE_OFF_ANIMATION_TICKS = 0xb36; + static constexpr int SAVE_OFF_EXPLORED = 0xb38; // the current area's record + static constexpr int SAVE_OFF_OOB_TILE = 0x12b0; + static constexpr int SAVE_OFF_STRVAR = 0x12b1; // 10 slots, X and M + static constexpr int STRVAR_BYTES = 22; + + static constexpr char save_header[SAVE_HEADER_BYTES + 1] = + "This is save data for GAKUEN. [PC-9801:FM-TOWNS:DOS/V:WINDOWS]" + " on NACT/ADV system (C)1995 ALICE-SOFT \r\n"; + + static void put_le16(std::vector& buf, int offset, uint16_t value) { + buf[offset] = value & 0xff; + buf[offset + 1] = value >> 8; + } + + static uint16_t get_le16(const std::vector& buf, int offset) { + return buf[offset] | (buf[offset + 1] << 8); + } + + void cmd_l() override { + int index = cali(); + TRACE("L %d:", index); + + if (index < 1 || index > 26 || !load_game(index)) + RND = 255; + } + + void cmd_q() override { + int index = cali(); + TRACE("Q %d:", index); + + RND = (index >= 1 && index <= 26 && save_game(index)) ? 1 : 255; + } + + bool save_game(int slot) { + std::vector block(SAVE_BLOCK_BYTES, 0); + memcpy(&block[0], save_header, SAVE_HEADER_BYTES); + put_le16(block, SAVE_OFF_PAGE, sco.page() + 1); + put_le16(block, SAVE_OFF_MUSIC, mako->current_music); + put_le16(block, SAVE_OFF_ADDR, sco.current_addr()); + put_le16(block, SAVE_OFF_MAP_AREA, map_current_area); + put_le16(block, SAVE_OFF_SPRITE_CG, map_sprite_cg_page); + for (int i = 0; i < NR_TILE_BANKS; i++) + put_le16(block, SAVE_OFF_TILE_CG + i * 4, map_tile_cg_page[i]); + for (int i = 0; i < NR_SAVED_VARS; i++) + put_le16(block, SAVE_OFF_VAR + i * 2, var[i]); + save_call_stack(block); + put_le16(block, SAVE_OFF_VIEW_X, map_view_x); + put_le16(block, SAVE_OFF_VIEW_Y, map_view_y); + put_le16(block, SAVE_OFF_ANIMATION_TICKS, map_animation_ticks); + // The current area's record goes into the block as well as into its + // slot at the end of the file. + memcpy(&block[SAVE_OFF_EXPLORED], explored_record().data(), EXPLORED_RECORD_BYTES); + block[SAVE_OFF_OOB_TILE] = map_out_of_bounds_tile; + save_text_state(block); + for (int i = 0; i < MAX_STRVAR; i++) { + size_t len = std::min(tvar[i].size(), (size_t)STRVAR_BYTES - 1); + memcpy(&block[SAVE_OFF_STRVAR + i * STRVAR_BYTES], tvar[i].data(), len); + } + + auto fio = FILEIO::open_save(slot, FILEIO_WRITE_BINARY); + if (!fio) { + WARNING("cannot write save slot %d", slot); + return false; + } + if (!fio->write(block.data(), block.size())) + return false; + for (int i = 0; i < NR_EXPLORED_RECORDS; i++) { + if (!fio->write(map_explored[i].data(), EXPLORED_RECORD_BYTES)) + return false; + } + return true; + } + + bool load_game(int slot) { + auto fio = FILEIO::open_save(slot, FILEIO_READ_BINARY); + if (!fio) + return false; + std::vector data(SAVE_TOTAL_BYTES); + if (!fio->read(data.data(), data.size())) { + WARNING("save slot %d is shorter than %d bytes", slot, SAVE_TOTAL_BYTES); + return false; + } + fio.reset(); + + for (int i = 0; i < NR_SAVED_VARS; i++) + var[i] = get_le16(data, SAVE_OFF_VAR + i * 2); + map_current_area = get_le16(data, SAVE_OFF_MAP_AREA); + map_sprite_cg_page = get_le16(data, SAVE_OFF_SPRITE_CG); + for (int i = 0; i < NR_TILE_BANKS; i++) + map_tile_cg_page[i] = get_le16(data, SAVE_OFF_TILE_CG + i * 4); + map_view_x = get_le16(data, SAVE_OFF_VIEW_X); + map_view_y = get_le16(data, SAVE_OFF_VIEW_Y); + map_animation_ticks = get_le16(data, SAVE_OFF_ANIMATION_TICKS); + for (int i = 0; i < NR_EXPLORED_RECORDS; i++) { + memcpy(map_explored[i].data(), + &data[SAVE_BLOCK_BYTES + i * EXPLORED_RECORD_BYTES], + EXPLORED_RECORD_BYTES); + } + // In a save written by GAKUEN.COM the slot at the end of the file is + // only as new as entering the area, so the block copy wins. + memcpy(explored_record().data(), &data[SAVE_OFF_EXPLORED], EXPLORED_RECORD_BYTES); + load_text_state(data); + for (int i = 0; i < MAX_STRVAR; i++) { + const char* p = (const char*)&data[SAVE_OFF_STRVAR + i * STRVAR_BYTES]; + tvar[i].assign(p, strnlen(p, STRVAR_BYTES - 1)); + } + + int page = get_le16(data, SAVE_OFF_PAGE); + sco.page_jump(page - 1, get_le16(data, SAVE_OFF_ADDR)); + load_call_stack(data); + + map_sprite_cg = load_map_sheet(map_sprite_cg_page); + for (int i = 0; i < NR_TILE_BANKS; i++) + map_tile_cg[i] = load_map_sheet(map_tile_cg_page[i]); + if (map_current_area >= 1 && map_current_area != amap_loaded_page) + load_amap_page(map_current_area); + clear_map_layers(); + map_out_of_bounds_tile = CELL_EMPTY; + reset_map_overview(); + + mako->play_music(get_le16(data, SAVE_OFF_MUSIC)); + + RND = 0; + map_overview_rebuild_pending = true; + return true; + } + + void save_text_state(std::vector& block) { + int off = SAVE_OFF_TEXT_STATE; + put_le16(block, off, ags->menu.font_size); + put_le16(block, off + 2, ags->text.font_size); + put_le16(block, off + 4, ags->palette_bank == -1 ? 0 : ags->palette_bank); + put_le16(block, off + 6, ags->text.font_color); + put_le16(block, off + 8, ags->menu.font_color); + put_le16(block, off + 10, ags->menu.frame_color); + put_le16(block, off + 12, ags->menu.back_color); + put_le16(block, off + 14, ags->text.frame_color); + put_le16(block, off + 16, ags->text.back_color); + } + + void load_text_state(const std::vector& data) { + int off = SAVE_OFF_TEXT_STATE; + ags->menu.font_size = get_le16(data, off); + ags->text.font_size = get_le16(data, off + 2); + int bank = get_le16(data, off + 4); + ags->palette_bank = bank ? bank : -1; + ags->text.font_color = get_le16(data, off + 6); + ags->menu.font_color = get_le16(data, off + 8); + ags->menu.frame_color = get_le16(data, off + 10); + ags->menu.back_color = get_le16(data, off + 12); + ags->text.frame_color = get_le16(data, off + 14); + ags->text.back_color = get_le16(data, off + 16); + } + + void save_call_stack(std::vector& block) { + int labels = 0, pages = 0; + for (const auto& frame : sco.get_call_stack()) { + if (frame.is_page_call) { + if (pages >= NR_PAGE_FRAMES) + ERROR("page call stack too deep to save"); + int off = SAVE_OFF_PAGE_FRAMES + pages++ * PAGE_FRAME_BYTES; + put_le16(block, off, frame.page + 1); + put_le16(block, off + PAGE_FRAME_OFF_ADDR, frame.addr); + put_le16(block, off + PAGE_FRAME_OFF_BODY, SCENARIO_BODY_OFFSET); + } else { + if (labels >= NR_LABEL_FRAMES) + ERROR("label call stack too deep to save"); + put_le16(block, SAVE_OFF_LABEL_FRAMES + labels++ * LABEL_FRAME_BYTES, + frame.addr); + } + } + put_le16(block, SAVE_OFF_LABEL_DEPTH, labels); + put_le16(block, SAVE_OFF_LABEL_NEXT, LABEL_STACK_ADDR + labels * LABEL_FRAME_BYTES); + put_le16(block, SAVE_OFF_PAGE_DEPTH, pages); + put_le16(block, SAVE_OFF_PAGE_NEXT, PAGE_STACK_ADDR + pages * PAGE_FRAME_BYTES); + } + + // Call this after page_jump(), because label frames carry no page number. + void load_call_stack(const std::vector& data) { + std::vector frames; + int pages = std::min(get_le16(data, SAVE_OFF_PAGE_DEPTH), NR_PAGE_FRAMES); + for (int i = 0; i < pages; i++) { + int off = SAVE_OFF_PAGE_FRAMES + i * PAGE_FRAME_BYTES; + frames.emplace_back(true, get_le16(data, off) - 1, + get_le16(data, off + PAGE_FRAME_OFF_ADDR)); + } + int labels = std::min(get_le16(data, SAVE_OFF_LABEL_DEPTH), NR_LABEL_FRAMES); + for (int i = 0; i < labels; i++) { + frames.emplace_back(false, sco.page(), + get_le16(data, SAVE_OFF_LABEL_FRAMES + i * LABEL_FRAME_BYTES)); + } + sco.set_call_stack(std::move(frames)); + } + + // Y 82,var. RND selects the save file; a variable that cannot be read from + // it becomes 0 only when the file itself is missing. + void load_saved_variable(int index) { + if (index < 0 || index >= NR_SAVED_VARS) { + WARNING("Y 82: variable %d is outside the saved area", index); + return; + } + auto fio = (RND >= 1 && RND <= 26) + ? FILEIO::open_save(RND, FILEIO_READ_BINARY) : nullptr; + if (!fio) { + var[index] = 0; + return; + } + uint8_t buf[2]; + if (fio->seek(SAVE_OFF_VAR + index * 2, SEEK_SET) == 0 && fio->read(buf, 2)) + var[index] = buf[0] | (buf[1] << 8); + } }; } // namespace diff --git a/src/sys/nact_input.cpp b/src/sys/nact_input.cpp index b745b78..6676100 100644 --- a/src/sys/nact_input.cpp +++ b/src/sys/nact_input.cpp @@ -106,11 +106,12 @@ void NACT::process_next_event() handle_event(std::move(e)); } -uint8 NACT::get_key() +uint8 NACT::get_key(bool notify_texthook) { uint8 val = 0; - texthook_keywait(); + if (notify_texthook) + texthook_keywait(); pump_events(); diff --git a/src/sys/scenario.h b/src/sys/scenario.h index 45fe4a9..eb18648 100644 --- a/src/sys/scenario.h +++ b/src/sys/scenario.h @@ -58,6 +58,7 @@ public: : is_page_call(is_page_call), page(page), addr(addr) {} }; const std::vector& get_call_stack() const { return call_stack; } + void set_call_stack(std::vector frames) { call_stack = std::move(frames); } int write_instruction(int page, int addr, uint8_t op); private: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c9933d8..f1e16cd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,18 +1,20 @@ -function(add_scenario_test system) - set(TEST_DIR ${CMAKE_CURRENT_BINARY_DIR}/scenario/${system}) +function(add_scenario_test name game) + set(TEST_DIR ${CMAKE_CURRENT_BINARY_DIR}/scenario/${name}) file(MAKE_DIRECTORY ${TEST_DIR}) - configure_file(scenario/${system}/ADISK.DAT ${TEST_DIR}/ADISK.DAT COPYONLY) + foreach(data ADISK.DAT ${ARGN}) + configure_file(scenario/${name}/${data} ${TEST_DIR}/${data} COPYONLY) + endforeach() add_test( - NAME scenario_${system} + NAME scenario_${name} COMMAND $ -gamedir ${TEST_DIR} - -game ${system}_generic + -game ${game} -encoding utf8 -fontfile ${PROJECT_SOURCE_DIR}/resources/fonts/MTLc3m.ttf -texthook print ) - set_tests_properties(scenario_${system} PROPERTIES + set_tests_properties(scenario_${name} PROPERTIES ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy" FAIL_REGULAR_EXPRESSION "\\[FAIL\\]" PASS_REGULAR_EXPRESSION "(^|[^0-9])0 tests failed" @@ -20,6 +22,7 @@ function(add_scenario_test system) ) endfunction() -add_scenario_test(system1) -add_scenario_test(system2) -add_scenario_test(system3) +add_scenario_test(system1 system1_generic) +add_scenario_test(system2 system2_generic) +add_scenario_test(system3 system3_generic) +add_scenario_test(gakuen_king gakuen_king AMAP.DAT) diff --git a/test/README.md b/test/README.md index f29367a..c33ddb8 100644 --- a/test/README.md +++ b/test/README.md @@ -10,7 +10,7 @@ require [sys3c](https://github.com/kichikuou/sys3c). After installing sys3c and changing the scenario sources, regenerate it with: ```sh -for system in system1 system2 system3; do - (cd test/scenario/$system && sys3c) +for dir in system1 system2 system3 gakuen_king; do + (cd test/scenario/$dir && sys3c) done ``` diff --git a/test/scenario/gakuen_king/ADISK.DAT b/test/scenario/gakuen_king/ADISK.DAT new file mode 100644 index 0000000000000000000000000000000000000000..f7db674473266c9af0917875d579c7353e5099ca GIT binary patch literal 3840 zcmd^C&u<$=6duQ+y&zNxBm}3iP-O`aYSX%Dsf4^vT9s;JYAdN_H!ZB~@ei``nw?Fe zXr*~>U5NuHgv5mtHzXuDfVeb=$QLS(aNt&f1c)2(4`ANR?#3JM*gexm(PVe$dvCt? z&3kVq3nWbz$+P4Xc{+bmsnkL${qoZpJVt$vWJcpWRLS39z-jy~z%?=&Lza4sdz`kc zuFaa+a%O-Zt7Hs$c%sDGI9Vmj78k(2zWy(G8!BEkiB))c-y>9>lSBp^~~99 z*}sQ?cV+>W5HNgSKz|nC90KlN7x3{c!1-*scpcV}NM68$1ZvN+x2(E-L%mXPx zZ!s~!0qM?R6rxo18h)ZDefq-$sq=Gspxs(KC7pG+jpm8oTW`D>9+@1jr zW8CXFD7do3rg_(Z%)LBhhU6h+?w6q8f(u1Cs8;i7!dD?GAl%MxwX;D@KNRqjc3M@? zH39!;E)-1ocL)X%l$*Goy)u(w|8s}tog6zVU?JnM3{zu%)_b!3%OIvqq)~}lk%vN zi&(RAG;qA6o-e8AH{lk3xvZWqtM7drCvB^Ia$2lrm-jKxY0I|SoNlqcN0nTs|5rDJ z7t@>1k7k`Az=-||^P{K++WCMtjwBf^-#6M(sZf+)y;1Vqt zSF!vLn^^m!gFGC|K+`z7S(vnmsn&Bm>N38AiRX0~<<>51(!RsHUf1cNoA5e?^N}QP{?R4868biuvcU6N6b6qk$s>Dg2_!!_yeM6ZdF+pPCb`GNTr8 zQ2RD{gkvYQ2Bj5zXyGo7+6DOuUk59=;bj=L!IwY*uO<%A>cu70#NJWTwQ#f7wU9m6 z@ff{nlrNl>Ef2j55RXG%K)QqVfIw~2ih($ literal 0 HcmV?d00001 diff --git a/test/scenario/gakuen_king/AMAP.DAT b/test/scenario/gakuen_king/AMAP.DAT new file mode 100644 index 0000000000000000000000000000000000000000..5cd1a80468d440322a36194d8f6116defcd1eb3b GIT binary patch literal 40960 zcmd^H4bxR)6kWZkC@GXuJ~j~%5fPCc5fQ0GL|a5eN{JLosfZLxsVFHbr6N-3uURv9 z-kCdd=R9Y<(>?Dwm-7qOS+nNZd+o0_bj1|x#{c!}(xpq=p#SU7^S^ufxzXFti$2~r z&i4!ALhl#-{h}D)1LI;J6odVexYRF;%iZD%zcPmSRdKZsjcfec80Oc-^*%gq@Ec=< z-xN3d$hgICjZuDE-0q{}4!<+T_+4?gkBxiWW1Qa`_xbp^-yetx{$M=h6XRijBqsTz z@t9AJ$9+mn^=a{hPmd@4shHtU$1^@Np7rNqmPb7Av*QJSG3NM7@v_g2SNzqO=dZ=< zK0k;Y{fx+M0EirejL7X$5IM|<+=hV2G1Q3MhJna2+=$#pfXFe@h}=ej$T8Z8+{S>& zG1iFO#(~H&-iX{LfXFe?h}(=fygo2h}`CY$T8Q5 z+~$GEG2h-|&v3>)HeY_v^Y#@nK+g`E4EAm{P1e-cCp{+agOT&tgfEQ9Z8L}*TWy2CWzd2I+0^n?8f)D$A7TB*cZ)Kr14iU_2#4br9I>N*%znjh(Y#1h zgj$H)$hjQ^Imco9#fcopg2?T-|89TaMEr@9cFObemU7iX&izP`^Z3<&3nGs{oXGuT zobtcoZ~rGwJEDuXMPA-gu3CzKY&*yE@|JSd zLgXOlfRS_SinD^qt%nmidPXmtYs}TH5Bf%4-l9h${cX|JLe8U`lXE{i&T%65ESHg& zx9E{be_M35^fYoFy+O{subq!)uLB0^757<-WFXgdIcjdZ_y)B z5o*!vU3qzn9*K%ji(c=_%UkqFRD@dedRJcFQm$I`>S$ixQm$I`>S$ixQm$I`>S!y} z(iKnI3bk~_leR)FUGb#lbzT&;)N1L9CoQk@qNt@-OIJK;d7T$UEwx&@;z^s`r)#cS zYPEF5lQz3g*Ic#KYUzq6ZFZloxoWA^(iKmtb#=>9ORbi!cv7vaTb5dCwRFXkYF*v3 z)KaUZE1p#A>MD^)e_M35)N1L9C)K*TN+i}XwGB@z{(7JXtz>*^|zs0g*_6FXX0SBXUW+oG#QpYPJTx=JL{-xggh z`h1tx)m0*q{l}J>CTJ&i^t*fg1Xd(vXI7@DP)`bp!oC+*12OXsVl ze$qJYNjtLh()ntspEOQ;(vIxBbiP{ZCympdbbeG>E%lSeX-~>+9eL*^9f|a}MORDx zq;cAla?eQKc}Yhi{cX|JQa@>&_N1#koww+bNPk;&wdnhd+}4qIUeb}M2({?@jNCJl zcV5zws0g*_`;1#7owt;$7Jd7b+dA^jOF9ziZ;P%LefyPrM)J-}Iuhw`i>?-Z`}L5g z^Ohps7F{j+t~9rGTcqmmyk;Qpyi~b5+G=+gvRp>1-C@Xb8Lf7QAc z5vViO(s+j<%Vl)@B2Z_lrST3!mdoh)MWD`9OXD4eESJ%KKdPE)X}rUbW{@_e>|r6)R^W^#B_f$p7I&-v_BIw z{n>cVXGQq)G235=7ky5=9CR z8rc-@HH7J$gH$cWsQ zfXK1b-t}d+9Ph>Z_`p^;Ik%x8=NM+>+=hdkV}y}&8wqlbQAW;fG{`x|7&*7GAmt_*W1W8-pZNOt)IW<2UTw9kG;$uRLC$@xtpkx`y%D)>z~`|Mo9qk!(!RpxsJ2>& z+{n3o3UZFmZKD%8z6>I_E&jD_#kTkc+ii!J@wVt{A?Lm+$a#F_n}f(>s}s3zj~%`< zzV+{7m+y}6eNU9}w&-fv0&?!#jGV^~>@*^`T_AGou^)V|?Zc1p6ZYEyFXL^|)k5ST z=h$uJ+U@BT-e@a9FLBGhuw$ax$FIrpP>3`CCOM&xz^f5u6ivcLRq`v<3^d6B3HwGg?H zbNe0S9DmwLCvyB9L~igd)`qTe7P?t?&&ylNRSP-yQ$fz-A3q&L9&JwK-YvR&k2u@U ziJsmo&h_4rm$#Iw7J!_4S0m@q9X*W5ttW^az3n{jV|{UcT!0I$pXcQ*<*J3qLC(?3 z$hn<|K0)MmffG6U$3+-m%++lW21j1rqDLbAZPC?2&ZDoBbH6b9Igw|&jJ&)>k3{<0 zqN}C9k@FY`a_)oe5@frKyu3w^MEcvJtA)so&dv;Ub{JyWE+a2*DOWAJC(X-S%2kW* zN%QiSa@C@H(!9K-h_^*oi(bLV%Ug%1sxsnyaIPg-8*MNvzwmacfxX7}lutCm_VUGb#N z?$b3_Ewx&@;z^s`r)#cSYPEF5lWJYvveZ(mr7NCP>*|)JmRc=c@uXT;w=A{PYUzq6 z)w;S$B+}m&T`jd*y5dQ-uC5Y^^tVMmE&9Zc*40%ak^Z*mYSHJrw63laiS)NcSBpO1rFC_c zNTk0lx?1%4F0HGpM4}?pqE7>AU0o#-6`>Y=8c^%%Dv_uNwdm7;T35IF)lsdhTm9;& z*43?kb+mORjnkgAbta9|p0sr)jnkgA?78%`TIwf_)1I{Kx%9MJ>L-oUp0w<_^t4*) zCympdw3sc1W~rrq(m3r&i`immmRjm3jnkgAm@S58sil6>IPFP0vh&jUYN?+zPJ7ah z?7VcoTIwf_)1I^=J1?EDmikHKv?pB}RaQ&=q;cAla$85&_N3f1 zl6PLxkw|}AbhXq^8mB$!22bZLdL+`{7F{j+J|nkva4R%{?P|=OrD9icpKbE8RQNd5a#2icpKbSGOBU4BTFrfcNnrWT~a`4nvm9sK(Wf zEVVS=VaReBMc#NhbG0-MVaReB?f0Xqsg}k&3|TIt j{eDz6)zWx}A low byte, N's D03 + bit13 = 1 when (x + y) is odd -> N's D02 (blocked) + bit14 = 1 when (x * y) % 3 == 0 -> N's D01 + +Bits 8-12 of the cell are left at 0; N derives D04 from the low byte anyway. + +Usage: + ./make_amap.py && dri create AMAP.DAT page1.bin page2.bin +""" + +import struct + +WIDTH = HEIGHT = 100 +PAGES = 2 + + +def cell(page, x, y): + tile = (x + y * 7 + page * 11) % 192 + value = tile + if (x + y) % 2: + value |= 1 << 13 + if (x * y) % 3 == 0: + value |= 1 << 14 + return value + + +def main(): + for page in range(1, PAGES + 1): + data = b"".join( + struct.pack(" N's D03 + ; bit13 = 1 when (x + y) is odd -> N's D02 + ; bit14 = 1 when (x * y) % 3 == 0 -> N's D01 + ; and leaves bits 8-12 zero, so D04 = tile & 31. + + Y 55, 1: + + ; --- base cells -------------------------------------------------------- + N 3, 4: + {D01 \ 1: !tests_failed : tests_failed + 1! '[FAIL] base D01' R} + {D02 \ 1: !tests_failed : tests_failed + 1! '[FAIL] base D02' R} + {D03 \ 42: !tests_failed : tests_failed + 1! '[FAIL] base D03' R} + {D04 \ 10: !tests_failed : tests_failed + 1! '[FAIL] base D04' R} + + N 10, 20: + {D01 \ 0: !tests_failed : tests_failed + 1! '[FAIL] walkable D01' R} + {D02 \ 0: !tests_failed : tests_failed + 1! '[FAIL] walkable D02' R} + {D03 \ 161: !tests_failed : tests_failed + 1! '[FAIL] walkable D03' R} + {D04 \ 1: !tests_failed : tests_failed + 1! '[FAIL] walkable D04' R} + + ; --- layer priority ---------------------------------------------------- + ; Z hides the base cell and reports itself as a wall. + Z 3, 4, 12: + N 3, 4: + {D01 \ 1: !tests_failed : tests_failed + 1! '[FAIL] Z D01' R} + {D02 \ 1: !tests_failed : tests_failed + 1! '[FAIL] Z D02' R} + {D03 \ 12: !tests_failed : tests_failed + 1! '[FAIL] Z D03' R} + {D04 \ 32: !tests_failed : tests_failed + 1! '[FAIL] Z D04' R} + + W 3, 4, 77: + N 3, 4: + {D03 \ 77: !tests_failed : tests_failed + 1! '[FAIL] W over Z' R} + {D04 \ 64: !tests_failed : tests_failed + 1! '[FAIL] W layer tag' R} + + ; 255 empties one cell of the layer only; N falls back to Z. + W 3, 4, 255: + N 3, 4: + {D03 \ 12: !tests_failed : tests_failed + 1! '[FAIL] W 255 fallback' R} + {D04 \ 32: !tests_failed : tests_failed + 1! '[FAIL] W 255 layer tag' R} + + Z 3, 4, 255: + N 3, 4: + {D03 \ 42: !tests_failed : tests_failed + 1! '[FAIL] Z 255 fallback' R} + {D04 \ 10: !tests_failed : tests_failed + 1! '[FAIL] Z 255 layer tag' R} + + ; Tile 0 is a real tile, not an empty cell. + Z 3, 4, 0: + N 3, 4: + {D03 \ 0: !tests_failed : tests_failed + 1! '[FAIL] Z tile 0' R} + {D04 \ 32: !tests_failed : tests_failed + 1! '[FAIL] Z tile 0 layer tag' R} + Z 3, 4, 255: + + ; --- 254 clears the whole layer --------------------------------------- + Z 20, 30, 100: + Z 21, 30, 101: + W 20, 30, 102: + W 22, 30, 103: + Z 0, 0, 254: + N 21, 30: + {D03 \ 50: !tests_failed : tests_failed + 1! '[FAIL] Z 254 clears layer' R} + {D04 \ 18: !tests_failed : tests_failed + 1! '[FAIL] Z 254 layer tag' R} + N 20, 30: + {D03 \ 102: !tests_failed : tests_failed + 1! '[FAIL] Z 254 keeps W' R} + W 0, 0, 254: + N 20, 30: + {D03 \ 49: !tests_failed : tests_failed + 1! '[FAIL] W 254 clears layer' R} + {D04 \ 17: !tests_failed : tests_failed + 1! '[FAIL] W 254 layer tag' R} + N 22, 30: + {D03 \ 51: !tests_failed : tests_failed + 1! '[FAIL] W 254 clears whole layer' R} + + ; --- B builds a base cell --------------------------------------------- + ; N is not B's inverse: D04 is derived from D03, so writing D04 = 17 + ; with D03 = 200 reads back as D04 = 8. + !D01 : 1! + !D02 : 0! + !D03 : 200! + !D04 : 17! + B 50, 50: + N 50, 50: + {D01 \ 1: !tests_failed : tests_failed + 1! '[FAIL] B D01' R} + {D02 \ 0: !tests_failed : tests_failed + 1! '[FAIL] B D02' R} + {D03 \ 200: !tests_failed : tests_failed + 1! '[FAIL] B D03' R} + {D04 \ 8: !tests_failed : tests_failed + 1! '[FAIL] B D04' R} + + ; --- Y 55 area switching ---------------------------------------------- + Z 3, 4, 12: + W 5, 5, 99: + Y 55, 2: + N 3, 4: + {D03 \ 53: !tests_failed : tests_failed + 1! '[FAIL] Y55 loads AMAP page' R} + {D04 \ 21: !tests_failed : tests_failed + 1! '[FAIL] Y55 AMAP page D04' R} + ; (D03/D04 above would be 12/32 if the Z layer had survived.) + {D02 \ 1: !tests_failed : tests_failed + 1! '[FAIL] Y55 AMAP page D02' R} + ; The W layer is cleared together with the Z layer. + N 5, 5: + {D03 \ 62: !tests_failed : tests_failed + 1! '[FAIL] Y55 clears W layer' R} + {D04 \ 30: !tests_failed : tests_failed + 1! '[FAIL] Y55 W layer tag' R} + + Z 3, 4, 12: + Y 55, 2: + N 3, 4: + {D03 \ 53: !tests_failed : tests_failed + 1! '[FAIL] Y55 same id base map' R} + {D04 \ 21: !tests_failed : tests_failed + 1! '[FAIL] Y55 same id clears Z' R} + + Y 55, 1: + N 3, 4: + {D03 \ 42: !tests_failed : tests_failed + 1! '[FAIL] Y55 return base map' R} + {D04 \ 10: !tests_failed : tests_failed + 1! '[FAIL] Y55 return D04' R} + N 5, 5: + {D03 \ 51: !tests_failed : tests_failed + 1! '[FAIL] Y55 return base map at 5,5' R} + + W 0, 0, 254: + + ; --- display flags ----------------------------------------------------- + ; These have no read-back command; only check that they run and do not + ; disturb the map data. + Y 56, 0: + Y 56, 1: + Y 57, 0: + Y 57, 1: + Y 58, 27: + Y 59, 1: + Y 59, 0: + Y 112, 0: + N 3, 4: + {D03 \ 42: !tests_failed : tests_failed + 1! '[FAIL] display flags kept map' R} + + %0: + +*default: + EOF diff --git a/test/scenario/gakuen_king/saveload.adv b/test/scenario/gakuen_king/saveload.adv new file mode 100644 index 0000000..5788ca1 --- /dev/null +++ b/test/scenario/gakuen_king/saveload.adv @@ -0,0 +1,81 @@ + ; Save and load tests. + ; + ; L resumes right after the Q that wrote the file, so the round trip runs + ; part of this page twice. The W layer tells the two passes apart: a load + ; clears both map layers, and every other state the scenario can read is + ; either restored or unchanged. + ; + ; Failures recorded between Q and L are rolled back together with the rest + ; of the variables, so those checks are caught by the '[FAIL]' text in the + ; text hook output rather than by the counter. + + \L_roundtrip: + \L_partial_load: + \L_bad_slots: + %0: + +*L_roundtrip: + Y 55, 1: + W 5, 5, 99: + !VAR100 : 1234! + !VAR700 : 4321! + Q 1: + !VAR101 : RND! + N 5, 5: + {D03 = 99: + ; First pass. + {VAR101 \ 1: !tests_failed : tests_failed + 1! '[FAIL] Q RND' R} + ; Wreck everything the load has to bring back. + !VAR100 : 0! + !VAR700 : 0! + Z 3, 4, 12: + Y 55, 2: + L 1: + '[FAIL] L did not resume the saved position' R + !tests_failed : tests_failed + 1! + } + + ; Second pass, after the load. + {VAR101 \ 0: !tests_failed : tests_failed + 1! '[FAIL] L RND' R} + {VAR100 \ 1234: !tests_failed : tests_failed + 1! '[FAIL] L variable' R} + {VAR700 \ 4321: !tests_failed : tests_failed + 1! '[FAIL] L high variable' R} + {D03 \ 51: !tests_failed : tests_failed + 1! '[FAIL] L clears the W layer' R} + N 3, 4: + {D03 \ 42: !tests_failed : tests_failed + 1! '[FAIL] L restores the map area' R} + {D04 \ 10: !tests_failed : tests_failed + 1! '[FAIL] L clears the Z layer' R} + + \0: + '[FAIL] label call stack lost by L' R + !tests_failed : tests_failed + 1! + +*L_partial_load: + ; Y 82,var reloads that one variable from the save file RND selects. + !VAR614 : 111! + !VAR615 : 222! + !VAR616 : 333! + Q 3: + !VAR614 : 1! + !VAR615 : 2! + !VAR616 : 3! + !RND : 3! + Y 82, VAR615: + {VAR615 \ 222: !tests_failed : tests_failed + 1! '[FAIL] Y82 value' R} + {VAR614 \ 1: !tests_failed : tests_failed + 1! '[FAIL] Y82 wrote VAR614' R} + {VAR616 \ 3: !tests_failed : tests_failed + 1! '[FAIL] Y82 wrote VAR616' R} + ; A file that does not exist zeroes the target instead. + !RND : 20! + Y 82, VAR616: + {VAR616 \ 0: !tests_failed : tests_failed + 1! '[FAIL] Y82 missing file' R} + \0: + +*L_bad_slots: + !RND : 0! + L 20: + {RND \ 255: !tests_failed : tests_failed + 1! '[FAIL] L missing slot' R} + !RND : 0! + Q 30: + {RND \ 255: !tests_failed : tests_failed + 1! '[FAIL] Q invalid slot' R} + \0: + +*default: + EOF diff --git a/test/scenario/gakuen_king/sys3c.cfg b/test/scenario/gakuen_king/sys3c.cfg new file mode 100644 index 0000000..2feac30 --- /dev/null +++ b/test/scenario/gakuen_king/sys3c.cfg @@ -0,0 +1,5 @@ +game = gakuen_king +hed = test.hed +variables = variables.txt +encoding = utf8 +unicode = true diff --git a/test/scenario/gakuen_king/test.adv b/test/scenario/gakuen_king/test.adv new file mode 100644 index 0000000..7d438b3 --- /dev/null +++ b/test/scenario/gakuen_king/test.adv @@ -0,0 +1,15 @@ + !tests_failed : 0! + + ; System3-sdl2 extension: draw ASCII as half-width characters so the + ; text-hook output is easy for CTest and humans to read. + Y 240, 1: + + %#map.adv: + %#saveload.adv: + + H 0, tests_failed: ' tests failed' R + !RND : tests_failed! + Y 255, 0: + +*default: + EOF diff --git a/test/scenario/gakuen_king/test.hed b/test/scenario/gakuen_king/test.hed new file mode 100644 index 0000000..9d63bd0 --- /dev/null +++ b/test/scenario/gakuen_king/test.hed @@ -0,0 +1,3 @@ +test.adv +map.adv +saveload.adv diff --git a/test/scenario/gakuen_king/variables.txt b/test/scenario/gakuen_king/variables.txt new file mode 100644 index 0000000..95652bb --- /dev/null +++ b/test/scenario/gakuen_king/variables.txt @@ -0,0 +1,768 @@ +RND +D01 +D02 +D03 +D04 +D05 +D06 +D07 +D08 +D09 +D10 +D11 +D12 +D13 +D14 +D15 +D16 +D17 +D18 +D19 +D20 +U01 +U02 +U03 +U04 +U05 +U06 +U07 +U08 +U09 +U10 +U11 +U12 +U13 +U14 +U15 +U16 +U17 +U18 +U19 +U20 +B01 +B02 +B03 +B04 +B05 +B06 +B07 +B08 +B09 +B10 +B11 +B12 +B13 +B14 +B15 +B16 +M_X +M_Y +VAR59 +VAR60 +VAR61 +VAR62 +VAR63 +VAR64 +VAR65 +VAR66 +VAR67 +VAR68 +VAR69 +VAR70 +VAR71 +VAR72 +VAR73 +VAR74 +VAR75 +VAR76 +VAR77 +VAR78 +VAR79 +VAR80 +VAR81 +VAR82 +VAR83 +VAR84 +VAR85 +VAR86 +VAR87 +VAR88 +VAR89 +VAR90 +VAR91 +VAR92 +VAR93 +VAR94 +VAR95 +VAR96 +VAR97 +VAR98 +VAR99 +VAR100 +VAR101 +VAR102 +VAR103 +VAR104 +VAR105 +VAR106 +VAR107 +VAR108 +VAR109 +VAR110 +VAR111 +VAR112 +VAR113 +VAR114 +VAR115 +VAR116 +VAR117 +VAR118 +VAR119 +VAR120 +VAR121 +VAR122 +VAR123 +VAR124 +VAR125 +VAR126 +VAR127 +VAR128 +VAR129 +VAR130 +VAR131 +VAR132 +VAR133 +VAR134 +VAR135 +VAR136 +VAR137 +VAR138 +VAR139 +VAR140 +VAR141 +VAR142 +VAR143 +VAR144 +VAR145 +VAR146 +VAR147 +VAR148 +VAR149 +VAR150 +VAR151 +VAR152 +VAR153 +VAR154 +VAR155 +VAR156 +VAR157 +VAR158 +VAR159 +VAR160 +VAR161 +VAR162 +VAR163 +VAR164 +VAR165 +VAR166 +VAR167 +VAR168 +VAR169 +VAR170 +VAR171 +VAR172 +VAR173 +VAR174 +VAR175 +VAR176 +VAR177 +VAR178 +VAR179 +VAR180 +VAR181 +VAR182 +VAR183 +VAR184 +VAR185 +VAR186 +VAR187 +VAR188 +VAR189 +VAR190 +VAR191 +VAR192 +VAR193 +VAR194 +VAR195 +VAR196 +VAR197 +VAR198 +VAR199 +VAR200 +VAR201 +VAR202 +VAR203 +VAR204 +VAR205 +VAR206 +VAR207 +VAR208 +VAR209 +VAR210 +VAR211 +VAR212 +VAR213 +VAR214 +VAR215 +VAR216 +VAR217 +VAR218 +VAR219 +VAR220 +VAR221 +VAR222 +VAR223 +VAR224 +VAR225 +VAR226 +VAR227 +VAR228 +VAR229 +VAR230 +VAR231 +VAR232 +VAR233 +VAR234 +VAR235 +VAR236 +VAR237 +VAR238 +VAR239 +VAR240 +VAR241 +VAR242 +VAR243 +VAR244 +VAR245 +VAR246 +VAR247 +VAR248 +VAR249 +VAR250 +VAR251 +VAR252 +VAR253 +VAR254 +VAR255 +VAR256 +VAR257 +VAR258 +VAR259 +VAR260 +VAR261 +VAR262 +VAR263 +VAR264 +VAR265 +VAR266 +VAR267 +VAR268 +VAR269 +VAR270 +VAR271 +VAR272 +VAR273 +VAR274 +VAR275 +VAR276 +VAR277 +VAR278 +VAR279 +VAR280 +VAR281 +VAR282 +VAR283 +VAR284 +VAR285 +VAR286 +VAR287 +VAR288 +VAR289 +VAR290 +VAR291 +VAR292 +VAR293 +VAR294 +VAR295 +VAR296 +VAR297 +VAR298 +VAR299 +VAR300 +VAR301 +VAR302 +VAR303 +VAR304 +VAR305 +VAR306 +VAR307 +VAR308 +VAR309 +VAR310 +VAR311 +VAR312 +VAR313 +VAR314 +VAR315 +VAR316 +VAR317 +VAR318 +VAR319 +VAR320 +VAR321 +VAR322 +VAR323 +VAR324 +VAR325 +VAR326 +VAR327 +VAR328 +VAR329 +VAR330 +VAR331 +VAR332 +VAR333 +VAR334 +VAR335 +VAR336 +VAR337 +VAR338 +VAR339 +VAR340 +VAR341 +VAR342 +VAR343 +VAR344 +VAR345 +VAR346 +VAR347 +VAR348 +VAR349 +VAR350 +VAR351 +VAR352 +VAR353 +VAR354 +VAR355 +VAR356 +VAR357 +VAR358 +VAR359 +VAR360 +VAR361 +VAR362 +VAR363 +VAR364 +VAR365 +VAR366 +VAR367 +VAR368 +VAR369 +VAR370 +VAR371 +VAR372 +VAR373 +VAR374 +VAR375 +VAR376 +VAR377 +VAR378 +VAR379 +VAR380 +VAR381 +VAR382 +VAR383 +VAR384 +VAR385 +VAR386 +VAR387 +VAR388 +VAR389 +VAR390 +VAR391 +VAR392 +VAR393 +VAR394 +VAR395 +VAR396 +VAR397 +VAR398 +VAR399 +VAR400 +VAR401 +VAR402 +VAR403 +VAR404 +VAR405 +VAR406 +VAR407 +VAR408 +VAR409 +VAR410 +VAR411 +VAR412 +VAR413 +VAR414 +VAR415 +VAR416 +VAR417 +VAR418 +VAR419 +VAR420 +VAR421 +VAR422 +VAR423 +VAR424 +VAR425 +VAR426 +VAR427 +VAR428 +VAR429 +VAR430 +VAR431 +VAR432 +VAR433 +VAR434 +VAR435 +VAR436 +VAR437 +VAR438 +VAR439 +VAR440 +VAR441 +VAR442 +VAR443 +VAR444 +VAR445 +VAR446 +VAR447 +VAR448 +VAR449 +VAR450 +VAR451 +VAR452 +VAR453 +VAR454 +VAR455 +VAR456 +VAR457 +VAR458 +VAR459 +VAR460 +VAR461 +VAR462 +VAR463 +VAR464 +VAR465 +VAR466 +VAR467 +VAR468 +VAR469 +VAR470 +VAR471 +VAR472 +VAR473 +VAR474 +VAR475 +VAR476 +VAR477 +VAR478 +VAR479 +VAR480 +VAR481 +VAR482 +VAR483 +VAR484 +VAR485 +VAR486 +VAR487 +VAR488 +VAR489 +VAR490 +VAR491 +VAR492 +VAR493 +VAR494 +VAR495 +VAR496 +VAR497 +VAR498 +VAR499 +VAR500 +VAR501 +VAR502 +VAR503 +VAR504 +VAR505 +VAR506 +VAR507 +VAR508 +VAR509 +VAR510 +VAR511 +VAR512 +VAR513 +VAR514 +VAR515 +VAR516 +VAR517 +VAR518 +VAR519 +VAR520 +VAR521 +VAR522 +VAR523 +VAR524 +VAR525 +VAR526 +VAR527 +VAR528 +VAR529 +VAR530 +VAR531 +VAR532 +VAR533 +VAR534 +VAR535 +VAR536 +VAR537 +VAR538 +VAR539 +VAR540 +VAR541 +VAR542 +VAR543 +VAR544 +VAR545 +VAR546 +VAR547 +VAR548 +VAR549 +VAR550 +VAR551 +VAR552 +VAR553 +VAR554 +VAR555 +VAR556 +VAR557 +VAR558 +VAR559 +VAR560 +VAR561 +VAR562 +VAR563 +VAR564 +VAR565 +VAR566 +VAR567 +VAR568 +VAR569 +VAR570 +VAR571 +VAR572 +VAR573 +VAR574 +VAR575 +VAR576 +VAR577 +VAR578 +VAR579 +VAR580 +VAR581 +VAR582 +VAR583 +VAR584 +VAR585 +VAR586 +VAR587 +VAR588 +VAR589 +VAR590 +VAR591 +VAR592 +VAR593 +VAR594 +VAR595 +VAR596 +VAR597 +VAR598 +VAR599 +VAR600 +VAR601 +VAR602 +VAR603 +VAR604 +VAR605 +VAR606 +VAR607 +VAR608 +VAR609 +VAR610 +VAR611 +VAR612 +VAR613 +VAR614 +VAR615 +VAR616 +VAR617 +VAR618 +VAR619 +VAR620 +VAR621 +VAR622 +VAR623 +VAR624 +VAR625 +VAR626 +VAR627 +VAR628 +VAR629 +VAR630 +VAR631 +VAR632 +VAR633 +VAR634 +VAR635 +VAR636 +VAR637 +VAR638 +VAR639 +VAR640 +VAR641 +VAR642 +VAR643 +VAR644 +VAR645 +VAR646 +VAR647 +VAR648 +VAR649 +VAR650 +VAR651 +VAR652 +VAR653 +VAR654 +VAR655 +VAR656 +VAR657 +VAR658 +VAR659 +VAR660 +VAR661 +VAR662 +VAR663 +VAR664 +VAR665 +VAR666 +VAR667 +VAR668 +VAR669 +VAR670 +VAR671 +VAR672 +VAR673 +VAR674 +VAR675 +VAR676 +VAR677 +VAR678 +VAR679 +VAR680 +VAR681 +VAR682 +VAR683 +VAR684 +VAR685 +VAR686 +VAR687 +VAR688 +VAR689 +VAR690 +VAR691 +VAR692 +VAR693 +VAR694 +VAR695 +VAR696 +VAR697 +VAR698 +VAR699 +VAR700 +VAR701 +VAR702 +VAR703 +VAR704 +VAR705 +VAR706 +VAR707 +VAR708 +VAR709 +VAR710 +VAR711 +VAR712 +VAR713 +VAR714 +VAR715 +VAR716 +VAR717 +VAR718 +VAR719 +VAR720 +VAR721 +VAR722 +VAR723 +VAR724 +VAR725 +VAR726 +VAR727 +VAR728 +VAR729 +VAR730 +VAR731 +VAR732 +VAR733 +VAR734 +VAR735 +VAR736 +VAR737 +VAR738 +VAR739 +VAR740 +VAR741 +VAR742 +VAR743 +VAR744 +VAR745 +VAR746 +VAR747 +VAR748 +VAR749 +VAR750 +VAR751 +VAR752 +VAR753 +VAR754 +VAR755 +VAR756 +VAR757 +VAR758 +VAR759 +VAR760 +VAR761 +VAR762 +VAR763 +VAR764 +VAR765 +VAR766 +tests_failed