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