diff options
Diffstat (limited to 'src')
46 files changed, 1614 insertions, 1548 deletions
| 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 | 
