diff --git a/iso.xml b/iso.xml
index 5368ac8..dea3094 100644
--- a/iso.xml
+++ b/iso.xml
@@ -21,8 +21,10 @@
+
+
diff --git a/src/engine/graphics.c b/src/engine/graphics.c
index 38f7d5c..4ed396c 100644
--- a/src/engine/graphics.c
+++ b/src/engine/graphics.c
@@ -52,6 +52,9 @@ gfx_surf_t gfx_surf[GFX_MAX_SURFACES];
// common clear color
const u8 gfx_clear_rgb[3] = { 0x00, 0x00, 0x20 };
+// when TRUE, don't show loading screens
+bool gfx_suppress_loading = FALSE;
+
static struct fb
{
DRAWENV draw;
@@ -355,6 +358,8 @@ void gfx_draw_clear_immediate(const u8 *rgb) {
}
void gfx_draw_loading(void) {
+ if (gfx_suppress_loading)
+ return;
// clear both framebuffers
gfx_draw_clear_immediate(gfx_clear_rgb);
// just plop it into the currently shown framebuffer
@@ -368,19 +373,40 @@ void gfx_draw_loading(void) {
LoadImage(&rc, (u_long *)img_loading);
}
-void gfx_upload_image(u8 *data, int w, int h, const int mode, const int surf_id) {
+void gfx_upload_image(u8 *data, int w, int h, const int mode, const int surf_id, const bool sync) {
const int shift = 2 - mode;
w >>= shift;
- if (shift) ++h; // CLUT underneath image
- // upload into bottom right corner of VRAM and hope it doesn't hit anything
- RECT rect = { 1024 - ALIGN(w, 16), 512 - h, w, h };
- gfx_surf[surf_id].clut = shift ? getClut(rect.x, rect.y + h - 1) : 0;
+
+ RECT rect = { 0, 0, w, h };
+ if (gfx_surf[surf_id].tex_x || gfx_surf[surf_id].tex_y) {
+ // there's already a surface with this ID, overwrite it
+ rect.x = gfx_surf[surf_id].tex_x;
+ rect.y = gfx_surf[surf_id].tex_y;
+ } else {
+ // slam it into the bottom right corner and hope it doesn't overwrite anything
+ rect.x = 1024 - ALIGN(w, 16);
+ rect.y = 512 - h - 1;
+ }
+
+ if (mode == 0) {
+ // 16 colors; CLUT is safely underneath image
+ ++rect.h;
+ gfx_surf[surf_id].clut = getClut(rect.x, rect.y + h);
+ } else if (mode == 1) {
+ // 256 colors; CLUT is too big for this shit, we have to do a separate upload
+ // shove it into the bottommost line under the framebuffer
+ RECT clutrect = { 0, 511, 256, 1 };
+ u8 *clutptr = data + (rect.w * rect.h * 2);
+ LoadImage(&clutrect, (u_long *)clutptr);
+ gfx_surf[surf_id].clut = getClut(clutrect.x, clutrect.y);
+ }
+
gfx_surf[surf_id].mode = mode;
gfx_surf[surf_id].id = surf_id;
gfx_surf[surf_id].tex_x = rect.x;
gfx_surf[surf_id].tex_y = rect.y;
LoadImage(&rect, (u_long *)data);
- DrawSync(0);
+ if (sync) DrawSync(0);
}
int gfx_upload_gfx_bank(gfx_bank_t *bank, u8 *bank_data) {
diff --git a/src/engine/graphics.h b/src/engine/graphics.h
index 45fe816..52538b0 100644
--- a/src/engine/graphics.h
+++ b/src/engine/graphics.h
@@ -41,10 +41,30 @@ typedef struct {
// [surfdata_h * VRAM_PAGE_WIDTH] words of surface data follows
} gfx_bank_t;
+/* essentially same shit but for RAM storage */
+
+typedef struct {
+ s16 mode; // 1 for 256-color, 0 for 16-color, -1 for no image
+ u16 w; // width, in words
+ u16 h; // height, in vram lines
+ u16 size; // size, in words
+ u32 ofs; // offset from beginning of ram_surfbank_t
+} gfx_ramsurf_t;
+
+typedef struct {
+ u32 numsurf; // total number of surfaces in bank, each surface has its own clut
+ gfx_ramsurf_t surf[]; // [numsurf] surface headers
+ // surface data follows:
+ // for each surface:
+ // some words: image data \ total: `size` words
+ // 16 or 256 words: clut data /
+} gfx_rambank_t;
+
#pragma pack(pop)
extern gfx_surf_t gfx_surf[];
extern const u8 gfx_clear_rgb[3];
+extern bool gfx_suppress_loading;
typedef struct {
rect_t r; // rect inside surface
@@ -81,7 +101,7 @@ void gfx_pop_cliprect(const int layer);
// if mode != 2, CLUT is immediately after the image (data + w * h) and may be up to w bytes in size
// w must be aligned to 16
-void gfx_upload_image(u8 *data, int w, int h, const int mode, const int surf_id);
+void gfx_upload_image(u8 *data, int w, int h, const int mode, const int surf_id, const bool sync);
// converts the `rect` field of `r` into tpage address, UVs and XYWH instead of LTRB
static inline void gfx_set_texrect(gfx_texrect_t *tr, const int s) {
diff --git a/src/game/credits.c b/src/game/credits.c
new file mode 100644
index 0000000..5c7b29e
--- /dev/null
+++ b/src/game/credits.c
@@ -0,0 +1,314 @@
+#include
+#include
+
+#include "engine/common.h"
+#include "engine/graphics.h"
+#include "engine/org.h"
+#include "engine/filesystem.h"
+#include "engine/memory.h"
+#include "engine/input.h"
+
+#include "game/game.h"
+#include "game/stage.h"
+#include "game/npc.h"
+#include "game/tsc.h"
+#include "game/camera.h"
+#include "game/player.h"
+#include "game/profile.h"
+#include "game/menu.h"
+#include "game/credits.h"
+
+#define CREDITS_MAX_CAST ((VID_HEIGHT / 16) + 1)
+#define CREDITS_BANK "\\MAIN\\CREDIT.CRD;1"
+#define CREDITS_SCRIPT "\\MAIN\\CREDIT.BSC;1"
+
+enum img_act {
+ IMG_ACT_IDLE,
+ IMG_ACT_SLIDE_IN,
+ IMG_ACT_SLIDE_OUT,
+};
+
+enum csc_mode {
+ CSC_MODE_OFF,
+ CSC_MODE_PARSE,
+ CSC_MODE_WAIT,
+};
+
+static struct {
+ s32 mode;
+ u8 *data;
+ u8 *readptr;
+ const char *laststr;
+ s32 wait;
+ s32 cast_x;
+ s32 img;
+ s32 img_x;
+ s32 img_act;
+} csc_state;
+
+typedef struct {
+ gfx_texrect_t texrect;
+ const char *text;
+ bool show;
+ s32 x;
+ s32 y;
+} cast_t;
+
+static cast_t cast_list[CREDITS_MAX_CAST];
+
+// might drop the name later, not really required
+typedef struct {
+ char name;
+ u8 num_args;
+} csc_opcode_t;
+
+static const csc_opcode_t csc_optab[] = {
+ { 'l', 1 }, { '+', 1 }, { '!', 1 }, { '-', 1 },
+ { 'f', 2 }, { 'j', 1 }, { '~', 0 }, { '/', 0 },
+ { '[', 0 }, { ']', 1 },
+};
+
+#define NUM_OPCODES (sizeof(tsc_optab) / sizeof(*tsc_optab))
+#define BASE_OPCODE 0x80
+
+static gfx_rambank_t *cbank;
+static gfx_texrect_t rc_illust = {{ 0, 0, 160, 240 }};
+
+void credits_start(void) {
+ // show loading screen while loading the credits bank
+ gfx_draw_loading();
+
+ memset_word(cast_list, 0, sizeof(cast_list));
+ memset_word(&csc_state, 0, sizeof(csc_state));
+
+ fs_file_t *f = fs_fopen(CREDITS_SCRIPT, FALSE);
+ if (!f) PANIC("could not open\n" CREDITS_SCRIPT);
+ int fsize = fs_fsize(f);
+ tsc_script_t *csc = mem_alloc(fsize);
+ fs_fread_or_die(csc, fsize, 1, f);
+ fs_fclose(f);
+
+ f = fs_fopen(CREDITS_BANK, FALSE);
+ if (!f) PANIC("could not open\n" CREDITS_BANK);
+ fsize = fs_fsize(f);
+ cbank = mem_alloc(fsize);
+ fs_fread_or_die(cbank, fsize, 1, f);
+ fs_fclose(f);
+
+ // upload the cast surface into the bottom right corner
+ const gfx_ramsurf_t *s = &cbank->surf[0];
+ gfx_surf[SURFACE_ID_CASTS].id = SURFACE_ID_CASTS;
+ gfx_surf[SURFACE_ID_CASTS].tex_x = 1024 - s->w / 2;
+ gfx_surf[SURFACE_ID_CASTS].tex_y = 512 - s->h;
+ gfx_upload_image((u8 *)cbank + s->ofs, s->w, s->h, s->mode, SURFACE_ID_CASTS, FALSE);
+
+ // upload the first credits image right away to the left of CASTS
+ s = &cbank->surf[1];
+ gfx_surf[SURFACE_ID_CREDITS_IMAGE].id = SURFACE_ID_CREDITS_IMAGE;
+ gfx_surf[SURFACE_ID_CREDITS_IMAGE].tex_x = 1024 - 256;
+ gfx_surf[SURFACE_ID_CREDITS_IMAGE].tex_y = 512 - s->h - 1; // 1 is for the CLUT
+ gfx_upload_image((u8 *)cbank + s->ofs, s->w, s->h, s->mode, SURFACE_ID_CREDITS_IMAGE, TRUE);
+
+ csc_state.data = csc_state.readptr = (u8 *)csc + csc->ev_map[0].ofs;
+ csc_state.img = 1;
+ csc_state.img_x = TO_FIX(-160);
+ csc_state.img_act = IMG_ACT_IDLE;
+ csc_state.mode = CSC_MODE_PARSE;
+ gfx_set_texrect(&rc_illust, SURFACE_ID_CREDITS_IMAGE);
+
+ // move hi mark to after the credits bank so credits stages would load after it
+ mem_set_mark(MEM_MARK_HI);
+
+ // clear camera fade
+ cam_clear_fade();
+
+ // don't show loading screens for the duration of the credits
+ gfx_suppress_loading = TRUE;
+ game_flags |= GFLAG_SHOW_CREDITS;
+
+ // lock player
+ player.cond |= PLRCOND_INVISIBLE;
+}
+
+void credits_stop(void) {
+ // stop the level script
+ tsc_stop_event();
+ // fade music if we haven't already
+ org_start_fade();
+
+ // free credits data, restore hi mark to lo mark value
+ mem_free_to_mark(MEM_MARK_LO);
+ mem_set_mark(MEM_MARK_HI);
+
+ csc_state.readptr = NULL;
+ csc_state.data = NULL;
+ csc_state.img = 0;
+ csc_state.mode = CSC_MODE_OFF;
+ cbank = NULL;
+
+ gfx_suppress_loading = FALSE;
+ game_flags &= ~GFLAG_SHOW_CREDITS;
+
+ // ask the user to save
+ menu_open(MENU_SAVE);
+}
+
+static void credits_new_cast(const int id, const char *text) {
+ int n = 0;
+ while (n < CREDITS_MAX_CAST && cast_list[n].show)
+ ++n;
+ if (n == CREDITS_MAX_CAST)
+ return;
+ cast_t *c = &cast_list[n];
+ c->show = TRUE;
+ c->text = text;
+ c->x = csc_state.cast_x;
+ c->y = TO_FIX(VID_HEIGHT + 8);
+ c->texrect.r.x = (id % 13) * 24;
+ c->texrect.r.y = (id / 13) * 24;
+ c->texrect.r.right = c->texrect.r.x + 24;
+ c->texrect.r.bottom = c->texrect.r.y + 24;
+ gfx_set_texrect(&c->texrect, SURFACE_ID_CASTS);
+}
+
+static inline void credits_update_cast(void) {
+ for (int i = 0; i < CREDITS_MAX_CAST; ++i) {
+ cast_t *cast = &cast_list[i];
+ if (cast->show) {
+ cast->y -= FIX_SCALE / 2;
+ if (cast->y < TO_FIX(-16))
+ cast->show = FALSE;
+ }
+ }
+}
+
+static inline void credits_draw_cast(void) {
+ for (int i = 0; i < CREDITS_MAX_CAST; ++i) {
+ cast_t *cast = &cast_list[i];
+ if (cast->show) {
+ const int cast_y = TO_INT(cast->y);
+ const int cast_x = cast->x - GFX_FONT_WIDTH;
+ gfx_draw_texrect(&cast->texrect, GFX_LAYER_FRONT, cast_x - 24, cast_y - 8);
+ gfx_draw_string(cast->text, GFX_LAYER_FRONT, cast_x, cast_y);
+ }
+ }
+}
+
+static inline void credits_parse_script(void) {
+ const u8 opcode = *csc_state.readptr++;
+ const csc_opcode_t *op = &csc_optab[opcode - BASE_OPCODE];
+ u16 args[4];
+ for (u32 i = 0; i < op->num_args; ++i) {
+ // might be misaligned, don't risk it
+ args[i] = *csc_state.readptr++;
+ args[i] |= (*csc_state.readptr++) << 8;
+ }
+
+ switch (opcode) {
+ case 0x80: // 'l'
+ // label; ideally we'll never come across one
+ break;
+ case 0x81: // '+'
+ csc_state.cast_x = args[0];
+ break;
+ case 0x82: // '!'
+ stage_change_music(args[0]);
+ break;
+ case 0x83: // '-'
+ csc_state.mode = CSC_MODE_WAIT;
+ csc_state.wait = args[0];
+ break;
+ case 0x84: // 'f'
+ if (npc_get_flag(args[0]))
+ csc_state.readptr = csc_state.data + args[1];
+ break;
+ case 0x85: // 'j'
+ csc_state.readptr = csc_state.data + args[0];
+ break;
+ case 0x86: // '~'
+ org_start_fade();
+ break;
+ case 0x87: // '/'
+ csc_state.mode = CSC_MODE_OFF;
+ break;
+ case 0x88: // '['
+ // start of cast definition; remember the string inside
+ ASSERT(*csc_state.readptr++ == 0xFF); // check for string start
+ csc_state.laststr = (char *)csc_state.readptr;
+ // skip til the end of string
+ while (*csc_state.readptr) ++csc_state.readptr;
+ // check that the definition is properly terminated
+ ASSERT(*++csc_state.readptr == 0x89);
+ break;
+ case 0x89: // ']'
+ // end of cast definition; use the string we remembered to set a cast slot
+ ASSERT(csc_state.laststr);
+ credits_new_cast(args[0], csc_state.laststr);
+ break;
+ default:
+ printf("unimplemented CSC opcode %02x\n", opcode);
+ break;
+ }
+}
+
+void credits_update(void) {
+ switch (csc_state.mode) {
+ case CSC_MODE_PARSE:
+ credits_parse_script();
+ break;
+ case CSC_MODE_WAIT:
+ if (--csc_state.wait <= 0)
+ csc_state.mode = CSC_MODE_PARSE;
+ break;
+ }
+
+ switch (csc_state.img_act) {
+ case IMG_ACT_SLIDE_IN:
+ csc_state.img_x += TO_FIX(40);
+ if (csc_state.img_x > 0)
+ csc_state.img_x = 0;
+ break;
+ case IMG_ACT_SLIDE_OUT:
+ csc_state.img_x -= TO_FIX(40);
+ if (csc_state.img_x < TO_FIX(-160))
+ csc_state.img_x = TO_FIX(-160);
+ break;
+ default:
+ csc_state.img_x = TO_FIX(-160);
+ break;
+ }
+
+ if (csc_state.mode != CSC_MODE_OFF)
+ credits_update_cast();
+
+ if (input_trig & IN_PAUSE) {
+ snd_play_sound(PRIO_HIGH, 18, FALSE);
+ credits_stop();
+ input_trig = 0;
+ }
+}
+
+void credits_draw(void) {
+ gfx_draw_fillrect(gfx_clear_rgb, GFX_LAYER_FRONT, 0, 0, 160, VID_HEIGHT);
+ // draw current illustration
+ gfx_draw_texrect(&rc_illust, GFX_LAYER_FRONT, TO_INT(csc_state.img_x), 0);
+ // draw cast list
+ credits_draw_cast();
+}
+
+void credits_show_image(const int id) {
+ if (csc_state.img != id) {
+ // upload new image
+ const gfx_ramsurf_t *s = &cbank->surf[id];
+ if (s->mode >= 0 && s->w)
+ gfx_upload_image((u8 *)cbank + s->ofs, s->w, s->h, s->mode, SURFACE_ID_CREDITS_IMAGE, FALSE);
+ csc_state.img = id;
+ }
+ // slide in from the left
+ csc_state.img_act = IMG_ACT_SLIDE_IN;
+}
+
+void credits_hide_image(void) {
+ // slide off screen to the left
+ csc_state.img_act = IMG_ACT_SLIDE_OUT;
+}
diff --git a/src/game/credits.h b/src/game/credits.h
new file mode 100644
index 0000000..ee0cd72
--- /dev/null
+++ b/src/game/credits.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "engine/common.h"
+
+void credits_start(void);
+void credits_stop(void);
+void credits_update(void);
+void credits_draw(void);
+void credits_show_image(const int id);
+void credits_hide_image(void);
diff --git a/src/game/game.c b/src/game/game.c
index 1dffcbc..f098cdd 100644
--- a/src/game/game.c
+++ b/src/game/game.c
@@ -15,6 +15,7 @@
#include "game/dmgnum.h"
#include "game/hud.h"
#include "game/menu.h"
+#include "game/credits.h"
#include "game/game.h"
u32 game_flags = GFLAG_INPUT_ENABLED;
@@ -63,7 +64,9 @@ void game_start_intro(void) {
// stage_transition(47, 92, 4, 17); // core
// stage_transition(53, 92, 4, 165); // oside
// stage_transition(91, 100, 4, 4); // island
+ // stage_transition(92, 500, 8, 52); // ballo2
// stage_transition(79, 94, 10, 8); // prefa2
+ // stage_transition(0, 100, 1, 15); // null
}
void game_start_new(void) {
@@ -164,6 +167,9 @@ void game_frame(void) {
cam_update();
}
+ if (game_flags & GFLAG_SHOW_CREDITS)
+ credits_update();
+
// fade always updates
cam_update_fade();
@@ -204,6 +210,9 @@ void game_frame(void) {
hud_draw_map_name(); // this has to be above the fade
tsc_draw();
+ if (game_flags & GFLAG_SHOW_CREDITS)
+ credits_draw();
+
++game_tick;
}
diff --git a/src/game/menu.c b/src/game/menu.c
index 2a4c4d7..986ac5f 100644
--- a/src/game/menu.c
+++ b/src/game/menu.c
@@ -557,7 +557,7 @@ static void menu_map_open(void) {
// append CLUT
memcpy(data + aw * ah, map_clut, sizeof(map_clut));
// upload for later
- gfx_upload_image(data, aw, ah, 1, SURFACE_ID_MAP);
+ gfx_upload_image(data, aw, ah, 1, SURFACE_ID_MAP, TRUE);
mem_free(data);
map.texrect.r.left = 0;
@@ -784,9 +784,14 @@ static inline void menu_saveload_close(const bool success) {
// close menu
menu_id = 0;
- // if we're in the intro stage, re-open the main menu
- if (!stage_data || stage_data->id == STAGE_OPENING_ID) {
- menu_open(MENU_TITLE);
+ // if we're in the intro or ending stage, re-open the main menu
+ if (!stage_data || stage_data->id == STAGE_OPENING_ID || stage_data->id == STAGE_CREDITS_ID) {
+ if (stage_data->id == STAGE_CREDITS_ID) {
+ game_reset();
+ game_start_intro();
+ } else {
+ menu_open(MENU_TITLE);
+ }
} else {
// nuke the old "want to save?" prompt
tsc_clear_text();
diff --git a/src/game/npc.h b/src/game/npc.h
index 134ffe9..b4b5137 100644
--- a/src/game/npc.h
+++ b/src/game/npc.h
@@ -90,9 +90,9 @@ typedef struct npc {
s16 count1;
s16 count2;
s16 size;
+ s16 dir;
u8 cond;
- s8 dir;
u8 shock;
u8 snd_die;
u8 snd_hit;
diff --git a/src/game/npc_act/npc_act_340.c b/src/game/npc_act/npc_act_340.c
index c524d5c..65132da 100644
--- a/src/game/npc_act/npc_act_340.c
+++ b/src/game/npc_act/npc_act_340.c
@@ -1326,6 +1326,7 @@ void npc_act_352(npc_t *npc) {
};
npc->rect = &rc[npc->anim + (npc->count1 * 2)];
+ npc->rect_prev = NULL; // always update rect
}
// Bute with sword (flying)
diff --git a/src/game/profile.c b/src/game/profile.c
index 7a5cb11..4ddd048 100644
--- a/src/game/profile.c
+++ b/src/game/profile.c
@@ -43,13 +43,16 @@ void profile_save(void) {
profile.save.stage_bank_id = stage_bank_id;
profile.save.music_id = org_get_id();
- // if this is a post game save, replace stage title with time
if (profile_stopwatch) {
+ // this is a post-game save with nikumaru timer on, save the record
const int total_seconds = profile_stopwatch / 50;
const int min = total_seconds / 60;
const int sec = total_seconds % 60;
const int sub = (profile_stopwatch / 5) % 10;
sprintf(profile.save.stage_title, "= %d'%02d\"%d =", min, sec, sub);
+ } else if (stage_data->id == STAGE_CREDITS_ID) {
+ // this is a post-game save without nikumaru timer on, mark it as such
+ strcpy(profile.save.stage_title, "= End =");
} else {
memcpy(profile.save.stage_title, stage_data->title, sizeof(profile.save.stage_title));
}
diff --git a/src/game/stage.c b/src/game/stage.c
index ca24a72..22240c6 100644
--- a/src/game/stage.c
+++ b/src/game/stage.c
@@ -136,7 +136,7 @@ void stage_free_stage_bank(void) {
stage_data = NULL;
stage_bank_id = 0;
- mem_free_to_mark(MEM_MARK_LO);
+ mem_free_to_mark(MEM_MARK_HI);
}
int stage_transition(const u32 id, const u32 event, int plr_x, int plr_y) {
@@ -199,7 +199,7 @@ static inline void stage_load_music(const u32 id) {
// bail if this song is from a different stage bank
// this can theoretically happen if we load into a stage bank different than where the music started
if (!sng) {
- printf("stage_load_music(): music %02x is not in stage bank %02x", id, stage_bank_id);
+ printf("stage_load_music(): music %02x is not in stage bank %02x\n", id, stage_bank_id);
// ensure that next time the proper music does start
music_prev = 0;
music_prev_pos = 0;
@@ -363,7 +363,7 @@ static inline void stage_draw_map(int cam_vx, int cam_vy) {
atrb = stage_data->atrb[tile];
if (atrb == 0x43)
gfx_draw_texrect_16x16(&rc_snack_tile, GFX_LAYER_FRONT, (tx << 4) - cam_vx, (ty << 4) - cam_vy);
- else if (atrb < 0x20 || (atrb >= 0x40 && atrb < 0x80))
+ else if ((atrb < 0x20) || (atrb >= 0x40 && atrb < 0x80))
gfx_draw_tile(tile & 0xF, tile >> 4, (atrb >= 0x40), (tx << 4) - cam_vx, (ty << 4) - cam_vy);
}
}
diff --git a/src/game/tsc.c b/src/game/tsc.c
index 0956506..1b3c59d 100644
--- a/src/game/tsc.c
+++ b/src/game/tsc.c
@@ -17,6 +17,7 @@
#include "game/hud.h"
#include "game/menu.h"
#include "game/profile.h"
+#include "game/credits.h"
#include "game/tsc.h"
#define FACE_SIZE 48
@@ -314,7 +315,8 @@ static inline bool tsc_parse_string(const char in_ch) {
if (ch[1]) text[line][tsc_state.writepos++] = ch[1];
text[line][tsc_state.writepos] = 0; // terminate for now
- snd_play_sound(PRIO_HIGH, 2, FALSE);
+ if (tsc_state.flags & 2)
+ snd_play_sound(PRIO_HIGH, 2, FALSE);
tsc_state.blink = 0;
if (tsc_state.writepos >= TSC_LINE_LEN) {
@@ -635,23 +637,24 @@ static inline bool tsc_exec_opcode(const u8 opcode) {
}
return TRUE;
case 0x51: // STC
- profile_stopwatch = game_stopwatch;
- profile_save();
- // the updated post-game save will be later written in the credits logic
+ if (player.equip & EQUIP_NIKUMARU_COUNTER) {
+ profile_stopwatch = game_stopwatch;
+ profile_save();
+ // the updated post-game save will be later written in the credits logic
+ }
return FALSE;
case 0x52: // CRE
- printf("TODO: credits\n");
- game_flags |= GFLAG_SHOW_CREDITS;
+ credits_start();
return FALSE;
case 0x53: // XX1
// open the falling island "menu"
menu_open(MENU_FALLING_ISLAND_0 + args[0]);
return TRUE;
case 0x54: // SIL
- printf("TODO: credits show illust\n");
+ credits_show_image(args[0]);
return FALSE;
case 0x55: // CIL
- printf("TODO: credits hide illust\n");
+ credits_hide_image();
return FALSE;
case 0x56: // ESC
// can't exit, so we restart instead in both cases
diff --git a/src/main.c b/src/main.c
index 2ba59fb..c42b073 100644
--- a/src/main.c
+++ b/src/main.c
@@ -29,9 +29,10 @@ int main(int argc, char **argv) {
timer_init();
game_init();
- // lo mark: all allocations after game_init() are unloaded every stagebank change
- // hi mark: dynamic allocations after stagebank load, if any
+ // all allocations after game_init() are unloaded every stagebank change
+ // high mark is changed during credits
mem_set_mark(MEM_MARK_LO);
+ mem_set_mark(MEM_MARK_HI);
u32 now = timer_ticks;
u32 next_frame = now;
diff --git a/tools/src/common/stage.c b/tools/src/common/stage.c
index b2ddb42..64a84f6 100644
--- a/tools/src/common/stage.c
+++ b/tools/src/common/stage.c
@@ -49,7 +49,7 @@ static inline bool parse_stage_entry(char **args, stage_list_t *list, const int
for (int i = 0; i < NUM_STAGE_ARGS; ++i) {
args[i] = trim_whitespace(args[i]);
- if (!strcmp(args[i], "0") && i != 1) // HACK: "0" is an actual map
+ if (!strcmp(args[i], "0") && i > 1) // HACK: "0" is an actual map
args[i] = ∅
}
@@ -107,7 +107,7 @@ static inline bool parse_bank_entry(char **args, stage_list_t *list, const int l
const char *rootname = args[0];
if (!rootname || !rootname[0]) {
- fprintf(stderr, "warning: bank root must not be empty\n");
+ fprintf(stderr, "warning: bank root must be a non-null stage\n");
return false;
}
@@ -136,6 +136,41 @@ static inline bool parse_bank_entry(char **args, stage_list_t *list, const int l
return true;
}
+static inline bool parse_music_entry(char **args, stage_list_t *list, const int listlen) {
+ for (int i = 0; i < NUM_BANK_ARGS; ++i) {
+ args[i] = trim_whitespace(args[i]);
+ if (!args[i][0]) args[i] = NULL;
+ }
+
+ const char *rootname = args[0];
+ if (!rootname || !rootname[0]) {
+ fprintf(stderr, "warning: bank root must not be empty\n");
+ return false;
+ }
+
+ stage_list_t *root = NULL;
+ for (int i = 0; i < listlen; ++i) {
+ if (!strcasecmp(list[i].name, rootname)) {
+ root = &list[i];
+ break;
+ }
+ }
+ if (!root) {
+ fprintf(stderr, "warning: unknown bank root stage '%s'\n", rootname);
+ return false;
+ }
+
+ root->numsongs = 0;
+ for (int a = 1; a < NUM_BANK_ARGS; ++a) {
+ if (args[a]) {
+ const int x = atoi(args[a]);
+ if (x) root->songs[root->numsongs++] = x;
+ }
+ }
+
+ return true;
+}
+
static inline bool parse_vram_surf_rect(char **args) {
if (!isdigit(args[0][7])) {
fprintf(stderr, "warning: SURFSET has to have a page numer at the end\n");
@@ -196,6 +231,8 @@ int read_stagelist(stage_list_t *list, FILE *f) {
parse_vram_surf_rect(token);
else if (!strncasecmp(token[0], "CLUTSET", 7) && i >= NUM_RECT_ARGS)
parse_vram_clut_rect(token);
+ else if (!strncasecmp(token[0], "MUSIC", 5) && i >= NUM_BANK_ARGS)
+ parse_music_entry(&token[1], list, num);
}
return num;
diff --git a/tools/src/common/vram.h b/tools/src/common/vram.h
index d2fe6e7..13e0e21 100644
--- a/tools/src/common/vram.h
+++ b/tools/src/common/vram.h
@@ -79,8 +79,8 @@ typedef struct {
ram_surf_t surf[]; // [numsurf] surface headers
// surface data follows:
// for each surface:
- // 16 or 256 words: clut data
- // `size` words: image data
+ // some words: image data \ total: `size` words
+ // 16 or 256 words: clut data /
} ram_surfbank_t;
#pragma pack(pop)
diff --git a/tools/src/stagepack.c b/tools/src/stagepack.c
index 3dcc72b..6ab06bb 100644
--- a/tools/src/stagepack.c
+++ b/tools/src/stagepack.c
@@ -25,8 +25,10 @@ int main(int argc, char **argv) {
const char *outpath = argv[3];
// init stage list
- for (int i = 0; i < MAX_STAGES; ++i)
+ for (int i = 0; i < MAX_STAGES; ++i) {
stages[i].numlinks = -1;
+ stages[i].numsongs = -1;
+ }
FILE *f = fopen(listname, "r");
if (!f) {
@@ -77,11 +79,9 @@ int main(int argc, char **argv) {
stage->bk_type = stages[i].bktype;
strncpy(stage->title, stages[i].title, sizeof(stage->title) - 1);
- //scan for songs used by the stage
- stages[i].numsongs = tsc_scan_music(tsc_src, stages[i].songs, MAX_STAGE_SONGS);
- // HACK: add extra title song if this is the intro stage
- if (stages[i].titlesheet[0] && stages[i].numsongs < MAX_STAGE_SONGS)
- stages[i].songs[stages[i].numsongs++] = 0x18;
+ // scan for songs used by the stage if they weren't specified manually with a MUSIC directive
+ if (stages[i].numsongs < 0)
+ stages[i].numsongs = tsc_scan_music(tsc_src, stages[i].songs, MAX_STAGE_SONGS);
// scan for transitions if they weren't specified manually with a BANK directive
if (stages[i].numlinks < 0)
diff --git a/tools/stagelists/stage_all.lst b/tools/stagelists/stage_all.lst
index 5a60bdd..bdf51ce 100644
--- a/tools/stagelists/stage_all.lst
+++ b/tools/stagelists/stage_all.lst
@@ -8,7 +8,7 @@ CLUTSET0, 0, 0, 320, 16
# copied right from Stage.cpp, some capitalization mistakes fixed
# numbers at the end of STAGE are for ease of reference, they can be omitted
-# tiles map bkType bkBmp npcBmp bossBmp bn name
+# tiles map bkType bkBmp npcBmp bossBmp bn name
STAGE000, 0, 0, BACKGROUND_TYPE_BLACK, bk0, Guest, 0, 0, Null
STAGE001, Pens, Pens1, BACKGROUND_TYPE_MOVE_DISTANT, bkBlue, Guest, 0, 0, Arthur's House
STAGE002, Eggs, Eggs, BACKGROUND_TYPE_MOVE_DISTANT, bkGreen, Eggs1, Ravil, 0, Egg Corridor
@@ -113,3 +113,18 @@ STAGE094, Oside, Clock, BACKGROUND_TYPE_CLOUDS_WINDY, bkMoon, Moon, 0,
BANK0000, Pens1, 0, 0, 0, 0, 0, 0, 0, 0, disallow linking arthur's house 1 and 2
BANK0001, Pens2, 0, 0, 0, 0, 0, 0, 0, 0, disallow linking arthur's house 2 and 1
+
+# explicitly specifies which music tracks to link into a stage bank; this overrides autodetect completely
+# numbers after MUSIC also don't matter; music is put into the root bank
+
+# root mus0 mus1 mus2 mus3 mus4 mus5 mus6 mus7 comment (always keep the trailing comma)
+
+MUSIC000, 0, 33, 1, 0, 0, 0, 0, 0, 0, credits
+MUSIC072, Kings, 24, 0, 0, 0, 0, 0, 0, 0, title screen
+MUSIC074, e_Maze, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages
+MUSIC075, e_Jenk, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages
+MUSIC076, e_Malc, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages
+MUSIC077, e_Ceme, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages
+MUSIC078, e_Sky, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages
+MUSIC089, e_Labo, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages
+MUSIC093, e_Blcn, 0, 0, 0, 0, 0, 0, 0, 0, don't pack any music into the ending stages