summaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/3p/chibicc/tokenize.c4
-rw-r--r--src/ac.c121
-rw-r--r--src/alias.c6
-rw-r--r--src/autojump.c16
-rw-r--r--src/bind.c5
-rw-r--r--src/bitbuf.h7
-rw-r--r--src/build/cmeta.c22
-rw-r--r--src/build/cmeta.h4
-rw-r--r--src/build/codegen.c9
-rw-r--r--src/build/mkentprops.c459
-rw-r--r--src/build/mkgamedata.c349
-rw-r--r--src/build/skiplist.h3
-rw-r--r--src/build/vec.h2
-rw-r--r--src/chunklets/msg.c6
-rw-r--r--src/clientcon.c41
-rw-r--r--src/clientcon.h33
-rw-r--r--src/democustom.c3
-rw-r--r--src/demorec.c15
-rw-r--r--src/engineapi.c26
-rw-r--r--src/engineapi.h38
-rw-r--r--src/ent.c16
-rw-r--r--src/errmsg.h20
-rw-r--r--src/extmalloc.c9
-rw-r--r--src/fastfwd.c46
-rw-r--r--src/fixes.c46
-rw-r--r--src/fov.c11
-rw-r--r--src/gameinfo.c14
-rw-r--r--src/gameserver.c5
-rw-r--r--src/gametype.h21
-rw-r--r--src/hook.c12
-rw-r--r--src/hud.c31
-rw-r--r--src/hud.h17
-rw-r--r--src/inputhud.c443
-rw-r--r--src/kv.c290
-rw-r--r--src/kv.h95
-rw-r--r--src/kvsys.c5
-rw-r--r--src/l4dmm.c27
-rw-r--r--src/l4dreset.c34
-rw-r--r--src/l4dwarp.c297
-rw-r--r--src/langext.h64
-rw-r--r--src/mem.h2
-rw-r--r--src/nomute.c21
-rw-r--r--src/noreturn.h11
-rw-r--r--src/nosleep.c5
-rw-r--r--src/os-unix.h125
-rw-r--r--src/os-win32.h131
-rw-r--r--src/os.c204
-rw-r--r--src/os.h250
-rw-r--r--src/portalcolours.c7
-rw-r--r--src/rinput.c33
-rw-r--r--src/sst.c157
-rw-r--r--src/stubs/BCryptPrimitives.c6
-rw-r--r--src/stubs/bcryptprimitives.def7
-rw-r--r--src/trace.c101
-rw-r--r--src/trace.h60
-rw-r--r--src/unreachable.h14
-rw-r--r--src/version.h4
-rw-r--r--src/x86.c5
-rw-r--r--src/x86.h17
-rw-r--r--src/x86util.h7
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;
diff --git a/src/ac.c b/src/ac.c
index 8b163b9..7f3157c 100644
--- a/src/ac.c
+++ b/src/ac.c
@@ -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,
diff --git a/src/bind.c b/src/bind.c
index deb943e..327d0c2 100644
--- a/src/bind.c
+++ b/src/bind.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 "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
diff --git a/src/ent.c b/src/ent.c
index a1ad08f..1845b1f 100644
--- a/src/ent.c
+++ b/src/ent.c
@@ -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
diff --git a/src/fov.c b/src/fov.c
index 73cd17e..f4b8575 100644
--- a/src/fov.c
+++ b/src/fov.c
@@ -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)
diff --git a/src/hook.c b/src/hook.c
index 9a6e885..9e5d694 100644
--- a/src/hook.c
+++ b/src/hook.c
@@ -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;
}
diff --git a/src/hud.c b/src/hud.c
index 24d8563..2334bb8 100644
--- a/src/hud.c
+++ b/src/hud.c
@@ -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);
}
diff --git a/src/hud.h b/src/hud.h
index 8ea0877..e3aea1d 100644
--- a/src/hud.h
+++ b/src/hud.h
@@ -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
diff --git a/src/mem.h b/src/mem.h
index 7b1a0b0..6acf018 100644
--- a/src/mem.h
+++ b/src/mem.h
@@ -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
diff --git a/src/os.h b/src/os.h
index c954d1a..cbecf2c 100644
--- a/src/os.h
+++ b/src/os.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,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);
}
diff --git a/src/sst.c b/src/sst.c
index e64ea58..5f09991 100644
--- a/src/sst.c
+++ b/src/sst.c
@@ -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"
diff --git a/src/x86.c b/src/x86.c
index 7a5d00e..5399af8 100644
--- a/src/x86.c
+++ b/src/x86.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
@@ -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;
diff --git a/src/x86.h b/src/x86.h
index a62c0ee..b4df9c8 100644
--- a/src/x86.h
+++ b/src/x86.h
@@ -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; \
} \