// High-level overview of the differences between the three games that can't be // easily abstracted away: // // 1) TH03 and TH04 allocate dedicated memory for backing up the text box area // in VRAM ([box_bg]), TH05 uses the bgimage system instead. // 2) TH03 types text onto both pages, with a delay after each glyph. TH04 and // TH05 render all text onto VRAM page 1 (the invisible one) without any // delay, and then fade the new text onto page 0 during box_1_to_0_animate(). // 3) TH03 and TH04 unblit both VRAM pages when starting a new text box, while // TH05 leaves the text of the previous box on the visible VRAM page 0. #pragma option -zPgroup_01 #include #include "platform.h" #include "x86real.h" #include "decomp.hpp" #include "pc98.h" #include "planar.h" #include "shiftjis.hpp" #include "master.hpp" #include "libs/kaja/kaja.h" #include "th02/v_colors.hpp" extern "C" { #include "th02/hardware/frmdelay.h" #if (GAME == 5) #include "th04/hardware/bgimage.hpp" #include "th04/hardware/grppsafx.h" #include "th04/snd/snd.h" #include "th04/gaiji/gaiji.h" #include "th05/hardware/input.h" #include "th05/formats/pi.hpp" #elif (GAME == 4) #include "th03/formats/pi.hpp" #include "th04/hardware/input.h" #include "th04/hardware/grppsafx.h" #include "th04/snd/snd.h" #else #include "th01/hardware/grppsafx.h" #include "th03/hardware/input.h" #include "th03/formats/pi.hpp" #include "th03/snd/snd.h" // Let's rather not have this one global, since it might be wrong in an // in-game context? #define key_det input_sp #endif } #include "th03/math/str_val.hpp" #include "th03/cutscene/cutscene.hpp" #pragma option -a2 #if (GAME == 3) #undef grcg_off // ZUN bloat #endif // Constants // --------- static const pixel_t PIC_W = PI_QUARTER_W; static const pixel_t PIC_H = PI_QUARTER_H; static const screen_x_t PIC_LEFT = ((RES_X / 2) - (PIC_W / 2)); static const screen_y_t PIC_TOP = 64; static const screen_x_t PIC_RIGHT = (PIC_LEFT + PIC_W); static const screen_x_t PIC_BOTTOM = (PIC_TOP + PIC_H); static const vram_byte_amount_t PIC_VRAM_W = (PIC_W / BYTE_DOTS); static const int PIC_SLOT = 0; // Note that this does not correspond to the tiled area painted into TH05's // EDBK?.PI images. static const screen_x_t BOX_LEFT = 80; static const screen_y_t BOX_TOP = 320; static const pixel_t BOX_W = 480; static const pixel_t BOX_H = (GLYPH_H * 4); static const vram_byte_amount_t BOX_VRAM_W = (BOX_W / BYTE_DOTS); static const screen_x_t BOX_RIGHT = (BOX_LEFT + BOX_W); static const screen_y_t BOX_BOTTOM = (BOX_TOP + BOX_H); static const shiftjis_ank_amount_t NAME_LEN = 6; static const shiftjis_kanji_amount_t NAME_KANJI_LEN = ( NAME_LEN / sizeof(shiftjis_kanji_t) ); // Adding a fullwidth colon after the name static const pixel_t NAME_W = ((NAME_LEN * GLYPH_HALF_W) + GLYPH_FULL_W); static const int TEXT_INTERVAL_DEFAULT = ((GAME == 5) ? 2 : 1); // --------- // State // ----- #if (GAME >= 4) // Statically allocated. MODDERS: TH03's dynamic allocation was better than // hardcoding a maximum size... extern unsigned char script[8192]; extern unsigned char near *script_p; #else // Dynamically allocated. extern unsigned char far *script; #define script_p script #endif extern Planar* box_bg; // Skips any delays during the cutscene if `true`. extern bool fast_forward; // [y] is always aligned to GLYPH_H pixels. extern screen_point_t cursor; extern int text_interval; extern uint8_t text_col; extern uint8_t text_fx; // TH04 and TH05 directly set [graph_putsa_fx_func]. extern int script_number_param_default; #if (GAME >= 4) #define text_fx graph_putsa_fx_func #endif // ----- #if (GAME == 5) // String-to-color map // ------------------- // Used to automatically change the text color whenever a specific // Shift-JIS code point is encountered. static const int COLMAP_COUNT = 8; typedef struct { uint4_t values[COLMAP_COUNT]; // Might have been originally meant for a complete character name? ShiftJISKanji keys[COLMAP_COUNT][NAME_KANJI_LEN]; } colmap_t; extern colmap_t colmap; extern unsigned char colmap_count; // ------------------- #endif // Function ordering fails // ----------------------- #if (GAME == 5) // Waits the given amount of frames (0 = forever) for the OK, Shot, or // Cancel button to be pressed, while showing the ⏎ Return key animation // in a blocking way. void pascal near box_wait_animate(int frames_to_wait = 0); #else #define box_wait_animate(frames_to_wait) { \ input_wait_for_change(frames_to_wait); \ } void near box_bg_allocate_and_snap(void); void near box_bg_free(void); #endif #if (GAME >= 4) // Crossfades the text box area from VRAM page 1 to VRAM page 0, spending // [text_interval] frames on each step. void near box_1_to_0_animate(void); #endif // ----------------------- // ZUN quirk: The cutscene system features both // 1) a top-level input sensing mechanism (for updating the fast-forward flag), // and // 2) nested, blocking input loops (during all the interruptable wait commands) // which are skipped based on the fast-forward flag. // With this combination, the accurate detection of held keys matters: Since // this function is only called once on every iteration of the loop before // evaluating a command, a momentary key release scancode from 1) can cause 2) // to be entered even if the fast-forward key is still being held. Only TH03's // and TH05's input functions defend against this possibility – at the cost of // 614.4 µs for every call to them. TH04's cutscene system does suffer from the // detection issue, but runs significantly faster in exchange, as it's not // slowed down on every iteration of the script interpreter loop, i.e., between // every script command or 2-byte text pair. inline void cutscene_input_sense(void) { #if (GAME == 5) input_reset_sense_held(); #elif (GAME == 4) input_reset_sense(); #elif (GAME == 3) input_mode_interface(); #endif } bool16 pascal near cutscene_script_load(const char* fn) { cutscene_script_free(); if(!file_ropen(fn)) { return true; } size_t size = file_size(); #if (GAME >= 4) // PORTERS: Required for TH03 on flat memory models as well. // ZUN landmine: Missing an error check if [size] >= sizeof(script); script_p = static_cast(script); #else script = reinterpret_cast(hmem_allocbyte(size)); #endif file_read(script_p, size); file_close(); return false; } #if (GAME <= 4) void near cutscene_script_free(void) { #if (GAME == 3) if(script) { HMem::free(script); script = nullptr; } #endif } #endif // ZUN bloat: Turn into a single global inline function. extern "C" { #define egc_start_copy near egc_start_copy #include "th01/hardware/egcstart.cpp" #undef egc_start_copy } // Picture crossfading works by doing a masked blit of the new picture on top // of the old one on the invisible VRAM page, then blitting the result to the // visible page using an EGC inter-page copy. While the latter is notoriously // slow (see TH01's end_pic_show() for more info), blitting a packed-pixel // format from RAM to VRAM is even worse on PC-98: At best, the inner 8-pixel // XLOOP of master.lib's graph_pack_put_8() takes 81 cycles on a 486 and 141 // cycles on a 386, and these are just the raw instruction timings without any // of the PC-98's infamous VRAM latencies factored in. On lower-end systems, // this can easily sum up to more than one frame for a 320×200 image. In that // light, it's understandable why ZUN first builds the final masked image on // the invisible VRAM plane and then uses a *relatively* quick EGC copy to // display it. // (Flipping the visible page on every picture change would have only made // everything even more complicated.) #if (GAME == 5) #define pic_copy_to_other(left, top) { \ egc_copy_rect_1_to_0_16(left, top, PIC_W, PIC_H); \ } void pascal near pic_put_both_masked( screen_x_t left, vram_y_t top, int quarter, int mask_id ) { graph_accesspage(1); pi_put_quarter_masked_8(left, top, PIC_SLOT, quarter, mask_id); pic_copy_to_other(left, top); } #else void pascal near pic_copy_to_other(screen_x_t left, vram_y_t top) { vram_offset_t vo = vram_offset_shift(left, top); pixel_t y; vram_byte_amount_t vram_x; egc_start_copy(); // Faster than TH01's very slow end_pic_show() version, but still not // as optimal as you can get within the EGC's limited 16-dot tile // register. y = 0; while(y < PIC_H) { vram_x = 0; while(vram_x < PIC_VRAM_W) { egc_temp_t tmp; // ZUN bloat: Remember that the call site temporarily switched // the visible page to page 1, and blitted the image to page 0. graph_accesspage(0); tmp = egc_chunk(vo); graph_accesspage(1); egc_chunk(vo) = tmp; vram_x += EGC_REGISTER_SIZE; vo += EGC_REGISTER_SIZE; } y++; vo += (ROW_SIZE - PIC_VRAM_W); } egc_off(); // ZUN bloat: All blitting operations in this module access the // intended page before they blit. That's why preliminary state // changes like this one are completely redundant, thankfully. graph_accesspage(0); } void pascal near pic_put_both_masked( screen_x_t left, vram_y_t top, int quarter, int mask_id ) { enum { TEMP_ROW = RES_Y, }; vram_word_amount_t vram_word; vram_offset_t vo_temp; pi_buffer_p_t row_p; pi_buffer_p_init_quarter(row_p, PIC_SLOT, quarter); // ZUN bloat: See the call site. graph_showpage(1); vram_offset_t vo = vram_offset_shift(left, top); graph_accesspage(0); for(pixel_t y = 0; y < PIC_H; y++) { // This might actually be faster than clearing the masked pixels // using the GRCG and doing an unaccelerated 4-plane VRAM OR. graph_pack_put_8_noclip(0, TEMP_ROW, row_p, PIC_W); egc_start_copy(); egc_setup_copy_masked(PI_MASKS[mask_id][y & (PI_MASK_COUNT - 1)]); vo_temp = vram_offset_shift(0, TEMP_ROW); vram_word = 0; while(vram_word < (PIC_W / EGC_REGISTER_DOTS)) { egc_chunk(vo) = egc_chunk(vo_temp); vram_word++; vo += EGC_REGISTER_SIZE; vo_temp += EGC_REGISTER_SIZE; } egc_off(); vo += (ROW_SIZE - PIC_VRAM_W); pi_buffer_p_offset(row_p, PI_W, 0); pi_buffer_p_normalize(row_p); } graph_showpage(0); pic_copy_to_other(left, top); } #define box_bg_snap_func(p, vo) { \ (&box_bg->B)[p++] = VRAM_CHUNK(B, vo, 16); \ (&box_bg->B)[p++] = VRAM_CHUNK(R, vo, 16); \ (&box_bg->B)[p++] = VRAM_CHUNK(G, vo, 16); \ (&box_bg->B)[p++] = VRAM_CHUNK(E, vo, 16); \ } #define box_bg_put_func(p, vo) { \ VRAM_CHUNK(B, vo, 16) = (&box_bg->B)[p++]; \ VRAM_CHUNK(R, vo, 16) = (&box_bg->B)[p++]; \ VRAM_CHUNK(G, vo, 16) = (&box_bg->B)[p++]; \ VRAM_CHUNK(E, vo, 16) = (&box_bg->B)[p++]; \ } #define box_bg_loop(func) /* No braces due to local variable layout */ \ size_t p; \ vram_y_t src_y; \ screen_x_t src_x; \ pixel_t dst_y; \ vram_byte_amount_t dst_byte; \ vram_offset_t vo; \ \ p = 0; \ src_y = BOX_TOP; \ dst_y = 0; \ while(dst_y < BOX_H) { \ src_x = BOX_LEFT; \ dst_byte = 0; \ while(dst_byte < BOX_VRAM_W) { \ vo = vram_offset_shift(src_x, src_y); \ func(p, vo); \ dst_byte += static_cast(sizeof(dots16_t)); \ src_x += 16; \ } \ dst_y++; \ src_y++; \ } void near box_bg_allocate_and_snap(void) { box_bg_free(); graph_accesspage(0); box_bg = HMem< Planar >::alloc( ((BOX_VRAM_W * BOX_H) / sizeof(dots16_t)) ); box_bg_loop(box_bg_snap_func); } void near box_bg_free(void) { if(box_bg) { HMem< Planar >::free(box_bg); box_bg = nullptr; } } void near box_bg_put(void) { box_bg_loop(box_bg_put_func); } #endif #define script_fn_param_read(ret, len, temp_c) { \ str_consume_control_or_space_separated_string( \ ret, len, script_p, PF_FN_LEN, temp_c \ ); \ } void pascal near script_number_param_read_first(int& ret) { str_consume_up_to_3_digits(&ret, script_p, script_number_param_default); } inline void script_number_param_read_first(int& ret, int default_value) { script_number_param_default = default_value; script_number_param_read_first(ret); } void pascal near script_number_param_read_second(int& ret) { if(*script_p == ',') { script_p++; script_number_param_read_first(ret); } else { ret = script_number_param_default; } } void near cursor_advance_and_animate(void) { cursor.x += GLYPH_FULL_W; if(cursor.x >= BOX_RIGHT) { cursor.y += GLYPH_H; cursor.x = (BOX_LEFT + NAME_W); if(cursor.y >= BOX_BOTTOM) { #if (GAME >= 4) box_1_to_0_animate(); #endif // ZUN quirk: Since [cursor.y] is >= BOX_BOTTOM here, the TH05 // Return key animation will be displayed at the right edge of the // "5th" line, below BOX_BOTTOM. // (Same issue as with the \n command.) if(!fast_forward) { box_wait_animate(0); } // Unconditionally moving the cursor into the name area? This is // not what a script author would expect, especially with automatic // line breaks doing the opposite. If you are forced to write your // script in a way that anticipates such a cursor move, you might // as well explicitly add the necessary text box change commands // manually. cursor.x = BOX_LEFT; cursor.y = BOX_TOP; // ZUN landmine: The \s command is the only place in TH05 where text // is unblitted from any page. Not doing it here completely breaks // automatically added text boxes in that game, as any new text // will just be blitted on top of old text. // (Not that the feature would have been particularly usable // without this bug, thanks to the assignments above…) #if (GAME != 5) graph_accesspage(1); box_bg_put(); graph_accesspage(0); box_bg_put(); #endif } } } #if (GAME >= 4) typedef enum { BOX_MASK_0, BOX_MASK_1, BOX_MASK_2, BOX_MASK_3, BOX_MASK_COPY, BOX_MASK_COUNT, _box_mask_t_FORCE_UINT16 = 0xFFFF } box_mask_t; // Copies the text box area from VRAM page 1 to VRAM page 0, applying the // given [mask]. Assumes that the EGC is active, and initialized for a copy. void pascal near box_1_to_0_masked(box_mask_t mask) { extern const dot_rect_t(16, 4) BOX_MASKS[BOX_MASK_COUNT]; egc_temp_t tmp; for(screen_y_t y = BOX_TOP; y < BOX_BOTTOM; y++) { egc_setup_copy_masked(BOX_MASKS[mask][y & 3]); vram_offset_t vram_offset = vram_offset_shift(BOX_LEFT, y); pixel_t x = 0; while(x < BOX_W) { graph_accesspage(1); tmp = egc_chunk(vram_offset); graph_accesspage(0); egc_chunk(vram_offset) = tmp; x += EGC_REGISTER_DOTS; vram_offset += EGC_REGISTER_SIZE; } } } void near box_1_to_0_animate(void) { // ZUN bloat: box_1_to_0_masked() switches the accessed page anyway. #if (GAME == 5) graph_accesspage(0); #endif egc_start_copy(); if(!fast_forward) { for(int i = BOX_MASK_0; i < BOX_MASK_COPY; i++) { box_1_to_0_masked(static_cast(i)); frame_delay(text_interval); } } box_1_to_0_masked(BOX_MASK_COPY); egc_off(); #if (GAME == 5) frame_delay(1); // ZUN quirk #endif } #endif #if (GAME == 5) void pascal near box_wait_animate(int frames_to_wait) { enum { LEFT = (BOX_RIGHT + GLYPH_FULL_W), }; unsigned int frames_waited = 0; bool16 ignore_frames = false; while(1) { cutscene_input_sense(); if(key_det == INPUT_NONE) { break; } frame_delay(1); } if(frames_to_wait == 0) { frames_to_wait = 999; ignore_frames = true; } graph_accesspage(0); while(1) { cutscene_input_sense(); // ZUN bloat: A white glyph aligned to the 8×16 cell grid, without // applying boldface… why not just show it on TRAM? bgimage_put_rect(LEFT, cursor.y, GLYPH_FULL_W, GLYPH_H); if( (frames_to_wait <= 0) || (key_det & INPUT_OK) || (key_det & INPUT_SHOT) || (key_det & INPUT_CANCEL) ) { return; } graph_gaiji_putc( LEFT, cursor.y, (ga_RETURN_KEY + ((frames_waited / 8) & (RETURN_KEY_CELS - 1))), V_WHITE ); frames_waited++; if(!ignore_frames) { frames_to_wait--; } frame_delay(1); } } #endif enum script_ret_t { CONTINUE = 0, STOP = -1, }; // Called with [script_p] at the character past [c]. script_ret_t pascal near script_op(unsigned char c) { int i; int p1; int p2; // ZUN bloat: PF_FN_LEN on its own is enough, it already includes the \0 // terminator. char fn[PF_FN_LEN + 3]; c = tolower(c); switch(c) { case 'n': cursor.y += GLYPH_H; cursor.x = BOX_LEFT; if(cursor.y < BOX_BOTTOM) { break; } // ZUN quirk: Since [cursor.y] is >= BOX_BOTTOM here, the TH05 Return // key animation will be displayed at the right edge of the "5th" line, // below BOX_BOTTOM. (Same issue as in cursor_advance_and_animate().) // fallthrough to \s if this box is full case 's': c = *script_p; #if (GAME >= 4) box_1_to_0_animate(); #endif if(c != '-') { script_number_param_read_first(p1, 0); if(!fast_forward) { box_wait_animate(p1); } } else { script_p++; } cursor.x = BOX_LEFT; cursor.y = BOX_TOP; #if (GAME == 5) graph_accesspage(1); bgimage_put_rect(BOX_LEFT, BOX_TOP, BOX_W, BOX_H); // ZUN bloat: All blitting operations in this module access the // intended page before they blit. That's why preliminary state // changes like this one are completely redundant, thankfully. graph_accesspage(0); #else // High-level overview, point 2) graph_accesspage(1); box_bg_put(); graph_accesspage(0); box_bg_put(); #endif break; case 'c': #if (GAME == 5) // ZUN bloat: Doesn't matter for either '=' or digits. c = tolower(*script_p); if(c == '=') { goto colmap_add; } #endif script_number_param_read_first(p1, V_WHITE); text_col = p1; break; case 'b': script_number_param_read_first(p1, WEIGHT_BOLD); #if (GAME >= 4) graph_putsa_fx_func = static_cast(p1); #else switch(p1) { case WEIGHT_NORMAL: text_fx = FX_WEIGHT_NORMAL; break; case WEIGHT_HEAVY: text_fx = FX_WEIGHT_HEAVY; break; case WEIGHT_BOLD: text_fx = FX_WEIGHT_BOLD; break; case WEIGHT_BLACK: text_fx = FX_WEIGHT_BLACK; break; } #endif break; case 'w': c = tolower(*script_p); if((c == 'o') || (c == 'i')) { script_p++; script_number_param_read_first(p1, 1); if(c == 'i') { palette_white_in(p1); #if (GAME == 5) // ZUN bloat: `break` or `return`, pick one! return CONTINUE; #endif } else { palette_white_out(p1); #if (GAME == 5) // ZUN bloat: `break` or `return`, pick one! return CONTINUE; #endif } #if (GAME <= 4) break; #endif } #if (GAME >= 4) box_1_to_0_animate(); #endif script_number_param_default = 64; if(c != 'm') { if(c == 'k') { script_p++; } script_number_param_read_first(p1); if(!fast_forward) { #if (GAME >= 4) frame_delay(p1); #else if(c != 'k') { frame_delay(p1); } else { input_wait_for_ok(p1); } #endif #if (GAME == 5) // ZUN bloat return CONTINUE; #endif } } else { script_p++; c = *script_p; if(c == 'k') { script_p++; } script_number_param_read_first(p1); script_number_param_read_second(p2); if(!fast_forward) { // ZUN landmine: Does not prevent the potential deadlock issue // with this function. #if (GAME >= 4) snd_delay_until_measure(p1, p2); #else if(c != 'k') { snd_delay_until_measure(p1, p2); } else { input_wait_for_ok_or_measure(p1, p2); } #endif } } break; case 'v': if(*script_p != 'p') { script_number_param_read_first(p1, TEXT_INTERVAL_DEFAULT); text_interval = p1; } else { script_p++; script_number_param_read_first(p1, 0); graph_showpage(p1); } break; case 't': script_number_param_read_first(p1, 100); if(!fast_forward) { frame_delay(1); } palette_settone(p1); break; case 'f': c = *script_p; if(c != 'm') { if((c == 'i') || (c == 'o')) { script_p++; script_number_param_read_first(p1, 1); if(c == 'i') { palette_black_in(p1); #if (GAME == 5) // ZUN bloat: `break` or `return`, pick one! return CONTINUE; #endif } else { palette_black_out(p1); #if (GAME == 5) // ZUN bloat: `break` or `return`, pick one! return CONTINUE; #endif } } } else { script_p++; script_number_param_read_first(p1, 1); snd_kaja_func(KAJA_SONG_FADE, p1); #if (GAME <= 4) // ZUN bloat: `break` or `return`, pick one! return CONTINUE; #endif } break; case 'g': if((GAME == 5) || (*script_p != 'a')) { script_number_param_read_first(p1, 8); for(p2 = 0; p2 <= p1; p2++) { if(p2 & 1) { graph_scrollup(4); } else { graph_scrollup(RES_Y - 4); } if(!fast_forward) { frame_delay(1); } } graph_scrollup(0); } else { script_p++; script_number_param_read_first(p1, 0); graph_accesspage(1); #if (GAME == 3) // Might look like a ZUN bug, but actually works around the // master.lib bug mentioned in the comment of this function, // which ZUN only fixed for TH04 and TH05. In the original // binary, the ASCII→digit conversion inside // str_consume_up_to_3_digits() is done by ADDing a negative // number, which causes the x86 carry flag to always be set // when we get here and haven't fallen back onto the default // value. Therefore, the bug will always add 1 onto the gaiji // ID, which can be worked around by subtracting 1 from ID // before passing it as a parameter. Once graph_gaiji_putc() // returns, the carry flag happens to be cleared, which is // why the subtraction is not necessary for the call below to // display the intended gaiji. graph_gaiji_putc(cursor.x, cursor.y, (p1 - 1), text_col); graph_accesspage(0); #endif // [text_fx] is also ignored here... graph_gaiji_putc(cursor.x, cursor.y, p1, text_col); // ZUN quirk: No [text_interval]-based delay in TH03. cursor_advance_and_animate(); } return CONTINUE; case 'k': // ZUN landmine: Should have also been done in TH04. Without this call, // this command will wait on an invisible text box, and needs to be // preceded by a \vp1 command to actually work as a mid-box pause. #if (GAME == 5) box_1_to_0_animate(); #endif script_number_param_read_first(p1, 0); if(!fast_forward) { // ZUN quirk: This parameter is ignored in TH03. Labeling this as a // quirk because the original TH03 scripts call this command with a // non-0 parameter in 19 of 34 cases, suggesting that ZUN made the // conscious decision to override these parameters with 0 later in // development. box_wait_animate((GAME >= 4) ? p1 : 0); } return CONTINUE; case '@': graph_accesspage(1); graph_clear(); graph_accesspage(0); graph_clear(); #if (GAME == 5) bgimage_snap(); #else // ZUN landmine: Missing a box_bg_allocate_and_snap() or equivalent // call. Any future box_bg_put() call will still display the box // area snapped from any previously displayed background image. // This bug therefore effectively restricts usage of this command // to either the beginning of a script (before the first background // image is shown) or its end (after no more new text boxes are // started). #endif break; case 'p': c = *script_p; script_p++; if((c == '=') || (c == '@')) { graph_accesspage(1); if(c == '=') { pi_palette_apply(PIC_SLOT); } pi_put_8(0, 0, PIC_SLOT); graph_copy_page(0); graph_accesspage(0); #if (GAME == 5) bgimage_snap(); #else box_bg_allocate_and_snap(); #endif } else if(c == '-') { pi_free(PIC_SLOT); return CONTINUE; } else if(c == 'p') { pi_palette_apply(PIC_SLOT); return CONTINUE; } else if(c != ',') { script_p--; } else { script_fn_param_read(fn, p1, c); #if (GAME >= 4) pi_free(PIC_SLOT); #endif pi_load(PIC_SLOT, fn); } break; case '=': script_number_param_default = PI_QUARTER_COUNT; c = *script_p; if(c != '=') { script_number_param_read_first(p1); #if (GAME == 5) frame_delay(1); // ZUN quirk graph_showpage(0); graph_accesspage(1); #else // ZUN bloat: Why did ZUN temporarily switch foreground and // background pages for the duration of this command?! Not only // is it completely unnecessary, it's also downright harmful. // If these lines didn't exist, the entire cutscene system // would have been both much easier to understand *and* more // performant: // // • The intent for both VRAM pages would have been crystal // clear: Page 0 is always shown and contains the actively // displayed picture and text, and page 1 is used for // temporarily storing pixels that are later crossfaded onto // page 0. Through their mere existence, these lines suggest // a more complex interplay between the two pages, which // doesn't actually exist. // • TH03 wouldn't have needed to render text and gaiji to both // VRAM pages. // • (Technically, TH03 wouldn't have even needed [box_bg] as a // result, but that was a decent investment regardless – // inter-page blitting is horribly slow no matter how you do // it. Also, this buffer does become necessary in TH04 – see // point 2) in the high-level overview) // • If \vp didn't exist (it's not used by the original scripts // anyway), the entire system would have only needed a single // graph_showpage(0) call at the start of cutscene_animate(). // // ZUN landmine: Since TH04 renders text to VRAM page 1, // calling \= or \== in the middle of a string of text // temporarily shows any text rendered since the last // box_1_to_0_animate() call if any of the following blit // operations spends more than one frame with page 1 visible. // In practice, this only happens on very underclocked systems // far below the game's target of 66 MHz, but it's a landmine // nonetheless. graph_showpage(1); graph_accesspage(0); #endif if(p1 < PI_QUARTER_COUNT) { pi_put_quarter_8(PIC_LEFT, PIC_TOP, PIC_SLOT, p1); } else { grcg_setcolor(GC_RMW, 0); grcg_boxfill_8( PIC_LEFT, PIC_TOP, (PIC_RIGHT - 1), (PIC_BOTTOM - 1) ); grcg_off(); } } else { script_p++; script_number_param_read_first(p1); script_number_param_default = 1; script_number_param_read_second(p2); for(i = 0; i < PI_MASK_COUNT; i++) { pic_put_both_masked(PIC_LEFT, PIC_TOP, p1, i); if(!fast_forward) { frame_delay(p2); } } #if (GAME == 5) graph_accesspage(1); pi_put_quarter_8(PIC_LEFT, PIC_TOP, PIC_SLOT, p1); frame_delay(1); // ZUN quirk #else // ZUN bloat: See above. // ZUN landmine: See above. graph_showpage(1); graph_accesspage(0); pi_put_quarter_8(PIC_LEFT, PIC_TOP, PIC_SLOT, p1); #endif } #if (GAME <= 4) graph_showpage(0); // ZUN bloat: See above. #endif pic_copy_to_other(PIC_LEFT, PIC_TOP); break; case 'm': c = *script_p; if(c == '$') { script_p++; snd_kaja_func(KAJA_SONG_STOP, 0); return CONTINUE; } else if(c == '*') { script_p++; snd_kaja_func(KAJA_SONG_PLAY, 0); return CONTINUE; } if(c == ',') { script_p++; script_fn_param_read(fn, p1, c); snd_kaja_func(KAJA_SONG_STOP, 0); snd_load(fn, SND_LOAD_SONG); snd_kaja_func(KAJA_SONG_PLAY, 0); } break; case 'e': script_number_param_read_first(p1); snd_se_play_force(p1); break; #if (GAME == 5) colmap_add: script_p++; colmap.keys[colmap_count][0].byte[0] = *script_p; script_p++; colmap.keys[colmap_count][0].byte[1] = *script_p; // ZUN landmine: Jumps over the additional comma separating the two // parameters, and assumes it's always present. Come on! // script_number_param_read_second() exists to handle exactly this // situation in a cleaner way. script_p += 2; script_number_param_read_first(p1, V_WHITE); // ZUN landmine: No bounds check colmap.values[colmap_count] = p1; colmap_count++; break; #endif case '$': return STOP; } return CONTINUE; } void near cutscene_animate(void) { extern ShiftJISKanji near CUTSCENE_KANJI[]; #if (GAME == 5) int gaiji; #endif unsigned char c; uint8_t speedup_cycle; ShiftJISKanji& kanji = *CUTSCENE_KANJI; cursor.x = BOX_LEFT; cursor.y = BOX_TOP; text_interval = TEXT_INTERVAL_DEFAULT; text_col = V_WHITE; text_fx = FX_WEIGHT_BOLD; #if (GAME == 3) speedup_cycle = 0; #endif // Necessary because scripts can (and do) show multiple text boxes on the // initially black background. // ZUN landmine: TH05 assumes that they don't, which is true for all // scripts in the original game. #if (GAME != 5) box_bg_allocate_and_snap(); #endif fast_forward = false; while(1) { cutscene_input_sense(); if(key_det & INPUT_CANCEL) { fast_forward = true; } else { fast_forward = false; } #if (GAME == 5) // ZUN bloat: Should be part of the colmap loop. int i = 0; #endif c = *(script_p++); if(str_sep_control_or_space(c)) { continue; } // Opcode? if(c == '\\') { c = *(script_p++); if(script_op(c) == STOP) { break; } continue; } #if (GAME == 5) if(c == '@') { c = tolower(*script_p); script_p++; switch(c) { case 't': gaiji = gs_SWEAT; break; case 'h': gaiji = gs_HEART_2; break; case '?': gaiji = gs_QUESTION; break; case '!': c = *(script_p++); switch(c) { case '!': gaiji = gs_DOUBLE_EXCLAMATION; break; case '?': gaiji = gs_EXCLAMATION_QUESTION; break; default: script_p--; gaiji = gs_EXCLAMATION; break; } break; default: script_p--; script_number_param_read_first(gaiji, gs_NOTES); break; } graph_showpage(0); graph_accesspage(1); // Still ignoring [text_fx]. graph_gaiji_putc(cursor.x, cursor.y, gaiji, text_col); cursor_advance_and_animate(); i = 1; // ZUN bloat continue; } #endif // Regular kanji kanji.byte[0] = c; c = *script_p; kanji.byte[1] = c; script_p++; #if (GAME == 5) if(cursor.x == BOX_LEFT) { for(i = 0; i < colmap_count; i++) { if(colmap.keys[i][0].t == kanji.t) { text_col = colmap.values[i]; break; } } } #endif #if (GAME >= 4) graph_showpage(0); graph_accesspage(1); graph_putsa_fx(cursor.x, cursor.y, text_col, kanji.byte); // ZUN bloat: All blitting operations in this module access the // intended page before they blit. That's why preliminary state // changes like this one are completely redundant, thankfully. #if (GAME == 5) graph_accesspage(0); #endif #else graph_accesspage(1); graph_putsa_fx( cursor.x, cursor.y, (text_col | text_fx), kanji.byte ); graph_accesspage(0); graph_putsa_fx( cursor.x, cursor.y, (text_col | text_fx), kanji.byte ); #endif cursor_advance_and_animate(); #if (GAME == 5) i = 1; // ZUN bloat #endif // High-level overview, point 3) #if (GAME == 3) if(fast_forward) { continue; } if(key_det == INPUT_NONE) { frame_delay(text_interval); } else { int speedup_interval = (text_interval / 3); if((speedup_cycle & 1) || speedup_interval) { if(speedup_interval == 0) { speedup_interval++; } frame_delay(speedup_interval); } speedup_cycle++; } #endif } #if (GAME == 5) bgimage_free(); pi_free(PIC_SLOT); #else box_bg_put(); box_bg_free(); #endif }