mirror of https://github.com/nmlgc/ReC98.git
1081 lines
30 KiB
C++
1081 lines
30 KiB
C++
#if (GAME == 2)
|
||
#pragma option -2 // ZUN bloat
|
||
#endif
|
||
|
||
#include <mem.h>
|
||
#include "planar.h"
|
||
#include "libs/master.lib/master.hpp"
|
||
#include "game/coords.hpp"
|
||
#include "th02/v_colors.hpp"
|
||
#include "th02/hardware/frmdelay.h"
|
||
#include "th02/formats/musiccmt.hpp"
|
||
#if (GAME == 5)
|
||
#include "th05/hardware/input.h"
|
||
#elif (GAME == 4)
|
||
#include "th04/hardware/input.h"
|
||
#elif (GAME == 3)
|
||
#include "th03/hardware/input.h"
|
||
#else
|
||
#include "th02/hardware/input.hpp"
|
||
#endif
|
||
#if (GAME >= 4)
|
||
#include "th01/hardware/grcg.hpp"
|
||
#include "th04/hardware/bgimage.hpp"
|
||
#include "th04/hardware/grppsafx.h"
|
||
#include "th04/snd/snd.h"
|
||
#else
|
||
#include "th01/hardware/grppsafx.h"
|
||
#include "th02/snd/snd.h"
|
||
#endif
|
||
#if (GAME >= 3)
|
||
#include "th03/formats/cdg.h"
|
||
#include "th03/math/polar.hpp"
|
||
#else
|
||
#include "th01/math/polar.hpp"
|
||
#endif
|
||
#include "th02/op/m_music.hpp"
|
||
#if (GAME == 5)
|
||
#include "th01/math/clamp.hpp"
|
||
#include "th05/formats/pi.hpp"
|
||
#include "th05/op/piano.hpp"
|
||
#include "th05/shiftjis/fns.hpp"
|
||
#include "th05/shiftjis/music.hpp"
|
||
|
||
int game_sel = (GAME_COUNT - 1);
|
||
const int TRACK_COUNT[GAME_COUNT] = { 14, 18, 24, 28, 23 };
|
||
#else
|
||
#include "th02/formats/pi.h"
|
||
#if (GAME == 4)
|
||
#include "th04/shiftjis/music.hpp"
|
||
#elif (GAME == 3)
|
||
#include "th03/shiftjis/music.hpp"
|
||
#elif (GAME == 2)
|
||
#include "th02/shiftjis/music.hpp"
|
||
#endif
|
||
static const size_t TRACK_COUNT = (
|
||
sizeof(MUSIC_FILES) / sizeof(MUSIC_FILES[0])
|
||
);
|
||
#endif
|
||
|
||
// Colors
|
||
// ------
|
||
// The Music Room only redraws text whenever it changes, but fully redraws the
|
||
// B plane on every frame – first by blitting [nopoly_B], then by drawing the
|
||
// polygons on top. This places the following constraints on text colors:
|
||
// 1) Colors 0 or 1 can't be used, because those don't include any of the bits
|
||
// that can stay constant between frames.
|
||
// 2) The hardware palette colors should be identical regardless of the state
|
||
// of the lowest bit to make the text show up on top of the polygons. That
|
||
// is, if a color is 5 *or* 6, hardware colors 5 *and* 6 should be
|
||
// identical. (TH04 and TH05 rely on this.)
|
||
// 3) TH02, TH03, and TH04 populate [nopoly_B] *after* they've blitted the
|
||
// constant tracklist, thereby including all of the text pixels in the B
|
||
// plane. TH02 and TH03 also update [nopoly_B] with the B plane from VRAM
|
||
// every time they blit a new comment. This allows the tracklist (TH04)
|
||
// and/or comment colors (TH02/TH03/TH04) to simply be odd, even if 2) would
|
||
// not apply. In that case, that lowest/least significant bit will always be
|
||
// set when blitting back [nopoly_B], and the polygons can't affect it.
|
||
// Together with the other constant color bits, the text then appears on top
|
||
// of the polygons, and in the defined color. (TH02 and TH03 rely on this.)
|
||
|
||
static const vc_t COL_TRACKLIST_SELECTED = ((GAME >= 4) ? 3 : V_WHITE);
|
||
static const vc_t COL_TRACKLIST = ((GAME >= 4) ? 5 : 3);
|
||
static const vc_t COL_CMT_TRACK = ((GAME >= 4) ? 7 : V_WHITE);
|
||
static const vc_t COL_CMT_COMMENT = ((GAME >= 4) ? 7 : 13);
|
||
// ------
|
||
|
||
// Coordinates
|
||
// -----------
|
||
|
||
static const screen_x_t TRACKLIST_LEFT = ((GAME == 5) ? 12 : 16);
|
||
#if (GAME == 5)
|
||
static const int TRACKLIST_VISIBLE_COUNT = 12;
|
||
|
||
static const screen_y_t TRACKLIST_TOP = 96;
|
||
static const pixel_t TRACKLIST_H = (TRACKLIST_VISIBLE_COUNT * GLYPH_H);
|
||
|
||
static const screen_y_t LABEL_GAME_TOP = 32;
|
||
static const screen_y_t LABEL_UP_TOP = (TRACKLIST_TOP - GLYPH_H);
|
||
static const screen_y_t LABEL_DOWN_TOP = (TRACKLIST_TOP + TRACKLIST_H);
|
||
#else
|
||
inline screen_y_t track_top(uint8_t sel) {
|
||
#if (GAME == 4)
|
||
return (8 + (sel * GLYPH_H));
|
||
#elif (GAME == 3)
|
||
return (40 + (sel * GLYPH_H));
|
||
#elif (GAME == 2)
|
||
return ((static_cast<tram_y_t>(sel) + 6) * GLYPH_H);
|
||
#endif
|
||
}
|
||
|
||
inline int16_t track_fx(vc_t col) {
|
||
if(GAME >= 4) {
|
||
return col;
|
||
} else {
|
||
return (col | FX_WEIGHT_BOLD);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
static const pixel_t CMT_LINE_W = (CMT_LINE_LENGTH * GLYPH_HALF_W);
|
||
static const vram_byte_amount_t CMT_LINE_VRAM_W = (CMT_LINE_W / BYTE_DOTS);
|
||
static const pixel_t CMT_COMMENT_H = (CMT_COMMENT_LINES * GLYPH_H);
|
||
|
||
static const screen_x_t CMT_TITLE_LEFT = (
|
||
/**/ (GAME >= 3) ? (RES_X - GLYPH_FULL_W - CMT_LINE_W) :
|
||
/* (GAME == 2) */ ((RES_X / 2) - (CMT_LINE_W / 2))
|
||
);
|
||
static const screen_y_t CMT_TITLE_TOP = ((GAME == 5) ? 32 : 64);
|
||
static const screen_x_t CMT_TITLE_RIGHT = (CMT_TITLE_LEFT + CMT_LINE_W);
|
||
static const screen_y_t CMT_TITLE_BOTTOM = (CMT_TITLE_TOP + GLYPH_H);
|
||
|
||
static const screen_x_t CMT_COMMENT_LEFT = (RES_X - GLYPH_FULL_W - CMT_LINE_W);
|
||
#if (GAME == 5)
|
||
static const screen_y_t CMT_COMMENT_TOP = (PIANO_BOTTOM + 8);
|
||
#else
|
||
static const screen_y_t CMT_COMMENT_TOP = CMT_TITLE_BOTTOM;
|
||
#endif
|
||
static const screen_x_t CMT_COMMENT_RIGHT = (CMT_COMMENT_LEFT + CMT_LINE_W);
|
||
static const screen_x_t CMT_COMMENT_BOTTOM = (CMT_COMMENT_TOP + CMT_COMMENT_H);
|
||
// -----------
|
||
|
||
// ZUN bloat
|
||
#if (GAME == 5)
|
||
static int8_t unused[104];
|
||
#elif (GAME == 4)
|
||
static int8_t unused[56];
|
||
#endif
|
||
|
||
// Polygon state
|
||
// -------------
|
||
|
||
struct polygon_point_t {
|
||
pixel_t x;
|
||
Subpixel y;
|
||
};
|
||
|
||
static const unsigned int POLYGON_COUNT = 16;
|
||
|
||
// ZUN quirk: This has to be intentional.
|
||
static const unsigned int POLYGONS_RENDERED = (
|
||
(GAME == 5) ? (POLYGON_COUNT - 2) : POLYGON_COUNT
|
||
);
|
||
|
||
bool polygons_initialized = false;
|
||
|
||
// ZUN bloat: Could have been local to polygons_update_and_render().
|
||
static screen_point_t points[10];
|
||
|
||
static polygon_point_t center[POLYGON_COUNT];
|
||
static polygon_point_t velocity[POLYGON_COUNT];
|
||
static unsigned char angle[POLYGON_COUNT];
|
||
static unsigned char angle_speed[POLYGON_COUNT];
|
||
// -------------
|
||
|
||
// Selection state
|
||
// ---------------
|
||
|
||
#if (GAME <= 4)
|
||
uint8_t track_playing = 0;
|
||
#endif
|
||
uint8_t music_sel;
|
||
page_t music_page;
|
||
#if (GAME >= 4)
|
||
// The initial comment is displayed immediately, without a fade-in
|
||
// animation.
|
||
bool cmt_shown_initial;
|
||
static int8_t unused_byte; // ZUN bloat
|
||
#endif
|
||
// ---------------
|
||
|
||
// Backgrounds
|
||
// -----------
|
||
|
||
// B plane of the background image as loaded from the .PI file, without any
|
||
// polygons drawn on top of it.
|
||
#if (GAME >= 3)
|
||
dots8_t __seg* nopoly_B;
|
||
#else
|
||
dots8_t* nopoly_B;
|
||
#endif
|
||
|
||
// ZUN bloat: Unused in TH04 and TH05 which use the bgimage system for that,
|
||
// but still present in the binary.
|
||
Planar<dots8_t far *> cmt_bg;
|
||
// -----------
|
||
|
||
struct cmt_line_t {
|
||
shiftjis_t c[CMT_LINE_SIZE];
|
||
};
|
||
cmt_line_t cmt[CMT_LINES];
|
||
|
||
#if (GAME == 5)
|
||
// TH05 selection state
|
||
// --------------------
|
||
|
||
int track_id_at_top;
|
||
int track_playing;
|
||
int track_count_cur;
|
||
// --------------------
|
||
|
||
void pascal near track_unput_or_put(uint8_t track_sel, bool16 put)
|
||
{
|
||
enum {
|
||
CURSOR_LEFT = TRACKLIST_LEFT,
|
||
CURSOR_RIGHT = (CURSOR_LEFT + TRACKLIST_W),
|
||
CURSOR_TOP = TRACKLIST_TOP,
|
||
CURSOR_BOTTOM = (TRACKLIST_TOP + GLYPH_H - 1),
|
||
};
|
||
|
||
const shiftjis_t* choice;
|
||
vc_t col = COL_TRACKLIST;
|
||
|
||
// ZUN bloat: The code could have been a lot simpler if [TRACKLIST_TOP]
|
||
// was added here rather than just before graph_putsa_fx(). Then, [top]
|
||
// could have been a `screen_y_t`.
|
||
pixel_t top = ((track_sel - track_id_at_top) * GLYPH_H);
|
||
if((top < 0) || (top >= TRACKLIST_H)) {
|
||
return;
|
||
}
|
||
if(put) {
|
||
grcg_setcolor(GC_RMW, COL_TRACKLIST);
|
||
grcg_hline(CURSOR_LEFT, CURSOR_RIGHT, (CURSOR_TOP + top));
|
||
grcg_hline(CURSOR_LEFT, CURSOR_RIGHT, (CURSOR_BOTTOM + top));
|
||
grcg_vline(CURSOR_LEFT, (CURSOR_TOP + top), (CURSOR_BOTTOM + top));
|
||
grcg_vline(CURSOR_RIGHT, (CURSOR_TOP + top), (CURSOR_BOTTOM + top));
|
||
grcg_off();
|
||
} else {
|
||
// ZUN bloat: Blitting [TRACKLIST_W] pixels starting at
|
||
// [TRACKLIST_LEFT] is enough.
|
||
bgimage_put_rect_16(0, (CURSOR_TOP + top), 320, GLYPH_H);
|
||
}
|
||
if(track_sel == track_playing) {
|
||
col = COL_TRACKLIST_SELECTED;
|
||
}
|
||
|
||
choice = MUSIC_CHOICES[game_sel][track_sel];
|
||
top += TRACKLIST_TOP;
|
||
graph_putsa_fx(TRACKLIST_LEFT, top, col, choice);
|
||
}
|
||
|
||
void pascal near tracklist_put(uint8_t sel)
|
||
{
|
||
for(int i = 0; i < (track_count_cur + 2); i++) {
|
||
track_unput_or_put(i, (i == sel));
|
||
}
|
||
graph_putsa_fx(TRACKLIST_LEFT, LABEL_UP_TOP, COL_TRACKLIST, LABEL_UP);
|
||
graph_putsa_fx(
|
||
TRACKLIST_LEFT, LABEL_DOWN_TOP, COL_TRACKLIST, LABEL_DOWN
|
||
);
|
||
graph_putsa_fx(
|
||
TRACKLIST_LEFT,
|
||
LABEL_GAME_TOP,
|
||
COL_TRACKLIST_SELECTED,
|
||
LABEL_GAME[game_sel]
|
||
);
|
||
}
|
||
#else
|
||
void pascal near track_put(uint8_t sel, vc_t col)
|
||
{
|
||
page_t other_page = (1 - music_page);
|
||
graph_accesspage(other_page);
|
||
graph_putsa_fx(
|
||
TRACKLIST_LEFT, track_top(sel), track_fx(col), MUSIC_CHOICES[sel]
|
||
);
|
||
graph_accesspage(music_page);
|
||
graph_putsa_fx(
|
||
TRACKLIST_LEFT, track_top(sel), track_fx(col), MUSIC_CHOICES[sel]
|
||
);
|
||
}
|
||
|
||
void pascal near tracklist_put(uint8_t sel)
|
||
{
|
||
int i;
|
||
for(i = 0; i < sizeof(MUSIC_CHOICES) / sizeof(MUSIC_CHOICES[0]); i++) {
|
||
track_put(i, ((i == sel) ? COL_TRACKLIST_SELECTED : COL_TRACKLIST));
|
||
}
|
||
}
|
||
#endif
|
||
|
||
void near nopoly_B_snap(void)
|
||
{
|
||
nopoly_B = HMem<dots8_t>::alloc(PLANE_SIZE);
|
||
for(vram_offset_t p = 0; p < PLANE_SIZE; p += int(sizeof(dots32_t))) {
|
||
*reinterpret_cast<dots32_t *>(nopoly_B + p) = VRAM_CHUNK(B, p, 32);
|
||
}
|
||
}
|
||
|
||
void near nopoly_B_free(void)
|
||
{
|
||
HMem<dots8_t>::free(nopoly_B);
|
||
}
|
||
|
||
void near nopoly_B_put(void)
|
||
{
|
||
#if (GAME >= 3)
|
||
// ZUN bloat: This doesn't even compile to a 32-wide memcpy().
|
||
asm { push ds; }
|
||
_ES = SEG_PLANE_B;
|
||
_AX = FP_SEG(nopoly_B);
|
||
_DS = _AX;
|
||
__memcpy__(MK_FP(_ES, 0), MK_FP(_DS, 0), PLANE_SIZE);
|
||
asm { pop ds; }
|
||
#else
|
||
for(vram_offset_t p = 0; p < PLANE_SIZE; p += int(sizeof(dots32_t))) {
|
||
VRAM_CHUNK(B, p, 32) = (
|
||
*reinterpret_cast<dots32_t *>(nopoly_B + p)
|
||
);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
void pascal near polygon_build(
|
||
screen_point_t near* points,
|
||
screen_x_t center_x,
|
||
space_changing_pixel_t center_y,
|
||
pixel_t radius,
|
||
int point_count,
|
||
unsigned char plus_angle
|
||
)
|
||
{
|
||
int i;
|
||
|
||
// ZUN bloat: center_y.pixel = center_y.sp.to_pixel();
|
||
center_y.sp.v >>= SUBPIXEL_BITS;
|
||
|
||
for(i = 0; i < point_count; i++) {
|
||
unsigned char point_angle = (((i << 8) / point_count) + plus_angle);
|
||
#if (GAME >= 3)
|
||
points[i].x = polar_x(center_x, radius, point_angle);
|
||
points[i].y = polar_y(center_y.pixel, radius, point_angle);
|
||
#else
|
||
points[i].x = polar_x_fast(center_x, radius, point_angle);
|
||
points[i].y = polar_y_fast(center_y.pixel, radius, point_angle);
|
||
#endif
|
||
}
|
||
points[i].x = points[0].x;
|
||
points[i].y = points[0].y;
|
||
}
|
||
|
||
#define polygon_init(i, center_y, velocity_x) { \
|
||
center[i].x = (irand() % RES_X); \
|
||
center[i].y.v = center_y; \
|
||
velocity[i].x = velocity_x; \
|
||
if(velocity[i].x == 0) { \
|
||
velocity[i].x = 1; \
|
||
} \
|
||
velocity[i].y.v = (to_sp(2.0f) + TO_SP(irand() & 3)); \
|
||
angle[i] = irand(); \
|
||
angle_speed[i] = (0x04 - (irand() & 0x07)); \
|
||
if(angle_speed[i] == 0x00) { \
|
||
angle_speed[i] = 0x04; \
|
||
} \
|
||
}
|
||
|
||
inline int polygon_vertex_count(int polygon_index) {
|
||
return ((polygon_index / 4) + 3);
|
||
}
|
||
|
||
void near polygons_update_and_render(void)
|
||
{
|
||
int i;
|
||
if(!polygons_initialized) {
|
||
for(i = 0; i < POLYGONS_RENDERED; i++) {
|
||
polygon_init(i, (irand() % to_sp(RES_Y)), (4 - (irand() & 7)));
|
||
}
|
||
|
||
// ZUN quirk: This is never reset.
|
||
polygons_initialized = true;
|
||
}
|
||
for(i = 0; i < POLYGONS_RENDERED; i++) {
|
||
polygon_build(
|
||
points,
|
||
center[i].x,
|
||
reinterpret_cast<space_changing_pixel_t &>(center[i].y),
|
||
(((i & 3) * 16) + 64),
|
||
polygon_vertex_count(i),
|
||
angle[i]
|
||
);
|
||
center[i].x += velocity[i].x;
|
||
center[i].y.v += velocity[i].y.v;
|
||
angle[i] += angle_speed[i];
|
||
if((center[i].x <= 0) || (center[i].x >= (RES_X - 1))) {
|
||
velocity[i].x *= -1;
|
||
}
|
||
|
||
// Enough to cover the maximum possible radius of 96.
|
||
if(center[i].y >= to_sp(RES_Y + 100.0f)) {
|
||
polygon_init(i, to_sp(-100.0f), (8 - (irand() & 15)));
|
||
}
|
||
|
||
grcg_polygon_c(points, polygon_vertex_count(i));
|
||
}
|
||
}
|
||
|
||
// ZUN bloat
|
||
#if ((GAME == 3) || (GAME == 4))
|
||
#define frame_delay frame_delay_2
|
||
#endif
|
||
|
||
void near music_flip(void)
|
||
{
|
||
nopoly_B_put();
|
||
#if (GAME == 5)
|
||
piano_render();
|
||
#endif
|
||
|
||
// Draw the polygons via the GRCG by setting all bits in all tile registers
|
||
// but restricting blitting to the B plane. Technically, using the GRCG
|
||
// wouldn't be necessary and is in fact a slower way of regularly writing
|
||
// pixels to just the B plane, but all master.lib polygon drawing functions
|
||
// write to VRAM using `=` (MOV) rather than `|=` (OR), and thus expect the
|
||
// GRCG to be enabled.
|
||
grcg_setcolor((GC_RMW | GC_B), 0xF);
|
||
polygons_update_and_render();
|
||
grcg_off();
|
||
|
||
// This is the correct position for a VSync delay. frame_delay() returns
|
||
// immediately after a VSync interrupt and at the beginning of the vertical
|
||
// blanking interval, which is the safest point to flip pages.
|
||
#if (GAME == 5)
|
||
frame_delay(1);
|
||
#endif
|
||
|
||
graph_showpage(music_page);
|
||
music_page = (1 - music_page);
|
||
graph_accesspage(music_page);
|
||
|
||
// ZUN landmine: Waiting for VSync *after* flipping, however, means that we
|
||
// almost certainly *don't* flip within the vertical blanking interval, but
|
||
// somewhere *within* a frame while the beam is still traveling across the
|
||
// screen. This ensures a tearing line on all but the fastest PC-98
|
||
// systems, with the pixels above always being one frame behind.
|
||
#if (GAME <= 4)
|
||
frame_delay(1);
|
||
#endif
|
||
}
|
||
|
||
#if (GAME <= 3)
|
||
#define cmt_bg_blit_planar(cmt_bg_p, vo, x, dst, dst_p, src, src_p) \
|
||
size_t cmt_bg_p = 0; \
|
||
screen_y_t y; \
|
||
for(y = CMT_TITLE_TOP; y < CMT_TITLE_BOTTOM; y++) { \
|
||
for(x = CMT_TITLE_LEFT; x < CMT_TITLE_RIGHT; x += 32) { \
|
||
vo = vram_offset_shift(x, y); \
|
||
*(dots32_t *)(dst[0] + dst_p) = *(dots32_t *)(src[0] + src_p); \
|
||
*(dots32_t *)(dst[1] + dst_p) = *(dots32_t *)(src[1] + src_p); \
|
||
*(dots32_t *)(dst[2] + dst_p) = *(dots32_t *)(src[2] + src_p); \
|
||
*(dots32_t *)(dst[3] + dst_p) = *(dots32_t *)(src[3] + src_p); \
|
||
cmt_bg_p += sizeof(dots32_t); \
|
||
} \
|
||
} \
|
||
for(y = CMT_COMMENT_TOP; y < CMT_COMMENT_BOTTOM; y++) { \
|
||
for(x = CMT_COMMENT_LEFT; x < CMT_COMMENT_RIGHT; x += 32) { \
|
||
vo = vram_offset_shift(x, y); \
|
||
*(dots32_t *)(dst[0] + dst_p) = *(dots32_t *)(src[0] + src_p); \
|
||
*(dots32_t *)(dst[1] + dst_p) = *(dots32_t *)(src[1] + src_p); \
|
||
*(dots32_t *)(dst[2] + dst_p) = *(dots32_t *)(src[2] + src_p); \
|
||
*(dots32_t *)(dst[3] + dst_p) = *(dots32_t *)(src[3] + src_p); \
|
||
cmt_bg_p += sizeof(dots32_t); \
|
||
} \
|
||
}
|
||
|
||
void near cmt_bg_snap(void)
|
||
{
|
||
screen_x_t x;
|
||
vram_offset_t vo;
|
||
for(int i = 0; i < PLANE_COUNT; i++) {
|
||
cmt_bg[i] = HMem<dots8_t>::alloc(
|
||
(GLYPH_H * CMT_LINE_VRAM_W) + (CMT_COMMENT_H * CMT_LINE_VRAM_W)
|
||
);
|
||
}
|
||
cmt_bg_blit_planar(cmt_bg_p, vo, x, cmt_bg, cmt_bg_p, VRAM_PLANE, vo);
|
||
}
|
||
#endif
|
||
|
||
void pascal near cmt_load(int track)
|
||
{
|
||
#if (GAME == 5)
|
||
char* FN = "_MUSIC0.TXT";
|
||
|
||
FN[6] = ('0' + game_sel);
|
||
file_ropen(FN);
|
||
#elif (GAME == 4)
|
||
file_ropen("_MUSIC.TXT");
|
||
#else
|
||
file_ropen("MUSIC.TXT");
|
||
#endif
|
||
file_seek((track * int(sizeof(cmt))), SEEK_SET);
|
||
file_read(cmt, sizeof(cmt));
|
||
file_close();
|
||
for(int i = 0; i < CMT_LINES; i++) {
|
||
cmt[i].c[CMT_LINE_LENGTH] = '\0';
|
||
}
|
||
}
|
||
|
||
// ZUN bloat: TH05 has the most straightforward version of this code.
|
||
#define cmt_put_macro(fx) \
|
||
graph_putsa_fx( \
|
||
CMT_TITLE_LEFT, CMT_TITLE_TOP, (COL_CMT_TRACK | fx), cmt[0].c \
|
||
); \
|
||
for(int line = 1; line < CMT_LINES; line++) { \
|
||
if((GAME >= 4) && (cmt[line].c[0] == ';')) { \
|
||
continue; \
|
||
} \
|
||
graph_putsa_fx( \
|
||
CMT_COMMENT_LEFT, \
|
||
((line + ((CMT_COMMENT_TOP - 1) / GLYPH_H)) * GLYPH_H), \
|
||
(COL_CMT_COMMENT | fx), \
|
||
cmt[line].c \
|
||
); \
|
||
}
|
||
|
||
#if (GAME >= 4)
|
||
void near cmt_put(void)
|
||
{
|
||
#if (GAME == 5)
|
||
graph_putsa_fx(
|
||
CMT_TITLE_LEFT, CMT_TITLE_TOP, COL_CMT_TRACK, cmt[0].c
|
||
);
|
||
const cmt_line_t near* cmt_p = &cmt[1];
|
||
int line = 1;
|
||
screen_y_t top = CMT_COMMENT_TOP;
|
||
while(line < CMT_LINES) {
|
||
if(cmt_p->c[0] != ';') {
|
||
graph_putsa_fx(
|
||
CMT_COMMENT_LEFT, top, COL_CMT_COMMENT, cmt_p->c
|
||
);
|
||
}
|
||
line++;
|
||
top += GLYPH_H;
|
||
cmt_p++;
|
||
}
|
||
#else
|
||
cmt_put_macro(0);
|
||
#endif
|
||
}
|
||
|
||
void near cmt_fadein_both_animate(void)
|
||
{
|
||
int func; // ACTUAL TYPE: graph_putsa_fx_func_t
|
||
for(func = FX_MASK; func < FX_MASK_END; func++) {
|
||
graph_putsa_fx_func = static_cast<graph_putsa_fx_func_t>(func);
|
||
cmt_put(); music_flip();
|
||
cmt_put(); music_flip();
|
||
}
|
||
graph_putsa_fx_func = FX_WEIGHT_BOLD;
|
||
cmt_put(); music_flip();
|
||
cmt_put();
|
||
}
|
||
|
||
inline void cmt_unput(void) {
|
||
enum {
|
||
W = (RES_X - CMT_TITLE_LEFT), // ZUN bloat: [CMT_LINE_W] is enough.
|
||
};
|
||
#if (GAME == 5)
|
||
bgimage_put_rect_16(CMT_TITLE_LEFT, CMT_TITLE_TOP, W, GLYPH_H);
|
||
bgimage_put_rect_16(
|
||
CMT_COMMENT_LEFT,
|
||
CMT_COMMENT_TOP,
|
||
W,
|
||
(CMT_COMMENT_LINES * GLYPH_H)
|
||
);
|
||
#else
|
||
bgimage_put_rect_16(
|
||
CMT_TITLE_LEFT, CMT_TITLE_TOP, W, (CMT_LINES * GLYPH_H)
|
||
);
|
||
#endif
|
||
}
|
||
|
||
void near cmt_unput_both_animate(void)
|
||
{
|
||
// ZUN bloat: Not related to unblitting.
|
||
graph_putsa_fx_func = FX_WEIGHT_BOLD;
|
||
|
||
cmt_unput();
|
||
music_flip();
|
||
cmt_unput();
|
||
}
|
||
|
||
void pascal near cmt_load_unput_and_put_both_animate(int track)
|
||
{
|
||
if(cmt_shown_initial) {
|
||
cmt_unput_both_animate();
|
||
}
|
||
cmt_load(track);
|
||
|
||
// ZUN bloat: These calls are never needed. Either we're rendering the
|
||
// first track and there's nothing to be unblitted, or we already
|
||
// unblitted the previous track title and comment in the call above.
|
||
// In fact, they have no effect at all, which is why TH05 can put these
|
||
// nonsensical coordinates and ZUN never noticed anything.
|
||
nopoly_B_put();
|
||
bgimage_put_rect_16(
|
||
CMT_TITLE_LEFT,
|
||
((GAME == 5) ? 64 : CMT_TITLE_TOP),
|
||
(RES_X - CMT_TITLE_LEFT),
|
||
((GAME == 5) ? 256 : (CMT_LINES * GLYPH_H))
|
||
);
|
||
|
||
if(cmt_shown_initial) {
|
||
cmt_fadein_both_animate();
|
||
} else {
|
||
cmt_shown_initial = true;
|
||
cmt_put();
|
||
music_flip();
|
||
cmt_put();
|
||
}
|
||
|
||
// ZUN bloat: Redundant; this is the first thing done in music_flip(),
|
||
// which runs almost immediately after this function on every code
|
||
// path.
|
||
nopoly_B_put();
|
||
}
|
||
#else
|
||
void near cmt_bg_free(void)
|
||
{
|
||
HMem<dots8_t>::free(cmt_bg.B);
|
||
HMem<dots8_t>::free(cmt_bg.R);
|
||
HMem<dots8_t>::free(cmt_bg.G);
|
||
HMem<dots8_t>::free(cmt_bg.E);
|
||
}
|
||
|
||
void near cmt_unput(void)
|
||
{
|
||
screen_x_t x;
|
||
vram_offset_t vo;
|
||
cmt_bg_blit_planar(cmt_bg_p, vo, x, VRAM_PLANE, vo, cmt_bg, cmt_bg_p);
|
||
}
|
||
|
||
void pascal near cmt_load_unput_and_put(int track)
|
||
{
|
||
// ZUN bloat: This function is called once per VRAM page, but we only
|
||
// need to load the track once.
|
||
cmt_load(track);
|
||
|
||
nopoly_B_put();
|
||
cmt_unput();
|
||
cmt_put_macro(FX_WEIGHT_HEAVY);
|
||
|
||
// Update [nopoly_B] to match the B plane of the comment we just
|
||
// blitted into the respective area. Since this buffer is reblitted
|
||
// every frame, updating it here preserves the lowest bit of the
|
||
// comment text pixels regardless of the polygons rendered on top.
|
||
// This allows comment colors to have any odd value, just like the
|
||
// tracklist colors.
|
||
//
|
||
// ZUN bloat: Same as above – the second call overwrites the previously
|
||
// snapped contents with the exact same pixels from the other page.
|
||
// ZUN bloat: Also, limiting it to just the comment area would have
|
||
// been enough.
|
||
for(vram_offset_t p = 0; p < PLANE_SIZE; p += int(sizeof(dots32_t))) {
|
||
*reinterpret_cast<dots32_t *>(nopoly_B + p) = VRAM_CHUNK(B, p, 32);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// Input wrappers
|
||
// --------------
|
||
// The Music Room is the only piece of UI that is more or less the same across
|
||
// 4 of the 5 games. Therefore, it makes sense to define these here rather than
|
||
// adding more complexity to the headers.
|
||
|
||
#if (GAME == 3)
|
||
#define key_det input_sp
|
||
#endif
|
||
|
||
// Same game-specific branches as in the TH03/TH04/TH05 cutscene system, but
|
||
// with a different, much smaller effect here.
|
||
//
|
||
// ZUN quirk: After processing an input, the Music Room loop wants to prevent
|
||
// held keys from having any effect by spinning in a different loop until all
|
||
// keys have been released. However, due to the different sensing functions,
|
||
// the actual handling of held keys differs depending on the game:
|
||
//
|
||
// • TH02 and TH04 use raw input sensing functions that don't address the
|
||
// hardware quirk documented in the `Research/HOLDKEY` example. Therefore,
|
||
// the eventual key release scancode for a held key is not filtered and gets
|
||
// through to [key_det], breaking the spinning loop despite the key still
|
||
// being held.
|
||
// • In TH03 and TH05, this eventual key release scancode is accurately
|
||
// detected and filtered. Thus, held keys truly don't have any effect, but at
|
||
// the cost of an additional 614.4 µs for every call to this function.
|
||
inline void music_input_sense(void) {
|
||
#if (GAME == 5)
|
||
input_reset_sense_held();
|
||
#elif (GAME == 4)
|
||
input_reset_sense();
|
||
#elif (GAME == 3)
|
||
input_mode_interface();
|
||
#else
|
||
input_reset_sense();
|
||
#endif
|
||
}
|
||
// --------------
|
||
|
||
#define wait_for_key_release_animate() { \
|
||
while(1) { \
|
||
music_input_sense(); \
|
||
if(key_det) { \
|
||
music_flip(); \
|
||
} else { \
|
||
break; \
|
||
} \
|
||
} \
|
||
}
|
||
|
||
#if (GAME == 5)
|
||
void pascal near tracklist_unput_and_put_both_animate(int sel)
|
||
{
|
||
bgimage_put_rect_16(0, LABEL_GAME_TOP, CMT_TITLE_LEFT, GLYPH_H);
|
||
bgimage_put_rect_16(0, TRACKLIST_TOP, CMT_TITLE_LEFT, TRACKLIST_H);
|
||
tracklist_put(sel);
|
||
music_flip();
|
||
bgimage_put_rect_16(0, LABEL_GAME_TOP, CMT_TITLE_LEFT, GLYPH_H);
|
||
bgimage_put_rect_16(0, TRACKLIST_TOP, CMT_TITLE_LEFT, TRACKLIST_H);
|
||
tracklist_put(sel);
|
||
}
|
||
|
||
inline void track_unput_and_put_both_animate(
|
||
const uint8_t& sel_prev, const uint8_t& sel_new
|
||
) {
|
||
track_unput_or_put(sel_prev, false);
|
||
track_unput_or_put(sel_new, true);
|
||
music_flip();
|
||
track_unput_or_put(sel_prev, false);
|
||
track_unput_or_put(sel_new, true);
|
||
}
|
||
|
||
inline void game_switch(void) {
|
||
music_sel = 0;
|
||
track_playing = 0;
|
||
track_id_at_top = 0;
|
||
track_count_cur = TRACK_COUNT[game_sel];
|
||
tracklist_unput_and_put_both_animate(0);
|
||
snd_kaja_func(KAJA_SONG_FADE, 32);
|
||
cmt_load_unput_and_put_both_animate(0);
|
||
snd_load(MUSIC_FILES[game_sel][0], SND_LOAD_SONG);
|
||
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
||
}
|
||
#endif
|
||
|
||
void MUSICROOM_DISTANCE musicroom_menu(void)
|
||
{
|
||
#if (GAME == 5)
|
||
int frames_since_last_input = 0;
|
||
uint8_t sel_prev;
|
||
track_id_at_top = 0;
|
||
track_playing = 0;
|
||
music_sel = 0;
|
||
track_count_cur = TRACK_COUNT[game_sel];
|
||
#define SEL_QUIT track_count_cur
|
||
#else
|
||
enum {
|
||
SEL_QUIT = (TRACK_COUNT + 1),
|
||
};
|
||
#endif
|
||
|
||
#if (GAME >= 4)
|
||
cmt_shown_initial = false;
|
||
#endif
|
||
|
||
// ZUN bloat: The call site would have been a better place for this.
|
||
#if (GAME >= 4)
|
||
cdg_free_all();
|
||
text_clear();
|
||
#elif (GAME == 3)
|
||
for(int i = 0; i < CDG_SLOT_COUNT; i++) {
|
||
cdg_free(i);
|
||
}
|
||
super_free();
|
||
text_clear();
|
||
#endif
|
||
|
||
music_page = 1;
|
||
|
||
palette_black();
|
||
graph_showpage(0);
|
||
|
||
// ZUN bloat: We copy page 1 to page 0 below anyway. The hardware palette
|
||
// is also entirely black, so no one will ever see a difference.
|
||
graph_accesspage(0);
|
||
graph_clear();
|
||
|
||
graph_accesspage(1);
|
||
|
||
#if (GAME >= 4)
|
||
pi_fullres_load_palette_apply_put_free(0, "music.pi");
|
||
#else
|
||
pi_fullres_load_palette_apply_put_free(0, "op3.pi");
|
||
#endif
|
||
|
||
#if (GAME == 5)
|
||
piano_setup_and_put_initial();
|
||
nopoly_B_snap();
|
||
bgimage_snap();
|
||
#else
|
||
music_sel = track_playing;
|
||
#endif
|
||
tracklist_put(music_sel);
|
||
graph_copy_page(0);
|
||
|
||
#if (GAME == 4)
|
||
bgimage_snap();
|
||
#endif
|
||
graph_accesspage(1);
|
||
graph_showpage(0);
|
||
|
||
#if (GAME == 5)
|
||
pfend();
|
||
pfstart("music.dat");
|
||
cmt_load_unput_and_put_both_animate(music_sel);
|
||
#else
|
||
nopoly_B_snap();
|
||
#if (GAME >= 4)
|
||
cmt_load_unput_and_put_both_animate(track_playing);
|
||
#else
|
||
cmt_bg_snap();
|
||
graph_accesspage(1); cmt_load_unput_and_put(track_playing);
|
||
graph_accesspage(0); cmt_load_unput_and_put(track_playing);
|
||
#endif
|
||
#endif
|
||
|
||
palette_100();
|
||
|
||
while(1) {
|
||
#if (GAME == 5)
|
||
// This loop also ignores any ← or → inputs while ↑ or ↓ are held,
|
||
// and vice versa.
|
||
while(1) {
|
||
music_input_sense();
|
||
if(!key_det) {
|
||
break;
|
||
}
|
||
if(frames_since_last_input >= 24) {
|
||
if((key_det == INPUT_UP) || (key_det == INPUT_DOWN)) {
|
||
frames_since_last_input = 20;
|
||
break;
|
||
}
|
||
}
|
||
frames_since_last_input++;
|
||
music_flip();
|
||
}
|
||
#else
|
||
// ZUN bloat: None of this `goto` business would have been
|
||
// necessary if the loop clearly defined its update and render
|
||
// steps. Especially since it does want to render the polygon
|
||
// animation every frame.
|
||
wait_for_key_release_animate();
|
||
#endif
|
||
controls:
|
||
// ZUN bloat: We already did that for this frame if we came from above,
|
||
// but not if we came from the `goto` below.
|
||
music_input_sense();
|
||
|
||
#if (GAME == 5)
|
||
if(key_det & INPUT_UP) {
|
||
sel_prev = music_sel;
|
||
if(music_sel > 0) {
|
||
music_sel--;
|
||
if(music_sel < track_id_at_top) {
|
||
track_id_at_top = music_sel;
|
||
tracklist_unput_and_put_both_animate(music_sel);
|
||
|
||
// ZUN quirk: This prevents game switches via ← or → ,
|
||
// but only in the very specific case of
|
||
// 1) the cursor being at the top of the list,
|
||
// 2) highlighting a track other than the first one of
|
||
// the respective game, and
|
||
// 3) ←/→ being pressed simultaneously with ↑.
|
||
// In any other case, ←/→ are processed as expected,
|
||
// and override this cursor movement with a game
|
||
// switch.
|
||
goto skip_processing_of_left_and_right;
|
||
} else {
|
||
track_unput_and_put_both_animate(sel_prev, music_sel);
|
||
}
|
||
} else {
|
||
music_sel = SEL_QUIT;
|
||
track_id_at_top = (
|
||
SEL_QUIT - (TRACKLIST_VISIBLE_COUNT - 1)
|
||
);
|
||
tracklist_unput_and_put_both_animate(SEL_QUIT);
|
||
}
|
||
}
|
||
if(key_det & INPUT_DOWN) {
|
||
sel_prev = music_sel;
|
||
if(music_sel < SEL_QUIT) {
|
||
music_sel++;
|
||
if(
|
||
music_sel >=
|
||
(track_id_at_top + TRACKLIST_VISIBLE_COUNT)
|
||
) {
|
||
track_id_at_top = (
|
||
music_sel - (TRACKLIST_VISIBLE_COUNT - 1)
|
||
);
|
||
tracklist_unput_and_put_both_animate(music_sel);
|
||
|
||
// Same as the quirk above, applying to the
|
||
// corresponding very specific case of
|
||
// 1) the cursor being at the bottom of the list,
|
||
// 2) highlighting anything except [SEL_QUIT], and
|
||
// 3) ←/→ being pressed simultaneously with ↓.
|
||
goto skip_processing_of_left_and_right;
|
||
} else {
|
||
track_unput_and_put_both_animate(sel_prev, music_sel);
|
||
}
|
||
} else {
|
||
music_sel = 0;
|
||
track_id_at_top = 0;
|
||
tracklist_unput_and_put_both_animate(music_sel);
|
||
}
|
||
}
|
||
if(key_det & INPUT_LEFT) {
|
||
ring_dec(game_sel, (GAME_COUNT - 1));
|
||
game_switch();
|
||
} else if(key_det & INPUT_RIGHT) {
|
||
ring_inc_ge(game_sel, GAME_COUNT);
|
||
game_switch();
|
||
}
|
||
#else
|
||
if(key_det & INPUT_UP) {
|
||
track_put(music_sel, COL_TRACKLIST);
|
||
if(music_sel > 0) {
|
||
music_sel--;
|
||
} else {
|
||
music_sel = SEL_QUIT;
|
||
}
|
||
|
||
// Skip over the empty line
|
||
if(music_sel == TRACK_COUNT) {
|
||
music_sel--;
|
||
}
|
||
|
||
track_put(music_sel, COL_TRACKLIST_SELECTED);
|
||
}
|
||
if(key_det & INPUT_DOWN) {
|
||
track_put(music_sel, COL_TRACKLIST);
|
||
if(music_sel < SEL_QUIT) {
|
||
music_sel++;
|
||
} else {
|
||
music_sel = 0;
|
||
}
|
||
|
||
// Skip over the empty line
|
||
if(music_sel == TRACK_COUNT) {
|
||
music_sel++;
|
||
}
|
||
|
||
track_put(music_sel, COL_TRACKLIST_SELECTED);
|
||
}
|
||
#endif
|
||
skip_processing_of_left_and_right:
|
||
if(key_det & INPUT_SHOT || key_det & INPUT_OK) {
|
||
if(music_sel != SEL_QUIT) {
|
||
#if (GAME >= 4)
|
||
snd_kaja_func(KAJA_SONG_FADE, 32);
|
||
#elif (GAME == 3)
|
||
// Avoids the snd_load() landmine that is still present in
|
||
// this game.
|
||
snd_kaja_func(KAJA_SONG_STOP, 0);
|
||
#elif (GAME == 2)
|
||
// ZUN landmine: Should have stopped the currently playing
|
||
// track according to snd_load()'s header comment.
|
||
// Especially since this game simultaneously loads both the
|
||
// PMD and MIDI versions and is therefore inherently slower
|
||
// than the others.
|
||
#endif
|
||
|
||
#if (GAME == 5)
|
||
sel_prev = track_playing;
|
||
track_playing = music_sel;
|
||
track_unput_and_put_both_animate(sel_prev, music_sel);
|
||
cmt_load_unput_and_put_both_animate(music_sel);
|
||
snd_load(MUSIC_FILES[game_sel][music_sel], SND_LOAD_SONG);
|
||
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
||
#elif (GAME == 4)
|
||
track_playing = music_sel;
|
||
cmt_load_unput_and_put_both_animate(music_sel);
|
||
snd_load(MUSIC_FILES[music_sel], SND_LOAD_SONG);
|
||
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
||
#else
|
||
#if (GAME == 3)
|
||
snd_load(MUSIC_FILES[music_sel], SND_LOAD_SONG);
|
||
#else
|
||
// Load both the MIDI and PMD versions of the selected
|
||
// track. Makes sense given that the track continues
|
||
// playing when leaving the Music Room – changing the
|
||
// music mode in the Option menu will then play the
|
||
// same selected track.
|
||
bool midi_active = snd_midi_active;
|
||
snd_midi_active = snd_midi_possible;
|
||
snd_load(MUSIC_FILES[music_sel], SND_LOAD_SONG);
|
||
snd_midi_active = 0;
|
||
snd_load(MUSIC_FILES[music_sel], SND_LOAD_SONG);
|
||
snd_midi_active = midi_active;
|
||
#endif
|
||
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
||
track_playing = music_sel;
|
||
cmt_load_unput_and_put(music_sel);
|
||
music_flip();
|
||
cmt_load_unput_and_put(music_sel);
|
||
#endif
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
if(key_det & INPUT_CANCEL) {
|
||
break;
|
||
}
|
||
if(!key_det) {
|
||
#if (GAME == 5)
|
||
frames_since_last_input = 0;
|
||
#endif
|
||
music_flip();
|
||
goto controls;
|
||
}
|
||
};
|
||
wait_for_key_release_animate();
|
||
|
||
#if (GAME >= 4)
|
||
#if (GAME == 5)
|
||
pfend();
|
||
pfstart(OP_AND_END_PF_FN);
|
||
#endif
|
||
snd_kaja_func(KAJA_SONG_FADE, 16);
|
||
nopoly_B_free();
|
||
graph_showpage(0);
|
||
graph_accesspage(0);
|
||
palette_black_out(1);
|
||
bgimage_free();
|
||
snd_load(BGM_MENU_MAIN_FN, SND_LOAD_SONG);
|
||
snd_kaja_func(KAJA_SONG_PLAY, 0);
|
||
#else
|
||
nopoly_B_free();
|
||
cmt_bg_free();
|
||
graph_showpage(0);
|
||
|
||
// ZUN quirk: graph_clear() sets all of VRAM to hardware color #0,
|
||
// which is purple in the original images, not black.
|
||
//
|
||
// ZUN bloat: Unlike the graph_clear() call at the beginning of the
|
||
// function, this one is not useless because we haven't reset the
|
||
// hardware palette yet. Still, setting all hardware colors to color #0
|
||
// would have been a more efficient way to accomplish the same hiding
|
||
// effect before the graph_copy_page() call below.
|
||
graph_accesspage(0);
|
||
graph_clear();
|
||
|
||
graph_accesspage(1);
|
||
|
||
#if (GAME == 2)
|
||
// ZUN bloat: The call site would have been a better place for this.
|
||
pi_fullres_load_palette_apply_put_free(0, "op2.pi");
|
||
palette_entry_rgb_show("op.rgb");
|
||
graph_copy_page(0);
|
||
#endif
|
||
|
||
graph_accesspage(0);
|
||
#endif
|
||
}
|