diff options
author | 2025-03-10 02:37:19 +0000 | |
---|---|---|
committer | 2025-04-06 16:41:13 +0100 | |
commit | 244fea664121acf12871ab5858a5fe95a2606b52 (patch) | |
tree | e42b1990ef97adc0f0ab48b9be7e11de7fee0558 | |
parent | d86b7b41453c69b3854baa7cdc05a79a3cdfe092 (diff) | |
download | sst-244fea664121acf12871ab5858a5fe95a2606b52.tar.gz sst-244fea664121acf12871ab5858a5fe95a2606b52.zip |
Rewrite and redesign codegen and feature system
Also switch to somewhat proper C23 flags while we're at it.
This is a huge change. It took me forever, in between being really busy.
Sorry about that. But the good news is I'm now free to start integrating
the various patches that have accumulated since last release. Well, at
least in between still being really busy. Gotta manage expectations.
The main benefit of introducing GAMESPECIFIC() is that features
that don't apply to a particular game no longer show up *at all*, and
less time is wasted on init. It also enables a cool optimisation wherein
unnecessary REQUIRE_GAMEDATA() checks can elided at compile time
whenever the gamedata is known up-front to always exist in supported
games.
The DEF_FEAT_CVAR macro family meanwhile makes it easier to manage the
lifecycle of cvars/ccmds, with less manual registering, unhiding and
such.
Originally I was going to try and just hack these features into the
existing codegen abomination, but it just got too terrible. This rewrite
should make it easier to continue tweaking codegen behaviour in future.
It also has slightly better error messages.
52 files changed, 1666 insertions, 1606 deletions
@@ -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> @@ -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) { @@ -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 { @@ -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 @@ -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) { @@ -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; @@ -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 @@ -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) @@ -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 { @@ -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 { @@ -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 |