diff options
Diffstat (limited to 'src')
60 files changed, 2473 insertions, 1366 deletions
diff --git a/src/3p/chibicc/tokenize.c b/src/3p/chibicc/tokenize.c index b13edd5..6738206 100644 --- a/src/3p/chibicc/tokenize.c +++ b/src/3p/chibicc/tokenize.c @@ -1,5 +1,9 @@ #include "chibicc.h" +#ifdef _WIN32 +#define strncasecmp _strnicmp +#endif + // Input file static File *current_file; @@ -17,6 +17,13 @@ #include <stdlib.h> +#ifdef _WIN32 +#include <Windows.h> +#include <werapi.h> +#else +#include <sys/mman.h> +#endif + #include "alias.h" #include "bind.h" #include "chunklets/fastspin.h" @@ -33,6 +40,7 @@ #include "gamedata.h" #include "gametype.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "ppmagic.h" @@ -41,10 +49,6 @@ #include "x86.h" #include "x86util.h" -#ifdef _WIN32 -#include <werapi.h> // must be after Windows.h (via os.h) -#endif - FEATURE() REQUIRE(bind) REQUIRE(democustom) @@ -99,34 +103,37 @@ static ulong inhooktid; static ssize __stdcall kproc(int code, usize wp, ssize lp) { KBDLLHOOKSTRUCT *data = (KBDLLHOOKSTRUCT *)lp; - if (enabled && data->flags & LLKHF_INJECTED && - GetForegroundWindow() == gamewin) { - // maybe this input is reasonable, but log it for closer inspection - // TODO(rta): figure out what to do with this stuff - // something like the following, but with a proper abstraction... - //uchar buf[28 + 16], *p = buf; - //msg_putasz4(p, 2); p += 1; - // msg_putssz5(p, 8); memcpy(p + 1, "FakeKey", 7); p += 8; - // msg_putmsz4(p, 2); p += 1; - // msg_putssz5(p, 3); memcpy(p + 1, "vk", 2); p += 3; - // p += msg_putu32(p, data->vkCode); - // msg_putssz5(p, 3); memcpy(p + 1, "scan", 4); p += 5; - // p += msg_putu32(p, data->scanCode); - //++keybox->nonce; - //// append mac at end of message - //crypto_aead_lock_djb(buf, p, keybox->shr, keybox->nonce_bytes, 0, 0, - // buf, p - buf); - //democustom_write(buf, p - buf + 16); + if_cold (enabled && data->flags & LLKHF_INJECTED) { + // fast-path the next branch because alt-tabbed speed is irrelevant + if_hot (GetForegroundWindow() == gamewin) { + // maybe this input is reasonable, but log it for closer inspection + // TODO(rta): figure out what to do with this stuff + // something like the following, but with a proper abstraction... + //uchar buf[28 + 16], *p = buf; + //msg_putasz4(p, 2); p += 1; + // msg_putssz5(p, 8); memcpy(p + 1, "FakeKey", 7); p += 8; + // msg_putmsz4(p, 2); p += 1; + // msg_putssz5(p, 3); memcpy(p + 1, "vk", 2); p += 3; + // p += msg_putu32(p, data->vkCode); + // msg_putssz5(p, 3); memcpy(p + 1, "scan", 4); p += 5; + // p += msg_putu32(p, data->scanCode); + //++keybox->nonce; + //// append mac at end of message + //crypto_aead_lock_djb(buf, p, keybox->shr, keybox->nonce_bytes, 0, + // 0, buf, p - buf); + //democustom_write(buf, p - buf + 16); + } } return CallNextHookEx(0, code, wp, lp); } static ssize __stdcall mproc(int code, usize wp, ssize lp) { MSLLHOOKSTRUCT *data = (MSLLHOOKSTRUCT *)lp; - if (enabled && data->flags & LLMHF_INJECTED && - GetForegroundWindow() == gamewin) { - // no way this input would ever be reasonable. just discard it - return 1; + if_cold (enabled && data->flags & LLMHF_INJECTED) { + if_hot (GetForegroundWindow() == gamewin) { + // no way this input would ever be reasonable. just discard it + return 1; + } } return CallNextHookEx(0, code, wp, lp); } @@ -135,7 +142,7 @@ static ssize __stdcall mproc(int code, usize wp, ssize lp) { // hook gets silently removed. plus, we don't wanna incur latency anyway. static ulong __stdcall inhookthrmain(void *param) { volatile int *sig = param; - if (!SetWindowsHookExW(WH_KEYBOARD_LL, (HOOKPROC)&kproc, 0, 0) || + if_cold (!SetWindowsHookExW(WH_KEYBOARD_LL, (HOOKPROC)&kproc, 0, 0) || !SetWindowsHookExW(WH_MOUSE_LL, (HOOKPROC)&mproc, 0, 0)) { fastspin_raise(sig, 2); return -1; @@ -148,29 +155,31 @@ static ulong __stdcall inhookthrmain(void *param) { static ssize orig_wndproc; static ssize __stdcall hook_wndproc(void *wnd, uint msg, usize wp, ssize lp) { - if (msg == WM_COPYDATA && enabled) return DefWindowProcW(wnd, msg, wp, lp); + if_cold (msg == WM_COPYDATA && enabled) { + return DefWindowProcW(wnd, msg, wp, lp); + } return CallWindowProcA((WNDPROC)orig_wndproc, wnd, msg, wp, lp); } -static bool win32_init(void) { +static inline bool win32_init(void) { // note: using A instead of W to avoid some weirdness with handles... gamewin = FindWindowA("Valve001", 0); // note: error messages here are a bit cryptic on purpose, but easy to find // in the code. in other words, we're hiding in plain sight :-) - if (!gamewin) { + if_cold (!gamewin) { errmsg_errorsys("failed to find window"); return false; } orig_wndproc = SetWindowLongPtrA(gamewin, GWLP_WNDPROC, (ssize)&hook_wndproc); - if (!orig_wndproc) { // XXX: assuming 0 won't be legitimately returned + if_cold (!orig_wndproc) { // XXX: assuming 0 won't be legitimately returned errmsg_errorsys("failed to attach message handler"); return false; } return true; } -static void win32_end(void) { +static inline void win32_end(void) { // no error handling here because we'd crash either way. good luck! SetWindowLongPtrA(gamewin, GWLP_WNDPROC, orig_wndproc); } @@ -185,7 +194,7 @@ static void inhook_check(void) { if (WaitForSingleObject(inhookthr, 0) == WAIT_OBJECT_0) { ulong status; GetExitCodeThread(inhookthr, &status); - if (status) { + if_cold (status) { // XXX: if this ever happens, it's a disaster! users might not // notice their run just dying all of a sudden. with any luck it // won't matter in practice but... this kind of sucks. @@ -206,7 +215,7 @@ static void inhook_stop(void) { // assume WAIT_OBJECT_0 ulong status; GetExitCodeThread(inhookthr, &status); - if (status) { + if_cold (status) { // not much else we can do now! errmsg_errorx("message loop didn't shut down cleanly\n"); } @@ -221,17 +230,18 @@ static void inhook_stop(void) { #endif bool ac_enable(void) { - if (enabled) return true; + if (!enabled) { #ifdef _WIN32 - volatile int sig = 0; - inhook_start(&sig); - fastspin_wait(&sig); - if (sig == 2) { // else 1 for success - con_warn("** sst: ERROR starting message loop, can't continue! **"); - CloseHandle(inhookthr); - return false; - } + volatile int sig = 0; + inhook_start(&sig); + fastspin_wait(&sig); + if_cold (sig == 2) { // else 1 for success + con_warn("** sst: ERROR starting message loop, can't continue! **"); + CloseHandle(inhookthr); + return false; + } #endif + } enabled = true; return true; } @@ -245,10 +255,11 @@ HANDLE_EVENT(Tick, bool simulating) { } void ac_disable(void) { - if (!enabled) return; + if (enabled) { #ifdef _WIN32 - inhook_stop(); + inhook_stop(); #endif + } enabled = false; } @@ -305,7 +316,7 @@ static bool find_Key_Event(void) { // -> CGame::DispatchAllStoredGameMessages vfunc // -> First call instruction (either DispatchInputEvent or Key_Event) void *gameuifuncs = factory_engine("VENGINE_GAMEUIFUNCS_VERSION005", 0); - if (!gameuifuncs) { + if_cold (!gameuifuncs) { errmsg_errorx("couldn't get engine game UI interface"); return false; } @@ -352,7 +363,7 @@ ok2: } HANDLE_EVENT(AllowPluginLoading, bool loading) { - if (enabled && demorec_demonum() != -1) { + if_cold(enabled) if_hot(demorec_demonum() != -1) { con_warn("sst: plugins cannot be %s while recording a run\n", loading ? "loaded" : "unloaded"); return false; @@ -372,39 +383,39 @@ PREINIT { } INIT { - if (!find_Key_Event()) return false; + if_cold (!find_Key_Event()) return false; orig_Key_Event = (Key_Event_func)hook_inline((void *)orig_Key_Event, (void *)&hook_Key_Event); - if (!orig_Key_Event) { + if_cold (!orig_Key_Event) { errmsg_errorsys("couldn't hook Key_Event function"); return false; } #ifdef _WIN32 keybox = VirtualAlloc(0, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); - if (!keybox) { + if_cold (!keybox) { errmsg_errorsys("couldn't allocate memory for session state"); return false; } - if (!VirtualLock(keybox, 4096)) { + if_cold (!VirtualLock(keybox, 4096)) { errmsg_errorsys("couldn't secure session state"); goto e2; } - if (WerRegisterExcludedMemoryBlock(keybox, 4096) != S_OK) { + if_cold (WerRegisterExcludedMemoryBlock(keybox, 4096) != S_OK) { // FIXME: stringify errors properly here errmsg_errorx("couldn't secure session state"); goto e2; } - if (!win32_init()) goto e; + if_cold (!win32_init()) goto e; #else keybox = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); - if (keybox == MAP_FAILED) { + if_cold (keybox == MAP_FAILED) { errmsg_errorstd("couldn't allocate memory for session state"); return false; } // 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) - if (madvise(keybox, 4096, MADV_DONTFORK) == -1 || + if_cold (madvise(keybox, 4096, MADV_DONTFORK) == -1 || madvise(keybox, 4096, MADV_DONTDUMP) == - 1 || mlock(keybox, 4096) == -1) { errmsg_errorstd("couldn't secure session state"); diff --git a/src/alias.c b/src/alias.c index 6af7467..e6f5f7e 100644 --- a/src/alias.c +++ b/src/alias.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -94,10 +94,10 @@ INIT { if (GAMETYPE_MATCHES(Portal2)) return false; struct con_cmd *cmd_alias = con_findcmd("alias"); - if (!find_alias_head(con_getcmdcb(cmd_alias))) { + if_cold (!find_alias_head(con_getcmdcb(cmd_alias))) { errmsg_warnx("couldn't find alias list"); return false; - }; + } con_reg(sst_alias_clear); con_reg(sst_alias_remove); return true; diff --git a/src/autojump.c b/src/autojump.c index 1bfc170..64ed436 100644 --- a/src/autojump.c +++ b/src/autojump.c @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -20,8 +20,9 @@ #include "feature.h" #include "gamedata.h" #include "gametype.h" -#include "intdefs.h" #include "hook.h" +#include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "vcall.h" @@ -34,7 +35,6 @@ REQUIRE_GLOBAL(factory_client) // note: server will never be null DEF_CVAR(sst_autojump, "Jump upon hitting the ground while holding space", 0, CON_REPLICATE | CON_DEMO | CON_HIDDEN) -#define IN_JUMP 2 #define NIDX 256 // *completely* arbitrary lol static bool justjumped[NIDX] = {0}; static inline int handleidx(ulong h) { return h & (1 << 11) - 1; } @@ -76,7 +76,7 @@ static bool unprot(void *gm) { // reimplementing cheats check for dumb and bad reasons, see below static struct con_var *sv_cheats; static void cheatcb(struct con_var *this) { - if (this->ival && !con_getvari(sv_cheats)) { + if (this->ival) if_cold(!con_getvari(sv_cheats)) { con_warn("Can't use cheat cvar sst_autojump, unless server has " "sv_cheats set to 1.\n"); con_setvari(this, 0); @@ -85,17 +85,17 @@ static void cheatcb(struct con_var *this) { INIT { gmsv = factory_server("GameMovement001", 0); - if (!gmsv) { + if_cold (!gmsv) { errmsg_errorx("couldn't get server-side game movement interface"); return false; } - if (!unprot(gmsv)) return false; + if_cold (!unprot(gmsv)) return false; gmcl = factory_client("GameMovement001", 0); - if (!gmcl) { + if_cold (!gmcl) { errmsg_errorx("couldn't get client-side game movement interface"); return false; } - if (!unprot(gmcl)) return false; + if_cold (!unprot(gmcl)) return false; origsv = (CheckJumpButton_func)hook_vtable(*(void ***)gmsv, vtidx_CheckJumpButton, (void *)&hooksv); origcl = (CheckJumpButton_func)hook_vtable(*(void ***)gmcl, @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -20,6 +20,7 @@ #include "feature.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "x86.h" #include "x86util.h" @@ -57,7 +58,7 @@ static bool find_keyinfo(con_cmdcb klbc_cb) { INIT { struct con_cmd *cmd_key_listboundkeys = con_findcmd("key_listboundkeys"); con_cmdcb cb = con_getcmdcb(cmd_key_listboundkeys); - if (!find_keyinfo(cb)) { + if_cold (!find_keyinfo(cb)) { errmsg_warnx("couldn't find key binding list"); return false; } diff --git a/src/bitbuf.h b/src/bitbuf.h index 404dc9d..9e0fe19 100644 --- a/src/bitbuf.h +++ b/src/bitbuf.h @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -77,11 +77,12 @@ static inline void bitbuf_appendbuf(struct bitbuf *bb, const char *buf, // shift the stored value (if it were big endian, the shift would have // to be the other way, or something) _bitbuf_append(bb, *p >> (unalign << 3), (bitbuf_align - unalign) << 3); - buf += sizeof(bitbuf_cell) - unalign; + buf += (int)sizeof(bitbuf_cell) - unalign; len -= unalign; } bitbuf_cell *aligned = (bitbuf_cell *)buf; - for (; len >= sizeof(bitbuf_cell); len -= sizeof(bitbuf_cell), ++aligned) { + for (; len >= (int)sizeof(bitbuf_cell); len -= (int)sizeof(bitbuf_cell), + ++aligned) { _bitbuf_append(bb, *aligned, bitbuf_cell_bits); } // unaligned end bytes diff --git a/src/build/cmeta.c b/src/build/cmeta.c index 0c9b3ca..8a2416d 100644 --- a/src/build/cmeta.c +++ b/src/build/cmeta.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -102,15 +102,15 @@ static void die(const char *s) { exit(100); } -static char *readsource(const os_char *f) { - int fd = os_open(f, O_RDONLY); - if (fd == -1) return 0; +static char *readsource(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 = read(fd, buf + off, bufsz - off)) > 0) { + while ((nread = os_read(f, buf + off, bufsz - off)) > 0) { off += nread; if (off == bufsz) { bufsz *= 2; @@ -122,24 +122,24 @@ static char *readsource(const os_char *f) { } if (nread == -1) die("couldn't read file"); buf[off] = 0; - close(fd); + 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 *f) { - char *buf = readsource(f); +const struct cmeta *cmeta_loadfile(const os_char *path) { + char *buf = readsource(path); if (!buf) return 0; #ifdef _WIN32 - char *realname = malloc(wcslen(f) + 1); + char *realname = malloc(wcslen(path) + 1); if (!realname) die("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. - *realname = *f; - for (const ushort *p = f + 1; p[-1]; ++p) realname[p - f] = *p; + *realname = *path; + for (const ushort *p = path + 1; p[-1]; ++p) realname[p - path] = *p; #else const char *realname = f; #endif diff --git a/src/build/cmeta.h b/src/build/cmeta.h index 48a2d6d..86ae0ec 100644 --- a/src/build/cmeta.h +++ b/src/build/cmeta.h @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -21,7 +21,7 @@ struct cmeta; -const struct cmeta *cmeta_loadfile(const os_char *f); +const struct cmeta *cmeta_loadfile(const os_char *path); /* * Iterates through all the #include directives in a file, passing each one in diff --git a/src/build/codegen.c b/src/build/codegen.c index 3cf4d8b..70b5e12 100644 --- a/src/build/codegen.c +++ b/src/build/codegen.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -20,6 +20,7 @@ #include <string.h> #include "../intdefs.h" +#include "../langext.h" #include "../os.h" #include "cmeta.h" #include "skiplist.h" @@ -45,8 +46,8 @@ static struct conent { static int nconents; #define PUT(name_, isvar_, unreg_) do { \ - if (nconents == sizeof(conents) / sizeof(*conents)) { \ - fprintf(stderr, "codegen: out of space; make ents bigger!\n"); \ + if (nconents == countof(conents)) { \ + fprintf(stderr, "codegen: out of space; make conents bigger!\n"); \ exit(1); \ } \ conents[nconents].name = name_; \ @@ -116,7 +117,7 @@ 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 (type) { + 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); diff --git a/src/build/mkentprops.c b/src/build/mkentprops.c index 0781f25..cda1c02 100644 --- a/src/build/mkentprops.c +++ b/src/build/mkentprops.c @@ -17,13 +17,11 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/stat.h> #include "../intdefs.h" -#include "../kv.h" -#include "../noreturn.h" +#include "../langext.h" #include "../os.h" -#include "skiplist.h" -#include "vec.h" #ifdef _WIN32 #define fS "S" @@ -31,176 +29,359 @@ #define fS "s" #endif -static noreturn die(const char *s) { +static noreturn die(int status, const char *s) { fprintf(stderr, "mkentprops: %s\n", s); - exit(100); + exit(status); } - -struct prop { - const char *varname; /* the C global name */ - const char *propname; /* the entity property name */ - struct prop *next; -}; -struct vec_prop VEC(struct prop *); - -DECL_SKIPLIST(static, class, struct class, const char *, 4) -struct class { - const char *name; /* the entity class name */ - struct vec_prop props; - struct skiplist_hdr_class hdr; -}; -static inline int cmp_class(struct class *c, const char *s) { - return strcmp(c->name, s); +static noreturn dieparse(const os_char *file, int line, const char *s) { + fprintf(stderr, "mkentprops: %" fS ":%d: %s\n", file, line, s); + exit(2); } -static inline struct skiplist_hdr_class *hdr_class(struct class *c) { - return &c->hdr; + +static char *sbase; // input file contents - string values are indices off this + +// custom data structure of the day: zero-alloc half-SoA adaptive radix trees(!) + +#define ART_MAXNODES 16384 +static uchar art_firstbytes[ART_MAXNODES]; +static struct art_core { + int soff; // string offset to entire key chunk (including firstbyte) + u16 slen; // number of bytes in key chunk (including firstbyte, >=1) + u16 next; // sibling node (same prefix, different firstbyte) +} art_cores[ART_MAXNODES]; +// if node doesn't end in \0: child node (i.e. key appends more bytes) +// if node DOES end in \0: offset into art_leaves (below) +static u16 art_children[ART_MAXNODES]; +static int art_nnodes = 0; + +#define ART_NULL ((u16)-1) + +static inline int art_newnode(void) { + if (art_nnodes == ART_MAXNODES) die(2, "out of tree nodes"); + return art_nnodes++; } -DEF_SKIPLIST(static, class, cmp_class, hdr_class) -static struct skiplist_hdr_class classes = {0}; -static int nclasses = 0; - -struct parsestate { - const os_char *filename; - struct kv_parser *parser; - char *lastvar; -}; - -static noreturn badparse(struct parsestate *state, const char *e) { - fprintf(stderr, "mkentprops: %" fS ":%d:%d: parse error: %s", - state->filename, state->parser->line, state->parser->col, e); - exit(1); + +#define ART_MAXLEAVES 8192 +static struct art_leaf { + int varstr; // offset of string (generated variable), if any, or -1 if none + u16 subtree; // art index of subtree (nested SendTable), or -1 if none + u16 nsubs; // number of subtrees (used to short-circuit the runtime search) +} art_leaves[ART_MAXLEAVES]; +static int art_nleaves = 0; +static u16 art_root = ART_NULL; // ServerClasses (which point at SendTables) +static u16 nclasses = 0; // similar short circuit for ServerClasses + +#define VAR_NONE -1 + +// for quick iteration over var names to generate decls header without ART faff +#define MAXDECLS 4096 +static int decls[MAXDECLS]; +static int ndecls = 0; + +static inline int art_newleaf(void) { + if (art_nleaves == ART_MAXLEAVES) die(2, "out of leaf nodes"); + return art_nleaves++; } -static void kv_cb(enum kv_token type, const char *p, uint len, void *ctxt) { - struct parsestate *state = ctxt; - switch (type) { - case KV_IDENT: case KV_IDENT_QUOTED: - state->lastvar = malloc(len + 1); - if (!state->lastvar) die("couldn't allocate memory"); - memcpy(state->lastvar, p, len); state->lastvar[len] = '\0'; - break; - case KV_NEST_START: badparse(state, "unexpected nested block"); - case KV_NEST_END: badparse(state, "unexpected closing brace"); - case KV_VAL: case KV_VAL_QUOTED:; - struct prop *prop = malloc(sizeof(*prop)); - if (!prop) die("couldn't allocate memory"); - prop->varname = state->lastvar; - char *classname = malloc(len + 1); - if (!classname) die("couldn't allocate memory"); - memcpy(classname, p, len); classname[len] = '\0'; - char *propname = strchr(classname, '/'); - if (!propname) { - badparse(state, "network name not in class/prop format"); +static struct art_lookup_ret { + bool isnew; // true if node created, false if found + u16 leafidx; // index of value (leaf) node +} art_lookup(u16 *art, int soff, int len) { + assume(len > 0); // everything must be null-terminated! + for (;;) { + const uchar *p = (const uchar *)sbase + soff; + u16 cur = *art; + if (cur == ART_NULL) { // append + int new = art_newnode(); + *art = new; + art_firstbytes[new] = *p; + art_cores[new].soff = soff; + art_cores[new].slen = len; + art_cores[new].next = ART_NULL; + int leaf = art_newleaf(); // N.B. firstbyte is already 0 here + art_children[new] = leaf; + return (struct art_lookup_ret){true, leaf}; + } + while (art_firstbytes[cur] == *p) { + int nodelen = art_cores[cur].slen; + int matchlen = 1; + const uchar *q = (const uchar *)sbase + art_cores[cur].soff; + for (; matchlen < nodelen; ++matchlen) { + if (p[matchlen] != q[matchlen]) { + // node and key diverge: split into child nodes + // (left is new part of key string, right is existing tail) + art_cores[cur].slen = matchlen; + int l = art_newnode(), r = art_newnode(); + art_firstbytes[l] = p[matchlen]; + art_cores[l].soff = soff + matchlen; + art_cores[l].slen = len - matchlen; + art_cores[l].next = r; + art_firstbytes[r] = q[matchlen]; + art_cores[r].soff = art_cores[cur].soff + matchlen; + art_cores[r].slen = nodelen - matchlen; + art_cores[r].next = ART_NULL; + art_children[r] = art_children[cur]; + art_children[cur] = l; + int leaf = art_newleaf(); + art_children[l] = leaf; + return (struct art_lookup_ret){true, leaf}; + } } - *propname = '\0'; ++propname; // split! - prop->propname = propname; - struct class *class = skiplist_get_class(&classes, classname); - if (!class) { - class = malloc(sizeof(*class)); - if (!class) die("couldn't allocate memory"); - *class = (struct class){.name = classname}; - skiplist_insert_class(&classes, classname, class); - ++nclasses; + if (matchlen == len) { + // node matches entire key: we have matched an existing entry + return (struct art_lookup_ret){false, art_children[cur]}; } - // (if class is already there just leak classname, no point freeing) - if (!vec_push(&class->props, prop)) die("couldn't append to array"); + // node is substring of key: descend into child nodes + soff += matchlen; + len -= matchlen; + cur = art_children[cur]; // note: this can't be ART_NULL (thus loop) + p = (const uchar *)sbase + soff; // XXX: kinda silly dupe + } + // if we didn't match this node, try the next sibling node. + // if sibling is null, we'll hit the append case above. + art = &art_cores[cur].next; + } +} + +static struct art_leaf *helpgetleaf(u16 *art, const char *s, int len, + const os_char *parsefile, int parseline, u16 *countvar) { + struct art_lookup_ret leaf = art_lookup(art, s - sbase, len); + if (leaf.isnew) { + art_leaves[leaf.leafidx].varstr = VAR_NONE; + art_leaves[leaf.leafidx].subtree = ART_NULL; + ++*countvar; + } + // if parsefile is null then we don't care about dupes (looking at subtable) + else if (parsefile && art_leaves[leaf.leafidx].varstr != VAR_NONE) { + dieparse(parsefile, parseline, "duplicate property name"); + } + return art_leaves + leaf.leafidx; +} + +static inline void handleentry(char *k, char *v, int vlen, + const os_char *file, int line) { + if (ndecls == MAXDECLS) die(2, "out of declaration entries"); + decls[ndecls++] = k - sbase; + char *propname = memchr(v, '/', vlen); + if (!propname) { + dieparse(file, line, "network name not in class/property format"); + } + *propname++ = '\0'; + int sublen = propname - v; + if (sublen > 65535) { + dieparse(file, line, "network class name is far too long"); + } + vlen -= sublen; + struct art_leaf *leaf = helpgetleaf(&art_root, v, sublen, 0, 0, &nclasses); + u16 *subtree = &leaf->subtree; + for (;;) { + if (vlen > 65535) { + dieparse(file, line, "property (SendTable) name is far too long"); + } + char *nextpart = memchr(propname, '/', vlen); + if (!nextpart) { + leaf = helpgetleaf(subtree, propname, vlen, file, line, + &leaf->nsubs); + leaf->varstr = k - sbase; break; - case KV_COND_PREFIX: case KV_COND_SUFFIX: - badparse(state, "unexpected conditional"); + } + *nextpart++ = '\0'; + sublen = nextpart - propname; + leaf = helpgetleaf(subtree, propname, sublen, 0, 0, &leaf->nsubs); + subtree = &leaf->subtree; + vlen -= sublen; + propname = nextpart; } } -static inline noreturn diewrite(void) { die("couldn't write to file"); } +static 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 }; + static const s8 statetrans[] = { + // layout: any, space|tab, #, \n + [BOL + 0] = KEY, [BOL + 1] = ERR, [BOL + 2] = COM, [BOL + 3] = BOL, + [KEY + 0] = KEY, [KEY + 1] = KWS, [KEY + 2] = ERR, [KEY + 3] = ERR, + [KWS + 0] = VAL, [KWS + 1] = KWS, [KWS + 2] = ERR, [KWS + 3] = ERR, + [VAL + 0] = VAL, [VAL + 1] = VAL, [VAL + 2] = COM, [VAL + 3] = BOL, + [COM + 0] = COM, [COM + 1] = COM, [COM + 2] = COM, [COM + 3] = BOL + }; + char *key, *val; + for (int state = BOL, i = 0, line = 1; i < len; ++i) { + int transidx = state; + switch (s[i]) { + case '\0': dieparse(file, line, "unexpected null byte"); + case ' ': case '\t': transidx += 1; break; + case '#': transidx += 2; break; + case '\n': transidx += 3; + } + int newstate = statetrans[transidx]; + if_cold (newstate == ERR) { + if (state == BOL) dieparse(file, line, "unexpected indentation"); + if (s[i] == '\n') dieparse(file, line, "unexpected end of line"); + dieparse(file, line, "unexpected comment"); + } + switch_exhaust (newstate) { + case KEY: if_cold (state != KEY) key = s + i; break; + case KWS: if_cold (state != KWS) s[i] = '\0'; break; + case VAL: if_cold (state == KWS) val = s + i; break; + case COM: case BOL: + if (state == VAL) { + int j = i; + while (s[j - 1] == ' ' || s[j - 1] == '\t') --j; + s[j] = '\0'; + int vallen = j - (val - s) + 1; + handleentry(key, val, vallen, file, line); + } + } + line += state == BOL; + state = newstate; + } +} -#define _(x) \ - if (fprintf(out, "%s\n", x) < 0) diewrite(); -#define F(f, ...) \ - if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +static inline noreturn diewrite(void) { die(100, "couldn't write to file"); } +#define _(x) if (fprintf(out, "%s\n", x) < 0) diewrite(); +#define _i(x) for (int i = 0; i < indent; ++i) fputc('\t', out); _(x) +#define F(f, ...) if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +#define Fi(...) for (int i = 0; i < indent; ++i) fputc('\t', out); F(__VA_ARGS__) #define H() \ _( "/* This file is autogenerated by src/build/mkentprops.c. DO NOT EDIT! */") \ _( "") -static void decls(FILE *out) { - for (struct class *c = classes.x[0]; c; c = c->hdr.x[0]) { - for (struct prop **pp = c->props.data; - pp - c->props.data < c->props.sz; ++pp) { -F( "extern bool has_%s;", (*pp)->varname) -F( "extern int %s;", (*pp)->varname) +static void dosendtables(FILE *out, u16 art, int indent) { +_i("switch (*p) {") + while (art != ART_NULL) { + // stupid hack: figure out char literal in case of null byte + char charlit[3] = {art_firstbytes[art], '0'}; + if_hot (charlit[0]) charlit[1] = '\0'; else charlit[0] = '\\'; + if (art_cores[art].slen != 1) { + const char *tail = sbase + art_cores[art].soff + 1; + int len = art_cores[art].slen - 1; +Fi(" case '%s': if (!strncmp(p + 1, \"%.*s\", %d)) {", +charlit, len, tail, len) + } + else { +Fi(" case '%s': {", charlit) } + int idx = art_children[art]; + // XXX: kind of a dumb and bad way to distinguish these. okay for now... + if (sbase[art_cores[art].soff + art_cores[art].slen - 1] != '\0') { +Fi(" p += %d;", art_cores[art].slen) + dosendtables(out, idx, indent + 2); + } + else { + // XXX: do we actually want to prefetch this before the for loop? +_i(" int off = baseoff + mem_loads32(mem_offset(sp, off_SP_offset));") + if (art_leaves[idx].varstr != VAR_NONE) { +Fi(" %s = off;", sbase + art_leaves[idx].varstr); + } + if (art_leaves[idx].subtree != ART_NULL) { +_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;") + } +_i(" } break;") + art = art_cores[art].next; } +_i("}") } -static void defs(FILE *out) { - for (struct class *c = classes.x[0]; c; c = c->hdr.x[0]) { - for (struct prop **pp = c->props.data; - pp - c->props.data < c->props.sz; ++pp) { -F( "bool has_%s = false;", (*pp)->varname) -F( "int %s;", (*pp)->varname) +static void doclasses(FILE *out, u16 art, int indent) { +_i("switch (*p) {") + for (; art != ART_NULL; art = art_cores[art].next) { + // stupid hack 2: exact dupe boogaloo + char charlit[3] = {art_firstbytes[art], '0'}; + if_hot (charlit[0]) charlit[1] = '\0'; else charlit[0] = '\\'; + if (art_cores[art].slen != 1) { + const char *tail = sbase + art_cores[art].soff + 1; + int len = art_cores[art].slen - 1; +Fi(" case '%s': if (!strncmp(p + 1, \"%.*s\", %d)) {", +charlit, len, tail, len) } - } -_( "") -_( "static void initentprops(struct ServerClass *class) {") -F( " for (int needclasses = %d; class; class = class->next) {", nclasses) - char *else1 = ""; - for (struct class *c = classes.x[0]; c; c = c->hdr.x[0]) { - // TODO(opt): some sort of PHF or trie instead of chained strcmp, if we - // ever have more than a few classes/properties? -F( " %sif (!strcmp(class->name, \"%s\")) {", else1, c->name) -_( " struct SendTable *st = class->table;") -F( " int needprops = %d;", c->props.sz) -_( " for (struct SendProp *p = st->props;") -_( " mem_diff(p, st->props) < st->nprops * sz_SendProp;") -_( " p = mem_offset(p, sz_SendProp)) {") -_( " const char *varname = mem_loadptr(mem_offset(p, off_SP_varname));") - char *else2 = ""; - for (struct prop **pp = c->props.data; - pp - c->props.data < c->props.sz; ++pp) { -F( " %sif (!strcmp(varname, \"%s\")) {", else2, (*pp)->propname) -F( " has_%s = true;", (*pp)->varname) -F( " %s = mem_loads32(mem_offset(p, off_SP_offset));", - (*pp)->varname) -_( " if (!--needprops) break;") -_( " }") - else2 = "else "; + else { +Fi(" case '%s': {", charlit) } -_( " }") -_( " if (!--needclasses) break;") -_( " }") - else1 = "else "; + int idx = art_children[art]; + // XXX: same dumb-and-bad-ness as above. there must be a better way! + if (sbase[art_cores[art].soff + art_cores[art].slen - 1] != '\0') { +Fi(" p += %d;", art_cores[art].slen) + doclasses(out, art_children[art], indent + 2); + } + else { + assume(art_leaves[idx].varstr == VAR_NONE); + assume(art_leaves[idx].subtree != ART_NULL); +_i(" const struct SendTable *st = class->table;") +Fi(" for (int i = 0, need = %d; i < st->nprops && need; ++i) {", +art_leaves[idx].nsubs + (art_leaves[idx].varstr != -1)) + // note: annoyingly long line here, but the generated code gets + // super nested anyway, so there's no point in caring really + // XXX: basically a dupe of dosendtables() - fold into above? +_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 + 3); +_i(" }") +Fi(" --need;") + } +_i(" } break;") + } +_i("}") +} + +static void dodecls(FILE *out) { + for (int i = 0; i < ndecls; ++i) { + const char *s = sbase + decls[i]; +F( "extern int %s;", s); +F( "#define has_%s (!!%s)", s, s); // offsets will NEVER be 0, due to vtable! + } +} + +static void doinit(FILE *out) { + for (int i = 0; i < ndecls; ++i) { + const char *s = sbase + decls[i]; +F( "int %s = 0;", s); } +_( "") +_( "static inline void initentprops(const struct ServerClass *class) {") +_( " enum { baseoff = 0 };") // can be shadowed for subtables. +F( " for (int need = %d; need && class; class = class->next) {", nclasses) +_( " const char *p = class->name;") + doclasses(out, art_root, 2); _( " }") _( "}") } int OS_MAIN(int argc, os_char *argv[]) { - for (++argv; *argv; ++argv) { - int fd = os_open(*argv, O_RDONLY); - if (fd == -1) die("couldn't open file"); - struct kv_parser kv = {0}; - struct parsestate state = {*argv, &kv}; - char buf[1024]; - int nread; - while (nread = read(fd, buf, sizeof(buf))) { - if (nread == -1) die("couldn't read file"); - if (!kv_parser_feed(&kv, buf, nread, &kv_cb, &state)) goto ep; - } - if (!kv_parser_done(&kv)) { -ep: fprintf(stderr, "mkentprops: %" fS ":%d:%d: bad syntax: %s\n", - *argv, kv.line, kv.col, kv.errmsg); - exit(1); - } - close(fd); - } + if (argc != 2) die(1, "wrong number of arguments"); + int f = os_open_read(argv[1]); + 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"); + sbase = malloc(len); + if (!sbase) die(100, "couldn't allocate memory"); + if (os_read(f, sbase, len) != len) die(100, "couldn't read file"); + os_close(f); + parse(argv[1], len); FILE *out = fopen(".build/include/entprops.gen.h", "wb"); - if (!out) die("couldn't open entprops.gen.h"); + if (!out) die(100, "couldn't open entprops.gen.h"); H(); - decls(out); + dodecls(out); out = fopen(".build/include/entpropsinit.gen.h", "wb"); - if (!out) die("couldn't open entpropsinit.gen.h"); + if (!out) die(100, "couldn't open entpropsinit.gen.h"); H(); - defs(out); + doinit(out); return 0; } diff --git a/src/build/mkgamedata.c b/src/build/mkgamedata.c index d49eef5..1fce1cf 100644 --- a/src/build/mkgamedata.c +++ b/src/build/mkgamedata.c @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -19,10 +19,8 @@ #include <string.h> #include "../intdefs.h" -#include "../kv.h" -#include "../noreturn.h" +#include "../langext.h" #include "../os.h" -#include "vec.h" #ifdef _WIN32 #define fS "S" @@ -30,214 +28,247 @@ #define fS "s" #endif -static noreturn die(const char *s) { - fprintf(stderr, "mkgamedata: %s\n", s); - exit(100); +static noreturn die(int status, const char *s) { + fprintf(stderr, "mkentprops: %s\n", s); + exit(status); +} + +// concatenated input file contents - string values are indices off this +static char *sbase = 0; + +static const os_char *const *srcnames; +static noreturn dieparse(int file, int line, const char *s) { + fprintf(stderr, "mkentprops: %" fS ":%d: %s\n", srcnames[file], line, s); + exit(2); +} + +#define MAXENTS 32768 +static int tags[MAXENTS]; // varname/gametype +static int exprs[MAXENTS]; +static uchar indents[MAXENTS]; // nesting level +static schar srcfiles[MAXENTS]; +static int srclines[MAXENTS]; +static int nents = 0; + +static inline void handleentry(char *k, char *v, int indent, + int file, int line) { + int previndent = nents ? indents[nents - 1] : -1; // meh + if_cold (indent > previndent + 1) { + dieparse(file, line, "excessive indentation"); + } + if_cold (indent == previndent && !exprs[nents - 1]) { + dieparse(file, line - 1, "missing a value and/or conditional(s)"); + } + if_cold (nents == MAXENTS) die(2, "out of array indices"); + tags[nents] = k - sbase; + exprs[nents] = v - sbase; // will produce garbage for null v. this is fine! + indents[nents] = indent; + srcfiles[nents] = file; + srclines[nents++] = line; } /* - * We keep the gamedata KV format as simple as possible. Default values are + * -- Quick file format documentation! -- + * + * We keep the gamedata format as simple as possible. Default values are * specified as direct key-value pairs: * - * <varname> <expr> + * <varname> <expr> * - * Game- or engine-specific values are set using blocks: + * Game- or engine-specific values are set using indented blocks: * - * <varname> { <gametype> <expr> <gametype> <expr> ... [default <expr>] } + * <varname> <optional-default> + * <gametype1> <expr> + * <gametype2> <expr> # you can write EOL comments too! + * <some-other-nested-conditional-gametype> <expr> * * The most complicated it can get is if conditionals are nested, which - * basically translates directly into nested ifs: - * <varname> { <gametype> { <gametype> <expr> <gametype> <expr> } } - * [however many entries...] + * basically translates directly into nested ifs. * - * If that doesn't make sense, just look at one of the existing data files and - * then it should be obvious. :^) - * - * Note: if `default` isn't given in a conditional block, that piece of gamedata - * is considered unavailable and modules that use it won't get initialised/used - * unless all the conditions are met. + * Just be aware that whitespace is significant, and you have to use tabs. + * Any and all future complaints about that decision SHOULD - and MUST - be + * directed to the Python Software Foundation and the authors of the POSIX + * Makefile specification. In that order. */ -struct vec_ent VEC(struct ent *); -struct ent { - const char *name; // (or condition tag, in a child node) - const char *defexpr; - struct vec_ent subents; - struct ent *parent; // to back up a level during parse -}; -// root only contains subents list but it's easier to use the same struct -static struct ent root = {0}; - -struct parsestate { - const os_char *filename; - struct kv_parser *parser; - struct ent *curent; // current ent lol - bool haddefault; // blegh; -}; - -static noreturn badparse(struct parsestate *state, const char *e) { - fprintf(stderr, "mkgamedata: %" fS ":%d:%d: parse error: %s", - state->filename, state->parser->line, state->parser->col, e); - exit(1); -} -static void kv_cb(enum kv_token type, const char *p, uint len, void *ctxt) { - struct parsestate *state = ctxt; - switch (type) { - case KV_IDENT: case KV_IDENT_QUOTED:; - if (len == 7 && !memcmp(p, "default", 7)) { // special case! - if (state->curent == &root) { - badparse(state, "unexpected default keyword at top level"); +static void parse(int file, char *s, int len) { + 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 }; + static const s8 statetrans[] = { + // layout: any, space|tab, #, \n + [BOL + 0] = KEY, [BOL + 1] = BOL, [BOL + 2] = COM, [BOL + 3] = BOL, + [KEY + 0] = KEY, [KEY + 1] = KWS, [KEY + 2] = COM, [KEY + 3] = BOL, + [KWS + 0] = VAL, [KWS + 1] = KWS, [KWS + 2] = COM, [KWS + 3] = BOL, + [VAL + 0] = VAL, [VAL + 1] = VAL, [VAL + 2] = COM, [VAL + 3] = BOL, + [COM + 0] = COM, [COM + 1] = COM, [COM + 2] = COM, [COM + 3] = BOL + }; + char *key, *val = sbase; // 0 index by default (invalid value works as null) + for (int state = BOL, i = 0, line = 1, indent = 0; i < len; ++i) { + int transidx = state; + char c = s[i]; + switch (c) { + case '\0': dieparse(file, line, "unexpected null byte"); + case ' ': + if_cold (state == BOL) { + dieparse(file, line, "unexpected space at start of line"); } - struct ent *e = state->curent; - if (e->defexpr) { - badparse(state, "multiple default keywords"); - } - state->haddefault = true; + case '\t': + transidx += 1; break; - } - state->haddefault = false; - char *k = malloc(len + 1); - if (!k) die("couldn't allocate key string"); - // FIXME(?): should check and prevent duplicate keys probably! - // need table.h or something to avoid O(n^2) :) - memcpy(k, p, len); k[len] = '\0'; - struct ent *e = malloc(sizeof(*e)); - if (!e) die("couldn't allocate memory"); - e->name = k; - e->defexpr = 0; - e->subents = (struct vec_ent){0}; - if (!vec_push(&state->curent->subents, e)) { - die("couldn't append to array"); - } - e->parent = state->curent; - state->curent = e; - break; - case KV_NEST_START: - if (state->haddefault) badparse(state, "default cannot be a block"); - break; - case KV_NEST_END: - if (!state->curent->parent) { - badparse(state, "unexpected closing brace"); - } - state->curent = state->curent->parent; - break; - case KV_VAL: case KV_VAL_QUOTED:; - char *s = malloc(len + 1); - if (!s) die("couldn't allocate value string"); - memcpy(s, p, len); s[len] = '\0'; - state->curent->defexpr = s; - if (!state->haddefault) { - // a non-default value is just a node that itself only has a - // default value. - state->curent = state->curent->parent; - } - break; - case KV_COND_PREFIX: case KV_COND_SUFFIX: - badparse(state, "unexpected conditional"); + case '#': transidx += 2; break; + case '\n': transidx += 3; + } + int newstate = statetrans[transidx]; + switch_exhaust (newstate) { + case KEY: if_cold (state != KEY) key = s + i; break; + case KWS: if_cold (state != KWS) s[i] = '\0'; break; + case VAL: if_cold (state == KWS) val = s + i; break; + case BOL: + indent += state == BOL; + if_cold (indent > 255) { // this shouldn't happen if we're sober + dieparse(file, line, "exceeded max nesting level (255)"); + } + case COM: + if_hot (state != BOL) { + if (state != COM) { // blegh! + int j = i; + while (s[j - 1] == ' ' || s[j - 1] == '\t') --j; + s[j] = '\0'; + handleentry(key, val, indent, file, line); + } + val = sbase; // reset this again + } + } + if_cold (c == '\n') { // ugh, so much for state transitions. + indent = 0; + ++line; + } + state = newstate; } } -static inline noreturn diewrite(void) { die("couldn't write to file"); } - -#define _doindent \ - for (int _indent = 0; _indent < indent; ++_indent) { \ - if (fputs("\t", out) == -1) diewrite(); \ - } -#define _(x) \ - if (fprintf(out, "%s\n", x) < 0) diewrite(); -#define _i(x) _doindent _(x) -#define F(f, ...) \ - if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); -#define Fi(...) _doindent F(__VA_ARGS__) +static inline noreturn diewrite(void) { die(100, "couldn't write to file"); } +#define _(x) if (fprintf(out, "%s\n", x) < 0) diewrite(); +#define _i(x) for (int i = 0; i < indent; ++i) fputc('\t', out); _(x) +#define F(f, ...) if (fprintf(out, f "\n", __VA_ARGS__) < 0) diewrite(); +#define Fi(...) for (int i = 0; i < indent; ++i) fputc('\t', out); F(__VA_ARGS__) #define H() \ _( "/* This file is autogenerated by src/build/mkgamedata.c. DO NOT EDIT! */") \ _( "") static void decls(FILE *out) { - for (struct ent *const *pp = root.subents.data; - pp - root.subents.data < root.subents.sz; ++pp) { - if ((*pp)->defexpr) { -F( "#define has_%s true", (*pp)->name) - if ((*pp)->subents.sz) { -F( "extern int %s;", (*pp)->name) - } - else { -F( "enum { %s = %s };", (*pp)->name, (*pp)->defexpr) - } + for (int i = 0; i < nents; ++i) { + if (indents[i] != 0) continue; +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if (exprs[i]) { // default value is specified - entry always exists + // *technically* this case is redundant - the other has_ macro would + // still work. however, having a distinct case makes the generated + // header a little easier to read at a glance. +F( "#define has_%s 1", sbase + tags[i]) } - else { -F( "extern bool has_%s;", (*pp)->name) -F( "extern int %s;", (*pp)->name) + else { // entry is missing unless a tag is matched + // implementation detail: INT_MIN is reserved for missing gamedata! + // XXX: for max robustness, should probably check for this in input? +F( "#define has_%s (%s != -2147483648)", sbase + tags[i], sbase + tags[i]) + } +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 +F( "extern int %s;", sbase + tags[i]); } } } -static void inits(FILE *out, const char *var, struct vec_ent *v, bool needhas, - int indent) { - for (struct ent *const *pp = v->data; pp - v->data < v->sz; ++pp) { -Fi("if (GAMETYPE_MATCHES(%s)) {", (*pp)->name) - if ((*pp)->defexpr) { - if (needhas) { -Fi(" has_%s = true;", var); +static void defs(FILE *out) { + for (int i = 0; i < nents; ++i) { + if (indents[i] != 0) continue; + if_hot (i < nents - 1 && indents[i + 1]) { +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if (exprs[i]) { +F( "int %s = (%s);", sbase + tags[i], sbase + exprs[i]) + } + else { +F( "int %s = -2147483648;", sbase + tags[i]) } -Fi(" %s = %s;", var, (*pp)->defexpr); } - inits(out, var, &(*pp)->subents, needhas && !(*pp)->defexpr, indent + 1); -_i("}") } } -static void defs(FILE *out) { - for (struct ent *const *pp = root.subents.data; - pp - root.subents.data < root.subents.sz; ++pp) { - if ((*pp)->defexpr) { - if ((*pp)->subents.sz) { -F( "int %s = %s;", (*pp)->name, (*pp)->defexpr); +static void init(FILE *out) { +_( "void gamedata_init(void) {") + int varidx; + int indent = 0; + for (int i = 0; i < nents; ++i) { + if (indents[i] < indents[i - 1]) { + for (; indent != indents[i]; --indent) { +_i("}") } } + if (indents[i] == 0) { + varidx = i; + continue; + } +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) + if (indents[i] > indents[i - 1]) { +Fi(" if (GAMETYPE_MATCHES(%s)) {", sbase + tags[i]) + ++indent; + } else { -F( "int %s;", (*pp)->name); -F( "bool has_%s = false;", (*pp)->name); +_i("}") +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) +Fi("else if (GAMETYPE_MATCHES(%s)) {", sbase + tags[i]) + } + if (exprs[i]) { +F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]]) +Fi(" %s = (%s);", sbase + tags[varidx], sbase + exprs[i]) } } -_( "") -_( "void gamedata_init(void) {") - for (struct ent *const *pp = root.subents.data; - pp - root.subents.data < root.subents.sz; ++pp) { - inits(out, (*pp)->name, &(*pp)->subents, !(*pp)->defexpr, 1); + for (; indent != 0; --indent) { +_i("}") } _( "}") } int OS_MAIN(int argc, os_char *argv[]) { - for (++argv; *argv; ++argv) { - int fd = os_open(*argv, O_RDONLY); - if (fd == -1) die("couldn't open file"); - struct kv_parser kv = {0}; - struct parsestate state = {*argv, &kv, &root}; - char buf[1024]; - int nread; - while (nread = read(fd, buf, sizeof(buf))) { - if (nread == -1) die("couldn't read file"); - if (!kv_parser_feed(&kv, buf, nread, &kv_cb, &state)) goto ep; + srcnames = (const os_char *const *)argv; + int sbase_len = 0, sbase_max = 65536; + sbase = malloc(sbase_max); + if (!sbase) die(100, "couldn't allocate memory"); + int n = 1; + for (++argv; *argv; ++argv, ++n) { + int f = os_open_read(*argv); + if (f == -1) die(100, "couldn't open file"); + vlong len = os_fsize(f); + if (sbase_len + len > 1u << 29) { + die(2, "combined input files are far too large"); } - if (!kv_parser_done(&kv)) { -ep: fprintf(stderr, "mkgamedata: %" fS ":%d:%d: bad syntax: %s\n", - *argv, kv.line, kv.col, kv.errmsg); - exit(1); + if (sbase_len + len > sbase_max) { + fprintf(stderr, "mkgamedata: warning: need to resize string. " + "increase sbase_max to avoid this extra work!\n"); + sbase_max *= 4; + sbase = realloc(sbase, sbase_max); + if (!sbase) die(100, "couldn't grow memory allocation"); } - close(fd); + char *s = sbase + sbase_len; + if (os_read(f, s, len) != len) die(100, "couldn't read file"); + os_close(f); + parse(n, s, len); + sbase_len += len; } FILE *out = fopen(".build/include/gamedata.gen.h", "wb"); - if (!out) die("couldn't open gamedata.gen.h"); + if (!out) die(100, "couldn't open gamedata.gen.h"); H(); decls(out); out = fopen(".build/include/gamedatainit.gen.h", "wb"); - if (!out) die("couldn't open gamedatainit.gen.h"); + if (!out) die(100, "couldn't open gamedatainit.gen.h"); H(); defs(out); + _("") + init(out); return 0; } diff --git a/src/build/skiplist.h b/src/build/skiplist.h index b747d89..ab6a920 100644 --- a/src/build/skiplist.h +++ b/src/build/skiplist.h @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -24,6 +24,7 @@ #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; diff --git a/src/build/vec.h b/src/build/vec.h index 6b4dffb..6dfa645 100644 --- a/src/build/vec.h +++ b/src/build/vec.h @@ -62,7 +62,7 @@ static bool _vec_make_room(struct _vec *v, uint tsize, uint addcnt) { // 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)) && \ + _vec_make_room((struct _vec *)(v), sizeof(val), 1)) && /*NOLINT*/ \ ((v)->data[(v)->sz++ - slack] = (val), true) \ ) diff --git a/src/chunklets/msg.c b/src/chunklets/msg.c index d99edc1..5b688cb 100644 --- a/src/chunklets/msg.c +++ b/src/chunklets/msg.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -155,9 +155,7 @@ int msg_putu32(unsigned char *out, unsigned int val) { } int msg_puts(unsigned char *out, long long val) { - if (val >= -2147483648 && val <= 2147483647) { - return msg_puts32(out, val); - } + if (val >= -2147483648 && val <= 2147483647) return msg_puts32(out, val); out[0] = 0xD3; doput64(out, val); return 9; diff --git a/src/clientcon.c b/src/clientcon.c new file mode 100644 index 0000000..29a4208 --- /dev/null +++ b/src/clientcon.c @@ -0,0 +1,41 @@ +/* + * 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 "con_.h" +#include "engineapi.h" +#include "ent.h" +#include "feature.h" +#include "gamedata.h" + +FEATURE("") +REQUIRE(ent) +REQUIRE_GAMEDATA(vtidx_ClientPrintf) +REQUIRE_GLOBAL(engserver) + +DECL_VFUNC_DYN(void, ClientPrintf, struct edict *, const char *) + +void clientcon_msg(struct edict *e, const char *s) { + ClientPrintf(engserver, e, s); +} + +void clientcon_reply(const char *s) { + struct edict *e = ent_getedict(con_cmdclient + 1); + if (e) { clientcon_msg(e, s); return; } +} + +INIT { return true; } + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/clientcon.h b/src/clientcon.h new file mode 100644 index 0000000..1b00c01 --- /dev/null +++ b/src/clientcon.h @@ -0,0 +1,33 @@ +/* + * 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_CLIENTCON_H +#define INC_CLIENTCON_H + +struct edict; + +/* Prints a message to a specific player's console. */ +void clientcon_msg(struct edict *e, const char *s); + +/* + * Prints a message in the console of whoever invoked the current command - only + * valid when called in the context of a CON_SERVERSIDE command. + */ +void clientcon_reply(const char *s); + +#endif + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/democustom.c b/src/democustom.c index 4c1baf2..30fc4ee 100644 --- a/src/democustom.c +++ b/src/democustom.c @@ -24,6 +24,7 @@ #include "feature.h" #include "gamedata.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "ppmagic.h" #include "vcall.h" @@ -46,7 +47,7 @@ static union { bitbuf_cell _align; // just in case... } bb_buf; static struct bitbuf bb = { - {bb_buf.x}, sizeof(bb_buf), sizeof(bb_buf) * 8, 0, false, false, "SST" + {bb_buf.x}, ssizeof(bb_buf), ssizeof(bb_buf) * 8, 0, false, false, "SST" }; static const void *createhdr(struct bitbuf *msg, int len, bool last) { diff --git a/src/demorec.c b/src/demorec.c index 8abba77..4a8efb5 100644 --- a/src/demorec.c +++ b/src/demorec.c @@ -26,6 +26,7 @@ #include "gameinfo.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "ppmagic.h" @@ -99,7 +100,7 @@ static struct con_cmd *cmd_record, *cmd_stop; static con_cmdcb orig_record_cb, orig_stop_cb; static void hook_record_cb(const struct con_cmdargs *args) { - if (!CHECK_DemoControlAllowed()) return; + if_cold (!CHECK_DemoControlAllowed()) return; bool was = *recording; if (!was && args->argc == 2 || args->argc == 3) { // safety check: make sure a directory exists, otherwise recording @@ -162,7 +163,7 @@ static void hook_record_cb(const struct con_cmdargs *args) { } static void hook_stop_cb(const struct con_cmdargs *args) { - if (!CHECK_DemoControlAllowed()) return; + if_cold (!CHECK_DemoControlAllowed()) return; wantstop = true; orig_stop_cb(args); wantstop = false; @@ -258,21 +259,21 @@ INIT { orig_record_cb = con_getcmdcb(cmd_record); cmd_stop = con_findcmd("stop"); orig_stop_cb = con_getcmdcb(cmd_stop); - if (!find_demorecorder()) { + if_cold (!find_demorecorder()) { errmsg_errorx("couldn't find demo recorder instance"); return false; } void **vtable = mem_loadptr(demorecorder); // XXX: 16 is totally arbitrary here! figure out proper bounds later - if (!os_mprot(vtable, 16 * sizeof(void *), PAGE_READWRITE)) { + if_cold (!os_mprot(vtable, 16 * sizeof(void *), PAGE_READWRITE)) { errmsg_errorsys("couldn't make virtual table writable"); return false; } - if (!find_recmembers(vtable[vtidx_StopRecording])) { + if_cold (!find_recmembers(vtable[vtidx_StopRecording])) { errmsg_errorx("couldn't find recording state variables"); return false; } - if (!find_demoname(vtable[vtidx_StartRecording])) { + if_cold (!find_demoname(vtable[vtidx_StartRecording])) { errmsg_errorx("couldn't find demo basename variable"); return false; } @@ -290,7 +291,7 @@ INIT { } END { - if (!sst_userunloaded) return; + if_hot (!sst_userunloaded) return; // avoid dumb edge case if someone somehow records and immediately unloads if (*recording && *demonum == 0) *demonum = 1; void **vtable = *(void ***)demorecorder; diff --git a/src/engineapi.c b/src/engineapi.c index 768510d..5a78a92 100644 --- a/src/engineapi.c +++ b/src/engineapi.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * Copyright © 2024 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 @@ -24,6 +24,7 @@ #include "gameinfo.h" #include "gametype.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" // " #include "os.h" #include "vcall.h" @@ -50,7 +51,7 @@ DECL_VFUNC_DYN(void *, GetAllServerClasses) #include <entpropsinit.gen.h> // generated by build/mkentprops.c bool engineapi_init(int pluginver) { - if (!con_detect(pluginver)) return false; + if_cold (!con_detect(pluginver)) return false; pluginhandler = factory_engine("ISERVERPLUGINHELPERS001", 0); if (engclient = factory_engine("VEngineClient015", 0)) { @@ -69,6 +70,9 @@ bool engineapi_init(int pluginver) { if (engserver = factory_engine("VEngineServer021", 0)) { _gametype_tag |= _gametype_tag_Server021; } + else if (engserver = factory_engine("VEngineServer022", 0)) { + //_gametype_tag |= _gametype_tag_Server022; // not needed yet + } // else if (engserver = others as needed...) { // } @@ -86,10 +90,15 @@ bool engineapi_init(int pluginver) { } // detect p1 for the benefit of specific features - if (!GAMETYPE_MATCHES(Portal2) && con_findcmd("upgrade_portalgun")) { - _gametype_tag |= _gametype_tag_Portal1; - if (!con_findvar("tf_arena_max_streak")) { - _gametype_tag |= _gametype_tag_Portal1_3420; + if (!GAMETYPE_MATCHES(Portal2)) { + if (con_findcmd("upgrade_portalgun")) { + _gametype_tag |= _gametype_tag_Portal1; + if (!con_findvar("tf_escort_score_rate")) { + _gametype_tag |= _gametype_tag_Portal1_3420; + } + } + else if (con_findcmd("phys_swap")) { + _gametype_tag |= _gametype_tag_HL2series; } } @@ -104,7 +113,7 @@ bool engineapi_init(int pluginver) { gamedata_init(); con_init(); - if (!gameinfo_init()) { con_disconnect(); return false; } + if_cold (!gameinfo_init()) { con_disconnect(); return false; } return true; } @@ -114,7 +123,8 @@ void engineapi_lateinit(void) { // > can detect that and set the SPROP_IS_VECTOR_ELEM flag. // by doing this at the deferred stage, we avoid having to abs() everything if (srvdll && has_vtidx_GetAllServerClasses && has_sz_SendProp && - has_off_SP_varname && has_off_SP_offset) { + has_off_SP_varname && has_off_SP_type && has_off_SP_offset && + has_DPT_DataTable) { initentprops(GetAllServerClasses(srvdll)); } } diff --git a/src/engineapi.h b/src/engineapi.h index 4489e21..308e34e 100644 --- a/src/engineapi.h +++ b/src/engineapi.h @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -96,15 +96,6 @@ struct CMoveData { struct vec3f origin; }; -#define SENDPROP_INT 0 -#define SENDPROP_FLOAT 1 -#define SENDPROP_VEC 2 -#define SENDPROP_VECXY 3 -#define SENDPROP_STR 4 -#define SENDPROP_ARRAY 5 -#define SENDPROP_DTABLE 6 -#define SENDPROP_INT64 7 - // these have to be opaque because, naturally, they're unstable between // branches - access stuff using gamedata offsets as usual struct RecvProp; @@ -163,6 +154,33 @@ struct CServerPlugin /* : IServerPluginHelpers */ { }; extern struct CServerPlugin *pluginhandler; +// input button bits +#define IN_ATTACK (1 << 0) +#define IN_JUMP (1 << 1) +#define IN_DUCK (1 << 2) +#define IN_FORWARD (1 << 3) +#define IN_BACK (1 << 4) +#define IN_USE (1 << 5) +#define IN_CANCEL (1 << 6) +#define IN_LEFT (1 << 7) +#define IN_RIGHT (1 << 8) +#define IN_MOVELEFT (1 << 9) +#define IN_MOVERIGHT (1 << 10) +#define IN_ATTACK2 (1 << 11) +#define IN_RUN (1 << 12) +#define IN_RELOAD (1 << 13) +#define IN_ALT1 (1 << 14) +#define IN_ALT2 (1 << 15) +#define IN_SCORE (1 << 16) +#define IN_SPEED (1 << 17) +#define IN_WALK (1 << 18) +#define IN_ZOOM (1 << 19) +#define IN_WEAPON1 (1 << 20) +#define IN_WEAPON2 (1 << 21) +#define IN_BULLRUSH (1 << 22) +#define IN_GRENADE1 (1 << 23) +#define IN_GRENADE2 (1 << 24) + /* * Called on plugin init to attempt to initialise various core interfaces. * This includes console/cvar initialisation and populating gametype and @@ -22,6 +22,7 @@ #include "gamedata.h" #include "gametype.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "vcall.h" #include "x86.h" @@ -35,8 +36,8 @@ static struct edict **edicts = 0; struct edict *ent_getedict(int idx) { if (edicts) { // globalvars->edicts seems to be null when disconnected - if (!*edicts) return 0; - return mem_offset(*edicts, sz_edict * idx); + if_hot (*edicts) return mem_offset(*edicts, sz_edict * idx); + return 0; } else { return PEntityOfEntIndex(engserver, idx); @@ -45,8 +46,8 @@ struct edict *ent_getedict(int idx) { void *ent_get(int idx) { struct edict *e = ent_getedict(idx); - if (!e) return 0; - return e->ent_unknown; + if_hot(e) return e->ent_unknown; + return 0; } struct CEntityFactory { @@ -143,14 +144,14 @@ void **ent_findvtable(const struct CEntityFactory *factory, const char *classname) { #ifdef _WIN32 ctor_func ctor = findctor(factory, classname); - if (!ctor) return 0; + if_cold (!ctor) return 0; const uchar *insns = (const uchar *)ctor; // the constructor itself should do *(void**)this = &vtable; almost right // away, so look for the first immediate load into indirect register for (const uchar *p = insns; p - insns < 32;) { if (p[0] == X86_MOVMIW && (p[1] & 0xF8) == 0) return mem_loadptr(p + 2); int len = x86_len(p); - if (len == -1) { + if_cold (len == -1) { errmsg_errorx("unknown or invalid instruction looking for %s " "vtable pointer", classname); return 0; @@ -166,7 +167,8 @@ void **ent_findvtable(const struct CEntityFactory *factory, INIT { #ifdef _WIN32 // TODO(linux): above struct con_cmd *dumpentityfactories = con_findcmd("dumpentityfactories"); - if (!dumpentityfactories || !find_entfactorydict(dumpentityfactories->cb)) { + if_cold (!dumpentityfactories || + !find_entfactorydict(dumpentityfactories->cb)) { errmsg_warnx("server entity factories unavailable"); } #endif diff --git a/src/errmsg.h b/src/errmsg.h index 17a4457..cdf5095 100644 --- a/src/errmsg.h +++ b/src/errmsg.h @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -17,6 +17,15 @@ #ifndef INC_ERRMSG_H #define INC_ERRMSG_H +#ifdef _WIN32 +#include <stdarg.h> + +// note: redundant declspec avoids warnings if Windows.h was also included +__declspec(dllimport) unsigned long __stdcall FormatMessageA(unsigned long, + const void *, unsigned long, unsigned long, char *, unsigned long, + va_list *); +#endif + #include "con_.h" #include "os.h" @@ -48,14 +57,17 @@ extern const char msg_note[], msg_warn[], msg_error[]; #ifdef _WIN32 #define _errmsg_sys(msg, ...) do { \ - char _warnsys_buf[512]; \ - OS_WINDOWS_ERROR(_warnsys_buf); \ - _errmsg_msg(_ERRMSG_STR(MODULE_NAME), msg, __VA_ARGS__, _warnsys_buf); \ + char _errmsg_buf[512]; \ + FormatMessageA(/*FORMAT_MESSAGE_FROM_SYSTEM*/ 4096, 0, os_lasterror(), \ + /*MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)*/ 1024, _errmsg_buf, \ + sizeof(_errmsg_buf), 0); \ + _errmsg_msg(_ERRMSG_STR(MODULE_NAME), msg, __VA_ARGS__, _errmsg_buf); \ } while (0) #define _errmsg_dl _errmsg_sys #else #define _errmsg_sys _errmsg_std static inline const char *_errmsg_dlerror(void) { + char *dlerror(void); const char *e = dlerror(); if (!e) return "Unknown error"; // just in case, better avoid weirdness! return e; diff --git a/src/extmalloc.c b/src/extmalloc.c index a23dc2a..d80ab8d 100644 --- a/src/extmalloc.c +++ b/src/extmalloc.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -41,6 +41,8 @@ void extfree(void *mem) { Free(g_pMemAlloc, mem); } #else +#include "langext.h" + void Error(const char *fmt, ...); // stub left out of con_.h (not that useful) // Linux Source doesn't seem to bother with the custom allocator stuff at all. @@ -48,15 +50,16 @@ void Error(const char *fmt, ...); // stub left out of con_.h (not that useful) // right, not a privilege. Like func_vehicle. void *extmalloc(usize sz) { void *ret = malloc(sz); - if (!ret) Error("sst: out of memory"); + if_cold (!ret) Error("sst: out of memory"); return ret; } void *extrealloc(void *mem, usize sz) { void *ret = realloc(mem, sz); - if (!ret) Error("sst: out of memory"); + if_cold (!ret) Error("sst: out of memory"); return ret; } // note: extfree is #defined to free in the header + #endif // vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/fastfwd.c b/src/fastfwd.c index 92d7623..e287770 100644 --- a/src/fastfwd.c +++ b/src/fastfwd.c @@ -26,6 +26,7 @@ #include "feature.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "ppmagic.h" @@ -46,19 +47,14 @@ static float *realtime, *host_frametime; static float skiptime = 0.0, skiprate; static void hook_Host_AccumulateTime(float dt) { float skipinc = skiprate * dt; - if (skiptime > skipinc) { - skiptime -= skipinc; - *realtime += skipinc; - *host_frametime = skipinc; - } - else if (skiptime > 0) { - *realtime += skiptime; - *host_frametime = skiptime; - skiptime = 0; - } - else { + if_hot (!skiptime) { orig_Host_AccumulateTime(dt); + return; } + if_random (skiptime <= skipinc) skipinc = skiptime; // should become fcmovbe + skiptime -= skipinc; + *realtime += skipinc; + *host_frametime = skipinc; } void fastfwd(float seconds, float timescale) { @@ -194,67 +190,67 @@ PREINIT { INIT { void *hldsapi = factory_engine("VENGINE_HLDS_API_VERSION002", 0); - if (!hldsapi) { + if_cold (!hldsapi) { errmsg_errorx("couldn't find HLDS API interface"); return false; } void *enginetool = factory_engine("VENGINETOOL003", 0); - if (!enginetool) { + if_cold (!enginetool) { errmsg_errorx("missing engine tool interface"); return false; } // behold: the greatest pointer chase of all time realtime = find_float((*(void ***)enginetool)[vtidx_GetRealTime]); - if (!realtime) { + if_cold (!realtime) { errmsg_errorx("couldn't find realtime variable"); return false; } host_frametime = find_float((*(void ***)enginetool)[vtidx_HostFrameTime]); - if (!host_frametime) { + if_cold (!host_frametime) { errmsg_errorx("couldn't find host_frametime variable"); return false; } void *eng = find_eng((*(void ***)hldsapi)[vtidx_RunFrame]); - if (!eng) { + if_cold (!eng) { errmsg_errorx("couldn't find eng global object"); return false; } void *func; - if (!(func = find_HostState_Frame((*(void ***)eng)[vtidx_Frame]))) { + if_cold (!(func = find_HostState_Frame((*(void ***)eng)[vtidx_Frame]))) { errmsg_errorx("couldn't find HostState_Frame function"); return false; } - if (!(func = find_FrameUpdate(func))) { + if_cold (!(func = find_FrameUpdate(func))) { errmsg_errorx("couldn't find FrameUpdate function"); return false; } - if (!(func = find_floatcall(func, GAMETYPE_MATCHES(L4D2_2147plus) ? 2 : 1, - "CHostState::State_Run"))) { + 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; } - if (!(func = find_floatcall(func, 1, "Host_RunFrame"))) { + if_cold (!(func = find_floatcall(func, 1, "Host_RunFrame"))) { errmsg_errorx("couldn't find Host_RunFrame function"); return false; } - if (!(func = find_floatcall(func, 1, "_Host_RunFrame"))) { + if_cold (!(func = find_floatcall(func, 1, "_Host_RunFrame"))) { errmsg_errorx("couldn't find _Host_RunFrame"); return false; } - if (!find_Host_AccumulateTime(func)) { + if_cold (!find_Host_AccumulateTime(func)) { errmsg_errorx("couldn't find Host_AccumulateTime"); return false; } orig_Host_AccumulateTime = (Host_AccumulateTime_func)hook_inline( (void *)orig_Host_AccumulateTime, (void *)hook_Host_AccumulateTime); - if (!orig_Host_AccumulateTime) { + if_cold (!orig_Host_AccumulateTime) { errmsg_errorsys("couldn't hook Host_AccumulateTime function"); } return true; } END { - if (!sst_userunloaded) return; + if_hot (!sst_userunloaded) return; unhook_inline((void *)orig_Host_AccumulateTime); } diff --git a/src/fixes.c b/src/fixes.c index 79f4e3d..84b3482 100644 --- a/src/fixes.c +++ b/src/fixes.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * Copyright © 2024 Michael Smith <mikesmiffy128@gmail.com> * Copyright © 2023 Hayden K <imaciidz@gmail.com> * * Permission to use, copy, modify, and/or distribute this software for any @@ -15,7 +15,6 @@ * PERFORMANCE OF THIS SOFTWARE. */ -#include <stdlib.h> #include <string.h> #ifdef _WIN32 @@ -23,7 +22,13 @@ #endif #include "con_.h" +#include "errmsg.h" #include "gametype.h" +#include "langext.h" +#include "mem.h" +#include "os.h" +#include "ppmagic.h" +#include "sst.h" static void chflags(const char *name, int unset, int set) { struct con_var *v = con_findvar(name); @@ -58,6 +63,7 @@ static void generalfixes(void) { unhide("con_filter_enable"); unhide("con_filter_text"); unhide("con_filter_text_out"); + unhide("con_logfile"); // things that could conceivably cause issues with speedrun verification // and/or pedantic following of rules; throw on cheat flag. this could be @@ -118,7 +124,7 @@ static void l4d2specific(void) { // possible on these earlier versions (who knows if that breaks // something...). struct con_var *v = con_findvar("mat_queue_mode"); - if (v && !(v->parent->base.flags & CON_ARCHIVE)) { // not already fixed + if_hot (v && !(v->parent->base.flags & CON_ARCHIVE)) { // not already fixed v->parent->base.flags = v->parent->base.flags & ~(CON_HIDDEN | CON_DEVONLY) | CON_ARCHIVE; v->parent->hasmin = true; v->parent->minval = -1; @@ -132,15 +138,14 @@ static void l4d2specific(void) { // so just blanket enable it if the primary adapter is Intel, since it // doesn't seem to break anything else anyway. v = con_findvar("mat_tonemapping_occlusion_use_stencil"); - if (!v || con_getvari(v)) goto e; + if_cold (!v || con_getvari(v)) goto e; // considered getting d3d9 object from actual game, but it's way easier // to just create another one IDirect3D9 *d3d9 = Direct3DCreate9(D3D_SDK_VERSION); - if (!d3d9) goto e; + if_cold (!d3d9) goto e; D3DADAPTER_IDENTIFIER9 ident; - if (IDirect3D9_GetAdapterIdentifier(d3d9, 0, 0, &ident) == D3D_OK && - ident.VendorId == 0x8086) { // neat vendor id, btw! - con_setvari(v, 1); + if_hot (IDirect3D9_GetAdapterIdentifier(d3d9, 0, 0, &ident) == D3D_OK) { + if (ident.VendorId == 0x8086) con_setvari(v, 1); // neat vendor id, btw! } IDirect3D9_Release(d3d9); e:; @@ -181,10 +186,35 @@ static void l4d1specific(void) { chcmdflags("update_addon_paths", 0, CON_CCMDEXEC); } +static void portal1specific(void) { +#ifdef _WIN32 + // TODO(compat): this is an absolutely atrocious way to implement this. it + // should only be temporary in the interests of getting 4104 working right + // away. since other versions also have broken demos, a more general fix + // should be done... eventually... + void *EyeAngles = mem_offset(clientlib, 0x19D1B0); // in C_PortalPlayer + static const char match[] = + HEXBYTES(56, 8B, F1, E8, 48, 50, EA, FF, 84, C0, 74, 25); + if (!memcmp(EyeAngles, match, sizeof(match))) { + char *patch = mem_offset(EyeAngles, 39); + if (patch[0] == 0x75 && patch[1] == 0x08) { + if_hot (os_mprot(patch, 2, PAGE_EXECUTE_READWRITE)) { + patch[0] = 0x90; patch[1] = 0x90; // replace je with nop + } + else { + errmsg_warnsys("unable to fix 4104 demo playback bug: " + "couldn't make memory writable"); + } + } + } +#endif +} + void fixes_apply(void) { generalfixes(); if (GAMETYPE_MATCHES(L4D1)) l4d1specific(); else if (GAMETYPE_MATCHES(L4D2x)) l4d2specific(); + else if (GAMETYPE_MATCHES(Portal1)) portal1specific(); } // vi: sw=4 ts=4 noet tw=80 cc=80 @@ -27,6 +27,7 @@ #include "gametype.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "sst.h" #include "vcall.h" @@ -67,7 +68,7 @@ static bool find_SetDefaultFOV(struct con_cmd *fov) { // replacement cvar needs to actively set player fov if in a map static void fovcb(struct con_var *v) { void *player = ent_get(1); // NOTE: singleplayer only! - if (player) orig_SetDefaultFOV(player, con_getvari(v)); + if_hot (player) orig_SetDefaultFOV(player, con_getvari(v)); } // ensure FOV is applied on load, if the engine wouldn't do that itself @@ -87,7 +88,7 @@ PREINIT { INIT { cmd_fov = con_findcmd("fov"); - if (!cmd_fov) return false; // shouldn't really happen but just in case! + if_cold (!cmd_fov) return false; // shouldn't happen but just in case! 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; @@ -98,13 +99,13 @@ INIT { con_reg(fov_desired); real_fov_desired = fov_desired; } - if (!find_SetDefaultFOV(cmd_fov)) { + if_cold (!find_SetDefaultFOV(cmd_fov)) { errmsg_errorx("couldn't find SetDefaultFOV function"); return false; } orig_SetDefaultFOV = (SetDefaultFOV_func)hook_inline( (void *)orig_SetDefaultFOV, (void *)&hook_SetDefaultFOV); - if (!orig_SetDefaultFOV) { + if_cold (!orig_SetDefaultFOV) { errmsg_errorsys("couldn't hook SetDefaultFOV function"); return false; } @@ -118,7 +119,7 @@ INIT { } END { - if (!sst_userunloaded) return; + if_hot (!sst_userunloaded) return; if (real_fov_desired && real_fov_desired != fov_desired) { real_fov_desired->parent->maxval = 90; if (con_getvarf(real_fov_desired) > 90) { diff --git a/src/gameinfo.c b/src/gameinfo.c index 85220a8..21205b7 100644 --- a/src/gameinfo.c +++ b/src/gameinfo.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -15,11 +15,15 @@ */ #include <string.h> +#ifdef _WIN32 +#include <Windows.h> // MultiByteToWideChar() +#endif #include "engineapi.h" #include "errmsg.h" #include "gamedata.h" #include "gametype.h" +#include "langext.h" #include "os.h" #include "vcall.h" @@ -38,7 +42,7 @@ const char *gameinfo_title = title; DECL_VFUNC_DYN(const char *, GetGameDirectory) bool gameinfo_init(void) { - if (!has_vtidx_GetGameDirectory) { + if_cold (!has_vtidx_GetGameDirectory) { errmsg_errorx("unsupported VEngineClient interface"); return false; } @@ -50,8 +54,8 @@ bool gameinfo_init(void) { // using e.g. Cyrillic folder names and successfully loading their // speedgames won't be able to load SST. Thanks Windows! const char *lcpgamedir = GetGameDirectory(engclient); - if (!MultiByteToWideChar(CP_ACP, 0, lcpgamedir, strlen(lcpgamedir), gamedir, - sizeof(gamedir) / sizeof(*gamedir))) { + if_cold (!MultiByteToWideChar(CP_ACP, 0, lcpgamedir, strlen(lcpgamedir), + gamedir, countof(gamedir))) { errmsg_errorsys("couldn't convert game directory path character set"); return false; } @@ -70,7 +74,7 @@ bool gameinfo_init(void) { // XXX: this same FindWindow call happens in ac.c - maybe factor out? void *gamewin = FindWindowW(L"Valve001", 0); // assuming: all games/mods use narrow chars only; this won't fail. - int len = GetWindowTextA(gamewin, title, sizeof(title)); + int len = GetWindowTextA(gamewin, title, ssizeof(title)); // argh, why did they start doing this, it's so pointless! // hopefully nobody included these suffixes in their mod names, lol if (len > 13 && !memcmp(title + len - 13, " - Direct3D 9", 13)) { diff --git a/src/gameserver.c b/src/gameserver.c index 5a76027..9e046ee 100644 --- a/src/gameserver.c +++ b/src/gameserver.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -19,6 +19,7 @@ #include "feature.h" #include "gamedata.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "x86.h" #include "vcall.h" @@ -66,7 +67,7 @@ static bool find_sv(con_cmdcb pause_cb) { INIT { struct con_cmd *pause = con_findcmd("pause"); - if (!find_sv(pause->cb)) { + if_cold (!find_sv(pause->cb)) { errmsg_errorx("couldn't find game server object\n"); return false; } diff --git a/src/gametype.h b/src/gametype.h index 81b860a..35a43be 100644 --- a/src/gametype.h +++ b/src/gametype.h @@ -38,22 +38,23 @@ extern u64 _gametype_tag; /* games needing game-specific stuff, but not tied to a singular branch */ #define _gametype_tag_Portal1 (1 << 8) +#define _gametype_tag_HL2series (1 << 9) /* HL2, episodes, and mods */ /* VEngineClient versions */ -#define _gametype_tag_Client015 (1 << 9) -#define _gametype_tag_Client014 (1 << 10) -#define _gametype_tag_Client013 (1 << 11) -#define _gametype_tag_Client012 (1 << 12) -#define _gametype_tag_Server021 (1 << 13) +#define _gametype_tag_Client015 (1 << 10) +#define _gametype_tag_Client014 (1 << 11) +#define _gametype_tag_Client013 (1 << 12) +#define _gametype_tag_Client012 (1 << 13) +#define _gametype_tag_Server021 (1 << 14) /* ServerGameDLL versions */ -#define _gametype_tag_SrvDLL009 (1 << 14) // 2013-ish -#define _gametype_tag_SrvDLL005 (1 << 15) // mostly everything else, it seems +#define _gametype_tag_SrvDLL009 (1 << 15) // 2013-ish +#define _gametype_tag_SrvDLL005 (1 << 16) // mostly everything else, it seems /* games needing version-specific stuff */ -#define _gametype_tag_Portal1_3420 (1 << 16) -#define _gametype_tag_L4D2_2147plus (1 << 17) -#define _gametype_tag_TheLastStand (1 << 18) /* The JAiZ update */ +#define _gametype_tag_Portal1_3420 (1 << 17) +#define _gametype_tag_L4D2_2147plus (1 << 18) +#define _gametype_tag_TheLastStand (1 << 19) /* The JAiZ update */ /* Matches for any multiple possible tags */ #define _gametype_tag_L4D (_gametype_tag_L4D1 | _gametype_tag_L4D2) @@ -19,10 +19,18 @@ #include "con_.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "x86.h" +#ifdef _WIN32 +// try to avoid pulling in all of Windows.h for this... (redundant dllimport +// avoids warnings in hook.test.c where Windows.h is included via test.h) +__declspec(dllimport) int __stdcall FlushInstructionCache( + void *, const void *, usize); +#endif + // Warning: half-arsed hacky implementation (because that's all we really need) // Almost certainly breaks in some weird cases. Oh well! Most of the time, // vtable hooking is more reliable, this is only for, uh, emergencies. @@ -60,7 +68,7 @@ void *hook_inline(void *func_, void *target) { // dumb hack: if we hit some thunk that immediately jumps elsewhere (which // seems common for win32 API functions), hook the underlying thing instead. while (*func == X86_JMPIW) func += mem_loads32(func + 1) + 5; - if (!os_mprot(func, 5, PAGE_EXECUTE_READWRITE)) return 0; + if_cold (!os_mprot(func, 5, PAGE_EXECUTE_READWRITE)) return 0; int len = 0; for (;;) { // FIXME: these cases may result in somewhat dodgy error messaging. They @@ -84,7 +92,7 @@ void *hook_inline(void *func_, void *target) { } } // for simplicity, just bump alloc the trampoline. no need to free anyway - if (nexttrampoline - trampolines > sizeof(trampolines) - len - 6) { + if_cold (nexttrampoline - trampolines > sizeof(trampolines) - len - 6) { con_warn("hook_inline: out of trampoline space\n"); return 0; } @@ -1,5 +1,6 @@ /* * Copyright © 2022 Matthew Wozniak <sirtomato999@gmail.com> + * 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 @@ -23,6 +24,7 @@ #include "hook.h" #include "hud.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "sst.h" @@ -79,10 +81,12 @@ DECL_VFUNC_DYN(void, DrawPolyLine, int *, int *, int) DECL_VFUNC_DYN(void, DrawSetTextFont, struct handlewrap) DECL_VFUNC_DYN(void, DrawSetTextColor, struct rgba) DECL_VFUNC_DYN(void, DrawSetTextPos, int, int) -DECL_VFUNC_DYN(void, DrawPrintText, ushort *, int, int) +DECL_VFUNC_DYN(void, DrawPrintText, hud_wchar *, int, int) DECL_VFUNC_DYN(void, GetScreenSize, int *, int *) DECL_VFUNC_DYN(int, GetFontTall, struct handlewrap) DECL_VFUNC_DYN(int, GetCharacterWidth, struct handlewrap, int) +DECL_VFUNC_DYN(int, GetTextSize, struct handlewrap, const hud_wchar *, + int *, int *) // vgui::Panel DECL_VFUNC_DYN(void, SetPaintEnabled, bool) @@ -92,7 +96,10 @@ static void *matsurf, *toolspanel, *scheme; typedef void (*VCALLCONV Paint_func)(void *); static Paint_func orig_Paint; void VCALLCONV hook_Paint(void *this) { - if (this == toolspanel) EMIT_HudPaint(); + // hopefully a smart branch predictor can figure this out - but we still + // want it to be the "slow" path, so that every other path does *not* need a + // prediction record. or.. I dunno, in theory that does make sense. shrug. + if_cold (this == toolspanel) EMIT_HudPaint(); orig_Paint(this); } @@ -117,7 +124,7 @@ void hud_drawpolyline(int *x, int *y, int npoints, struct rgba colour) { DrawPolyLine(matsurf, x, y, npoints); } -void hud_drawtext(ulong font, int x, int y, struct rgba colour, ushort *str, +void hud_drawtext(ulong font, int x, int y, struct rgba colour, hud_wchar *str, int len) { DrawSetTextFont(matsurf, (struct handlewrap){font}); DrawSetTextPos(matsurf, x, y); @@ -133,10 +140,14 @@ int hud_fontheight(ulong font) { return GetFontTall(matsurf, (struct handlewrap){font}); } -int hud_charwidth(ulong font, int ch) { +int hud_charwidth(ulong font, hud_wchar ch) { return GetCharacterWidth(matsurf, (struct handlewrap){font}, ch); } +void hud_textsize(ulong font, const ushort *s, int *width, int *height) { + GetTextSize(matsurf, (struct handlewrap){font}, s, width, height); +} + static bool find_toolspanel(void *enginevgui) { const uchar *insns = (const uchar *)VFUNC(enginevgui, GetPanel); for (const uchar *p = insns; p - insns < 16;) { @@ -156,21 +167,21 @@ static bool find_toolspanel(void *enginevgui) { INIT { matsurf = factory_engine("MatSystemSurface006", 0); - if (!matsurf) { + if_cold (!matsurf) { errmsg_errorx("couldn't get MatSystemSurface006 interface"); return false; } void *schememgr = factory_engine("VGUI_Scheme010", 0); - if (!schememgr) { + if_cold (!schememgr) { errmsg_errorx("couldn't get VGUI_Scheme010 interface"); return false; } - if (!find_toolspanel(vgui)) { + if_cold (!find_toolspanel(vgui)) { errmsg_errorx("couldn't find engine tools panel"); return false; } void **vtable = *(void ***)toolspanel; - if (!os_mprot(vtable + vtidx_Paint, sizeof(void *), + if_cold (!os_mprot(vtable + vtidx_Paint, sizeof(void *), PAGE_READWRITE)) { errmsg_errorsys("couldn't make virtual table writable"); return false; @@ -184,8 +195,8 @@ INIT { } END { - // don't unhook toolspanel if exiting, it's already long gone! - if (sst_userunloaded) { + // don't unhook toolspanel if exiting: it's already long gone! + if_cold (sst_userunloaded) { unhook_vtable(*(void ***)toolspanel, vtidx_Paint, (void *)orig_Paint); SetPaintEnabled(toolspanel, false); } @@ -1,5 +1,6 @@ /* * Copyright © 2022 Matthew Wozniak <sirtomato999@gmail.com> + * 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 @@ -21,6 +22,13 @@ #include "engineapi.h" #include "intdefs.h" +// ugh! +#ifdef _WIN32 +typedef ushort hud_wchar; +#else +typedef int hud_wchar; +#endif + /* * Emitted when the game HUD is being drawn. Allows features to draw their own * additional overlays atop the game's standard HUD. @@ -57,16 +65,19 @@ void hud_drawline(int x0, int y0, int x1, int y1, struct rgba colour); void hud_drawpolyline(int *xs, int *ys, int npoints, struct rgba colour); /* Draws text using a given font handle. */ -void hud_drawtext(ulong font, int x, int y, struct rgba colour, ushort *str, +void hud_drawtext(ulong font, int x, int y, struct rgba colour, hud_wchar *str, int len); -/* Returns the width and height of the game window in pixels. */ +/* Gets the width and height of the game window in pixels. */ void hud_screensize(int *width, int *height); /* Returns the height of a font, in pixels. */ int hud_fontheight(ulong font); /* Returns the width of a font character, in pixels. */ -int hud_getcharwidth(ulong font, int ch); +int hud_charwidth(ulong font, hud_wchar ch); + +/* Gets the width and height of string s, in pixels, using the given font. */ +void hud_textsize(ulong font, const ushort *s, int *width, int *height); #endif diff --git a/src/inputhud.c b/src/inputhud.c new file mode 100644 index 0000000..c4f7342 --- /dev/null +++ b/src/inputhud.c @@ -0,0 +1,443 @@ +/* + * Copyright © 2022 Matthew Wozniak <sirtomato999@gmail.com> + * Copyright © 2022 Willian Henrique <wsimanbrazil@yahoo.com.br> + * 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 <math.h> + +#include "con_.h" +#include "engineapi.h" +#include "event.h" +#include "errmsg.h" +#include "gamedata.h" +#include "gametype.h" +#include "hexcolour.h" +#include "hook.h" +#include "hud.h" +#include "intdefs.h" +#include "feature.h" +#include "mem.h" +#include "vcall.h" +#include "x86.h" +#include "x86util.h" + +FEATURE("button input HUD") +REQUIRE_GAMEDATA(vtidx_CreateMove) +REQUIRE_GAMEDATA(vtidx_DecodeUserCmdFromBuffer) +REQUIRE_GAMEDATA(vtidx_GetUserCmd) +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, + "Input HUD default key background colour (RGBA hex)", "4040408C", + CON_ARCHIVE | CON_HIDDEN) +DEF_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, + "Input HUD x position (fraction between screen left and right)", + 0.02, 0, 1, CON_ARCHIVE | CON_HIDDEN) +DEF_CVAR_MINMAX(sst_inputhud_y, + "Input HUD y position (fraction between screen top and bottom)", + 0.95, 0, 1, CON_ARCHIVE | CON_HIDDEN) + +static void *input; +static int heldbuttons = 0, tappedbuttons = 0; + +static struct rgba colours[3] = { + {64, 64, 64, 140}, + {16, 16, 16, 200}, + {240, 240, 240, 255} +}; + +static void colourcb(struct con_var *v) { + if (v == sst_inputhud_bgcolour_normal) { + hexcolour_rgba(colours[0].bytes, con_getvarstr(v)); + } + else if (v == sst_inputhud_bgcolour_pressed) { + hexcolour_rgba(colours[1].bytes, con_getvarstr(v)); + } + else /* v == sst_inputhud_fg */ { + hexcolour_rgba(colours[2].bytes, con_getvarstr(v)); + } +} + +struct CUserCmd { + void **vtable; + int cmd, tick; + struct vec3f angles; + float fmove, smove, umove; + int buttons; + char impulse; + int weaponselect, weaponsubtype; + int rngseed; + short mousedx, mousedy; + // client only: + bool predicted; + struct CUtlVector *entgroundcontact; +}; + +#define vtidx_GetUserCmd_l4dbased vtidx_GetUserCmd +DECL_VFUNC_DYN(struct CUserCmd *, GetUserCmd, int) +DECL_VFUNC_DYN(struct CUserCmd *, GetUserCmd_l4dbased, int, int) + +typedef void (*VCALLCONV CreateMove_func)(void *, int, float, bool); +static CreateMove_func orig_CreateMove; +static void VCALLCONV hook_CreateMove(void *this, int seq, float ft, + bool active) { + orig_CreateMove(this, seq, ft, active); + struct CUserCmd *cmd = GetUserCmd(this, seq); + // trick: to ensure every input (including scroll wheel) is displayed for at + // least a frame, even at sub-tickrate framerates, we accumulate tapped + // buttons with bitwise or. once these are drawn, tappedbuttons is cleared, + // but heldbuttons maintains its state, so stuff doesn't flicker constantly + if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; } +} +// basically a dupe, but calling the other version of GetUserCmd +static void VCALLCONV hook_CreateMove_l4dbased(void *this, int seq, float ft, + bool active) { + orig_CreateMove(this, seq, ft, active); + struct CUserCmd *cmd = GetUserCmd_l4dbased(this, -1, seq); + if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; } +} + +typedef void (*VCALLCONV DecodeUserCmdFromBuffer_func)(void *, void *, int); +typedef void (*VCALLCONV DecodeUserCmdFromBuffer_l4dbased_func)(void *, int, + void *, int); +static union { + DecodeUserCmdFromBuffer_func prel4d; + DecodeUserCmdFromBuffer_l4dbased_func l4dbased; +} _orig_DecodeUserCmdFromBuffer; +#define orig_DecodeUserCmdFromBuffer _orig_DecodeUserCmdFromBuffer.prel4d +#define orig_DecodeUserCmdFromBuffer_l4dbased \ + _orig_DecodeUserCmdFromBuffer.l4dbased +static void VCALLCONV hook_DecodeUserCmdFromBuffer(void *this, void *reader, + int seq) { + orig_DecodeUserCmdFromBuffer(this, reader, seq); + struct CUserCmd *cmd = GetUserCmd(this, seq); + if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; } +} +static void VCALLCONV hook_DecodeUserCmdFromBuffer_l4dbased(void *this, + int slot, void *reader, int seq) { + orig_DecodeUserCmdFromBuffer_l4dbased(this, slot, reader, seq); + struct CUserCmd *cmd = GetUserCmd_l4dbased(this, slot, seq); + if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; } +} + +static inline int bsf(uint x) { + // this should generate xor <ret>, <ret>; bsfl <ret>, <x>. + // doing a straight bsf (e.g. via BitScanForward or __builtin_ctz) creates + // a false dependency on many CPUs, which compilers don't understand somehow + int ret = 0; +#if defined(__GNUC__) || defined(__clang__) + __asm__ volatile ( + "bsfl %1, %0\n" + : "+r" (ret) + : "r" (x) + ); + return ret; +#else +#error need some sort of inline asm, or a non-broken(!) bitscan intrinsic +#endif +} + +// IMPORTANT: these things must all match the button order in engineapi.h +static const struct { + hud_wchar *s; + int len; +} text[] = { + /* IN_ATTACK */ {L"Pri", 3}, + /* IN_JUMP */ {L"Jump", 4}, + /* IN_DUCK */ {L"Duck", 4}, + /* IN_FORWARD */ {L"Fwd", 3}, + /* IN_BACK */ {L"Back", 4}, + /* IN_USE */ {L"Use", 3}, + /* IN_CANCEL */ {0}, + /* IN_LEFT */ {L"LTurn", 5}, + /* IN_RIGHT */ {L"RTurn", 5}, + /* IN_MOVELEFT */ {L"Left", 4}, + /* IN_MOVERIGHT */ {L"Right", 5}, + /* IN_ATTACK2 */ {L"Sec", 3}, + /* IN_RUN */ {0}, + /* IN_RELOAD */ {L"Rld", 3}, + /* IN_ALT1 */ {0}, + /* IN_ALT2 */ {0}, + /* IN_SCORE */ {0}, + /* IN_SPEED */ {L"Speed", 5}, + /* IN_WALK */ {L"Walk", 4}, + /* IN_ZOOM */ {L"Zoom", 4} + // ignoring the rest +}; + +struct layout { + int mask; + schar w, h; + struct { schar x, y; } pos[20]; // XXX: should make flexible??? +}; + +// input layouts (since some games don't use all input bits) {{{ + +static const struct layout layout_hl2 = { + IN_ATTACK | IN_JUMP | IN_DUCK | IN_FORWARD | IN_BACK | IN_USE | IN_LEFT | + IN_RIGHT | IN_MOVELEFT | IN_MOVERIGHT | IN_ATTACK2 | IN_RELOAD | IN_SPEED | + IN_WALK | IN_ZOOM, + 15, 6, + { + // F 1 2 + // L B R U R Z + // W S D J l r + /* IN_ATTACK */ {10, 0}, /* IN_JUMP */ { 6, 4}, + /* IN_DUCK */ { 4, 4}, /* IN_FORWARD */ { 3, 0}, + /* IN_BACK */ { 3, 2}, /* IN_USE */ { 9, 2}, + /* IN_CANCEL */ {0}, /* IN_LEFT */ {10, 4}, + /* IN_RIGHT */ {12, 4}, /* IN_MOVELEFT */ { 1, 2}, + /* IN_MOVERIGHT */ { 5, 2}, /* IN_ATTACK2 */ {12, 0}, + /* IN_RUN */ {0}, /* IN_RELOAD */ {11, 2}, + /* IN_ALT1 */ {0}, /* IN_ALT2 */ {0}, + /* IN_SCORE */ {0}, /* IN_SPEED */ { 2, 4}, + /* IN_WALK */ { 0, 4}, /* IN_ZOOM */ {13, 2} + } +}; + +static const struct layout layout_portal1 = { + IN_ATTACK | IN_JUMP | IN_DUCK | IN_FORWARD | IN_BACK | IN_USE | IN_LEFT | + IN_RIGHT | IN_MOVELEFT | IN_MOVERIGHT | IN_ATTACK2, + 11, 6, + { + // F 1 2 + // L B R U + // D J l r + /* IN_ATTACK */ {7, 0}, /* IN_JUMP */ {3, 4}, + /* IN_DUCK */ {1, 4}, /* IN_FORWARD */ {2, 0}, + /* IN_BACK */ {2, 2}, /* IN_USE */ {8, 2}, + /* IN_CANCEL */ {0}, /* IN_LEFT */ {7, 4}, + /* IN_RIGHT */ {9, 4}, /* IN_MOVELEFT */ {0, 2}, + /* IN_MOVERIGHT */ {4, 2}, /* IN_ATTACK2 */ {9, 0} + } +}; + +// TODO(compat): add portal2 layout once there's hud gamedata for portal 2 +//static const struct layout layout_portal2 = { +// IN_ATTACK | IN_JUMP | IN_DUCK | IN_FORWARD | IN_BACK | IN_USE | IN_LEFT | +// IN_RIGHT | IN_MOVELEFT | IN_MOVERIGHT | IN_ATTACK2 | IN_ZOOM, +// 11, 6, +// { +// // F 1 2 +// // L B R U Z +// // D J l r +// /* IN_ATTACK */ {7, 0}, /* IN_JUMP */ {3, 4}, +// /* IN_DUCK */ {1, 4}, /* IN_FORWARD */ {2, 0}, +// /* IN_BACK */ {2, 2}, /* IN_USE */ {7, 2}, +// /* IN_CANCEL */ {0}, /* IN_LEFT */ {7, 4}, +// /* IN_RIGHT */ {9, 4}, /* IN_MOVELEFT */ {0, 2}, +// /* IN_MOVERIGHT */ {4, 2}, /* IN_ATTACK2 */ {9, 0}, +// /* IN_RUN */ {0}, /* IN_RELOAD */ {0}, +// /* IN_ALT1 */ {0}, /* IN_ALT2 */ {0}, +// /* IN_SCORE */ {0}, /* IN_SPEED */ {0}, +// /* IN_WALK */ {0}, /* IN_ZOOM */ {9, 2} +// } +//}; + +static const struct layout layout_l4d = { + IN_ATTACK | IN_JUMP | IN_DUCK | IN_FORWARD | IN_BACK | IN_USE | IN_LEFT | + IN_RIGHT | IN_MOVELEFT | IN_MOVERIGHT | IN_ATTACK2 | IN_SPEED | IN_ZOOM, + 11, 6, + { + // F 1 2 + // L B R U Z + // W D J l r + /* IN_ATTACK */ {7, 0}, /* IN_JUMP */ {4, 4}, + /* IN_DUCK */ {2, 4}, /* IN_FORWARD */ {2, 0}, + /* IN_BACK */ {2, 2}, /* IN_USE */ {7, 2}, + /* IN_CANCEL */ {0}, /* IN_LEFT */ {7, 4}, + /* IN_RIGHT */ {9, 4}, /* IN_MOVELEFT */ {0, 2}, + /* IN_MOVERIGHT */ {4, 2}, /* IN_ATTACK2 */ {9, 0}, + /* IN_RUN */ {0}, /* IN_RELOAD */ {0}, + /* IN_ALT1 */ {0}, /* IN_ALT2 */ {0}, + /* IN_SCORE */ {0}, /* IN_SPEED */ {0, 4}, + /* IN_WALK */ {0}, /* IN_ZOOM */ {9, 2} + } +}; + +// }}} + +static const struct layout *layout = &layout_hl2; + +static const char *const fontnames[] = { + "DebugFixedSmall", + "HudSelectionText", + "CommentaryDefault", + "DefaultVerySmall", + "DefaultSmall", + "Default" +}; +static struct { ulong h; int sz; } fonts[countof(fontnames)]; + +HANDLE_EVENT(HudPaint, void) { + if (!con_getvari(sst_inputhud)) return; + int screenw, screenh; + hud_screensize(&screenw, &screenh); + int basesz = screenw > screenh ? screenw : screenh; + int boxsz = ceilf(basesz * 0.025f); + if (boxsz < 24) boxsz = 24; + boxsz *= con_getvarf(sst_inputhud_scale); + int idealfontsz = boxsz - 8; // NOTE: this is overall text width, see INIT + ulong font = 0; int fontsz = 0; + // get the biggest font that'll fit the box + // XXX: can/should we avoid doing this every frame? + for (int i = 0; i < countof(fonts); ++i) { + // XXX: fonts aren't sorted... should we bother? + if_cold (!fonts[i].h) continue; + if (fonts[i].sz < fontsz) continue; + if (fonts[i].sz <= idealfontsz) font = fonts[i].h; + //else break; // not sorted + } + int gap = (boxsz | 32) >> 5; // minimum 1 pixel gap + int w = (boxsz + gap) * layout->w / 2 - gap; + int h = (boxsz + gap) * layout->h / 2 - gap; + int basex = roundf(con_getvarf(sst_inputhud_x) * (screenw - w)); + int basey = roundf(con_getvarf(sst_inputhud_y) * (screenh - h)); + int buttons = heldbuttons | tappedbuttons; + for (int mask = layout->mask, bitidx, bit; mask; mask ^= bit) { + bitidx = bsf(mask); bit = 1 << bitidx; + // divide sizes by 2 here to allow in-between positioning + int x = basex + layout->pos[bitidx].x * (boxsz + gap) / 2; + int y = basey + layout->pos[bitidx].y * (boxsz + gap) / 2; + hud_drawrect(x, y, x + boxsz, y + boxsz, + colours[!!(buttons & bit)], true); + if_hot (font) { + int tw, th; + hud_textsize(font, text[bitidx].s, &tw, &th); + hud_drawtext(font, x + (boxsz - tw) / 2, y + (boxsz - th) / 2, + colours[2], text[bitidx].s, text[bitidx].len); + } + } + tappedbuttons = 0; +} + +// find the CInput "input" global +static inline bool find_input(void* vclient) { +#ifdef _WIN32 + // the only CHLClient::DecodeUserCmdFromBuffer() does is call a virtual + // function, so find its thisptr being loaded into ECX + void* decodeusercmd = + (*(void***)vclient)[vtidx_VClient_DecodeUserCmdFromBuffer]; + for (uchar *p = (uchar *)decodeusercmd; p - (uchar *)decodeusercmd < 32;) { + if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) { + void **indirect = mem_loadptr(p + 2); + input = *indirect; + return true; + } + NEXT_INSN(p, "input object"); + } +#else +#warning TODO(linux): implement linux equivalent (see demorec.c) +#endif + return false; +} + +INIT { + void *vclient; + if (!(vclient = factory_client("VClient015", 0)) && + !(vclient = factory_client("VClient016", 0)) && + !(vclient = factory_client("VClient017", 0))) { + errmsg_errorx("couldn't get client interface"); + return false; + } + if (!find_input(vclient)) { + errmsg_errorx("couldn't find input global"); + return false; + } + for (int i = 0; i < countof(fontnames); ++i) { + fonts[i].h = hud_getfont(fontnames[i], true); + if (!fonts[i].h) { + errmsg_warnx("couldn't get \"%s\" font", fontnames[i]); + } + else { + int dummy; + // use (roughly) the widest string as a reference for what will fit + hud_textsize(fonts[i].h, L"Speed", &fonts[i].sz, &dummy); + } + } + void **vtable = mem_loadptr(input); + // just unprotect the first few pointers (GetUserCmd is 8) + if (!os_mprot(vtable, sizeof(void *) * 8, PAGE_READWRITE)) { + errmsg_errorsys("couldn't make virtual table writable"); + return false; + } + if (GAMETYPE_MATCHES(L4Dbased)) { + orig_CreateMove = (CreateMove_func)hook_vtable(vtable, vtidx_CreateMove, + (void *)&hook_CreateMove_l4dbased); + orig_DecodeUserCmdFromBuffer = (DecodeUserCmdFromBuffer_func)hook_vtable( + vtable, vtidx_DecodeUserCmdFromBuffer, + (void *)&hook_DecodeUserCmdFromBuffer_l4dbased); + } + else { + orig_CreateMove = (CreateMove_func)hook_vtable(vtable, vtidx_CreateMove, + (void *)&hook_CreateMove); + orig_DecodeUserCmdFromBuffer = (DecodeUserCmdFromBuffer_func)hook_vtable( + vtable, vtidx_DecodeUserCmdFromBuffer, + (void *)&hook_DecodeUserCmdFromBuffer); + } + + if (GAMETYPE_MATCHES(Portal1)) layout = &layout_portal1; + //else if (GAMETYPE_MATCHES(Portal2)) layout = &layout_portal2; + 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 + if (GAMETYPE_MATCHES(L4D)) { + sst_inputhud_y->defaultval = "0.82"; + con_setvarstr(sst_inputhud_y, "0.82"); + } + else if (GAMETYPE_MATCHES(HL2series)) { + sst_inputhud_y->defaultval = "0.75"; + con_setvarstr(sst_inputhud_y, "0.75"); + } + + return true; +} + +END { + void **vtable = mem_loadptr(input); + unhook_vtable(vtable, vtidx_CreateMove, (void *)orig_CreateMove); + // N.B.: since the orig_ function is in a union, we don't have to worry + // about which version we're unhooking + unhook_vtable(vtable, vtidx_DecodeUserCmdFromBuffer, + (void *)orig_DecodeUserCmdFromBuffer); +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 fdm=marker diff --git a/src/kv.c b/src/kv.c deleted file mode 100644 index 019d017..0000000 --- a/src/kv.c +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright © 2022 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 "intdefs.h" -#include "kv.h" -#include "unreachable.h" - -#define EOF -1 - -// parser states, implemented by STATE() macros in kv_parser_feed() below. -// needs to be kept in sync! -enum { - ok, ok_slash, - ident, ident_slash, identq, - sep, sep_slash, condsep, condsep_slash, - cond_prefix, - val, val_slash, valq, afterval, afterval_slash, - cond_suffix -}; - -bool kv_parser_feed(struct kv_parser *this, const char *in, uint sz, - kv_parser_cb cb, void *ctxt) { - const char *p = in; - short c; - - // slight hack, makes init more convenient (just {0}) - if (!this->line) this->line = 1; - if (!this->outp) this->outp = this->tokbuf; - - // this is a big ol' blob of ugly state machine macro spaghetti - too bad! - #define INCCOL() (*p == '\n' ? (++this->line, this->col = 0) : ++this->col) - #define READ() (p == in + sz ? EOF : (INCCOL(), *p++)) - #define ERROR(s) do { \ - this->errmsg = s; \ - return false; \ - } while (0) - #define OUT(c) do { \ - if (this->outp - this->tokbuf == KV_TOKEN_MAX) { \ - ERROR("token unreasonably large!"); \ - } \ - *this->outp++ = (c); \ - } while (0) - #define CASE_WS case ' ': case '\t': case '\n': case '\r' - // note: multi-eval - #define IS_WS(c) ((c) == ' ' || (c) == '\t' || (c) == '\n' || (c) == '\r') - #define STATE(s) case s: s - #define HANDLE_EOF() do { case EOF: return true; } while (0) - #define SKIP_COMMENT(next) do { \ - this->state = next; \ - this->incomment = true; \ - goto start; \ - } while (0) - #define GOTO(s) do { this->state = s; goto s; } while (0) - #define CB(type) do { \ - cb(type, this->tokbuf, this->outp - this->tokbuf, ctxt); \ - this->outp = this->tokbuf; \ - } while (0) - // prefix and suffix conditions are more or less the same, just in different - // contexts, because very good syntax yes. - #define CONDSTATE(name, type, next) do { \ - STATE(name): \ - switch (c = READ()) { \ - HANDLE_EOF(); \ - CASE_WS: ERROR("unexpected whitespace in conditional"); \ - case '[': ERROR("unexpected opening bracket in conditional"); \ - case '{': case '}': ERROR("unexpected brace in conditional"); \ - case '/': ERROR("unexpected slash in conditional"); \ - case ']': CB(type); GOTO(next); \ - default: OUT(c); goto name; \ - } \ - } while (0) - -start: // special spaghetti so we don't have a million different comment states - if (this->incomment) while ((c = READ()) != '\n') if (c == EOF) return true; - this->incomment = false; - -switch (this->state) { - -STATE(ok): - c = READ(); -ident_postread: - switch (c) { - HANDLE_EOF(); - CASE_WS: goto ok; - case '#': ERROR("kv macros not supported"); - case '{': ERROR("unexpected control character"); - case '}': - if (!this->nestlvl) ERROR("too many closing braces"); - --this->nestlvl; - char c_ = c; - cb(KV_NEST_END, &c_, 1, ctxt); - goto ok; - case '"': GOTO(identq); - case '/': GOTO(ok_slash); - case '[': case ']': ERROR("unexpected conditional bracket"); - default: GOTO(ident); - } - -STATE(ok_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(ok); - default: GOTO(ident); - } - -ident: - OUT(c); -case ident: // continue here - switch (c = READ()) { - HANDLE_EOF(); - case '{': - CB(KV_IDENT); - ++this->nestlvl; - char c_ = c; - cb(KV_NEST_START, &c_, 1, ctxt); - GOTO(ok); - // XXX: assuming [ is a token break; haven't checked Valve's code - case '[': CB(KV_IDENT); GOTO(cond_prefix); - case '}': ERROR("unexpected closing brace"); - case ']': ERROR("unexpected closing bracket"); - case '"': ERROR("unexpected quote mark"); - CASE_WS: CB(KV_IDENT); GOTO(sep); - case '/': GOTO(ident_slash); - default: goto ident; - } - -STATE(ident_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': CB(KV_IDENT); SKIP_COMMENT(sep); - default: OUT('/'); GOTO(ident); - } - -STATE(identq): - switch (c = READ()) { - HANDLE_EOF(); - case '"': CB(KV_IDENT_QUOTED); GOTO(sep); - default: OUT(c); goto identq; - } - -STATE(sep): - do c = READ(); while (IS_WS(c)); - switch (c) { - HANDLE_EOF(); - case '{':; - char c_ = c; - ++this->nestlvl; - cb(KV_NEST_START, &c_, 1, ctxt); - GOTO(ok); - case '[': GOTO(cond_prefix); - case '"': GOTO(valq); - case '}': ERROR("unexpected closing brace"); - case ']': ERROR("unexpected closing bracket"); - case '/': GOTO(sep_slash); - default: GOTO(val); - } - -STATE(sep_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(sep); - default: GOTO(val); - } - -CONDSTATE(cond_prefix, KV_COND_PREFIX, condsep); - -STATE(condsep): - do c = READ(); while (IS_WS(c)); - switch (c) { - HANDLE_EOF(); - case '{':; - char c_ = c; - ++this->nestlvl; - cb(KV_NEST_START, &c_, 1, ctxt); - GOTO(ok); - case '}': ERROR("unexpected closing brace"); - case '[': ERROR("unexpected opening bracket"); - case ']': ERROR("unexpected closing bracket"); - case '/': GOTO(condsep_slash); - // these conditions only go before braces because very good syntax - default: ERROR("unexpected string value after prefix condition"); - } - -STATE(condsep_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(condsep); - default: ERROR("unexpected string value after prefix condition"); - } - -val: - OUT(c); -case val: // continue here - switch (c = READ()) { - HANDLE_EOF(); - case '{': ERROR("unexpected opening brace"); - case ']': ERROR("unexpected closing bracket"); - case '"': ERROR("unexpected quotation mark"); - // might get [ or } with no whitespace - case '}': - CB(KV_VAL); - --this->nestlvl; - char c_ = c; - cb(KV_NEST_END, &c_, 1, ctxt); - GOTO(afterval); - case '[': CB(KV_VAL); GOTO(cond_suffix); - CASE_WS: CB(KV_VAL); GOTO(afterval); - case '/': GOTO(val_slash); - default: goto val; - } - -STATE(val_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': CB(KV_VAL); SKIP_COMMENT(afterval); - default: OUT('/'); GOTO(val); - } - -STATE(valq): - switch (c = READ()) { - HANDLE_EOF(); - case '"': CB(KV_VAL_QUOTED); GOTO(afterval); - default: OUT(c); goto valq; - } - -STATE(afterval): - switch (c = READ()) { - HANDLE_EOF(); - CASE_WS: goto afterval; - case '[': GOTO(cond_suffix); - case '/': GOTO(afterval_slash); - // mildly dumb hack: if no conditional, we can just use the regular - // starting state handler to get next transition correct - just avoid - // double-reading the character - default: goto ident_postread; - } - -STATE(afterval_slash): - switch (c = READ()) { - HANDLE_EOF(); - case '/': SKIP_COMMENT(afterval); - default: GOTO(ident); - } - -CONDSTATE(cond_suffix, KV_COND_SUFFIX, ok); - -} - - #undef CONDSTATE - #undef CB - #undef GOTO - #undef SKIP_COMMENT - #undef HANDLE_EOF - #undef STATE - #undef IS_WS - #undef CASE_WS - #undef OUT - #undef ERROR - #undef READ - #undef INCCOL - - unreachable; // pretty sure! -} - -bool kv_parser_done(struct kv_parser *this) { - if (this->state != ok && this->state != afterval) { - this->errmsg = "unexpected end of input"; - return false; - } - if (this->nestlvl != 0) { - this->errmsg = "unterminated object (unbalanced braces)"; - return false; - } - return true; -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/kv.h b/src/kv.h deleted file mode 100644 index 63a9146..0000000 --- a/src/kv.h +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright © 2022 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_KV_H -#define INC_KV_H - -#include "intdefs.h" - -/* - * Maximum length of a single token. Since this code is trying to avoid dynamic - * memory allocations, this arbitrary limit is chosen to accomodate all known - * "reasonable" tokens likely to come in any real files, probably. - */ -#define KV_TOKEN_MAX 512 - -/* - * Contains all the state associated with parsing (lexing?) a KeyValues file. - * Should be zeroed out prior to the first call (initialise with `= {0};`). - */ -struct kv_parser { - ushort line, col; /* the current line and column in the text */ - char state : 7; /* internal, shouldn't usually be touched directly */ - bool incomment : 1; /* internal */ - ushort nestlvl; /* internal */ - const char *errmsg; /* the error message, *IF* parsing just failed */ - - // trying to avoid dynamic allocations - valve's own parser seems to have - // a similar limit as well and our use case doesn't really need to worry - // about stupid massive values, so it's fine - char *outp; - char tokbuf[KV_TOKEN_MAX]; -}; - -/* - * These are the tokens that can be received by a kv_parser_cb (below). - * The x-macro and string descriptions are given to allow for easy debug - * stringification. Note that this "parser" is really just lexing out these - * tokens - handling the actual structure of the file should be done in the - * callback. This is so that data can be streamed rather than all read into - * memory at once. - */ -#define KV_TOKENS(X) \ - X(KV_IDENT, "ident") \ - X(KV_IDENT_QUOTED, "quoted-ident") \ - X(KV_VAL, "value") \ - X(KV_VAL_QUOTED, "quoted-value") \ - X(KV_COND_PREFIX, "cond-prefix") \ - X(KV_COND_SUFFIX, "cond-suffix") \ - X(KV_NEST_START, "object-start") \ - X(KV_NEST_END, "object-end") - -#define _ENUM(s, ignore) s, -enum kv_token { KV_TOKENS(_ENUM) }; -#undef _ENUM - -typedef void (*kv_parser_cb)(enum kv_token type, const char *p, uint len, - void *ctxt); - -/* - * Feed a block of text into the lexer. This would usually be a block of data - * read in from a file. - * - * The lexer is reentrant and can be fed arbitrarily sized blocks of data at a - * time. The function may return early in the event of an error; a return value - * of false indicates thaat this has happened, otherwise true is returned. - * - * In the event of an error, the errmsg, line and col fields of the parser - * struct can be used for diagnostics. - */ -bool kv_parser_feed(struct kv_parser *this, const char *in, uint sz, - kv_parser_cb cb, void *ctxt); - -/* - * This indicates that parsing is done; if this is called at an unexpected time, - * a parsing error will result; this is indicated in the return value as with - * kv_parser_feed. - */ -bool kv_parser_done(struct kv_parser *this); - -#endif - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/kvsys.c b/src/kvsys.c index fc10b93..31652f3 100644 --- a/src/kvsys.c +++ b/src/kvsys.c @@ -24,6 +24,7 @@ #include "gametype.h" #include "hook.h" #include "kvsys.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "vcall.h" @@ -31,7 +32,7 @@ FEATURE() -IMPORT void *KeyValuesSystem(void); // vstlib symbol +void *KeyValuesSystem(void); // vstlib symbol static void *kvs; static int vtidx_GetSymbolForString = 3, vtidx_GetStringForSymbol = 4; static bool iskvv2 = false; @@ -101,7 +102,7 @@ INIT { if (GAMETYPE_MATCHES(L4D2x)) { void **kvsvt = mem_loadptr(kvs); detectabichange(kvsvt); - if (!os_mprot(kvsvt + vtidx_GetStringForSymbol, sizeof(void *), + if_cold (!os_mprot(kvsvt + vtidx_GetStringForSymbol, sizeof(void *), PAGE_READWRITE)) { errmsg_warnx("couldn't make KeyValuesSystem vtable writable"); errmsg_note("won't be able to prevent any nag messages"); diff --git a/src/l4dmm.c b/src/l4dmm.c index 2439d6b..05a03a7 100644 --- a/src/l4dmm.c +++ b/src/l4dmm.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -22,6 +22,7 @@ #include "gamedata.h" #include "gametype.h" #include "kvsys.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "vcall.h" @@ -62,7 +63,7 @@ const char *l4dmm_curcampaign(void) { if (!matchfwk) { // we must have oldmmiface, then struct contextval *ctxt = unknown_contextlookup(oldmmiface, "CONTEXT_L4D_CAMPAIGN"); - if (!ctxt) return 0; + if_cold (!ctxt) return 0; // HACK: since this context symbol stuff was the best that was found for // this old MM interface, just map things back to their names manually. // bit stupid, but it gets the (rather difficult) job done @@ -76,18 +77,18 @@ const char *l4dmm_curcampaign(void) { #endif void *ctrlr = GetMatchNetworkMsgController(matchfwk); struct KeyValues *kv = GetActiveGameServerDetails(ctrlr, 0); - if (!kv) return 0; // not in server, probably + if_cold (!kv) return 0; // not in server, probably const char *ret = 0; struct KeyValues *subkey = kvsys_getsubkey(kv, sym_game); - if (subkey) subkey = kvsys_getsubkey(subkey, sym_campaign); - if (subkey) ret = kvsys_getstrval(subkey); - if (ret) { + if_hot (subkey) subkey = kvsys_getsubkey(subkey, sym_campaign); + if_hot (subkey) ret = kvsys_getstrval(subkey); + if_hot (ret) { // ugh, we have to free all the memory allocated by the engine, so copy // this glorified global state to a buffer so the caller doesn't have to // deal with freeing. this necessitates a length cap but it's hopefully // reasonable... - int len = strlen(ret); - if (len > sizeof(campaignbuf) - 1) ret = 0; + usize len = strlen(ret); + if_cold (len > sizeof(campaignbuf) - 1) ret = 0; else ret = memcpy(campaignbuf, ret, len + 1); } kvsys_free(kv); @@ -112,8 +113,8 @@ bool l4dmm_firstmap(void) { if (!kv) return false; int chapter = 0; struct KeyValues *subkey = kvsys_getsubkey(kv, sym_game); - if (subkey) subkey = kvsys_getsubkey(subkey, sym_chapter); - if (subkey) chapter = subkey->ival; + if_hot (subkey) subkey = kvsys_getsubkey(subkey, sym_chapter); + if_hot (subkey) chapter = subkey->ival; kvsys_free(kv); return chapter == 1; } @@ -122,12 +123,12 @@ INIT { void *mmlib = os_dlhandle(OS_LIT("matchmaking") OS_LIT(OS_DLSUFFIX)); if (mmlib) { ifacefactory factory = (ifacefactory)os_dlsym(mmlib, "CreateInterface"); - if (!factory) { + if_cold (!factory) { errmsg_errordl("couldn't get matchmaking interface factory"); return false; } matchfwk = factory("MATCHFRAMEWORK_001", 0); - if (!matchfwk) { + if_cold (!matchfwk) { errmsg_errorx("couldn't get IMatchFramework interface"); return false; } @@ -138,7 +139,7 @@ INIT { #ifdef _WIN32 // L4D1 has no Linux build, btw! else { oldmmiface = factory_engine("VENGINE_MATCHMAKING_VERSION001", 0); - if (!oldmmiface) { + if_cold (!oldmmiface) { errmsg_errorx("couldn't get IMatchmaking interface"); return false; } diff --git a/src/l4dreset.c b/src/l4dreset.c index 4a8792e..a53b987 100644 --- a/src/l4dreset.c +++ b/src/l4dreset.c @@ -31,6 +31,7 @@ #include "gameserver.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "l4dmm.h" #include "mem.h" #include "sst.h" @@ -84,14 +85,13 @@ static struct CVoteIssue *getissue(const char *textkey) { static inline void reset(void) { // reset the vote cooldowns if possible (will skip L4D1). only necessary on - // versions >2045 and on map 1, but it's easiest to do unconditionally - if (off_callerrecords != -1) { - // This is equivalent to CUtlVector::RemoveAll() as there's no - // destructors to call. The result as is if nobody had ever voted. - struct CUtlVector *recordvector = mem_offset(*votecontroller, - off_callerrecords); - recordvector->sz = 0; - } + // versions >2045 and on map 1, but it's easiest to do unconditionally. + // the way this is written will *hopefully* produce a nice neat lea+cmovne. + struct CUtlVector *recordvector = mem_offset(*votecontroller, + off_callerrecords); + // This is equivalent to CUtlVector::RemoveAll() as there's no + // destructors to call. The result as is if nobody had ever voted. + if_random (off_callerrecords != -1) recordvector->sz = 0; ExecuteCommand(getissue("#L4D_vote_restart_game")); } @@ -503,24 +503,24 @@ ok: // Director::Update calls UnfreezeTeam after the first jmp instruction INIT { struct con_cmd *cmd_listissues = con_findcmd("listissues"); - if (!cmd_listissues) { + if_cold (!cmd_listissues) { errmsg_errorx("couldn't find \"listissues\" command"); return false; } con_cmdcbv1 listissues_cb = con_getcmdcbv1(cmd_listissues); const uchar *nextinsns = find_votecontroller(listissues_cb); - if (!nextinsns) { + if_cold (!nextinsns) { errmsg_errorx("couldn't find vote controller variable"); return false; } - if (!find_voteissues(nextinsns)) { + if_cold (!find_voteissues(nextinsns)) { errmsg_errorx("couldn't find vote issues list offset\n"); return false; } void **vtable; #ifdef _WIN32 void *GameShutdown = (*(void ***)srvdll)[vtidx_GameShutdown]; - if (!find_TheDirector(GameShutdown)) { + if_cold (!find_TheDirector(GameShutdown)) { errmsg_errorx("couldn't find TheDirector variable"); return false; } @@ -532,7 +532,7 @@ INIT { if (GAMETYPE_MATCHES(L4D2)) { #endif vtable = mem_loadptr(director); - if (!os_mprot(vtable + vtidx_OnGameplayStart, sizeof(*vtable), + if_cold (!os_mprot(vtable + vtidx_OnGameplayStart, sizeof(*vtable), PAGE_READWRITE)) { errmsg_errorsys("couldn't make virtual table writable"); return false; @@ -543,7 +543,7 @@ INIT { } else /* L4D1 */ { void *GameFrame = (*(void ***)srvdll)[vtidx_GameFrame]; - if (!find_UnfreezeTeam(GameFrame)) { + if_cold (!find_UnfreezeTeam(GameFrame)) { errmsg_errorx("couldn't find UnfreezeTeam function"); return false; } @@ -556,16 +556,16 @@ INIT { // g_voteController is invalid if not running a server so get the // vtable by inspecting the ent factory code instead const struct CEntityFactory *factory = ent_getfactory("vote_controller"); - if (!factory) { + if_cold (!factory) { errmsg_errorx("couldn't find vote controller entity factory"); goto nocd; } vtable = ent_findvtable(factory, "CVoteController"); - if (!vtable) { + if_cold (!vtable) { errmsg_errorx("couldn't find CVoteController vtable"); goto nocd; } - if (!find_votecallers(vtable[vtidx_Spawn])) { + if_cold (!find_votecallers(vtable[vtidx_Spawn])) { errmsg_errorx("couldn't find vote callers list offset"); nocd: errmsg_note("resetting a first map will not clear vote cooldowns"); } diff --git a/src/l4dwarp.c b/src/l4dwarp.c index 24c3873..f508460 100644 --- a/src/l4dwarp.c +++ b/src/l4dwarp.c @@ -1,5 +1,6 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * Copyright © 2024 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 * purpose with or without fee is hereby granted, provided that the above @@ -17,6 +18,7 @@ #define _USE_MATH_DEFINES // ... windows. #include <math.h> +#include "clientcon.h" #include "con_.h" #include "engineapi.h" #include "errmsg.h" @@ -25,39 +27,310 @@ #include "gamedata.h" #include "gametype.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" +#include "trace.h" #include "vcall.h" +#include "x86.h" +#include "x86util.h" FEATURE("Left 4 Dead warp testing") +REQUIRE(clientcon) REQUIRE(ent) +REQUIRE(trace) REQUIRE_GAMEDATA(off_entpos) REQUIRE_GAMEDATA(off_eyeang) +REQUIRE_GAMEDATA(off_teamnum) REQUIRE_GAMEDATA(vtidx_Teleport) -DECL_VFUNC_DYN(void *, GetBaseEntity) DECL_VFUNC_DYN(void, Teleport, const struct vec3f */*pos*/, const struct vec3f */*pos*/, const struct vec3f */*vel*/) +DECL_VFUNC(const struct vec3f *, OBBMaxs, 2) -DEF_CCMD_HERE_UNREG(sst_l4d_testwarp, "Simulate a bot warping to you", - CON_SERVERSIDE | CON_CHEAT) { - struct edict *ed = ent_getedict(con_cmdclient + 1); - if (!ed) { errmsg_errorx("couldn't access player entity"); return; } - void *e = GetBaseEntity(ed->ent_unknown); // is this call required? - struct vec3f *org = mem_offset(e, off_entpos); - struct vec3f *ang = mem_offset(e, off_eyeang); +// IMPORTANT: padsz parameter is missing in L4D1, but since it's cdecl, we can +// still call it just the same (we always pass 0, so there's no difference). +typedef bool (*EntityPlacementTest_func)(void *ent, const struct vec3f *origin, + struct vec3f *out, bool drop, uint mask, void *filt, float padsz); +static EntityPlacementTest_func EntityPlacementTest; + +// Technically the warp uses a CTraceFilterSkipTeam, not a CTraceFilterSimple. +// That does, however, inherit from the simple filter and run some minor checks +// on top of it. I couldn't find a case where these checks actually mattered +// and, if needed, they could be easily reimplemented using the extra hit check +// (instead of hunting for the CTraceFilterSkipTeam vtable). +static struct CTraceFilterSimple { + void **vtable; + void *pass_ent; + int collision_group; + void * /* ShouldHitFunc_t */ extrahitcheck_func; + //int teamnum; // player's team number. member of CTraceFilterSkipTeam +} filter; + +typedef void (*VCALLCONV CTraceFilterSimple_ctor)( + struct CTraceFilterSimple *this, void *pass_ent, int collisiongroup, + void *extrahitcheck_func); + +// Trace mask for non-bot survivors. Constant in all L4D versions +#define PLAYERMASK 0x0201420B + +// debug overlay stuff, only used by sst_l4d_previewwarp +static void *dbgoverlay; +DECL_VFUNC_DYN(void, AddLineOverlay, const struct vec3f *, + const struct vec3f *, int, int, int, bool, float) +DECL_VFUNC_DYN(void, AddBoxOverlay2, const struct vec3f *, + const struct vec3f *, const struct vec3f *, const struct vec3f *, + const struct rgba *, const struct rgba *, float) + +static struct vec3f warptarget(void *ent) { + const struct vec3f *org = mem_offset(ent, off_entpos); + const struct vec3f *ang = mem_offset(ent, off_eyeang); // L4D idle warps go up to 10 units behind yaw, lessening based on pitch. float pitch = ang->x * M_PI / 180, yaw = ang->y * M_PI / 180; float shift = -10 * cos(pitch); - Teleport(e, &(struct vec3f){org->x + shift * cos(yaw), - org->y + shift * sin(yaw), org->z}, 0, &(struct vec3f){0, 0, 0}); + return (struct vec3f){ + org->x + shift * cos(yaw), + org->y + shift * sin(yaw), + org->z + }; +} + +DEF_CCMD_HERE_UNREG(sst_l4d_testwarp, "Simulate a bot warping to you " + "(specify \"staystuck\" to skip take-control simulation)", + CON_SERVERSIDE | CON_CHEAT) { + bool staystuck = false; + // TODO(autocomplete): suggest this argument + if (cmd->argc == 2 && !strcmp(cmd->argv[1], "staystuck")) { + staystuck = true; + } + else if (cmd->argc != 1) { + clientcon_reply("usage: sst_l4d_testwarp [staystuck]\n"); + return; + } + struct edict *ed = ent_getedict(con_cmdclient + 1); + if_cold (!ed || !ed->ent_unknown) { + errmsg_errorx("couldn't access player entity"); + return; + } + void *e = ed->ent_unknown; + if_cold (mem_loadu32(mem_offset(e, off_teamnum)) != 2) { + clientcon_msg(ed, "error: must be in the Survivor team"); + return; + } + filter.pass_ent = e; + struct vec3f stuckpos = warptarget(e); + struct vec3f finalpos; + if (staystuck || !EntityPlacementTest(e, &stuckpos, &finalpos, false, + PLAYERMASK, &filter, 0.0)) { + finalpos = stuckpos; + } + Teleport(e, &finalpos, 0, &(struct vec3f){0, 0, 0}); +} + +static const struct rgba + red_edge = {200, 0, 0, 100}, red_face = {220, 0, 0, 10}, + yellow_edge = {240, 200, 20, 100},// yellow_face = {240, 240, 20, 10}, + green_edge = {20, 210, 50, 100}, green_face = {49, 220, 30, 10}, + clear_face = {0, 0, 0, 0}, + orange_line = {255, 100, 0, 255}, cyan_line = {0, 255, 255, 255}; + +static const struct vec3f zerovec = {0}; + +static bool draw_testpos(struct vec3f start, struct vec3f testpos, + struct vec3f mins, struct vec3f maxs, bool needline) { + struct CGameTrace t = trace_hull(testpos, testpos, mins, maxs, PLAYERMASK, + &filter); + if (t.base.frac != 1.0f || t.base.allsolid || t.base.startsolid) { + AddBoxOverlay2(dbgoverlay, &testpos, &mins, &maxs, &zerovec, + &clear_face, &red_edge, 1000.0); + return needline; + } + AddBoxOverlay2(dbgoverlay, &testpos, &mins, &maxs, &zerovec, + &clear_face, &yellow_edge, 1000.0); + if (needline) { + t = trace_line(start, testpos, PLAYERMASK, &filter); + AddLineOverlay(dbgoverlay, &start, &t.base.endpos, + orange_line.r, orange_line.g, orange_line.b, true, 1000.0); + // current knowledge indicates that this should never happen, but it's + // good to issue a warning if the code ever happens to be wrong + if_cold (t.base.frac == 1.0 && !t.base.allsolid && !t.base.startsolid) { + // XXX: should this be sent to client console? more effort... + errmsg_warnx("false positive test position %.3f %.3f %.3f", + testpos.x, testpos.y, testpos.z); + return true; + } + } + return false; +} + +DEF_CCMD_HERE_UNREG(sst_l4d_previewwarp, "Visualise bot warp unstuck logic " + "(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"); + return; + } + if (con_cmdclient != 0) { + clientcon_msg(ed, "error: only the server host can see visualisations"); + return; + } + void *e = ed->ent_unknown; + if_cold (mem_loadu32(mem_offset(e, off_teamnum)) != 2) { + clientcon_msg(ed, "error: must be in the Survivor team"); + return; + } + filter.pass_ent = e; + struct vec3f stuckpos = warptarget(e); + struct vec3f finalpos; + // we use the real EntityPlacementTest and then work backwards to figure out + // what to draw. that way there's very little room for missed edge cases + bool success = EntityPlacementTest(e, &stuckpos, &finalpos, false, + PLAYERMASK, &filter, 0.0); + struct vec3f mins = {-16.0f, -16.0f, 0.0f}; + struct vec3f maxs = *OBBMaxs(mem_offset(ed->ent_unknown, off_collision)); + struct vec3f step = {maxs.x - mins.x, maxs.y - mins.y, maxs.z - mins.z}; + struct failranges { struct { int neg, pos; } x, y, z; } ranges; + if (success) { + AddBoxOverlay2(dbgoverlay, &finalpos, &mins, &maxs, &zerovec, + &green_face, &green_edge, 1000.0); + if (finalpos.x != stuckpos.x) { + float iters = roundf((finalpos.x - stuckpos.x) / step.x); + int isneg = iters < 0; + iters = fabs(iters); + ranges = (struct failranges){ + {-iters + isneg, iters - 1}, + {-iters + 1, iters - 1}, + {-iters + 1, iters - 1} + }; + } + else if (finalpos.y != stuckpos.y) { + float iters = roundf((finalpos.y - stuckpos.y) / step.y); + int isneg = iters < 0; + iters = fabs(iters); + ranges = (struct failranges){ + {-iters, iters}, + {-iters + isneg, iters - 1}, + {-iters + 1, iters - 1} + }; + } + else if (finalpos.z != stuckpos.z) { + float iters = roundf((finalpos.z - stuckpos.z) / step.z); + int isneg = iters > 0; + iters = fabs(iters); + ranges = (struct failranges){ + {-iters, iters}, + {-iters, iters}, + {-iters + isneg, iters - 1} + }; + } + else { + // we were never actually stuck - no need to draw all the boxes + return; + } + AddLineOverlay(dbgoverlay, &stuckpos, &finalpos, + cyan_line.r, cyan_line.g, cyan_line.b, true, 1000.0); + } + else { + finalpos = stuckpos; + // searched the entire 15 iteration range, found nowhere to go + ranges = (struct failranges){{-15, 15}, {-15, 15}, {-15, 15}}; + } + AddBoxOverlay2(dbgoverlay, &stuckpos, &mins, &maxs, &zerovec, + &red_face, &red_edge, 1000.0); + bool needline = true; + for (int i = ranges.x.neg; i <= ranges.x.pos; ++i) { + if (i == 0) { needline = true; continue; } + struct vec3f pos = {stuckpos.x + step.x * i, stuckpos.y, stuckpos.z}; + needline = draw_testpos(stuckpos, pos, mins, maxs, needline); + } + needline = true; + for (int i = ranges.y.neg; i <= ranges.y.pos; ++i) { + if (i == 0) { needline = true; continue; } + struct vec3f pos = {stuckpos.x, stuckpos.y + step.y * i, stuckpos.z}; + needline = draw_testpos(stuckpos, pos, mins, maxs, needline); + } + needline = true; + for (int i = ranges.z.neg; i <= ranges.z.pos; ++i) { + if (i == 0) { needline = true; continue; } + struct vec3f pos = {stuckpos.x, stuckpos.y, stuckpos.z + step.z * i}; + needline = draw_testpos(stuckpos, pos, mins, maxs, needline); + } +} + +static bool find_EntityPlacementTest(con_cmdcb z_add_cb) { +#ifdef _WIN32 + const uchar *insns = (const uchar *)z_add_cb; + for (const uchar *p = insns; p - insns < 0x300;) { + // Find 0, 0x200400B and 1 being pushed to the stack + if (p[0] == X86_PUSHI8 && p[1] == 0 && + p[2] == X86_PUSHIW && mem_loadu32(p + 3) == 0x200400B && + p[7] == X86_PUSHI8 && p[8] == 1) { + p += 9; + // Next call is the one we are looking for + while (p - insns < 0x300) { + if (p[0] == X86_CALL) { + EntityPlacementTest = (EntityPlacementTest_func)( + p + 5 + mem_loads32(p + 1)); + return true; + } + NEXT_INSN(p, "EntityPlacementTest function"); + } + return false; + } + NEXT_INSN(p, "EntityPlacementTest function"); + } +#else +#warning TODO(linux): usual asm search stuff +#endif + return false; +} + +static bool init_filter(void) { + const uchar *insns = (const uchar *)EntityPlacementTest; + for (const uchar *p = insns; p - insns < 0x60;) { + if (p[0] == X86_CALL) { + CTraceFilterSimple_ctor ctor = (CTraceFilterSimple_ctor)( + p + 5 + mem_loads32(p + 1)); + // calling the constructor to fill the vtable and other members + // with values used by the engine. pass_ent is filled in runtime + ctor(&filter, 0, 8, 0); + return true; + } + NEXT_INSN(p, "CTraceFilterSimple constructor"); + } + return false; } PREINIT { - return GAMETYPE_MATCHES(L4Dx); + 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; + } + if (!init_filter()) { + errmsg_errorx("couldn't init trace filter for EntityPlacementTest"); + return false; + } 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"); + } + else if_cold (!(dbgoverlay = factory_engine("VDebugOverlay003", 0))) { + errmsg_warnx("couldn't find debug overlay interface - " + "warp preview unavailable"); + } + else { + con_reg(sst_l4d_previewwarp); + } return true; } diff --git a/src/langext.h b/src/langext.h new file mode 100644 index 0000000..ef0f18d --- /dev/null +++ b/src/langext.h @@ -0,0 +1,64 @@ +/* This file is dedicated to the public domain. */ + +#ifndef INC_LANGEXT_H +#define INC_LANGEXT_H + +#include "intdefs.h" + +#define ssizeof(x) ((ssize)sizeof(x)) +#define countof(x) (ssizeof(x) / ssizeof(*x)) + +#if defined(__GNUC__) || defined(__clang__) +#define if_hot(x) if (__builtin_expect(!!(x), 1)) +#define if_cold(x) if (__builtin_expect(!!(x), 0)) +#define if_random(x) if (__builtin_expect_with_probability(!!(x), 1, 0.5)) +#define unreachable __builtin_unreachable() +#define assume(x) ((void)(!!(x) || (unreachable, 0))) +#define cold __attribute__((__cold__, __noinline__)) +#else +#define if_hot(x) if (x) +#define if_cold(x) if (x) +#define if_random(x) if (x) +#ifdef _MSC_VER +#define unreachable __assume(0) +#define assume(x) ((void)(__assume(x), 0)) +#define cold __declspec(noinline) +#else +#define unreachable ((void)(0)) // might still give some warnings, but too bad +#define assume(x) ((void)0) +#define cold +#endif +#endif + +#define switch_exhaust(x) switch (x) if (0) default: unreachable; else +#if defined(__GNUC__) || defined(__clang__) +#define switch_exhaust_enum(E, x) \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic error \"-Wswitch-enum\"") \ + switch_exhaust ((enum E)(x)) \ + _Pragma("GCC diagnostic pop") +#else +// NOTE: pragma trick doesn't work in MSVC (the pop seems to happen before the +// switch is evaluated, so nothing happens) but you can still get errors using +// -we4061. This doesn't matter for sst but might come in handy elsewhere... +#define switch_exhaust_enum(E, x) switch_exhaust ((enum E)(x)) +#endif + +#define noreturn _Noreturn void + +#ifdef _WIN32 +#define import __declspec(dllimport) // only needed for variables +#define export __declspec(dllexport) +#else +#define import +#ifdef __GNUC__ +// N.B. we assume -fvisibility=hidden +#define export __attribute__((visibility("default")) +#else +#define export int exp[-!!"compiler needs a way to export symbols!"]; +#endif +#endif + +#endif + +// vi: sw=4 ts=4 noet tw=80 cc=80 @@ -67,7 +67,7 @@ static inline usize mem_loadusize(const void *p) { } /* Adds a byte count to a pointer and returns a freely-assignable pointer. */ -static inline void *mem_offset(void *p, int off) { return (char *)p + off; } +static inline void *mem_offset(const void *p, int off) { return (char *)p + off; } /* Returns the offset in bytes from one pointer to another (p - q). */ static inline ssize mem_diff(const void *p, const void *q) { diff --git a/src/nomute.c b/src/nomute.c index 93cfcd2..4e5bcc4 100644 --- a/src/nomute.c +++ b/src/nomute.c @@ -1,6 +1,6 @@ /* * Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br> - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -18,16 +18,18 @@ // TODO(linux): this is currently only built on Windows, but a linux // implementation would be also useful for some games e.g. L4D2 +// NOTE: all three of these headers must be in this order. annoyingly. +#include <Windows.h> +#include <mmeapi.h> +#include <dsound.h> + #include "con_.h" #include "errmsg.h" #include "feature.h" +#include "langext.h" #include "os.h" #include "sst.h" -// these have to come after Windows.h (via os.h) and in this order -#include <mmeapi.h> -#include <dsound.h> - FEATURE("inactive window audio control") DEF_CVAR_UNREG(snd_mute_losefocus, @@ -43,7 +45,7 @@ static con_cmdcbv1 snd_restart_cb = 0; // unless we were loaded later with plugin_load in which case we actually do. static bool skiprestart; static void losefocuscb(struct con_var *v) { - if (!skiprestart) snd_restart_cb(); + if_hot (!skiprestart) snd_restart_cb(); skiprestart = false; } @@ -64,14 +66,15 @@ PREINIT { INIT { skiprestart = sst_earlyloaded; // see above IDirectSound *ds_obj = 0; - if (DirectSoundCreate(0, &ds_obj, 0) != DS_OK) { + 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; } ds_vt = ds_obj->lpVtbl; ds_obj->lpVtbl->Release(ds_obj); - if (!os_mprot(&ds_vt->CreateSoundBuffer, sizeof(void *), PAGE_READWRITE)) { + if_cold (!os_mprot(&ds_vt->CreateSoundBuffer, sizeof(void *), + PAGE_READWRITE)) { errmsg_errorsys("couldn't make virtual table writable"); return false; } @@ -80,7 +83,7 @@ INIT { snd_mute_losefocus->base.flags &= ~CON_HIDDEN; struct con_cmd *snd_restart = con_findcmd("snd_restart"); - if (snd_restart) { + if_hot (snd_restart) { snd_restart_cb = con_getcmdcbv1(snd_restart); snd_mute_losefocus->cb = &losefocuscb; } diff --git a/src/noreturn.h b/src/noreturn.h deleted file mode 100644 index 81b2bae..0000000 --- a/src/noreturn.h +++ /dev/null @@ -1,11 +0,0 @@ -/* This file is dedicated to the public domain. */ - -#ifndef INC_NORETURN_H -#define INC_NORETURN_H - -#undef noreturn -#define noreturn _Noreturn void - -#endif - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/nosleep.c b/src/nosleep.c index e75c6e6..52c88a9 100644 --- a/src/nosleep.c +++ b/src/nosleep.c @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -20,6 +20,7 @@ #include "feature.h" #include "gamedata.h" #include "hook.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "vcall.h" @@ -48,7 +49,7 @@ PREINIT { INIT { vtable = mem_loadptr(inputsystem); - if (!os_mprot(vtable + vtidx_SleepUntilInput, sizeof(void *), + if_cold (!os_mprot(vtable + vtidx_SleepUntilInput, sizeof(void *), PAGE_READWRITE)) { errmsg_errorx("couldn't make virtual table writable"); return false; diff --git a/src/os-unix.h b/src/os-unix.h deleted file mode 100644 index 097d300..0000000 --- a/src/os-unix.h +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright © 2023 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. - */ - -/* Unix-specific definitions for os.h - don't include this directly! */ - -#ifdef __GNUC__ -#define EXPORT __attribute__((visibility("default"))) -#else -#define EXPORT int novis[-!!"visibility attribute requires Clang or GCC"]; -#endif -#define IMPORT - -typedef char os_char; -#define OS_LIT(x) x -#define os_snprintf snprintf -#define os_strchr strchr -#define os_strcmp strcmp -#define os_strcpy strcpy -#define os_strlen strlen -#define os_fopen fopen -#define os_open open -#define os_access access -#define os_stat stat -#define os_mkdir(f) mkdir(f, 0755) // use real mkdir(2) if the mode matters -#define os_unlink unlink -#define os_getenv getenv -#define os_getcwd getcwd - -#define OS_DLPREFIX "lib" -#define OS_DLSUFFIX ".so" - -#define OS_MAIN main - -static inline void *os_dlopen(const char *name) { - return dlopen(name, RTLD_NOW); -} -#define os_dlsym dlsym - -#ifdef __linux__ -// note: this is glibc-specific. it shouldn't be used in build-time code, just -// the plugin itself (that really shouldn't be a problem). - -// private struct hidden behind _GNU_SOURCE. see dlinfo(3) or <link.h> -struct gnu_link_map { - unsigned long l_addr; - const char *l_name; - void *l_ld; - struct gnu_link_map *l_next, *l_prev; - // [more private stuff below] -}; - -static inline void *os_dlhandle(const char *name) { - extern struct gnu_link_map *_os_lmbase; // note: defined in sst.c for now - if (!_os_lmbase) { // IMPORTANT: not thread safe. don't forget later! - _os_lmbase = (struct gnu_link_map *)dlopen("libc.so.6", - RTLD_LAZY | RTLD_NOLOAD); - dlclose(_os_lmbase); // assume success - while (_os_lmbase->l_prev) _os_lmbase = _os_lmbase->l_prev; - } - // this is a tiny bit crude, but basically okay. we just want to find - // something that roughly matches the basename, rather than needing an exact - // path, in a manner vaguely similar to Windows' GetModuleHandle(). that way - // we can just look up client.so or something without having to figure out - // where exactly that is. - for (struct gnu_link_map *lm = _os_lmbase; lm; lm = lm->l_next) { - if (name[0] == '/') { - if (!strcmp(name, lm->l_name)) return lm; - continue; - } - int namelen = strlen(lm->l_name); - int sublen = strlen(name); - if (sublen >= namelen) continue; - if (lm->l_name[namelen - sublen - 1] == '/' && !memcmp( - lm->l_name + namelen - sublen, name, sublen)) { - return lm; - } - } - return 0; -} - -static inline int os_dlfile(void *m, char *buf, int sz) { - struct gnu_link_map *lm = m; - int ssz = strlen(lm->l_name) + 1; - if (ssz > sz) { errno = ENAMETOOLONG; return -1; } - memcpy(buf, lm->l_name, ssz); - return ssz; -} - -#endif - -// unix mprot flags are much nicer but cannot be defined in terms of the windows -// ones, so we use the windows ones and define them in terms of the unix ones. -// another victory for stupid! -#define PAGE_NOACCESS 0 -#define PAGE_READONLY PROT_READ -#define PAGE_READWRITE PROT_READ | PROT_WRITE -#define PAGE_EXECUTE_READ PROT_READ | PROT_EXEC -#define PAGE_EXECUTE_READWRITE PROT_READ | PROT_WRITE | PROT_EXEC - -static inline bool os_mprot(void *addr, int len, int fl) { - // round down address and round up size - addr = (void *)((unsigned long)addr & ~(4095)); - len = len + 4095 & ~(4095); - return mprotect(addr, len, fl) != -1; -} - -static inline void os_randombytes(void *buf, int sz) { - // if this call fails, the system is doomed, so loop until success and pray. - while (getentropy(buf, sz) == -1); -} - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/os-win32.h b/src/os-win32.h deleted file mode 100644 index db20964..0000000 --- a/src/os-win32.h +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright © 2023 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. - */ - -/* Windows-specific definitions for os.h - don't include this directly! */ - -#define IMPORT __declspec(dllimport) // only needed for variables -#define EXPORT __declspec(dllexport) - -typedef unsigned short os_char; -#define _OS_CAT(L, x) L##x -#define OS_LIT(x) _OS_CAT(L, x) - -#define os_snprintf _snwprintf -#define os_strchr wcschr -#define os_strcmp wcscmp -#define os_strcpy wcscpy -#define os_strlen wcslen -#define strncasecmp _strnicmp // stupid! - -#define os_fopen _wfopen -// yuck :( -#define _os_open3(path, flags, mode) _wopen((path), (flags) | _O_BINARY, (mode)) -#define _os_open2(path, flags) _wopen((path), (flags) | _O_BINARY) -#define _os_open(a, b, c, x, ...) x -#define os_open(...) _os_open(__VA_ARGS__, _os_open3, _os_open2, _)(__VA_ARGS__) -#define os_access _waccess -// ucrt defines __stat64 to _stat64. we want _wstat64 to be the actual function -#define _stat64(path, buf) _wstat64(path, buf) -#define os_stat _stat64 -#define os_mkdir _wmkdir -#define os_unlink _wunlink -#define os_getenv _wgetenv -#define os_getcwd _wgetcwd - -#define OS_DLPREFIX "" -#define OS_DLSUFFIX ".dll" - -#define OS_MAIN wmain - -static inline void *os_dlopen(const unsigned short *name) { - return LoadLibraryW(name); -} -static inline void *os_dlhandle(const unsigned short *name) { - return GetModuleHandleW(name); -} -static inline void *os_dlsym(void *m, const char *s) { - return (void *)GetProcAddress(m, s); -} - -static inline int os_dlfile(void *m, unsigned short *buf, int sz) { - unsigned int n = GetModuleFileNameW(m, buf, sz); - if (n == 0 || n == sz) return -1; - // get the canonical capitalisation, as for some reason GetModuleFileName() - // returns all lowercase. this doesn't really matter except it looks nicer - GetLongPathNameW(buf, buf, n + 1); - // the drive letter will also be lower case, if it is an actual drive letter - // of course. it should be; I'm not gonna lose sleep over UNC paths and such - if (buf[0] >= L'a' && buf[0] <= L'z' && buf[1] == L':') buf[0] &= ~32u; - return n; -} - -static inline bool os_mprot(void *addr, int len, int fl) { - unsigned long old; - return !!VirtualProtect(addr, len, fl, &old); -} - -// SystemFunction036 is the *real* name of "RtlGenRandom," and is also -// incorrectly defined in system headers. Yay. -int __stdcall SystemFunction036(void *buf, unsigned long sz); - -static inline void os_randombytes(void *buf, int sz) { - // if this call fails, the system is doomed, so loop until success and pray. - while (!SystemFunction036(buf, sz)); -} - -/* Further Windows-specific workarounds because Windows is great */ - -#ifndef PATH_MAX -// XXX: win32/crt has this dumb 260 limit even though the actual kernel imposes -// no limit (though apparently NTFS has a limit of 65535). Theoretically we -// could do some memes with UNC paths to extend it to at least have parity with -// Unix PATH_MAX (4096), but for now we just kind of accept that Windows is a -// disaster. -#define PATH_MAX MAX_PATH -#endif - -// why does Windows prefix so many things with underscores? -#define alloca _alloca -#define read _read -#define write _write -#define close _close -#define fdopen _fdopen -#define dup _dup -#define dup2 _dup2 -#define strdup _strdup -#define fstat _fstat64 -#define lseek _lseeki64 -#define O_RDONLY _O_RDONLY -#define O_RDWR _O_RDWR -#define O_CLOEXEC _O_NOINHERIT // and why did they rename this!? -#define O_CREAT _O_CREAT -#define O_EXCL _O_EXCL -// missing access() flags -#define F_OK 0 -#define R_OK 4 -#define W_OK 2 -#define X_OK R_OK // there's no actual X bit, just pretend - -// windows doesn't define this for some reason!? note: add others as needed... -#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) - -// just dump this boilerplate here as well, I spose -#define OS_WINDOWS_ERROR(arrayname) \ - FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, GetLastError(), \ - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), arrayname, \ - sizeof(arrayname), 0) - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/os.c b/src/os.c new file mode 100644 index 0000000..e6d32e8 --- /dev/null +++ b/src/os.c @@ -0,0 +1,204 @@ +/* + * 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 <fcntl.h> +#include <stdlib.h> +#ifdef _WIN32 +#include <Windows.h> +#else +#include <errno.h> +#include <dlfcn.h> +#include <limits.h> +#include <link.h> +#include <stdio.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <unistd.h> +#endif + +#include "intdefs.h" +#include "langext.h" + +#ifdef _WIN32 + +int os_lasterror(void) { return GetLastError(); } + +// N.B. file handle values are 32-bit, even in 64-bit builds. I'm not crazy! + +int os_open_read(const ushort *path) { + return (int)(ssize)CreateFileW(path, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, 0); +} +int os_open_write(const ushort *file) { + return (int)(ssize)CreateFileW(file, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, 0); +} +int os_open_writetrunc(const ushort *file) { + return (int)(ssize)CreateFileW(file, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, 0, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, 0); +} + +int os_read(int f, void *buf, int max) { + ulong n; + return ReadFile((void *)(ssize)f, buf, max, &n, 0) ? n : -1; +} +int os_write(int f, const void *buf, int len) { + ulong n; + return WriteFile((void *)(ssize)f, buf, len, &n, 0) ? n : -1; +} + +vlong os_fsize(int f) { + vlong ret; + if_cold (!GetFileSizeEx((void *)(ssize)f, (LARGE_INTEGER *)&ret)) return -1; + return ret; +} + +void os_close(int f) { + CloseHandle((void *)(ssize)f); +} + +void os_getcwd(ushort buf[static 260]) { GetCurrentDirectoryW(260, buf); } + +bool os_mkdir(const ushort *path) { return CreateDirectoryW(path, 0); } +bool os_unlink(const ushort *path) { return DeleteFileW(path); } +bool os_rmdir(const ushort *path) { return RemoveDirectoryW(path); } + +int __stdcall ProcessPrng(char *data, usize sz); // from bcryptprimitives.dll +void os_randombytes(void *buf, int sz) { ProcessPrng(buf, sz); } + +void *os_dlhandle(const ushort *name) { + return GetModuleHandleW(name); +} +void *os_dlsym(void *restrict lib, const char *restrict s) { + return (void *)GetProcAddress(lib, s); +} +int os_dlfile(void *lib, ushort *buf, int sz) { + unsigned int n = GetModuleFileNameW(lib, buf, sz); + if_cold (n == 0 || n == sz) return -1; + // get the canonical capitalisation, as for some reason GetModuleFileName() + // returns all lowercase. this doesn't really matter except it looks nicer + GetLongPathNameW(buf, buf, n + 1); + // the drive letter will also be lower case, if it is an actual drive letter + // of course. it should be; I'm not gonna lose sleep over UNC paths and such + if_hot (buf[0] >= L'a' && buf[0] <= L'z' && buf[1] == L':') buf[0] &= ~32u; + return n; +} + +bool os_mprot(void *addr, int len, int mode) { + ulong old; + return !!VirtualProtect(addr, len, mode, &old); +} + +#else + +int os_lasterror(void) { return errno; } + +int os_open_read(const char *path) { + return open(path, O_RDONLY | O_CLOEXEC); +} +int os_open_write(const char *path) { + return open(path, O_RDWR | O_CLOEXEC | O_CREAT, 0644); +} +int os_open_writetrunc(const char *path) { + return open(path, O_RDWR | O_CLOEXEC | O_CREAT | O_TRUNC, 0644); +} +int os_read(int f, void *buf, int max) { return read(f, buf, max); } +int os_write(int f, const void *buf, int max) { return write(f, buf, max); } +void os_close(int f) { close(f); } + +vlong os_fsize(int f) { + struct stat s; + if_cold (stat(f, &s) == -1) return -1; + return s.st_size; +} + +void os_getcwd(char buf[PATH_MAX]) { getcwd(buf, PATH_MAX); } + +bool os_mkdir(const char *path) { return mkdir(path, 0555) != -1; } +bool os_unlink(const char *path) { return unlink(path) != -1; } +bool os_rmdir(const char *path) { return rmdir(path) != -1; } + +void *os_dlsym(void *restrict lib, const char *restrict name) { + return dlsym(lib, name); +} + +bool os_mprot(void *addr, int len, int mode) { + // round down address and round up size + addr = (void *)((ulong)addr & ~(4095)); + len = len + 4095 & ~(4095); + return mprotect(addr, len, mode) != -1; +} + +void os_randombytes(void *buf, int sz) { while (getentropy(buf, sz) == -1); } + +#endif + +#ifdef __linux__ +// glibc-specific stuff here. it shouldn't be used in build-time code, just +// the plugin itself (that really shouldn't be a problem). + +// private struct hidden behind _GNU_SOURCE. see dlinfo(3) or <link.h> +struct link_map { + ulong l_addr; + const char *l_name; + void *l_ld; + struct link_map *l_next, *l_prev; + // [more private stuff below] +}; + +static struct link_map *lmbase = 0; + +void *os_dlhandle(const char *name) { + if_cold (!lmbase) { // IMPORTANT: not thread safe. don't forget later! + lmbase = (struct link_map *)dlopen("libc.so.6", RTLD_LAZY | RTLD_NOLOAD); + dlclose(lmbase); // assume success + while (lmbase->l_prev) lmbase = lmbase->l_prev; + } + // this is a tiny bit crude, but basically okay. we just want to find + // something that roughly matches the basename, rather than needing an exact + // path, in a manner vaguely similar to Windows' GetModuleHandle(). that way + // we can just look up client.so or something without having to figure out + // where exactly that is. + for (struct link_map *lm = lmbase; lm; lm = lm->l_next) { + if (name[0] == '/') { + if (!strcmp(name, lm->l_name)) return lm; + continue; + } + int namelen = strlen(lm->l_name); + int sublen = strlen(name); + if (sublen >= namelen) continue; + if (lm->l_name[namelen - sublen - 1] == '/' && !memcmp( + lm->l_name + namelen - sublen, name, sublen)) { + return lm; + } + } + return 0; +} + +int os_dlfile(void *lib, char *buf, int sz) { + struct link_map *lm = lib; + int ssz = strlen(lm->l_name) + 1; + if_cold (ssz > sz) { errno = ENAMETOOLONG; return -1; } + memcpy(buf, lm->l_name, ssz); + return ssz; +} +#endif + +// vi: sw=4 ts=4 noet tw=80 cc=80 @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -17,48 +17,228 @@ #ifndef INC_OS_H #define INC_OS_H -/* - * Here we declare an absolute ton of wrappers, macros, compatibility shims, - * reimplementations and so on to try in vain to sweep the inconsistencies - * between Windows and not-Windows under the rug. - */ +#include <string.h> +#include <sys/stat.h> // XXX: try abstracting stat() and avoiding ucrt dep here -#include <errno.h> -#include <fcntl.h> #ifdef _WIN32 -#include <direct.h> -#include <io.h> -#include <wchar.h> -// DUMB HACK: noreturn.h is alphabetically before os.h so including it after -// looks weird and inconsistent. including it before breaks Windows.h though -// because of __declspec(noreturn). Undef for now, and restore at the end of -// this header. -#undef noreturn -#include <Windows.h> -#include <winternl.h> -#else -#include <dlfcn.h> -#include <limits.h> -#include <link.h> -#include <stdio.h> -#include <string.h> -#include <strings.h> -#include <sys/mman.h> -#include <sys/stat.h> -#include <unistd.h> + +#include <wchar.h> // XXX: there's kind of a lot of junk in this header! + +typedef unsigned short os_char; +#define _OS_CAT(x, y) x##y +#define OS_LIT(x) _OS_CAT(L, x) +#define os_strcmp wcscmp +#define os_strlen wcslen +// ucrt defines __stat64 to _stat64. we want _wstat64 to be the actual function +#define _stat64(path, buf) _wstat64(path, buf) +#define os_stat _stat64 + +#define OS_DLPREFIX "" +#define OS_DLSUFFIX ".dll" + +#define OS_MAIN wmain + +// add to these as needed... +#define OS_EEXIST /*ERROR_ALREADY_EXISTS*/ 183 +#define OS_ENOENT /*ERROR_PATH_NOT_FOUND*/ 3 + +// Further Windows-specific workarounds because Windows is great... + +#ifndef PATH_MAX +// XXX: win32/crt has this dumb 260 limit even though the actual kernel imposes +// no limit (though apparently NTFS has a limit of 65535). Theoretically we +// could do some memes with UNC paths to extend it to at least have parity with +// Unix PATH_MAX (4096), but for now we just kind of accept that Windows is a +// disaster. +#define PATH_MAX 260 #endif -// Things were getting unwieldy so they're now split into these headers... -#ifdef _WIN32 -#include "os-win32.h" -// DUMB HACK part 2: restore what the #including source file had asked for -#ifdef INC_NORETURN_H -#define noreturn _Noreturn void +// windows doesn't define this for some reason!? note: add others as needed... +#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) + +#define alloca _alloca // ugh! + +// one last thing: mprot constants are part of os.h API, whether or not +// Windows.h was included. this is a bit of a hack, but whatever! +#ifndef _INC_WINDOWS +#define PAGE_NOACCESS 1 +#define PAGE_READONLY 2 +#define PAGE_READWRITE 4 +#define PAGE_EXECUTE_READ 32 +#define PAGE_EXECUTE_READWRITE 64 #endif + #else -#include "os-unix.h" + +#include <errno.h> // meh + +// trying to avoid pulling in unnecessary headers as much as possible: define +// our own constants for os_mprot() / mprotect() +#if defined(__linux__) // apparently linux is pretty much the sole oddball here! +#define _OS_PROT_READ 4 +#define _OS_PROT_WRITE 2 +#define _OS_PROT_EXEC 1 +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || \ + defined(__DragonFly__) || defined(__sun) || defined(__serenity) +#define _OS_PROT_READ 1 +#define _OS_PROT_WRITE 2 +#define _OS_PROT_EXEC 4 +#elif !defined(_WIN32) // what else could this possibly even be!? +#include <sys/mman.h> // just fall back on this. not the end of the world... +#define _OS_PROT_READ PROT_READ +#define _OS_PROT_WRITE PROT_WRITE +#define _OS_PROT_EXEC PROT_EXEC +#endif + +typedef char os_char; +#define OS_LIT(x) x +#define os_strcmp strcmp +#define os_strlen strlen +#define os_stat stat + +#define OS_DLPREFIX "lib" +#define OS_DLSUFFIX ".so" + +#define OS_MAIN main + +// unix mprotect flags are much nicer but cannot be defined in terms of the +// windows ones, so we use the windows ones and define them in terms of the unix +// ones. another victory for stupid! +#define PAGE_NOACCESS 0 +#define PAGE_READONLY _OS_PROT_READ +#define PAGE_READWRITE _OS_PROT_READ | _OS_PROT_WRITE +#define PAGE_EXECUTE_READ _OS_PROT_READ | _OS_PROT_EXEC +#define PAGE_EXECUTE_READWRITE _OS_PROT_READ | _OS_PROT_WRITE | _OS_PROT_EXEC + +#define OS_EEXIST EEXIST +#define OS_ENOENT ENOENT + #endif +/* Copies n characters from src to dest, using the OS-specific char type. */ +static inline void os_spancopy(os_char *restrict dest, + const os_char *restrict src, int n) { + memcpy(dest, src, n * sizeof(os_char)); +} + +/* + * Returns the last error code from an OS function - equivalent to calling + * GetLastError() in Windows and reading errno in Unix-like operating systems. + * For standard libc functions (implemented by UCRT on Windows), the value of + * errno should be used directly instead. + */ +int os_lasterror(void); + +/* + * Opens a file for reading. Returns an OS-specific file handle, or -1 on error. + */ +int os_open_read(const os_char *path); + +/* + * Opens a file for writing, creating it if required. Returns an OS-specific + * file handle, or -1 on error. + */ +int os_open_write(const os_char *path); + +/* + * Opens a file for writing, creating it if required, or truncating it if it + * already exists. Returns an OS-specific file handle, or -1 on error. + */ +int os_open_writetrunc(const os_char *path); + +/* + * Reads up to max bytes from OS-specific file handle f into buf. Returns the + * number of bytes read, or -1 on error. + */ +int os_read(int f, void *buf, int max); + +/* + * Reads up to len bytes from buf to OS-specific file handle f. Returns the + * number of bytes written, or -1 on error. Generally the number of bytes + * written will be len, unless writing to a pipe/socket, which has a limited + * internal buffer, or possibly being preempted by a signal handler. + */ +int os_write(int f, const void *buf, int len); + +/* + * Returns the length of the on-disk file referred to by OS-specific file handle + * f, or -1 on error (the most likely error would be that the file is not + * actually on disk and instead refers to a pipe or something). + */ +long long os_fsize(int f); + +/* + * Closes the OS-specific file handle f. On Windows, this causes pending writes + * to be flushed; on Unix-likes, this generally happens asynchronously. If + * blocking is a concern when writing files to disk, some sort of thread pool + * should always be used. + */ +void os_close(int f); + +/* + * Gets the current working directory, which may be up to PATH_MAX characters in + * length (using the OS-specific character type). + */ +void os_getcwd(os_char buf[static PATH_MAX]); + +/* + * Tries to create a directory at path. Returns true on success, false on + * failure. One may wish to ignore a failure if the directory already exists; + * this can be done by checking if os_lasterror() returns OS_EEXIST. + */ +bool os_mkdir(const os_char *path); + +/* + * Tries to delete a file(name) at path. Returns true on success, false on + * failure. One may wish to ignore a failure if the file already doesn't exist; + * this can be done by checking if os_lasterror() returns OS_ENOENT. + * + * On some Unix-like operating systems, this may work on empty directories, but + * for portably reliable results, one should call os_rmdir() for that instead. + */ +bool os_unlink(const os_char *path); + +/* + * Tries to delete a directory at path. Returns true on success, false on + * failure. One may wish to ignore a failure if the directory already doesn't + * exist; this can be done by checking if os_lasterror() returns OS_ENOENT. + */ +bool os_rmdir(const os_char *path); + +/* + * Tries to look up the symbol sym from the shared library handle lib. Returns + * an opaque pointer on success, or null on failure. + */ +void *os_dlsym(void *restrict lib, const char *restrict sym); + +#if defined(_WIN32) || defined(__linux__) +/* + * Tries to get a handle to an already-loaded shared library matching name. + * Returns the library handle on success, or null on failure. + */ +void *os_dlhandle(const os_char *name); + +/* + * Tries to determine the file path to the shared library handle lib, and stores + * it in buf (with max length given by sz). Returns the length of the resulting + * string, or -1 on failure. + */ +int os_dlfile(void *lib, os_char *buf, int sz); +#endif + +/* + * Changes memory protection for the address range given by addr and len, using + * one of the Win32-style PAGE_* flags specified above. Returns true on success + * and false on failure. + */ +bool os_mprot(void *addr, int len, int mode); + +/* + * Fills buf with up to sz cryptographically random bytes. sz has an OS-specific + * upper limit - a safe value across all major operating systems is 256. + */ +void os_randombytes(void *buf, int sz); + #endif // vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/portalcolours.c b/src/portalcolours.c index 3167c0b..61d9cc7 100644 --- a/src/portalcolours.c +++ b/src/portalcolours.c @@ -24,6 +24,7 @@ #include "hexcolour.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "os.h" #include "ppmagic.h" @@ -102,13 +103,13 @@ PREINIT { INIT { #ifdef _WIN32 - if (!find_UTIL_Portal_Color(clientlib)) { + if_cold (!find_UTIL_Portal_Color(clientlib)) { errmsg_errorx("couldn't find UTIL_Portal_Color"); return false; } orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)hook_inline( (void *)orig_UTIL_Portal_Color, (void *)&hook_UTIL_Portal_Color); - if (!orig_UTIL_Portal_Color) { + if_cold (!orig_UTIL_Portal_Color) { errmsg_errorsys("couldn't hook UTIL_Portal_Color"); return false; } @@ -126,7 +127,7 @@ INIT { } END { - if (!sst_userunloaded) return; + if_hot (!sst_userunloaded) return; unhook_inline((void *)orig_UTIL_Portal_Color); } diff --git a/src/rinput.c b/src/rinput.c index 6b61bff..3baa67c 100644 --- a/src/rinput.c +++ b/src/rinput.c @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -28,6 +28,7 @@ #include "gamedata.h" #include "hook.h" #include "intdefs.h" +#include "langext.h" #include "mem.h" #include "sst.h" #include "vcall.h" @@ -63,17 +64,17 @@ DEF_CVAR_UNREG(m_rawinput, "Use Raw Input for mouse input (SST reimplementation) 0, CON_ARCHIVE | CON_HIDDEN) DEF_CVAR_MINMAX(sst_mouse_factor, "Number of hardware mouse counts per step", - 1, 1, 20, /*CON_ARCHIVE |*/ CON_HIDDEN) + 1, 1, 100, /*CON_ARCHIVE |*/ CON_HIDDEN) static ssize __stdcall inproc(void *wnd, uint msg, usize wp, ssize lp) { switch (msg) { case WM_INPUT:; - char buf[sizeof(RAWINPUTHEADER) + sizeof(RAWMOUSE) /* = 40 */]; + char buf[ssizeof(RAWINPUTHEADER) + ssizeof(RAWMOUSE) /* = 40 */]; uint sz = sizeof(buf); - if (GetRawInputData((void *)lp, RID_INPUT, buf, &sz, + if_hot (GetRawInputData((void *)lp, RID_INPUT, buf, &sz, sizeof(RAWINPUTHEADER)) != -1) { RAWINPUT *ri = (RAWINPUT *)buf; - if (ri->header.dwType == RIM_TYPEMOUSE) { + if_hot (ri->header.dwType == RIM_TYPEMOUSE) { int d = con_getvari(sst_mouse_factor); int dx = rx + ri->data.mouse.lLastX; int dy = ry + ri->data.mouse.lLastY; @@ -129,8 +130,8 @@ INIT { if (!inputsystem) return false; vtable_insys = mem_loadptr(inputsystem); // XXX: this is kind of duping nosleep, but that won't always init... - if (!os_mprot(vtable_insys + vtidx_GetRawMouseAccumulators, - sizeof(void *), PAGE_READWRITE)) { + if_cold (!os_mprot(vtable_insys + vtidx_GetRawMouseAccumulators, + ssizeof(void *), PAGE_READWRITE)) { errmsg_errorx("couldn't make virtual table writable"); return false; } @@ -149,7 +150,7 @@ INIT { .lpfnWndProc = (WNDPROC)&inproc, .lpszClassName = L"RInput" }; - if (!RegisterClassExW(&wc)) { + if_cold (!RegisterClassExW(&wc)) { struct rgba gold = {255, 210, 0, 255}; struct rgba blue = {45, 190, 190, 255}; struct rgba white = {200, 200, 200, 255}; @@ -158,7 +159,7 @@ INIT { "Consider launching without that and using "); con_colourmsg(&gold, "m_rawinput 1"); con_colourmsg(&blue, " instead!\n"); - if (has_rawinput) { + if_cold (has_rawinput) { // slow path because this'd be kinda weird! con_colourmsg(&white, "This is built into this version of game, and" " will also get provided by SST in older versions. "); } @@ -181,18 +182,18 @@ INIT { orig_GetCursorPos = (GetCursorPos_func)hook_inline((void *)&GetCursorPos, (void *)&hook_GetCursorPos); - if (!orig_GetCursorPos) { + if_cold (!orig_GetCursorPos) { errmsg_errorsys("couldn't hook %s", "GetCursorPos"); goto e0; } orig_SetCursorPos = (SetCursorPos_func)hook_inline((void *)&SetCursorPos, (void *)&hook_SetCursorPos); - if (!orig_SetCursorPos) { + if_cold (!orig_SetCursorPos) { errmsg_errorsys("couldn't hook %s", "SetCursorPos"); goto e1; } inwin = CreateWindowExW(0, L"RInput", L"RInput", 0, 0, 0, 0, 0, 0, 0, 0, 0); - if (!inwin) { + if_cold (!inwin) { errmsg_errorsys("couldn't create input window"); goto e2; } @@ -201,7 +202,7 @@ INIT { .usUsagePage = USAGEPAGE_MOUSE, .usUsage = USAGE_MOUSE }; - if (!RegisterRawInputDevices(&rd, 1, sizeof(rd))) { + if_cold (!RegisterRawInputDevices(&rd, 1, sizeof(rd))) { errmsg_errorsys("couldn't create raw mouse device"); goto e3; } @@ -218,8 +219,8 @@ e0: UnregisterClassW(L"RInput", 0); } END { - if (!sst_userunloaded) return; - if (orig_SetCursorPos) { // if null, we didn't init our own implementation + if_hot (!sst_userunloaded) return; + if_hot (orig_SetCursorPos) { // we inited our own implementation RAWINPUTDEVICE rd = { .dwFlags = RIDEV_REMOVE, .hwndTarget = 0, @@ -232,7 +233,7 @@ END { unhook_inline((void *)orig_GetCursorPos); unhook_inline((void *)orig_SetCursorPos); } - else { + else { // we must have hooked the *existing* implementation unhook_vtable(vtable_insys, vtidx_GetRawMouseAccumulators, (void *)orig_GetRawMouseAccumulators); } @@ -14,13 +14,14 @@ * PERFORMANCE OF THIS SOFTWARE. */ -#ifndef _WIN32 -#include <stdlib.h> // unsetenv -#endif #include <string.h> #ifdef _WIN32 +#include <Windows.h> #include <shlwapi.h> +#else +#include <stdlib.h> // unsetenv +#include <sys/uio.h> #endif #include "con_.h" @@ -32,6 +33,8 @@ #include "gameinfo.h" #include "gametype.h" #include "hook.h" +#include "intdefs.h" +#include "langext.h" #include "os.h" #include "sst.h" #include "vcall.h" @@ -69,13 +72,11 @@ int dladdr1(const void *addr, Dl_info *info, void **extra_info, int flags); static void *ownhandle(void) { static void *cached = 0; Dl_info dontcare; - if (!cached) { + if_cold (!cached) { dladdr1((void *)&ownhandle, &dontcare, &cached, /*RTLD_DL_LINKMAP*/ 2); } return cached; } - -struct gnu_link_map *_os_lmbase = 0; // XXX: stupid place to put this, oh well #endif #ifdef _WIN32 @@ -83,14 +84,14 @@ struct gnu_link_map *_os_lmbase = 0; // XXX: stupid place to put this, oh well static inline bool checksamedrive(const ushort *restrict path1, const ushort *restrict path2) { bool ret = (path1[0] | 32) == (path2[0] | 32); - if (!ret) errmsg_errorx("game and plugin must be on the same drive\n"); + if_cold (!ret) errmsg_errorx("game and plugin must be on the same drive\n"); return ret; } #endif DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { os_char path[PATH_MAX]; - if (os_dlfile(ownhandle(), path, sizeof(path) / sizeof(*path)) == -1) { + if_cold (os_dlfile(ownhandle(), path, countof(path)) == -1) { // hopefully by this point this won't happen, but, like, never know errmsg_errordl("failed to get path to plugin"); return; @@ -99,26 +100,25 @@ DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { const os_char *startdir; if (ifacever == 2) { startdir = _startdir; - os_getcwd(_startdir, PATH_MAX); // if this fails, OS devs are all fired. + os_getcwd(_startdir); #ifdef _WIN32 // note: strictly speaking we *could* allow this with an absolute path // since old builds allow absolute plugin_load paths but since it's less // reliable if e.g. a disk is removed, and also doesn't work for all // games, just rule it out entirely to keep things simple. - if (!checksamedrive(path, startdir)) return; + if_cold (!checksamedrive(path, startdir)) return; #endif int len = os_strlen(startdir); - if (len + sizeof("/bin") >= PATH_MAX) { + if_cold (len + ssizeof("/bin") >= PATH_MAX) { errmsg_errorx("path to game is too long"); return; } - memcpy(_startdir + len, #ifdef _WIN32 - L"\\bin", // PathRelativePathToW actually NEEDS a backslash, UGH + // PathRelativePathToW actually NEEDS a backslash, UGH + os_spancopy(_startdir + len, L"\\bin", 5); #else - "/bin", + os_spancopy(_startdir + len, L"/bin", 5); #endif - 5 * sizeof(os_char)); } else /* ifacever == 3 */ { // newer games load from the mod dir instead of engine bin, and search @@ -130,7 +130,7 @@ DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { // obscure gameinfo.txt arrangement could technically allow that to work startdir = gameinfo_gamedir; #ifdef _WIN32 - if (!checksamedrive(path, startdir)) return; + if_cold (!checksamedrive(path, startdir)) return; #endif } os_char relpath[PATH_MAX]; @@ -138,13 +138,22 @@ DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { // note: dll isn't actually in gamedir if it's in a base mod directory // note: gamedir doesn't account for if the dll is in a base mod's // directory, although it will yield a valid/working relative path anyway. - if (!PathRelativePathToW(relpath, startdir, FILE_ATTRIBUTE_DIRECTORY, + if_cold (!PathRelativePathToW(relpath, startdir, FILE_ATTRIBUTE_DIRECTORY, path, 0)) { errmsg_errorsys("couldn't compute a relative path"); return; } - // arbitrary aesthetic judgement - for (ushort *p = relpath; *p; ++p) if (*p == L'\\') *p = L'/'; + // arbitrary aesthetic judgement - use forward slashes. while we're at it, + // also make sure there's no unicode in there, just in case... + int rellen = 0; + for (ushort *p = relpath; *p; ++p, ++rellen) { + if_cold (*p > 127) { + errmsg_errorx("mod dir contains Unicode characters which Source " + "doesn't handle well - autoload file not created"); + return; + } + if_random (*p == L'\\') *p = L'/'; + } #else const char *p = path, *q = startdir; int slash = 0; @@ -172,47 +181,52 @@ DEF_CCMD_HERE(sst_autoload_enable, "Register SST to load on game startup", 0) { c: memcpy(r, p + slash + 1, rellen); #endif int len = os_strlen(gameinfo_gamedir); - if (len + sizeof("/addons/" VDFBASENAME ".vdf") > - sizeof(path) / sizeof(*path)) { + if (len + ssizeof("/addons/" VDFBASENAME ".vdf") > countof(path)) { errmsg_errorx("path to VDF is too long"); return; } - memcpy(path, gameinfo_gamedir, len * sizeof(*gameinfo_gamedir)); - memcpy(path + len, OS_LIT("/addons"), 8 * sizeof(os_char)); - if (os_mkdir(path) == -1 && errno != EEXIST) { - errmsg_errorstd("couldn't create %" fS, path); + os_spancopy(path, gameinfo_gamedir, len); + os_spancopy(path + len, OS_LIT("/addons"), 8); + if (!os_mkdir(path)) if_cold (os_lasterror() != OS_EEXIST) { + errmsg_errorsys("couldn't create %" fS, path); return; } - memcpy(path + len + sizeof("/addons") - 1, + os_spancopy(path + len + ssizeof("/addons") - 1, OS_LIT("/") OS_LIT(VDFBASENAME) OS_LIT(".vdf"), - sizeof("/" VDFBASENAME ".vdf") * sizeof(os_char)); - FILE *f = os_fopen(path, OS_LIT("wb")); - if (!f) { - errmsg_errorstd("couldn't open %" fS, path); - return; - } - // XXX: oh crap, we're clobbering unicode again. welp, let's continue - // relying on the theory that the engine would fail to deal with it anyway. - if (fprintf(f, "Plugin { file \"%" fS "\" }\n", relpath) < 0 || - fflush(f) == -1) { - errmsg_errorstd("couldn't write to %" fS, path); + ssizeof("/" VDFBASENAME ".vdf")); + int f = os_open_write(path); + if_cold (f == -1) { errmsg_errorsys("couldn't open %" fS, path); return; } +#ifdef _WIN32 + char buf[19 + PATH_MAX]; + memcpy(buf, "Plugin { file \"", 15); + for (int i = 0; i < rellen; ++i) buf[i + 15] = relpath[i]; + memcpy(buf + 15 + rellen, "\" }\n", 4); + if_cold (os_write(f, buf, rellen + 19) == -1) { // blegh +#else + struct iovec iov[3] = { + {"Plugin { file \"", 15}, + {relpath, rellen}, + {"\" }\n", 4} + }; + if_cold (writev(fd, &iov, 3) == -1) { +#endif + errmsg_errorsys("couldn't write to %" fS, path); } - fclose(f); + os_close(f); } DEF_CCMD_HERE(sst_autoload_disable, "Stop loading SST on game startup", 0) { os_char path[PATH_MAX]; int len = os_strlen(gameinfo_gamedir); - if (len + sizeof("/addons/" VDFBASENAME ".vdf") > - sizeof(path) / sizeof(*path)) { + if_cold (len + ssizeof("/addons/" VDFBASENAME ".vdf") > countof(path)) { errmsg_errorx("path to VDF is too long"); return; } - memcpy(path, gameinfo_gamedir, len * sizeof(*gameinfo_gamedir)); - memcpy(path + len, OS_LIT("/addons/") OS_LIT(VDFBASENAME) OS_LIT(".vdf"), - sizeof("/addons/" VDFBASENAME ".vdf") * sizeof(os_char)); - if (os_unlink(path) == -1 && errno != ENOENT) { - errmsg_warnstd("couldn't delete %" fS, path); + os_spancopy(path, gameinfo_gamedir, len); + os_spancopy(path + len, OS_LIT("/addons/") OS_LIT(VDFBASENAME) OS_LIT(".vdf"), + ssizeof("/addons/" VDFBASENAME ".vdf")); + if (!os_unlink(path)) if_cold (os_lasterror() != OS_ENOENT) { + errmsg_warnsys("couldn't delete %" fS, path); } } @@ -224,11 +238,11 @@ DEF_CCMD_HERE(sst_printversion, "Display plugin version information", 0) { // interested parties identify the version of SST used by just writing a dummy // 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 != 6 +#if VERSION_MAJOR != 0 || VERSION_MINOR != 9 #error Need to change this manually, since codegen requires it to be spelled \ out in DEF_CVAR - better yet, can we get rid of this yet? #endif -DEF_CVAR(__sst_0_6_beta, "", 0, CON_HIDDEN | CON_DEMO) +DEF_CVAR(__sst_0_9_beta, "", 0, CON_HIDDEN | CON_DEMO) // most plugin callbacks are unused - define dummy functions for each signature static void VCALLCONV nop_v_v(void *this) {} @@ -254,14 +268,7 @@ static bool already_loaded = false, skip_unload = false; // auto-update message. see below in do_featureinit() static const char *updatenotes = "\ -* Added sst_xhair_* commands to draw a custom crosshair overlay\n\ -* Added Left 4 Dead cutscene skipping on quick-reset (sst_l4d_quickreset_fastfwd)\n\ -* Fixed crashing on newest Left 4 Dead 2 Steam update\n\ -* Fixed broken autoload registration in some earlier engine branches\n\ -* Added mouse factor support to the current Left 4 Dead 1 version on Steam\n\ -* Removed requirement to install Visual C++ runtime redistributable\n\ -* Made various internal preparations for more cool stuff in the future\n\ -* Cleaned up even more code and fixed a few minor bugs, because nobody's perfect\n\ +* More behind-the-scenes changes, as always\n\ "; #include <featureinit.gen.h> // generated by build/codegen.c @@ -270,23 +277,24 @@ static void do_featureinit(void) { engineapi_lateinit(); // load libs that might not be there early (...at least on Linux???) clientlib = os_dlhandle(OS_LIT("client") OS_LIT(OS_DLSUFFIX)); - if (!clientlib) { + if_cold (!clientlib) { errmsg_warndl("couldn't get the game's client library"); } - else if (!(factory_client = (ifacefactory)os_dlsym(clientlib, + else if_cold (!(factory_client = (ifacefactory)os_dlsym(clientlib, "CreateInterface"))) { errmsg_warndl("couldn't get client's CreateInterface"); } void *inputsystemlib = os_dlhandle(OS_LIT("bin/") OS_LIT("inputsystem") OS_LIT(OS_DLSUFFIX)); - if (!inputsystemlib) { + if_cold (!inputsystemlib) { errmsg_warndl("couldn't get the input system library"); } - else if (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib, + else if_cold (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib, "CreateInterface"))) { errmsg_warndl("couldn't get input system's CreateInterface"); } - else if (!(inputsystem = factory_inputsystem("InputSystemVersion001", 0))) { + else if_cold (!(inputsystem = factory_inputsystem( + "InputSystemVersion001", 0))) { errmsg_warnx("missing input system interface"); } // ... and now for the real magic! @@ -294,7 +302,7 @@ static void do_featureinit(void) { // if we're autoloaded and the external autoupdate script downloaded a new // version, let the user know about the cool new stuff! - if (getenv("SST_UPDATED")) { + if_cold (getenv("SST_UPDATED")) { // avoid displaying again if we're unloaded and reloaded in one session #ifdef _WIN32 SetEnvironmentVariableA("SST_UPDATED", 0); @@ -331,7 +339,7 @@ DECL_VFUNC_DYN(bool, VGuiIsInitialized) // // Route credit to bill for helping figure a lot of this out - mike static bool deferinit(void) { - if (!vgui) { + if_cold (!vgui) { errmsg_warnx("can't use VEngineVGui for deferred feature setup"); goto e; } @@ -341,7 +349,7 @@ static bool deferinit(void) { // CEngineVGui::IsInitialized() which works everywhere. if (VGuiIsInitialized(vgui)) return false; sst_earlyloaded = true; // let other code know - if (!os_mprot(*(void ***)vgui + vtidx_VGuiConnect, sizeof(void *), + if_cold (!os_mprot(*(void ***)vgui + vtidx_VGuiConnect, ssizeof(void *), PAGE_READWRITE)) { errmsg_warnsys("couldn't make CEngineVGui vtable writable for deferred " "feature setup"); @@ -385,7 +393,7 @@ static void hook_plugin_unload_cb(const struct con_cmdargs *args) { if (!CHECK_AllowPluginLoading(false)) return; int idx = atoi(args->argv[1]); struct CPlugin **plugins = pluginhandler->plugins.m.mem; - if (idx >= 0 && idx < pluginhandler->plugins.sz) { + if_hot (idx >= 0 && idx < pluginhandler->plugins.sz) { const struct CPlugin *plugin = plugins[idx]; // XXX: *could* memoise the ispluginv1 call, but... meh. effort. const struct CPlugin_common *common = ispluginv1(plugin) ? @@ -410,12 +418,12 @@ static void hook_plugin_unload_cb(const struct con_cmdargs *args) { } static bool do_load(ifacefactory enginef, ifacefactory serverf) { - if (!hook_init()) { + if_cold (!hook_init()) { errmsg_warnsys("couldn't set up memory for function hooking"); return false; } factory_engine = enginef; factory_server = serverf; - if (!engineapi_init(ifacever)) return false; + if_cold (!engineapi_init(ifacever)) return false; const void **p = vtable_firstdiff; if (GAMETYPE_MATCHES(Portal2)) *p++ = (void *)&nop_p_v; // ClientFullyConnect *p++ = (void *)&nop_p_v; // ClientDisconnect @@ -430,7 +438,7 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { *p++ = (void *)&nop_p_v; // OnEdictAllocated *p = (void *)&nop_p_v; // OnEdictFreed if (!deferinit()) { do_featureinit(); fixes_apply(); } - if (pluginhandler) { + if_hot (pluginhandler) { cmd_plugin_load = con_findcmd("plugin_load"); orig_plugin_load_cb = cmd_plugin_load->cb; cmd_plugin_load->cb = &hook_plugin_load_cb; @@ -442,7 +450,8 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) { } static void do_unload(void) { - if (sst_userunloaded) { // note: pluginhandler must also be set here + // slow path: reloading shouldn't happen all the time, prioritise fast exit + if_cold (sst_userunloaded) { // note: if we're here, pluginhandler is set cmd_plugin_load->cb = orig_plugin_load_cb; cmd_plugin_unload->cb = orig_plugin_unload_cb; #ifdef _WIN32 // this bit is only relevant in builds that predate linux support @@ -463,7 +472,7 @@ static void do_unload(void) { static bool VCALLCONV Load(void *this, ifacefactory enginef, ifacefactory serverf) { - if (already_loaded) { + if_cold (already_loaded) { con_warn("Already loaded! Doing nothing!\n"); skip_unload = true; return false; @@ -475,7 +484,7 @@ static bool VCALLCONV Load(void *this, ifacefactory enginef, static void VCALLCONV Unload(void *this) { // the game tries to unload on a failed load, for some reason - if (skip_unload) { skip_unload = false; return; } + if_cold (skip_unload) { skip_unload = false; return; } do_unload(); } @@ -490,8 +499,6 @@ static const char *VCALLCONV GetPluginDescription(void *this) { return LONGNAME " v" VERSION; } -DECL_VFUNC_DYN(void, ServerCommand, const char *) - DEF_EVENT(ClientActive, struct edict */*player*/) DEF_EVENT(Tick, bool /*simulating*/) @@ -528,9 +535,9 @@ static const void **vtable_firstdiff = vtable + 10; // this is equivalent to a class with no members! static const void *const *const plugin_obj = vtable; -EXPORT const void *CreateInterface(const char *name, int *ret) { - if (!strncmp(name, "ISERVERPLUGINCALLBACKS00", 24)) { - if (name[24] >= '1' && name[24] <= '3' && name[25] == '\0') { +export const void *CreateInterface(const char *name, int *ret) { + if_hot (!strncmp(name, "ISERVERPLUGINCALLBACKS00", 24)) { + if_hot (name[24] >= '1' && name[24] <= '3' && name[25] == '\0') { if (ret) *ret = 0; ifacever = name[24] - '0'; return &plugin_obj; diff --git a/src/stubs/BCryptPrimitives.c b/src/stubs/BCryptPrimitives.c new file mode 100644 index 0000000..afcaa7e --- /dev/null +++ b/src/stubs/BCryptPrimitives.c @@ -0,0 +1,6 @@ +/* This file is dedicated to the public domain. */ + +// We have to define a real function with the right number of arguments to get +// the mangled symbol _ProcessPrng@8, which the .def file apparently turns +// into an "undecorate" .lib entry through some undocumented magic (of course!). +int __stdcall ProcessPrng(void *x, unsigned int y) { return 0; } diff --git a/src/stubs/bcryptprimitives.def b/src/stubs/bcryptprimitives.def new file mode 100644 index 0000000..9021d7c --- /dev/null +++ b/src/stubs/bcryptprimitives.def @@ -0,0 +1,7 @@ +; This file is dedicated to the public domain. + +LIBRARY bcryptprimitives +EXPORTS + ProcessPrng + +; vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/trace.c b/src/trace.c new file mode 100644 index 0000000..0a301a7 --- /dev/null +++ b/src/trace.c @@ -0,0 +1,101 @@ +/* + * Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br> + * 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 "engineapi.h" +#include "errmsg.h" +#include "feature.h" +#include "gametype.h" +#include "intdefs.h" +#include "trace.h" + +FEATURE() + +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 + const float _Alignas(16) (*worldaxistransform)[3][4]; + bool isray, isswept; +}; + +static void *srvtrace; + +DECL_VFUNC(void, TraceRay, 5, struct ray *, uint /*mask*/, void */*filter*/, + struct CGameTrace *) + +static inline bool nonzero(struct vec3f v) { + union { struct vec3f v; struct { unsigned int x, y, z; }; } u = {v}; + return (u.x | u.y | u.z) << 1 != 0; // ignore sign bit +} + +struct CGameTrace trace_line(struct vec3f start, struct vec3f end, uint mask, + void *filt) { + struct CGameTrace t; + struct vec3f delta = {end.x - start.x, end.y - start.y, end.z - start.z}; + struct ray r = { + .isray = true, + .isswept = nonzero(delta), + .start = start, + .delta = delta + }; + TraceRay(srvtrace, &r, mask, filt, &t); + return t; +} + +struct CGameTrace trace_hull(struct vec3f start, struct vec3f end, + struct vec3f mins, struct vec3f maxs, uint mask, void *filt) { + struct CGameTrace t; + struct vec3f delta = {end.x - start.x, end.y - start.y, end.z - start.z}; + struct vec3f extents = { + (maxs.x - mins.x) * 0.5f, + (maxs.y - mins.y) * 0.5f, + (maxs.z - mins.z) * 0.5f + }; + struct ray r = { + // NOTE: could maybe hardcode this to false, but we copy engine logic + // just on the off chance we're tracing some insanely thin hull + .isray = (extents.x * extents.x + r.extents.y * r.extents.y + + extents.z * extents.z) < 1e-6, + .isswept = nonzero(delta), + .start = start, + .delta = delta, + .extents = extents, + .startoff = { + (mins.x + maxs.x) * -0.5f, + (mins.y + maxs.y) * -0.5f, + (mins.z + maxs.z) * -0.5f + } + }; + TraceRay(srvtrace, &r, mask, filt, &t); + return t; +} + +PREINIT { + // TODO(compat): restricting this to tested branches for now + return GAMETYPE_MATCHES(L4D); +} + +INIT { + if (!(srvtrace = factory_engine("EngineTraceServer003", 0))) { + errmsg_errorx("couldn't get server-side tracing interface"); + return false; + } + return true; +} + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/trace.h b/src/trace.h new file mode 100644 index 0000000..82d2dac --- /dev/null +++ b/src/trace.h @@ -0,0 +1,60 @@ +/* + * Copyright © 2023 Willian Henrique <wsimanbrazil@yahoo.com.br> + * + * 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_TRACE_H +#define INC_TRACE_H + +#include "intdefs.h" +#include "engineapi.h" + +struct CBaseTrace { + struct vec3f startpos, endpos; + struct { + struct vec3f normal; + float dist; + u8 type, signbits; + //u8 pad[2]; + } plane; // surface normal at impact + float frac; + int contents; + ushort dispflags; + bool allsolid, startsolid; +}; + +struct CGameTrace { + struct CBaseTrace base; + float fracleftsolid; + struct { + const char *name; + short surfprops; + ushort flags; + } surf; + int hitgroup; + short physbone; + ushort worldsurfidx; // not in every branch, but doesn't break ABI + void *ent; // CBaseEntity (C_BaseEntity in client.dll) + int hitbox; +}; + +struct CGameTrace trace_line(struct vec3f start, struct vec3f end, uint mask, + void *filt); + +struct CGameTrace trace_hull(struct vec3f start, struct vec3f end, + struct vec3f mins, struct vec3f maxs, uint mask, void *filt); + +#endif + +// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/unreachable.h b/src/unreachable.h deleted file mode 100644 index 99c82b5..0000000 --- a/src/unreachable.h +++ /dev/null @@ -1,14 +0,0 @@ -/* This file is dedicated to the public domain. */ - -#ifndef INC_UNREACHABLE_H -#define INC_UNREACHABLE_H - -#if defined(__GNUC__) || defined(__clang__) -#define unreachable __builtin_unreachable() -#else -#define unreachable do; while (0) -#endif - -#endif - -// vi: sw=4 ts=4 noet tw=80 cc=80 diff --git a/src/version.h b/src/version.h index 2d07166..fc82f66 100644 --- a/src/version.h +++ b/src/version.h @@ -1,5 +1,5 @@ #define NAME "SST" #define LONGNAME "Source Speedrun Tools Beta" #define VERSION_MAJOR 0 -#define VERSION_MINOR 6 -#define VERSION "0.6" +#define VERSION_MINOR 9 +#define VERSION "0.9" @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -40,7 +40,7 @@ static int mrmsib(const uchar *p, int addrlen) { case 0x80: return 1 + addrlen + sib; } } - if (addrlen == 2 && *p == 0x26) return 3; + if (addrlen == 2 && (*p & 0xC7) == 0x06) return 3; return 1; // note: include the mrm itself in the byte count } @@ -65,6 +65,7 @@ P: X86_SEG_PREFIXES(CASES) X86_OPS_1BYTE_NO(CASES) return pfxlen + 1; X86_OPS_1BYTE_I8(CASES) operandlen = 1; X86_OPS_1BYTE_IW(CASES) return pfxlen + 1 + operandlen; + X86_OPS_1BYTE_IWI(CASES) return pfxlen + 1 + addrlen; X86_OPS_1BYTE_I16(CASES) return pfxlen + 3; X86_OPS_1BYTE_MRM(CASES) return pfxlen + 1 + mrmsib(insn + 1, addrlen); X86_OPS_1BYTE_MRM_I8(CASES) operandlen = 1; @@ -25,6 +25,9 @@ */ // XXX: no BOUND (0x62): ambiguous with EVEX prefix - can't be arsed! +// XXX: no LES (0xC4) or DES (0xC5) either for similar reasons. better to report +// an unknown instruction than to potentially misinterpret an AVX thing. +// these are all legacy instructions that won't really be used much anyway. /* Instruction prefixes: segments */ #define X86_SEG_PREFIXES(X) \ @@ -143,8 +146,6 @@ X(X86_XORALI, 0x34) \ X(X86_CMPALI, 0x3C) \ X(X86_PUSHI8, 0x6A) \ - X(X86_MOVALII, 0xA0) /* From offset (indirect) */ \ - X(X86_MOVIIAL, 0xA2) /* To offset (indirect) */ \ X(X86_TESTALI, 0xA8) \ X(X86_JO, 0x70) \ X(X86_JNO, 0x71) \ @@ -190,8 +191,6 @@ X(X86_XOREAXI, 0x35) \ X(X86_CMPEAXI, 0x3D) \ X(X86_PUSHIW, 0x68) \ - X(X86_MOVEAXII, 0xA1) /* From offset (indirect) */ \ - X(X86_MOVIIEAX, 0xA3) /* To offset (indirect) */ \ X(X86_TESTEAXI, 0xA9) \ X(X86_MOVEAXI, 0xB8) \ X(X86_MOVECXI, 0xB9) \ @@ -204,6 +203,13 @@ X(X86_CALL, 0xE8) \ X(X86_JMPIW, 0xE9) +/* Single-byte opcodes with a word-sized immediate operand (indirect) */ +#define X86_OPS_1BYTE_IWI(X) \ + X(X86_MOVALII, 0xA0) /* From offset (indirect) */ \ + X(X86_MOVEAXII, 0xA1) /* From offset (indirect) */ \ + X(X86_MOVIIAL, 0xA2) /* To offset (indirect) */ \ + X(X86_MOVIIEAX, 0xA3) /* To offset (indirect) */ \ + /* Single-byte opcodes with 16-bit immediate operands, regardless of prefixes */ #define X86_OPS_1BYTE_I16(X) \ X(X86_RETI16, 0xC2) \ @@ -259,8 +265,6 @@ X(X86_LEA, 0x8D) \ X(X86_MOVSM, 0x8E) /* Store 4 bytes to segment register */ \ X(X86_POPM, 0x8F) \ - X(X86_LES, 0xC4) \ - X(X86_LDS, 0xC5) \ X(X86_SHIFTM18, 0xD0) /* Shift/roll by 1 place */ \ X(X86_SHIFTM1W, 0xD1) /* Shift/roll by 1 place */ \ X(X86_SHIFTMCL8, 0xD2) /* Shift/roll by CL places */ \ @@ -297,6 +301,7 @@ X86_OPS_1BYTE_NO(X) \ X86_OPS_1BYTE_I8(X) \ X86_OPS_1BYTE_IW(X) \ + X86_OPS_1BYTE_IWI(X) \ X86_OPS_1BYTE_I16(X) \ X86_OPS_1BYTE_MRM(X) \ X86_OPS_1BYTE_MRM_I8(X) \ diff --git a/src/x86util.h b/src/x86util.h index 85a824e..9fcdfff 100644 --- a/src/x86util.h +++ b/src/x86util.h @@ -1,5 +1,5 @@ /* - * Copyright © 2022 Michael Smith <mikesmiffy128@gmail.com> + * 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 @@ -18,14 +18,15 @@ #define INC_X86UTIL_H #include "errmsg.h" +#include "langext.h" #include "x86.h" // XXX: don't know where else to put this, or how else to design this, so this -// is very much a plonk-it-here-for-now scenario. +// is very much a plonk-it-here-for-now scenario (and has been for years!) #define NEXT_INSN(p, tgt) do { \ int _len = x86_len(p); \ - if (_len == -1) { \ + if_cold (_len == -1) { \ errmsg_errorx("unknown or invalid instruction looking for %s", tgt); \ return false; \ } \ |