aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENCE2
-rwxr-xr-xcompile27
-rw-r--r--compile.bat26
-rw-r--r--dist/LICENCE.linux2
-rw-r--r--dist/LICENCE.windows2
-rw-r--r--src/3p/chibicc/chibicc.h3
-rw-r--r--src/3p/chibicc/hashmap.c7
-rw-r--r--src/ac.c23
-rw-r--r--src/alias.c14
-rw-r--r--src/autojump.c15
-rw-r--r--src/bind.c4
-rw-r--r--src/build/cmeta.c454
-rw-r--r--src/build/cmeta.h115
-rw-r--r--src/build/codegen.c537
-rw-r--r--src/build/gluegen.c963
-rw-r--r--src/build/mkentprops.c12
-rw-r--r--src/build/mkgamedata.c45
-rw-r--r--src/build/skiplist.h206
-rw-r--r--src/build/vec.h95
-rw-r--r--src/clientcon.c4
-rw-r--r--src/con_.c16
-rw-r--r--src/con_.h46
-rw-r--r--src/democustom.c3
-rw-r--r--src/demorec.c17
-rw-r--r--src/engineapi.c9
-rw-r--r--src/ent.c11
-rw-r--r--src/event.h4
-rw-r--r--src/fastfwd.c40
-rw-r--r--src/feature.h115
-rw-r--r--src/fixes.c2
-rw-r--r--src/fov.c25
-rw-r--r--src/gamedata.c22
-rw-r--r--src/gamedata.h5
-rw-r--r--src/gameserver.c6
-rw-r--r--src/gametype.h6
-rw-r--r--src/hud.c10
-rw-r--r--src/inputhud.c56
-rw-r--r--src/kvsys.c2
-rw-r--r--src/l4dmm.c11
-rw-r--r--src/l4dreset.c45
-rw-r--r--src/l4dwarp.c31
-rw-r--r--src/langext.h3
-rw-r--r--src/nomute.c14
-rw-r--r--src/nosleep.c8
-rw-r--r--src/os.h10
-rw-r--r--src/portalcolours.c34
-rw-r--r--src/rinput.c14
-rw-r--r--src/sst.c38
-rw-r--r--src/trace.c17
-rw-r--r--src/x86util.h4
-rw-r--r--src/xhair.c41
-rw-r--r--test/hook.test.c51
52 files changed, 1666 insertions, 1606 deletions
diff --git a/LICENCE b/LICENCE
index 63cefdb..8c8afa8 100644
--- a/LICENCE
+++ b/LICENCE
@@ -1,6 +1,6 @@
Except where otherwise noted, the following terms apply:
════════════════════════════════════════════════════════════════════════════════
-Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
Copyright © 2024 Willian Henrique <wsimanbrazil@yahoo.com.br>
Copyright © 2024 Hayden K <imaciidz@gmail.com>
Copyright © 2023 Matthew Wozniak <sirtomato999@gmail.com>
diff --git a/compile b/compile
index 31e4b8f..5b55f55 100755
--- a/compile
+++ b/compile
@@ -21,6 +21,8 @@ warnings="-Wall -pedantic -Wno-parentheses -Wno-missing-braces \
-Wno-gnu-zero-variadic-macro-arguments -Werror=implicit-function-declaration \
-Werror=vla"
+stdflags="-std=c2x -D_DEFAULT_SOURCE -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64"
+
dbg=0
if [ "$dbg" = 1 ]; then
cflags="-O0 -g3"
@@ -38,10 +40,8 @@ cc() {
# ugly annoying special case
if [ "$_mn" = " -DMODULE_NAME=con_" ]; then _mn=" -DMODULE_NAME=con"
elif [ "$_mn" = "-DMODULE_NAME=sst" ]; then _mn=; fi
- # note: using typeof and bool from C23 - see detailed comment in compile.bat
$CC -c -flto -fpic -fno-ident $cflags $warnings -I.build/include \
- -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64$_mn \
- -Dtypeof=__typeof -include stdbool.h -o ".build/${_bn%%.c}.o" "src/$1"
+ $stdflags$_mn -o ".build/${_bn%%.c}.o" "src/$1"
}
ld() {
@@ -69,7 +69,6 @@ src="\
fastfwd.c
fixes.c
fov.c
- gamedata.c
gameinfo.c
gameserver.c
hexcolour.c
@@ -85,20 +84,20 @@ src="\
portalcolours.c
sst.c
trace.c
- xhair.c
- x86.c"
+ x86.c
+ xhair.c"
if [ "$dbg" = 1 ]; then src="$src \
dbg.c
udis86.c"
fi
-$HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \
- -o .build/codegen src/build/codegen.c src/build/cmeta.c src/os.c
-$HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \
+$HOSTCC -O2 -fuse-ld=lld $warnings $stdflags \
+ -o .build/gluegen src/build/gluegen .c src/build/cmeta.c src/os.c
+$HOSTCC -O2 -fuse-ld=lld $warnings $stdflags \
-o .build/mkgamedata src/build/mkgamedata.c src/os.c
-$HOSTCC -O2 -fuse-ld=lld $warnings -D_FILE_OFFSET_BITS=64 -include stdbool.h \
+$HOSTCC -O2 -fuse-ld=lld $warnings $stdflags \
-o .build/mkentprops src/build/mkentprops.c src/os.c
-.build/codegen `for s in $src; do echo "src/$s"; done`
+.build/gluegen `for s in $src; do echo "src/$s"; done`
.build/mkgamedata gamedata/engine.txt gamedata/gamelib.txt gamedata/inputsystem.txt \
gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt
.build/mkentprops gamedata/entprops.txt
@@ -107,14 +106,14 @@ $CC -shared -fpic -fuse-ld=lld -O0 -w -o .build/libtier0.so src/stubs/tier0.c
$CC -shared -fpic -fuse-ld=lld -O0 -w -o .build/libvstdlib.so src/stubs/vstdlib.c
ld
-$HOSTCC -O2 -g3 -include test/test.h -o .build/bitbuf.test test/bitbuf.test.c
+$HOSTCC -O2 -g3 $warnings $stdflags -include test/test.h -o .build/bitbuf.test test/bitbuf.test.c
.build/bitbuf.test
# skipping this test on linux for now, since inline hooks aren't compiled in
#$HOSTCC -m32 -O2 -g3 -include test/test.h -o .build/hook.test test/hook.test.c
#.build/hook.test
-$HOSTCC -O2 -g3 -include test/test.h -o .build/kv.test test/kv.test.c
+$HOSTCC -O2 -g3 $warnings $stdflags -include test/test.h -o .build/kv.test test/kv.test.c
.build/kv.test
-$HOSTCC -O2 -g3 -include test/test.h -o .build/x86.test test/x86.test.c
+$HOSTCC -O2 -g3 $warnings $stdflags -include test/test.h -o .build/x86.test test/x86.test.c
.build/x86.test
# vi: sw=4 tw=4 noet tw=80 cc=80
diff --git a/compile.bat b/compile.bat
index 9f736e4..8b0e877 100644
--- a/compile.bat
+++ b/compile.bat
@@ -47,9 +47,8 @@ set objs=%objs% .build/%basename%.o
:: year to get anything done. typeof=__typeof prevents pedantic warnings caused
:: by typeof still technically being an extension, and stdbool gives us
:: predefined bool/true/false before compilers start doing that by default
-%CC% -c -flto -mno-stack-arg-probe %cflags% %warnings% -I.build/include ^
--D_CRT_SECURE_NO_WARNINGS -D_DLL -DWIN32_LEAN_AND_MEAN -DNOMINMAX%dmodname% ^
--Dtypeof=__typeof -include stdbool.h -o .build/%basename%.o %1 || goto :end
+%CC% -c -flto -mno-stack-arg-probe %cflags% %warnings% %stdflags% -I.build/include ^
+-D_DLL%dmodname% -o .build/%basename%.o %1 || goto :end
goto :eof
:src
@@ -57,6 +56,8 @@ goto :eof
:main
+set stdflags=-std=c2x -D_CRT_SECURE_NO_WARNINGS -DWIN32_LEAN_AND_MEAN -DNOMINMAX
+
set src=
:: funny hack to build a list conveniently, lol.
setlocal EnableDelayedExpansion
@@ -80,7 +81,6 @@ setlocal DisableDelayedExpansion
:+ fastfwd.c
:+ fixes.c
:+ fov.c
-:+ gamedata.c
:+ gameinfo.c
:+ gameserver.c
:+ hexcolour.c
@@ -98,8 +98,8 @@ setlocal DisableDelayedExpansion
:+ rinput.c
:+ sst.c
:+ trace.c
-:+ xhair.c
:+ x86.c
+:+ xhair.c
:: just tack these on, whatever (repeated condition because of expansion memes)
if "%dbg%"=="1" set src=%src% src/dbg.c
if "%dbg%"=="1" set src=%src% src/udis86.c
@@ -115,13 +115,13 @@ if %host64%==1 (
%CC% -fuse-ld=lld -shared -O0 -w -o .build/tier0.dll src/stubs/tier0.c
%CC% -fuse-ld=lld -shared -O0 -w -o .build/vstdlib.dll src/stubs/vstdlib.c
-%HOSTCC% -fuse-ld=lld -O2 %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^
--L.build %lbcryptprimitives_host% -o .build/codegen.exe src/build/codegen.c src/build/cmeta.c src/os.c || goto :end
-%HOSTCC% -fuse-ld=lld -O2 %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^
+%HOSTCC% -fuse-ld=lld -O2 %warnings% %stdflags% -include stdbool.h ^
+-L.build %lbcryptprimitives_host% -o .build/gluegen.exe src/build/gluegen.c src/build/cmeta.c src/os.c || goto :end
+%HOSTCC% -fuse-ld=lld -O2 %warnings% %stdflags% -include stdbool.h ^
-L.build %lbcryptprimitives_host% -o .build/mkgamedata.exe src/build/mkgamedata.c src/os.c || goto :end
-%HOSTCC% -fuse-ld=lld -O2 -g %warnings% -D_CRT_SECURE_NO_WARNINGS -include stdbool.h ^
+%HOSTCC% -fuse-ld=lld -O2 %warnings% %stdflags% -include stdbool.h ^
-L.build %lbcryptprimitives_host% -o .build/mkentprops.exe src/build/mkentprops.c src/os.c || goto :end
-.build\codegen.exe%src% || goto :end
+.build\gluegen.exe%src% || goto :end
.build\mkgamedata.exe gamedata/engine.txt gamedata/gamelib.txt gamedata/inputsystem.txt ^
gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt || goto :end
.build\mkentprops.exe gamedata/entprops.txt || goto :end
@@ -158,12 +158,12 @@ move /y .build\sst.dll sst.dll >nul 2>nul || (
:: in some eventual future invocation
if exist .build\sst.old.dll del .build\sst.old.dll >nul 2>nul
-%HOSTCC% -fuse-ld=lld -O2 -g -include test/test.h -o .build/bitbuf.test.exe test/bitbuf.test.c || goto :end
+%HOSTCC% -fuse-ld=lld -O2 -g %warnings% %stdflags% -include test/test.h -o .build/bitbuf.test.exe test/bitbuf.test.c || goto :end
.build\bitbuf.test.exe || goto :end
:: special case: test must be 32-bit
-%HOSTCC% -fuse-ld=lld -m32 -O2 -g -L.build -lbcryptprimitives -include test/test.h -o .build/hook.test.exe test/hook.test.c || goto :end
+%HOSTCC% -fuse-ld=lld -m32 -O2 -g %warnings% %stdflags% -L.build -lbcryptprimitives -include test/test.h -o .build/hook.test.exe test/hook.test.c || goto :end
.build\hook.test.exe || goto :end
-%HOSTCC% -fuse-ld=lld -O2 -g -include test/test.h -o .build/x86.test.exe test/x86.test.c || goto :end
+%HOSTCC% -fuse-ld=lld -O2 -g %warnings% %stdflags% -include test/test.h -o .build/x86.test.exe test/x86.test.c || goto :end
.build\x86.test.exe || goto :end
:end
diff --git a/dist/LICENCE.linux b/dist/LICENCE.linux
index 2647bda..8991329 100644
--- a/dist/LICENCE.linux
+++ b/dist/LICENCE.linux
@@ -1,6 +1,6 @@
Source Speedrun Tools is released under the following copyright licence:
════════════════════════════════════════════════════════════════════════════════
-Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
Copyright © 2024 Willian Henrique <wsimanbrazil@yahoo.com.br>
Copyright © 2024 Hayden K <imaciidz@gmail.com>
Copyright © 2023 Matthew Wozniak <sirtomato999@gmail.com>
diff --git a/dist/LICENCE.windows b/dist/LICENCE.windows
index 7905ac8..1691946 100644
--- a/dist/LICENCE.windows
+++ b/dist/LICENCE.windows
@@ -1,6 +1,6 @@
Source Speedrun Tools is released under the following copyright licence:
════════════════════════════════════════════════════════════════════════════════
-Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
Copyright © 2024 Willian Henrique <wsimanbrazil@yahoo.com.br>
Copyright © 2024 Hayden K <imaciidz@gmail.com>
Copyright © 2023 Matthew Wozniak <sirtomato999@gmail.com>
diff --git a/src/3p/chibicc/chibicc.h b/src/3p/chibicc/chibicc.h
index 2a80ecf..f3f87ab 100644
--- a/src/3p/chibicc/chibicc.h
+++ b/src/3p/chibicc/chibicc.h
@@ -117,9 +117,6 @@ static inline char *format(const char *fmt, ...) {
return ret;
}
-#define unreachable() \
- error("internal error at %s:%d", __FILE__, __LINE__)
-
//
// type.c
//
diff --git a/src/3p/chibicc/hashmap.c b/src/3p/chibicc/hashmap.c
index 47110c6..2090274 100644
--- a/src/3p/chibicc/hashmap.c
+++ b/src/3p/chibicc/hashmap.c
@@ -2,6 +2,9 @@
#include "chibicc.h"
+// mike: moved from chibicc.h and also renamed, to avoid conflicts with langext.h
+#define chibi_unreachable() error("internal error at %s:%d", __FILE__, __LINE__)
+
// Initial hash bucket size
#define INIT_SIZE 16
@@ -70,7 +73,7 @@ static HashEntry *get_entry(HashMap *map, char *key, int keylen) {
if (ent->key == NULL)
return NULL;
}
- unreachable();
+ chibi_unreachable();
}
static HashEntry *get_or_insert_entry(HashMap *map, char *key, int keylen) {
@@ -102,7 +105,7 @@ static HashEntry *get_or_insert_entry(HashMap *map, char *key, int keylen) {
return ent;
}
}
- unreachable();
+ chibi_unreachable();
}
void *hashmap_get(HashMap *map, char *key) {
diff --git a/src/ac.c b/src/ac.c
index 7f3157c..e8aef8d 100644
--- a/src/ac.c
+++ b/src/ac.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -32,13 +32,13 @@
#include "crypto.h"
#include "democustom.h"
#include "demorec.h"
-#include "hook.h"
#include "engineapi.h"
#include "errmsg.h"
#include "event.h"
#include "feature.h"
#include "gamedata.h"
#include "gametype.h"
+#include "hook.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
@@ -50,6 +50,7 @@
#include "x86util.h"
FEATURE()
+GAMESPECIFIC(L4D) // TODO(compat): wanna add support for more stuff, obviously!
REQUIRE(bind)
REQUIRE(democustom)
REQUIRE_GAMEDATA(vtidx_GetDesktopResolution)
@@ -291,7 +292,7 @@ static void hook_Key_Event(struct inputevent *ev) {
//const char *desc[] = {"DOWN", "UP", "DBL"};
//const char desclen[] = {4, 2, 3};
switch (ev->type) {
- CASES(BTNDOWN, BTNUP, BTNDOUBLECLICK):;
+ CASES(BTNDOWN, BTNUP, BTNDOUBLECLICK):
// TODO(rta): do something interesting with button data
//uchar buf[28], *p = buf;
//msg_putasz4(p, 2); p += 1;
@@ -378,24 +379,20 @@ HANDLE_EVENT(PluginUnloaded, void) {
// TODO(rta): do something with plugin list here
}
-PREINIT {
- return GAMETYPE_MATCHES(L4D); // TODO(compat): add more here obviously
-}
-
INIT {
- if_cold (!find_Key_Event()) return false;
+ if_cold (!find_Key_Event()) return FEAT_INCOMPAT;
orig_Key_Event = (Key_Event_func)hook_inline((void *)orig_Key_Event,
(void *)&hook_Key_Event);
if_cold (!orig_Key_Event) {
errmsg_errorsys("couldn't hook Key_Event function");
- return false;
+ return FEAT_FAIL;
}
#ifdef _WIN32
keybox = VirtualAlloc(0, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if_cold (!keybox) {
errmsg_errorsys("couldn't allocate memory for session state");
- return false;
+ return FEAT_FAIL;
}
if_cold (!VirtualLock(keybox, 4096)) {
errmsg_errorsys("couldn't secure session state");
@@ -411,7 +408,7 @@ INIT {
keybox = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if_cold (keybox == MAP_FAILED) {
errmsg_errorstd("couldn't allocate memory for session state");
- return false;
+ return FEAT_FAIL;
}
// linux-specific madvise stuff (there are some equivalents in OpenBSD and
// FreeBSD, if anyone's wondering, but we don't need to worry about those)
@@ -432,7 +429,7 @@ INIT {
// run of bytes
memcpy(keybox->lbpub, lbpubkeys[LBPK_L4D], 32);
}
- return true;
+ return FEAT_OK;
#ifdef _WIN32
e: WerUnregisterExcludedMemoryBlock(keybox); // this'd better not fail!
@@ -441,7 +438,7 @@ e2: VirtualFree(keybox, 4096, MEM_RELEASE);
e: munmap(keybox, 4096);
#endif
unhook_inline((void *)orig_Key_Event);
- return false;
+ return FEAT_FAIL;
}
END {
diff --git a/src/alias.c b/src/alias.c
index e6f5f7e..a2d2b21 100644
--- a/src/alias.c
+++ b/src/alias.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -50,7 +50,7 @@ void alias_rm(const char *name) {
}
}
-DEF_CCMD_HERE_UNREG(sst_alias_clear, "Remove all command aliases", 0) {
+DEF_FEAT_CCMD_HERE(sst_alias_clear, "Remove all command aliases", 0) {
if (cmd->argc != 1) {
con_warn("usage: sst_alias_clear\n");
return;
@@ -58,7 +58,7 @@ DEF_CCMD_HERE_UNREG(sst_alias_clear, "Remove all command aliases", 0) {
alias_nuke();
}
-DEF_CCMD_HERE_UNREG(sst_alias_remove, "Remove a command alias", 0) {
+DEF_FEAT_CCMD_HERE(sst_alias_remove, "Remove a command alias", 0) {
if (cmd->argc != 2) {
con_warn("usage: sst_alias_remove name\n");
return;
@@ -91,16 +91,14 @@ static bool find_alias_head(con_cmdcb alias_cb) {
INIT {
// TODO(compat): no idea why sst_alias_clear crashes in p2, figure out later
- if (GAMETYPE_MATCHES(Portal2)) return false;
+ if (GAMETYPE_MATCHES(Portal2)) return FEAT_INCOMPAT;
struct con_cmd *cmd_alias = con_findcmd("alias");
if_cold (!find_alias_head(con_getcmdcb(cmd_alias))) {
errmsg_warnx("couldn't find alias list");
- return false;
+ return FEAT_INCOMPAT;
}
- con_reg(sst_alias_clear);
- con_reg(sst_alias_remove);
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/autojump.c b/src/autojump.c
index bc6739f..23bc2e0 100644
--- a/src/autojump.c
+++ b/src/autojump.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -35,8 +35,8 @@ REQUIRE_GLOBAL(factory_client) // note: server will never be null
DEF_ACCESSORS(struct CMoveData *, mv)
-DEF_CVAR(sst_autojump, "Jump upon hitting the ground while holding space", 0,
- CON_REPLICATE | CON_DEMO | CON_HIDDEN)
+DEF_FEAT_CVAR(sst_autojump, "Jump upon hitting the ground while holding space", 0,
+ CON_REPLICATE | CON_DEMO)
#define NIDX 256 // *completely* arbitrary lol
static bool justjumped[NIDX] = {0};
@@ -90,21 +90,20 @@ INIT {
gmsv = factory_server("GameMovement001", 0);
if_cold (!gmsv) {
errmsg_errorx("couldn't get server-side game movement interface");
- return false;
+ return FEAT_FAIL;
}
if_cold (!unprot(gmsv)) return false;
gmcl = factory_client("GameMovement001", 0);
if_cold (!gmcl) {
errmsg_errorx("couldn't get client-side game movement interface");
- return false;
+ return FEAT_FAIL;
}
- if_cold (!unprot(gmcl)) return false;
+ if_cold (!unprot(gmcl)) return FEAT_FAIL;
origsv = (CheckJumpButton_func)hook_vtable(*(void ***)gmsv,
vtidx_CheckJumpButton, (void *)&hooksv);
origcl = (CheckJumpButton_func)hook_vtable(*(void ***)gmcl,
vtidx_CheckJumpButton, (void *)&hookcl);
- sst_autojump->base.flags &= ~CON_HIDDEN;
if (GAMETYPE_MATCHES(Portal1)) {
// this is a stupid, stupid policy that doesn't make any sense, but I've
// tried arguing about it already and with how long it takes to convince
@@ -116,7 +115,7 @@ INIT {
sv_cheats = con_findvar("sv_cheats");
sst_autojump->cb = cheatcb;
}
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/bind.c b/src/bind.c
index 327d0c2..7db4720 100644
--- a/src/bind.c
+++ b/src/bind.c
@@ -60,9 +60,9 @@ INIT {
con_cmdcb cb = con_getcmdcb(cmd_key_listboundkeys);
if_cold (!find_keyinfo(cb)) {
errmsg_warnx("couldn't find key binding list");
- return false;
+ return FEAT_INCOMPAT;
}
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/build/cmeta.c b/src/build/cmeta.c
index 8a2416d..1903e84 100644
--- a/src/build/cmeta.c
+++ b/src/build/cmeta.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -15,22 +15,12 @@
*/
#include <stdio.h>
-#include <string.h>
+#include <stdlib.h>
#include "../intdefs.h"
+#include "../langext.h"
#include "../os.h"
#include "cmeta.h"
-#include "vec.h"
-
-/*
- * This file does C metadata parsing/scraping for the build system. This
- * facilitates tasks ranging from determining header dependencies to searching
- * for certain magic macros (for example cvar/command declarations) to generate
- * other code.
- *
- * It's a bit of a mess since it's kind of just hacked together for use at build
- * time. Don't worry about it too much.
- */
// lazy inlined 3rd party stuff {{{
// too lazy to write a C tokenizer at the moment, or indeed probably ever, so
@@ -56,350 +46,218 @@ Type *ty_double = &(Type){TY_DOUBLE, 8, 8};
Type *ty_ldouble = &(Type){TY_LDOUBLE, 16, 16};
// inline just a couple more things, super lazy, but whatever
static Type *new_type(TypeKind kind, int size, int align) {
- Type *ty = calloc(1, sizeof(Type));
- ty->kind = kind;
- ty->size = size;
- ty->align = align;
- return ty;
+ Type *ty = calloc(1, sizeof(Type));
+ ty->kind = kind;
+ ty->size = size;
+ ty->align = align;
+ return ty;
}
Type *array_of(Type *base, int len) {
- Type *ty = new_type(TY_ARRAY, base->size * len, base->align);
- ty->base = base;
- ty->array_len = len;
- return ty;
+ Type *ty = new_type(TY_ARRAY, base->size * len, base->align);
+ ty->base = base;
+ ty->array_len = len;
+ return ty;
}
#include "../3p/chibicc/hashmap.c"
#include "../3p/chibicc/strings.c"
#include "../3p/chibicc/tokenize.c"
-// one more copypaste from preprocess.c for #include <filename> and then I'm
-// done I promise
-static char *join_tokens(const Token *tok, const Token *end) {
- int len = 1;
- for (const Token *t = tok; t != end && t->kind != TK_EOF; t = t->next) {
- if (t != tok && t->has_space)
- len++;
- len += t->len;
- }
- char *buf = calloc(1, len);
- int pos = 0;
- for (const Token *t = tok; t != end && t->kind != TK_EOF; t = t->next) {
- if (t != tok && t->has_space)
- buf[pos++] = ' ';
- strncpy(buf + pos, t->loc, t->len);
- pos += t->len;
- }
- buf[pos] = '\0';
- return buf;
-}
// }}}
#ifdef _WIN32
#include "../3p/openbsd/asprintf.c" // missing from libc; plonked here for now
#endif
-static void die(const char *s) {
+static noreturn die(int status, const char *s) {
fprintf(stderr, "cmeta: fatal: %s\n", s);
- exit(100);
+ exit(status);
}
-static char *readsource(const os_char *path) {
+struct cmeta cmeta_loadfile(const os_char *path) {
int f = os_open_read(path);
- if (f == -1) return 0;
- uint bufsz = 8192;
- char *buf = malloc(bufsz);
- if (!buf) die("couldn't allocate memory");
- int nread;
- int off = 0;
- while ((nread = os_read(f, buf + off, bufsz - off)) > 0) {
- off += nread;
- if (off == bufsz) {
- bufsz *= 2;
- // somewhat arbitrary cutoff
- if (bufsz == 1 << 30) die("input file is too large");
- buf = realloc(buf, bufsz);
- if (!buf) die("couldn't reallocate memory");
- }
- }
- if (nread == -1) die("couldn't read file");
- buf[off] = 0;
+ if (f == -1) die(100, "couldn't open file");
+ vlong len = os_fsize(f);
+ if (len > 1u << 30 - 1) die(2, "input file is far too large");
+ struct cmeta ret;
+ ret.sbase = malloc(len + 1);
+ ret.sbase[len] = '\0'; // chibicc needs a null terminator
+ if (!ret.sbase) die(100, "couldn't allocate memory");
+ if (os_read(f, ret.sbase, len) != len) die(100, "couldn't read file");
+ int maxitems = len / 4; // shortest word is "END"
+ ret.nitems = 0;
+ // eventual overall memory requirement: file size * 6. seems fine to me.
+ // current memory requirement: file size * 10, + all the chibicc linked list
+ // crap. not as good but we'll continue tolerating it... probably for years!
+ //ret.itemoffs = malloc(maxitems * sizeof(*ret.itemoffs));
+ //if (!ret.itemoffs) die(100, "couldn't allocate memory");
+ ret.itemtoks = malloc(maxitems * sizeof(*ret.itemtoks));
+ if (!ret.itemtoks) die(100, "couldn't allocate memory");
+ ret.itemtypes = malloc(maxitems * sizeof(*ret.itemtypes));
+ if (!ret.itemtypes) die(100, "couldn't allocate memory");
os_close(f);
- return buf;
-}
-
-// as per cmeta.h this is totally opaque; it's actually just a Token in disguise
-struct cmeta;
-
-const struct cmeta *cmeta_loadfile(const os_char *path) {
- char *buf = readsource(path);
- if (!buf) return 0;
#ifdef _WIN32
char *realname = malloc(wcslen(path) + 1);
- if (!realname) die("couldn't allocate memory");
+ if (!realname) die(100, "couldn't allocate memory");
// XXX: being lazy about Unicode right now; a general purpose tool should
// implement WTF8 or something. SST itself doesn't have any unicode paths
- // though, so don't really care as much.
+ // though, so we don't really care as much. this code still sucks though.
*realname = *path;
for (const ushort *p = path + 1; p[-1]; ++p) realname[p - path] = *p;
#else
const char *realname = f;
#endif
- return (const struct cmeta *)tokenize_buf(realname, buf);
-}
-
-// NOTE: we don't care about conditional includes, nor do we expand macros. We
-// just parse the minimum info to get what we need for SST. Also, there's not
-// too much in the way of syntax checking; if an error gets ignored the compiler
-// picks it up anyway, and gives far better diagnostics.
-void cmeta_includes(const struct cmeta *cm,
- void (*cb)(const char *f, bool issys, void *ctxt), void *ctxt) {
- const Token *tp = (const Token *)cm;
- if (!tp || !tp->next || !tp->next->next) return; // #, include, "string"
- while (tp) {
- if (!tp->at_bol || !equal(tp, "#")) { tp = tp->next; continue; }
- if (!equal(tp->next, "include")) { tp = tp->next->next; continue; }
- tp = tp->next->next;
- if (!tp) break;
- if (tp->at_bol) tp = tp->next;
- if (!tp) break;
- if (tp->kind == TK_STR) {
- // include strings are a special case; they don't have \escapes.
- char *copy = malloc(tp->len - 1);
- if (!copy) die("couldn't allocate memory");
- memcpy(copy, tp->loc + 1, tp->len - 2);
- copy[tp->len - 2] = '\0';
- cb(copy, false, ctxt);
- //free(copy); // ??????
+ struct Token *t = tokenize_buf(realname, ret.sbase);
+ // everything is THING() or THING {} so we need at least 3 tokens ahead - if
+ // we have fewer tokens left in the file we can bail
+ if (t && t->next) while (t->next->next) {
+ if (!t->at_bol) {
+ t = t->next;
+ continue;
}
- else if (equal(tp, "<")) {
- tp = tp->next;
- if (!tp) break;
- const Token *end = tp;
- while (!equal(end, ">")) {
- end = end->next;
- if (!end) return; // shouldn't happen in valid source obviously
- if (end->at_bol) break; // ??????
- }
- char *joined = join_tokens(tp, end); // just use func from chibicc
- cb(joined, true, ctxt);
- //free(joined); // ??????
+ int type;
+ if ((equal(t, "DEF_CVAR") || equal(t, "DEF_CVAR_MIN") ||
+ equal(t, "DEF_CVAR_MAX") || equal(t, "DEF_CVAR_MINMAX") ||
+ equal(t, "DEF_CVAR_UNREG") || equal(t, "DEF_CVAR_MIN_UNREG") ||
+ equal(t, "DEF_CVAR_MAX_UNREG") ||
+ equal(t, "DEF_CVAR_MINMAX_UNREG") ||
+ equal(t, "DEF_FEAT_CVAR") || equal(t, "DEF_FEAT_CVAR_MIN") ||
+ equal(t, "DEF_FEAT_CVAR_MAX") ||
+ equal(t, "DEF_FEAT_CVAR_MINMAX")) && equal(t->next, "(")) {
+ type = CMETA_ITEM_DEF_CVAR;
}
- // get to the next line (standard allows extra tokens because)
- while (!tp->at_bol) {
- tp = tp->next;
- if (!tp) return;
+ else if ((equal(t, "DEF_CCMD") || equal(t, "DEF_CCMD_HERE") ||
+ equal(t, "DEF_CCMD_UNREG") || equal(t, "DEF_CCMD_HERE_UNREG") ||
+ equal(t, "DEF_CCMD_PLUSMINUS") ||
+ equal(t, "DEF_CCMD_PLUSMINUS_UNREG") ||
+ equal(t, "DEF_FEAT_CCMD") || equal(t, "DEF_FEAT_CCMD_HERE") ||
+ equal(t, "DEF_FEAT_CCMD_PLUSMINUS")) && equal(t->next, "(")) {
+ type = CMETA_ITEM_DEF_CCMD;
}
- }
-}
-
-// AGAIN, NOTE: this doesn't *perfectly* match top level decls only in the event
-// that someone writes something weird, but we just don't really care because
-// we're not writing something weird. Don't write something weird!
-void cmeta_conmacros(const struct cmeta *cm,
- void (*cb)(const char *, bool, bool)) {
- const Token *tp = (const Token *)cm;
- if (!tp || !tp->next || !tp->next->next) return; // DEF_xyz, (, name
- while (tp) {
- bool isplusminus = false, isvar = false;
- bool unreg = false;
- // this is like the worst thing ever, but oh well it's just build time
- // XXX: tidy this up some day, though, probably
- if (equal(tp, "DEF_CCMD_PLUSMINUS")) {
- isplusminus = true;
+ else if ((equal(t, "DEF_EVENT") || equal(t, "DEF_PREDICATE")) &&
+ equal(t->next, "(")) {
+ type = CMETA_ITEM_DEF_EVENT;
+ }
+ else if (equal(t, "HANDLE_EVENT") && equal(t->next, "(")) {
+ type = CMETA_ITEM_HANDLE_EVENT;
}
- else if (equal(tp, "DEF_CCMD_PLUSMINUS_UNREG")) {
- isplusminus = true;
- unreg = true;
+ else if (equal(t, "FEATURE") && equal(t->next, "(")) {
+ type = CMETA_ITEM_FEATURE;
}
- else if (equal(tp, "DEF_CVAR") || equal(tp, "DEF_CVAR_MIN") ||
- equal(tp, "DEF_CVAR_MAX") || equal(tp, "DEF_CVAR_MINMAX")) {
- isvar = true;
+ else if ((equal(t, "REQUIRE") || equal(t, "REQUIRE_GAMEDATA") ||
+ equal(t, "REQUIRE_GLOBAL") || equal(t, "REQUEST")) &&
+ equal(t->next, "(")) {
+ type = CMETA_ITEM_REQUIRE;
}
- else if (equal(tp, "DEF_CVAR_UNREG") ||
- equal(tp, "DEF_CVAR_MIN_UNREG") ||
- equal(tp, "DEF_CVAR_MAX_UNREG") ||
- equal(tp, "DEF_CVAR_MINMAX_UNREG")) {
- isvar = true;
- unreg = true;
+ else if (equal(t, "GAMESPECIFIC") && equal(t->next, "(")) {
+ type = CMETA_ITEM_GAMESPECIFIC;
}
- else if (equal(tp, "DEF_CCMD_UNREG") ||
- equal(tp, "DEF_CCMD_HERE_UNREG")) {
- unreg = true;
+ else if (equal(t, "PREINIT") && equal(t->next, "{")) {
+ type = CMETA_ITEM_PREINIT;
}
- else if (!equal(tp, "DEF_CCMD") && !equal(tp, "DEF_CCMD_HERE")) {
- tp = tp->next; continue;
+ else if (equal(t, "INIT") && equal(t->next, "{")) {
+ type = CMETA_ITEM_INIT;
}
- if (!equal(tp->next, "(")) { tp = tp->next->next; continue; }
- tp = tp->next->next;
- if (isplusminus) {
- // XXX: this is stupid but whatever
- char *plusname = malloc(sizeof("PLUS_") + tp->len);
- if (!plusname) die("couldn't allocate memory");
- memcpy(plusname, "PLUS_", 5);
- memcpy(plusname + sizeof("PLUS_") - 1, tp->loc, tp->len);
- plusname[sizeof("PLUS_") - 1 + tp->len] = '\0';
- cb(plusname, false, unreg);
- char *minusname = malloc(sizeof("MINUS_") + tp->len);
- if (!minusname) die("couldn't allocate memory");
- memcpy(minusname, "MINUS_", 5);
- memcpy(minusname + sizeof("MINUS_") - 1, tp->loc, tp->len);
- minusname[sizeof("MINUS_") - 1 + tp->len] = '\0';
- cb(minusname, false, unreg);
+ else if (equal(t, "END") && equal(t->next, "{")) {
+ type = CMETA_ITEM_END;
}
else {
- char *name = malloc(tp->len + 1);
- if (!name) die("couldn't allocate memory");
- memcpy(name, tp->loc, tp->len);
- name[tp->len] = '\0';
- cb(name, isvar, unreg);
+ t = t->next;
+ continue;
}
- tp = tp->next;
+ ret.itemtoks[ret.nitems] = t;
+ ret.itemtypes[ret.nitems] = type;
+ ++ret.nitems;
+ // this is kind of inefficient; in most cases we can skip more stuff,
+ // but then also, we're always scanning for something specific, so who
+ // cares actually, this will do for now.
+ t = t->next->next;
}
+ return ret;
}
-const char *cmeta_findfeatmacro(const struct cmeta *cm) {
- const Token *tp = (const Token *)cm;
- if (!tp || !tp->next) return 0; // FEATURE, (
- while (tp) {
- if (equal(tp, "FEATURE") && equal(tp->next, "(")) {
- if (equal(tp->next->next, ")")) return ""; // no arg = no desc
- if (!tp->next->next || tp->next->next->kind != TK_STR) {
- return 0; // it's invalid, whatever, just return...
- }
- return tp->next->next->str;
- }
- tp = tp->next;
+int cmeta_flags_cvar(const struct cmeta *cm, u32 i) {
+ struct Token *t = cm->itemtoks[i];
+ switch_exhaust (t->len) {
+ // It JUST so happens all of the possible tokens here have a unique
+ // length. I swear this wasn't planned. But it IS convenient!
+ case 8: case 12: case 15: return 0;
+ case 14: case 18: case 21: return CMETA_CVAR_UNREG;
+ case 13: case 17: case 20: return CMETA_CVAR_FEAT;
}
- return 0;
}
-void cmeta_featinfomacros(const struct cmeta *cm, void (*cb)(
- enum cmeta_featmacro type, const char *param, void *ctxt), void *ctxt) {
- const Token *tp = (const Token *)cm;
- if (!tp || !tp->next) return;
- while (tp) {
- int type = -1;
- if (equal(tp, "PREINIT")) {
- type = CMETA_FEAT_PREINIT;
- }
- else if (equal(tp, "INIT")) {
- type = CMETA_FEAT_INIT;
- }
- else if (equal(tp, "END")) {
- type = CMETA_FEAT_END;
- }
- if (type != - 1) {
- if (equal(tp->next, "{")) {
- cb(type, 0, ctxt);
- tp = tp->next;
- }
- tp = tp->next;
- continue;
- }
- if (equal(tp, "REQUIRE")) {
- type = CMETA_FEAT_REQUIRE;
- }
- else if (equal(tp, "REQUIRE_GAMEDATA")) {
- type = CMETA_FEAT_REQUIREGD;
- }
- else if (equal(tp, "REQUIRE_GLOBAL")) {
- type = CMETA_FEAT_REQUIREGLOBAL;
- }
- else if (equal(tp, "REQUEST")) {
- type = CMETA_FEAT_REQUEST;
- }
- if (type != -1) {
- if (equal(tp->next, "(") && tp->next->next) {
- tp = tp->next->next;
- char *param = malloc(tp->len + 1);
- if (!param) die("couldn't allocate memory");
- memcpy(param, tp->loc, tp->len);
- param[tp->len] = '\0';
- cb(type, param, ctxt);
- tp = tp->next;
- }
- }
- tp = tp->next;
+int cmeta_flags_ccmd(const struct cmeta *cm, u32 i) {
+ struct Token *t = cm->itemtoks[i];
+ switch_exhaust (t->len) {
+ case 13: if (t->loc[4] == 'F') return CMETA_CCMD_FEAT;
+ case 8: return 0;
+ case 18: if (t->loc[4] == 'F') return CMETA_CCMD_FEAT;
+ return CMETA_CCMD_PLUSMINUS;
+ case 14: case 19: return CMETA_CCMD_UNREG;
+ case 23: return CMETA_CCMD_FEAT | CMETA_CCMD_PLUSMINUS;
+ case 24: return CMETA_CCMD_UNREG | CMETA_CCMD_PLUSMINUS;
}
}
-struct vec_str VEC(const char *);
+int cmeta_flags_event(const struct cmeta *cm, u32 i) {
+ // assuming CMETA_EVENT_ISPREDICATE remains 1, the ternary should
+ // optimise out
+ return cm->itemtoks[i]->len == 13 ? CMETA_EVENT_ISPREDICATE : 0;
+}
-static void pushmacroarg(const Token *last, const char *start,
- struct vec_str *list) {
- int len = last->loc - start + last->len;
- char *dup = malloc(len + 1);
- if (!dup) die("couldn't allocate memory");
- memcpy(dup, start, len);
- dup[len] = '\0';
- if (!vec_push(list, dup)) die("couldn't append to array");
+int cmeta_flags_require(const struct cmeta *cm, u32 i) {
+ struct Token *t = cm->itemtoks[i];
+ // NOTE: this is somewhat more flexible to enable REQUEST_GAMEDATA or
+ // something in future, although that's kind of useless currently
+ int optflag = t->loc[4] == 'E'; // REQU[E]ST
+ switch_exhaust (t->len) {
+ case 7: return optflag;
+ case 16: return optflag | CMETA_REQUIRE_GAMEDATA;
+ case 14: return optflag | CMETA_REQUIRE_GLOBAL;
+ };
}
-// XXX: maybe this should be used for the other functions too. it'd be less ugly
-// and handle closing parentheses better, but alloc for tokens we don't care
-// about. probably a worthy tradeoff?
-static const Token *macroargs(const Token *t, struct vec_str *list) {
- int paren = 1;
- const Token *last; // avoids copying extra ws/comments in
- for (const char *start = t->loc; t; last = t, t = t->next) {
- if (equal(t, "(")) {
- ++paren;
- }
- else if (equal(t, ")")) {
- if (!--paren) {
- pushmacroarg(last, start, list);
- return t->next;
- }
- }
- else if (paren == 1 && equal(t, ",")) {
- pushmacroarg(last, start, list);
- t = t->next;
- if (t) start = t->loc; // slightly annoying...
- }
+int cmeta_nparams(const struct cmeta *cm, u32 i) {
+ int argc = 1, nest = 0;
+ struct Token *t = cm->itemtoks[i]->next->next;
+ if (equal(t, ")")) return 0; // XXX: stupid special case, surely improvable?
+ for (; t; t = t->next) {
+ if (equal(t, "(")) { ++nest; continue; }
+ if (!nest && equal(t, ",")) ++argc;
+ else if (equal(t, ")") && !nest--) break;
}
- // I guess we handle this here.
- fprintf(stderr, "cmeta: fatal: unexpected EOF in %s\n", t->filename);
- exit(2);
+ if (nest != -1) return 0; // XXX: any need to do anything better here?
+ return argc;
+}
+
+struct cmeta_param_iter cmeta_param_iter_init(const struct cmeta *cm, u32 i) {
+ return (struct cmeta_param_iter){cm->itemtoks[i]->next->next};
}
-void cmeta_evdefmacros(const struct cmeta *cm, void (*cb)(const char *name,
- const char *const *params, int nparams, bool predicate)) {
- const Token *tp = (const Token *)cm;
- if (!tp || !tp->next || !tp->next->next) return; // DEF_EVENT, (, name
- while (tp) {
- bool predicate = true;
- if (equal(tp, "DEF_EVENT") && equal(tp->next, "(")) {
- predicate = false;
+struct cmeta_slice cmeta_param_iter(struct cmeta_param_iter *it) {
+ int nest = 0;
+ const char *start = it->cur->loc;
+ for (struct Token *last = 0; it->cur;
+ last = it->cur, it->cur = it->cur->next) {
+ if (equal(it->cur, "(")) { ++nest; continue; }
+ if (!nest && equal(it->cur, ",")) {
+ if (!last) { // , immediately after (, for some reason. treat as ""
+ return (struct cmeta_slice){start, 0};
+ }
+ it->cur = it->cur->next;
}
- else if (!equal(tp, "DEF_PREDICATE") || !equal(tp->next, "(")) {
- tp = tp->next;
- continue;
+ else if (equal(it->cur, ")") && !nest--) {
+ if (!last) break;
}
- tp = tp->next->next;
- struct vec_str args = {0};
- tp = macroargs(tp, &args);
- if (args.sz == 0) {
- fprintf(stderr, "cmeta: fatal: missing event parameters in %s\n",
- tp->filename);
- exit(2);
+ else {
+ continue;
}
- cb(args.data[0], args.data + 1, args.sz - 1, predicate);
+ return (struct cmeta_slice){start, last->loc - start + last->len};
}
+ return (struct cmeta_slice){0, 0};
}
-void cmeta_evhandlermacros(const struct cmeta *cm, const char *modname,
- void (*cb_handler)(const char *evname, const char *modname)) {
- const Token *tp = (const Token *)cm;
- while (tp) {
- if (equal(tp, "HANDLE_EVENT") && equal(tp->next, "(")) {
- tp = tp->next->next;
- char *name = malloc(tp->len + 1);
- if (!name) die("couldn't allocate memory");
- memcpy(name, tp->loc, tp->len);
- name[tp->len] = '\0';
- cb_handler(name, modname);
- }
- tp = tp->next;
- }
+u32 cmeta_line(const struct cmeta *cm, u32 i) {
+ return cm->itemtoks[i]->line_no;
}
// vi: sw=4 ts=4 noet tw=80 cc=80 fdm=marker
diff --git a/src/build/cmeta.h b/src/build/cmeta.h
index 86ae0ec..2a3f741 100644
--- a/src/build/cmeta.h
+++ b/src/build/cmeta.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -17,71 +17,72 @@
#ifndef INC_CMETA_H
#define INC_CMETA_H
+#include "../intdefs.h"
#include "../os.h"
-struct cmeta;
+// XXX: leaking chibicc internals. won't matter after we do away with that
+typedef struct Token Token;
-const struct cmeta *cmeta_loadfile(const os_char *path);
+enum cmeta_item {
+ CMETA_ITEM_DEF_CVAR, // includes all min/max/unreg variants
+ CMETA_ITEM_DEF_CCMD, // includes plusminus/unreg variants
+ CMETA_ITEM_DEF_EVENT, // includes predicates
+ CMETA_ITEM_HANDLE_EVENT,
+ CMETA_ITEM_FEATURE,
+ CMETA_ITEM_REQUIRE, // includes all REQUIRE_*/REQUEST variants
+ CMETA_ITEM_GAMESPECIFIC,
+ CMETA_ITEM_PREINIT,
+ CMETA_ITEM_INIT,
+ CMETA_ITEM_END
+};
-/*
- * Iterates through all the #include directives in a file, passing each one in
- * turn to the callback cb.
- */
-void cmeta_includes(const struct cmeta *cm,
- void (*cb)(const char *f, bool issys, void *ctxt), void *ctxt);
+struct cmeta {
+ char *sbase;
+ u32 nitems; // number of interesting macros
+ //u32 *itemoffs; // file offsets of interesting macros (ONE DAY!)
+ Token **itemtoks; // crappy linked token structures, for the time being
+ u8 *itemtypes; // CMETA_ITEM_* enum values
+};
-/*
- * Iterates through all commands and variables declared using the macros in
- * con_.h, passing each one in turn to the callback cb.
- */
-void cmeta_conmacros(const struct cmeta *cm,
- void (*cb)(const char *name, bool isvar, bool unreg));
+enum cmeta_flag_cvar {
+ CMETA_CVAR_UNREG = 1,
+ CMETA_CVAR_FEAT = 2,
+};
+enum cmeta_flag_ccmd {
+ CMETA_CCMD_UNREG = 1,
+ CMETA_CCMD_FEAT = 2,
+ CMETA_CCMD_PLUSMINUS = 4
+};
+enum cmeta_flag_event {
+ CMETA_EVENT_ISPREDICATE = 1
+};
+enum cmeta_flag_require {
+ CMETA_REQUIRE_OPTIONAL = 1, // i.e. REQUEST() macro, could be extended
+ CMETA_REQUIRE_GAMEDATA = 2,
+ CMETA_REQUIRE_GLOBAL = 4
+};
-/*
- * Looks for a feature description macro in file, returning the description
- * string if it exists, an empty string if the feature is defined without a
- * user-facing description, and null if source file does not define a feature.
- */
-const char *cmeta_findfeatmacro(const struct cmeta *cm);
+struct cmeta_slice { const char *s; int len; };
-/*
- * the various kinds of feature specficiation macros, besides the feature
- * declaration macro itself
- */
-enum cmeta_featmacro {
- CMETA_FEAT_REQUIRE,
- CMETA_FEAT_REQUIREGD,
- CMETA_FEAT_REQUIREGLOBAL,
- CMETA_FEAT_REQUEST,
- CMETA_FEAT_PREINIT,
- CMETA_FEAT_INIT,
- CMETA_FEAT_END
-};
+struct cmeta cmeta_loadfile(const os_char *path);
+int cmeta_flags_cvar(const struct cmeta *cm, u32 i);
+int cmeta_flags_ccmd(const struct cmeta *cm, u32 i);
+int cmeta_flags_event(const struct cmeta *cm, u32 i);
+int cmeta_flags_require(const struct cmeta *cm, u32 i);
-/*
- * Iterates through all feature dependency macros and init/end/preinit
- * indicators, passing each bit of information to the callback cb.
- *
- * PREINT, INIT and END macros don't pass anything to param.
- *
- * This one takes a context pointer, while the others don't, because this is all
- * cobbled together without much consistent abstraction.
- */
-void cmeta_featinfomacros(const struct cmeta *cm, void (*cb)(
- enum cmeta_featmacro type, const char *param, void *ctxt), void *ctxt);
+int cmeta_nparams(const struct cmeta *cm, u32 i);
+struct cmeta_param_iter { Token *cur; };
+struct cmeta_param_iter cmeta_param_iter_init(const struct cmeta *cm, u32 i);
+struct cmeta_slice cmeta_param_iter(struct cmeta_param_iter *it);
+
+#define cmeta_param_foreach(varname, cm, u32) \
+ switch (0) for (struct cmeta_slice varname; 0;) default: \
+ for (struct cmeta_param_iter _it = cmeta_param_iter_init(cm, i); \
+ varname = cmeta_param_iter(&_it), varname.s;) \
+ /* {...} */
+
+u32 cmeta_line(const struct cmeta *cm, u32 i);
-/*
- * Iterates through all event-related macros and takes note of which events are
- * defined, giving a callback for each.
- */
-void cmeta_evdefmacros(const struct cmeta *cm, void (*cb)(const char *name,
- const char *const *params, int nparams, bool predicate));
-/*
- * Iterates through all event-related macros and gives a callback for each event
- * that is handled by the given module.
- */
-void cmeta_evhandlermacros(const struct cmeta *cm, const char *modname,
- void (*cb)(const char *evname, const char *modname));
#endif
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/build/codegen.c b/src/build/codegen.c
deleted file mode 100644
index 70b5e12..0000000
--- a/src/build/codegen.c
+++ /dev/null
@@ -1,537 +0,0 @@
-/*
- * Copyright © 2024 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 <ctype.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "../intdefs.h"
-#include "../langext.h"
-#include "../os.h"
-#include "cmeta.h"
-#include "skiplist.h"
-#include "vec.h"
-
-#ifdef _WIN32
-#define fS "S"
-#else
-#define fS "s"
-#endif
-
-static void die(const char *s) {
- fprintf(stderr, "codegen: fatal: %s\n", s);
- exit(100);
-}
-
-#define MAXENT 65536 // arbitrary limit!
-static struct conent {
- const char *name;
- bool unreg;
- bool isvar; // false for cmd
-} conents[MAXENT];
-static int nconents;
-
-#define PUT(name_, isvar_, unreg_) do { \
- if (nconents == countof(conents)) { \
- fprintf(stderr, "codegen: out of space; make conents bigger!\n"); \
- exit(1); \
- } \
- conents[nconents].name = name_; \
- conents[nconents].isvar = isvar_; conents[nconents++].unreg = unreg_; \
-} while (0)
-
-static void oncondef(const char *name, bool isvar, bool unreg) {
- PUT(name, isvar, unreg);
-}
-
-struct vec_str VEC(const char *);
-struct vec_usize VEC(usize);
-struct vec_featp VEC(struct feature *);
-
-enum { UNSEEN, SEEING, SEEN };
-
-DECL_SKIPLIST(static, feature, struct feature, const char *, 4)
-DECL_SKIPLIST(static, feature_bydesc, struct feature, const char *, 4)
-struct feature {
- const char *modname;
- const char *desc;
- const struct cmeta *cm; // backref for subsequent options pass
- struct vec_featp needs;
- // keep optionals in a separate array mainly so we have separate counts
- struct vec_featp wants;
- uint dfsstate : 2; // used for sorting and cycle checking
- bool has_preinit : 1, /*has_init : 1, <- required anyway! */ has_end : 1;
- bool has_evhandlers : 1;
- bool is_requested : 1; // determines if has_ variable needs to be extern
- //char pad : 2;
- //char pad[3];
- struct vec_str need_gamedata;
- struct vec_str need_globals;
- struct skiplist_hdr_feature hdr; // by id/modname
- struct skiplist_hdr_feature_bydesc hdr_bydesc;
-};
-static inline int cmp_feature(struct feature *e, const char *s) {
- return strcmp(e->modname, s);
-}
-static inline int cmp_feature_bydesc(struct feature *e, const char *s) {
- for (const char *p = e->desc; ; ++p, ++s) {
- // shortest string first
- if (!*p) return !!*s; if (!*s) return -1;
- // case insensitive sort where possible
- if (tolower((uchar)*p) > tolower((uchar)*s)) return 1;
- if (tolower((uchar)*p) < tolower((uchar)*s)) return -1;
- // prioritise upper-case if same letter
- if (isupper((uchar)*p) && islower((uchar)*s)) return 1;
- if (islower((uchar)*p) && isupper((uchar)*s)) return -1;
- }
- return 0;
-}
-static inline struct skiplist_hdr_feature *hdr_feature(struct feature *e) {
- return &e->hdr;
-}
-static inline struct skiplist_hdr_feature_bydesc *hdr_feature_bydesc(
- struct feature *e) {
- return &e->hdr_bydesc;
-}
-DEF_SKIPLIST(static, feature, cmp_feature, hdr_feature)
-DEF_SKIPLIST(static, feature_bydesc, cmp_feature_bydesc, hdr_feature_bydesc)
-static struct skiplist_hdr_feature features = {0};
-// sort in two different ways, so we can alphabetise the user-facing display
-// NOTE: not all features will show up in this second list!
-static struct skiplist_hdr_feature_bydesc features_bydesc = {0};
-
-static void onfeatinfo(enum cmeta_featmacro type, const char *param,
- void *ctxt) {
- struct feature *f = ctxt;
- switch_exhaust_enum (cmeta_featmacro, type) {
- case CMETA_FEAT_REQUIRE:; bool optional = false; goto dep;
- case CMETA_FEAT_REQUEST: optional = true;
-dep:; struct feature *dep = skiplist_get_feature(&features, param);
- if (optional) dep->is_requested = true;
- if (!dep) {
- fprintf(stderr, "codegen: error: feature `%s` tried to depend "
- "on non-existent feature `%s`\n", f->modname, param);
- exit(1); \
- }
- if (!vec_push(optional ? &f->wants : &f->needs, dep)) {
- die("couldn't allocate memory");
- }
- break;
- case CMETA_FEAT_REQUIREGD:;
- struct vec_str *vecp = &f->need_gamedata;
- goto push;
- case CMETA_FEAT_REQUIREGLOBAL:
- vecp = &f->need_globals;
-push: if (!vec_push(vecp, param)) die("couldn't allocate memory");
- break;
- case CMETA_FEAT_PREINIT: f->has_preinit = true; break;
- case CMETA_FEAT_END: f->has_end = true; break;
- case CMETA_FEAT_INIT:; // nop for now, I guess
- }
-}
-
-DECL_SKIPLIST(static, event, struct event, const char *, 4)
- struct event {
- usize name; // string, but tagged pointer - see below
- const char *const *params;
- int nparams;
- //char pad[4];
- struct vec_usize handlers; // strings, but with tagged pointers - see below
- struct skiplist_hdr_event hdr;
-};
-static inline int cmp_event(struct event *e, const char *s) {
- return strcmp((const char *)(e->name & ~1ull), s);
-}
-static inline struct skiplist_hdr_event *hdr_event(struct event *e) {
- return &e->hdr;
-}
-DEF_SKIPLIST(static, event, cmp_event, hdr_event)
-static struct skiplist_hdr_event events = {0};
-
-static void onevdef(const char *name, const char *const *params, int nparams,
- bool predicate) {
- struct event *e = skiplist_get_event(&events, name);
- if (!e) {
- struct event *e = malloc(sizeof(*e));
- if (!e) die("couldn't allocate memory");
- // hack: using unused pointer bit to distinguish the two types of event
- e->name = (usize)name | predicate;
- e->params = params; e->nparams = nparams;
- e->handlers = (struct vec_usize){0};
- e->hdr = (struct skiplist_hdr_event){0};
- skiplist_insert_event(&events, name, e);
- }
- else {
- fprintf(stderr, "codegen: error: duplicate event definition `%s`\n",
- name);
- exit(2);
- }
-}
-
-static void onevhandler(const char *evname, const char *modname) {
- struct event *e = skiplist_get_event(&events, evname);
- if (!e) {
- fprintf(stderr, "codegen: error: module `%s` trying to handle "
- "non-existent event `%s`\n", modname, evname);
- exit(2);
- }
- usize taggedptr = (usize)modname;
- struct feature *f = skiplist_get_feature(&features, modname);
- f->has_evhandlers = true;
- // hack: using unused pointer bit to determine whether a handler is tied to
- // a feature and thus conditional. relies on malloc alignment!
- if (f) taggedptr |= 1ull;
- // NOTE: not bothering to check for more than one handler in a file.
- // compiler will get that anyway.
- if (!vec_push(&e->handlers, taggedptr)) die("couldn't allocate memory");
-}
-
-struct passinfo {
- const struct cmeta *cm;
- const os_char *path;
-};
-static struct vec_passinfo VEC(struct passinfo) pass2 = {0};
-
-#define _(x) \
- if (fprintf(out, "%s\n", x) < 0) die("couldn't write to file");
-#define F(f, ...) \
- if (fprintf(out, f "\n", __VA_ARGS__) < 0) die("couldn't write to file");
-#define H_() \
- _( "/* This file is autogenerated by "__FILE__". DO NOT EDIT! */")
-#define H() H_() _( "")
-
-static struct vec_featp endstack = {0}; // stack for reversing order
-
-static void featdfs(FILE *out, struct feature *f) {
- if (f->dfsstate == SEEN) return;
- if (f->dfsstate == SEEING) {
- // XXX: could unwind for full cycle listing like in build.
- // purely being lazy by not doing that here, and assuming there won't
- // actually be cycles anyway, because this is not a general purpose tool
- // and people working on this codebase are very smart.
- fprintf(stderr, "codegen: error: dependency cycle found at feature `%s`\n",
- f->modname);
- exit(2);
- }
- f->dfsstate = SEEING;
- // easier to do wants first, then we can do the conditional counter nonsense
- // without worrying about how that fits in...
- for (struct feature *const *pp = f->wants.data;
- pp - f->wants.data < f->wants.sz; ++pp) {
- featdfs(out, *pp);
- }
-F( " char status_%s = FEAT_OK;", f->modname);
- const char *else_ = "";
- if (f->needs.sz == 1) {
- featdfs(out, f->needs.data[0]);
-F( " if (status_%s != FEAT_OK) status_%s = FEAT_REQFAIL;",
- f->needs.data[0]->modname, f->modname)
- else_ = "else ";
- }
- else if (f->needs.sz > 1) {
- for (struct feature *const *pp = f->needs.data;
- pp - f->needs.data < f->needs.sz; ++pp) {
- featdfs(out, *pp);
- }
-F( " bool metdeps_%s =", f->modname)
- for (struct feature *const *pp = f->needs.data;
- pp - f->needs.data < f->needs.sz; ++pp) {
-F( " status_%s == FEAT_OK%s", (*pp)->modname,
- pp - f->needs.data == f->needs.sz - 1 ? ";" : " &&") // dumb but oh well
- }
-F( " if (!metdeps_%s) status_%s = FEAT_REQFAIL;", f->modname, f->modname)
- else_ = "else ";
- }
- if (f->has_preinit) {
-F( " %sif (!_feature_preinit_%s()) status_%s = FEAT_PREFAIL;", else_,
- f->modname, f->modname);
- else_ = "else ";
- }
- for (const char **pp = f->need_gamedata.data;
- pp - f->need_gamedata.data < f->need_gamedata.sz; ++pp) {
-F( " %sif (!has_%s) status_%s = FEAT_NOGD;", else_, *pp, f->modname)
- else_ = "else "; // blegh
- }
- for (const char **pp = f->need_globals.data;
- pp - f->need_globals.data < f->need_globals.sz; ++pp) {
-F( " %sif (!%s) status_%s = FEAT_NOGLOBAL;", else_, *pp, f->modname)
- else_ = "else "; // blegh 2
- }
-F( " %sif (!_feature_init_%s()) status_%s = FEAT_FAIL;", else_, f->modname,
- f->modname)
- if (f->has_end || f->has_evhandlers || f->is_requested) {
-F( " has_%s = status_%s == FEAT_OK;", f->modname, f->modname)
- }
- if (!vec_push(&endstack, f)) die("couldn't allocate memory");
- f->dfsstate = SEEN;
-}
-
-int OS_MAIN(int argc, os_char *argv[]) {
- for (++argv; *argv; ++argv) {
- const struct cmeta *cm = cmeta_loadfile(*argv);
- if (!cm) {
- fprintf(stderr, "codegen: fatal: couldn't load file %" fS "\n",
- *argv);
- exit(100);
- }
- cmeta_conmacros(cm, &oncondef);
- cmeta_evdefmacros(cm, &onevdef);
- if (!vec_push(&pass2, ((struct passinfo){cm, *argv}))) {
- die("couldn't allocate memory");
- }
- }
-
- // we have to do a second pass for features and event handlers. also,
- // there's a bunch of terrible garbage here. don't stare for too long...
- for (struct passinfo *pi = pass2.data; pi - pass2.data < pass2.sz; ++pi) {
- // XXX: I guess we should cache these by name or something!
- const struct cmeta *cm = pi->cm;
-#ifdef _WIN32
- int arglen = wcslen(pi->path);
- char *p = malloc(arglen + 1);
- if (!p) die("couldn't allocate string");
- // XXX: Unicode isn't real, it can't hurt you.
- for (const ushort *q = pi->path; q - pi->path < arglen; ++q) {
- p[q - pi->path] = *q; // ugh this is stupid
- }
- p[arglen] = '\0';
-#else
- const char *p = pi->path;
-#endif
- const char *lastslash = p - 1;
- for (; *p; ++p) {
-#ifdef _WIN32
- if (*p == '/' || *p == '\\') {
-#else
- if (*p == '/') {
-#endif
- lastslash = p;
- }
- }
- int len = strlen(lastslash + 1);
- if (len <= 3 || lastslash[len - 1] != '.' || lastslash[len] != 'c') {
- fprintf(stderr, "filenames should end in .c probably\n");
- exit(2);
- }
- char *modname = malloc(len - 1);
- if (!modname) die("couldn't allocate string");
- memcpy(modname, lastslash + 1, len - 2);
- modname[len - 2] = '\0';
- // ugh. same dumb hacks from compile scripts
- if (!strcmp(modname, "con_")) {
- free(modname); // might as well
- modname = "con";
- }
- else if (!strcmp(modname, "sst")) {
- continue; // I guess???
- }
- const char *featdesc = cmeta_findfeatmacro(cm);
- if (featdesc) {
- struct feature *f = malloc(sizeof(*f));
- if (!f) die("couldn't allocate memory");
- *f = (struct feature){
- .modname = modname,
- .desc = featdesc[0] ? featdesc : 0,
- .cm = cm
- };
- skiplist_insert_feature(&features, modname, f);
- if (f->desc) {
- skiplist_insert_feature_bydesc(&features_bydesc, f->desc, f);
- }
- }
- cmeta_evhandlermacros(cm, modname, &onevhandler);
- }
- // yet another pass because I am stupid and don't want to think harder :)
- for (struct feature *f = features.x[0]; f; f = f->hdr.x[0]) {
- cmeta_featinfomacros(f->cm, &onfeatinfo, f);
- }
-
- FILE *out = fopen(".build/include/cmdinit.gen.h", "wb");
- if (!out) die("couldn't open cmdinit.gen.h");
-H();
- for (const struct conent *p = conents; p - conents < nconents; ++p) {
-F( "extern struct con_%s *%s;", p->isvar ? "var" : "cmd", p->name)
- }
-_( "")
-_( "static void regcmds(void) {")
- for (const struct conent *p = conents; p - conents < nconents; ++p) {
- if (p->isvar) {
-F( " initval(%s);", p->name)
- }
- if (!p->unreg) {
-F( " con_reg(%s);", p->name)
- }
- }
-_( "}")
-_( "")
-_( "static void freevars(void) {")
- for (const struct conent *p = conents; p - conents < nconents; ++p) {
- if (p->isvar) {
-F( " extfree(%s->strval);", p->name)
- }
- }
-_( "}")
- if (fclose(out) == EOF) die("couldn't fully write cmdinit.gen.h");
-
- out = fopen(".build/include/featureinit.gen.h", "wb");
- if (!out) die("couldn't open featureinit.gen.h");
- H()
- // XXX: I dunno whether this should just be defined in sst.c. It's sort of
- // internal to the generated stuff hence tucking it away here, but that's at
- // the cost of extra string-spaghettiness
-_( "enum {")
-_( " FEAT_OK,")
-_( " FEAT_REQFAIL,")
-_( " FEAT_PREFAIL,")
-_( " FEAT_NOGD,")
-_( " FEAT_NOGLOBAL,")
-_( " FEAT_FAIL")
-_( "};")
-_( "")
-_( "static const char *const featmsgs[] = {")
-_( " \" [ OK! ] %s\\n\",")
-_( " \" [ skipped ] %s (requires another feature)\\n\",")
-_( " \" [ skipped ] %s (not applicable or useful)\\n\",")
-_( " \" [ unsupported ] %s (missing gamedata)\\n\",")
-_( " \" [ FAILED! ] %s (failed to access engine)\\n\",")
-_( " \" [ FAILED! ] %s (error in initialisation)\\n\"")
-_( "};")
-_( "")
- for (struct feature *f = features.x[0]; f; f = f->hdr.x[0]) {
- if (f->has_preinit) {
-F( "extern bool _feature_preinit_%s(void);", f->modname)
- }
-F( "extern bool _feature_init_%s(void);", f->modname)
- if (f->has_end) {
-F( "extern bool _feature_end_%s(void);", f->modname)
- }
- if (f->is_requested) {
-F( "bool has_%s = false;", f->modname)
- }
- else if (f->has_end || f->has_evhandlers) {
-F( "static bool has_%s = false;", f->modname)
- }
- }
-_( "")
-_( "static void initfeatures(void) {")
- for (struct feature *f = features.x[0]; f; f = f->hdr.x[0]) featdfs(out, f);
-_( "")
- // note: old success message is moved in here, to get the ordering right
-_( " con_colourmsg(&(struct rgba){64, 255, 64, 255},")
-_( " LONGNAME \" v\" VERSION \" successfully loaded\");")
-_( " con_colourmsg(&(struct rgba){255, 255, 255, 255}, \" for game \");")
-_( " con_colourmsg(&(struct rgba){0, 255, 255, 255}, \"%s\\n\", ")
-_( " gameinfo_title);")
-_( " struct rgba white = {255, 255, 255, 255};")
-_( " struct rgba green = {128, 255, 128, 255};")
-_( " struct rgba red = {255, 128, 128, 255};")
-_( " con_colourmsg(&white, \"---- List of plugin features ---\\n\");");
- for (const struct feature *f = features_bydesc.x[0]; f;
- f = f->hdr_bydesc.x[0]) {
-F( " con_colourmsg(status_%s == FEAT_OK ? &green : &red,", f->modname)
-F( " featmsgs[(int)status_%s], \"%s\");", f->modname, f->desc)
- }
-_( "}")
-_( "")
-_( "static void endfeatures(void) {")
- for (struct feature **pp = endstack.data + endstack.sz - 1;
- pp - endstack.data >= 0; --pp) {
- if ((*pp)->has_end) {
-F( " if (has_%s) _feature_end_%s();", (*pp)->modname, (*pp)->modname)
- }
- }
-_( "}")
-_( "")
- if (fclose(out) == EOF) die("couldn't fully write featureinit.gen.h");
-
- out = fopen(".build/include/evglue.gen.h", "wb");
- if (!out) die("couldn't open evglue.gen.h");
- H_()
- for (const struct event *e = events.x[0]; e; e = e->hdr.x[0]) {
-_( "")
- // gotta break from the string emit macros for a sec in order to do the
- // somewhat more complicated task sometimes referred to as a "for loop"
- fprintf(out, "%s_%s(", e->name & 1 ? "bool CHECK" : "void EMIT",
- (const char *)(e->name & ~1ull));
- for (int n = 0; n < (int)e->nparams - 1; ++n) {
- fprintf(out, "typeof(%s) a%d, ", e->params[n], n + 1);
- }
- if (e->nparams && strcmp(e->params[0], "void")) {
- fprintf(out, "typeof(%s) a%d", e->params[e->nparams - 1],
- e->nparams);
- }
- else {
- // just unilaterally doing void for now. when we're fully on C23
- // eventually we can unilaterally do nothing instead
- fputs("void", out);
- }
-_( ") {")
- for (usize *pp = e->handlers.data;
- pp - e->handlers.data < e->handlers.sz; ++pp) {
- const char *modname = (const char *)(*pp & ~1ull);
- fprintf(out, "\t%s _evhandler_%s_%s(", e->name & 1 ? "bool" : "void",
- modname, (const char *)(e->name & ~1ull));
- for (int n = 0; n < (int)e->nparams - 1; ++n) {
- fprintf(out, "typeof(%s) a%d, ", e->params[n], n + 1);
- }
- if (e->nparams && strcmp(e->params[0], "void")) {
- fprintf(out, "typeof(%s) a%d", e->params[e->nparams - 1],
- e->nparams);
- }
- else {
- fputs("void", out);
- }
- fputs(");\n\t", out);
- // conditional and non-conditional cases - in theory could be
- // unified a bit but this is easier to make output relatively pretty
- // note: has_* variables are already included by this point (above)
- if (e->name & 1) {
- if (*pp & 1) fprintf(out, "if (has_%s && !", modname);
- else fprintf(out, "if (!");
- fprintf(out, "_evhandler_%s_%s(", modname,
- (const char *)(e->name & ~1ull));
- // XXX: much repetitive drivel here
- for (int n = 0; n < (int)e->nparams - 1; ++n) {
- fprintf(out, "a%d,", n + 1);
- }
- if (e->nparams && strcmp(e->params[0], "void")) {
- fprintf(out, "a%d", e->nparams);
- }
- fputs(")) return false;\n", out);
- }
- else {
- if (*pp & 1) fprintf(out, "if (has_%s) ", modname);
- fprintf(out, "_evhandler_%s_%s(", modname,
- (const char *)(e->name & ~1ull));
- for (int n = 0; n < (int)e->nparams - 1; ++n) {
- fprintf(out, "a%d,", n + 1);
- }
- if (e->nparams && strcmp(e->params[0], "void")) {
- fprintf(out, "a%d", e->nparams);
- }
- fputs(");\n", out);
- }
- }
- if (e->name & 1) fputs("\treturn true;\n", out);
-_( "}")
- }
- if (fclose(out) == EOF) die("couldn't fully write evglue.gen.h");
-
- return 0;
-}
-
-// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/build/gluegen.c b/src/build/gluegen.c
new file mode 100644
index 0000000..4a0f4a1
--- /dev/null
+++ b/src/build/gluegen.c
@@ -0,0 +1,963 @@
+/*
+ * Copyright © 2025 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "../intdefs.h"
+#include "../langext.h"
+#include "../os.h"
+#include "cmeta.h"
+
+#ifdef _WIN32
+#define fS "S"
+#else
+#define fS "s"
+#endif
+
+static inline noreturn die(int status, const char *s) {
+ fprintf(stderr, "gluegen: fatal: %s\n", s);
+ exit(status);
+}
+
+static inline noreturn diefile(int status, const os_char *f, int line,
+ const char *s) {
+ if (line) {
+ fprintf(stderr, "gluegen: fatal: %" fS ":%d: %s\n", f, line, s);
+ }
+ else {
+ fprintf(stderr, "gluegen: fatal: %" fS ": %s\n", f, s);
+ }
+ exit(status);
+}
+
+#define ARENASZ (1024 * 1024)
+static _Alignas(64) char _arena[ARENASZ] = {0}, *const arena = _arena - 64;
+static int arena_last = 0;
+static int arena_used = 64; // using 0 indices as null; reserve and stay aligned
+
+static inline void _arena_align(void) {
+ enum { ALIGN = ssizeof(void *) };
+ if (arena_used & ALIGN - 1) arena_used = (arena_used + ALIGN) & ~(ALIGN - 1);
+}
+
+static inline int arena_bump(int amt) {
+ if_cold (arena_used + amt >= ARENASZ) die(2, "out of arena memory");
+ int ret = arena_used;
+ arena_used += amt;
+ return ret;
+}
+
+static int arena_new(int sz) {
+ _arena_align();
+ arena_last = arena_used;
+ int off = arena_bump(sz);
+ return off;
+}
+
+#define LIST_MINALLOC 64
+#define LIST_MINSPACE (LIST_MINALLOC - ssizeof(struct list_chunkhdr))
+
+struct list_chunkhdr { int next, sz; };
+struct list_chunk { struct list_chunkhdr hdr; char space[LIST_MINSPACE]; };
+
+static struct list_grow_ret {
+ struct list_chunkhdr *tail;
+ void *data;
+} list_grow(struct list_chunkhdr *tail, int amt) {
+ if_hot ((char *)tail == arena + arena_last) {
+ arena_bump(amt);
+ }
+ else if (tail->sz + amt > LIST_MINSPACE) {
+ int allocsz = ssizeof(struct list_chunkhdr) + amt;
+ int new = arena_new(allocsz > LIST_MINALLOC ? allocsz : LIST_MINALLOC);
+ struct list_chunkhdr *newptr = (struct list_chunkhdr *)(arena + new);
+ newptr->next = 0; newptr->sz = 0;
+ tail->next = new;
+ return (struct list_grow_ret){newptr, (char *)(newptr + 1)};
+ }
+ struct list_grow_ret r = {tail, (char *)(tail + 1) + tail->sz};
+ tail->sz += amt;
+ return r;
+}
+
+static inline void *list_grow_p(struct list_chunkhdr **tailp, int amt) {
+ struct list_grow_ret r = list_grow(*tailp, amt);
+ *tailp = r.tail;
+ return r.data;
+}
+
+#define list_append_p(T, tailp) ((T *)list_grow_p((tailp), ssizeof(T)))
+#define list_append(tailp, x) ((void)(*list_append_p(typeof(x), (tailp)) = (x)))
+
+#define _list_foreach(T, varname, firstchunkp, extra) \
+ for (struct list_chunkhdr *_h = &(firstchunkp)->hdr; _h; \
+ _h = _h->next ? (struct list_chunkhdr *)(arena + _h->next) : 0) \
+ for (typeof(T) *varname = (typeof(T) *)(_h + 1); \
+ (char *)varname < (char *)(_h + 1) + _h->sz && (extra); \
+ ++varname) \
+ /* ... */
+
+#define list_foreach_p(T, varname, firstchunkp) \
+ _list_foreach(T, varname, firstchunkp, 1)
+ /* {...} */
+
+#define list_foreach(T, varname, firstchunkp) \
+ /* N.B. this crazy looking construct avoids unused warnings on varname
+ in case we just want to count up the entries or something */ \
+ switch (0) for (typeof(T) varname; 0;) if ((void)varname, 0) case 0: \
+ _list_foreach(T, _foreachp, firstchunkp, (varname = *_foreachp, 1)) \
+ /* ... */
+
+#define DEF_NEW(type, func, nvar, maxvar, desc) \
+ static inline type func(void) { \
+ if_cold (nvar == maxvar) { \
+ die(2, "out of " desc " - increase " #maxvar " in gluegen.c!"); \
+ } \
+ return nvar++; \
+ }
+
+// trickery to enable 1-based indexing (and thus 0-as-null) for various arrays
+#define SHUNT(T, x) typeof(T) _array_##x[], *const x = _array_##x - 1, _array_##x
+
+#define MAX_MODULES 512
+// note that by 1-indexing these, we enable the radix stuff above to use the
+// default initialisation as null and we *also* allow argc to be used as a
+// direct index into each array when looping through files passed to main()!
+// XXX: padded 16-byte structs. could eventually switch to u32 indices. for now,
+// locality is favoured over packing. no clue whether that's the right call!
+static SHUNT(struct cmeta_slice, mod_names)[MAX_MODULES] = {0};
+static SHUNT(struct cmeta_slice, mod_featdescs)[MAX_MODULES] = {0};
+static SHUNT(struct cmeta_slice, mod_gamespecific)[MAX_MODULES] = {0};
+enum {
+ HAS_INIT = 1, // really, this means this *is a feature*!
+ HAS_PREINIT = 2,
+ HAS_END = 4,
+ HAS_EVENTS = 8,
+ HAS_OPTDEPS = 16, // something else depends on *us* with REQUEST()
+ DFS_SEEING = 64, // for REQUIRE() cycle detection
+ DFS_SEEN = 128
+};
+static u8 mod_flags[MAX_MODULES] = {0};
+static SHUNT(struct list_chunk, mod_needs)[MAX_MODULES] = {0};
+static SHUNT(struct list_chunk, mod_wants)[MAX_MODULES] = {0};
+static SHUNT(struct list_chunk, mod_gamedata)[MAX_MODULES] = {0};
+static SHUNT(struct list_chunk, mod_globals)[MAX_MODULES] = {0};
+static int nmods = 1;
+
+static s16 feat_initorder[MAX_MODULES];
+static int nfeatures = 0;
+
+#define MAX_CVARS 8192
+#define MAX_CCMDS MAX_CVARS
+static SHUNT(struct cmeta_slice, cvar_names)[MAX_CVARS];
+static SHUNT(struct cmeta_slice, ccmd_names)[MAX_CCMDS];
+static SHUNT(s16, cvar_feats)[MAX_CVARS];
+static SHUNT(s16, ccmd_feats)[MAX_CVARS];
+static SHUNT(u8, cvar_flags)[MAX_CVARS];
+static SHUNT(u8, ccmd_flags)[MAX_CVARS];
+static int ncvars = 1, nccmds = 1;
+DEF_NEW(s16, cvar_new, ncvars, MAX_CVARS, "cvar entries")
+DEF_NEW(s16, ccmd_new, nccmds, MAX_CCMDS, "ccmd entries")
+
+#define MAX_EVENTS 512
+static SHUNT(struct cmeta_slice, event_names)[MAX_EVENTS];
+static SHUNT(s16, event_owners)[MAX_EVENTS];
+static SHUNT(struct list_chunk, event_handlers)[MAX_EVENTS] = {0};
+static SHUNT(struct list_chunkhdr *, event_handlers_tails)[MAX_EVENTS] = {0};
+static SHUNT(struct list_chunk, event_params)[MAX_EVENTS] = {0};
+// XXX: would simply things a little if we could segregate the regular event and
+// predicate arrays, but because of how the event.h API is currently set up,
+// HANDLE_EVENT doesn't give context for what kind of event something is, so we
+// can't do that in a single pass (right now we create placeholder event entries
+// and check at the end to make sure we didn't miss one). fixing this is tricky!
+static SHUNT(bool, event_predicateflags)[MAX_EVENTS] = {0};
+static int nevents = 1;
+DEF_NEW(s16, event_new, nevents, MAX_EVENTS, "event entries")
+
+// a "crit-nybble tree" (see also: djb crit-bit trees)
+struct radix { s16 children[16], critpos; };
+static SHUNT(struct radix, radices)[MAX_MODULES * 2 + MAX_EVENTS];
+static int nradices = 1; // also reserve a null value
+
+// NOTE: this will never fail, as node count is bounded by modules * 2 + events
+static inline s16 radix_new(void) { return nradices++; }
+
+static int matchlen(const char *s1, const char *s2, int len, bool ignorecase) {
+ uchar c1, c2;
+ for (int i = 0; i < len; ++i) {
+ c1 = s1[i]; c2 = s2[i];
+ if (ignorecase) {
+ c1 |= (c1 >= 'A' && c1 <= 'Z') << 5; // if A-Z, |= 32 -> a-z
+ c2 |= (c2 >= 'A' && c2 <= 'Z') << 5; // "
+ }
+ int diff = c1 ^ c2;
+ if (diff) return (i << 1) | (diff < 16);
+ }
+ return (len << 1);
+}
+static inline int radixbranch(const char *s, int i, bool ignorecase) {
+ uchar c = s[i >> 1];
+ if (ignorecase) c |= (c >= 'A' && c <= 'Z') << 5;
+ return c >> ((~i & 1) << 2) & 0xF;
+}
+
+/*
+ * Tries to insert a string index into a radix/crit-nybble/whatever trie.
+ * Does not actually put the index in; that way an item can be allocated only
+ * after determining that it doesn't already exist. Callers have to check
+ * ret.isnew and act accordingly.
+ */
+static struct radix_insert_ret {
+ bool isnew;
+ union {
+ s16 *strp; /* If isnew, caller must do `*ret.strp = -stridx;`. */
+ s16 stridx; /* If !isnew, `stridx` points at the existing string. */
+ };
+} radix_insert(s16 *nodep, const struct cmeta_slice *strlist,
+ const char *s, int len, bool ignorecase) {
+ // special case: an empty tree is just null; replace it with a leaf node
+ if (!*nodep) return (struct radix_insert_ret){true, nodep};
+ s16 *insp = nodep;
+ for (s16 cur;;) {
+ cur = *nodep;
+ assume(cur);
+ if (cur < 0) {
+ // once we find an existing leaf node, we have to compare our string
+ // against it to find the critical position (i.e. the point where
+ // the strings differ) and then insert a new node at that point.
+ const struct cmeta_slice existing = strlist[-cur];
+ int ml;
+ if (existing.len == len) {
+ ml = matchlen(existing.s, s, len, ignorecase);
+ if (ml == len << 1) {
+ return (struct radix_insert_ret){false, .stridx = -cur};
+ }
+ }
+ else {
+ // ugh this is a little inelegant, can we think of a better way?
+ ml = matchlen(existing.s, s,
+ len < existing.len ? len : existing.len, ignorecase);
+ }
+ int oldbranch = radixbranch(existing.s, ml, ignorecase);
+ int newbranch = radixbranch(s, ml, ignorecase);
+ assume(oldbranch != newbranch);
+ for (;;) {
+ // splice in front of an existing string *or* an empty slot.
+ if (*insp <= 0) break;
+ struct radix *r = radices + *insp;
+ if (r->critpos > ml) break;
+ insp = r->children + radixbranch(s, r->critpos, ignorecase);
+ }
+ s16 new = radix_new();
+ radices[new].critpos = ml;
+ radices[new].children[oldbranch] = *insp;
+ s16 *strp = radices[new].children + newbranch;
+ *insp = new;
+ return (struct radix_insert_ret){true, strp};
+ }
+ // try to take the exact path to match common prefixes, but otherwise
+ // just pick any path that lets us find a string to compare with.
+ // we always have to compare against an existing string to determine the
+ // exact correct point at which to insert a new node.
+ int branch = 0;
+ if_hot (radices[cur].critpos <= len << 1) {
+ branch = radixbranch(s, radices[cur].critpos, ignorecase);
+ }
+ while (!radices[cur].children[branch]) branch = (branch + 1) & 15;
+ nodep = radices[cur].children + branch;
+ }
+}
+
+/*
+ * Inserts an entry that's already in a string list. Can be used for
+ * dupe-checking after appending a new entry. Returns false on duplicate
+ * entries.
+ */
+static bool radix_insertidx(s16 *nodep, const struct cmeta_slice *strlist,
+ s16 stridx, bool ignorecase) {
+ const char *s = strlist[stridx].s; int len = strlist[stridx].len;
+ struct radix_insert_ret r = radix_insert(nodep, strlist, s, len, ignorecase);
+ if (r.isnew) *r.strp = -stridx;
+ return r.isnew;
+}
+
+/*
+ * Returns the string index of an existing entry matching `s` and `len`.
+ * Returns 0 (the reserved null index) if an entry does not exist.
+ */
+static s16 radix_lookup(s16 node, const struct cmeta_slice *strlist,
+ const char *s, int len, bool ignorecase) {
+ while (node) {
+ if (node < 0) {
+ const struct cmeta_slice matched = strlist[-node];
+ if (matched.len != len) return 0;
+ if (matchlen(matched.s, s, len, ignorecase) == len << 1) {
+ return -node;
+ }
+ return 0;
+ }
+ if (radices[node].critpos >= len << 1) return 0;
+ int branch = radixbranch(s, radices[node].critpos, ignorecase);
+ node = radices[node].children[branch];
+ }
+ return 0;
+}
+
+static inline void handle(s16 mod, s16 mods, s16 *featdescs, s16 *events,
+ const os_char *file, const struct cmeta *cm) {
+ bool isfeat = false;
+ const char *needfeat = 0;
+ bool canpreinit = true, haspreinit = false, hasinit = false;
+ struct list_chunkhdr *tail_needs = &mod_needs[mod].hdr;
+ struct list_chunkhdr *tail_wants = &mod_wants[mod].hdr;
+ struct list_chunkhdr *tail_gamedata = &mod_gamedata[mod].hdr;
+ struct list_chunkhdr *tail_globals = &mod_globals[mod].hdr;
+ for (u32 i = 0; i != cm->nitems; ++i) {
+ switch_exhaust_enum(cmeta_item, cm->itemtypes[i]) {
+ case CMETA_ITEM_DEF_CVAR:
+ if (!cmeta_nparams(cm, i)) {
+ diefile(2, file, cmeta_line(cm, i),
+ "cvar macro missing required parameter");
+ }
+ int flags = cmeta_flags_cvar(cm, i);
+ s16 idx = cvar_new();
+ cvar_flags[idx] = flags;
+ // NOTE: always hooking cvar/ccmd up to a feat entry for
+ // GAMESPECIFIC checks, even if it's not a DEF_FEAT_*.
+ // If a module doesn't declare FEATURE, its cvars/ccmds will
+ // end up getting initialised/registered unconditionally.
+ cvar_feats[idx] = mod;
+ if (flags & CMETA_CVAR_FEAT) needfeat = "feature cvar defined";
+ cmeta_param_foreach (name, cm, i) {
+ cvar_names[idx] = name;
+ break; // ignore subsequent args
+ }
+ break;
+ case CMETA_ITEM_DEF_CCMD:
+ if (!cmeta_nparams(cm, i)) {
+ diefile(2, file, cmeta_line(cm, i),
+ "ccmd macro missing required parameter");
+ }
+ flags = cmeta_flags_ccmd(cm, i);
+ if (flags & CMETA_CCMD_FEAT) {
+ needfeat = "feature ccmd defined";
+ }
+ if (flags & CMETA_CCMD_PLUSMINUS) {
+ // split PLUSMINUS entries in two; makes stuff easier later.
+ flags &= ~CMETA_CCMD_PLUSMINUS;
+ idx = ccmd_new();
+ ccmd_flags[idx] = flags;
+ ccmd_feats[idx] = mod;
+ cmeta_param_foreach (name, cm, i) {
+ char *s = arena + arena_new(5 + name.len);
+ memcpy(s, "PLUS_", 5);
+ memcpy(s + 5, name.s, name.len);
+ ccmd_names[idx] = (struct cmeta_slice){s, 5 + name.len};
+ break;
+ }
+ idx = ccmd_new();
+ ccmd_flags[idx] = flags;
+ ccmd_feats[idx] = mod;
+ cmeta_param_foreach (name, cm, i) {
+ char *s = arena + arena_new(6 + name.len);
+ memcpy(s, "MINUS_", 6);
+ memcpy(s + 6, name.s, name.len);
+ ccmd_names[idx] = (struct cmeta_slice){s, 6 + name.len};
+ break;
+ }
+ }
+ else {
+ idx = ccmd_new();
+ ccmd_flags[idx] = flags;
+ ccmd_feats[idx] = mod;
+ cmeta_param_foreach (name, cm, i) {
+ ccmd_names[idx] = name;
+ break;
+ }
+ }
+ break;
+ case CMETA_ITEM_DEF_EVENT:
+ if (!cmeta_nparams(cm, i)) {
+ diefile(2, file, cmeta_line(cm, i),
+ "event macro missing required parameter");
+ }
+ flags = cmeta_flags_event(cm, i);
+ struct cmeta_param_iter it = cmeta_param_iter_init(cm, i);
+ struct cmeta_slice evname = cmeta_param_iter(&it);
+ struct radix_insert_ret r = radix_insert(events, event_names,
+ evname.s, evname.len, false);
+ int e;
+ if (r.isnew) {
+ e = event_new();
+ *r.strp = -e;
+ event_names[e] = evname;
+ event_handlers_tails[e] = &event_handlers[e].hdr;
+ }
+ else {
+ e = r.stridx;
+ if (event_owners[e]) {
+ diefile(2, file, cmeta_line(cm, i),
+ "conflicting event definition");
+ }
+ }
+ event_owners[e] = mod;
+ event_predicateflags[e] = !!(flags & CMETA_EVENT_ISPREDICATE);
+ struct list_chunkhdr *tail = &event_params[e].hdr;
+ for (struct cmeta_slice param; param = cmeta_param_iter(&it),
+ param.s;) {
+ list_append(&tail, param);
+ }
+ break;
+ case CMETA_ITEM_HANDLE_EVENT:
+ int nparams = cmeta_nparams(cm, i);
+ if (!nparams) {
+ diefile(2, file, cmeta_line(cm, i),
+ "event handler macro missing required parameter");
+ }
+ it = cmeta_param_iter_init(cm, i);
+ evname = cmeta_param_iter(&it);
+ r = radix_insert(events, event_names, evname.s, evname.len,
+ false);
+ if (r.isnew) {
+ e = event_new();
+ *r.strp = -e;
+ event_names[e] = evname;
+ event_handlers_tails[e] = &event_handlers[e].hdr;
+ }
+ else {
+ e = r.stridx;
+ }
+ list_append(event_handlers_tails + e, mod);
+ mod_flags[mod] |= HAS_EVENTS;
+ break;
+ case CMETA_ITEM_FEATURE:
+ isfeat = true;
+ // note: if no param given, featdesc will still be null
+ cmeta_param_foreach (param, cm, i) {
+ mod_featdescs[mod] = param;
+ if (!radix_insertidx(featdescs, mod_featdescs, mod, true)) {
+ diefile(2, file, cmeta_line(cm, i),
+ "duplicate feature description text");
+ }
+ break;
+ }
+ break;
+ case CMETA_ITEM_REQUIRE:
+ if (!cmeta_nparams(cm, i)) {
+ diefile(2, file, cmeta_line(cm, i),
+ "dependency macro missing required parameter");
+ }
+ flags = cmeta_flags_require(cm, i);
+ struct list_chunkhdr **tailp;
+ switch_exhaust(flags) {
+ case 0: tailp = &tail_needs; break;
+ case CMETA_REQUIRE_OPTIONAL: tailp = &tail_wants; break;
+ case CMETA_REQUIRE_GAMEDATA: tailp = &tail_gamedata; break;
+ case CMETA_REQUIRE_GLOBAL: tailp = &tail_globals;
+ }
+ cmeta_param_foreach(param, cm, i) {
+ int modflags = 0;
+ switch_exhaust(flags) {
+ case CMETA_REQUIRE_OPTIONAL:
+ modflags = HAS_OPTDEPS;
+ case 0:
+ canpreinit = false;
+ s16 depmod = radix_lookup(mods, mod_names, param.s,
+ param.len, false);
+ if (!depmod) {
+ fprintf(stderr, "cmeta_fatal: %" fS ":%d: "
+ "feature `%.*s` does not exist\n",
+ file, cmeta_line(cm, i),
+ param.len, param.s);
+ exit(2);
+ }
+ mod_flags[depmod] |= modflags;
+ list_append(tailp, depmod);
+ break;
+ case CMETA_REQUIRE_GAMEDATA: case CMETA_REQUIRE_GLOBAL:
+ list_append(tailp, param);
+ }
+ }
+ break;
+ case CMETA_ITEM_GAMESPECIFIC:
+ canpreinit = false;
+ if_cold (!cmeta_nparams(cm, i)) {
+ diefile(2, file, cmeta_line(cm, i),
+ "GAMESPECIFIC macro missing required parameter");
+ }
+ needfeat = "GAMESPECIFIC specified";
+ if_cold (mod_gamespecific[mod].s) {
+ diefile(2, file, cmeta_line(cm, i),
+ "conflicting GAMESPECIFIC macros");
+ }
+ cmeta_param_foreach(param, cm, i) {
+ mod_gamespecific[mod] = param;
+ break;
+ }
+ break;
+ case CMETA_ITEM_INIT:
+ if (hasinit) {
+ diefile(2, file, cmeta_line(cm, i), "multiple INIT blocks");
+ }
+ hasinit = true;
+ mod_flags[mod] |= HAS_INIT;
+ needfeat = "INIT block defined";
+ break;
+ case CMETA_ITEM_PREINIT:
+ if (haspreinit) {
+ diefile(2, file, cmeta_line(cm, i),
+ "multiple PREINIT blocks");
+ }
+ haspreinit = true;
+ mod_flags[mod] |= HAS_PREINIT;
+ needfeat = "PREINIT block defined";
+ break;
+ case CMETA_ITEM_END:
+ if (mod_flags[mod] & HAS_END) {
+ diefile(2, file, cmeta_line(cm, i), "multiple END blocks");
+ }
+ mod_flags[mod] |= HAS_END;
+ needfeat = "END block defined";
+ }
+ }
+ if (needfeat && !isfeat) {
+ fprintf(stderr, "gluegen: fatal: %" fS ": %s without FEATURE()", file,
+ needfeat);
+ exit(2);
+ }
+ if (isfeat && !hasinit) {
+ diefile(2, file, 0, "feature is missing INIT block");
+ }
+ if (!canpreinit && haspreinit) {
+ diefile(2, file, 0, "cannot use dependencies along with PREINIT");
+ }
+}
+
+static int dfs(s16 mod, bool first);
+static int dfs_inner(s16 mod, s16 dep, bool first) {
+ if (!(mod_flags[dep] & HAS_INIT)) {
+ fprintf(stderr, "gluegen: fatal: feature `%.*s` tried to depend on "
+ "non-feature module `%.*s`\n",
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[dep].len, mod_names[dep].s);
+ exit(2);
+ }
+ switch (dfs(dep, false)) {
+ // unwind the call stack by printing each node in the dependency cycle.
+ // ASCII arrows are kind of ugly here but windows' CRT handles unicode
+ // *so* horrendously and I'm not in the mood to replace stdio for this
+ // (maybe another time!)
+ case 1:
+ fprintf(stderr, ".-> %.*s\n",
+ mod_names[mod].len, mod_names[mod].s);
+ return 2;
+ case 2:
+ fprintf(stderr, first ? "'-- %.*s\n" : "| %.*s\n",
+ mod_names[mod].len, mod_names[mod].s);
+ return 2;
+ }
+ return 0;
+}
+static int dfs(s16 mod, bool first) {
+ if (mod_flags[mod] & DFS_SEEN) return 0;
+ if (mod_flags[mod] & DFS_SEEING) {
+ fprintf(stderr, "gluegen: fatal: feature dependency cycle:\n");
+ return 1;
+ }
+ mod_flags[mod] |= DFS_SEEING;
+ list_foreach(s16, dep, mod_needs + mod) {
+ if (dfs_inner(mod, dep, first)) return 2;
+ }
+ list_foreach(s16, dep, mod_wants + mod) {
+ if (dfs_inner(mod, dep, first)) return 2;
+ }
+ feat_initorder[nfeatures++] = mod;
+ mod_flags[mod] |= DFS_SEEN; // note: no need to bother unsetting SEEING.
+ return 0;
+}
+
+static inline void sortfeatures(void) {
+ for (int i = 1; i < nmods; ++i) {
+ if ((mod_flags[i] & HAS_INIT) && dfs(i, true)) exit(2);
+ }
+}
+
+static inline noreturn diewrite(void) { die(100, "couldn't write to file"); }
+#define _(x) \
+ if (fprintf(out, "%s\n", x) < 0) diewrite();
+#define F(f, ...) \
+ if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite();
+#define H_() \
+ _("/* This file is autogenerated by src/build/gluegen.c. DO NOT EDIT! */")
+#define H() H_() _("")
+
+static void recursefeatdescs(FILE *out, s16 node) {
+ if (node < 0) {
+ if (mod_featdescs[-node].s) {
+F( " if (status_%.*s != FEAT_SKIP) {",
+ mod_names[-node].len, mod_names[-node].s)
+F( " con_colourmsg(status_%.*s == FEAT_OK ? &green : &red,",
+ mod_names[-node].len, mod_names[-node].s)
+F( " featmsgs[status_%.*s], %.*s);",
+ mod_names[-node].len, mod_names[-node].s,
+ mod_featdescs[-node].len, mod_featdescs[-node].s)
+_( " }")
+ }
+ }
+ else if (node > 0) {
+ for (int i = 0; i < 16; ++i) {
+ recursefeatdescs(out, radices[node].children[i]);
+ }
+ }
+}
+
+static int evargs(FILE *out, s16 i, const char *suffix) {
+ int j = 1;
+ if (fprintf(out, "(") < 0) diewrite();
+ list_foreach (struct cmeta_slice, param, event_params + i) {
+ if (param.len == 4 && !memcmp(param.s, "void", 4)) {
+ // UGH, crappy special case for (void). with C23 this could be
+ // redundant, but I think we still want to avoid blowing up gluegen
+ // if someone (me?) does it the old way out of habit.
+ // also if someone does void, int something_else this will create
+ // nonsense, but that's just garbage-in-garbage-out I guess.
+ break;
+ }
+ else if (fprintf(out, "%stypeof(%.*s) a%d", j == 1 ? "" : ", ",
+ param.len, param.s, j) < 0) {
+ diewrite();
+ }
+ ++j;
+ }
+ if (fputs(suffix, out) < 0) diewrite();
+ return j;
+}
+
+static int evargs_notype(FILE *out, s16 i, const char *suffix) {
+ int j = 1;
+ if (fprintf(out, "(") < 0) diewrite();
+ list_foreach(struct cmeta_slice, param, event_params + i) {
+ if (param.len == 4 && !memcmp(param.s, "void", 4)) {
+ break;
+ }
+ if (fprintf(out, "%sa%d", j == 1 ? "" : ", ", j) < 0) {
+ diewrite();
+ }
+ ++j;
+ }
+ if (fputs(suffix, out) < 0) diewrite();
+ return j;
+}
+
+static inline void gencode(FILE *out, s16 featdescs) {
+ for (int i = 1; i < nmods; ++i) {
+ if (mod_flags[i] & HAS_INIT) {
+F( "extern int _feat_init_%.*s(void);", mod_names[i].len, mod_names[i].s)
+ }
+ if (mod_flags[i] & HAS_PREINIT) {
+F( "extern int _feat_preinit_%.*s(void);", mod_names[i].len, mod_names[i].s)
+ }
+ if (mod_flags[i] & HAS_END) {
+F( "extern void _feat_end_%.*s(void);", mod_names[i].len, mod_names[i].s)
+ }
+ }
+_( "")
+_( "static struct {")
+ for (int i = 1; i < nmods; ++i) {
+ if (mod_flags[i] & HAS_OPTDEPS) continue;
+ if ((mod_flags[i] & HAS_INIT) &&
+ (mod_flags[i] & (HAS_END | HAS_EVENTS))) {
+F( " bool _has_%.*s : 1;", mod_names[i].len, mod_names[i].s)
+ }
+ }
+ for (int i = 1; i < nmods; ++i) {
+ if (mod_flags[i] & HAS_PREINIT) {
+F( " int preinit_%.*s : 2;", mod_names[i].len, mod_names[i].s)
+ }
+ }
+_( "} feats = {0};")
+_( "")
+ for (int i = 1; i < nmods; ++i) {
+ if (!(mod_flags[i] & HAS_INIT)) continue;
+ if (mod_flags[i] & HAS_OPTDEPS) {
+ // If something REQUESTS a feature, it needs to be able to query
+ // has_*, so make it extern. XXX: this could be bitpacked as well
+ // potentially however the whole struct would then have to be
+ // included in all the places that use feature macros, which would
+ // probably screw up the potential for fast incremental builds...
+F( "bool has_%.*s = false;", mod_names[i].len, mod_names[i].s)
+ }
+ else if (mod_flags[i] & (HAS_END | HAS_EVENTS)) {
+ // mildly stupid, but easier really. paper over the difference so we
+ // can generate the same has_ checks elsewhere :^)
+F( "#define has_%.*s (feats._has_%.*s)",
+ mod_names[i].len, mod_names[i].s, mod_names[i].len, mod_names[i].s)
+ }
+ }
+_( "")
+ for (int i = 1; i < ncvars; ++i) {
+F( "extern struct con_var *%.*s;", cvar_names[i].len, cvar_names[i].s);
+ }
+ for (int i = 1; i < nccmds; ++i) {
+F( "extern struct con_cmd *%.*s;", ccmd_names[i].len, ccmd_names[i].s);
+ }
+_( "")
+_( "static inline void preinitfeatures(void) {")
+ for (int i = 1; i < nmods; ++i) {
+ if (mod_flags[i] & HAS_PREINIT) {
+F( " feats.preinit_%.*s = _feat_preinit_%.*s();",
+ mod_names[i].len, mod_names[i].s, mod_names[i].len, mod_names[i].s)
+ }
+ }
+_( "}")
+_( "")
+_( "static inline void initfeatures(void) {")
+ for (int i = 0; i < nfeatures; ++i) { // N.B.: this *should* be 0-indexed!
+ const char *else_ = "";
+ s16 mod = feat_initorder[i];
+ if (mod_flags[mod] & HAS_PREINIT) {
+F( " s8 status_%.*s = feats.preinit_%.*s;",
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
+ }
+ else {
+F( " s8 status_%.*s;", mod_names[mod].len, mod_names[mod].s)
+ }
+ if (mod_gamespecific[mod].s) {
+F( " %sif (!GAMETYPE_MATCHES(%.*s)) status_%.*s = FEAT_SKIP;", else_,
+ mod_gamespecific[mod].len, mod_gamespecific[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
+ else_ = "else ";
+ }
+ list_foreach (struct cmeta_slice, gamedata, mod_gamedata + mod) {
+ // this is not a *totally* ideal way of doing this, but it's easy.
+ // if we had some info about what gamedata was doing, we could avoid
+ // having to ifdef these cases and could just directly generate the
+ // right thing. but that'd be quite a bit of work, so... we don't!
+ if (mod_gamespecific[mod].s) {
+F( "#ifdef _GAMES_WITH_%.*s", gamedata.len, gamedata.s)
+F( " %sif (!(_gametype_tag_%.*s & _GAMES_WITH_%.*s) && !has_%.*s) {", else_,
+ mod_gamespecific[mod].len, mod_gamespecific[mod].s,
+ gamedata.len, gamedata.s, gamedata.len, gamedata.s)
+F( " status_%.*s = NOGD;", mod_names[mod].len, mod_names[mod].s)
+_( " }")
+_( "#else")
+ }
+F( " %sif (!has_%.*s) status_%.*s = NOGD;", else_,
+ gamedata.len, gamedata.s, mod_names[mod].len, mod_names[mod].s)
+ if (mod_gamespecific[mod].s) {
+_( "#endif")
+ }
+ else_ = "else ";
+ }
+ list_foreach (struct cmeta_slice, global, mod_globals + mod) {
+F( " %sif (!(%.*s)) status_%.*s = NOGLOBAL;", else_,
+ global.len, global.s, mod_names[mod].len, mod_names[mod].s)
+ else_ = "else ";
+ }
+ list_foreach (s16, dep, mod_needs + mod) {
+F( " %sif (status_%.*s != FEAT_OK) status_%.*s = REQFAIL;", else_,
+ mod_names[dep].len, mod_names[dep].s,
+ mod_names[mod].len, mod_names[mod].s)
+ else_ = "else ";
+ }
+ if (mod_flags[mod] & (HAS_END | HAS_EVENTS | HAS_OPTDEPS)) {
+F( " %sif ((status_%.*s = _feat_init_%.*s()) == FEAT_OK) has_%.*s = true;",
+ else_,
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
+ }
+ else {
+F( " %sstatus_%.*s = _feat_init_%.*s();", else_,
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
+ }
+ }
+_( "")
+ for (int i = 1; i < ncvars; ++i) {
+ if (!(cvar_flags[i] & CMETA_CVAR_UNREG)) {
+ if (cvar_flags[i] & CMETA_CVAR_FEAT) {
+ struct cmeta_slice modname = mod_names[cvar_feats[i]];
+F( " if (status_%.*s != FEAT_SKIP) con_regvar(%.*s);",
+ modname.len, modname.s, cvar_names[i].len, cvar_names[i].s)
+F( " else if (status_%.*s != FEAT_OK) %.*s->base.flags |= CON_HIDDEN;",
+ modname.len, modname.s, cvar_names[i].len, cvar_names[i].s)
+ }
+ else {
+F( " con_regvar(%.*s);", cvar_names[i].len, cvar_names[i].s)
+ }
+ }
+ }
+ for (int i = 1; i < nccmds; ++i) {
+ if (!(ccmd_flags[i] & CMETA_CCMD_UNREG)) {
+ if (ccmd_flags[i] & CMETA_CCMD_FEAT) {
+ struct cmeta_slice modname = mod_names[ccmd_feats[i]];
+F( " if (status_%.*s == FEAT_OK) con_regcmd(%.*s);",
+ modname.len, modname.s, ccmd_names[i].len, ccmd_names[i].s)
+ }
+ else {
+F( " con_regcmd(%.*s);", ccmd_names[i].len, ccmd_names[i].s)
+ }
+ }
+ }
+_( "")
+_( " successbanner();")
+_( " struct rgba white = {255, 255, 255, 255};")
+_( " struct rgba green = {128, 255, 128, 255};")
+_( " struct rgba red = {255, 128, 128, 255};")
+_( " con_colourmsg(&white, \"---- List of plugin features ---\\n\");");
+ recursefeatdescs(out, featdescs);
+_( "}")
+_( "")
+_( "static inline void endfeatures(void) {")
+ for (int i = nfeatures - 1; i >= 0; --i) {
+ s16 mod = feat_initorder[i];
+ if (mod_flags[mod] & HAS_END) {
+F( " if (has_%.*s) _feat_end_%.*s();",
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
+ }
+ }
+_( "}")
+_( "")
+_( "static inline void freevars(void) {")
+ for (int i = 1; i < ncvars; ++i) {
+F( " extfree(%.*s->strval);", cvar_names[i].len, cvar_names[i].s)
+ }
+_( "}")
+ for (int i = 1; i < nevents; ++i) {
+ const char *prefix = event_predicateflags[i] ?
+ "bool CHECK_" : "void EMIT_";
+ if (fprintf(out, "\n%s%.*s", prefix,
+ event_names[i].len, event_names[i].s) < 0) {
+ diewrite();
+ }
+ evargs(out, i, ") {\n");
+ list_foreach(s16, mod, event_handlers + i) {
+ const char *type = event_predicateflags[i] ? "bool" : "void";
+ if (fprintf(out, "\t%s _evhandler_%.*s_%.*s", type,
+ mod_names[mod].len, mod_names[mod].s,
+ event_names[i].len, event_names[i].s) < 0) {
+ diewrite();
+ }
+ evargs(out, i, ");\n");
+ if (event_predicateflags[i]) {
+ if (mod_flags[mod] & HAS_INIT) {
+ if (fprintf(out, "\tif (has_%.*s && !",
+ mod_names[mod].len, mod_names[mod].s) < 0) {
+ diewrite();
+ }
+ }
+ else if (fputs("\tif (!", out) < 0) {
+ diewrite();
+ }
+ if (fprintf(out, "_evhandler_%.*s_%.*s",
+ mod_names[mod].len, mod_names[mod].s,
+ event_names[i].len, event_names[i].s) < 0) {
+ diewrite();
+ }
+ evargs_notype(out, i, ")) return false;\n");
+ }
+ else {
+ if (fputc('\t', out) < 0) diewrite();
+ if ((mod_flags[mod] & HAS_INIT) && fprintf(out, "if (has_%.*s) ",
+ mod_names[mod].len, mod_names[mod].s) < 0) {
+ diewrite();
+ }
+ if (fprintf(out, "_evhandler_%.*s_%.*s",
+ mod_names[mod].len, mod_names[mod].s,
+ event_names[i].len, event_names[i].s) < 0) {
+ diewrite();
+ }
+ evargs_notype(out, i, ");\n");
+ }
+ }
+ if (event_predicateflags[i]) {
+_( " return true;")
+ }
+_( "}")
+ }
+}
+
+int OS_MAIN(int argc, os_char *argv[]) {
+ s16 modlookup = 0, featdesclookup = 0, eventlookup = 0;
+ if (argc > MAX_MODULES) {
+ die(2, "too many files passed - increase MAX_MODULES in gluegen.c!");
+ }
+ nmods = argc;
+ for (int i = 1; i < nmods; ++i) {
+ const os_char *f = argv[i];
+ int len = 0;
+ int lastpart = 0;
+ for (; f[len]; ++len) {
+#ifdef _WIN32
+ if (f[len] == '/' || f[len] == '\\') lastpart = len + 1;
+#else
+ if (f[len] == '/') lastpart = len + 1;
+#endif
+ }
+ if_cold (!len) die(1, "empty file path given");
+ if_cold (len < lastpart) diefile(1, f, 0, "invalid file path");
+ if_cold (len < 3 || memcmp(f + len - 2, OS_LIT(".c"),
+ 2 * ssizeof(os_char))) {
+ diefile(1, f, 0, "not a C source file (.c)");
+ }
+ struct cmeta_slice modname;
+ // ugh. same dumb hack from compile scripts
+ if_cold (len - lastpart == 6 && !memcmp(f + lastpart, OS_LIT("con_.c"),
+ 6 * ssizeof(os_char))) {
+ modname.s = "con"; modname.len = 3;
+ }
+ else {
+ char *p = arena + arena_new(len - lastpart - 2);
+ // XXX: Unicode isn't real, it can't hurt you.
+ for (int i = lastpart, j = 0; i < len - 2; ++i, ++j) p[j] = f[i];
+ modname.s = p; modname.len = len - lastpart - 2;
+ }
+ mod_names[i] = modname;
+ if (!radix_insertidx(&modlookup, mod_names, i, false)) {
+ // XXX: might have to fix this some day to handle subdirs and such.
+ // for now we rely on it happening not to be a problem basically lol
+ diefile(2, f, 0, "duplicate module name");
+ }
+ }
+ for (int i = 1; i < nmods; ++i) {
+ struct cmeta cm = cmeta_loadfile(argv[i]);
+ handle(i, modlookup, &featdesclookup, &eventlookup, argv[i], &cm);
+ }
+ // double check that events are defined. the compiler would also catch this,
+ // but we can do it faster and with arguably more helpful error information.
+ for (int i = 1; i < nevents; ++i) {
+ if (!event_owners[i]) {
+ fprintf(stderr, "gluegen: fatal: undefined event %.*s\n",
+ event_names[i].len, event_names[i].s);
+ exit(2);
+ }
+ }
+ sortfeatures();
+
+ FILE *out = fopen(".build/include/glue.gen.h", "wb");
+ if (!out) die(100, "couldn't open .build/include/glue.gen.h");
+ H()
+ gencode(out, featdesclookup);
+ if (fflush(out)) die(100, "couldn't finish writing output");
+ return 0;
+}
+
+// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/build/mkentprops.c b/src/build/mkentprops.c
index cda1c02..3df21ed 100644
--- a/src/build/mkentprops.c
+++ b/src/build/mkentprops.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -30,7 +30,7 @@
#endif
static noreturn die(int status, const char *s) {
- fprintf(stderr, "mkentprops: %s\n", s);
+ fprintf(stderr, "mkentprops: fatal: %s\n", s);
exit(status);
}
static noreturn dieparse(const os_char *file, int line, const char *s) {
@@ -194,7 +194,7 @@ static inline void handleentry(char *k, char *v, int vlen,
}
}
-static void parse(const os_char *file, int len) {
+static inline void parse(const os_char *file, int len) {
char *s = sbase; // for convenience
if (s[len - 1] != '\n') dieparse(file, 0, "invalid text file (missing EOL)");
enum { BOL = 0, KEY = 4, KWS = 8, VAL = 12, COM = 16, ERR = -1 };
@@ -279,14 +279,12 @@ Fi(" %s = off;", sbase + art_leaves[idx].varstr);
_i(" if (mem_loads32(mem_offset(sp, off_SP_type)) == DPT_DataTable) {")
_i(" int baseoff = off;")
_i(" const struct SendTable *st = mem_loadptr(mem_offset(sp, off_SP_subtable));")
-_i(" // BEGIN SUBTABLE")
Fi(" for (int i = 0, need = %d; i < st->nprops && need; ++i) {",
art_leaves[idx].nsubs + (art_leaves[idx].varstr != -1))
_i(" const struct SendProp *sp = mem_offset(st->props, sz_SendProp * i);")
_i(" const char *p = mem_loadptr(mem_offset(sp, off_SP_varname));")
dosendtables(out, art_leaves[idx].subtree, indent + 4);
_i(" }")
-_i(" // END SUBTABLE")
_i(" }")
}
Fi(" --need;")
@@ -338,7 +336,7 @@ _i(" } break;")
_i("}")
}
-static void dodecls(FILE *out) {
+static inline void dodecls(FILE *out) {
for (int i = 0; i < ndecls; ++i) {
const char *s = sbase + decls[i];
F( "extern int %s;", s);
@@ -346,7 +344,7 @@ F( "#define has_%s (!!%s)", s, s); // offsets will NEVER be 0, due to vtable!
}
}
-static void doinit(FILE *out) {
+static inline void doinit(FILE *out) {
for (int i = 0; i < ndecls; ++i) {
const char *s = sbase + decls[i];
F( "int %s = 0;", s);
diff --git a/src/build/mkgamedata.c b/src/build/mkgamedata.c
index 1fce1cf..ed8cf97 100644
--- a/src/build/mkgamedata.c
+++ b/src/build/mkgamedata.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -29,7 +29,7 @@
#endif
static noreturn die(int status, const char *s) {
- fprintf(stderr, "mkentprops: %s\n", s);
+ fprintf(stderr, "mkentprops: fatal: %s\n", s);
exit(status);
}
@@ -156,7 +156,37 @@ static inline noreturn diewrite(void) { die(100, "couldn't write to file"); }
_( "/* This file is autogenerated by src/build/mkgamedata.c. DO NOT EDIT! */") \
_( "")
-static void decls(FILE *out) {
+static inline void knowngames(FILE *out) {
+ // kind of tricky optimisation: if a gamedata entry has no default but
+ // does have game-specific values which match a feature's GAMESPECIFIC()
+ // macro, load-time conditional checks resulting from REQUIRE_GAMEDATA() can
+ // be elided at compile-time.
+ for (int i = 0, j; i < nents - 1; i = j) {
+ while (exprs[i]) { // if there's a default value, we don't need this
+ // skip to next unindented thing, return if there isn't one with at
+ // least one indented thing under it.
+ for (++i; indents[i] != 0; ++i) if (i == nents - 1) return;
+ }
+F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
+ if (fprintf(out, "#define _GAMES_WITH_%s (", sbase + tags[i]) < 0) {
+ diewrite();
+ }
+ const char *pipe = "";
+ for (j = i + 1; j < nents && indents[j] != 0; ++j) {
+ // don't attempt to optimise for nested conditionals because that's
+ // way more complicated and also basically defeats the purpose.
+ if (indents[j] != 1) continue;
+ if (fprintf(out, "%s \\\n\t _gametype_tag_%s", pipe,
+ sbase + tags[j]) < 0) {
+ diewrite();
+ }
+ pipe = " |";
+ }
+ fputs(" \\\n)\n", out);
+ }
+}
+
+static inline void decls(FILE *out) {
for (int i = 0; i < nents; ++i) {
if (indents[i] != 0) continue;
F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
@@ -175,13 +205,13 @@ F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
if_cold (i == nents - 1 || !indents[i + 1]) { // no tags - it's constant
F( "enum { %s = (%s) };", sbase + tags[i], sbase + exprs[i])
}
- else { // global variable intialised by gamedata_init() call
+ else { // global variable intialised by initgamedata() call
F( "extern int %s;", sbase + tags[i]);
}
}
}
-static void defs(FILE *out) {
+static inline void defs(FILE *out) {
for (int i = 0; i < nents; ++i) {
if (indents[i] != 0) continue;
if_hot (i < nents - 1 && indents[i + 1]) {
@@ -196,8 +226,8 @@ F( "int %s = -2147483648;", sbase + tags[i])
}
}
-static void init(FILE *out) {
-_( "void gamedata_init(void) {")
+static inline void init(FILE *out) {
+_( "static void initgamedata(void) {")
int varidx;
int indent = 0;
for (int i = 0; i < nents; ++i) {
@@ -261,6 +291,7 @@ int OS_MAIN(int argc, os_char *argv[]) {
FILE *out = fopen(".build/include/gamedata.gen.h", "wb");
if (!out) die(100, "couldn't open gamedata.gen.h");
H();
+ knowngames(out);
decls(out);
out = fopen(".build/include/gamedatainit.gen.h", "wb");
diff --git a/src/build/skiplist.h b/src/build/skiplist.h
deleted file mode 100644
index ab6a920..0000000
--- a/src/build/skiplist.h
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * Copyright © 2024 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.
- */
-
-#ifndef INC_SKIPLIST_H
-#define INC_SKIPLIST_H
-
-#include <stdlib.h>
-
-#include "../intdefs.h"
-#include "../os.h"
-
-#ifdef _WIN32
-static inline int _skiplist_ffs(uint x) {
- uchar _BitScanForward(ulong *idx, ulong mask);
- uint ret;
- // on Windows, sizeof(ulong) == sizeof(uint)
- if (_BitScanForward((ulong *)&ret, x)) return ret + 1; else return 0;
-}
-#else
-#include <strings.h>
-#define _skiplist_ffs ffs
-#endif
-
-// WARNING: this is a really hacked-up version of the skiplist.h from cbits in
-// order to support windows. It probably isn't a good idea to plop straight into
-// your own use case.
-
-#if defined(__GNUC__) || defined(__clang__)
-#define _skiplist_unused __attribute__((unused)) // heck off gcc
-#else
-#define _skiplist_unused
-#endif
-
-// NOTE: using xoroshiro128++, a comparatively bad (i.e. non-cryptographic) prng
-// for the sake of simplicity; original cbits skiplist.h relies on libcpoly to
-// get arc4random() everywhere but since we're only using this at build time
-// that seemed like a silly dependency to bother with.
-//#define _skiplist_rng arc4random
-
-// ALSO NOTE: the PRNG code here is *decidedly not* thread safe. again, this
-// isn't a problem for our use case. just keep it in mind if reusing this header
-// for something else. or ideally, don't reuse this header for something else...
-static inline uvlong _skiplist_rotl(const uvlong x, int k) {
- return (x << k) | (x >> (64 - k));
-}
-_skiplist_unused static uvlong _skiplist_rng(void) {
- static uvlong s[2];
- static bool init = false;
- if (!init) { os_randombytes(s, sizeof(s)); init = true; }
- uvlong s0 = s[0], s1 = s[1];
- uvlong ret = _skiplist_rotl(s0 * 5, 7) * 9;
- s1 ^= s0;
- s[0] = _skiplist_rotl(s0, 24) ^ s1 ^ (s1 << 16);
- s[1] = _skiplist_rotl(s1, 37);
- return ret;
-}
-
-/*
- * Declares the skiplist header struct skiplist_hdr##name, but none of the
- * associated functions. Use when the structure needs to be passed around in
- * some way but actual operations on the list are a private implementation
- * detail. Otherwise, see DECL_SKIPLIST below.
- */
-#define DECL_SKIPLIST_TYPE(name, dtype, ktype, levels) \
-typedef dtype _skiplist_dt_##name; \
-typedef ktype _skiplist_kt_##name; \
-enum { skiplist_lvls_##name = (levels) }; \
-struct skiplist_hdr_##name { dtype *x[levels]; };
-
-/*
- * Declares the skiplist header struct skiplist_hdr_##name, with functions
- * skiplist_{get,del,pop,insert}_##name for operating on the list. A single
- * occurrence of DEF_SKIPLIST is required to actually implement the
- * functions.
- *
- * This macro implies DECL_SKIPLIST_TYPE (both should not be used).
- *
- * mod should be either static or extern.
- *
- * dtype should be the struct type that the skiplist header will be embedded in,
- * forming the linked structure.
- *
- * ktype should be the type of the struct member used for comparisons, for
- * example int or char *.
- *
- * levels should be the number of levels in each node. 4 is probably a
- * reasonable number, depending on the size of the structure and how many
- * entries need to be stored and looked up.
- *
- * The resulting get, del, pop and insert functions are hopefully self-
- * explanatory - get and del return the relevant node or a null pointer if
- * no such node is found.
- */
-#define DECL_SKIPLIST(mod, name, dtype, ktype, levels) \
-DECL_SKIPLIST_TYPE(name, dtype, ktype, levels) \
-\
-_skiplist_unused mod dtype *skiplist_get_##name(struct skiplist_hdr_##name *l, \
- ktype k); \
-_skiplist_unused mod dtype *skiplist_del_##name(struct skiplist_hdr_##name *l, \
- ktype k); \
-_skiplist_unused mod dtype *skiplist_pop_##name(struct skiplist_hdr_##name *l); \
-_skiplist_unused mod void skiplist_insert_##name(struct skiplist_hdr_##name *l, \
- ktype k, dtype *node);
-
-/*
- * Implements the functions corresponding to a skiplist - must come after
- * DECL_SKIPLIST with the same modifier and name.
- *
- * compfunc should be a function declared as follows (or an equivalent macro):
- * int cf(dtype *x, ktype y);
- *
- * hdrfunc should be a function declared as follows (or an equivalent macro):
- * struct skiplist_hdr_##name *hf(dtype *l);
- */
-#define DEF_SKIPLIST(mod, name, compfunc, hdrfunc) \
-static inline int _skiplist_lvl_##name(void) { \
- int i; \
- /* for 2 levels we get 1 50% of the time, 2 25% of the time, 0 25% of the
- time. loop if 0 to distribute this evenly (this gets less likely the more
- levels there are: at 4 levels, only loops 6% of the time) */ \
- while (!(i = _skiplist_ffs(_skiplist_rng() & \
- ((1 << skiplist_lvls_##name) - 1)))); \
- /* ffs gives bit positions as 1-N but we actually want an array index */ \
- return i - 1; \
-} \
-\
-_skiplist_unused \
-mod _skiplist_dt_##name *skiplist_get_##name(struct skiplist_hdr_##name *l, \
- _skiplist_kt_##name k) { \
- for (int cmp, lvl = skiplist_lvls_##name - 1; lvl > -1; --lvl) { \
- while (l->x[lvl] && (cmp = compfunc(l->x[lvl], k)) < 0) { \
- l = hdrfunc(l->x[lvl]); \
- } \
- /* NOTE: cmp can be uninitialised here, but only if the list is
- completely empty, in which case we'd return 0 anyway - so it doesn't
- actually matter! */ \
- if (cmp == 0) return l->x[lvl]; \
- } \
- /* reached the end, no match */ \
- return 0; \
-} \
-\
-_skiplist_unused \
-_skiplist_dt_##name *skiplist_del_##name(struct skiplist_hdr_##name *l, \
- _skiplist_kt_##name k) { \
- _skiplist_dt_##name *ret = 0; \
- /* ALSO NOTE: in *this* case, cmp DOES need to be initialised to prevent a
- possible null-deref via hdrfunc(l->x[lvl])->x */ \
- for (int cmp = 1, lvl = skiplist_lvls_##name - 1; lvl > -1; --lvl) { \
- while (l->x[lvl] && (cmp = compfunc(l->x[lvl], k)) < 0) { \
- l = hdrfunc(l->x[lvl]); \
- } \
- if (cmp == 0) { \
- ret = l->x[lvl]; \
- /* just shift each link by 1 */ \
- l->x[lvl] = hdrfunc(l->x[lvl])->x[0]; \
- /* ... and update every level of links via loop */ \
- } \
- } \
- /* reached the end, return whatever was found */ \
- return ret; \
-} \
-\
-_skiplist_unused \
-mod _skiplist_dt_##name *skiplist_pop_##name(struct skiplist_hdr_##name *l) { \
- _skiplist_dt_##name *cur = l->x[0]; \
- if (!cur) return 0; \
- l->x[0] = hdrfunc(cur)->x[0]; \
- for (int lvl = 1; lvl < skiplist_lvls_##name; ++lvl) { \
- if (l->x[lvl]) l->x[lvl] = hdrfunc(l->x[lvl])->x[lvl]; \
- } \
- return cur; \
-} \
-\
-_skiplist_unused \
-mod void skiplist_insert_##name(struct skiplist_hdr_##name *l, \
- _skiplist_kt_##name k, _skiplist_dt_##name *node) { \
- /* note: higher levels are unset but also skipped in other searches */ \
- int inslvl = _skiplist_lvl_##name(); \
- for (int lvl = skiplist_lvls_##name - 1; lvl > -1; --lvl) { \
- while (l->x[lvl] && compfunc(l->x[lvl], k) < 0) { \
- l = hdrfunc(l->x[lvl]); \
- } \
- if (lvl <= inslvl) { \
- hdrfunc(node)->x[lvl] = l->x[lvl]; \
- l->x[lvl] = node; \
- } \
- } \
-} \
-
-#endif
-
-// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/build/vec.h b/src/build/vec.h
deleted file mode 100644
index 6dfa645..0000000
--- a/src/build/vec.h
+++ /dev/null
@@ -1,95 +0,0 @@
-/* This file is dedicated to the public domain. */
-
-#ifndef INC_VEC_H
-#define INC_VEC_H
-
-#include <errno.h>
-#include <stdlib.h>
-
-#include "../intdefs.h"
-
-struct _vec {
- uint sz;
- uint max;
- void *data;
-};
-
-/*
- * A dynamic array with push, pop and concatenate operations.
- *
- * Usage: struct VEC(my_type) myvec = {0};
- * Or: struct myvec VEC(my_type);
- * Or: typedef struct VEC(my_type) myvec;
- */
-#define VEC(type) { \
- uint sz; \
- uint max; \
- type *data; \
-}
-
-#if defined(__GNUC__) || defined(__clang__)
-__attribute__((unused)) // heck off gcc
-#endif
-static bool _vec_ensure(struct _vec *v, uint tsize, uint newmax) {
- // FIXME: potential overflow at least on 32-bit hosts (if any!?).
- // should use reallocarray or something but didn't feel like porting right
- // now. consider doing later.
- void *new = realloc(v->data, tsize * newmax);
- if (new) { v->data = new; v->max = newmax; }
- return !!new;
-}
-
-#if defined(__GNUC__) || defined(__clang__)
-__attribute__((unused)) // heck off gcc 2
-#endif
-static bool _vec_make_room(struct _vec *v, uint tsize, uint addcnt) {
- // this overflow check is probably unnecessary, but just in case
- u64 chk = v->max + addcnt;
- if (chk > 1u << 30) { errno = ENOMEM; return false; }
- u32 x = chk;
- if (x < 16) {
- x = 16;
- }
- else {
- // round up to next 2*n
- --x;
- x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16;
- x++;
- }
- return _vec_ensure(v, tsize, x);
-}
-
-// internal: for reuse by vec0
-#define _vec_push(v, val, slack) ( \
- ((v)->sz + (slack) < (v)->max || \
- _vec_make_room((struct _vec *)(v), sizeof(val), 1)) && /*NOLINT*/ \
- ((v)->data[(v)->sz++ - slack] = (val), true) \
-)
-
-#define _vec_pushall(v, vals, n, slack) ( \
- ((v)->sz + (n) + (slack) <= (v)->max || \
- _vec_make_room((struct _vec *)(v), sizeof(*(vals)), (n))) && \
- (memcpy((v)->data + (v)->sz - (slack), (vals), (n) * sizeof(*(vals))), \
- (v)->sz += (n), true) \
-)
-
-/*
- * Appends an item to the end of a vector. Gives true on success and false if
- * memory allocation fails.
- */
-#define vec_push(v, val) _vec_push(v, val, 0)
-
-/*
- * Appends n items from an array to the end of a vector. Gives true on success
- * and false if memory allocation fails.
- */
-#define vec_pushall(v, vals, n) _vec_pushall(v, vals, n, 0)
-
-/*
- * Removes an item from the end of a vector and gives that item.
- */
-#define vec_pop(v) ((v)->data[--(v)->sz])
-
-#endif
-
-// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/clientcon.c b/src/clientcon.c
index 29a4208..9673848 100644
--- a/src/clientcon.c
+++ b/src/clientcon.c
@@ -20,7 +20,7 @@
#include "feature.h"
#include "gamedata.h"
-FEATURE("")
+FEATURE()
REQUIRE(ent)
REQUIRE_GAMEDATA(vtidx_ClientPrintf)
REQUIRE_GLOBAL(engserver)
@@ -36,6 +36,6 @@ void clientcon_reply(const char *s) {
if (e) { clientcon_msg(e, s); return; }
}
-INIT { return true; }
+INIT { return FEAT_OK; }
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/con_.c b/src/con_.c
index 6fc8857..1f306b8 100644
--- a/src/con_.c
+++ b/src/con_.c
@@ -1,6 +1,6 @@
/* THIS FILE SHOULD BE CALLED `con.c` BUT WINDOWS IS STUPID */
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -69,8 +69,6 @@ static inline void initval(struct con_var *v) {
memcpy(v->strval, v->defaultval, v->strlen);
}
-#include <cmdinit.gen.h> // generated by build/codegen.c
-
// to try and match the engine even though it's probably not strictly required,
// we call the Internal* virtual functions via the actual vtable. since vtables
// are built dynamically (below), we store this index; other indices are just
@@ -303,8 +301,13 @@ struct _con_vtab_iconvar_wrap _con_vtab_iconvar_wrap = {
#endif
};
-void con_reg(void *cmd_or_var) {
- RegisterConCommand(_con_iface, cmd_or_var);
+void con_regvar(struct con_var *v) {
+ initval(v);
+ RegisterConCommand(_con_iface, v);
+}
+
+void con_regcmd(struct con_cmd *c) {
+ RegisterConCommand(_con_iface, c);
}
// XXX: these should use vcall/gamedata stuff as they're only used for the
@@ -384,8 +387,6 @@ void con_init(void) {
*pi++ = (void *)&IsFlagSet_thunk;
// last one: not in 004, but doesn't matter. one less branch!
*pi++ = (void *)&GetSplitScreenPlayerSlot;
-
- regcmds();
}
static void helpuserhelpus(int pluginver, char ifaceverchar) {
@@ -460,7 +461,6 @@ bool con_detect(int pluginver) {
void con_disconnect(void) {
UnregisterConCommands(_con_iface, dllid);
- freevars();
}
struct con_var *con_findvar(const char *name) {
diff --git a/src/con_.h b/src/con_.h
index 0e6efef..dc06335 100644
--- a/src/con_.h
+++ b/src/con_.h
@@ -1,6 +1,6 @@
/* THIS FILE SHOULD BE CALLED `con.h` BUT WINDOWS IS STUPID */
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -74,7 +74,7 @@ typedef void (*con_cmdcbv1)(void);
/*
* This is an autocompletion callback for suggesting arguments to a command.
- * XXX: Autocompletion isn't really totally figured out or implemented yet.
+ * TODO(autocomplete): Autocompletion isn't really implemented yet.
*/
typedef int (*con_complcb)(const char *part,
char cmds[CON_CMD_MAXCOMPLETE][CON_CMD_MAXCOMPLLEN]);
@@ -286,7 +286,8 @@ extern struct _con_vtab_iconvar_wrap {
/*
* Defines a console command with the handler function body immediately
- * following the macro (like in Source itself).
+ * following the macro (like in Source itself). The function takes the argument
+ * `struct con_cmdargs *cmd` for command arguments.
*/
#define DEF_CCMD_HERE(name, desc, flags) \
static void _cmdf_##name(const struct con_cmdargs *cmd); \
@@ -295,8 +296,35 @@ extern struct _con_vtab_iconvar_wrap {
/* { body here } */
/*
+ * These are exactly the same as the above macros, but instead of
+ * unconditionally registering things, they have the following conditions:
+ *
+ * - Variables are always registered, but get hidden if a feature fails to
+ * initialise.
+ * - If a feature specifies GAMESPECIFIC(), its cvars will remain unregistered
+ * unless the game matches.
+ * - Commands are only registered if the feature successfully initialises.
+ *
+ * In situations where exact control over initialisation is not required, these
+ * macros ought to make life a lot easier and are generally recommended.
+ *
+ * Obviously, these should only be used inside of a feature (see feature.h). The
+ * code generator will produce an error otherwise.
+ */
+#define DEF_FEAT_CVAR DEF_CVAR
+#define DEF_FEAT_CVAR_MIN DEF_CVAR_MIN
+#define DEF_FEAT_CVAR_MAX DEF_CVAR_MAX
+#define DEF_FEAT_CVAR_MINMAX DEF_CVAR_MINMAX
+#define DEF_FEAT_CCMD DEF_CCMD
+#define DEF_FEAT_CCMD_HERE DEF_CCMD_HERE
+#define DEF_FEAT_CCMD_PLUSMINUS DEF_CCMD_PLUSMINUS
+
+/*
* These are exactly the same as the above macros, but they don't cause the
- * commands or variables to be registered on plugin load.
+ * commands or variables to be registered on plugin load or feature
+ * initialisation. Registration must be done manually. These are generally not
+ * recommended but may be needed in specific cases such as conditionally
+ * reimplementing a built-in engine feature.
*/
#define DEF_CVAR_UNREG DEF_CVAR
#define DEF_CVAR_MIN_UNREG DEF_CVAR_MIN
@@ -307,10 +335,14 @@ extern struct _con_vtab_iconvar_wrap {
#define DEF_CCMD_PLUSMINUS_UNREG DEF_CCMD_PLUSMINUS
/*
- * Registers a command or variable defined with the _UNREG variants of the above
- * macros. Can be used to conditionally register things.
+ * These functions register a command or variable, respectively, defined with
+ * the _UNREG variants of the above macros. These can be used to conditionally
+ * register things. Wherever possible, it is advised to use the DEF_FEAT_*
+ * macros instead for conditional registration, as they handle the common cases
+ * automatically.
*/
-void con_reg(void *cmd_or_var);
+void con_regvar(struct con_var *v);
+void con_regcmd(struct con_cmd *c);
#endif
diff --git a/src/democustom.c b/src/democustom.c
index 30fc4ee..749415b 100644
--- a/src/democustom.c
+++ b/src/democustom.c
@@ -120,7 +120,8 @@ INIT {
if (buildnum >= 2042) nbits_datalen = 11; else nbits_datalen = 12;
//}
- return find_WriteMessages();
+ if (!find_WriteMessages()) return FEAT_INCOMPAT;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/demorec.c b/src/demorec.c
index 4a8efb5..66ffa1d 100644
--- a/src/demorec.c
+++ b/src/demorec.c
@@ -1,6 +1,6 @@
/*
* Copyright © 2021 Willian Henrique <wsimanbrazil@yahoo.com.br>
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -38,8 +38,8 @@
FEATURE("improved demo recording")
REQUIRE_GAMEDATA(vtidx_StopRecording)
-DEF_CVAR(sst_autorecord, "Continuously record demos even after reconnecting", 1,
- CON_ARCHIVE | CON_HIDDEN)
+DEF_FEAT_CVAR(sst_autorecord,
+ "Continuously record demos even after reconnecting", 1, CON_ARCHIVE)
void *demorecorder;
static int *demonum;
@@ -261,21 +261,21 @@ INIT {
orig_stop_cb = con_getcmdcb(cmd_stop);
if_cold (!find_demorecorder()) {
errmsg_errorx("couldn't find demo recorder instance");
- return false;
+ return FEAT_INCOMPAT;
}
void **vtable = mem_loadptr(demorecorder);
// XXX: 16 is totally arbitrary here! figure out proper bounds later
if_cold (!os_mprot(vtable, 16 * sizeof(void *), PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
- return false;
+ return FEAT_FAIL;
}
if_cold (!find_recmembers(vtable[vtidx_StopRecording])) {
errmsg_errorx("couldn't find recording state variables");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!find_demoname(vtable[vtidx_StartRecording])) {
errmsg_errorx("couldn't find demo basename variable");
- return false;
+ return FEAT_INCOMPAT;
}
orig_SetSignonState = (SetSignonState_func)hook_vtable(vtable,
@@ -286,8 +286,7 @@ INIT {
cmd_record->cb = &hook_record_cb;
cmd_stop->cb = &hook_stop_cb;
- sst_autorecord->base.flags &= ~CON_HIDDEN;
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/engineapi.c b/src/engineapi.c
index 5a78a92..62a4eb1 100644
--- a/src/engineapi.c
+++ b/src/engineapi.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -18,6 +18,7 @@
#include <stdlib.h> // used in generated code
#include <string.h> // "
+#include "abi.h" // for NVDTOR use in gamedata generated code
#include "con_.h"
#include "engineapi.h"
#include "gamedata.h"
@@ -30,7 +31,7 @@
#include "vcall.h"
#include "x86.h"
-u64 _gametype_tag = 0; // declared in gametype.h but seems sensible enough here
+u32 _gametype_tag = 0; // declared in gametype.h but seems sensible enough here
ifacefactory factory_client = 0, factory_server = 0, factory_engine = 0,
factory_inputsystem = 0;
@@ -49,6 +50,7 @@ struct CServerPlugin *pluginhandler;
DECL_VFUNC_DYN(void *, GetAllServerClasses)
#include <entpropsinit.gen.h> // generated by build/mkentprops.c
+#include <gamedatainit.gen.h> // generated by build/mkgamedata.c
bool engineapi_init(int pluginver) {
if_cold (!con_detect(pluginver)) return false;
@@ -110,8 +112,7 @@ bool engineapi_init(int pluginver) {
_gametype_tag |= _gametype_tag_TheLastStand;
}
}
-
- gamedata_init();
+ initgamedata();
con_init();
if_cold (!gameinfo_init()) { con_disconnect(); return false; }
return true;
diff --git a/src/ent.c b/src/ent.c
index 2eb8e51..7a0d8fe 100644
--- a/src/ent.c
+++ b/src/ent.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -127,7 +127,7 @@ static inline ctor_func findctor(const struct CEntityFactory *factory,
seencall = 0;
continue;
}
- return false;
+ return 0;
}
}
// duping NEXT_INSN macro here in the name of a nicer message
@@ -180,13 +180,12 @@ INIT {
// for PEntityOfEntIndex we don't really have to do any more init, we
// can just call the function later.
- if (has_vtidx_PEntityOfEntIndex) return true;
+ if (has_vtidx_PEntityOfEntIndex) return FEAT_OK;
if (globalvars && has_off_edicts) {
edicts = getptr_edicts(globalvars);
- return true;
+ return FEAT_OK;
}
- errmsg_warnx("not implemented for this engine");
- return false;
+ return FEAT_INCOMPAT;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/event.h b/src/event.h
index 86a443e..7e173d4 100644
--- a/src/event.h
+++ b/src/event.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -29,7 +29,7 @@
#define DECL_EVENT(evname, ...) typedef void _must_declare_event_##evname;
/*
- * Declares a predicate - a special type of even returning bool. Predicates are
+ * Declares a predicate - a special type of event returning bool. Predicates are
* used to determine whether some other action should be performed, and
* generally should not have side effects, since they get short-circuited and
* thus won't always fire when a check is being performed.
diff --git a/src/fastfwd.c b/src/fastfwd.c
index e287770..7378b04 100644
--- a/src/fastfwd.c
+++ b/src/fastfwd.c
@@ -1,6 +1,6 @@
/*
* Copyright © 2023 Matthew Wozniak <sirtomato999@gmail.com>
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -35,6 +35,10 @@
#include "x86util.h"
FEATURE()
+// currently only used for l4d quick reset stuff, and conflicts with SPT's
+// tas_pause hook. so, disable for non-L4D games for now, to be polite.
+// TODO(compat): come up with a real solution for this if/when required
+GAMESPECIFIC(L4Dbased)
REQUIRE_GAMEDATA(vtidx_RunFrame)
REQUIRE_GAMEDATA(vtidx_Frame)
REQUIRE_GAMEDATA(vtidx_GetRealTime)
@@ -174,79 +178,73 @@ static void *find_floatcall(void *func, int fldcnt, const char *name) {
}
NEXT_INSN(p, name);
}
- return false;
+ return 0;
}
next: NEXT_INSN(p, name);
}
return 0;
}
-PREINIT {
- // currently only used for l4d quick reset stuff, and conflicts with SPT's
- // tas_pause hook. so, disable for non-L4D games for now, to be polite.
- // TODO(compat): come up with a real solution for this if/when required
- return GAMETYPE_MATCHES(L4Dbased);
-}
-
INIT {
void *hldsapi = factory_engine("VENGINE_HLDS_API_VERSION002", 0);
if_cold (!hldsapi) {
errmsg_errorx("couldn't find HLDS API interface");
- return false;
+ return FEAT_INCOMPAT;
}
void *enginetool = factory_engine("VENGINETOOL003", 0);
if_cold (!enginetool) {
errmsg_errorx("missing engine tool interface");
- return false;
+ return FEAT_INCOMPAT;
}
// behold: the greatest pointer chase of all time
realtime = find_float((*(void ***)enginetool)[vtidx_GetRealTime]);
if_cold (!realtime) {
errmsg_errorx("couldn't find realtime variable");
- return false;
+ return FEAT_INCOMPAT;
}
host_frametime = find_float((*(void ***)enginetool)[vtidx_HostFrameTime]);
if_cold (!host_frametime) {
errmsg_errorx("couldn't find host_frametime variable");
- return false;
+ return FEAT_INCOMPAT;
}
void *eng = find_eng((*(void ***)hldsapi)[vtidx_RunFrame]);
if_cold (!eng) {
errmsg_errorx("couldn't find eng global object");
- return false;
+ return FEAT_INCOMPAT;
}
void *func;
if_cold (!(func = find_HostState_Frame((*(void ***)eng)[vtidx_Frame]))) {
errmsg_errorx("couldn't find HostState_Frame function");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!(func = find_FrameUpdate(func))) {
errmsg_errorx("couldn't find FrameUpdate function");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!(func = find_floatcall(func, GAMETYPE_MATCHES(L4D2_2147plus) ?
2 : 1, "CHostState::State_Run"))) {
errmsg_errorx("couldn't find State_Run function");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!(func = find_floatcall(func, 1, "Host_RunFrame"))) {
errmsg_errorx("couldn't find Host_RunFrame function");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!(func = find_floatcall(func, 1, "_Host_RunFrame"))) {
errmsg_errorx("couldn't find _Host_RunFrame");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!find_Host_AccumulateTime(func)) {
errmsg_errorx("couldn't find Host_AccumulateTime");
- return false;
+ return FEAT_INCOMPAT;
}
orig_Host_AccumulateTime = (Host_AccumulateTime_func)hook_inline(
(void *)orig_Host_AccumulateTime, (void *)hook_Host_AccumulateTime);
if_cold (!orig_Host_AccumulateTime) {
errmsg_errorsys("couldn't hook Host_AccumulateTime function");
+ return FEAT_FAIL;
}
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/feature.h b/src/feature.h
index e1e4688..81370b1 100644
--- a/src/feature.h
+++ b/src/feature.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -21,18 +21,42 @@
#define _FEATURE_CAT(a, b) _FEATURE_CAT1(a, b)
/*
- * Declares that this translation unit implements a "feature" - a unit of
- * plugin functionality.
+ * Declares that this translation unit implements a "feature" - a unit of plugin
+ * functionality.
*
- * desc specifies a string to be displayed to the user. Omit this to declare an
- * internal feature, which won't be advertised, but will be available to other
- * features.
+ * At build time, the code generator automatically creates the glue code
+ * required to make features load and unload in the correct order, subject to
+ * compatibility with each supported game and engine version. As such, there is
+ * no need to manually plug a new feature into SST's initialisation code as long
+ * as dependency information is properly declared (see the macros below).
+ *
+ * desc specifies a string to be displayed to the user in the console printout
+ * that gets displayed when the plugin finishes loading. Omit this to declare an
+ * internal feature, which won't be displayed to users but will still be
+ * available for other features to call into and build functionality on top of.
*/
#define FEATURE(... /*desc*/)
/*
- * Indicates that the specified feature is required for this feature to function.
- * If that feature fails to initialise, this feature will not be enabled.
+ * Declares that this feature should only be loaded for games matching the given
+ * gametype tag (see gametype.h). Console variables and commands created using
+ * DEF_FEAT_* macros will not be registered if SST is loaded by some other game.
+ *
+ * As an optimisation, REQUIRE_GAMEDATA() checks (see below) can also be elided
+ * in cases where gamedata is always present for this particular game. As such,
+ * it is wise to still specify gamedata dependencies correctly, so that the
+ * definitions can be changed in the data files without breaking code.
+ */
+#define GAMESPECIFIC(tag)
+
+/*
+ * Indicates that the specified feature is required for this feature to
+ * function. If that feature fails to initialise, this feature will not be
+ * enabled.
+ *
+ * By convention, the name of the feature is the name of its implementation
+ * source file, minus the .c extension. For instance, foo.c would be referred to
+ * by REQUIRE(foo).
*/
#define REQUIRE(feature)
@@ -40,9 +64,9 @@
* Indicates that the specified feature should be initialised before this one,
* but is not a hard requirement.
*
- * Presence of a feature can be tested for using has_<featurename>.
+ * Presence of a feature can in turn be tested for using has_<featname>.
*/
-#define REQUEST(featname) extern bool has_##featname;
+#define REQUEST(feature) extern bool has_##feature;
/*
* Indicates that the specified gamedata entry is required for this feature to
@@ -52,40 +76,85 @@
* effect on whether this feature is loaded. It can simply be tested for using
* has_<entryname>.
*/
-#define REQUIRE_GAMEDATA(feature)
+#define REQUIRE_GAMEDATA(entry)
/*
* Indicates that this feature requires a global variable (such as a factory or
* globally-exposed engine interface) to be non-null in order to function. If
* the variable has a null/zero value prior to feature initialisation, this
* feature will not be enabled.
+ *
+ * Note that this really only works for variables known to engineapi.c which is
+ * kind of a bad abstraction, but it's currently just necessary in practice.
+ *
+ * Correct usage of this macro will generally be very similar to other usages
+ * found elsewhere in the codebase already, so use those as a reference.
*/
#define REQUIRE_GLOBAL(varname)
+/* status values for INIT and PREINIT below */
+enum {
+ FEAT_SKIP = -1, /* feature isn't useful here, pretend it doesn't exist */
+ FEAT_OK, /* feature successfully initialised/enabled */
+ FEAT_FAIL, /* error in starting up feature */
+ FEAT_INCOMPAT, /* feature is incompatible with this game/engine version */
+ _FEAT_INTERNAL_STATUSES // internal detail, do not use
+};
+
/*
- * Defines the special feature init function which is unique to this translation
- * unit. This should return true to indicate success, or false to indicate
- * failure. Features which start to load will cause dependent features not to be
- * started.
+ * Defines the special feature initialisation function which is unique to this
+ * translation unit. All features are required to specify this function.
+ *
+ * The defined function must return FEAT_OK on success, FEAT_FAIL on failure due
+ * to some transient error, FEAT_INCOMPAT to indicate incompatibility with the
+ * current game/engine version, or FEAT_SKIP to indicate that the feature is
+ * useless or unnecessary.
+ *
+ * If a value other than FEAT_OK is returned, END (see below) will not be called
+ * later and other features that depend on this feature will be disabled. If
+ * this feature provides other functions as API, they can be assumed not to get
+ * called unless initialisation is successful.
+ *
+ * For features with a description (see FEATURE() above), all return values with
+ * the exception of FEAT_SKIP will cause a corresponding status message to be
+ * displayed in the listing after the plugin finishes loading, while FEAT_SKIP
+ * will simply hide the feature from the listing. Features with no description
+ * will not be displayed anyway.
*
- * Features are required to specify this function.
+ * Features which either fail to initialise or elect to skip loading will cause
+ * dependent features not to be enabled.
*/
-#define INIT bool _FEATURE_CAT(_feature_init_, MODULE_NAME)(void) // { code... }
+#define INIT int _FEATURE_CAT(_feat_init_, MODULE_NAME)(void) // { code... }
/*
* Defines the special, optional feature shutdown function which is unique to
* this translation unit. This does not return a value, and may be either
* specified once, or left out if no cleanup is required for this feature.
*/
-#define END void _FEATURE_CAT(_feature_end_, MODULE_NAME)(void) // { code... }
+#define END void _FEATURE_CAT(_feat_end_, MODULE_NAME)(void) // { code... }
/*
- * Defines a conditional check to run prior to checking other requirements for
- * this feature. This can be used to match a certain game type or conditionally
- * register console variables, and should return true or false to indicate
- * whether the feature should continue to initialise.
+ * Defines a special feature pre-init function which performs early feature
+ * initialisation, the moment the plugin is loaded. If the plugin is autoloaded
+ * via VDF, this will be called long before the deferred initialisation that
+ * usually happens after the client and VGUI have spun up. Since most of the
+ * rest of SST is also deferred, care must be taken not to call anything else
+ * that is not yet initialised.
+ *
+ * When in doubt, do not use this; it exists only to serve a couple of fringe
+ * cases.
+ *
+ * Like INIT above, the function created by this macro is expected to return one
+ * of FEAT_OK, FEAT_FAIL, FEAT_INCOMPAT, or FEAT_SKIP. If a value other than
+ * FEAT_OK is returned, the INIT block won't be run afterwards.
+ *
+ * Features that use this macro are currently disallowed from using REQUIRE()
+ * and REQUEST(), as well as GAMESPECIFIC(), because it's not clear how the
+ * semantics of doing so should work. It is still possible to use
+ * REQUIRE_GAMEDATA and REQUIRE_GLOBAL, however these only apply to the INIT
+ * block, *NOT* the PREINIT.
*/
-#define PREINIT bool _FEATURE_CAT(_feature_preinit_, MODULE_NAME)(void) // {...}
+#define PREINIT int _FEATURE_CAT(_feat_preinit_, MODULE_NAME)(void) // {...}
#endif
diff --git a/src/fixes.c b/src/fixes.c
index 84b3482..dbb60f5 100644
--- a/src/fixes.c
+++ b/src/fixes.c
@@ -148,7 +148,7 @@ static void l4d2specific(void) {
if (ident.VendorId == 0x8086) con_setvari(v, 1); // neat vendor id, btw!
}
IDirect3D9_Release(d3d9);
-e:;
+e:
#endif
// There's a rare, inexplicable issue where the game will drop to an
diff --git a/src/fov.c b/src/fov.c
index f4b8575..2426d6e 100644
--- a/src/fov.c
+++ b/src/fov.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2022 Willian Henrique <wsimanbrazil@yahoo.com.br>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -35,6 +35,9 @@
#include "x86util.h"
FEATURE("extended FOV range")
+// could work for other games, but generally only portal 1 people want this (the
+// rest of us consider this cheating and a problem for runs...)
+GAMESPECIFIC(Portal1)
REQUEST(ent)
DEF_CVAR_MINMAX_UNREG(fov_desired,
@@ -80,34 +83,28 @@ HANDLE_EVENT(ClientActive, struct edict *e) {
static struct con_cmd *cmd_fov;
-PREINIT {
- // could work for other games, but generally only portal 1 people want this
- // (the rest of us consider this cheating and a problem for runs...)
- return GAMETYPE_MATCHES(Portal1);
-}
-
INIT {
cmd_fov = con_findcmd("fov");
- if_cold (!cmd_fov) return false; // shouldn't happen but just in case!
+ if_cold (!cmd_fov) return FEAT_INCOMPAT; // shouldn't happen, but who knows!
if (real_fov_desired = con_findvar("fov_desired")) {
// latest steampipe already goes up to 120 fov
- if (real_fov_desired->parent->maxval == 120) return false;
+ if (real_fov_desired->parent->maxval == 120) return FEAT_SKIP;
real_fov_desired->parent->maxval = 120;
}
else {
- if (!has_ent) return false;
- con_reg(fov_desired);
+ if (!has_ent) return FEAT_INCOMPAT;
+ con_regvar(fov_desired);
real_fov_desired = fov_desired;
}
if_cold (!find_SetDefaultFOV(cmd_fov)) {
errmsg_errorx("couldn't find SetDefaultFOV function");
- return false;
+ return FEAT_INCOMPAT;
}
orig_SetDefaultFOV = (SetDefaultFOV_func)hook_inline(
(void *)orig_SetDefaultFOV, (void *)&hook_SetDefaultFOV);
if_cold (!orig_SetDefaultFOV) {
errmsg_errorsys("couldn't hook SetDefaultFOV function");
- return false;
+ return FEAT_FAIL;
}
// we might not be using our cvar but simpler to do this unconditionally
@@ -115,7 +112,7 @@ INIT {
fov_desired->parent->base.flags &= ~CON_HIDDEN;
// hide the original fov command since we've effectively broken it anyway :)
cmd_fov->base.flags |= CON_DEVONLY;
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/gamedata.c b/src/gamedata.c
deleted file mode 100644
index 247bf25..0000000
--- a/src/gamedata.c
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright © 2024 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 "abi.h" // purely for NVDTOR
-#include "gametype.h"
-
-#include <gamedatainit.gen.h> // generated by build/mkgamedata.c
-
-// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/gamedata.h b/src/gamedata.h
index 050d3b8..9f4932a 100644
--- a/src/gamedata.h
+++ b/src/gamedata.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -32,9 +32,6 @@
#undef NVDTOR
#endif
-/* Called as part of plugin init to set up various metadata about the game. */
-void gamedata_init(void);
-
#endif
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/gameserver.c b/src/gameserver.c
index 9e046ee..c2df33f 100644
--- a/src/gameserver.c
+++ b/src/gameserver.c
@@ -68,10 +68,10 @@ static bool find_sv(con_cmdcb pause_cb) {
INIT {
struct con_cmd *pause = con_findcmd("pause");
if_cold (!find_sv(pause->cb)) {
- errmsg_errorx("couldn't find game server object\n");
- return false;
+ errmsg_errorx("couldn't find game server object");
+ return FEAT_INCOMPAT;
}
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/gametype.h b/src/gametype.h
index 35a43be..a6d6b0c 100644
--- a/src/gametype.h
+++ b/src/gametype.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -20,7 +20,7 @@
#include "intdefs.h"
-extern u64 _gametype_tag;
+extern u32 _gametype_tag;
/* general engine branches used in a bunch of stuff */
#define _gametype_tag_OE 1
@@ -58,7 +58,7 @@ extern u64 _gametype_tag;
/* Matches for any multiple possible tags */
#define _gametype_tag_L4D (_gametype_tag_L4D1 | _gametype_tag_L4D2)
-// XXX: *stupid* naming, refactor later (damn Survivors ruining everything)
+// XXX: *stupid* naming, refactor one day (damn Survivors ruining everything)
#define _gametype_tag_L4D2x (_gametype_tag_L4D2 | _gametype_tag_L4DS)
#define _gametype_tag_L4Dx (_gametype_tag_L4D1 | _gametype_tag_L4D2x)
#define _gametype_tag_L4Dbased (_gametype_tag_L4Dx | _gametype_tag_Portal2)
diff --git a/src/hud.c b/src/hud.c
index 2334bb8..1e3c544 100644
--- a/src/hud.c
+++ b/src/hud.c
@@ -169,29 +169,29 @@ INIT {
matsurf = factory_engine("MatSystemSurface006", 0);
if_cold (!matsurf) {
errmsg_errorx("couldn't get MatSystemSurface006 interface");
- return false;
+ return FEAT_INCOMPAT;
}
void *schememgr = factory_engine("VGUI_Scheme010", 0);
if_cold (!schememgr) {
errmsg_errorx("couldn't get VGUI_Scheme010 interface");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!find_toolspanel(vgui)) {
errmsg_errorx("couldn't find engine tools panel");
- return false;
+ return FEAT_INCOMPAT;
}
void **vtable = *(void ***)toolspanel;
if_cold (!os_mprot(vtable + vtidx_Paint, sizeof(void *),
PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
- return false;
+ return FEAT_FAIL;
}
orig_Paint = (Paint_func)hook_vtable(vtable, vtidx_Paint,
(void *)&hook_Paint);
SetPaintEnabled(toolspanel, true);
// 1 is the default, first loaded scheme. should always be sourcescheme.res
scheme = GetIScheme(schememgr, (struct handlewrap){1});
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/inputhud.c b/src/inputhud.c
index af3bb59..da96d5e 100644
--- a/src/inputhud.c
+++ b/src/inputhud.c
@@ -1,7 +1,7 @@
/*
* Copyright © 2022 Matthew Wozniak <sirtomato999@gmail.com>
* Copyright © 2022 Willian Henrique <wsimanbrazil@yahoo.com.br>
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com
+ * Copyright © 2025 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
@@ -43,23 +43,23 @@ REQUIRE_GAMEDATA(vtidx_VClient_DecodeUserCmdFromBuffer)
REQUIRE_GLOBAL(factory_client)
REQUIRE(hud)
-DEF_CVAR(sst_inputhud, "Enable button input HUD", 0, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR(sst_inputhud_bgcolour_normal,
+DEF_FEAT_CVAR(sst_inputhud, "Enable button input HUD", 0, CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_inputhud_bgcolour_normal,
"Input HUD default key background colour (RGBA hex)", "4040408C",
- CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR(sst_inputhud_bgcolour_pressed,
+ CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_inputhud_bgcolour_pressed,
"Input HUD pressed key background colour (RGBA hex)", "202020C8",
- CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR(sst_inputhud_fgcolour, "Input HUD text colour (RGBA hex)", "F0F0F0FF",
- CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_MINMAX(sst_inputhud_scale, "Input HUD size (multiple of minimum)",
- 1.5, 1, 4, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_MINMAX(sst_inputhud_x,
+ CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_inputhud_fgcolour, "Input HUD text colour (RGBA hex)",
+ "F0F0F0FF", CON_ARCHIVE)
+DEF_FEAT_CVAR_MINMAX(sst_inputhud_scale, "Input HUD size (multiple of minimum)",
+ 1.5, 1, 4, CON_ARCHIVE)
+DEF_FEAT_CVAR_MINMAX(sst_inputhud_x,
"Input HUD x position (fraction between screen left and right)",
- 0.02, 0, 1, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_MINMAX(sst_inputhud_y,
+ 0.02, 0, 1, CON_ARCHIVE)
+DEF_FEAT_CVAR_MINMAX(sst_inputhud_y,
"Input HUD y position (fraction between screen top and bottom)",
- 0.95, 0, 1, CON_ARCHIVE | CON_HIDDEN)
+ 0.95, 0, 1, CON_ARCHIVE)
static void *input;
static int heldbuttons = 0, tappedbuttons = 0;
@@ -379,11 +379,11 @@ INIT {
!(vclient = factory_client("VClient016", 0)) &&
!(vclient = factory_client("VClient017", 0))) {
errmsg_errorx("couldn't get client interface");
- return false;
+ return FEAT_INCOMPAT;
}
if (!find_input(vclient)) {
errmsg_errorx("couldn't find input global");
- return false;
+ return FEAT_INCOMPAT;
}
for (int i = 0; i < countof(fontnames); ++i) {
if (fonts[i].h = hud_getfont(fontnames[i], true)) {
@@ -396,7 +396,7 @@ INIT {
// just unprotect the first few pointers (GetUserCmd is 8)
if_cold (!os_mprot(vtable, sizeof(void *) * 8, PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
- return false;
+ return FEAT_FAIL;
}
if (GAMETYPE_MATCHES(L4Dbased)) {
orig_CreateMove = (CreateMove_func)hook_vtable(vtable, vtidx_CreateMove,
@@ -418,31 +418,25 @@ INIT {
else if (GAMETYPE_MATCHES(L4D)) layout = &layout_l4d;
// TODO(compat): more game-specific layouts!
- sst_inputhud->base.flags &= ~CON_HIDDEN;
- sst_inputhud_scale->base.flags &= ~CON_HIDDEN;
- sst_inputhud_bgcolour_normal->base.flags &= ~CON_HIDDEN;
sst_inputhud_bgcolour_normal->cb = &colourcb;
- sst_inputhud_bgcolour_pressed->base.flags &= ~CON_HIDDEN;
sst_inputhud_bgcolour_pressed->cb = &colourcb;
- sst_inputhud_fgcolour->base.flags &= ~CON_HIDDEN;
sst_inputhud_fgcolour->cb = &colourcb;
- sst_inputhud_x->base.flags &= ~CON_HIDDEN;
- sst_inputhud_y->base.flags &= ~CON_HIDDEN;
- // HACK: default HUD position would clash with L4D player health HUDs and
- // HL2 sprint HUD, so move it up. this currently has to be done in a super
- // crappy, nasty way to get the defaults to display right in the console...
- // TODO(opt): move PREINIT stuff to before cvar init and avoid this nonsense
+ // Default HUD position would clash with L4D player health HUDs and
+ // HL2 sprint HUD, so move it up. This is a bit yucky, but at least we don't
+ // have to go through all the virtual setter crap twice...
if (GAMETYPE_MATCHES(L4D)) {
sst_inputhud_y->defaultval = "0.82";
- con_setvarstr(sst_inputhud_y, "0.82");
+ sst_inputhud_y->fval = 0.82f;
+ sst_inputhud_y->ival = 0;
}
else if (GAMETYPE_MATCHES(HL2series)) {
sst_inputhud_y->defaultval = "0.75";
- con_setvarstr(sst_inputhud_y, "0.75");
+ sst_inputhud_y->fval = 0.75f;
+ sst_inputhud_y->ival = 0;
}
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/kvsys.c b/src/kvsys.c
index 31652f3..bc9230e 100644
--- a/src/kvsys.c
+++ b/src/kvsys.c
@@ -113,7 +113,7 @@ INIT {
(void *)hook_GetStringForSymbol);
}
}
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/l4dmm.c b/src/l4dmm.c
index 05a03a7..40e6fd5 100644
--- a/src/l4dmm.c
+++ b/src/l4dmm.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -28,6 +28,7 @@
#include "vcall.h"
FEATURE()
+GAMESPECIFIC(L4D)
REQUIRE(kvsys)
REQUIRE_GAMEDATA(vtidx_GetMatchNetworkMsgController)
REQUIRE_GAMEDATA(vtidx_GetActiveGameServerDetails)
@@ -125,12 +126,12 @@ INIT {
ifacefactory factory = (ifacefactory)os_dlsym(mmlib, "CreateInterface");
if_cold (!factory) {
errmsg_errordl("couldn't get matchmaking interface factory");
- return false;
+ return FEAT_INCOMPAT;
}
matchfwk = factory("MATCHFRAMEWORK_001", 0);
if_cold (!matchfwk) {
errmsg_errorx("couldn't get IMatchFramework interface");
- return false;
+ return FEAT_INCOMPAT;
}
sym_game = kvsys_strtosym("game");
sym_campaign = kvsys_strtosym("campaign");
@@ -141,11 +142,11 @@ INIT {
oldmmiface = factory_engine("VENGINE_MATCHMAKING_VERSION001", 0);
if_cold (!oldmmiface) {
errmsg_errorx("couldn't get IMatchmaking interface");
- return false;
+ return FEAT_INCOMPAT;
}
#endif
}
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/l4dreset.c b/src/l4dreset.c
index 9e72f04..8333839 100644
--- a/src/l4dreset.c
+++ b/src/l4dreset.c
@@ -1,6 +1,6 @@
/*
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2024 Hayden K <imaciidz@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -45,6 +45,7 @@
#endif
FEATURE("Left 4 Dead quick resetting")
+GAMESPECIFIC(L4D)
REQUIRE(ent)
REQUIRE(fastfwd)
REQUIRE(gameserver)
@@ -196,11 +197,11 @@ static short ffdelay = 0;
static float ffadj = 0;
static int nextmapnum = 0;
-DEF_CVAR_MINMAX_UNREG(sst_l4d_quickreset_peektime,
+DEF_FEAT_CVAR_MINMAX(sst_l4d_quickreset_peektime,
"Number of seconds to show each relevant item spot during fast-forward",
- 1.5, 0, 3, CON_ARCHIVE | CON_HIDDEN)
+ 1.5, 0, 3, CON_ARCHIVE)
-DEF_CCMD_HERE_UNREG(sst_l4d_quickreset_continue,
+DEF_FEAT_CCMD_HERE(sst_l4d_quickreset_continue,
"Get to the end of the current cutscene without further slowdowns", 0) {
if (!ffdelay) {
con_warn("not currently fast-forwarding a cutscene\n");
@@ -340,11 +341,10 @@ static int getffidx(const char *campaign) {
return -1; // if unknown, just don't fast-forward, I guess.
}
-DEF_CVAR_UNREG(sst_l4d_quickreset_fastfwd,
- "Fast-forward through cutscenes when quick-resetting", 1,
- CON_ARCHIVE | CON_HIDDEN)
+DEF_FEAT_CVAR(sst_l4d_quickreset_fastfwd,
+ "Fast-forward through cutscenes when quick-resetting", 1, CON_ARCHIVE)
-DEF_CCMD_HERE_UNREG(sst_l4d_quickreset,
+DEF_FEAT_CCMD_HERE(sst_l4d_quickreset,
"Reset (or switch) campaign and clear all vote cooldowns", 0) {
if (cmd->argc > 2) {
con_warn("usage: sst_l4d_quickreset [campaignid]\n");
@@ -376,13 +376,6 @@ DEF_CCMD_HERE_UNREG(sst_l4d_quickreset,
}
}
-PREINIT {
- if (!GAMETYPE_MATCHES(L4D)) return false;
- con_reg(sst_l4d_quickreset_fastfwd);
- con_reg(sst_l4d_quickreset_peektime);
- return true;
-}
-
// Note: this returns a pointer to subsequent bytes for find_voteissues() below
static inline const uchar *find_votecontroller(con_cmdcbv1 listissues_cb) {
const uchar *insns = (const uchar *)listissues_cb;
@@ -490,7 +483,7 @@ static inline bool find_UnfreezeTeam(void *GameFrame) { // note: L4D1 only
NEXT_INSN(p, "Director::Update call");
}
return false;
-ok: // Director::Update calls UnfreezeTeam after the first jmp instruction
+ok: // Director::Update calls UnfreezeTeam after the first jmp instruction
while (p - insns < 96) {
// jz XXX; mov ecx, <reg>; call Director::UnfreezeTeam
if (p[0] == X86_JZ && p[2] == X86_MOVRMW && (p[3] & 0xF8) == 0xC8 &&
@@ -508,28 +501,28 @@ INIT {
struct con_cmd *cmd_listissues = con_findcmd("listissues");
if_cold (!cmd_listissues) {
errmsg_errorx("couldn't find \"listissues\" command");
- return false;
+ return FEAT_INCOMPAT;
}
con_cmdcbv1 listissues_cb = con_getcmdcbv1(cmd_listissues);
const uchar *nextinsns = find_votecontroller(listissues_cb);
if_cold (!nextinsns) {
errmsg_errorx("couldn't find vote controller variable");
- return false;
+ return FEAT_INCOMPAT;
}
if_cold (!find_voteissues(nextinsns)) {
errmsg_errorx("couldn't find vote issues list offset\n");
- return false;
+ return FEAT_INCOMPAT;
}
void **vtable;
#ifdef _WIN32
void *GameShutdown = (*(void ***)srvdll)[vtidx_GameShutdown];
if_cold (!find_TheDirector(GameShutdown)) {
errmsg_errorx("couldn't find TheDirector variable");
- return false;
+ return FEAT_INCOMPAT;
}
#else
#warning TODO(linux): should be able to just dlsym(server, "TheDirector")
- return false;
+ return FEAT_INCOMPAT;
#endif
#ifdef _WIN32 // L4D1 has no Linux build, no need to check whether L4D2
if (GAMETYPE_MATCHES(L4D2)) {
@@ -538,7 +531,7 @@ INIT {
if_cold (!os_mprot(vtable + vtidx_OnGameplayStart, sizeof(*vtable),
PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
- return false;
+ return FEAT_FAIL;
}
orig_OnGameplayStart = (OnGameplayStart_func)hook_vtable(vtable,
vtidx_OnGameplayStart, (void *)&hook_OnGameplayStart);
@@ -548,7 +541,7 @@ INIT {
void *GameFrame = (*(void ***)srvdll)[vtidx_GameFrame];
if_cold (!find_UnfreezeTeam(GameFrame)) {
errmsg_errorx("couldn't find UnfreezeTeam function");
- return false;
+ return FEAT_INCOMPAT;
}
orig_UnfreezeTeam = (UnfreezeTeam_func)hook_inline(
(void *)orig_UnfreezeTeam, (void *)&hook_UnfreezeTeam);
@@ -573,11 +566,7 @@ INIT {
nocd: errmsg_note("resetting a first map will not clear vote cooldowns");
}
}
- con_reg(sst_l4d_quickreset);
- con_reg(sst_l4d_quickreset_continue);
- sst_l4d_quickreset_fastfwd->base.flags &= ~CON_HIDDEN;
- sst_l4d_quickreset_peektime->base.flags &= ~CON_HIDDEN;
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/l4dwarp.c b/src/l4dwarp.c
index b9e019d..eb638e4 100644
--- a/src/l4dwarp.c
+++ b/src/l4dwarp.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 Michael Smith <mikesmiffy128@gmail.com>
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
*
* Permission to use, copy, modify, and/or distribute this software for any
@@ -36,12 +36,15 @@
#include "x86util.h"
FEATURE("Left 4 Dead warp testing")
+GAMESPECIFIC(L4D)
REQUIRE(clientcon)
REQUIRE(ent)
REQUIRE(trace)
REQUIRE_GAMEDATA(off_entpos)
REQUIRE_GAMEDATA(off_eyeang)
REQUIRE_GAMEDATA(off_teamnum)
+REQUIRE_GAMEDATA(vtidx_AddBoxOverlay2)
+REQUIRE_GAMEDATA(vtidx_AddLineOverlay)
REQUIRE_GAMEDATA(vtidx_Teleport)
DECL_VFUNC_DYN(void, Teleport, const struct vec3f */*pos*/,
@@ -99,7 +102,7 @@ static struct vec3f warptarget(void *ent) {
};
}
-DEF_CCMD_HERE_UNREG(sst_l4d_testwarp, "Simulate a bot warping to you "
+DEF_FEAT_CCMD_HERE(sst_l4d_testwarp, "Simulate a bot warping to you "
"(specify \"staystuck\" to skip take-control simulation)",
CON_SERVERSIDE | CON_CHEAT) {
bool staystuck = false;
@@ -167,9 +170,9 @@ static bool draw_testpos(struct vec3f start, struct vec3f testpos,
return false;
}
+// note: UNREG because testwarp can still work without this
DEF_CCMD_HERE_UNREG(sst_l4d_previewwarp, "Visualise bot warp unstuck logic "
- "(use clear_debug_overlays to remove)",
- CON_SERVERSIDE | CON_CHEAT) {
+ "(use clear_debug_overlays to remove)", CON_SERVERSIDE | CON_CHEAT) {
struct edict *ed = ent_getedict(con_cmdclient + 1);
if_cold (!ed || !ed->ent_unknown) {
errmsg_errorx("couldn't access player entity");
@@ -306,26 +309,16 @@ static bool init_filter(void) {
return false;
}
-PREINIT {
- return GAMETYPE_MATCHES(L4D);
-}
-
INIT {
struct con_cmd *z_add = con_findcmd("z_add");
if (!z_add || !find_EntityPlacementTest(z_add->cb)) {
errmsg_errorx("couldn't find EntityPlacementTest function");
- return false;
+ return FEAT_INCOMPAT;
}
if (!init_filter()) {
- errmsg_errorx("couldn't init trace filter for EntityPlacementTest");
- return false;
+ errmsg_errorx("couldn't find trace filter ctor for EntityPlacementTest");
+ return FEAT_INCOMPAT;
}
- con_reg(sst_l4d_testwarp);
- // NOTE: assuming has_vtidx_AddLineOverlay && has_vtidx_AddBoxOverlay2
- // since those are specified for L4D.
- // TODO(opt): add some zero-cost/compile-time way to make sure gamedata
- // exists in a game-specific scenario? (probably requires declarative
- // game-specific features in codegen, which hasn't been high-priority)
if_cold (!has_off_collision) {
errmsg_warnx("missing m_Collision gamedata - warp preview unavailable");
}
@@ -334,9 +327,9 @@ INIT {
"warp preview unavailable");
}
else {
- con_reg(sst_l4d_previewwarp);
+ con_regcmd(sst_l4d_previewwarp);
}
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/langext.h b/src/langext.h
index ef0f18d..8d3ee6e 100644
--- a/src/langext.h
+++ b/src/langext.h
@@ -8,6 +8,8 @@
#define ssizeof(x) ((ssize)sizeof(x))
#define countof(x) (ssizeof(x) / ssizeof(*x))
+#undef unreachable // C23 stddef.h; prefer the non-function-like look of ours.
+
#if defined(__GNUC__) || defined(__clang__)
#define if_hot(x) if (__builtin_expect(!!(x), 1))
#define if_cold(x) if (__builtin_expect(!!(x), 0))
@@ -44,6 +46,7 @@
#define switch_exhaust_enum(E, x) switch_exhaust ((enum E)(x))
#endif
+// could do [[noreturn]] in future, _Noreturn probably supports more compilers.
#define noreturn _Noreturn void
#ifdef _WIN32
diff --git a/src/nomute.c b/src/nomute.c
index 4e5bcc4..a72f067 100644
--- a/src/nomute.c
+++ b/src/nomute.c
@@ -1,6 +1,6 @@
/*
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -58,9 +58,9 @@ static long __stdcall hook_CreateSoundBuffer(IDirectSound *this,
}
PREINIT {
- if (con_findvar("snd_mute_losefocus")) return false;
- con_reg(snd_mute_losefocus);
- return true;
+ if (con_findvar("snd_mute_losefocus")) return FEAT_SKIP;
+ con_regvar(snd_mute_losefocus);
+ return FEAT_OK;
}
INIT {
@@ -69,14 +69,14 @@ INIT {
if_cold (DirectSoundCreate(0, &ds_obj, 0) != DS_OK) {
// XXX: can this error be usefully stringified?
errmsg_errorx("couldn't create IDirectSound instance");
- return false;
+ return FEAT_OK;
}
ds_vt = ds_obj->lpVtbl;
ds_obj->lpVtbl->Release(ds_obj);
if_cold (!os_mprot(&ds_vt->CreateSoundBuffer, sizeof(void *),
PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
- return false;
+ return FEAT_OK;
}
orig_CreateSoundBuffer = ds_vt->CreateSoundBuffer;
ds_vt->CreateSoundBuffer = &hook_CreateSoundBuffer;
@@ -93,7 +93,7 @@ INIT {
"audio settings or restarting the game with SST autoloaded in "
"order to have an effect");
}
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/nosleep.c b/src/nosleep.c
index 52c88a9..170b4d6 100644
--- a/src/nosleep.c
+++ b/src/nosleep.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -43,7 +43,7 @@ static void VCALLCONV hook_SleepUntilInput(void *this, int timeout) {
PREINIT {
if (con_findvar("engine_no_focus_sleep")) return false;
- con_reg(engine_no_focus_sleep);
+ con_regvar(engine_no_focus_sleep);
return true;
}
@@ -52,12 +52,12 @@ INIT {
if_cold (!os_mprot(vtable + vtidx_SleepUntilInput, sizeof(void *),
PAGE_READWRITE)) {
errmsg_errorx("couldn't make virtual table writable");
- return false;
+ return FEAT_FAIL;
}
orig_SleepUntilInput = (SleepUntilInput_func)hook_vtable(vtable,
vtidx_SleepUntilInput, (void *)&hook_SleepUntilInput);
engine_no_focus_sleep->base.flags &= ~CON_HIDDEN;
- return true;
+ return FEAT_OK;
}
END {
diff --git a/src/os.h b/src/os.h
index cbecf2c..ca376b7 100644
--- a/src/os.h
+++ b/src/os.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -17,6 +17,10 @@
#ifndef INC_OS_H
#define INC_OS_H
+#ifdef INC_LANGEXT_H // we can't rely on include order!
+#undef noreturn // ugh, this breaks Windows headers. how annoying. temp-undef it
+#endif
+
#include <string.h>
#include <sys/stat.h> // XXX: try abstracting stat() and avoiding ucrt dep here
@@ -239,6 +243,10 @@ bool os_mprot(void *addr, int len, int mode);
*/
void os_randombytes(void *buf, int sz);
+#ifdef INC_LANGEXT_H
+#define noreturn _Noreturn void // HACK: put this back if undef'd above
+#endif
+
#endif
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/portalcolours.c b/src/portalcolours.c
index 61d9cc7..89d1d17 100644
--- a/src/portalcolours.c
+++ b/src/portalcolours.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -32,16 +32,17 @@
#include "vcall.h"
FEATURE("portal gun colour customisation")
+GAMESPECIFIC(Portal1)
REQUIRE_GLOBAL(clientlib)
// It's like the thing Portal Tools does, but at runtime!
-DEF_CVAR_UNREG(sst_portal_colour0, "Crosshair colour for gravity beam (RGB hex)",
- "F2CAA7", CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_UNREG(sst_portal_colour1, "Crosshair colour for left portal (RGB hex)",
- "40A0FF", CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_UNREG(sst_portal_colour2, "Crosshair colour for right portal (RGB hex)",
- "FFA020", CON_ARCHIVE | CON_HIDDEN)
+DEF_FEAT_CVAR(sst_portal_colour0, "Crosshair colour for gravity beam (RGB hex)",
+ "F2CAA7", CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_portal_colour1, "Crosshair colour for left portal (RGB hex)",
+ "40A0FF", CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_portal_colour2, "Crosshair colour for right portal (RGB hex)",
+ "FFA020", CON_ARCHIVE)
static struct rgba colours[3] = {
{242, 202, 167, 255}, {64, 160, 255, 255}, {255, 160, 32, 255}};
@@ -93,36 +94,25 @@ static bool find_UTIL_Portal_Color(void *base) {
return false;
}
-PREINIT {
- if (!GAMETYPE_MATCHES(Portal1)) return false;
- con_reg(sst_portal_colour0);
- con_reg(sst_portal_colour1);
- con_reg(sst_portal_colour2);
- return true;
-}
-
INIT {
#ifdef _WIN32
if_cold (!find_UTIL_Portal_Color(clientlib)) {
errmsg_errorx("couldn't find UTIL_Portal_Color");
- return false;
+ return FEAT_INCOMPAT;
}
orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)hook_inline(
(void *)orig_UTIL_Portal_Color, (void *)&hook_UTIL_Portal_Color);
if_cold (!orig_UTIL_Portal_Color) {
errmsg_errorsys("couldn't hook UTIL_Portal_Color");
- return false;
+ return FEAT_INCOMPAT;
}
- sst_portal_colour0->base.flags &= ~CON_HIDDEN;
sst_portal_colour0->cb = &colourcb;
- sst_portal_colour1->base.flags &= ~CON_HIDDEN;
sst_portal_colour1->cb = &colourcb;
- sst_portal_colour2->base.flags &= ~CON_HIDDEN;
sst_portal_colour2->cb = &colourcb;
- return true;
+ return FEAT_OK;
#else
#warning TODO(linux): yet more stuff!
- return false;
+ return FEAT_INCOMPAT;
#endif
}
diff --git a/src/rinput.c b/src/rinput.c
index 1187e15..6d568f3 100644
--- a/src/rinput.c
+++ b/src/rinput.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -68,7 +68,7 @@ DEF_CVAR_MINMAX(sst_mouse_factor, "Number of hardware mouse counts per step",
static ssize __stdcall inproc(void *wnd, uint msg, usize wp, ssize lp) {
switch (msg) {
- case WM_INPUT:;
+ case WM_INPUT:
char buf[ssizeof(RAWINPUTHEADER) + ssizeof(RAWMOUSE) /* = 40 */];
uint sz = sizeof(buf);
if_hot (GetRawInputData((void *)lp, RID_INPUT, buf, &sz,
@@ -133,7 +133,7 @@ INIT {
if_cold (!os_mprot(vtable_insys + vtidx_GetRawMouseAccumulators,
ssizeof(void *), PAGE_READWRITE)) {
errmsg_errorx("couldn't make virtual table writable");
- return false;
+ return FEAT_FAIL;
}
orig_GetRawMouseAccumulators = (GetRawMouseAccumulators_func)hook_vtable(
vtable_insys, vtidx_GetRawMouseAccumulators,
@@ -141,7 +141,7 @@ INIT {
}
else {
// create cvar hidden so config is still preserved if we fail to init
- con_reg(m_rawinput);
+ con_regvar(m_rawinput);
}
WNDCLASSEXW wc = {
.cbSize = sizeof(wc),
@@ -172,7 +172,7 @@ INIT {
con_colourmsg(&blue, ", you can scale down the sensor input with ");
con_colourmsg(&gold, "sst_mouse_factor");
con_colourmsg(&blue, "!\n");
- return false;
+ return FEAT_INCOMPAT;
}
if (has_rawinput) {
// no real reason to keep this around receiving useless window messages
@@ -209,13 +209,13 @@ INIT {
ok: m_rawinput->base.flags &= ~CON_HIDDEN;
sst_mouse_factor->base.flags &= ~CON_HIDDEN;
- return true;
+ return FEAT_OK;
e3: DestroyWindow(inwin);
e2: unhook_inline((void *)orig_SetCursorPos);
e1: unhook_inline((void *)orig_GetCursorPos);
e0: UnregisterClassW(L"RInput", 0);
- return false;
+ return FEAT_FAIL;
}
END {
diff --git a/src/sst.c b/src/sst.c
index 5f09991..726fe2d 100644
--- a/src/sst.c
+++ b/src/sst.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -28,6 +28,8 @@
#include "engineapi.h"
#include "errmsg.h"
#include "event.h"
+#include "extmalloc.h" // for freevars() in generated code
+#include "feature.h"
#include "fixes.h"
#include "gamedata.h"
#include "gameinfo.h"
@@ -239,7 +241,7 @@ DEF_CCMD_HERE(sst_printversion, "Display plugin version information", 0) {
// cvar to the top of the demo. this will be removed later, once there's a less
// stupid way of achieving the same goal.
#if VERSION_MAJOR != 0 || VERSION_MINOR != 9
-#error Need to change this manually, since codegen requires it to be spelled \
+#error Need to change this manually, since gluegen requires it to be spelled \
out in DEF_CVAR - better yet, can we get rid of this yet?
#endif
DEF_CVAR(__sst_0_9_beta, "", 0, CON_HIDDEN | CON_DEMO)
@@ -271,7 +273,28 @@ static const char *updatenotes = "\
* More behind-the-scenes changes, as always\n\
";
-#include <featureinit.gen.h> // generated by build/codegen.c
+enum { // used in generated code, must line up with
+ REQFAIL = _FEAT_INTERNAL_STATUSES,
+ NOGD,
+ NOGLOBAL
+};
+static const char *const featmsgs[] = { // "
+ " [ OK! ] %s\n",
+ " [ FAILED! ] %s (error in initialisation)\n",
+ " [ unsupported ] %s (incompatible with this game or engine)\n",
+ " [ skipped ] %s (requires another feature)\n",
+ " [ unsupported ] %s (missing required gamedata entry)\n",
+ " [ FAILED! ] %s (failed to access engine)\n"
+};
+
+static inline void successbanner(void) { // called by generated code
+ con_colourmsg(&(struct rgba){64, 255, 64, 255},
+ LONGNAME " v" VERSION " successfully loaded");
+ con_colourmsg(&(struct rgba){255, 255, 255, 255}, " for game ");
+ con_colourmsg(&(struct rgba){0, 255, 255, 255}, "%s\n", gameinfo_title);
+}
+
+#include <glue.gen.h> // generated by build/gluegen.c
static void do_featureinit(void) {
engineapi_lateinit();
@@ -297,7 +320,7 @@ static void do_featureinit(void) {
"InputSystemVersion001", 0))) {
errmsg_warnx("missing input system interface");
}
- // ... and now for the real magic!
+ // ... and now for the real magic! (n.b. this also registers feature cvars)
initfeatures();
// if we're autoloaded and the external autoupdate script downloaded a new
@@ -374,7 +397,7 @@ static con_cmdcb orig_plugin_load_cb, orig_plugin_unload_cb;
static int ownidx; // XXX: super hacky way of getting this to do_unload()
static bool ispluginv1(const struct CPlugin *plugin) {
- // basename string is set with strncmp(), so if there's null bytes with more
+ // basename string is set with strncpy(), so if there's null bytes with more
// stuff after, we can't be looking at a v2 struct. and we expect null bytes
// in ifacever, since it's a small int value
return (plugin->v2.basename[0] == 0 || plugin->v2.basename[0] == 1) &&
@@ -437,6 +460,7 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) {
*p++ = (void *)&nop_ipipp_v; // OnQueryCvarValueFinished (002+)
*p++ = (void *)&nop_p_v; // OnEdictAllocated
*p = (void *)&nop_p_v; // OnEdictFreed
+ preinitfeatures();
if (!deferinit()) { do_featureinit(); fixes_apply(); }
if_hot (pluginhandler) {
cmd_plugin_load = con_findcmd("plugin_load");
@@ -468,6 +492,7 @@ static void do_unload(void) {
}
endfeatures();
con_disconnect();
+ freevars();
}
static bool VCALLCONV Load(void *this, ifacefactory enginef,
@@ -547,7 +572,4 @@ export const void *CreateInterface(const char *name, int *ret) {
return 0;
}
-// no better place to put this lol
-#include <evglue.gen.h> // generated by src/build/codegen.c
-
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/trace.c b/src/trace.c
index 0a301a7..c3e25ec 100644
--- a/src/trace.c
+++ b/src/trace.c
@@ -1,6 +1,6 @@
/*
* Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br>
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -23,12 +23,14 @@
#include "trace.h"
FEATURE()
+// TODO(compat): limiting to tested branches for now; support others as needed
+GAMESPECIFIC(L4D)
struct ray {
// these have type VectorAligned in the engine, which occupies 16 bytes
struct vec3f _Alignas(16) start, delta, startoff, extents;
// align to 16 since "extents" is supposed to occupy 16 bytes.
- // TODO(compat): this member isn't in every engine branch
+ // TODO(compat): this member isn't in every engine branch
const float _Alignas(16) (*worldaxistransform)[3][4];
bool isray, isswept;
};
@@ -85,17 +87,12 @@ struct CGameTrace trace_hull(struct vec3f start, struct vec3f end,
return t;
}
-PREINIT {
- // TODO(compat): restricting this to tested branches for now
- return GAMETYPE_MATCHES(L4D);
-}
-
INIT {
- if (!(srvtrace = factory_engine("EngineTraceServer003", 0))) {
+ if (!(srvtrace = factory_engine("EngineTraceServer003", 0))) {
errmsg_errorx("couldn't get server-side tracing interface");
- return false;
+ return FEAT_INCOMPAT;
}
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/x86util.h b/src/x86util.h
index 9fcdfff..214f30e 100644
--- a/src/x86util.h
+++ b/src/x86util.h
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -28,7 +28,7 @@
int _len = x86_len(p); \
if_cold (_len == -1) { \
errmsg_errorx("unknown or invalid instruction looking for %s", tgt); \
- return false; \
+ return 0; \
} \
(p) += _len; \
} while (0)
diff --git a/src/xhair.c b/src/xhair.c
index dddcdab..30fc69b 100644
--- a/src/xhair.c
+++ b/src/xhair.c
@@ -1,5 +1,5 @@
/*
- * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © 2025 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
@@ -14,8 +14,6 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
-#include <string.h>
-
#include "con_.h"
#include "engineapi.h"
#include "feature.h"
@@ -25,24 +23,24 @@
#include "intdefs.h"
#include "vcall.h"
-FEATURE("crosshair drawing")
+FEATURE("custom crosshair drawing")
REQUIRE(hud)
DECL_VFUNC_DYN(bool, IsInGame)
-DEF_CVAR(sst_xhair, "Enable custom crosshair", 0, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR(sst_xhair_colour, "Colour for alternative crosshair (RGBA hex)",
- "FFFFFF", CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_MIN(sst_xhair_thickness, "Thickness of custom crosshair in pixels", 2,
- 1, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_MIN(sst_xhair_size, "Length of lines in custom crosshair in pixels", 8,
- 0, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR_MIN(sst_xhair_gap, "Gap between lines in custom crosshair in pixels",
- 16, 0, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR(sst_xhair_dot, "Whether to draw dot in middle of custom crosshair",
- 1, CON_ARCHIVE | CON_HIDDEN)
-DEF_CVAR(sst_xhair_outline, "Whether to draw outline around custom crosshair",
- 0, CON_ARCHIVE | CON_HIDDEN)
+DEF_FEAT_CVAR(sst_xhair, "Enable custom crosshair", 0, CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_xhair_colour,
+ "Colour for custom crosshair (RGBA hex)", "FFFFFF", CON_ARCHIVE)
+DEF_FEAT_CVAR_MIN(sst_xhair_thickness,
+ "Thickness of custom crosshair in pixels", 2, 1, CON_ARCHIVE)
+DEF_FEAT_CVAR_MIN(sst_xhair_size,
+ "Length of lines in custom crosshair in pixels", 8, 0, CON_ARCHIVE)
+DEF_FEAT_CVAR_MIN(sst_xhair_gap,
+ "Gap between lines in custom crosshair in pixels", 16, 0, CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_xhair_dot,
+ "Whether to draw dot in middle of custom crosshair", 1, CON_ARCHIVE)
+DEF_FEAT_CVAR(sst_xhair_outline,
+ "Whether to draw outline around custom crosshair", 0, CON_ARCHIVE)
static struct rgba colour = {255, 255, 255, 255};
@@ -80,15 +78,8 @@ HANDLE_EVENT(HudPaint, void) {
}
INIT {
- sst_xhair->base.flags &= ~CON_HIDDEN;
- sst_xhair_colour->base.flags &= ~CON_HIDDEN;
sst_xhair_colour->cb = &colourcb;
- sst_xhair_thickness->base.flags &= ~CON_HIDDEN;
- sst_xhair_size->base.flags &= ~CON_HIDDEN;
- sst_xhair_gap->base.flags &= ~CON_HIDDEN;
- sst_xhair_dot->base.flags &= ~CON_HIDDEN;
- sst_xhair_outline->base.flags &= ~CON_HIDDEN;
- return true;
+ return FEAT_OK;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/test/hook.test.c b/test/hook.test.c
index a2447cc..9e7cfa9 100644
--- a/test/hook.test.c
+++ b/test/hook.test.c
@@ -12,7 +12,7 @@
#include <stdio.h>
#include <string.h>
-// stubs
+// stub
void con_warn(const char *msg, ...) {
va_list l;
va_start(l, msg);
@@ -20,43 +20,38 @@ void con_warn(const char *msg, ...) {
va_end(l);
}
-__attribute__((noinline))
-static int some_function(int a, int b) { return a + b; }
-static int (*orig_some_function)(int, int);
-static int some_hook(int a, int b) {
- return orig_some_function(a, b) + 5;
-}
-__attribute__((noinline))
-static int other_function(int a, int b) { return a - b; }
-static int (*orig_other_function)(int, int);
-static int other_hook(int a, int b) {
- return orig_other_function(a, b) + 5;
-}
+typedef int (*testfunc)(int, int);
+
+__attribute__((noinline)) static int func1(int a, int b) { return a + b; }
+static int (*orig_func1)(int, int);
+static int hook1(int a, int b) { return orig_func1(a, b) + 5; }
+
+__attribute__((noinline)) static int func2(int a, int b) { return a - b; }
+static int (*orig_func2)(int, int);
+static int hook2(int a, int b) { return orig_func2(a, b) + 5; }
TEST("Inline hooks should be able to wrap the original function") {
if (!hook_init()) return false;
- orig_some_function = hook_inline(&some_function, &some_hook);
- if (!orig_some_function) return false;
- return some_function(5, 5) == 15;
+ orig_func1 = (testfunc)hook_inline((void *)&func1, (void *)&hook1);
+ if (!orig_func1) return false;
+ return func1(5, 5) == 15;
}
TEST("Inline hooks should be removable again") {
if (!hook_init()) return false;
- orig_some_function = hook_inline(&some_function, &some_hook);
- if (!orig_some_function) return false;
- unhook_inline(orig_some_function);
- return some_function(5, 5) == 10;
+ orig_func1 = (testfunc)hook_inline((void *)&func1, (void *)&hook1);
+ if (!orig_func1) return false;
+ unhook_inline((void *)orig_func1);
+ return func1(5, 5) == 10;
}
-TEST("Multiple functions should be able to be inline hooked at once") {
+TEST("Multiple functions should be able to be inline-hooked at once") {
if (!hook_init()) return false;
- orig_some_function = hook_inline(&some_function, &some_hook);
- if (!orig_some_function) return false;
-
- orig_other_function = hook_inline(&other_function, &other_hook);
- if (!orig_other_function) return false;
-
- return other_function(5, 5) == 5;
+ orig_func1 = (testfunc)hook_inline((void *)&func1, (void *)&hook1);
+ if (!orig_func1) return false;
+ orig_func2 = (testfunc)hook_inline((void *)&func2, (void *)&hook2);
+ if (!orig_func2) return false;
+ return func2(5, 5) == 5;
}
#endif