From 8b8c0eceb91ce0a20e2fe7167f4de90355126c21 Mon Sep 17 00:00:00 2001 From: Miloslav Ciz Date: Mon, 5 Jun 2023 22:22:11 +0200 Subject: [PATCH] Add demo mod --- mods/demo.diff | 411 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 mods/demo.diff diff --git a/mods/demo.diff b/mods/demo.diff new file mode 100644 index 0000000..35993ec --- /dev/null +++ b/mods/demo.diff @@ -0,0 +1,411 @@ +This mod adds demo (recorded gameplay) support to the SDL version of the game. +This allows recording gameplay, making tool assisted speedruns by manually +crafting game inputs or even saving the game at arbitrary positions. Rewinding +of demos is supported. For details see the file demo.h and check the game help. +Note that each demo is specific to game resolution, FPS and version! Also note +that when using mouse, a long demo can get big (megabytes) as many mouse inputs +have to be recorded (with keyboard only the demo will be relatively small). + +by drummyfish, released under CC0 1.0, public domain + +diff --git a/demo.h b/demo.h +new file mode 100644 +index 0000000..94898be +--- /dev/null ++++ b/demo.h +@@ -0,0 +1,275 @@ ++/** ++ @file demo.h ++ ++ Implementation of demo recording/playback for Anarch, can be used to record ++ gameplay or even make tool assisted runs. ++ ++ Demo file format: demo is a text file with one record per line. If line ++ starts with '#', it is ignored. First line of a demo should be a comment ++ holding the FPS, resolution (due to mouse offsets) and version of the game ++ used to record the demo. If a line starts with '*', it will be rewinded ++ to this position when played back. Otherwise the line is of format: ++ ++ F Z X Y ++ ++ where F is a non-negative frame number, X and Y are horizontal and vertical ++ mouse offsets (with possible sign), all in decimal, and Z is the button state ++ in format ++ ++ PONMLKJIHGFEDCBA ++ ++ where each letter is either 0 (not pressed) or 1 (pressed), A means key ++ with value 0 (SFG_KEY_UP), B means key with value 1 (SFG_KEY_RIGHT) etc. ++ ++ by drummyfish, released under CC0 1.0, public domain ++*/ ++ ++#include ++#include ++#include "game.h" ++ ++#define DEMO_FILENAME "demo.txt" ++#define DEMO_MAXRECORDS 262144 ///< Maximum number of records in a demo. ++ ++#define DEMO_NOTHING 0 ///< Do not use demo. ++#define DEMO_REPLAY 1 ///< Load and replay demo. ++#define DEMO_REPLAY_RECORD 2 ///< Load and replay demo, then record and save. ++ ++#define DEMO_STATE_DONE 0 ++#define DEMO_STATE_REWINDING 1 ++#define DEMO_STATE_PLAYING 2 ++#define DEMO_STATE_RECORDING 3 ++ ++#define DEMO_PRINT(s) puts("DEMO: " s) ++ ++typedef struct ++{ ++ uint32_t frame; ++ uint16_t buttonStates; ++ int16_t mouseDx; ++ int16_t mouseDy; ++} DemoRecord; ++ ++struct ++{ ++ uint32_t recordCount; ++ int32_t currentRecord; ++ uint32_t rewindTo; ++ uint8_t action; ++ uint8_t state; ++ DemoRecord records[DEMO_MAXRECORDS]; ++} demo; ++ ++void _demoRecordInputs(void) ++{ ++ uint16_t oldB = 0; ++ int16_t oldDx = 0, oldDy = 0; ++ ++ if (demo.recordCount > 0) ++ { ++ oldB = demo.records[demo.recordCount - 1].buttonStates; ++ oldDx = demo.records[demo.recordCount - 1].mouseDx; ++ oldDy = demo.records[demo.recordCount - 1].mouseDy; ++ } ++ ++ uint16_t newB = 0; ++ int16_t newDx = 0, newDy = 0; ++ ++ SFG_getMouseOffset(&newDx,&newDy); ++ ++ for (uint8_t i = 0; i < SFG_KEY_COUNT; ++i) ++ { ++ newB <<= 1; ++ newB |= (SFG_keyPressed(SFG_KEY_COUNT - i - 1) != 0); ++ } ++ ++ if (newB != oldB || newDx != oldDx || newDy != oldDy) ++ { ++ if (demo.recordCount >= DEMO_MAXRECORDS - 1) ++ { ++ DEMO_PRINT("max records reached, stopping recording"); ++ demo.state = DEMO_STATE_DONE; ++ } ++ else ++ { ++ demo.records[demo.recordCount].buttonStates = newB; ++ demo.records[demo.recordCount].mouseDx = newDx; ++ demo.records[demo.recordCount].mouseDy = newDy; ++ demo.records[demo.recordCount].frame = SFG_game.frame; ++ demo.recordCount++; ++ } ++ } ++} ++ ++// Call before executing a game (NOT rendering) frame. Returns the demo state. ++uint8_t demoFrameStart() ++{ ++ switch (demo.state) ++ { ++ case DEMO_STATE_REWINDING: ++ if (SFG_game.frame >= demo.rewindTo) ++ { ++ DEMO_PRINT("rewinding finished, replaying"); ++ demo.state = DEMO_STATE_PLAYING; ++ } ++ ++ case DEMO_STATE_PLAYING: ++ if (demo.currentRecord >= ((int32_t) demo.recordCount) - 1) ++ { ++ DEMO_PRINT("replaying done"); ++ demo.state = DEMO_STATE_DONE; ++ ++ if (demo.action == DEMO_REPLAY_RECORD) ++ { ++ DEMO_PRINT("recording"); ++ demo.state = DEMO_STATE_RECORDING; ++ } ++ } ++ ++ if (demo.state != DEMO_STATE_DONE && ++ SFG_game.frame == demo.records[demo.currentRecord + 1].frame) ++ demo.currentRecord++; ++ ++ break; ++ ++ case DEMO_STATE_RECORDING: ++ _demoRecordInputs(); ++ break; ++ ++ default: break; ++ } ++ ++ return demo.state; ++} ++ ++// Behaves the same as SFG_keyPressed but acts with the replayed demo. ++int8_t demoKeyPressed(uint8_t key) ++{ ++ uint16_t b = demo.currentRecord >= 0 ? ++ demo.records[demo.currentRecord].buttonStates : 0; ++ ++ return (b >> key) & 0x01; ++} ++ ++// Behaves the same as SFG_getMouseOffset but acts with the replayed demo. ++void demoGetMouseOffset(int16_t *x, int16_t *y) ++{ ++ *x = 0; ++ *y = 0; ++ ++ if (demo.currentRecord >= 0) ++ { ++ *x = demo.records[demo.currentRecord].mouseDx; ++ *y = demo.records[demo.currentRecord].mouseDy; ++ } ++} ++ ++/* Loads demo from demo file or just initializes a new demo. The action ++ parameter is DEMO_NOTHING, DEMO_REPLAY or DEMO_REPLAY_RECORD. */ ++void demoInit(uint8_t action) ++{ ++ DEMO_PRINT("initializing"); ++ demo.action = action; ++ ++ if (action == DEMO_NOTHING) ++ { ++ demo.state = DEMO_STATE_DONE; ++ return; ++ } ++ ++ DEMO_PRINT("rewinding"); ++ ++ demo.state = DEMO_STATE_REWINDING; ++ demo.currentRecord = -1; ++ demo.recordCount = 0; ++ demo.rewindTo = 0; ++ ++ char line[128]; ++ FILE *file = fopen(DEMO_FILENAME,"r"); ++ ++ if (file == NULL) ++ { ++ DEMO_PRINT("couldn't open demo file for reading"); ++ return; ++ } ++ ++ while (fgets(line,128,file)) ++ { ++ if (line[0] == '*') ++ { ++ if (demo.recordCount >= 1) ++ demo.rewindTo = demo.records[demo.recordCount - 1].frame + 1; ++ } ++ else if (line[0] != '#') ++ { ++ unsigned long f, b; ++ int dx, dy; ++ ++ if (demo.recordCount >= DEMO_MAXRECORDS) ++ { ++ DEMO_PRINT("demo too big"); ++ return; ++ } ++ ++ if (sscanf(line," %lu %lu %d %d",&f,&b,&dx,&dy) != 4) ++ DEMO_PRINT("bad format of line in demo"); ++ ++ if (demo.recordCount >= 1 && ++ f <= demo.records[demo.recordCount - 1].frame) ++ { ++ DEMO_PRINT("demo has backwards records"); ++ return; ++ } ++ ++ demo.records[demo.recordCount].buttonStates = 0; ++ ++ for (int i = 0; i < 32; ++i) ++ { ++ demo.records[demo.recordCount].buttonStates |= ((b % 10) >= 1) << i; ++ b /= 10; ++ } ++ ++ demo.records[demo.recordCount].frame = f; ++ demo.records[demo.recordCount].mouseDx = dx; ++ demo.records[demo.recordCount].mouseDy = dy; ++ ++ demo.recordCount++; ++ } ++ } ++ ++ fclose(file); ++} ++ ++// Call before program end. ++void demoEnd(void) ++{ ++ if (demo.action == DEMO_REPLAY_RECORD) ++ { ++ DEMO_PRINT("saving to file"); ++ ++ FILE *file = fopen(DEMO_FILENAME,"w"); ++ ++ if (file == NULL) ++ { ++ DEMO_PRINT("couldn't open demo file for writing"); ++ return; ++ } ++ ++ fprintf(file,"# Anarch demo, %d x %d, %d FPS, v. " SFG_VERSION_STRING "\n", ++ SFG_SCREEN_RESOLUTION_X,SFG_SCREEN_RESOLUTION_Y,SFG_FPS); ++ ++ for (int i = 0; i < demo.recordCount; ++i) ++ { ++ unsigned long b = 0; ++ ++ for (int j = 0; j < 32; ++j) ++ b = b * 10 + ++ ((demo.records[i].buttonStates & (((uint32_t) 0x01) << (31 - j))) != 0); ++ ++ fprintf(file,"%u %016lu %d %d\n", ++ demo.records[i].frame,b,demo.records[i].mouseDx,demo.records[i].mouseDy); ++ } ++ ++ fclose(file); ++ } ++} +diff --git a/main_sdl.c b/main_sdl.c +index b2b28ae..9dd2649 100644 +--- a/main_sdl.c ++++ b/main_sdl.c +@@ -97,8 +97,13 @@ + #include + #include + ++void stepDemo(void); ++ ++#define SFG_GAME_STEP_COMMAND stepDemo(); ++ + #include "game.h" + #include "sounds.h" ++#include "demo.h" + + const uint8_t *sdlKeyboardState; + uint8_t webKeyboardState[SFG_KEY_COUNT]; +@@ -113,6 +118,11 @@ SDL_Renderer *renderer; + SDL_Texture *texture; + SDL_Surface *screenSurface; + ++void stepDemo(void) ++{ ++ demoFrameStart(); ++} ++ + // now implement the Anarch API functions (SFG_*) + + void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex) +@@ -122,7 +132,7 @@ void SFG_setPixel(uint16_t x, uint16_t y, uint8_t colorIndex) + + uint32_t SFG_getTimeMs() + { +- return SDL_GetTicks(); ++ return SDL_GetTicks() + demo.rewindTo * SFG_MS_PER_FRAME; + } + + void SFG_save(uint8_t data[SFG_SAVE_SIZE]) +@@ -185,6 +195,12 @@ int8_t mouseMoved = 0; /* Whether the mouse has moved since program started, + + void SFG_getMouseOffset(int16_t *x, int16_t *y) + { ++ if (demo.state == DEMO_STATE_REWINDING || demo.state == DEMO_STATE_PLAYING) ++ { ++ demoGetMouseOffset(x,y); ++ return; ++ } ++ + #ifndef __EMSCRIPTEN__ + if (mouseMoved) + { +@@ -220,6 +236,9 @@ void SFG_processEvent(uint8_t event, uint8_t data) + + int8_t SFG_keyPressed(uint8_t key) + { ++ if (demo.state == DEMO_STATE_REWINDING || demo.state == DEMO_STATE_PLAYING) ++ return demoKeyPressed(key); ++ + if (webKeyboardState[key]) // this only takes effect in the web version + return 1; + +@@ -368,6 +387,9 @@ void SFG_setMusic(uint8_t value) + + void SFG_playSound(uint8_t soundIndex, uint8_t volume) + { ++ if (demo.state == DEMO_STATE_REWINDING) ++ return; ++ + uint16_t pos = (audioPos + + ((SFG_game.frame - audioUpdateFrame) * SFG_MS_PER_FRAME * 8)) % + SFG_SFX_SAMPLE_COUNT; +@@ -393,6 +415,7 @@ int main(int argc, char *argv[]) + uint8_t argHelp = 0; + uint8_t argForceWindow = 0; + uint8_t argForceFullscreen = 0; ++ uint8_t argDemo = DEMO_NOTHING; + + #ifndef __EMSCRIPTEN__ + argForceFullscreen = 1; +@@ -409,6 +432,10 @@ int main(int argc, char *argv[]) + argForceWindow = 1; + else if (argv[i][0] == '-' && argv[i][1] == 'f' && argv[i][2] == 0) + argForceFullscreen = 1; ++ else if (argv[i][0] == '-' && argv[i][1] == 'd' && argv[i][2] == 0) ++ argDemo = DEMO_REPLAY; ++ else if (argv[i][0] == '-' && argv[i][1] == 'D' && argv[i][2] == 0) ++ argDemo = DEMO_REPLAY_RECORD; + else + puts("SDL: unknown argument"); + } +@@ -423,7 +450,9 @@ int main(int argc, char *argv[]) + puts("CLI flags:\n"); + puts("-h print this help and exit"); + puts("-w force window"); +- puts("-f force fullscreen\n"); ++ puts("-f force fullscreen"); ++ puts("-d play demo file " DEMO_FILENAME); ++ puts("-D play and record (append) demo file " DEMO_FILENAME "\n"); + puts("controls:\n"); + puts("- arrows, numpad, [W] [S] [A] [D] [Q] [E]: movement"); + puts("- mouse: rotation, [LMB] shoot, [RMB] toggle free look"); +@@ -439,6 +468,8 @@ int main(int argc, char *argv[]) + return 0; + } + ++ demoInit(argDemo); ++ + SFG_init(); + + puts("SDL: initializing SDL"); +@@ -516,6 +547,8 @@ int main(int argc, char *argv[]) + mainLoopIteration(); + #endif + ++ demoEnd(); ++ + puts("SDL: freeing SDL"); + + SDL_GameControllerClose(sdlController);