summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Michael Smith <mikesmiffy128@gmail.com> 2026-04-04 01:34:31 +0100
committerGravatar Michael Smith <mikesmiffy128@gmail.com> 2026-10-10 04:23:05 +0100
commitc219ee378c374ceaf576ac477ccbe34a73e65d94 (patch)
tree2c849a5e0b76edb99d768eb68ab40624398a2524
parent3ddb31ea086155c551507357b77d83d5b57935c6 (diff)
downloadsst-c219ee378c374ceaf576ac477ccbe34a73e65d94.tar.gz
sst-c219ee378c374ceaf576ac477ccbe34a73e65d94.zip
Add very basic speedrun demo record helper
To be enhanced and extended later!
-rwxr-xr-xcompile1
-rw-r--r--compile.bat1
-rw-r--r--src/demorec.h2
-rw-r--r--src/speedrun.c242
-rw-r--r--src/sst.h2
5 files changed, 246 insertions, 2 deletions
diff --git a/compile b/compile
index 0860bdf..39a2d99 100755
--- a/compile
+++ b/compile
@@ -86,6 +86,7 @@ src="\
nosleep.c
os.c
portalcolours.c
+ speedrun.c
sst.c
trace.c
xhair.c"
diff --git a/compile.bat b/compile.bat
index b101ebe..b015925 100644
--- a/compile.bat
+++ b/compile.bat
@@ -104,6 +104,7 @@ setlocal DisableDelayedExpansion
:+ portalcolours.c
:+ portalisg.c
:+ rinput.c
+:+ speedrun.c
:+ sst.c
:+ trace.c
:+ xhair.c
diff --git a/src/demorec.h b/src/demorec.h
index 4e33cc5..d6ce498 100644
--- a/src/demorec.h
+++ b/src/demorec.h
@@ -26,7 +26,7 @@ extern struct CDemoRecorder { void **vtable; } *demorecorder;
/*
* Whether to ignore the value of the sst_autorecord cvar and just keep
- * recording anyway. Will be used to further automate demo recording later.
+ * recording anyway. Currently used by the speedrun recording feature.
*/
extern bool demorec_forceauto;
diff --git a/src/speedrun.c b/src/speedrun.c
new file mode 100644
index 0000000..8c2d10b
--- /dev/null
+++ b/src/speedrun.c
@@ -0,0 +1,242 @@
+/*
+ * Copyright © Michael Smith <mikesmiffy128@gmail.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <time.h>
+
+#include "con_.h"
+#include "demorec.h"
+#include "errmsg.h"
+#include "feature.h"
+#include "gameinfo.h"
+#include "intdefs.h"
+#include "mem.h"
+#include "os.h"
+#include "sst.h"
+
+#ifdef _WIN32
+#define fS "S"
+#define localtime_r localtime_s
+#else
+#define fS "s"
+#endif
+
+FEATURE("speedrun recording helper")
+REQUIRE(demorec)
+
+DEF_FEAT_CVAR(sst_speedrun_dir, "Directory for speedrun demos (\"\" for none)",
+ "speedruns", CON_ARCHIVE)
+
+DEF_FEAT_CVAR(sst_speedrun_usesubdirs,
+ "Use timestamped subdirectories for each run", 1, CON_ARCHIVE)
+
+static bool recording = false;
+
+static inline struct datetime {
+ s16 Y; s8 M, D;
+ s8 h, m, s;
+} datetime() {
+ // TODO(opt): doing this without the libc dep would be nice, but bleh long
+ // division figure out later
+ struct tm tm;
+ vlong epoch = time(0);
+ localtime_r(&tm, &epoch);
+ return (struct datetime){
+ tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
+ tm.tm_hour, tm.tm_min, tm.tm_sec
+ };
+}
+static void fmttime(os_char out[static 14], const struct datetime *dt) {
+ uint Y = dt->Y, M = dt->M, D = dt->D, h = dt->h, m = dt->m, s = dt->s;
+ uint div10 = Y * 3435973837ull >> 35;
+ uint div100 = Y * 1374389535ull >> 37; // increase ILP
+ uint mod10 = Y - div10 * 10, mod100div10 = div10 - div100 * 10;
+ Y = div100;
+ out[2] = OS_LIT('0') + mod100div10; out[3] = OS_LIT('0') + mod10;
+ div10 = Y * 3435973837ull >> 35; mod10 = Y - div10 * 10;
+ // assuming a 4-digit year here; will be true for the next ~8k years!
+ out[0] = OS_LIT('0') + div10; out[1] = OS_LIT('0') + mod10;
+ // for the month, we don't need to divide (or rather multiply-shift) at all;
+ // the tens column is always either zero or one. */
+ int tens = M >= 10;
+ out[4] = OS_LIT('0') + tens; out[5] = OS_LIT('0') + M - 10 * tens;
+ // the rest of these are always 2-digit, which makes things quite easy.
+ div10 = D * 3435973837ull >> 35; mod10 = D - div10 * 10;
+ out[6] = OS_LIT('0') + div10; out[7] = OS_LIT('0') + mod10;
+ div10 = h * 3435973837ull >> 35; mod10 = h - div10 * 10;
+ out[8] = OS_LIT('0') + div10; out[9] = OS_LIT('0') + mod10;
+ div10 = m * 3435973837ull >> 35; mod10 = m - div10 * 10;
+ out[10] = OS_LIT('0') + div10; out[11] = OS_LIT('0') + mod10;
+ div10 = s * 3435973837ull >> 35; mod10 = s - div10 * 10;
+ out[12] = OS_LIT('0') + div10; out[13] = OS_LIT('0') + mod10;
+}
+
+HANDLE_EVENT(DemoControlAllowed) {
+ if (recording) {
+ con_warn("%s during speedrun recording. %s\n",
+ "Manual demo control disabled",
+ "Use sst_speedrun_stop to finish a run.");
+ return false;
+ }
+ return true;
+}
+
+HANDLE_EVENT(AllowPluginLoading) {
+ if (recording) {
+ con_warn("%s during speedrun recording. %s\n",
+ "Cannot load or unload plugins",
+ "Use sst_speedrun_stop to finish a run.");
+ return false;
+ }
+ return true;
+}
+
+DEF_CCMD_HERE(sst_speedrun_start, "Start recording demos for a speedrun", 0) {
+ if (recording) {
+ con_warn("Already recording a speedrun. %s\n",
+ "Use sst_speedrun_stop to finish a run.");
+ return;
+ }
+ if (demorec_demonum() != -1) {
+ con_warn("%sUse the stop command to finish the recording first.\n",
+ "Currently manually recording a demo. ");
+ return;
+ }
+ const char *basedir = con_getvarstr(sst_speedrun_dir);
+#ifdef _WIN32
+ if (strpbrk(basedir, "/\\")) {
+#else
+ if (strchr(basedir, '/')) {
+#endif
+ errmsg_errorx("sst_speedrun_dir must not contain a slash");
+ return;
+ }
+ if (basedir[0] == '.' && (basedir[1] == '\0' ||
+ basedir[1] == '.' && basedir[2] == '\0')) {
+ errmsg_errorx("sst_speedrun_dir must not point outside the game");
+ return;
+ }
+ // basedir/yyyymmddhhmmssNN/yymmdd
+ int baselen = strlen(basedir);
+ int maxlen = baselen + !!baselen; // slash, unless empty (no subdir)
+ bool usesubdirs = con_getvari(sst_speedrun_usesubdirs);
+ if (usesubdirs) maxlen += sizeof("yyyymmddhhmmss/") - 1;
+ // for this length calc we assume there'll never been 100 *million* demos
+ // (the max possible int would of course be 2 billion, but the game would
+ // likely crash way before then)
+ maxlen += sizeof("yymmddhhmmss_99999999.dem"); // include \0
+ if (maxlen > PATH_MAX) {
+ errmsg_errorx("demo filename is too long");
+ return;
+ }
+ if (os_strlen(gameinfo_gamedir) + 1 + maxlen > PATH_MAX) {
+ errmsg_errorx("path to game directory is too long for demo path");
+ return;
+ }
+ os_char path[PATH_MAX];
+ // TODO(opt): maybe gameinfo should have globals for the lengths?
+ int pathsz = os_strlen(gameinfo_gamedir);
+ os_spancopy(path, gameinfo_gamedir, pathsz);
+ path[pathsz++] = OS_LIT('/');
+ int prefixsz = pathsz;
+ if (basedir[0]) {
+#ifdef _WIN32
+ for (int i = 0; i < baselen; ++i) path[pathsz + i] = basedir[i];
+#else
+ mem_copy(path + pathsz, basedir, baselen);
+#endif
+ pathsz += baselen;
+ path[pathsz] = OS_LIT('\0');
+ if (!os_mkdir(path) && os_lasterror() != OS_EEXIST) {
+ errmsg_errorsys("couldn't create directory %" fS, path);
+ return;
+ }
+ path[pathsz++] = OS_LIT('/');
+ }
+ struct datetime dt = datetime();
+ if (usesubdirs) {
+ fmttime(path + pathsz, &dt);
+ path[pathsz + 14] = OS_LIT('\0');
+ if (!os_mkdir(path) && os_lasterror() != OS_EEXIST) {
+ errmsg_errorsys("couldn't create directory %" fS, path);
+ return;
+ }
+ path[pathsz + 14] = OS_LIT('/');
+ __builtin_memcpy_inline(path + pathsz + 15, path + pathsz,
+ 14 * sizeof(*path));
+ __builtin_memcpy_inline(path + pathsz + 29, OS_LIT(".dem"),
+ sizeof(OS_LIT(".dem")));
+ pathsz += 33;
+ }
+ else {
+ fmttime(path + pathsz, &dt);
+ __builtin_memcpy_inline(path + pathsz + 14, OS_LIT(".dem"),
+ sizeof(OS_LIT(".dem")));
+ pathsz += 18;
+ }
+ struct os_stat st;
+ if (os_stat(path, &st) != -1) {
+ errmsg_errorx("demo with this timestamp already exists; "
+ "try again in ~1s");
+ return;
+ }
+ else if (errno != ENOENT) {
+ errmsg_errorstd("couldn't get info for file %" fS, path);
+ return;
+ }
+#ifdef _WIN32
+ char recordpath[PATH_MAX];
+ for (int i = 0; i < pathsz - prefixsz - 4; ++i) { // leave off .dem
+ recordpath[i] = path[i + prefixsz]; // back to ascii
+ }
+#else
+ char *recordpath = path + prefixsz;
+#endif
+ recordpath[pathsz - prefixsz - 4] = OS_LIT('\0');
+ con_msg("Speedrun recording started\n");
+ recording = true;
+ demorec_forceauto = true;
+ demorec_start(recordpath);
+}
+
+DEF_CCMD_HERE(sst_speedrun_stop, "Finish recording speedrun demos", 0) {
+ if (!recording) {
+ if (demorec_demonum() != -1) {
+ con_warn("%sUse the stop command to end the recording.\n",
+ "Currently manually recording a demo. ");
+ }
+ // seems best to be silent if not recording at all, for keybinds etc.
+ return;
+ }
+ recording = false;
+ demorec_forceauto = false;
+ int n = demorec_stop();
+ if (n) {
+ con_msg("Finished speedrun; recorded %d demos.\n", n);
+ }
+ else {
+ con_msg("Cancelled speedrun; no demos were recorded.\n");
+ // XXX: do we want to delete the now-empty dirs in this case?
+ }
+}
+
+// TODO(rta): add some sort of abort/abandon command too, which deletes demos
+// rather than leaving them behind.
+
+INIT { return FEAT_OK; }
+
+// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/sst.h b/src/sst.h
index 8bed771..fbc9587 100644
--- a/src/sst.h
+++ b/src/sst.h
@@ -24,7 +24,7 @@
DECL_EVENT(ClientActive, struct edict */*player*/)
DECL_EVENT(Tick, bool /*simulating*/)
-DECL_PREDICATE(AllowPluginLoading, void)
+DECL_PREDICATE(AllowPluginLoading, bool)
DECL_EVENT(PluginLoaded, void)
DECL_EVENT(PluginUnloaded, void)