diff --git a/src/game/game_init.c b/src/game/game_init.c index 0852a141..004941d8 100644 --- a/src/game/game_init.c +++ b/src/game/game_init.c @@ -332,6 +332,45 @@ void display_and_vsync(void) { gGlobalTimer++; } +/* + * This enhancement allows you to record gameplay demos for the mario head screen. + * + * Note: + * This enhancement does require the lastest versions of PJ64 from the nightly builds, + * because it uses the javascript API to automatically dump the demo files from RAM + * once the demo is completed. See enhancements/RecordDemo.js for more info + * +*/ + +#include "../src/game/mario.h" + +#define DEMOREC_STATUS_NOT_RECORDING 0 +#define DEMOREC_STATUS_PREPARING 1 +#define DEMOREC_STATUS_RECORDING 2 +#define DEMOREC_STATUS_STOPPING 3 +#define DEMOREC_STATUS_DONE 4 + +#define DEMOREC_PRINT_X 10 +#define DEMOREC_PRINT_Y 10 + +#define DEMOREC_DONE_DELAY 60 // Show "DONE" message for 2 seconds. + +#define DEMOREC_MAX_INPUTS 1025 // Max number of recorded inputs. + +/* + DO NOT REMOVE, MODIFY, OR MAKE A COPY OF THIS EXACT STRING! + This is here so that the js dump script can find the control variables easily. +*/ +char gDemoRecTag[] = "DEMORECVARS"; + +// Control variables. It is easier if they are each 4 byte aligned, which is why they are u32. +u32 gRecordingStatus = DEMOREC_STATUS_NOT_RECORDING; +u32 gDoneDelay = 0; +u32 gNumOfRecordedInputs = 0; +struct DemoInput gRecordedInputs[DEMOREC_MAX_INPUTS]; +struct DemoInput* gRecordedInputsPtr = (struct DemoInput*)gRecordedInputs; +struct DemoInput gRecordedDemoInputCopy; + // this function records distinct inputs over a 255-frame interval to RAM locations and was likely // used to record the demo sequences seen in the final game. This function is unused. static void record_demo(void) { @@ -365,6 +404,118 @@ static void record_demo(void) { gRecordedDemoInput.timer++; } +void record_new_demo_input(void) { + if(gRecordedDemoInput.timer == 1 && gRecordedDemoInputCopy.timer > 0) { + gRecordedInputs[gNumOfRecordedInputs].timer = gRecordedDemoInputCopy.timer; + gRecordedInputs[gNumOfRecordedInputs + 1].timer = 0; + gRecordedInputs[gNumOfRecordedInputs].rawStickX = gRecordedDemoInputCopy.rawStickX; + gRecordedInputs[gNumOfRecordedInputs + 1].rawStickX = gRecordedDemoInputCopy.rawStickX; + gRecordedInputs[gNumOfRecordedInputs].rawStickY = gRecordedDemoInputCopy.rawStickY; + gRecordedInputs[gNumOfRecordedInputs + 1].rawStickY = gRecordedDemoInputCopy.rawStickY; + gRecordedInputs[gNumOfRecordedInputs].buttonMask = gRecordedDemoInputCopy.buttonMask; + gRecordedInputs[gNumOfRecordedInputs + 1].buttonMask = gRecordedDemoInputCopy.buttonMask; + gNumOfRecordedInputs++; + } +} + +// Self explanitory +void copy_gRecordedDemoInput(void) { + gRecordedDemoInputCopy.timer = gRecordedDemoInput.timer; + gRecordedDemoInputCopy.rawStickX = gRecordedDemoInput.rawStickX; + gRecordedDemoInputCopy.rawStickY = gRecordedDemoInput.rawStickY; + gRecordedDemoInputCopy.buttonMask = gRecordedDemoInput.buttonMask; +} + +// Runs when the demo is recording. +void recording(void) { + + // Force-stop when someone makes too many inputs. + if(gNumOfRecordedInputs + 1 > DEMOREC_MAX_INPUTS) { + gRecordingStatus = DEMOREC_STATUS_STOPPING; + return; + } + + copy_gRecordedDemoInput(); + record_demo(); // Defined in game.c + record_new_demo_input(); +} + +// Makes sure the last demo input is zeroed out, to make it look more clean. +void record_cleanup(void) { + gRecordedInputs[gNumOfRecordedInputs].timer = 0; + gRecordedInputs[gNumOfRecordedInputs].rawStickX = 0; + gRecordedInputs[gNumOfRecordedInputs].rawStickY = 0; + gRecordedInputs[gNumOfRecordedInputs].buttonMask = 0; + + // Make sure the done delay is reset before moving to DONE status. + gDoneDelay = 0; +} + +void record_run(void) { + switch(gRecordingStatus) { + case DEMOREC_STATUS_NOT_RECORDING: + break; + case DEMOREC_STATUS_PREPARING: + if(gMarioObject != NULL && gCurrLevelNum >= 5) { // If the game is in an active level + gRecordingStatus = DEMOREC_STATUS_RECORDING; + + // A bit of a hack, but it works. + gNumOfRecordedInputs = 1; + gRecordedInputs[0].timer = gCurrLevelNum; + gRecordedInputs[0].rawStickX = 0; + gRecordedInputs[0].rawStickY = 0; + gRecordedInputs[0].buttonMask = 0; + } + break; + case DEMOREC_STATUS_RECORDING: + recording(); + break; + case DEMOREC_STATUS_DONE: + if(gDoneDelay > DEMOREC_DONE_DELAY) + gRecordingStatus = DEMOREC_STATUS_NOT_RECORDING; + else + gDoneDelay++; + break; + } +} + +// Prints the status on the bottom-left side of the screen in colorful text. +void print_status(void) { + switch(gRecordingStatus) { + case DEMOREC_STATUS_PREPARING: + print_text(DEMOREC_PRINT_X, DEMOREC_PRINT_Y, "READY"); + break; + case DEMOREC_STATUS_RECORDING: + print_text(DEMOREC_PRINT_X, DEMOREC_PRINT_Y, "REC"); + break; + case DEMOREC_STATUS_STOPPING: + print_text(DEMOREC_PRINT_X, DEMOREC_PRINT_Y, "WAIT"); + break; + case DEMOREC_STATUS_DONE: + print_text(DEMOREC_PRINT_X, DEMOREC_PRINT_Y, "DONE"); + break; + } +} + +// Main function that should be called from thread5_game_loop() +void recordingDemo(void) { + // Mario needs to enter directly into a level and not from a warp, + // so the debug level select is used for that. + gDebugLevelSelect = TRUE; + + if(gPlayer1Controller->buttonPressed & L_TRIG) { + if(gRecordingStatus == DEMOREC_STATUS_NOT_RECORDING) { + gRecordingStatus = DEMOREC_STATUS_PREPARING; + } else if (gRecordingStatus == DEMOREC_STATUS_RECORDING) { + gRecordingStatus = DEMOREC_STATUS_STOPPING; + record_cleanup(); + } + } + + record_run(); + print_status(); +} + // take the updated controller struct and calculate // the new x, y, and distance floats. void adjust_analog_stick(struct Controller *controller) { @@ -605,6 +756,7 @@ void thread5_game_loop(UNUSED void *arg) { audio_game_loop_tick(); config_gfx_pool(); read_controller_inputs(); + recordingDemo(); addr = level_script_execute(addr); display_and_vsync();