Files
kichikuou_system3-sdl2/src/sys/nact_gakuenking.cpp
T

2065 lines
65 KiB
C++

#include <algorithm>
#include <array>
#include <string.h>
#include <string>
#include <string_view>
#include <vector>
#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) {
text_wait_time = 0; // Y 13 and Y 14 set this
box[0] = {0, 16, 8, 367, 295};
box[1] = {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);
}
private:
void cmd_a() override {
set_push_bitmap();
NACT::cmd_a();
map_overview_rebuild_pending = true;
}
// --- Y command dispatch -----------------------------------------------
void cmd_y() override {
int cmd = cali();
// Y 82 takes a variable reference, not a value.
int param = (cmd == 82) ? cali2() : cali();
TRACE("Y %d,%d:", cmd, param);
switch (cmd) {
// --- 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;
case 25:
fixed_shapes = (param == 1);
if (0 <= param && param <= 4) {
ags->menu.back_color = ags->text.back_color =
(param == 1 || param == 2) ? 0 : 1;
text_window = param + 1;
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;
// --- 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;
}
}
// --- 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<uint8_t> exe(CREDIT_DATA_END);
if (!fio->read(exe.data(), exe.size())) {
WARNING("%s: too short", EXECUTABLE_NAME);
return;
}
load_digit_font(exe);
load_mouse_cursors(exe);
load_push_bitmaps(exe);
load_credit_text(exe);
}
// --- 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;
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<uint16_t, MAP_CELLS> map_base;
std::array<uint8_t, MAP_CELLS> map_layer_z;
std::array<uint8_t, MAP_CELLS> map_layer_w;
std::array<std::array<uint8_t, EXPLORED_RECORD_BYTES>, 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<uint16_t>(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<uint16_t>(x), static_cast<uint16_t>(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<uint8_t, MAP_CELLS>& 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<uint8_t> 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<uint8_t, EXPLORED_RECORD_BYTES>& 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;
static constexpr int MAP_WORK_SCREEN = 2;
static constexpr int ANIMATION_FRAME_MS = 16;
uint16_t map_view_x = 0;
uint16_t map_view_y = 0;
uint16_t map_sprite_x = 0;
uint16_t map_sprite_y = 0;
uint8_t map_sprite_frame = SPRITE_HIDDEN;
uint8_t map_out_of_bounds_tile = CELL_EMPTY;
// The view and sprite state last drawn, for step detection.
uint16_t map_view_drawn_x = 0;
uint16_t map_view_drawn_y = 0;
uint16_t map_sprite_drawn_x = 0;
uint16_t map_sprite_drawn_y = 0;
bool map_sprite_drawn = false;
bool map_view_drawn = false;
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;
if (direction == 0 || direction == 255) {
map_sprite_frame = static_cast<uint8_t>(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;
int view_step_x = static_cast<int>(map_view_x) - map_view_drawn_x;
int view_step_y = static_cast<int>(map_view_y) - map_view_drawn_y;
int sprite_step_x = static_cast<int>(map_sprite_x) - map_sprite_drawn_x;
int sprite_step_y = static_cast<int>(map_sprite_y) - map_sprite_drawn_y;
if (map_sprite_frame == SPRITE_HIDDEN || !map_sprite_drawn)
sprite_step_x = sprite_step_y = 0;
if (!can_animate(view_step_x, view_step_y, sprite_step_x, sprite_step_y))
view_step_x = view_step_y = sprite_step_x = sprite_step_y = 0;
// The composed image holds the cells of both the old and the new view.
// The sprite is drawn on top of every animation frame.
int origin_x = map_view_x - 5 - std::max(view_step_x, 0);
int origin_y = map_view_y - 4 - std::max(view_step_y, 0);
draw_map_cells(origin_x, origin_y, VIEW_COLS + std::abs(view_step_x),
VIEW_ROWS + std::abs(view_step_y));
update_map_overview();
animate_map_view(origin_x, origin_y, view_step_x, view_step_y,
sprite_step_x, sprite_step_y);
map_overview_rebuild_pending = false;
map_view_drawn_x = map_view_x;
map_view_drawn_y = map_view_y;
map_sprite_drawn_x = map_sprite_x;
map_sprite_drawn_y = map_sprite_y;
map_sprite_drawn = map_sprite_frame != SPRITE_HIDDEN;
map_view_drawn = true;
}
static bool is_single_step(int step) { return -1 <= step && step <= 1; }
bool can_animate(int view_step_x, int view_step_y,
int sprite_step_x, int sprite_step_y) {
return map_view_drawn && !map_overview_rebuild_pending &&
is_single_step(view_step_x) && is_single_step(view_step_y) &&
is_single_step(sprite_step_x) && is_single_step(sprite_step_y);
}
void draw_map_cells(int cell_x0, int cell_y0, int cols, int rows) {
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int cell_x = cell_x0 + col;
int cell_y = cell_y0 + 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;
int px = col * TILE_SIZE;
int py = row * TILE_SIZE;
if (!map_base_visible_view)
base = CELL_EMPTY;
if (base == CELL_EMPTY) {
ags->box_fill(MAP_WORK_SCREEN, 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);
}
}
}
// The pixel position of something that moves `step` cells and arrives at
// `to`, at animation frame `frame` of `frames`.
static int step_position(int to, int step, int frame, int frames) {
return to - step * TILE_SIZE * (frames - frame) / frames;
}
// Draws the view window of the composed map and the sprite on top of it,
// moving both from where they were drawn last to where they are now.
// This is an enhancement; GAKUEN.COM draws the new position at once and
// then waits for map_animation_ticks.
void animate_map_view(int origin_x, int origin_y,
int view_step_x, int view_step_y,
int sprite_step_x, int sprite_step_y) {
int duration = ticks_to_ms(map_animation_ticks);
int frames = std::clamp(duration / ANIMATION_FRAME_MS, 2, TILE_SIZE);
// Pixel positions on the composed image.
int window_x = (map_view_x - 5 - origin_x) * TILE_SIZE;
int window_y = (map_view_y - 4 - origin_y) * TILE_SIZE;
int sprite_x = (map_sprite_x - origin_x) * TILE_SIZE;
int sprite_y = (map_sprite_y - origin_y) * TILE_SIZE;
uint32_t start = SDL_GetTicks();
for (int frame = 1; frame <= frames && !terminate; frame++) {
int wx = step_position(window_x, view_step_x, frame, frames);
int wy = step_position(window_y, view_step_y, frame, frames);
ags->copy_screen(MAP_WORK_SCREEN, 0, wx, wy,
wx + VIEW_COLS * TILE_SIZE - 1,
wy + VIEW_ROWS * TILE_SIZE - 1, VIEW_X, VIEW_Y);
if (map_sprite_frame != SPRITE_HIDDEN) {
int sx = step_position(sprite_x, sprite_step_x, frame, frames);
int sy = step_position(sprite_y, sprite_step_y, frame, frames);
draw_map_sprite(map_sprite_frame, VIEW_X + sx - wx, VIEW_Y + sy - wy);
}
uint32_t deadline = start + duration * frame / frames;
uint32_t now = SDL_GetTicks();
if (now < deadline)
sys_sleep(deadline - now);
}
}
CG load_map_sheet(int page) {
if (page == 0)
return CG();
CG cg = ags->load_cg_surface(page, TRANSPARENT_COLOR, cg_flags);
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_WORK_SCREEN, map_tile_cg[bank],
(index % SHEET_COLS) * TILE_SIZE,
(index / SHEET_COLS) * TILE_SIZE,
TILE_SIZE, TILE_SIZE, px, py, transparent);
}
// Draws a sprite into the view of screen 0, clipped at the view border.
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;
}
int left = std::max(px, VIEW_X);
int top = std::max(py, VIEW_Y);
int right = std::min(px + TILE_SIZE, VIEW_X + VIEW_COLS * TILE_SIZE);
int bottom = std::min(py + TILE_SIZE, VIEW_Y + VIEW_ROWS * TILE_SIZE);
if (left >= right || top >= bottom)
return;
blit_sheet(0, map_sprite_cg, frame * TILE_SIZE + (left - px), top - py,
right - left, bottom - top, left, top, true);
}
void blit_sheet(int dest, 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(dest, 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<uint16_t>(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<uint16_t>(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(0, 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_click = false;
bool key_from_mouse = false;
std::vector<uint16_t> 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_click = true;
} else if (!in_view) {
D01 = left ? 0x10 : 0x40;
} else if (!left) {
D01 = 0x20;
key_menu_from_click = true;
} 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))
take_step(dx < 0 ? 4 : 8);
else
take_step(dy < 0 ? 1 : 2);
key_from_mouse = true;
}
}
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_click) {
key_menu_from_click = 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<uint8_t, MAP_CELLS> dist = {};
std::vector<uint16_t> queue{static_cast<uint16_t>(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<uint16_t>(index));
}
}
if (dist[target] < 2)
return false;
for (int d = dist[target]; d > 1; d--) {
path.push_back(static_cast<uint16_t>(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<uint8_t>& 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<uint8_t>(value / 10000),
static_cast<uint8_t>(value / 1000 % 10),
static_cast<uint8_t>(value / 100 % 10),
static_cast<uint8_t>(value / 10 % 10),
static_cast<uint8_t>(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);
load_cg(208, -1);
VAR_TROOP_COUNT_FULL = VAR_TROOP_COUNT;
draw_troop_counts();
set_cursor(99, 223);
const TroopButton* hover = nullptr;
while (!terminate) {
int mx, my;
get_cursor(&mx, &my);
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<uint16_t>(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;
}
// --- mouse cursor ------------------------------------------------------
// The shapes GAKUEN.COM hands to the mouse driver: six 16x16 patterns of
// two 32-byte planes each, the interior first and the silhouette second.
// Pattern 0 is blank and unused.
static constexpr int MOUSE_CURSOR_OFFSET = 0x2a92;
static constexpr int NR_MOUSE_CURSORS = 6;
static constexpr int MOUSE_CURSOR_SIZE = 16;
struct MouseCursor {
uint16_t body[MOUSE_CURSOR_SIZE];
uint16_t silhouette[MOUSE_CURSOR_SIZE]; // body plus its outline
};
MouseCursor mouse_cursor[NR_MOUSE_CURSORS] = {};
bool fixed_shapes = false; // set by Y 25,1
void load_mouse_cursors(const std::vector<uint8_t>& exe) {
const uint8_t* p = &exe[MOUSE_CURSOR_OFFSET];
for (MouseCursor& cursor : mouse_cursor) {
for (uint16_t* plane : {cursor.body, cursor.silhouette}) {
for (int row = 0; row < MOUSE_CURSOR_SIZE; row++, p += 2)
plane[row] = p[0] | p[1] << 8;
}
}
}
int pick_cursor_shape() {
if (fixed_shapes)
return 5;
int r = random(100);
return r < 5 ? r : 1;
}
void draw_mouse_cursor(int shape, int x, int y) {
const MouseCursor& cursor = mouse_cursor[shape];
for (int row = 0; row < MOUSE_CURSOR_SIZE; row++) {
for (int col = 0; col < MOUSE_CURSOR_SIZE; col++) {
uint16_t bit = 0x8000 >> col;
if (!(cursor.silhouette[row] & bit))
continue;
ags->set_pixel(0, x + col, y + row, (cursor.body[row] & bit) ? 15 : 0);
}
}
ags->invalidate_screen(x, y, MOUSE_CURSOR_SIZE, MOUSE_CURSOR_SIZE);
}
// --- A key wait icon --------------------------------------------------
static constexpr int PUSH_BITMAP_OFFSET = 0x08aa;
static constexpr int NR_PUSH_BITMAPS = 4;
uint8_t push_bitmap[NR_PUSH_BITMAPS][32] = {};
void load_push_bitmaps(const std::vector<uint8_t>& exe) {
memcpy(push_bitmap, &exe[PUSH_BITMAP_OFFSET], sizeof(push_bitmap));
}
void set_push_bitmap() {
int r = random(100);
r = r < NR_PUSH_BITMAPS ? r : 0;
ags->set_push_bitmap(fixed_shapes ? nullptr : push_bitmap[r]);
}
// --- 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];
// The original relies on the mouse pointer movement for visual feedback
// on player input. In system3-sdl2 the pointer may be absent or
// unmovable, so we draw the cursor shape onto the grid instead. It
// disappears once the player moves the pointer, and comes back on the
// next direction key.
const int shape = pick_cursor_shape();
bool cursor_drawn = false;
int seen_x, seen_y;
get_cursor(&seen_x, &seen_y);
// Hides the system pointer and draws the cursor at (cx, cy) instead.
auto show_cursor = [&] {
SDL_ShowCursor(SDL_DISABLE);
draw_mouse_cursor(shape, cx, cy);
cursor_drawn = true;
};
// Erases the drawn cursor and restores the system pointer. Must be
// called before (cx, cy) moves, which is where the cursor is drawn.
auto hide_cursor = [&] {
if (!cursor_drawn)
return;
SDL_ShowCursor(SDL_ENABLE);
ags->copy_screen(WORK_SCREEN, 0, cx, cy, cx + MOUSE_CURSOR_SIZE - 1,
cy + MOUSE_CURSOR_SIZE - 1, cx, cy);
cursor_drawn = false;
};
set_cursor(cx, cy);
show_cursor();
while (!terminate) {
uint8_t key = get_key();
int mx, my;
get_cursor(&mx, &my);
// A pointer position that is neither where it was nor where
// set_cursor() put it is a move by the player. The pointer comes
// back through a scaling that rounds, so allow it one pixel.
if ((mx != seen_x || my != seen_y) &&
(std::abs(mx - cx) > 1 || std::abs(my - cy) > 1))
hide_cursor();
seen_x = mx;
seen_y = my;
if (!key) {
sys_sleep(16);
continue;
}
if (!(key & 0xb0)) {
hide_cursor();
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);
show_cursor();
wait_key_release(0x0f);
continue;
}
if (key & 0xa0) {
hide_cursor();
wait_key_release();
return GRID_SELECT_CANCELED;
}
int col, row;
if (cursor_drawn) {
col = (cx - (wide ? 432 : 392)) / cell_w;
row = (cy - (wide ? 22 : 23)) / 32;
} else {
if (mx < GRID_X || mx >= GRID_X + cols * cell_w ||
my < 7 || my >= 7 + rows * 32) {
// outside the grid.
wait_key_release();
continue;
}
col = (mx - GRID_X) / cell_w;
row = (my - 7 - (wide ? 1 : 0)) / 32;
cx = col * cell_w + (wide ? 432 : 392);
cy = row * 32 + (wide ? 22 : 23);
set_cursor(cx, cy);
}
hide_cursor();
wait_key_release();
// The state table is not consulted: state 0 returns its number
// too, and the scenario rejects it.
return static_cast<uint16_t>(row * cols + col + 1);
}
hide_cursor();
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<uint8_t> table = ags->load_cg_data(DISSOLVE_TABLE_PAGE);
if (static_cast<int>(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<CreditEntry> credit_entries;
void load_credit_text(const std::vector<uint8_t>& 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<const char*>(&exe[start]), pos++ - start);
};
std::string title = read_string();
std::string subtitle = read_string();
std::vector<CreditEntry> 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<uint8_t>& buf, int offset, uint16_t value) {
buf[offset] = value & 0xff;
buf[offset + 1] = value >> 8;
}
static uint16_t get_le16(const std::vector<uint8_t>& 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<uint8_t> 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<uint8_t> 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<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<uint8_t>& data) {
std::vector<Scenario::StackFrame> frames;
int pages = std::min<int>(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<int>(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
NACT_Sys3* create_gakuen_king(const Config& config, const GameId& game_id) {
return new NACT_GakuenKing(config, game_id);
}