mirror of
https://gitlab.com/drummyfish/anarch.git
synced 2024-11-21 16:35:08 -05:00
412 lines
11 KiB
Diff
412 lines
11 KiB
Diff
|
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);
|