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