mirror of https://github.com/nmlgc/ReC98.git
389 lines
11 KiB
C++
389 lines
11 KiB
C++
#pragma option -zPmain_01
|
|
|
|
// Function ordering fails
|
|
// -----------------------
|
|
|
|
// Replaces the super_*() sprites loaded from MIKO16.BFT with the ones from
|
|
// ST06_16.BFT used in the EX-Alice fight, in addition to clearing all
|
|
// stage-specific sprites.
|
|
void near main_pat_exalice_override_and_super_clean_stage(void);
|
|
// -----------------------
|
|
|
|
#include <string.h>
|
|
#include "th02/main/execl.hpp"
|
|
#include "th04/main/dialog/shared.cpp"
|
|
#include "th04/main/demo.hpp"
|
|
#include "th04/main/ems.hpp"
|
|
#include "th04/score.h"
|
|
#include "th05/mem.h"
|
|
#include "th05/playchar.h"
|
|
#include "th05/resident.hpp"
|
|
#include "th05/formats/dialog.hpp"
|
|
#include "th05/main/dialog/dialog.hpp"
|
|
#include "th05/sprites/main_pat.h"
|
|
#include "th05/shiftjis/fns.hpp"
|
|
|
|
#pragma option -a2
|
|
|
|
// Called with [dialog_p] at the character past [c]. Returns, um… whether the
|
|
// corresponding operation is allowed to be used while printing a box of text?
|
|
// ZUN landmine: This should have only returned `false` for values of [c] that
|
|
// don't correspond to commands. As a result, the `true`-returning gaiji and
|
|
// line break commands are the only ones that can be used within the box loop.
|
|
// For all other commands, the box loop will then also interpret [c] as a
|
|
// Shift-JIS lead byte and write it to TRAM, followed by what's next in
|
|
// [dialog_p] after this function returned. The text chip will then render [c]
|
|
// using the control character glyphs from the PC-98 font ROM.
|
|
bool pascal near dialog_op(uint8_t c)
|
|
{
|
|
screen_x_t left;
|
|
screen_y_t top;
|
|
const unsigned char* fn;
|
|
|
|
switch(c) {
|
|
case 0x00:
|
|
dialog_cursor.x = to_dialog_x(TEXT_PLAYCHAR_LEFT);
|
|
dialog_cursor.y = to_dialog_y(TEXT_PLAYCHAR_TOP);
|
|
dialog_side = SIDE_PLAYCHAR;
|
|
break;
|
|
|
|
case 0x01:
|
|
dialog_cursor.x = to_dialog_x(TEXT_BOSS_LEFT);
|
|
dialog_cursor.y = to_dialog_y(TEXT_BOSS_TOP);
|
|
dialog_side = SIDE_BOSS;
|
|
break;
|
|
|
|
case 0x02:
|
|
c = *(dialog_p++);
|
|
|
|
// ZUN quirk: dialog_face_load_unput_put_free_8() already has a 1-frame
|
|
// delay.
|
|
frame_delay(1);
|
|
|
|
if(dialog_side == SIDE_PLAYCHAR) {
|
|
dialog_face_load_unput_put_free_8(
|
|
FACE_PLAYCHAR_LEFT, FACE_PLAYCHAR_TOP, c
|
|
);
|
|
} else {
|
|
dialog_face_load_unput_put_free_8(FACE_BOSS_LEFT, FACE_BOSS_TOP, c);
|
|
}
|
|
break;
|
|
|
|
case 0x03:
|
|
// ZUN landmine: See the comment about manual sprite memory management
|
|
// for dialog_op_super_clean_stage().
|
|
fn = reinterpret_cast<const unsigned char *>(dialog_p);
|
|
super_entry_bfnt(fn);
|
|
dialog_p += (strlen(fn) + 1);
|
|
break;
|
|
|
|
case 0x04:
|
|
if(stage_id != STAGE_EXTRA) {
|
|
dialog_op_super_clean_stage();
|
|
} else {
|
|
main_pat_exalice_override_and_super_clean_stage();
|
|
}
|
|
break;
|
|
|
|
case 0x05:
|
|
fn = reinterpret_cast<const unsigned char *>(dialog_p);
|
|
if(fn[0] == '$') {
|
|
snd_kaja_func(KAJA_SONG_STOP, 0);
|
|
} else {
|
|
// TH05's implementation of snd_load() automatically stops any
|
|
// playing BGM, so it's not necessary here.
|
|
snd_load(fn, SND_LOAD_SONG);
|
|
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
|
}
|
|
dialog_p += (strlen(fn) + 1);
|
|
break;
|
|
|
|
case 0x06:
|
|
left = reinterpret_cast<tx2_op_06_params *>(dialog_p)->left;
|
|
top = reinterpret_cast<tx2_op_06_params *>(dialog_p)->top;
|
|
c = reinterpret_cast<tx2_op_06_params *>(dialog_p)->patnum;
|
|
dialog_p += sizeof(tx2_op_06_params);
|
|
super_roll_put(left, top, c);
|
|
break;
|
|
|
|
case 0x09:
|
|
dialog_box_wipe(
|
|
to_dialog_x(TEXT_BOSS_LEFT), to_dialog_y(TEXT_BOSS_TOP)
|
|
);
|
|
dialog_box_wipe(
|
|
to_dialog_x(TEXT_PLAYCHAR_LEFT), to_dialog_y(TEXT_PLAYCHAR_TOP)
|
|
);
|
|
c = *(dialog_p++);
|
|
palette_white_out(c);
|
|
break;
|
|
|
|
case 0x0A:
|
|
c = *(dialog_p++);
|
|
palette_white_in(c);
|
|
break;
|
|
|
|
case 0x0B: // That's one after the ASCII \n! Boo!
|
|
dialog_op_linebreak();
|
|
return true;
|
|
|
|
case 0x0C:
|
|
c = *(dialog_p++);
|
|
dialog_op_gaiji(c);
|
|
return true;
|
|
|
|
case 0x0D:
|
|
return true;
|
|
|
|
case 0x0E:
|
|
dialog_box_wipe(dialog_cursor.x, dialog_cursor.y);
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void near dialog_run(void)
|
|
{
|
|
uint8_t c;
|
|
|
|
// ZUN bloat: An obvious remnant from the cutscene code introduced in TH03;
|
|
// unused here.
|
|
uint8_t speedup_cycle;
|
|
|
|
extern shiftjis_t near dialog_kanji_buf[];
|
|
shiftjis_t* kanji_str = dialog_kanji_buf;
|
|
|
|
while(1) {
|
|
c = *(dialog_p++);
|
|
if(c == 0xFF) {
|
|
break;
|
|
} else if(c == 0x0D) {
|
|
// Start a new dialog box by clearing it and rewinding the cursor.
|
|
// TH04 features separate '0' or '1' commands that include the side
|
|
// in the command itself, whereas TH05 requires the script to use
|
|
// separate 0x00 or 0x01 commands before this one. Thankfully, the
|
|
// original scripts always call these before the first 0x0D command
|
|
// and don't rely on [dialog_side]'s initial zero value.
|
|
if(dialog_side == SIDE_PLAYCHAR) {
|
|
dialog_cursor.x = to_dialog_x(TEXT_PLAYCHAR_LEFT);
|
|
dialog_cursor.y = to_dialog_y(TEXT_PLAYCHAR_TOP);
|
|
} else {
|
|
dialog_cursor.x = to_dialog_x(TEXT_BOSS_LEFT);
|
|
dialog_cursor.y = to_dialog_y(TEXT_BOSS_TOP);
|
|
}
|
|
|
|
// Technically, this only needs to be done for the other box
|
|
// because the new one is immediately wiped below, but who cares.
|
|
text_boxfilla(
|
|
to_tram_x(to_dialog_x(TEXT_PLAYCHAR_LEFT)),
|
|
to_tram_y(to_dialog_y(TEXT_PLAYCHAR_TOP)),
|
|
to_tram_x(to_dialog_x(TEXT_PLAYCHAR_LEFT + TEXT_W)),
|
|
to_tram_y(to_dialog_y(TEXT_PLAYCHAR_TOP + BOX_H)),
|
|
TX_BLUE
|
|
);
|
|
text_boxfilla(
|
|
to_tram_x(to_dialog_x(TEXT_BOSS_LEFT)),
|
|
to_tram_y(to_dialog_y(TEXT_BOSS_TOP)),
|
|
to_tram_x(to_dialog_x(TEXT_BOSS_LEFT + TEXT_W)),
|
|
to_tram_y(to_dialog_y(TEXT_BOSS_TOP + BOX_H)),
|
|
TX_BLUE
|
|
);
|
|
|
|
dialog_box_wipe(dialog_cursor.x, dialog_cursor.y);
|
|
speedup_cycle = 0;
|
|
|
|
// Box loop
|
|
while(1) {
|
|
// ZUN bloat: This would have only been necessary before, or,
|
|
// better yet, inside dialog_delay(). It should have also been
|
|
// input_reset_sense_held() instead, which would have addressed
|
|
// the hardware quirk documented in the `Research/HOLDKEY`
|
|
// example. Y'know, just to ensure that held keys are always
|
|
// recognized as such, and don't cause a sporadic 2-frame delay
|
|
// before the text is displayed completely after all.
|
|
input_reset_sense();
|
|
|
|
c = *(dialog_p++);
|
|
// End of text box?
|
|
if(c == 0xFF) {
|
|
input_wait_for_change(0);
|
|
break;
|
|
} else if(!dialog_op(c)) {
|
|
// Regular kanji
|
|
kanji_str[0] = c;
|
|
kanji_str[1] = *(dialog_p++);
|
|
dialog_text_put(kanji_str);
|
|
|
|
// ZUN bug: Is this supposed to be the second input sensing
|
|
// call that addresses the aforementioned hardware quirk?
|
|
// In most cases, this won't ever change the value of
|
|
// [key_det] compared to the above call as it will take the
|
|
// target hardware of this game much less than the PC-98
|
|
// keyboard UART delay of ≈0.6ms to run from the above call
|
|
// to this one. This call can only ever make a difference
|
|
// if this box loop happens to run on a particularly
|
|
// unlucky UART cycle where the value *did* change in the
|
|
// meantime.
|
|
input_sense();
|
|
|
|
dialog_delay(speedup_cycle);
|
|
}
|
|
}
|
|
} else {
|
|
dialog_op(c);
|
|
}
|
|
}
|
|
overlay_wipe();
|
|
}
|
|
|
|
void dialog_animate(void)
|
|
{
|
|
// If we get here during a replay, we assume it's the hidden Extra Stage
|
|
// one because it's the only one that's long enough, and then skip the
|
|
// dialog.
|
|
// You might argue that this is gross and ZUN should have just added a
|
|
// proper skip feature that would still run through the script for its side
|
|
// effects, but the only bad aspect I can see is that this happens here and
|
|
// not at the call site. In regular gameplay, the (hardcoded) boss render
|
|
// functions rely on the dialog scripts for loading their sprites at the
|
|
// intended patnums, so it would have made much more sense to hardcode all
|
|
// of these load calls and completely remove the dependency on game data.
|
|
if(resident->demo_num != 0) {
|
|
extern int8_t dialog_sequence_id;
|
|
if(dialog_sequence_id == 0) {
|
|
// Pre-boss. Replicate all asset load calls that would be done if
|
|
// `_DM06.TX2` would run normally, ...
|
|
#undef BGM_EXTRA_BOSS_FN
|
|
#undef DEMO_EXTRA_PART_2_FN
|
|
extern const char st06_bb1[];
|
|
extern const char st06_bb2[];
|
|
extern const char BGM_EXTRA_BOSS_FN[];
|
|
extern const char DEMO_EXTRA_PART_2_FN[];
|
|
|
|
main_pat_exalice_override_and_super_clean_stage();
|
|
super_entry_bfnt(st06_bb1);
|
|
super_entry_bfnt(st06_bb2);
|
|
snd_load(BGM_EXTRA_BOSS_FN, SND_LOAD_SONG);
|
|
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
|
|
|
// ... then load the second half of the replay.
|
|
// ZUN bloat: This could have definitely been integrated into
|
|
// demo_load(), including the avoided reallocation.
|
|
file_ropen(DEMO_EXTRA_PART_2_FN);
|
|
file_read(DemoBuf, (DEMO_N_EXTRA * sizeof(REC<1>)));
|
|
file_close();
|
|
stage_frame = 0; // This is actually the index into [DemoBuf].
|
|
dialog_sequence_id++;
|
|
} else {
|
|
// Post-boss. Just return to the title screen.
|
|
#undef BINARY_OP
|
|
#define BINARY_OP DIALOG_BINARY_OP
|
|
extern const char BINARY_OP[];
|
|
demo_end();
|
|
}
|
|
return;
|
|
}
|
|
|
|
cdg_free(CDG_BG_PLAYCHAR_BOMB); // "dialog_init()"
|
|
dialog_pre();
|
|
dialog_run();
|
|
dialog_exit();
|
|
dialog_post();
|
|
}
|
|
|
|
extern char faceset_boss_format[];
|
|
extern char faceset_playchar_format[];
|
|
|
|
void pascal near dialog_face_load_unput_put_free_8(
|
|
screen_x_t left, vram_y_t top, int face_id
|
|
)
|
|
{
|
|
size_t size;
|
|
uint32_t offset;
|
|
char near *faceset_fn = faceset_boss_format;
|
|
char near *faceset_playchar_fn = faceset_playchar_format;
|
|
|
|
if(face_id != FACE_NONE) {
|
|
if(Ems) {
|
|
if(dialog_side == SIDE_PLAYCHAR) {
|
|
offset = EMS_FACESET_PLAYCHAR_OFFSET;
|
|
} else {
|
|
offset = EMS_FACESET_BOSS_OFFSET;
|
|
}
|
|
size = cdg_slots[CDG_DIALOG_FACE].bitplane_size;
|
|
offset += (face_id * size * (PLANE_COUNT + 1));
|
|
allocate_and_load_from_ems(
|
|
cdg_slots[CDG_DIALOG_FACE].seg_alpha(), offset, size
|
|
);
|
|
offset += size;
|
|
size *= PLANE_COUNT;
|
|
allocate_and_load_from_ems(
|
|
cdg_slots[CDG_DIALOG_FACE].seg_colors(), offset, size
|
|
);
|
|
offset += size;
|
|
} else {
|
|
if(dialog_side != SIDE_PLAYCHAR) {
|
|
faceset_fn[3] = ('0' + stage_id);
|
|
} else {
|
|
faceset_fn = faceset_playchar_fn;
|
|
faceset_fn[3] = ('0' + playchar);
|
|
}
|
|
cdg_load_single(CDG_DIALOG_FACE, faceset_fn, face_id);
|
|
}
|
|
}
|
|
frame_delay(1);
|
|
dialog_face_unput_8(left, top);
|
|
if(face_id != FACE_NONE) {
|
|
cdg_put_8(left, top, CDG_DIALOG_FACE);
|
|
cdg_free(CDG_DIALOG_FACE);
|
|
}
|
|
}
|
|
|
|
void near dialog_exit(void)
|
|
{
|
|
#undef BOMB_BG_REIMU_FN
|
|
#undef BOMB_BG_MARISA_FN
|
|
#undef BOMB_BG_MIMA_FN
|
|
#undef BOMB_BG_YUUKA_FN
|
|
extern const char BOMB_BG_REIMU_FN[];
|
|
extern const char BOMB_BG_MARISA_FN[];
|
|
extern const char BOMB_BG_MIMA_FN[];
|
|
extern const char BOMB_BG_YUUKA_FN[];
|
|
|
|
if(Ems) {
|
|
playchar_bomb_bg_load_from_ems();
|
|
} else {
|
|
switch(playchar) {
|
|
case PLAYCHAR_REIMU:
|
|
cdg_load_single_noalpha(CDG_BG_PLAYCHAR_BOMB, BOMB_BG_REIMU_FN, 0);
|
|
break;
|
|
case PLAYCHAR_MARISA:
|
|
cdg_load_single_noalpha(CDG_BG_PLAYCHAR_BOMB, BOMB_BG_MARISA_FN, 0);
|
|
break;
|
|
case PLAYCHAR_MIMA:
|
|
cdg_load_single_noalpha(CDG_BG_PLAYCHAR_BOMB, BOMB_BG_MIMA_FN, 0);
|
|
break;
|
|
case PLAYCHAR_YUUKA:
|
|
cdg_load_single_noalpha(CDG_BG_PLAYCHAR_BOMB, BOMB_BG_YUUKA_FN, 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void near main_pat_exalice_override_and_super_clean_stage(void)
|
|
{
|
|
#undef BOMB_SHAPE_FN
|
|
#undef BOMB_SHAPE_YUUKA_FN
|
|
#define BOMB_SHAPE_FN BOMB_SHAPE_FN_2
|
|
#define BOMB_SHAPE_YUUKA_FN BOMB_SHAPE_YUUKA_FN_2
|
|
extern const char BOMB_SHAPE_FN[];
|
|
extern const char BOMB_SHAPE_YUUKA_FN[];
|
|
extern const char MIKO16_EXALICE_FN[];
|
|
|
|
super_clean(TINY_MIKO16_START, PAT_MAX);
|
|
super_entry_bfnt(MIKO16_EXALICE_FN);
|
|
for(int i = TINY_MIKO16_START; i < TINY_MIKO16_END; i++) {
|
|
super_convert_tiny(i);
|
|
}
|
|
main_pat_bomb_shape_load();
|
|
}
|