sm64pc/enhancements/record_demo.patch

178 lines
6.4 KiB
Diff

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();