123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- 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 <stdio.h>
- +#include <stdint.h>
- +#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 <unistd.h>
- #include <SDL2/SDL.h>
-
- +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);
|