aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--DevDocs/code-style.txt28
-rw-r--r--README27
-rwxr-xr-xcompile8
-rw-r--r--compile.bat9
-rw-r--r--gamedata/engine.txt38
-rw-r--r--gamedata/gamelib.txt2
-rw-r--r--gamedata/vguimatsurface.txt31
-rw-r--r--gamedata/vphysics.txt10
-rw-r--r--src/ac.c2
-rw-r--r--src/accessor.h2
-rw-r--r--src/alias.c15
-rw-r--r--src/autojump.c2
-rw-r--r--src/bind.c10
-rw-r--r--src/build/gluegen.c66
-rw-r--r--src/build/mkentprops.c1
-rw-r--r--src/build/mkgamedata.c40
-rw-r--r--src/chatrate.c15
-rw-r--r--src/chunklets/README-x8656
-rw-r--r--src/chunklets/fastspin.c6
-rw-r--r--src/chunklets/x86.c (renamed from src/x86.c)28
-rw-r--r--src/chunklets/x86.h (renamed from src/x86.h)32
-rw-r--r--src/con_.c720
-rw-r--r--src/con_.h261
-rw-r--r--src/dbg.c12
-rw-r--r--src/democustom.c2
-rw-r--r--src/demorec.c37
-rw-r--r--src/engineapi.c42
-rw-r--r--src/engineapi.h19
-rw-r--r--src/ent.c7
-rw-r--r--src/fastfwd.c5
-rw-r--r--src/feature.h20
-rw-r--r--src/fixes.c140
-rw-r--r--src/fov.c25
-rw-r--r--src/gamedata.h9
-rw-r--r--src/gameinfo.c2
-rw-r--r--src/gameserver.c7
-rw-r--r--src/gametype.h69
-rw-r--r--src/hook.c2
-rw-r--r--src/hud.c10
-rw-r--r--src/inputhud.c24
-rw-r--r--src/kvsys.c2
-rw-r--r--src/l4d1democompat.c31
-rw-r--r--src/l4daddon.c7
-rw-r--r--src/l4dreset.c18
-rw-r--r--src/l4dwarp.c12
-rw-r--r--src/langext.h18
-rw-r--r--src/mem.h5
-rw-r--r--src/nomute.c4
-rw-r--r--src/nosleep.c4
-rw-r--r--src/portalcolours.c6
-rw-r--r--src/portalisg.c152
-rw-r--r--src/rinput.c11
-rw-r--r--src/sst.c231
-rw-r--r--src/vcall.h7
-rw-r--r--src/version.h4
-rw-r--r--src/wincrt.c29
-rw-r--r--src/x86util.h2
-rw-r--r--src/xhair.c1
-rw-r--r--test/hook.test.c2
-rw-r--r--test/x86.test.c5
-rw-r--r--tools/mkbindist.bat4
-rw-r--r--tools/steamfix.bat27
-rw-r--r--tools/windbg/.gitignore1
-rw-r--r--tools/windbg/initcmds9
-rw-r--r--tools/windbg/install.ps144
-rw-r--r--tools/windbg/natvis.xml5
-rw-r--r--tools/windbg/windbg.bat16
68 files changed, 1726 insertions, 773 deletions
diff --git a/.gitignore b/.gitignore
index d880120..08d5f85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
/.build/
/sst.dll
-/sst.pdb
/sst.so
/compile_commands.json
/compile_flags.txt
diff --git a/DevDocs/code-style.txt b/DevDocs/code-style.txt
index 189d0f8..cdbc666 100644
--- a/DevDocs/code-style.txt
+++ b/DevDocs/code-style.txt
@@ -129,13 +129,13 @@ In no particular order:
child and now my editor still does it by default in C so it’s just kind of
what I do. I think it looks fine and at least one other option is worse.
- "Bad" example:
+ “Bad” example:
if (cond1 &&
cond2) {
do_thing();
}
- "Good" example:
+ “Good” example:
if (cond1 &&
cond2) {
do_thing();
@@ -169,7 +169,7 @@ In no particular order:
Bad example:
call_function("parameter 1 takes up some space", param_2 + 5, another_var,
- x);
+ x);
Good example:
call_function("parameter 1 takes up some space", param_2 + 5,
@@ -292,7 +292,7 @@ In no particular order:
Rationale:
Having the if and else line up in the same column looks better to me when
scanning vertically through the file. It also makes it easier to edit and move
- things aruond when selection can be done linewise, and easier to comment stuff
+ things around when selection can be done linewise, and easier to comment stuff
out temporarily for debugging and such.
Bad example:
@@ -530,7 +530,7 @@ In no particular order:
// to that thing I
// mentioned.
Good example:
- // either condition cam trigger this here due to that thing I mentioned.
+ // either condition can trigger this here due to that thing I mentioned.
if (cond1 || cond2) reasonably_long_call(param1);
Pro tip: if you use Vim or Neovim as I do, consider doing set fo-=oc in your
@@ -543,14 +543,6 @@ In no particular order:
Firstly we have some rules about interfacing with Valve’s code, since that’s
kind of a core thing we have to do in this project:
-• Use British English in all code and documentation.
-
- Rationale:
- I am from Scotland.
-
- Bad example: void analyze();
- Good example: void analyse();
-
• Use exact SDK names (where known) for Source engine types (structs/classes).
Rationale:
@@ -602,6 +594,14 @@ kind of a core thing we have to do in this project:
The rest of these points apply to first-party code, i.e., SST itself:
+• Use British English in all first-party code and documentation.
+
+ Rationale:
+ I am from Scotland.
+
+ Bad example: static void analyze();
+ Good example: static void analyse();
+
• Functions, variable names and struct/enum tags should all be lowercase.
Rationale:
@@ -646,7 +646,7 @@ The rest of these points apply to first-party code, i.e., SST itself:
Caveat:
I feel like I might have unthinkingly broken this rule myself at some point,
and at the time of writing I’m not intent on doing a full cleanup pass to find
- out. Said cleanup pass is bound happen at some point though, don’t worry.
+ out. Said cleanup pass is bound to happen at some point though, don’t worry.
• The case of macros… kind of depends. If your macro is a constant, or a
function-like macro that expands to a blob of context-dependent syntax, then
diff --git a/README b/README
index df7aac0..af0b2b6 100644
--- a/README
+++ b/README
@@ -19,16 +19,33 @@ Windows:
Linux:
• Install Clang (and LLD) via your system package manager. Technically, GCC
should be able to compile most of this too, but we are currently relying on
- a Clang-specific extension or two, and GCC in general doesn't get tested nor
- used for binary releases, so it's probably not worth wasting time on.
+ a Clang-specific extension or two, and GCC in general doesn’t get tested nor
+ used for binary releases, so it’s probably not worth wasting time on.
• Install 32-bit glibc and libstdc++ libraries and associated C headers if
they’re not already installed.
• Run ./compile (in lieu of a better build tool, to be added later).
NOTE: Linux code should maybe compile now but still crashes on cvar registration
and almost none of the features usefully work. In other words, it needs quite a
-lot more development before it's of use to anyone. It's also not actively tested
-really so don't be surprised if it doesn't compile at all again at some point.
+lot more development before it’s of use to anyone. It’s also not actively tested
+really so don’t be surprised if it doesn’t compile at all again at some point.
+
+════ Debugging ════
+
+On Windows, SST’s preferred debugger is WinDBG (specifically the new frontend),
+because it does everything we need, is free, and isn’t horribly slow usually.
+
+The script tools/windbg/windbg.bat will automatically download the latest
+version into tools/windbg/bin and run it. Alternatively, if you already have a
+copy, set the environment variable WINDBG_BIN and that copy will be used
+instead.
+
+NatVis definitions are contained in tools/windbg/natvis.xml. Currently there is
+not much in there but it can be expanded as and when useful things come up.
+
+Note that after debugging some specific games (mainly some old versions of Left
+4 Dead 2) it may be necessary to run tools/steamfix.bat to make other Steam
+games launch correctly again.
════ How and where to install ════
@@ -49,7 +66,7 @@ make a directory for SST in the top-level engine directory and do for instance
`plugin_load ../SST/sst`. The way the paths work out, that always works no
matter what, and also avoids cluttering up your game files.
-When actively developing the plugin, it's possible to back out of the game
+When actively developing the plugin, it’s possible to back out of the game
installation with `../../` etcetera and load from anywhere you want, as long as
it’s not on a different Windows drive letter. This is essentially the best way
to work with SST built from source as it avoids the need to copy it to different
diff --git a/compile b/compile
index 2ff2ae2..6e36880 100755
--- a/compile
+++ b/compile
@@ -25,10 +25,10 @@ stdflags="-std=c2x -D_DEFAULT_SOURCE -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64"
dbg=0
if [ "$dbg" = 1 ]; then
- cflags="-O0 -g3 -fsanitize-trap=undefined -DSST_DBG"
+ cflags="-O0 -g3 -masm=intel -fsanitize-trap=undefined -DSST_DBG"
ldflags="-O0 -g3"
else
- cflags="-O2 -fvisibility=hidden"
+ cflags="-O2 -fvisibility=hidden -masm=intel"
ldflags="-O2 -s"
fi
@@ -58,6 +58,7 @@ src="\
chatrate.c
chunklets/fastspin.c
chunklets/msg.c
+ chunklets/x86.c
clientcon.c
con_.c
crypto.c
@@ -86,7 +87,6 @@ src="\
portalcolours.c
sst.c
trace.c
- x86.c
xhair.c"
if [ "$dbg" = 1 ]; then src="$src \
dbg.c
@@ -101,7 +101,7 @@ $HOSTCC -O2 -fuse-ld=lld $warnings $stdflags \
-o .build/mkentprops src/build/mkentprops.c src/os.c
.build/gluegen `for s in $src; do echo "src/$s"; done`
.build/mkgamedata gamedata/engine.txt gamedata/gamelib.txt gamedata/inputsystem.txt \
-gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt
+gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt gamedata/vphysics.txt
.build/mkentprops gamedata/entprops.txt
for s in $src; do cc "$s"; done
$CC -shared -fpic -fuse-ld=lld -O0 -w -o .build/libtier0.so src/stubs/tier0.c
diff --git a/compile.bat b/compile.bat
index 543e50b..0affd05 100644
--- a/compile.bat
+++ b/compile.bat
@@ -26,10 +26,10 @@ set dbg=0
:: XXX: -Og would be nice but apparently a bunch of stuff still gets inlined
:: which can be somewhat annoying so -O0 it is.
if "%dbg%"=="1" (
- set cflags=-O0 -g3 -fsanitize-trap=undefined -DSST_DBG
+ set cflags=-O0 -g3 -masm=intel -fsanitize-trap=undefined -DSST_DBG
set ldflags=-O0 -g3
) else (
- set cflags=-O2
+ set cflags=-O2 -masm=intel
set ldflags=-O2
)
@@ -68,6 +68,7 @@ setlocal DisableDelayedExpansion
:+ chatrate.c
:+ chunklets/fastspin.c
:+ chunklets/msg.c
+:+ chunklets/x86.c
:+ crypto.c
:+ democustom.c
:+ demorec.c
@@ -94,10 +95,10 @@ setlocal DisableDelayedExpansion
:+ nosleep.c
:+ os.c
:+ portalcolours.c
+:+ portalisg.c
:+ rinput.c
:+ sst.c
:+ trace.c
-:+ x86.c
:+ xhair.c
:: just tack these on, whatever (repeated condition because of expansion memes)
if "%dbg%"=="1" set src=%src% src/dbg.c
@@ -122,7 +123,7 @@ if %host64%==1 (
-L.build %lbcryptprimitives_host% -o .build/mkentprops.exe src/build/mkentprops.c src/os.c || goto :end
.build\gluegen.exe%src% || goto :end
.build\mkgamedata.exe gamedata/engine.txt gamedata/gamelib.txt gamedata/inputsystem.txt ^
-gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt || goto :end
+gamedata/matchmaking.txt gamedata/vgui2.txt gamedata/vguimatsurface.txt gamedata/vphysics.txt || goto :end
.build\mkentprops.exe gamedata/entprops.txt || goto :end
llvm-rc /FO .build\dll.res src\dll.rc || goto :end
for %%b in (%src%) do ( call :cc %%b || goto :end )
diff --git a/gamedata/engine.txt b/gamedata/engine.txt
index a50f9ce..67cf47e 100644
--- a/gamedata/engine.txt
+++ b/gamedata/engine.txt
@@ -1,22 +1,27 @@
# ICvar
# XXX: const and non-const entries might be flipped here on Windows, not 100%
# sure. kind of just choosing not to care thusfar, as it still works the same!
-vtidx_AllocateDLLIdentifier 5
- Portal2 8
+vtidx_AllocateDLLIdentifier
+ !OE 5
+ Portal2 8
vtidx_RegisterConCommand 6
+ OE 5 # named RegisterConCommandBase here, but same thing
Portal2 9
-vtidx_UnregisterConCommands 8
- Portal2 11
-# unused:
-#vtidx_FindCommandBase 10
-# Portal2 13
+vtidx_UnregisterConCommands
+ !OE 8
+ Portal2 11
vtidx_FindVar 12
+ OE 7
Portal2 15
-vtidx_FindCommand 14
- Portal2 17
-vtidx_CallGlobalChangeCallbacks 20
- L4Dx 18
- Portal2 21
+vtidx_FindCommand
+ !OE 14
+ Portal2 17
+vtidx_CallGlobalChangeCallbacks
+ !OE 20
+ L4Dx 18
+ Portal2 21
+vtidx_CallGlobalChangeCallbacks_OE # different function signature, no float arg
+ OE 12
vtidx_ConsoleColorPrintf
OrangeBoxbased 23
L4Dx 21
@@ -88,9 +93,11 @@ vtidx_SetPaintEnabled 67
L4D2_2125plus 72
Client014
L4D2 70
+ 2013 72
vtidx_Paint 123
Client014
L4D2 126 # 2000
+ 2013 127
Client013
L4D2 127 # 2045
L4D2_2125plus 128
@@ -145,9 +152,10 @@ vtidx_GetPanel NVDTOR
vtidx_VGuiConnect 3 + NVDTOR
L4Dbased 4 + NVDTOR # ActivateGameUI added
L4DS 5 + NVDTOR # some other crap added, god knows
-vtidx_VGuiIsInitialized 6 + NVDTOR # this is also just called IsInitialized()
- L4Dbased 7 + NVDTOR
- L4DS 8 + NVDTOR
+vtidx_VGuiIsInitialized # this is also just called IsInitialized()
+ !OE 6 + NVDTOR
+ L4Dbased 7 + NVDTOR
+ L4DS 8 + NVDTOR
# CDedicatedServerAPI
vtidx_RunFrame 7
diff --git a/gamedata/gamelib.txt b/gamedata/gamelib.txt
index 486a216..26e4758 100644
--- a/gamedata/gamelib.txt
+++ b/gamedata/gamelib.txt
@@ -1,11 +1,13 @@
# CGameMovement
vtidx_CheckJumpButton
+ OE 14 + NVDTOR
Portal1_3420 22 + NVDTOR
2013 28 + NVDTOR
L4D 32 + NVDTOR
L4DS 33 + NVDTOR
Portal2 35 + NVDTOR
off_mv 8
+ OE 4
Portal1_3420 4
# IServerGameDLL
diff --git a/gamedata/vguimatsurface.txt b/gamedata/vguimatsurface.txt
index dbb5891..ed9ed54 100644
--- a/gamedata/vguimatsurface.txt
+++ b/gamedata/vguimatsurface.txt
@@ -27,26 +27,45 @@ vtidx_DrawPrintText
OrangeBoxbased 22
L4D 22
vtidx_GetScreenSize
- OrangeBoxbased 37
+ Client013
+ OrangeBoxbased 37
+ Client014
+ 2013 38
L4D 37
L4D2_2125plus 35
# Unused: currently no good way to create custom fonts without leaking them
#vtidx_CreateFont
-# OrangeBoxbased 64
+# Client013
+# OrangeBoxbased 64
+# Client014
+# 2013 66
# L4D 64
# L4D2_2125plus 63
#vtidx_SetFontGlyphSet
-# OrangeBoxbased 65
+# Client013
+# OrangeBoxbased 65
+# Client014
+# 2013 67
# L4D 65
# L4D2_2125plus 64
vtidx_GetFontTall
- OrangeBoxbased 67
+ Client013
+ OrangeBoxbased 67
+ Client014
+ 2013 69
+ OrangeBox 67
L4D 67
vtidx_GetCharacterWidth
- OrangeBoxbased 71
+ Client013
+ OrangeBoxbased 71
+ Client014
+ 2013 74
L4D 71
vtidx_GetTextSize
- OrangeBoxbased 72
+ Client013
+ OrangeBoxbased 72
+ Client014
+ 2013 75
L4D 72
# vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/gamedata/vphysics.txt b/gamedata/vphysics.txt
new file mode 100644
index 0000000..9c8bfb6
--- /dev/null
+++ b/gamedata/vphysics.txt
@@ -0,0 +1,10 @@
+# IPhysics
+vtidx_CreateEnvironment 5
+
+# IPhysicsEnvironment
+vtidx_CreatePolyObject 7
+
+# IPhysicsObject
+vtidx_RecheckCollisionFilter 26
+
+# vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/ac.c b/src/ac.c
index 00edaed..e762083 100644
--- a/src/ac.c
+++ b/src/ac.c
@@ -27,6 +27,7 @@
#include "bind.h"
#include "chunklets/fastspin.h"
#include "chunklets/msg.h"
+#include "chunklets/x86.h"
#include "con_.h"
#include "crypto.h"
#include "democustom.h"
@@ -45,7 +46,6 @@
#include "ppmagic.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE()
diff --git a/src/accessor.h b/src/accessor.h
index ec63b3c..ef05309 100644
--- a/src/accessor.h
+++ b/src/accessor.h
@@ -21,7 +21,7 @@
#include "mem.h"
#if defined(__GNUC__) || defined(__clang__)
-#define _ACCESSOR_UNUSED __attribute__((unused))
+#define _ACCESSOR_UNUSED __attribute((unused))
#else
#define _ACCESSOR_UNUSED
#endif
diff --git a/src/alias.c b/src/alias.c
index 300ea10..add8add 100644
--- a/src/alias.c
+++ b/src/alias.c
@@ -17,13 +17,13 @@
#include <string.h>
#include "alias.h"
+#include "chunklets/x86.h"
#include "con_.h"
#include "errmsg.h"
#include "extmalloc.h"
#include "feature.h"
#include "gametype.h"
#include "mem.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("alias management")
@@ -51,7 +51,7 @@ void alias_rm(const char *name) {
}
DEF_FEAT_CCMD_HERE(sst_alias_clear, "Remove all command aliases", 0) {
- if (cmd->argc != 1) {
+ if (argc != 1) {
con_warn("usage: sst_alias_clear\n");
return;
}
@@ -59,19 +59,18 @@ DEF_FEAT_CCMD_HERE(sst_alias_clear, "Remove all command aliases", 0) {
}
DEF_FEAT_CCMD_HERE(sst_alias_remove, "Remove a command alias", 0) {
- if (cmd->argc != 2) {
+ if (argc != 2) {
con_warn("usage: sst_alias_remove name\n");
return;
}
- if (strlen(cmd->argv[1]) > 31) {
+ if (strlen(argv[1]) > 31) {
con_warn("invalid alias name (too long)\n");
return;
}
- alias_rm(cmd->argv[1]);
+ alias_rm(argv[1]);
}
-static bool find_alias_head(con_cmdcb alias_cb) {
- const uchar *insns = (const uchar *)alias_cb;
+static bool find_alias_head(const uchar *insns) {
#ifdef _WIN32
for (const uchar *p = insns; p - insns < 64;) {
// alias command with no args calls ConMsg() then loads the head pointer
@@ -94,7 +93,7 @@ INIT {
if (GAMETYPE_MATCHES(Portal2)) return FEAT_INCOMPAT;
struct con_cmd *cmd_alias = con_findcmd("alias");
- if_cold (!find_alias_head(con_getcmdcb(cmd_alias))) {
+ if_cold (!find_alias_head(cmd_alias->cb_insns)) {
errmsg_warnx("couldn't find alias list");
return FEAT_INCOMPAT;
}
diff --git a/src/autojump.c b/src/autojump.c
index 2656796..eb0e34f 100644
--- a/src/autojump.c
+++ b/src/autojump.c
@@ -78,7 +78,7 @@ static bool unprot(struct CGameMovement *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) if_cold (!con_getvari(sv_cheats)) {
+ if (con_getvari(this)) 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);
diff --git a/src/bind.c b/src/bind.c
index a746ef5..3a7cb34 100644
--- a/src/bind.c
+++ b/src/bind.c
@@ -14,15 +14,13 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
+#include "chunklets/x86.h"
#include "con_.h"
-#include "dbg.h"
#include "errmsg.h"
#include "feature.h"
-#include "hook.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
-#include "x86.h"
#include "x86util.h"
FEATURE()
@@ -36,9 +34,8 @@ static struct keyinfo *keyinfo; // engine keybinds list (s_pKeyInfo[])
const char *bind_get(int keycode) { return keyinfo[keycode].binding; }
-static bool find_keyinfo(con_cmdcb klbc_cb) {
+static bool find_keyinfo(const uchar *insns) {
#ifdef _WIN32
- const uchar *insns = (const uchar *)klbc_cb;
for (const uchar *p = insns; p - insns < 64;) {
// key_listboundkeys loops through each index, moving into a register:
// mov <reg>, dword ptr [<reg> * 8 + s_pKeyInfo]
@@ -57,8 +54,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_cold (!find_keyinfo(cb)) {
+ if_cold (!find_keyinfo(cmd_key_listboundkeys->cb_insns)) {
errmsg_warnx("couldn't find key binding list");
return FEAT_INCOMPAT;
}
diff --git a/src/build/gluegen.c b/src/build/gluegen.c
index 905706c..574aa92 100644
--- a/src/build/gluegen.c
+++ b/src/build/gluegen.c
@@ -749,16 +749,23 @@ F( " feats.preinit_%.*s = _feat_preinit_%.*s();",
_( "}")
_( "")
_( "static inline void initfeatures() {")
+ // note: hidden flag could be 0 on OE but it's useful to know which things
+ // *would* be hidden. in particular, GetHelpText currently checks for both
+ // CON_INIT_HIDDEN and _CON_NE_HIDDEN when deciding whether to prepend
+ // the unsupported marker to the help text. the value of CON_INIT_HIDDEN
+ // is otherwise unused in OE so won't do any harm being set all the time.
+_( " int _hiddenflag = GAMETYPE_MATCHES(OE) ?")
+_( " CON_INIT_HIDDEN : _CON_NE_HIDDEN;")
for (int i = 0; i < nfeatures; ++i) { // N.B.: this *should* be 0-indexed!
const char *else_ = "";
s16 mod = feat_initorder[i];
+F( " s8 status_%.*s;", mod_names[mod].len, mod_names[mod].s)
if (mod_flags[mod] & HAS_PREINIT) {
-F( " s8 status_%.*s = feats.preinit_%.*s;",
+F( " if (feats.preinit_%.*s != FEAT_OK) status_%.*s = feats.preinit_%.*s;",
+ mod_names[mod].len, mod_names[mod].s,
mod_names[mod].len, mod_names[mod].s,
mod_names[mod].len, mod_names[mod].s)
- }
- else {
-F( " s8 status_%.*s;", mod_names[mod].len, mod_names[mod].s)
+ else_ = "else ";
}
if (mod_gamespecific[mod].s) {
F( " %sif (!GAMETYPE_MATCHES(%.*s)) status_%.*s = FEAT_SKIP;", else_,
@@ -767,48 +774,41 @@ F( " %sif (!GAMETYPE_MATCHES(%.*s)) status_%.*s = FEAT_SKIP;", else_,
else_ = "else ";
}
list_foreach (struct cmeta_slice, gamedata, mod_gamedata + mod) {
- // this is not a *totally* ideal way of doing this, but it's easy.
- // if we had some info about what gamedata was doing, we could avoid
- // having to ifdef these cases and could just directly generate the
- // right thing. but that'd be quite a bit of work, so... we don't!
if (mod_gamespecific[mod].s) {
-F( "#ifdef _GAMES_WITH_%.*s", gamedata.len, gamedata.s)
-F( " %sif (!(_gametype_tag_%.*s & _GAMES_WITH_%.*s) && !has_%.*s) {", else_,
- mod_gamespecific[mod].len, mod_gamespecific[mod].s,
- gamedata.len, gamedata.s, gamedata.len, gamedata.s)
+F( " %sif (!_HAS_%.*s(_gametype_tag_%.*s)) {", else_,
+ gamedata.len, gamedata.s,
+ mod_gamespecific[mod].len, mod_gamespecific[mod].s)
F( " status_%.*s = NOGD;", mod_names[mod].len, mod_names[mod].s)
_( " }")
-_( "#else")
}
-F( " %sif (!has_%.*s) status_%.*s = NOGD;", else_,
- gamedata.len, gamedata.s, mod_names[mod].len, mod_names[mod].s)
- if (mod_gamespecific[mod].s) {
-_( "#endif")
+ else {
+F( " %sif (!_HAS_%.*s(0)) status_%.*s = NOGD;", else_,
+ gamedata.len, gamedata.s, mod_names[mod].len, mod_names[mod].s)
}
else_ = "else ";
}
list_foreach (struct cmeta_slice, global, mod_globals + mod) {
F( " %sif (!(%.*s)) status_%.*s = NOGLOBAL;", else_,
- global.len, global.s, mod_names[mod].len, mod_names[mod].s)
+ global.len, global.s, mod_names[mod].len, mod_names[mod].s)
else_ = "else ";
}
list_foreach (s16, dep, mod_needs + mod) {
F( " %sif (status_%.*s != FEAT_OK) status_%.*s = REQFAIL;", else_,
- mod_names[dep].len, mod_names[dep].s,
- mod_names[mod].len, mod_names[mod].s)
+ mod_names[dep].len, mod_names[dep].s,
+ mod_names[mod].len, mod_names[mod].s)
else_ = "else ";
}
if (mod_flags[mod] & (HAS_END | HAS_EVENTS | HAS_OPTDEPS)) {
F( " %sif ((status_%.*s = _feat_init_%.*s()) == FEAT_OK) has_%.*s = true;",
- else_,
- mod_names[mod].len, mod_names[mod].s,
- mod_names[mod].len, mod_names[mod].s,
- mod_names[mod].len, mod_names[mod].s)
+ else_,
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
}
else {
F( " %sstatus_%.*s = _feat_init_%.*s();", else_,
- mod_names[mod].len, mod_names[mod].s,
- mod_names[mod].len, mod_names[mod].s)
+ mod_names[mod].len, mod_names[mod].s,
+ mod_names[mod].len, mod_names[mod].s)
}
}
_( "")
@@ -820,7 +820,7 @@ F( " if (status_%.*s != FEAT_SKIP) {",
modname.len, modname.s)
F( " con_regvar(%.*s);",
cvar_names[i].len, cvar_names[i].s)
-F( " if (status_%.*s != FEAT_OK) %.*s->base.flags |= CON_HIDDEN;",
+F( " if (status_%.*s != FEAT_OK) %.*s->base.flags |= _hiddenflag;",
modname.len, modname.s, cvar_names[i].len, cvar_names[i].s)
_( " }")
}
@@ -869,9 +869,19 @@ _( "}")
_( "")
_( "static inline void freevars() {")
for (int i = 1; i < ncvars; ++i) {
-F( " extfree(%.*s->strval);", cvar_names[i].len, cvar_names[i].s)
+F( " extfree(con_getvarcommon(%.*s)->strval);",
+ cvar_names[i].len, cvar_names[i].s)
}
_( "}")
+_( "")
+_( "static inline void shuntvars() {")
+_( "#ifdef _WIN32")
+ for (int i = 1; i < ncvars; ++i) {
+F( " memmove(&%.*s->v1, &%.*s->v2, sizeof(struct con_var_common));",
+ cvar_names[i].len, cvar_names[i].s, cvar_names[i].len, cvar_names[i].s)
+ }
+_( "#endif")
+_( "}")
for (int i = 1; i < nevents; ++i) {
const char *prefix = event_predicateflags[i] ?
"bool CHECK_" : "void EMIT_";
diff --git a/src/build/mkentprops.c b/src/build/mkentprops.c
index 17d9aa0..7c195d0 100644
--- a/src/build/mkentprops.c
+++ b/src/build/mkentprops.c
@@ -350,6 +350,7 @@ static inline void dodecls(FILE *out) {
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!
+F( "#define _HAS_%s(x) has_%s", s, s); // HACK: stupid dupe for gluegen
}
}
diff --git a/src/build/mkgamedata.c b/src/build/mkgamedata.c
index f187028..e0d08f3 100644
--- a/src/build/mkgamedata.c
+++ b/src/build/mkgamedata.c
@@ -163,28 +163,29 @@ _( "")
static inline void knowngames(FILE *out) {
// kind of tricky optimisation: if a gamedata entry has no default but
// does have game-specific values which match a feature's GAMESPECIFIC()
- // macro, load-time conditional checks resulting from REQUIRE_GAMEDATA() can
- // be elided at compile-time.
+ // macro, we can elide has_* and REQUIRE_GAMEDATA() checks at compile time.
for (int i = 0, j; i < nents - 1; i = j) {
while (exprs[i]) { // if there's a default value, we don't need this
// skip to next unindented thing, return if there isn't one with at
// least one indented thing under it.
- for (++i; indents[i] != 0; ++i) if (i == nents - 1) return;
+ do {
+ if (++i == nents - 1) return;
+ } while (indents[i] != 0);
}
F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
- if_cold (fprintf(out, "#define _GAMES_WITH_%s (", sbase + tags[i]) < 0) {
+ if_cold (fprintf(out, "#define _GAMES_WITH_%s (0", sbase + tags[i]) < 0) {
diewrite();
}
- const char *pipe = "";
for (j = i + 1; j < nents && indents[j] != 0; ++j) {
// don't attempt to optimise for nested conditionals because that's
// way more complicated and also basically defeats the purpose.
- if (indents[j] != 1) continue;
- if_cold (fprintf(out, "%s \\\n\t _gametype_tag_%s", pipe,
- sbase + tags[j]) < 0) {
+ if (indents[j] != 1 || !exprs[j]) continue;
+ bool neg = sbase[tags[j]] == '!';
+ const char *tilde = (const char *)"~" + !neg; // cast away warning
+ if_cold (fprintf(out, " | \\\n\t%s_gametype_tag_%s", tilde,
+ sbase + tags[j] + neg) < 0) {
diewrite();
}
- pipe = " |";
}
fputs(" \\\n)\n", out);
}
@@ -195,17 +196,22 @@ static inline void decls(FILE *out) {
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])
+F( "#define _HAS_%s(feattags) 1", sbase + tags[i])
}
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( "#define _HAS_%s(feattags) ( \\", sbase + tags[i])
+_( " !!feattags && \\")
+F( " (feattags & _GAMES_WITH_%s) == feattags || \\",
+ sbase + tags[i])
+F( " %s != -2147483648 \\", sbase + tags[i])
+_(")")
}
F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
+F( "#define has_%s _HAS_%s(_gamedata_feattags)",
+ 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])
}
@@ -245,14 +251,16 @@ _i("}")
continue;
}
F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
+ bool neg = sbase[tags[i]] == '!';
+ const char *excl = (const char *)"!" + !neg; // cast away warning
if (indents[i] > indents[i - 1]) {
-Fi(" if (GAMETYPE_MATCHES(%s)) {", sbase + tags[i])
+Fi(" if (%sGAMETYPE_MATCHES(%s)) {", excl, sbase + tags[i] + neg);
++indent;
}
else {
_i("}")
F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
-Fi("else if (GAMETYPE_MATCHES(%s)) {", sbase + tags[i])
+Fi("if (%sGAMETYPE_MATCHES(%s)) {", excl, sbase + tags[i] + neg);
}
if (exprs[i]) {
F( "#line %d \"%" fS "\"", srclines[i], srcnames[srcfiles[i]])
diff --git a/src/chatrate.c b/src/chatrate.c
index ba14ea1..daa275a 100644
--- a/src/chatrate.c
+++ b/src/chatrate.c
@@ -14,13 +14,14 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
+#include "chunklets/x86.h"
#include "con_.h"
#include "errmsg.h"
#include "feature.h"
+#include "gametype.h"
#include "intdefs.h"
#include "langext.h"
#include "os.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("chat rate limit removal")
@@ -32,18 +33,16 @@ static uchar *patchedbyte;
// So, instead of adding 0.66 to the current time, we subtract it, and that
// means we can always chat immediately.
-static inline bool find_ratelimit_insn(con_cmdcb say_cb) {
- // Find the add instruction
- uchar *insns = (uchar *)say_cb;
- for (uchar *p = insns; p - insns < 128;) {
+static inline bool find_ratelimit_insn(const uchar *insns) {
+ for (const uchar *p = insns; p - insns < 128;) {
// find FADD
if (p[0] == X86_FLTBLK5 && p[1] == X86_MODRM(0, 0, 5)) {
- patchedbyte = p + 1;
+ patchedbyte = (uchar *)p + 1;
return true;
}
// Portal 2, L4D2 2125-2134, L4D:S all use SSE2, so try finding ADDSD
if (p[0] == X86_PFX_REPN && p[1] == X86_2BYTE & p[2] == X86_2B_ADD) {
- patchedbyte = p + 2;
+ patchedbyte = (uchar *)p + 2;
return true;
}
NEXT_INSN(p, "chat rate limit");
@@ -72,7 +71,7 @@ static inline void unpatch_ratelimit_insn() {
INIT {
struct con_cmd *cmd_say = con_findcmd("say");
if_cold (!cmd_say) return FEAT_INCOMPAT; // should never happen!
- if (!find_ratelimit_insn(cmd_say->cb)) {
+ if (!find_ratelimit_insn(cmd_say->cb_insns)) {
errmsg_errorx("couldn't find chat rate limit instruction");
return FEAT_INCOMPAT;
}
diff --git a/src/chunklets/README-x86 b/src/chunklets/README-x86
new file mode 100644
index 0000000..cbfcb5d
--- /dev/null
+++ b/src/chunklets/README-x86
@@ -0,0 +1,56 @@
+x86.{c,h}: opcode-based x86 instruction analysis (NOT a disassembler)
+
+Currently only handles opcodes found in basic 32-bit userspace functions;
+there’s no kernel-mode instructions, no SSE 3+, no AVX, no REX (64-bit), no
+EVEX, yadda yadda.
+
+Subject to extension later if there’s ever a use for it.
+
+== Compiling ==
+
+ gcc -c -O2 [-flto] x86.c
+ clang -c -O2 [-flto] x86.c
+ tcc -c x86.c
+ cl.exe /c /O2 x86.c
+
+In most cases you can just drop the .c file straight into your codebase/build
+system. LTO is advised to avoid dead code and enable more efficient calls
+including potential inlining.
+
+== Compiler compatibility ==
+
+- Any reasonable GCC
+- Any reasonable Clang
+- Any reasonable MSVC
+- TinyCC
+- Probably almost all others; this is very portable code
+
+Note that GCC and Clang will generally give the best-performing output.
+
+Once the .c file is built, the public header can be consumed by virtually any C
+or C++ compiler, as well as probably most half-decent FFIs.
+
+Note that the .c source file is probably C++-compatible at the moment, but this
+is not guaranteed, so it's best to compile it as a C source. The header will
+work fine from either language.
+
+== API usage ==
+
+See documentation comments in x86.h for a basic idea. Some *pro tips*:
+
+== OS compatibility ==
+
+- All.
+- Seriously, this library doesn’t even use libc.
+
+== Architecture compatibility ==
+
+- All, so long as char is 8 bits.
+
+== Copyright ==
+
+The source file and header both fall under the ISC licence — read the notices in
+both of the files for specifics.
+
+Thanks, and have fun!
+- Michael Smith <mikesmiffy128@gmail.com>
diff --git a/src/chunklets/fastspin.c b/src/chunklets/fastspin.c
index e972b23..d349395 100644
--- a/src/chunklets/fastspin.c
+++ b/src/chunklets/fastspin.c
@@ -29,12 +29,12 @@ _Static_assert(_Alignof(int) == _Alignof(_Atomic int),
#if defined(__GNUC__) || defined(__clang__) || defined(__TINYC__)
#if defined(__i386__) || defined(__x86_64__) || defined(_WIN32) || \
defined(__mips__) // same asm syntax for pause
-#define RELAX() __asm__ volatile ("pause" ::: "memory")
+#define RELAX() __asm volatile ("pause" ::: "memory")
#elif defined(__arm__) || defined(__aarch64__)
-#define RELAX() __asm__ volatile ("yield" ::: "memory")
+#define RELAX() __asm volatile ("yield" ::: "memory")
#elif defined(__powerpc__) || defined(__ppc64__)
// POWER7 (2010) - older arches may be less efficient
-#define RELAX() __asm__ volatile ("or 27, 27, 27" ::: "memory")
+#define RELAX() __asm volatile ("or 27, 27, 27" ::: "memory")
#endif
#elif defined(_MSC_VER)
#if defined(_M_ARM) || defined(_M_ARM64)
diff --git a/src/x86.c b/src/chunklets/x86.c
index b017a70..012cbb0 100644
--- a/src/x86.c
+++ b/src/chunklets/x86.c
@@ -14,39 +14,35 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
-#include "intdefs.h"
+// _Static_assert needs MSVC >= 2019, and this check is irrelevant on Windows
+#ifndef _MSC_VER
+_Static_assert((unsigned char)-1 == 255, "this code requires 8-bit chars");
+#endif
+
#include "x86.h"
-static int mrmsib(const uchar *p, int addrlen) {
- // I won't lie: I thought I almost understood this, but after bill walked me
- // through correcting a bunch of wrong cases I now realise that I don't
- // really understand it at all. If it helps, I used this as a reference:
- // https://github.com/Nomade040/length-disassembler/blob/e8b34546/ldisasm.cpp#L14
- // But it's confusingly-written enough that the code I wrote before didn't
- // work, so with any luck nobody will need to refer to it again and this is
- // actually correct now. Fingers crossed.
+static int mrmsib(const unsigned char *p, int addrlen) {
if (addrlen == 4 || *p & 0xC0) {
int sib = addrlen == 4 && *p < 0xC0 && (*p & 7) == 4;
switch (*p & 0xC0) {
- // disp8
- case 0x40: return 2 + sib;
- // disp16/32
- case 0:
+ case 0x40: // disp8
+ return 2 + sib;
+ case 0: // disp16/32
if ((*p & 7) != 5) {
// disp8/32 via SIB
if (sib && (p[1] & 7) == 5) return *p & 0x40 ? 3 : 6;
return 1 + sib;
}
- case 0x80: return 1 + addrlen + sib;
+ case 0x80:
+ return 1 + addrlen + sib;
}
}
if (addrlen == 2 && (*p & 0xC7) == 0x06) return 3;
return 1; // note: include the mrm itself in the byte count
}
-int x86_len(const void *insn_) {
+int x86_len(const unsigned char *insn) {
#define CASES(name, _) case name:
- const uchar *insn = insn_;
int pfxlen = 0, addrlen = 4, operandlen = 4;
p: switch (*insn) {
diff --git a/src/x86.h b/src/chunklets/x86.h
index 92e4ccb..e47d4ed 100644
--- a/src/x86.h
+++ b/src/chunklets/x86.h
@@ -14,21 +14,23 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
-#ifndef INC_X86_H
-#define INC_X86_H
-
-/*
- * Opcode-based X86 instruction analysis. In other words, *NOT* a disassembler.
- * Only cares about the instructions we expect to see in basic 32-bit userspace
- * functions; there's no kernel-mode instructions, no SSE 3+, no AVX, no REX,
- * EVEX, yadda yadda.
- */
+#ifndef INC_CHUNKLETS_X86_H
+#define INC_CHUNKLETS_X86_H
// 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.
+/*
+ * Below, we define groups of instruction opcode bytes based on their
+ * variable-length characteristics. This is used to drive the actual parsing of
+ * the instructions by x86_len(). The instructions are also put into a large
+ * enum defining their byte values. This allows pattern-matching on instruction
+ * bytes, useful for searching for certain instructions, or patterns thereof,
+ * for reverse-engineering or modding purposes.
+ */
+
/* Instruction prefixes: segments */
#define X86_SEG_PREFIXES(X) \
X(X86_PFX_ES, 0x26) \
@@ -554,13 +556,21 @@ enum {
};
#undef _X86_ENUM
+#ifdef __cplusplus
+extern "C"
+#endif
/*
* Returns the length of an instruction, or -1 if it's a "known unknown" or
* invalid instruction. Doesn't handle unknown unknowns: may explode or hang on
* arbitrary untrusted data. Also doesn't handle, among other things, 3DNow!,
- * SSE3+, MMX, AVX, and such. Aims to be small and fast, not comprehensive.
+ * SSE3+, MMX, AVX, and such. Doesn't cover 64-bit nor kernel-only stuff either
+ * Aims to be small and fast, not comprehensive.
+ *
+ * The main purpose of this function to assist in hooking functions or searching
+ * for certain instruction patterns in existing known and trusted binaries. It
+ * is once again not suitable for use with arbitrary unknown data.
*/
-int x86_len(const void *insn);
+int x86_len(const unsigned char *insn);
/* Constructs a ModRM byte, assuming the parameters are all in range. */
#define X86_MODRM(mod, reg, rm) (unsigned char)((mod) << 6 | (reg) << 3 | rm)
diff --git a/src/con_.c b/src/con_.c
index efc5912..23291d7 100644
--- a/src/con_.c
+++ b/src/con_.c
@@ -1,6 +1,7 @@
/* THIS FILE SHOULD BE CALLED `con.c` BUT WINDOWS IS STUPID */
/*
* Copyright © Michael Smith <mikesmiffy128@gmail.com>
+ * Copyright © Hayden K <imaciidz@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,33 +19,36 @@
#include <stddef.h> // should be implied by stdlib but glibc is dumb (offsetof)
#include <stdlib.h>
#include <stdio.h>
+#include <string.h>
#include "abi.h"
+#include "chunklets/x86.h"
#include "con_.h"
#include "engineapi.h" // for factories and rgba - XXX: is this a bit circular?
+#include "errmsg.h"
#include "extmalloc.h"
#include "gamedata.h"
#include "gametype.h"
+#include "langext.h"
#include "mem.h"
#include "os.h"
#include "vcall.h"
#include "version.h"
+#include "x86util.h"
/******************************************************************************\
* Have you ever noticed that when someone comments "here be dragons" there's *
* no actual dragons? Turns out, that's because the dragons all migrated over *
* here, so that they could build multiple inheritance vtables in C, by hand. *
* *
+ * Also there's self-modifying code now. *
+ * *
* Don't get set on fire. *
\******************************************************************************/
static int dllid; // from AllocateDLLIdentifier(), lets us unregister in bulk
int con_cmdclient;
-DECL_VFUNC(struct ICvar, void *, FindCommandBase_p2, 13, const char *)
-DECL_VFUNC(struct ICvar, void *, FindCommand_nonp2, 14, const char *)
-DECL_VFUNC(struct ICvar, void *, FindVar_nonp2, 12, const char *)
-
DECL_VFUNC_DYN(struct ICvar, int, AllocateDLLIdentifier)
DECL_VFUNC_DYN(struct ICvar, void, RegisterConCommand, /*ConCommandBase*/ void *)
DECL_VFUNC_DYN(struct ICvar, void, UnregisterConCommands, int)
@@ -53,72 +57,107 @@ DECL_VFUNC_DYN(struct ICvar, struct con_var *, FindVar, const char *)
DECL_VFUNC_DYN(struct ICvar, struct con_cmd *, FindCommand, const char *)
DECL_VFUNC_DYN(struct ICvar, void, CallGlobalChangeCallbacks, struct con_var *,
const char *, float)
-// sad: since adding the cool abstraction, we can't do varargs (because you
-// can't pass varargs to other varargs of course). we only get a pointer to it
-// via VFUNC so just declare the typedef here - I don't wanna write any more
-// macros today.
-typedef void (*ConsoleColorPrintf_func)(struct ICvar *, const struct rgba *,
- const char *, ...);
-
-// these have to be extern for con_colourmsg(), due to varargs nonsense
-struct ICvar *_con_iface;
-ConsoleColorPrintf_func _con_colourmsgf;
-
-static inline void initval(struct con_var *v) {
- v->strval = extmalloc(v->strlen); // note: strlen is preset in _DEF_CVAR()
- memcpy(v->strval, v->defaultval, v->strlen);
-}
-
-// to try and match the engine even though it's probably not strictly required,
-// we call the Internal* virtual functions via the actual vtable. since vtables
-// are built dynamically (below), we store this index; other indices are just
-// offset from it since these 3-or-4 functions are all right next to each other.
-static int vtidx_InternalSetValue;
-#define vtidx_InternalSetFloatValue (vtidx_InternalSetValue + 1)
-#define vtidx_InternalSetIntValue (vtidx_InternalSetValue + 2)
-#define vtidx_InternalSetColorValue (vtidx_InternalSetValue + 3)
+
+#ifdef _WIN32
+DECL_VFUNC_DYN(struct ICvar, void, CallGlobalChangeCallbacks_OE,
+ struct con_var *, const char *)
+
+// other OE stuff. TODO(compat): should this be in gamedata? fine for now?
+DECL_VFUNC(struct ICvar, struct con_cmdbase *, GetCommands_OE, 9)
+DECL_VFUNC(struct VEngineClient, void *, Cmd_Argv, 32)
+#endif
+
+// bootstrap things for con_detect(), not used after that
+DECL_VFUNC(struct ICvar, void *, FindCommandBase_p2, 13, const char *)
+DECL_VFUNC(struct ICvar, void *, FindCommand_nonp2, 14, const char *)
+DECL_VFUNC(struct ICvar, void *, FindVar_nonp2, 12, const char *)
+#ifdef _WIN32
+DECL_VFUNC(struct ICvar, void *, FindVar_OE, 7, const char *)
+#endif
+
+static struct ICvar *coniface;
+static void *colourmsgf;
+
+#ifdef _WIN32
+#pragma section("selfmod", execute)
+__attribute((used, section("selfmod"), noinline))
+#endif
+asm_only void _con_colourmsg(void *dummy, const struct rgba *c,
+ const char *fmt, ...) {
+ // NE: ConsoleColorPrintf is virtual, so the dummy param is a carve-out for
+ // `this` (which is coniface).
+ __asm volatile (
+ "mov eax, %0\n"
+ "mov [esp + 4], eax\n" // put coniface in the empty stack slot
+ "jmp dword ptr %1\n" // jump to the real function
+ :
+ : "m" (coniface), "m" (colourmsgf)
+ : "eax", "memory"
+ );
+}
+
+#ifdef _WIN32
+// this function is defined as data because we'll be using it to self-modify the
+// main _con_colourmsg function!
+__attribute((used, section("rdata")))
+asm_only static void _con_colourmsg_OE(void *dummy, const struct rgba *c,
+ const char *fmt, ...) {
+ // OE: it's a global function, with no this param, so we have to fix up the
+ // stack a bit. This will be less efficient than NE, but that seems like a
+ // reasonable tradeoff considering most games are NE. We could in theory
+ // self-modify every single call site to avoid the fixups but haha are you
+ // out of your mind we're not doing that.
+ __asm volatile (
+ "pop ebx\n" // pop return address, store in callee-save (*see header!*)
+ "add esp, 4\n" // pop the dummy stack slot, it's only useful for NE
+ "call dword ptr %1\n" // jump to the real function
+ "sub esp, 4\n" // pad the stack back out for the caller
+ "jmp ebx\n" // return to saved address
+ :
+ : "m" (coniface), "m" (colourmsgf)
+ : "eax", "ebx", "memory"
+ );
+}
+#define SELFMOD_LEN 15 // above instructions assemble to this many bytes!
+
+static bool selfmod() {
+ if (!os_mprot((void *)_con_colourmsg, SELFMOD_LEN, PAGE_EXECUTE_READWRITE)) {
+ errmsg_errorsys("couldn't make memory writable");
+ return false;
+ }
+ memcpy((void *)&_con_colourmsg, (void *)&_con_colourmsg_OE, SELFMOD_LEN);
+ if (!os_mprot((void *)_con_colourmsg, SELFMOD_LEN, PAGE_EXECUTE_READ)) {
+ errmsg_warnsys("couldn't restore self-modified page to read-only");
+ }
+ return true;
+}
+#endif
static void VCALLCONV dtor(void *_) {} // we don't use constructors/destructors
static bool VCALLCONV IsCommand_cmd(void *this) { return true; }
static bool VCALLCONV IsCommand_var(void *this) { return false; }
-static bool VCALLCONV IsFlagSet_cmd(struct con_cmd *this, int flags) {
- return !!(this->base.flags & flags);
-}
-static bool VCALLCONV IsFlagSet_var(struct con_var *this, int flags) {
- return !!(this->parent->base.flags & flags);
+static bool VCALLCONV IsFlagSet(struct con_cmdbase *this, int flags) {
+ return !!(this->flags & flags);
}
-static void VCALLCONV AddFlags_cmd(struct con_cmd *this, int flags) {
- this->base.flags |= flags;
+static void VCALLCONV AddFlags(struct con_cmdbase *this, int flags) {
+ this->flags |= flags;
}
-static void VCALLCONV AddFlags_var(struct con_var *this, int flags) {
- this->parent->base.flags |= flags;
+static void VCALLCONV RemoveFlags(struct con_cmdbase *this, int flags) {
+ this->flags &= ~flags;
}
-static void VCALLCONV RemoveFlags_cmd(struct con_cmd *this, int flags) {
- this->base.flags &= ~flags;
+static int VCALLCONV GetFlags(struct con_cmdbase *this) {
+ return this->flags;
}
-static void VCALLCONV RemoveFlags_var(struct con_var *this, int flags) {
- this->parent->base.flags &= ~flags;
+static const char *VCALLCONV GetName(struct con_cmdbase *this) {
+ return this->name;
}
-static int VCALLCONV GetFlags_cmd(struct con_cmd *this) {
- return this->base.flags;
-}
-static int VCALLCONV GetFlags_var(struct con_var *this) {
- return this->parent->base.flags;
-}
-
-static const char *VCALLCONV GetName_cmd(struct con_cmd *this) {
- return this->base.name;
-}
-static const char *VCALLCONV GetName_var(struct con_var *this) {
- return this->parent->base.name;
-}
-static const char *VCALLCONV GetHelpText_cmd(struct con_cmd *this) {
- return this->base.help;
-}
-static const char *VCALLCONV GetHelpText_var(struct con_var *this) {
- return this->parent->base.help;
+static const char *VCALLCONV GetHelpText(struct con_cmdbase *this) {
+ if_cold (this->flags & (CON_INIT_HIDDEN | _CON_NE_HIDDEN)) {
+ return this->help - 18; // see _DEF_* macros in con_.h
+ }
+ return this->help;
}
static bool VCALLCONV IsRegistered(struct con_cmdbase *this) {
return this->registered;
@@ -130,11 +169,45 @@ static void VCALLCONV Create_base(struct con_cmdbase *this, const char *name,
const char *help, int flags) {} // nop, we static init already
static void VCALLCONV Init(struct con_cmdbase *this) {} // ""
-static bool VCALLCONV ClampValue(struct con_var *this, float *f) {
+static bool ClampValue_common(struct con_var_common *this, float *f) {
if (this->hasmin && this->minval > *f) { *f = this->minval; return true; }
if (this->hasmax && this->maxval < *f) { *f = this->maxval; return true; }
return false;
}
+static bool VCALLCONV ClampValue(struct con_var *this, float *f) {
+ return ClampValue_common(&this->v2, f);
+}
+#ifdef _WIN32
+static bool VCALLCONV ClampValue_OE(struct con_var *this, float *f) {
+ return ClampValue_common(&this->v1, f);
+}
+#endif
+
+// global argc/argv. also OE only. extern for use in sst.c plugin_unload hook
+// as well as in DEF_CCMD_COMPAT_HOOK
+int *_con_argc;
+const char **_con_argv; // note: points to array of 80
+
+static bool find_argcargv() {
+ const uchar *insns = (const uchar *)VFUNC(engclient, Cmd_Argv);
+ for (const uchar *p = insns; p - insns < 32;) {
+ if (p[0] == X86_CALL) { insns = p + 5 + mem_loads32(p + 1); goto _1; }
+ NEXT_INSN(p, "global Cmd_Argv function");
+ }
+ return false;
+_1: for (const uchar *p = insns; p - insns < 32;) {
+ if (p[0] == X86_CMPRMW && p[1] == X86_MODRM(0, 0, 5)) {
+ _con_argc = mem_loadptr(p + 2);
+ }
+ else if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 0, 4) &&
+ p[2] == X86_TESTMRW) {
+ _con_argv = mem_loadptr(p + 3);
+ }
+ if (_con_argc && _con_argv) return true;
+ NEXT_INSN(p, "global argc and argv variables");
+ }
+ return false;
+}
int VCALLCONV AutoCompleteSuggest(struct con_cmd *this, const char *partial,
/*CUtlVector*/ void *commands) {
@@ -144,106 +217,155 @@ int VCALLCONV AutoCompleteSuggest(struct con_cmd *this, const char *partial,
bool VCALLCONV CanAutoComplete(struct con_cmd *this) {
return false;
}
-void VCALLCONV Dispatch(struct con_cmd *this, const struct con_cmdargs *args) {
- // only try cb; cbv1 and iface should never get used by us
- if (this->use_newcb && this->cb) this->cb(args);
+void VCALLCONV Dispatch(struct con_cmd *this, struct con_cmdargs *args) {
+ this->cb(args->argc, args->argv);
+}
+#ifdef _WIN32
+void VCALLCONV Dispatch_OE(struct con_cmd *this) {
+ this->cb(*_con_argc, _con_argv);
}
+#endif
-static void VCALLCONV ChangeStringValue(struct con_var *this, const char *s,
- float oldf) {
- char *old = alloca(this->strlen);
- memcpy(old, this->strval, this->strlen);
+static void ChangeStringValue_common(struct con_var *this,
+ struct con_var_common *common, char *old, const char *s) {
+ memcpy(old, common->strval, common->strlen);
int len = strlen(s) + 1;
- if (len > this->strlen) {
- this->strval = extrealloc(this->strval, len);
- this->strlen = len;
+ if (len > common->strlen) {
+ common->strval = extrealloc(common->strval, len);
+ common->strlen = len;
}
- memcpy(this->strval, s, len);
+ memcpy(common->strval, s, len);
// callbacks don't matter as far as ABI compat goes (and thank goodness
// because e.g. portal2 randomly adds a *list* of callbacks!?). however we
// do need callbacks for at least one feature, so do our own minimal thing
if (this->cb) this->cb(this);
- // also call global callbacks, as is polite.
- CallGlobalChangeCallbacks(_con_iface, this, old, oldf);
}
+static void VCALLCONV ChangeStringValue(struct con_var *this, const char *s,
+ float oldf) {
+ char *old = alloca(this->v2.strlen);
+ ChangeStringValue_common(this, &this->v2, old, s);
+ CallGlobalChangeCallbacks(coniface, this, old, oldf);
+}
+#ifdef _WIN32
+static void VCALLCONV ChangeStringValue_OE(struct con_var *this, const char *s) {
+ char *old = alloca(this->v1.strlen);
+ ChangeStringValue_common(this, &this->v1, old, s);
+ CallGlobalChangeCallbacks_OE(coniface, this, old);
+}
+#endif
+
+// NOTE: these Internal* functions are virtual in the engine, but nowadays we
+// just call them directly since they're private to us. We still put them in the
+// vtable just in case (see below), though arguably nothing in the engine
+// *should* be calling these internal things anyway.
-static void VCALLCONV InternalSetValue_impl(struct con_var *this, const char *v) {
- float oldf = this->fval;
+static void InternalSetValue_common(struct con_var *this,
+ struct con_var_common *common, const char *v) {
float newf = atof(v);
char tmp[32];
- // NOTE: calling our own ClampValue and ChangeString, not bothering with
- // vtable (it's internal anyway, so we're never calling into engine code)
- if (ClampValue(this, &newf)) {
+ if (ClampValue_common(common, &newf)) {
snprintf(tmp, sizeof(tmp), "%f", newf);
v = tmp;
}
- this->fval = newf;
- this->ival = (int)newf;
+ common->fval = newf;
+ common->ival = (int)newf;
+}
+static void VCALLCONV InternalSetValue(struct con_var *this, const char *v) {
+ float oldf = this->v2.fval;
+ InternalSetValue_common(this, &this->v2, v);
if (!(this->base.flags & CON_NOPRINT)) ChangeStringValue(this, v, oldf);
}
+#ifdef _WIN32
+static void VCALLCONV InternalSetValue_OE(struct con_var *this, const char *v) {
+ InternalSetValue_common(this, &this->v1, v);
+ if (!(this->base.flags & CON_NOPRINT)) ChangeStringValue_OE(this, v);
+}
+#endif
-static void VCALLCONV InternalSetFloatValue_impl(struct con_var *this, float v) {
- if (v == this->fval) return;
- ClampValue(this, &v);
- float old = this->fval;
- this->fval = v; this->ival = (int)this->fval;
+static void VCALLCONV InternalSetFloatValue(struct con_var *this, float v) {
+ if (v == this->v2.fval) return;
+ float old = this->v2.fval;
+ ClampValue_common(&this->v2, &v);
+ this->v2.fval = v; this->v2.ival = (int)v;
if (!(this->base.flags & CON_NOPRINT)) {
char tmp[32];
- snprintf(tmp, sizeof(tmp), "%f", this->fval);
+ snprintf(tmp, sizeof(tmp), "%f", this->v2.fval);
ChangeStringValue(this, tmp, old);
}
}
+#ifdef _WIN32
+static void VCALLCONV InternalSetFloatValue_OE(struct con_var *this, float v) {
+ if (v == this->v1.fval) return;
+ ClampValue_common(&this->v1, &v);
+ this->v1.fval = v; this->v1.ival = (int)v;
+ if (!(this->base.flags & CON_NOPRINT)) {
+ char tmp[32];
+ snprintf(tmp, sizeof(tmp), "%f", this->v1.fval);
+ ChangeStringValue_OE(this, tmp);
+ }
+}
+#endif
-static void VCALLCONV InternalSetIntValue_impl(struct con_var *this, int v) {
- if (v == this->ival) return;
+static void InternalSetIntValue_impl(struct con_var *this,
+ struct con_var_common *common, int v) {
float f = (float)v;
- if (ClampValue(this, &f)) v = (int)f;
- float old = this->fval;
- this->fval = f; this->ival = v;
+ if (ClampValue_common(common, &f)) v = (int)f;
+ common->fval = f; common->ival = v;
+}
+static void VCALLCONV InternalSetIntValue(struct con_var *this, int v) {
+ if (v == this->v2.ival) return;
+ float old = this->v2.fval;
+ InternalSetIntValue_impl(this, &this->v2, v);
if (!(this->base.flags & CON_NOPRINT)) {
char tmp[32];
- snprintf(tmp, sizeof(tmp), "%f", this->fval);
+ snprintf(tmp, sizeof(tmp), "%f", this->v2.fval);
ChangeStringValue(this, tmp, old);
}
}
-
-DECL_VFUNC_DYN(struct con_var, void, InternalSetValue, const char *)
-DECL_VFUNC_DYN(struct con_var, void, InternalSetFloatValue, float)
-DECL_VFUNC_DYN(struct con_var, void, InternalSetIntValue, int)
-DECL_VFUNC_DYN(struct con_var, void, InternalSetColorValue, struct rgba)
+#ifdef _WIN32
+static void VCALLCONV InternalSetIntValue_OE(struct con_var *this, int v) {
+ if (v == this->v1.ival) return;
+ InternalSetIntValue_impl(this, &this->v1, v);
+ if (!(this->base.flags & CON_NOPRINT)) {
+ char tmp[32];
+ snprintf(tmp, sizeof(tmp), "%f", this->v1.fval);
+ ChangeStringValue_OE(this, tmp);
+ }
+}
+#endif
// IConVar calls get this-adjusted pointers, so just subtract the offset
static void VCALLCONV SetValue_str_thunk(void *thisoff, const char *v) {
struct con_var *this = mem_offset(thisoff,
-offsetof(struct con_var, vtable_iconvar));
- InternalSetValue(this->parent, v);
+ InternalSetValue(this, v);
}
static void VCALLCONV SetValue_f_thunk(void *thisoff, float v) {
struct con_var *this = mem_offset(thisoff,
-offsetof(struct con_var, vtable_iconvar));
- InternalSetFloatValue(this->parent, v);
+ InternalSetFloatValue(this, v);
}
static void VCALLCONV SetValue_i_thunk(void *thisoff, int v) {
struct con_var *this = mem_offset(thisoff,
-offsetof(struct con_var, vtable_iconvar));
- InternalSetIntValue(this->parent, v);
+ InternalSetIntValue(this, v);
}
static void VCALLCONV SetValue_colour_thunk(void *thisoff, struct rgba v) {
struct con_var *this = mem_offset(thisoff,
-offsetof(struct con_var, vtable_iconvar));
- InternalSetColorValue(this->parent, v);
+ InternalSetIntValue(this, v.val);
}
-// more misc thunks, hopefully these just compile to a lea and a jmp
+// more misc thunks for IConVar, hopefully these just compile to a lea and a jmp
static const char *VCALLCONV GetName_thunk(void *thisoff) {
struct con_var *this = mem_offset(thisoff,
-offsetof(struct con_var, vtable_iconvar));
- return GetName_var(this);
+ return GetName(&this->base);
}
static bool VCALLCONV IsFlagSet_thunk(void *thisoff, int flags) {
struct con_var *this = mem_offset(thisoff,
-offsetof(struct con_var, vtable_iconvar));
- return IsFlagSet_var(this, flags);
+ return IsFlagSet(&this->base, flags);
}
// dunno what this is actually for...
@@ -263,8 +385,8 @@ void *_con_vtab_cmd[14 + NVDTOR] = {
(void *)&dtor,
#endif
(void *)&IsCommand_cmd,
- (void *)&IsFlagSet_cmd,
- (void *)&AddFlags_cmd
+ (void *)&IsFlagSet,
+ (void *)&AddFlags
};
// the engine does dynamic_casts on ConVar at some points so we have to fill out
@@ -285,8 +407,8 @@ struct _con_vtab_var_wrap _con_vtab_var_wrap = {
(void *)&dtor,
#endif
(void *)&IsCommand_var,
- (void *)&IsFlagSet_var,
- (void *)&AddFlags_var
+ (void *)&IsFlagSet,
+ (void *)&AddFlags
};
struct _con_vtab_iconvar_wrap _con_vtab_iconvar_wrap = {
@@ -303,13 +425,43 @@ struct _con_vtab_iconvar_wrap _con_vtab_iconvar_wrap = {
#endif
};
+#ifdef _WIN32
+static int off_cvar_common = offsetof(struct con_var, v2);
+#else
+enum { off_cvar_common = offsetof(struct con_var, v2) };
+#endif
+
+struct con_var_common *con_getvarcommon(const struct con_var *v) {
+ return mem_offset(v, off_cvar_common);
+}
+
+static inline void fudgeflags(struct con_cmdbase *b) {
+ if_hot (!GAMETYPE_MATCHES(OE)) if (b->flags & CON_INIT_HIDDEN) {
+ b->flags = (b->flags & ~CON_INIT_HIDDEN) | _CON_NE_HIDDEN;
+ }
+}
+
void con_regvar(struct con_var *v) {
- initval(v);
- RegisterConCommand(_con_iface, v);
+ fudgeflags(&v->base);
+ struct con_var_common *c = con_getvarcommon(v);
+ c->strval = extmalloc(c->strlen); // note: _DEF_CVAR() sets strlen member
+ memcpy(c->strval, c->defaultval, c->strlen);
+ RegisterConCommand(coniface, v);
}
void con_regcmd(struct con_cmd *c) {
- RegisterConCommand(_con_iface, c);
+ fudgeflags(&c->base);
+ if_hot (!GAMETYPE_MATCHES(OE)) if (c->base.flags & CON_INIT_HIDDEN) {
+ c->base.flags = (c->base.flags & ~CON_INIT_HIDDEN) | _CON_NE_HIDDEN;
+ }
+ RegisterConCommand(coniface, c);
+}
+
+void con_hide(struct con_cmdbase *b) {
+ if_hot (!GAMETYPE_MATCHES(OE)) b->flags |= _CON_NE_HIDDEN;
+}
+void con_unhide(struct con_cmdbase *b) {
+ if_hot (!GAMETYPE_MATCHES(OE)) b->flags &= ~_CON_NE_HIDDEN;
}
// XXX: these should use vcall/gamedata stuff as they're only used for the
@@ -318,78 +470,48 @@ void con_regcmd(struct con_cmd *c) {
// just hacked in for now to get things working because it was broken before...
#ifdef _WIN32
static int vtidx_SetValue_str = 2, vtidx_SetValue_f = 1, vtidx_SetValue_i = 0;
+static int off_setter_vtable = offsetof(struct con_var, vtable_iconvar);
#else
enum { vtidx_SetValue_str = 0, vtidx_SetValue_f = 1, vtidx_SetValue_i = 2 };
#endif
-void con_init() {
- _con_colourmsgf = VFUNC(_con_iface, ConsoleColorPrintf);
- dllid = AllocateDLLIdentifier(_con_iface);
-
- void **pc = _con_vtab_cmd + 3 + NVDTOR, **pv = _con_vtab_var + 3 + NVDTOR,
- **pi = _con_vtab_iconvar
-#ifndef _WIN32
- + 3
-#endif
- ;
- if (GAMETYPE_MATCHES(L4Dbased)) { // 007 base
- *pc++ = (void *)&RemoveFlags_cmd;
- *pc++ = (void *)&GetFlags_cmd;
- *pv++ = (void *)&RemoveFlags_var;
- *pv++ = (void *)&GetFlags_var;
- }
- // base stuff in cmd
- *pc++ = (void *)&GetName_cmd;
- *pc++ = (void *)&GetHelpText_cmd;
- *pc++ = (void *)&IsRegistered;
- *pc++ = (void *)&GetDLLIdentifier;
- *pc++ = (void *)&Create_base;
- *pc++ = (void *)&Init;
- // cmd-specific
- *pc++ = (void *)&AutoCompleteSuggest;
- *pc++ = (void *)&CanAutoComplete;
- *pc++ = (void *)&Dispatch;
- // base stuff in var
- *pv++ = (void *)&GetName_var;
- *pv++ = (void *)&GetHelpText_var;
- *pv++ = (void *)&IsRegistered;
- *pv++ = (void *)&GetDLLIdentifier;
- *pv++ = (void *)&Create_base;
- *pv++ = (void *)&Init;
- // var-specific
- vtidx_InternalSetValue = pv - _con_vtab_var;
- *pv++ = (void *)&InternalSetValue_impl;
- *pv++ = (void *)&InternalSetFloatValue_impl;
- *pv++ = (void *)&InternalSetIntValue_impl;
- if (GAMETYPE_MATCHES(L4D2x) || GAMETYPE_MATCHES(Portal2)) { // ugh, annoying
- // InternalSetColorValue, literally the same machine instructions as int
- *pv++ = (void *)&InternalSetIntValue_impl;
- }
- *pv++ = (void *)&ClampValue;;
- *pv++ = (void *)&ChangeStringValue;
- *pv++ = (void *)&Create_var;
- if (GAMETYPE_MATCHES(L4D2x) || GAMETYPE_MATCHES(Portal2)) {
- *pi++ = (void *)&SetValue_colour_thunk;
#ifdef _WIN32
- // stupid hack for above mentioned crazy overload ordering
- ++vtidx_SetValue_str;
- ++vtidx_SetValue_i;
- ++vtidx_SetValue_f;
-#endif
+struct con_cmdbase **linkedlist = 0; // indirect command list, OE only!
+
+static bool find_linkedlist(const uchar *insns) {
+ // note: it's a jmp in the disasm I've seen but a call seems plausible too
+ if (insns[0] != X86_JMPIW && *insns != X86_CALL) return false;
+ insns += 5 + mem_loads32(insns + 1); // follow the call
+ if (insns[0] != X86_MOVEAXII || insns[5] != X86_RET) return false;
+ linkedlist = mem_loadptr(insns + 1);
+ return true;
+}
+
+static bool find_Con_ColorPrintf() {
+ typedef void *(*GetSpewOutputFunc_func)();
+ void *tier0 = os_dlhandle(L"tier0.dll");
+ if_cold (!tier0) {
+ errmsg_errorsys("couldn't get tier0.dll handle");
+ return false;
}
-#ifdef _WIN32
- // see above: these aren't prefilled due the the reverse order
- *pi++ = (void *)&SetValue_i_thunk;
- *pi++ = (void *)&SetValue_f_thunk;
- *pi++ = (void *)&SetValue_str_thunk;
-#endif
- *pi++ = (void *)&GetName_thunk;
- // GetBaseName (we just return actual name in all cases)
- if (GAMETYPE_MATCHES(L4Dbased)) *pi++ = (void *)&GetName_thunk;
- *pi++ = (void *)&IsFlagSet_thunk;
- // last one: not in 004, but doesn't matter. one less branch!
- *pi++ = (void *)&GetSplitScreenPlayerSlot;
+ GetSpewOutputFunc_func GetSpewOutputFunc = (GetSpewOutputFunc_func)os_dlsym(
+ tier0, "GetSpewOutputFunc");
+ if_cold (!GetSpewOutputFunc) {
+ errmsg_errorx("couldn't find GetSpewOutputFunc symbol");
+ return false;
+ }
+ uchar *insns = (uchar *)GetSpewOutputFunc();
+ for (uchar *p = insns; p - insns < 320;) {
+ if (p[0] == X86_PUSHECX && p[1] == X86_PUSHIW && p[6] == X86_CALL &&
+ p[11] == X86_ALUMI8S && p[12] == X86_MODRM(3, 0, 4)) {
+ colourmsgf = p + 11 + mem_loads32(p + 7);
+ return true;
+ }
+ NEXT_INSN(p, "Con_ColorPrintf function");
+ }
+ return false;
}
+#endif
static void helpuserhelpus(int pluginver, char ifaceverchar) {
con_msg("\n");
@@ -402,57 +524,105 @@ static void helpuserhelpus(int pluginver, char ifaceverchar) {
// note: for now at least, not using errmsg_*() macros here because it doesn't
// really make sense for these messages to be coming from "con"
-static void warnoe() {
+static void badver() {
con_warn("sst: error: this engine version is not yet supported\n");
}
bool con_detect(int pluginver) {
- if (_con_iface = factory_engine("VEngineCvar007", 0)) {
+ if (coniface = factory_engine("VEngineCvar007", 0)) {
// GENIUS HACK (BUT STILL BAD): Portal 2 has everything in ICvar shifted
// down 3 places due to the extra stuff in IAppSystem. This means that
// if we look up the Portal 2-specific cvar using FindCommandBase, it
// *actually* calls the const-overloaded FindVar on other branches,
// which just happens to still work fine. From there, we can figure out
// the actual ABI to use to avoid spectacular crashes.
- if (FindCommandBase_p2(_con_iface, "portal2_square_portals")) {
+ if (FindCommandBase_p2(coniface, "portal2_square_portals")) {
_gametype_tag |= _gametype_tag_Portal2;
return true;
}
- if (FindCommand_nonp2(_con_iface, "l4d2_snd_adrenaline")) {
+ if (FindCommand_nonp2(coniface, "l4d2_snd_adrenaline")) {
// while we're here, also distinguish Survivors, the stupid Japanese
// arcade game a few people seem to care about for some reason
// (which for some other reason also has some vtable changes)
- if (FindVar_nonp2(_con_iface, "avatarbasemodel")) {
+ if (FindVar_nonp2(coniface, "avatarbasemodel")) {
_gametype_tag |= _gametype_tag_L4DS;
}
else {
_gametype_tag |= _gametype_tag_L4D2;
}
+ if (FindVar_nonp2(coniface, "sv_zombie_touch_trigger_delay")) {
+ _gametype_tag |= _gametype_tag_L4D2_2125plus;
+ }
+ if (FindVar_nonp2(coniface, "director_cs_weapon_spawn_chance")) {
+ _gametype_tag |= _gametype_tag_TheLastStand;
+ }
return true;
}
- if (FindVar_nonp2(_con_iface, "z_difficulty")) {
+ if (FindVar_nonp2(coniface, "z_difficulty")) {
_gametype_tag |= _gametype_tag_L4D1;
+ // Crash Course update
+ if (FindCommand_nonp2(coniface, "director_log_scavenge_items")) {
+ _gametype_tag |= _gametype_tag_L4D1_1015plus;
+ // seems there was some code shuffling in the Mac update (1022).
+ // this update came out like 2-3 weeks after The Sacrifice
+ if (con_findvar("tank_stasis_time_suicide")) {
+ _gametype_tag |= _gametype_tag_L4D1_1022plus;
+ }
+ }
return true;
}
con_warn("sst: error: game is unsupported (using VEngineCvar007)\n");
helpuserhelpus(pluginver, '7');
return false;
}
- if (_con_iface = factory_engine("VEngineCvar004", 0)) {
+ if (coniface = factory_engine("VEngineCvar004", 0)) {
// TODO(compat): are there any cases where 004 is incompatible? could
// this crash? find out!
if (pluginver == 3) _gametype_tag |= _gametype_tag_2013;
else _gametype_tag |= _gametype_tag_OrangeBox;
+ // detect Portal 1 versions while we're here...
+ if (FindCommand_nonp2(coniface, "upgrade_portalgun")) {
+ _gametype_tag |= _gametype_tag_Portal1;
+ if (!FindVar_nonp2(coniface, "tf_escort_score_rate")) {
+ _gametype_tag |= _gametype_tag_Portal1_3420;
+ }
+ }
+ else if (FindCommand_nonp2(coniface, "phys_swap")) {
+ _gametype_tag |= _gametype_tag_HL2series;
+ }
return true;
}
- if (factory_engine("VEngineCvar003", 0)) {
- warnoe();
- helpuserhelpus(pluginver, '3');
- return false;
+ if (coniface = factory_engine("VEngineCvar003", 0)) {
+#ifdef _WIN32 // there's no OE on linux!
+ _gametype_tag |= _gametype_tag_OE;
+ // for deletion/unlinking on unload, we need an indirect linked list
+ // pointer. calling GetCommands gives us a direct pointer. so we have to
+ // actually pull out the indirect pointer from the actual asm lol.
+ if (!find_linkedlist((uchar *)VFUNC(coniface, GetCommands_OE))) {
+ errmsg_errorx("couldn't find command list pointer");
+ return false;
+ }
+ if (!find_argcargv()) return false;
+ if (!find_Con_ColorPrintf()) return false;
+ if (!selfmod()) return false;
+ // NOTE: the default static struct layout is for NE; immediately after
+ // engineapi init finishes, the generated glue code will shunt
+ // everything along for OE if required, in shuntvars(). since all the
+ // gluegen code is currently hooked up in sst.c this is a little bit
+ // annoyingly removed from here. not sure how to do it better, sorry.
+ off_cvar_common = offsetof(struct con_var, v1);
+ if (FindVar_OE(coniface, "mm_ai_facehugger_enablehugeattack")) {
+ _gametype_tag |= _gametype_tag_DMoMM;
+ }
+ return true;
+#else
+ badver();
+ helpuserhelpus(pluginver, '2');
+#endif
}
// I don't suppose there's anything below 002 worth caring about? Shrug.
if (factory_engine("VEngineCvar002", 0)) {
- warnoe();
+ badver();
helpuserhelpus(pluginver, '2');
return false;
}
@@ -461,38 +631,192 @@ bool con_detect(int pluginver) {
return false;
}
+static int *find_host_initialized() {
+ const uchar *insns = colourmsgf;
+ for (const uchar *p = insns; p - insns < 32;) {
+ // cmp byte ptr [<pointer>], <value>
+ if (p[0] == X86_ALUMI8 && p[1] == X86_MODRM(0, 7, 5)) {
+ return mem_loadptr(p + 2);
+ }
+ NEXT_INSN(p, "host_initialized variable");
+ }
+ return 0;
+}
+
+void con_init() {
+ if (GAMETYPE_MATCHES(OE)) {
+ // if we're autoloaded, we have to set host_initialized early or colour
+ // log output (including error output!) won't be visible, for some inane
+ // reason. *as far as we know* this doesn't have any bad side effects.
+ // note: if this fails, too bad. not like we can log a warning.
+ int *host_initialized = find_host_initialized();
+ if (host_initialized && *host_initialized == 0) *host_initialized = 1;
+ }
+ else {
+ colourmsgf = coniface->vtable[vtidx_ConsoleColorPrintf];
+ dllid = AllocateDLLIdentifier(coniface);
+ }
+
+ void **pc = _con_vtab_cmd + 3 + NVDTOR, **pv = _con_vtab_var + 3 + NVDTOR,
+#ifdef _WIN32
+ **pi = _con_vtab_iconvar;
+#else
+ **pi = _con_vtab_iconvar + 3;
+#endif
+ if (GAMETYPE_MATCHES(L4Dbased)) { // 007 base
+ *pc++ = (void *)&RemoveFlags;
+ *pc++ = (void *)&GetFlags;
+ *pv++ = (void *)&RemoveFlags;
+ *pv++ = (void *)&GetFlags;
+ }
+ // base stuff in cmd
+ *pc++ = (void *)&GetName;
+ *pc++ = (void *)&GetHelpText;
+ *pc++ = (void *)&IsRegistered;
+ if (!GAMETYPE_MATCHES(OE)) *pc++ = (void *)&GetDLLIdentifier;
+ *pc++ = (void *)&Create_base;
+ *pc++ = (void *)&Init;
+ // cmd-specific
+ *pc++ = (void *)&AutoCompleteSuggest;
+ *pc++ = (void *)&CanAutoComplete;
+ if (GAMETYPE_MATCHES(OE)) {
+#ifdef _WIN32 // function only defined in windows
+ *pc++ = (void *)&Dispatch_OE;
+#endif
+ }
+ else {
+ *pc++ = (void *)&Dispatch;
+ }
+ // base stuff in var
+ *pv++ = (void *)&GetName;
+ *pv++ = (void *)&GetHelpText;
+ *pv++ = (void *)&IsRegistered;
+ if (!GAMETYPE_MATCHES(OE)) *pv++ = (void *)&GetDLLIdentifier;
+ *pv++ = (void *)&Create_base;
+ *pv++ = (void *)&Init;
+ // var-specific
+ if (GAMETYPE_MATCHES(OE)) {
+#ifdef _WIN32
+ // these there are for the SetValue overloads but we effectively inline
+ // them by putting in pointers to call the Internal ones directly. this
+ // specifically works now that we've opted not to bother with the parent
+ // pointer stuff, otherwise we'd still need wrappers here.
+ vtidx_SetValue_i = pv - _con_vtab_var;
+ *pv++ = (void *)&InternalSetIntValue_OE;
+ vtidx_SetValue_f = pv - _con_vtab_var;
+ *pv++ = (void *)&InternalSetFloatValue_OE;
+ vtidx_SetValue_str = pv - _con_vtab_var;
+ *pv++ = (void *)&InternalSetValue_OE;
+ off_setter_vtable = 0; // setters should use the single vtable (below)
+ *pv++ = (void *)&InternalSetValue_OE;
+ *pv++ = (void *)&InternalSetFloatValue_OE;
+ *pv++ = (void *)&InternalSetIntValue_OE;
+ *pv++ = (void *)&ClampValue_OE;
+ *pv++ = (void *)&ChangeStringValue_OE;
+#endif
+ }
+ else {
+ *pv++ = (void *)&InternalSetValue;
+ *pv++ = (void *)&InternalSetFloatValue;
+ *pv++ = (void *)&InternalSetIntValue;
+ if (GAMETYPE_MATCHES(L4D2x) || GAMETYPE_MATCHES(Portal2)) { // ugh.
+ // InternalSetColorValue, exact same machine instructions as for int
+ *pv++ = (void *)&InternalSetIntValue;
+ }
+ *pv++ = (void *)&ClampValue;
+ *pv++ = (void *)&ChangeStringValue;
+ }
+ *pv++ = (void *)&Create_var;
+ if (GAMETYPE_MATCHES(OE)) return; // we can just skip the rest on OE!
+ if (GAMETYPE_MATCHES(L4D2x) || GAMETYPE_MATCHES(Portal2)) {
+ *pi++ = (void *)&SetValue_colour_thunk;
+#ifdef _WIN32
+ // stupid hack for above mentioned crazy overload ordering
+ ++vtidx_SetValue_str;
+ ++vtidx_SetValue_i;
+ ++vtidx_SetValue_f;
+#endif
+ }
+#ifdef _WIN32
+ // see above: these aren't prefilled due to the reverse order
+ *pi++ = (void *)&SetValue_i_thunk;
+ *pi++ = (void *)&SetValue_f_thunk;
+ *pi++ = (void *)&SetValue_str_thunk;
+#endif
+ *pi++ = (void *)&GetName_thunk;
+ // GetBaseName (we just return actual name in all cases)
+ if (GAMETYPE_MATCHES(L4Dbased)) *pi++ = (void *)&GetName_thunk;
+ *pi++ = (void *)&IsFlagSet_thunk;
+ // last one: not in 004, but doesn't matter. one less branch!
+ *pi++ = (void *)&GetSplitScreenPlayerSlot;
+}
+
void con_disconnect() {
- UnregisterConCommands(_con_iface, dllid);
+#ifdef _WIN32
+ if (linkedlist) {
+ // there's no DLL identifier system in OE so we have to manually unlink
+ // our commands and variables from the global list.
+ for (struct con_cmdbase **pp = linkedlist; *pp; ) {
+ struct con_cmdbase **next = &(*pp)->next;
+ // HACK: easiest way to do this is by vtable. dumb, but whatever!
+ const struct con_cmdbase *p = *pp;
+ if (p->vtable == _con_vtab_cmd || p->vtable == _con_vtab_var) {
+ *pp = *next;
+ }
+ else {
+ pp = next;
+ }
+ }
+ return;
+ }
+#endif
+ UnregisterConCommands(coniface, dllid);
}
struct con_var *con_findvar(const char *name) {
- return FindVar(_con_iface, name);
+ return FindVar(coniface, name);
}
struct con_cmd *con_findcmd(const char *name) {
- return FindCommand(_con_iface, name);
+#ifdef _WIN32
+ if (linkedlist) {
+ // OE has a FindVar but no FindCommand. interesting oversight...
+ for (struct con_cmdbase *p = *linkedlist; p; p = p->next) {
+ if (!_stricmp(name, p->name)) {
+ // FIXME: this'll get variables too! make the appropriate vcall!
+ return (struct con_cmd *)p;
+ }
+ }
+ return 0;
+ }
+#endif
+ return FindCommand(coniface, name);
}
-#define GETTER(T, N, M) T N(const struct con_var *v) { return v->parent->M; }
+// NOTE: getters here still go through the parent pointer although we stopped
+// doing that internally, just in case we run into parented cvars in the actual
+// engine. a little less efficient, but safest and simplest for now.
+#define GETTER(T, N, M) \
+ T N(const struct con_var *v) { \
+ return con_getvarcommon(con_getvarcommon(v)->parent)->M; \
+ }
GETTER(const char *, con_getvarstr, strval)
GETTER(float, con_getvarf, fval)
GETTER(int, con_getvari, ival)
#undef GETTER
-// XXX: move this to vcall/gamedata (will require win/linux conditionals first!)
-// see also above comment on the vtidx definitions
#define SETTER(T, I, N) \
void N(struct con_var *v, T x) { \
- ((void (*VCALLCONV)(void *, T))(v->vtable_iconvar[I]))( \
- &v->vtable_iconvar, x); \
+ void (***VCALLCONV vtp)(void *, T) = mem_offset(v, off_setter_vtable); \
+ (*vtp)[I](vtp, x); \
}
SETTER(const char *, vtidx_SetValue_str, con_setvarstr)
SETTER(float, vtidx_SetValue_f, con_setvarf)
SETTER(int, vtidx_SetValue_i, con_setvari)
#undef SETTER
-con_cmdcb con_getcmdcb(const struct con_cmd *cmd) {
- return !cmd->use_newcmdiface && cmd->use_newcb ? cmd->cb : 0;
+con_cmdcbv2 con_getcmdcbv2(const struct con_cmd *cmd) {
+ return !cmd->use_newcmdiface && cmd->use_newcb ? cmd->cb_v2 : 0;
}
con_cmdcbv1 con_getcmdcbv1(const struct con_cmd *cmd) {
diff --git a/src/con_.h b/src/con_.h
index 0dd50a5..eb03642 100644
--- a/src/con_.h
+++ b/src/con_.h
@@ -21,7 +21,7 @@
#include "intdefs.h"
#if defined(__GNUC__) || defined(__clang__)
-#define _CON_PRINTF(x, y) __attribute__((format(printf, (x), (y))))
+#define _CON_PRINTF(x, y) __attribute((format(printf, (x), (y))))
#else
#define _CON_PRINTF(x, y)
#endif
@@ -41,35 +41,44 @@ struct con_cmdargs {
#define CON_CMD_MAXCOMPLETE 64
#define CON_CMD_MAXCOMPLLEN 64
-/* ConVar/ConCommand flag bits - undocumented ones are probably not useful... */
+/* ConVar/ConCommand flag bits stable across engines */
enum {
- CON_UNREG = 1,
- CON_DEVONLY = 1 << 1, /* hide unless developer 1 is set */
+ _CON_NE_DEVONLY = 1 << 1, /* hide entirely and disallow usage. NE only. */
CON_SERVERSIDE = 1 << 2, /* set con_cmdclient and run on server side */
- CON_CLIENTDLL = 1 << 3,
- CON_HIDDEN = 1 << 4, /* hide completely, often useful to remove! */
+ _CON_NE_HIDDEN = 1 << 4, /* don't autocomplete. NE only; use con_hide() */
CON_PROTECTED = 1 << 5, /* don't send to clients (example: password) */
- CON_SPONLY = 1 << 6,
- CON_ARCHIVE = 1 << 7, /* save in config - plugin would need a VDF! */
+ CON_ARCHIVE = 1 << 7, /* save in config.cfg. needs VDF autoload. */
CON_NOTIFY = 1 << 8, /* announce changes in game chat */
- CON_USERINFO = 1 << 9,
- CON_PRINTABLE = 1 << 10, /* do not allow non-printable values */
- CON_UNLOGGED = 1 << 11,
- CON_NOPRINT = 1 << 12, /* do not attempt to print, contains junk! */
- CON_REPLICATE = 1 << 13, /* client will use server's value */
- CON_CHEAT = 1 << 14, /* require sv_cheats 1 to change from default */
- CON_DEMO = 1 << 16, /* record value at the start of a demo */
+ CON_PRINTABLE = 1 << 10, /* do not allow non-printable characters */
+ CON_NOPRINT = 1 << 12, /* contains junk; do not attempt to print */
+ CON_REPLICATE = 1 << 13, /* client will value from server */
+ CON_CHEAT = 1 << 14, /* require sv_cheats 1 to change (or run) */
+ CON_DEMO = 1 << 16, /* record cvar value at the start of a demo */
CON_NORECORD = 1 << 17, /* don't record the command to a demo, ever */
- CON_NOTCONN = 1 << 22, /* cannot be changed while in-game */
- CON_SRVEXEC = 1 << 28, /* server can make clients run the command */
- CON_NOSRVQUERY = 1 << 29, /* server cannot query the clientside value */
- CON_CCMDEXEC = 1 << 30 /* ClientCmd() function may run the command */
+ CON_NOTCONN = 1 << 22, /* cannot be changed while in a server */
+ _CON_NE_CCMDEXEC = 1 << 30 /* ClientCmd() can run on client. NE only. */
};
-/* A callback function invoked to execute a command. */
-typedef void (*con_cmdcb)(const struct con_cmdargs *cmd);
+/*
+ * Placeholder flags for DEF_* usage. Mapped to correct runtime flags at
+ * registration time (see con_regvar(), con_regcmd()).
+ */
+enum {
+ /*
+ * Causes a command or variable to be registered as hidden on NE. Currently
+ * does nothing on OE. Cannot be used to hide/unhide something after
+ * registration. Use con_hide() or con_unhide() for that.
+ */
+ CON_INIT_HIDDEN = 1 << 29
+};
+
+/* A callback function invoked by SST to execute its own commands. */
+typedef void (*con_cmdcb)(int argc, const char **argv);
+
+/* A callback function used by most commands in most versions of the engine. */
+typedef void (*con_cmdcbv2)(struct con_cmdargs *cmd);
-/* Obsolete callback; not used by SST, but might still exist in the engine. */
+/* An older style of callback function used by some old commands, and in OE. */
typedef void (*con_cmdcbv1)();
/*
@@ -101,9 +110,11 @@ struct con_cmdbase { // ConCommandBase in engine
struct con_cmd { // ConCommand in engine
struct con_cmdbase base;
union {
+ con_cmdcb cb; // N.B.: only used by *our* commands!
con_cmdcbv1 cb_v1;
- con_cmdcb cb;
- /*ICommandCallback*/ void *cb_iface; // does source even use this?
+ con_cmdcbv2 cb_v2;
+ const uchar *cb_insns; // for the sake of instruction-scanning and such
+ /*ICommandCallback*/ void *cb_iface; // what in Source even uses this?
};
union {
con_complcb complcb;
@@ -112,9 +123,7 @@ struct con_cmd { // ConCommand in engine
bool has_complcb : 1, use_newcb : 1, use_newcmdiface : 1;
};
-struct con_var { // ConVar in engine
- struct con_cmdbase base;
- void **vtable_iconvar; // IConVar in engine (pure virtual)
+struct con_var_common {
struct con_var *parent;
const char *defaultval;
char *strval;
@@ -126,8 +135,19 @@ struct con_var { // ConVar in engine
float minval;
bool hasmax; // just sticking to sdk position
float maxval;
+};
+
+struct con_var { // ConVar in engine
+ struct con_cmdbase base;
+ union {
+ struct con_var_common v1; // OE
+ struct {
+ void **vtable_iconvar; // IConVar in engine (pure virtual)
+ struct con_var_common v2;
+ };
+ };
/*
- * Our quickly-chucked-in optional callback - doesn't match the engine!!
+ * Our quickly-chucked-in optional callback - doesn't match the engine ABI!
* Also has to be manually set in code, although that's probably fine anyway
* as it's common to only want a cvar to do something if the feature
* succesfully init-ed.
@@ -138,13 +158,24 @@ struct con_var { // ConVar in engine
/* The change callback used in most branches of Source. Takes an IConVar :) */
typedef void (*con_varcb)(void *v, const char *, float);
+/* Returns a registered variable with the given name, or null if not found. */
+struct con_var *con_findvar(const char *name);
+
+/* Returns a registered command with the given name, or null if not found. */
+struct con_cmd *con_findcmd(const char *name);
+
+/*
+ * Returns a pointer to the common (i.e. middle) part of a ConVar struct, the
+ * offset of which varies by engine version. This sub-struct contains
+ * essentially all the actual cvar-specific data.
+ */
+struct con_var_common *con_getvarcommon(const struct con_var *v);
+
/*
* These functions get and set the values of console variables in a
* neatly-abstracted manner. Note: cvar values are always strings internally -
* numerical values are just interpretations of the underlying value.
*/
-struct con_var *con_findvar(const char *name);
-struct con_cmd *con_findcmd(const char *name);
const char *con_getvarstr(const struct con_var *v);
float con_getvarf(const struct con_var *v);
int con_getvari(const struct con_var *v);
@@ -160,7 +191,7 @@ void con_setvari(struct con_var *v, int i);
* callback being requested. If this is already known, consider just grabbing
* the member directly to avoid the small amount of unnecessary work.
*/
-con_cmdcb con_getcmdcb(const struct con_cmd *cmd);
+con_cmdcbv2 con_getcmdcbv2(const struct con_cmd *cmd);
con_cmdcbv1 con_getcmdcbv1(const struct con_cmd *cmd);
/*
@@ -168,30 +199,47 @@ con_cmdcbv1 con_getcmdcbv1(const struct con_cmd *cmd);
* respectively. They are aliases to direct tier0 calls, so they work early on
* even before anything else is initialised.
*/
-#if defined(__GNUC__) || defined(__clang__)
#ifdef _WIN32
-#define __asm__(x) __asm__("_" x) // stupid mangling meme, only on windows!
-#endif
-void con_msg(const char *fmt, ...) _CON_PRINTF(1, 2) __asm__("Msg");
-void con_warn(const char *fmt, ...) _CON_PRINTF(1, 2) __asm__("Warning");
-#undef __asm__
+void con_msg(const char *fmt, ...) _CON_PRINTF(1, 2) __asm("_Msg");
+void con_warn(const char *fmt, ...) _CON_PRINTF(1, 2) __asm("_Warning");
#else
-#error Need an equivalent of asm names for your compiler!
+void con_msg(const char *fmt, ...) _CON_PRINTF(1, 2) __asm("Msg");
+void con_warn(const char *fmt, ...) _CON_PRINTF(1, 2) __asm("Warning");
#endif
struct rgba; // in engineapi.h - forward declare here to avoid warnings
struct ICvar; // "
-extern struct ICvar *_con_iface;
-extern void (*_con_colourmsgf)(struct ICvar *this, const struct rgba *c,
- const char *fmt, ...) _CON_PRINTF(3, 4);
+// DO NOT CALL THIS DIRECTLY UNDER ANY CIRCUMSTANCES.
+void _con_colourmsg(void *dummy, const struct rgba *c, const char *fmt, ...)
+ _CON_PRINTF(3, 4);
+
/*
* This provides the same functionality as ConColorMsg which was removed from
* tier0 in the L4D engine branch - specifically, it allows printing a message
* with an arbitrary RGBA colour. It must only be used after a successful
* con_init() call.
*/
-#define con_colourmsg(c, ...) _con_colourmsgf(_con_iface, c, __VA_ARGS__)
+#define con_colourmsg(/*c, fmt, */...) do { \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wuninitialized\"") \
+ _Pragma("GCC diagnostic ignored \"-Wunused\"") \
+ /* intentionally uninitialised value allows the compiler to just create a
+ hole in the stack without actually writing anything. this has been
+ confirmed by looking at the asm, because I'm that type of weirdo :^) */ \
+ void *_dummy; \
+ /* we also have to reserve EBX as a register that our wrapper can clobber
+ but the callee (engine function) won't (as it's normally callee-save).
+ the way we do this is by marking the register as clobbered both before
+ and after the call and tying both to the lifetime of a dummy variable.
+ this ensures anything that'd otherwise get put in ebx is spilled
+ elsewhere until after the call has returned. */ \
+ register uint _ebx __asm("ebx"); \
+ __asm volatile ("" : "=r" (_ebx)); \
+ _con_colourmsg(_dummy, __VA_ARGS__); \
+ __asm volatile ("" : "=r" (_ebx)); \
+ _Pragma("GCC diagnostic pop") \
+} while (0)
/*
* The index of the client responsible for the currently executing command,
@@ -228,16 +276,22 @@ extern struct _con_vtab_iconvar_wrap {
static struct con_var _cvar_##name_ = { \
.base = { \
.vtable = _con_vtab_var, \
- .name = "" #name_, .help = "" desc, .flags = (flags_) \
+ .name = "" #name_, \
+ /* n.b. redundant cast to avoid warnings */ \
+ .help = (const char *)("** unsupported ** " desc) + 18, \
+ .flags = (flags_) \
}, \
.vtable_iconvar = _con_vtab_iconvar, \
- .parent = &_cvar_##name_, /* bizarre, but how the engine does it */ \
- .defaultval = _Generic(value, char *: value, int: #value, \
- double: #value), \
- .strlen = sizeof(_Generic(value, char *: value, default: #value)), \
- .fval = _Generic(value, char *: 0, int: value, double: value), \
- .ival = _Generic(value, char *: 0, int: value, double: (int)value), \
- .hasmin = hasmin_, .minval = (min), .hasmax = hasmax_, .maxval = (max) \
+ .v2 = { \
+ .parent = &_cvar_##name_, /* bizarre, but how the engine does it */ \
+ .defaultval = _Generic(value, char *: value, int: #value, \
+ double: #value), \
+ .strlen = sizeof(_Generic(value, char *: value, default: #value)), \
+ .fval = _Generic(value, char *: 0, int: value, double: value), \
+ .ival = _Generic(value, char *: 0, int: value, double: (int)value), \
+ .hasmin = hasmin_, .minval = (min), \
+ .hasmax = hasmax_, .maxval = (max) \
+ } \
}; \
struct con_var *name_ = &_cvar_##name_;
@@ -261,12 +315,14 @@ extern struct _con_vtab_iconvar_wrap {
static struct con_cmd _ccmd_##varname = { \
.base = { \
.vtable = _con_vtab_cmd, \
- .name = "" #name_, .help = "" desc, .flags = (flags_) \
+ .name = "" #name_, \
+ /* n.b. redundant cast to avoid warnings */ \
+ .help = (const char *)("** unsupported ** " desc) + 18, \
+ .flags = (flags_) \
}, \
.cb = &func, \
- .use_newcb = true \
}; \
- struct con_cmd *varname = (struct con_cmd *)&_ccmd_##varname;
+ struct con_cmd *varname = &_ccmd_##varname;
/* Defines a command with a given function as its handler. */
#define DEF_CCMD(name, desc, func, flags) \
@@ -282,13 +338,13 @@ extern struct _con_vtab_iconvar_wrap {
/*
* Defines a console command with the handler function body immediately
- * following the macro (like in Source itself). The function takes the argument
- * `struct con_cmdargs *cmd` for command arguments.
+ * following the macro (like in Source itself). The function takes the implicit
+ * arguments `int argc` and `const char **argv` for command arguments.
*/
#define DEF_CCMD_HERE(name, desc, flags) \
- static void _cmdf_##name(const struct con_cmdargs *cmd); \
+ static void _cmdf_##name(int argc, const char **argv); \
_DEF_CCMD(name, name, desc, _cmdf_##name, flags) \
- static void _cmdf_##name(const struct con_cmdargs *cmd) \
+ static void _cmdf_##name(int argc, const char **argv) \
/* { body here } */
/*
@@ -331,6 +387,81 @@ extern struct _con_vtab_iconvar_wrap {
#define DEF_CCMD_PLUSMINUS_UNREG DEF_CCMD_PLUSMINUS
/*
+ * Defines a hook function in-place to hook a command callback, factoring in
+ * different callback ABIs used by different commands and/or different engine
+ * branches. Defines a hook_##name##_cb function to install the hook and an
+ * unhook_##name##_cb function to remove it.
+ *
+ * The hook function has the implicit arguments argc and argv, just like a
+ * command handler defined with DEF_CCMD_HERE. Calling the original command
+ * handler can be done using orig_##name##_cb, passing through argc and argv.
+ *
+ * In some cases, a command will be defined to take no arguments, in which case
+ * argc will be zero and argv will be null. In these cases, the parameters
+ * should still be passed through to the orig_ function, as this ensures
+ * compatibility with other game/engine versions.
+ */
+#define DEF_CCMD_COMPAT_HOOK(name) \
+ static union { \
+ con_cmdcbv1 v1; \
+ con_cmdcbv2 v2; \
+ } _orig_##name##_cb; \
+ static void _orig_##name##_cbv1(int argc, const char **argv) { \
+ extern int *_con_argc; \
+ extern const char **_con_argv; \
+ int _orig_argc = *_con_argc; \
+ *_con_argc = argc; \
+ if (argv != _con_argv) { \
+ /* args can be passed through as-is, or modified in place, however
+ here we have a whole different array, so we have to copy it out
+ and back to avoid confusing side effects for the caller. */ \
+ /* XXX: not bothering with the null term here; should we be? */ \
+ const char *_orig_argv[80]; \
+ memcpy(_orig_argv, _con_argv, _orig_argc * sizeof(*argv)); \
+ memcpy(_con_argv, argv, argc * sizeof(*argv)); \
+ _orig_##name##_cb.v1(); \
+ memcpy(_con_argv, _orig_argv, _orig_argc * sizeof(*argv)); \
+ } \
+ else { \
+ _orig_##name##_cb.v1(); \
+ } \
+ *_con_argc = _orig_argc; \
+ } \
+ static void _orig_##name##_cbv2(int argc, const char **argv) { \
+ struct con_cmdargs args; \
+ args.argc = argc; \
+ /* XXX: having to copy argv sucks, but can't see how to avoid without
+ ruining the interface? */ \
+ for (int i = 0; i < argc; ++i) args.argv[i] = argv[i]; \
+ _orig_##name##_cb.v2(&args); \
+ } \
+ static void (*orig_##name##_cb)(int argc, const char **argv); \
+ static void _hook_##name##_cb(int argc, const char **argv); \
+ static void _hook_##name##_cbv1() { \
+ extern int *_con_argc; \
+ extern const char **_con_argv; \
+ _hook_##name##_cb(*_con_argc, _con_argv); \
+ } \
+ static void _hook_##name##_cbv2(struct con_cmdargs *args) { \
+ _hook_##name##_cb(args->argc, args->argv); \
+ } \
+ static void hook_##name##_cb(struct con_cmd *cmd) { \
+ _orig_##name##_cb.v1 = cmd->cb_v1; \
+ if (cmd->use_newcb) { \
+ cmd->cb_v2 = &_hook_##name##_cbv2; \
+ orig_##name##_cb = &_orig_##name##_cbv2; \
+ } \
+ else { \
+ cmd->cb_v1 = _hook_##name##_cbv1; \
+ orig_##name##_cb = &_orig_##name##_cbv1; \
+ } \
+ } \
+ static void unhook_##name##_cb(struct con_cmd *cmd) { \
+ cmd->cb_v1 = _orig_##name##_cb.v1; \
+ } \
+ static void _hook_##name##_cb(int argc, const char **argv) /* ... */
+
+/*
* These functions register a command or variable, respectively, defined with
* the _UNREG variants of the above macros. These can be used to conditionally
* register things. Wherever possible, it is advised to use the DEF_FEAT_*
@@ -340,6 +471,22 @@ extern struct _con_vtab_iconvar_wrap {
void con_regvar(struct con_var *v);
void con_regcmd(struct con_cmd *c);
+/*
+ * These functions cause a command or variable to be hidden or unhidden from
+ * autocompletion and command listing results, on engine branches which support
+ * doing so. In practice this means anything that's not OE. On OE, these
+ * functions currently just do nothing, although it would be possible in theory
+ * to patch in command-hiding support if deemed important enough.
+ *
+ * Note: con_hide() will not work on an unregistered command or variable with
+ * CON_INIT_HIDDEN; this includes any of a feature's commands/variables during
+ * feature initialisation, except those that are manually registered first.
+ * In cases where a variable/command is to be registered automatically, the
+ * CON_INIT_HIDDEN flag can be removed using bitwise ops.
+ */
+void con_hide(struct con_cmdbase *b);
+void con_unhide(struct con_cmdbase *b);
+
#endif
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/dbg.c b/src/dbg.c
index 180c686..b5de6ba 100644
--- a/src/dbg.c
+++ b/src/dbg.c
@@ -82,20 +82,20 @@ void dbg_asmdump(const char *name, const void *p, int len) {
}
DEF_CCMD_HERE(sst_dbg_getcmdcb, "Get the address of a command callback", 0) {
- if (cmd->argc != 2) {
+ if (argc != 2) {
con_warn("usage: sst_dbg_getcmdcb command\n");
return;
}
- struct con_cmd *thecmd = con_findcmd(cmd->argv[1]);
+ struct con_cmd *thecmd = con_findcmd(argv[1]);
if (!thecmd) {
- errmsg_errorstd("couldn't find command %s\n", cmd->argv[1]);
+ errmsg_errorx("couldn't find command %s", argv[1]);
return;
}
#ifdef _WIN32
- con_msg("addr: %p\nghidra: %p\n", (void *)thecmd->cb,
- (void *)dbg_toghidra((void *)thecmd->cb)); // ugh
+ con_msg("addr: %p\nghidra: %p\n", (void *)thecmd->cb_insns,
+ (void *)dbg_toghidra(thecmd->cb_insns)); // ugh
#else
- con_msg("addr: %p\n", (void *)thecmd->cb);
+ con_msg("addr: %p\n", (void *)thecmd->cb_insns);
#endif
}
diff --git a/src/democustom.c b/src/democustom.c
index cae58a8..05c1d41 100644
--- a/src/democustom.c
+++ b/src/democustom.c
@@ -17,6 +17,7 @@
#include <string.h>
#include "bitbuf.h"
+#include "chunklets/x86.h"
#include "demorec.h"
#include "engineapi.h"
#include "feature.h"
@@ -25,7 +26,6 @@
#include "langext.h"
#include "mem.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE()
diff --git a/src/demorec.c b/src/demorec.c
index 7bdc517..0511d46 100644
--- a/src/demorec.c
+++ b/src/demorec.c
@@ -17,6 +17,7 @@
#include <string.h>
+#include "chunklets/x86.h"
#include "con_.h"
#include "demorec.h"
#include "engineapi.h"
@@ -32,7 +33,6 @@
#include "os.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("improved demo recording")
@@ -101,16 +101,15 @@ static void VCALLCONV hook_StopRecording(struct CDemoRecorder *this) {
DECL_VFUNC_DYN(struct CDemoRecorder, void, StartRecording)
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) {
+DEF_CCMD_COMPAT_HOOK(record) {
if_cold (!CHECK_DemoControlAllowed()) return;
bool was = *recording;
- if (!was && args->argc == 2 || args->argc == 3) {
+ if (!was && argc == 2 || argc == 3) {
// safety check: make sure a directory exists, otherwise recording
// silently fails. this is necessarily TOCTOU, but in practice it's
// way better than not doing it - just to have a sanity check.
- const char *arg = args->argv[1];
+ const char *arg = argv[1];
const char *lastslash = 0;
for (const char *p = arg; *p; ++p) {
#ifdef _WIN32
@@ -155,7 +154,7 @@ static void hook_record_cb(const struct con_cmdargs *args) {
}
}
}
- orig_record_cb(args);
+ orig_record_cb(argc, argv);
if (!was && *recording) {
*demonum = 0; // see SetSignonState comment above
// For UX, make it more obvious we're recording, in particular when not
@@ -166,16 +165,15 @@ static void hook_record_cb(const struct con_cmdargs *args) {
EMIT_DemoRecordStarting();
}
-static void hook_stop_cb(const struct con_cmdargs *args) {
+DEF_CCMD_COMPAT_HOOK(stop) {
if_cold (!CHECK_DemoControlAllowed()) return;
wantstop = true;
- orig_stop_cb(args);
+ orig_stop_cb(argc, argv);
wantstop = false;
}
-static inline bool find_demorecorder() {
+static inline bool find_demorecorder(const uchar *insns) {
#ifdef _WIN32
- const uchar *insns = (const uchar *)orig_stop_cb;
// The stop command loads `demorecorder` into ECX to call IsRecording()
for (const uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) {
@@ -238,8 +236,8 @@ bool demorec_start(const char *name) {
if (was) return false;
// dumb but easy way to do this: call the record command callback. note:
// this args object is very incomplete by enough to make the command work
- struct con_cmdargs args = {.argc = 2, .argv = {0, name, 0}};
- orig_record_cb(&args);
+ // TODO(compat): will this be a problem for OE with the global argc/argv?
+ orig_record_cb(2, (const char *[]){0, name});
if (!was && *recording) *demonum = 0; // same logic as in the hook
EMIT_DemoRecordStarting();
return *recording;
@@ -260,10 +258,8 @@ int demorec_demonum() {
INIT {
cmd_record = con_findcmd("record");
- orig_record_cb = con_getcmdcb(cmd_record);
cmd_stop = con_findcmd("stop");
- orig_stop_cb = con_getcmdcb(cmd_stop);
- if_cold (!find_demorecorder()) {
+ if_cold (!find_demorecorder(cmd_stop->cb_insns)) {
errmsg_errorx("couldn't find demo recorder instance");
return FEAT_INCOMPAT;
}
@@ -281,15 +277,12 @@ INIT {
errmsg_errorx("couldn't find demo basename variable");
return FEAT_INCOMPAT;
}
-
orig_SetSignonState = (SetSignonState_func)hook_vtable(vtable,
vtidx_SetSignonState, (void *)&hook_SetSignonState);
orig_StopRecording = (StopRecording_func)hook_vtable(vtable,
vtidx_StopRecording, (void *)&hook_StopRecording);
-
- cmd_record->cb = &hook_record_cb;
- cmd_stop->cb = &hook_stop_cb;
-
+ hook_record_cb(cmd_record);
+ hook_stop_cb(cmd_stop);
return FEAT_OK;
}
@@ -300,8 +293,8 @@ END {
void **vtable = demorecorder->vtable;
unhook_vtable(vtable, vtidx_SetSignonState, (void *)orig_SetSignonState);
unhook_vtable(vtable, vtidx_StopRecording, (void *)orig_StopRecording);
- cmd_record->cb = orig_record_cb;
- cmd_stop->cb = orig_stop_cb;
+ unhook_record_cb(cmd_record);
+ unhook_stop_cb(cmd_stop);
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/engineapi.c b/src/engineapi.c
index 44713da..980ee99 100644
--- a/src/engineapi.c
+++ b/src/engineapi.c
@@ -28,9 +28,7 @@
#include "intdefs.h"
#include "langext.h"
#include "mem.h" // "
-#include "os.h"
#include "vcall.h"
-#include "x86.h"
u32 _gametype_tag = 0; // declared in gametype.h but seems sensible enough here
@@ -55,7 +53,9 @@ DECL_VFUNC_DYN(struct IServerGameDLL, struct ServerClass *, GetAllServerClasses)
#include <gamedatainit.gen.h> // generated by build/mkgamedata.c
bool engineapi_init(int pluginver) {
- if_cold (!con_detect(pluginver)) return false;
+ // set up all these interfaces first, so con_detect can use them (currently
+ // it just uses engclient for OE, and arguably that usage should also be
+ // moved out of con_detect, but whatever, it'll do.)
pluginhandler = factory_engine("ISERVERPLUGINHELPERS001", 0);
if (engclient = factory_engine("VEngineClient015", 0)) {
@@ -93,41 +93,9 @@ bool engineapi_init(int pluginver) {
_gametype_tag |= _gametype_tag_SrvDLL005;
}
- // detect p1 for the benefit of specific features
- 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;
- }
- }
-
- if (GAMETYPE_MATCHES(L4D1)) {
- // Crash Course update
- if (con_findcmd("director_log_scavenge_items")) {
- _gametype_tag |= _gametype_tag_L4D1_1015plus;
- }
- // seems there was some code shuffling in the Mac update (1022)
- // this update came like 2-3 weeks after The Sacrifice itself released
- if (con_findvar("tank_stasis_time_suicide")) {
- _gametype_tag |= _gametype_tag_L4D1_1022plus;
- }
- }
-
- if (GAMETYPE_MATCHES(L4D2)) {
- if (con_findvar("sv_zombie_touch_trigger_delay")) {
- _gametype_tag |= _gametype_tag_L4D2_2125plus;
- }
- if (con_findvar("director_cs_weapon_spawn_chance")) {
- _gametype_tag |= _gametype_tag_TheLastStand;
- }
- }
+ if_cold (!con_detect(pluginver)) return false;
initgamedata();
- con_init();
+ con_init(); // rest of console setup requires having gamedata in place
if_cold (!gameinfo_init()) { con_disconnect(); return false; }
return true;
}
diff --git a/src/engineapi.h b/src/engineapi.h
index ef716a9..f644a0c 100644
--- a/src/engineapi.h
+++ b/src/engineapi.h
@@ -23,7 +23,6 @@
#define INC_ENGINEAPI_H
#include "intdefs.h"
-#include "vcall.h"
/*
* Here, we define a bunch of random data types as well as interfaces that don't
@@ -128,10 +127,10 @@ extern struct CGlobalVars *globalvars;
extern struct IInputSystem *inputsystem;
extern struct CEngineVGui *vgui;
-// XXX: not exactly engine *API* but not curently clear where else to put this
-struct CPlugin_common {
+// XXX: not exactly engine *API* but not currently clear where else to put this
+struct CPlugin_common_v2v3 {
bool paused;
- void *theplugin; // our own "this" pointer (or whichever other plugin it is)
+ void *theplugin; // plugin's own "this" pointer
int ifacever;
// should be the plugin library, but in old Source branches it's just null,
// because CServerPlugin::Load() erroneously shadows this field with a local
@@ -140,11 +139,17 @@ struct CPlugin_common {
struct CPlugin {
char description[128];
union {
- struct CPlugin_common v1;
+ struct {
+ // same again, but no ifacever member, for OE.
+ bool paused;
+ void *theplugin;
+ void *module;
+ } v1;
+ struct CPlugin_common_v2v3 v2;
struct {
char basename[128]; // WHY VALVE WHYYYYYYY!!!!
- struct CPlugin_common common;
- } v2;
+ struct CPlugin_common_v2v3 v3;
+ };
};
};
struct CServerPlugin /* : IServerPluginHelpers */ {
diff --git a/src/ent.c b/src/ent.c
index 14e0788..137aa75 100644
--- a/src/ent.c
+++ b/src/ent.c
@@ -15,6 +15,7 @@
*/
#include "accessor.h"
+#include "chunklets/x86.h"
#include "con_.h"
#include "dictmaptree.h"
#include "engineapi.h"
@@ -25,7 +26,6 @@
#include "langext.h"
#include "mem.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE()
@@ -71,8 +71,7 @@ struct CEntityFactoryDictionary {
#ifdef _WIN32 // TODO(linux): this'll be different too, leaving out for now
static struct CEntityFactoryDictionary *entfactorydict = 0;
-static inline bool find_entfactorydict(con_cmdcb dumpentityfactories_cb) {
- const uchar *insns = (const uchar *)dumpentityfactories_cb;
+static inline bool find_entfactorydict(const uchar *insns) {
for (const uchar *p = insns; p - insns < 64;) {
// EntityFactoryDictionary() is inlined, and returns a static, which is
// lazy-inited (trivia: this was old MSVC, so it's not thread-safe like
@@ -172,7 +171,7 @@ INIT {
#ifdef _WIN32 // TODO(linux): above
struct con_cmd *dumpentityfactories = con_findcmd("dumpentityfactories");
if_cold (!dumpentityfactories ||
- !find_entfactorydict(dumpentityfactories->cb)) {
+ !find_entfactorydict(dumpentityfactories->cb_insns)) {
errmsg_warnx("server entity factories unavailable");
}
#endif
diff --git a/src/fastfwd.c b/src/fastfwd.c
index 1b7588d..78f2d47 100644
--- a/src/fastfwd.c
+++ b/src/fastfwd.c
@@ -19,7 +19,7 @@
#include <stdlib.h>
-#include "con_.h"
+#include "chunklets/x86.h"
#include "engineapi.h"
#include "errmsg.h"
#include "gamedata.h"
@@ -29,10 +29,7 @@
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
-#include "os.h"
-#include "ppmagic.h"
#include "sst.h"
-#include "x86.h"
#include "x86util.h"
FEATURE()
diff --git a/src/feature.h b/src/feature.h
index aa3ab9d..af593cf 100644
--- a/src/feature.h
+++ b/src/feature.h
@@ -39,15 +39,19 @@
/*
* Declares that this feature should only be loaded for games matching the given
- * gametype tag (see gametype.h). Console variables and commands created using
- * DEF_FEAT_* macros will not be registered if SST is loaded by some other game.
- *
- * As an optimisation, REQUIRE_GAMEDATA() checks (see below) can also be elided
- * in cases where gamedata is always present for this particular game. As such,
- * it is wise to still specify gamedata dependencies correctly, so that the
- * definitions can be changed in the data files without breaking code.
+ * gametype tag. gametype.h must be included to use this as it defines the tag
+ * values. Console variables and commands created using DEF_FEAT_* macros will
+ * not be registered if SST is loaded by some other game.
+ *
+ * This also enables a build-time optimisation to elide REQUIRE_GAMEDATA()
+ * checks as well as has_* conditionals. As such, it is wise to still specify
+ * gamedata dependencies correctly, so that the definitions can be changed in
+ * the data files without breaking code.
*/
-#define GAMESPECIFIC(tag)
+#define GAMESPECIFIC(tag) \
+ /* impl note: see comment in gamedata.h */ \
+ __attribute((unused)) \
+ static const int _gamedata_feattags = _gametype_tag_##tag;
/*
* Indicates that the specified feature is required for this feature to
diff --git a/src/fixes.c b/src/fixes.c
index ea008e5..6a34c43 100644
--- a/src/fixes.c
+++ b/src/fixes.c
@@ -30,24 +30,33 @@
#include "ppmagic.h"
#include "sst.h"
-static void chflags(const char *name, int unset, int set) {
+static inline void chflags(const char *name, int unset, int unset_ne, int set) {
struct con_var *v = con_findvar(name);
- if (v) v->parent->base.flags = v->parent->base.flags & ~unset | set;
+ if_hot (!GAMETYPE_MATCHES(OE)) {
+ unset |= unset_ne;
+ }
+ if (v) {
+ struct con_var *p = con_getvarcommon(v)->parent;
+ p->base.flags = p->base.flags & ~unset | set;
+ }
}
static inline void unhide(const char *name) {
- chflags(name, CON_HIDDEN | CON_DEVONLY, 0);
+ chflags(name, 0, _CON_NE_HIDDEN | _CON_NE_DEVONLY, 0);
}
-static void chcmdflags(const char *name, int unset, int set) {
+static inline void chcmdflags(const char *name, int unset, int unset_ne,
+ int set) {
struct con_cmd *v = con_findcmd(name);
if (v) v->base.flags = v->base.flags & ~unset | set;
}
static inline void unhidecmd(const char *name) {
- chcmdflags(name, CON_HIDDEN | CON_DEVONLY, 0);
+ chcmdflags(name, 0, _CON_NE_HIDDEN | _CON_NE_DEVONLY, 0);
}
+// TOOD(opt): had to hack this up badly for OE compat. think of a nicer way?
+
static void generalfixes() {
// Expose all the demo stuff, for games like L4D that hide it for some
// reason.
@@ -72,45 +81,74 @@ static void generalfixes() {
// things that could conceivably cause issues with speedrun verification
// and/or pedantic following of rules; throw on cheat flag. this could be
// relaxed with the Eventual Fancy Demo Verification Stuff.
- chflags("director_afk_timeout", CON_HIDDEN | CON_DEVONLY, CON_CHEAT);
- chflags("mp_restartgame", CON_HIDDEN | CON_DEVONLY, CON_CHEAT);
+ chflags("mp_restartgame", 0, _CON_NE_HIDDEN | _CON_NE_DEVONLY, CON_CHEAT);
// also, ensure the initial state of sv_cheats goes into demos so you can't
// start a demo with cheats already on and then do something subtle
- chflags("sv_cheats", 0, CON_DEMO);
+ chflags("sv_cheats", 0, 0, CON_DEMO);
// also, let people use developer, it's pretty handy. ensure it goes in the
// demo though. even though it's obvious looking at a video, maybe some day
// a game will want to require demos only (probably not till demos are more
// robust anyway... whatever)
- chflags("developer", CON_HIDDEN | CON_DEVONLY, CON_DEMO);
+ chflags("developer", 0, _CON_NE_HIDDEN | _CON_NE_DEVONLY, CON_DEMO);
+}
+
+static void l4dspecific() {
+ // NOTE: using unconditional dev-only flags here since we know it's NE.
+ chflags("director_afk_timeout", _CON_NE_HIDDEN | _CON_NE_DEVONLY, 0,
+ CON_CHEAT);
// fps_max policy varies a bit between speedgames and their communities!
// in theory we might wanna remove CON_NOTCONN on Portal 1 in a future
// release, but for now people haven't fully talked themselves into it.
struct con_var *v = con_findvar("fps_max");
- if (GAMETYPE_MATCHES(L4Dx)) {
- // for L4D games, generally changing anything above normal limits is
- // disallowed, but externally capping FPS will always be possible so we
- // might as well allow lowering it ingame for convenience.
- if (v->parent->base.flags & (CON_HIDDEN | CON_DEVONLY)) {
- v->parent->base.flags &= ~(CON_HIDDEN | CON_DEVONLY);
- v->parent->hasmax = true; v->parent->maxval = 300;
- }
- else if (!v->parent->hasmax) {
- // in TLS, this was made changeable, but still limit to 1000 to
- // prevent breaking the engine
- v->parent->hasmax = true; v->parent->maxval = 1000;
- }
- // also show the lower limit in help, and prevent 0 (which is unlimited)
- v->parent->hasmin = true; v->parent->minval = 30;
- con_setvarf(v, con_getvarf(v)); // hack: reapply limit if we loaded late
+ // for L4D games, generally changing anything above normal limits is
+ // disallowed, but externally capping FPS will always be possible so we
+ // might as well allow lowering it in-game for convenience.
+ struct con_var *p = con_getvarcommon(v)->parent;
+ struct con_var_common *c = con_getvarcommon(p);
+ if (p->base.flags & (_CON_NE_HIDDEN | _CON_NE_DEVONLY)) {
+ p->base.flags &= ~(_CON_NE_HIDDEN | _CON_NE_DEVONLY);
+ c->hasmax = true; c->maxval = 300;
+ }
+ else if (!c->hasmax) {
+ // in TLS, this was made changeable, but still limit to 1000 to
+ // prevent breaking the engine
+ c->hasmax = true; c->maxval = 1000;
}
+ // also show the lower limit in help, and prevent 0 (which is unlimited)
+ c->hasmin = true; c->minval = 30;
+ con_setvarf(v, con_getvarf(v)); // hack: reapply limit if we loaded late
+}
+
+static void l4d1specific() {
+ // For some reason, L4D1 hides mat_monitorgamma and doesn't archive it.
+ // This means on every startup it's necessary to manually set non-default
+ // values via the menu. This change here brings it in line with pretty much
+ // all other Source games for convenience.
+ chflags("mat_monitorgamma", _CON_NE_HIDDEN | _CON_NE_DEVONLY, 0,
+ CON_ARCHIVE);
+
+ // Very early versions of L4D1 have a bunch of useless console spam. Setting
+ // these hidden variables to 0 gets rid of it.
+ struct con_var *v = con_findvar("ui_l4d_debug");
+ if (v) con_setvari(v, 0);
+ v = con_findvar("mm_l4d_debug");
+ if (v) con_setvari(v, 0);
+
+ // same thing as above, seemed easier to just dupe :)
+ chcmdflags("cl_fullupdate", CON_CHEAT, 0, 0);
+
+ // These commands lack CLIENTCMD_CAN_EXECUTE, so enabling/disabling addons
+ // doesn't work without manually running these in the console afterwards.
+ chcmdflags("mission_reload", 0, 0, _CON_NE_CCMDEXEC);
+ chcmdflags("update_addon_paths", 0, 0, _CON_NE_CCMDEXEC);
}
static void l4d2specific() {
// L4D2 doesn't let you set sv_cheats in lobbies, but turns out it skips all
- // the lobby checks if this random command is developer-only, presumably
+ // the lobby checks if this random command is not developer-only, presumably
// because that flag is compiled out in debug builds and devs want to be
// able to use cheats. Took literally hours of staring at Ghidra to find
// this out. Good meme 8/10.
@@ -128,15 +166,17 @@ static void l4d2specific() {
// possible on these earlier versions (who knows if that breaks
// something...).
struct con_var *v = con_findvar("mat_queue_mode");
- 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;
- v->parent->hasmax = true; v->parent->maxval = 0;
+ struct con_var *p = con_getvarcommon(v)->parent;
+ if_hot (v && !(p->base.flags & CON_ARCHIVE)) { // not already fixed
+ struct con_var_common *c = con_getvarcommon(p);
+ p->base.flags = p->base.flags &
+ ~(_CON_NE_HIDDEN | _CON_NE_DEVONLY) | CON_ARCHIVE;
+ c->hasmin = true; c->minval = -1;
+ c->hasmax = true; c->maxval = 0;
}
#ifdef _WIN32
- // L4D2 has broken (dark) rendering on Intel iGPUs unless
+ // L4D2 has broken (overly dark) rendering on Intel iGPUs unless
// mat_tonemapping_occlusion_use_stencil is enabled. Supposedly Valve used
// to detect device IDs to enable it on, but new devices are still broken,
// so just blanket enable it if the primary adapter is Intel, since it
@@ -164,30 +204,7 @@ e:
// We're preemptively removing its cheat flag here, so if it turns out to be
// absolutely necessary, people can use it. If it doesn't work, or some
// other workaround is found, this might get reverted.
- chcmdflags("cl_fullupdate", CON_CHEAT, 0);
-}
-
-static void l4d1specific() {
- // For some reason, L4D1 hides mat_monitorgamma and doesn't archive it.
- // This means on every startup it's necessary to manually set non-default
- // values via the menu. This change here brings it in line with pretty much
- // all other Source games for convenience.
- chflags("mat_monitorgamma", CON_HIDDEN | CON_DEVONLY, CON_ARCHIVE);
-
- // Very early versions of L4D1 have a bunch of useless console spam. Setting
- // these hidden variables to 0 gets rid of it.
- struct con_var *v = con_findvar("ui_l4d_debug");
- if (v) con_setvari(v, 0);
- v = con_findvar("mm_l4d_debug");
- if (v) con_setvari(v, 0);
-
- // same thing as above, seemed easier to just dupe :)
- chcmdflags("cl_fullupdate", CON_CHEAT, 0);
-
- // These commands lack CLIENTCMD_CAN_EXECUTE, so enabling/disabling addons
- // doesn't work without manually running these in the console afterwards.
- chcmdflags("mission_reload", 0, CON_CCMDEXEC);
- chcmdflags("update_addon_paths", 0, CON_CCMDEXEC);
+ chcmdflags("cl_fullupdate", CON_CHEAT, 0, 0);
}
static void portal1specific() {
@@ -216,9 +233,14 @@ static void portal1specific() {
void fixes_apply() {
generalfixes();
- if (GAMETYPE_MATCHES(L4D1)) l4d1specific();
- else if (GAMETYPE_MATCHES(L4D2x)) l4d2specific();
- else if (GAMETYPE_MATCHES(Portal1)) portal1specific();
+ if (GAMETYPE_MATCHES(L4Dx)) {
+ l4dspecific();
+ 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 bfef858..b7a278c 100644
--- a/src/fov.c
+++ b/src/fov.c
@@ -18,6 +18,7 @@
// TODO(linux): theoretically, probably ifdef out the cvar-replacement stuff; we
// expect any game that's been ported to linux to already have fov_desired
+#include "chunklets/x86.h"
#include "con_.h"
#include "engineapi.h"
#include "errmsg.h"
@@ -31,7 +32,6 @@
#include "mem.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("extended FOV range")
@@ -42,7 +42,7 @@ REQUEST(ent)
DEF_CVAR_MINMAX_UNREG(fov_desired,
"Set the base field of view (SST reimplementation)", 75, 75, 120,
- CON_HIDDEN | CON_ARCHIVE)
+ CON_INIT_HIDDEN | CON_ARCHIVE)
static struct con_var *real_fov_desired; // engine's if it has it, or ours
typedef void (*VCALLCONV SetDefaultFOV_func)(void *, int);
@@ -88,8 +88,10 @@ INIT {
if_cold (!cmd_fov) return FEAT_INCOMPAT; // shouldn't happen, but who knows!
if (real_fov_desired = con_findvar("fov_desired")) {
// latest steampipe already goes up to 120 fov
- if (real_fov_desired->parent->maxval == 120) return FEAT_SKIP;
- real_fov_desired->parent->maxval = 120;
+ struct con_var *p = con_getvarcommon(real_fov_desired)->parent;
+ struct con_var_common *c = con_getvarcommon(p);
+ if (c->maxval == 120) return FEAT_SKIP;
+ c->maxval = 120;
}
else {
if (!has_ent) return FEAT_INCOMPAT;
@@ -109,26 +111,27 @@ INIT {
// we might not be using our cvar but simpler to do this unconditionally
fov_desired->cb = &fovcb;
- fov_desired->parent->base.flags &= ~CON_HIDDEN;
+ con_unhide(&fov_desired->base);
// hide the original fov command since we've effectively broken it anyway :)
- cmd_fov->base.flags |= CON_DEVONLY;
+ // NOTE: assumes NE. fine for now because we're GAMESPECIFIC.
+ cmd_fov->base.flags |= _CON_NE_DEVONLY;
return FEAT_OK;
}
END {
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) {
- con_setvarf(real_fov_desired, 90); // blegh.
- }
+ struct con_var *p = con_getvarcommon(real_fov_desired)->parent;
+ struct con_var_common *c = con_getvarcommon(p);
+ c->maxval = 90;
+ if (c->fval > 90) con_setvarf(real_fov_desired, 90); // blegh.
}
else {
void *player = ent_get(1); // also singleplayer only
if (player) orig_SetDefaultFOV(player, 75);
}
unhook_inline((void *)orig_SetDefaultFOV);
- cmd_fov->base.flags &= ~CON_DEVONLY;
+ cmd_fov->base.flags &= ~_CON_NE_DEVONLY;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/gamedata.h b/src/gamedata.h
index 7d91373..c14c83c 100644
--- a/src/gamedata.h
+++ b/src/gamedata.h
@@ -17,6 +17,15 @@
#ifndef INC_GAMEDATA_H
#define INC_GAMEDATA_H
+#include "gametype.h"
+
+// this defaults to zero (tentative definition), but gets defined to a value by
+// GAMESPECIFIC() in feature.h. static const int variables get constant-folded
+// even in -O0. so, this lets us short-circuit has_ checks inside features.
+// we also check if a gamedata entry's
+__attribute((unused))
+static const int _gamedata_feattags;
+
// STUPID HACK to avoid pollution if abi.h not already included (only because
// generated gamedata stuff relies on this being defined)
#ifndef NVDTOR
diff --git a/src/gameinfo.c b/src/gameinfo.c
index a3f4eac..877ca2a 100644
--- a/src/gameinfo.c
+++ b/src/gameinfo.c
@@ -99,7 +99,7 @@ bool gameinfo_init() {
int casebit = 0;
for (char *p = title; *p; ++p) {
if (*p >= 'A' && *p <= 'Z') *p |= casebit;
- casebit = (*p == ' ' || *p == '-') << 5; // ? 32 : 0
+ casebit = (*p != ' ' && *p != '-') << 5; // ? 32 : 0
}
}
}
diff --git a/src/gameserver.c b/src/gameserver.c
index 6f3d394..4315afe 100644
--- a/src/gameserver.c
+++ b/src/gameserver.c
@@ -14,6 +14,7 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
+#include "chunklets/x86.h"
#include "con_.h"
#include "errmsg.h"
#include "feature.h"
@@ -21,7 +22,6 @@
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
-#include "x86.h"
#include "vcall.h"
#include "x86util.h"
@@ -35,13 +35,12 @@ static struct CGameServer *sv;
int gameserver_spawncount() { return GetSpawnCount(sv); }
-static bool find_sv(con_cmdcb pause_cb) {
+static bool find_sv(const uchar *insns) {
#ifdef _WIN32
// The last thing pause does is call BroadcastPrintf with 4 args including
// `this`, all on the stack since it's varargs. 2 of the args are pushed
// immediately before `this`, so we can just look for 3 back-to-back pushes
// and a call.
- const uchar *insns = (const uchar *)pause_cb;
int pushes = 0;
for (const uchar *p = insns; p - insns < 256;) {
if (*p == X86_PUSHIW || *p >= X86_PUSHEAX && *p <= X86_PUSHEDI) {
@@ -68,7 +67,7 @@ static bool find_sv(con_cmdcb pause_cb) {
INIT {
struct con_cmd *pause = con_findcmd("pause");
- if_cold (!find_sv(pause->cb)) {
+ if_cold (!find_sv(pause->cb_insns)) {
errmsg_errorx("couldn't find game server object");
return FEAT_INCOMPAT;
}
diff --git a/src/gametype.h b/src/gametype.h
index fa899c2..4e2dc55 100644
--- a/src/gametype.h
+++ b/src/gametype.h
@@ -23,51 +23,66 @@
extern u32 _gametype_tag;
-#define GAMETYPE_BASETAGS(X) \
+#define GAMETYPE_BASETAGS(ALL, WINDOWSONLY) \
/* general engine branches used in a bunch of stuff */ \
- X(OE) \
- X(OrangeBox) \
- X(2013) \
+ WINDOWSONLY(OE) \
+ ALL(OrangeBox) \
+ ALL(2013) \
\
/* specific games with dedicated branches / engine changes */ \
- /* TODO(compat): detect dmomm, if only to fail (VEngineServer broke) */ \
- X(DMoMM) \
- X(L4D1) \
- X(L4D2) \
- X(L4DS) /* Survivors (weird arcade port) */ \
- X(Portal2) \
+ /* TODO(compat): dmomm seems to fail currently (VEngineServer broke?) */ \
+ WINDOWSONLY(DMoMM) \
+ WINDOWSONLY(L4D1) \
+ ALL(L4D2) \
+ WINDOWSONLY(L4DS) /* Survivors (weird arcade port) */ \
+ ALL(Portal2) \
\
/* games needing game-specific stuff, but not tied to a singular branch */ \
- X(Portal1) \
- X(HL2series) /* HL2, episodes, mods */ \
+ ALL(Portal1) \
+ ALL(HL2series) /* HL2, episodes, mods */ \
\
/* VEngineClient versions */ \
- X(Client015) \
- X(Client014) \
- X(Client013) \
- X(Client012) \
+ ALL(Client015) \
+ ALL(Client014) \
+ ALL(Client013) \
+ ALL(Client012) \
\
/* VEngineServer versions */ \
- X(Server021) \
+ ALL(Server021) \
\
/* ServerGameDLL versions */ \
- X(SrvDLL009) /* 2013-ish */ \
- X(SrvDLL005) /* mostly everything else, it seems */ \
+ ALL(SrvDLL009) /* 2013-ish */ \
+ ALL(SrvDLL005) /* mostly everything else, it seems */ \
\
/* games needing version-specific stuff */ \
- X(Portal1_3420) \
- X(L4D1_1015plus) /* Crash Course update */ \
- X(L4D1_1022plus) /* Mac update, bunch of code reshuffling */ \
- X(L4D2_2125plus) \
- X(TheLastStand) /* The JAiZ update */ \
+ WINDOWSONLY(Portal1_3420) \
+ WINDOWSONLY(L4D1_1015plus) /* Crash Course update */ \
+ WINDOWSONLY(L4D1_1022plus) /* Mac update, bunch of code reshuffling */ \
+ ALL(L4D2_2125plus) \
+ ALL(TheLastStand) /* The JAiZ update */ \
enum {
+ // here we define the enum values in such a way that on linux, the windows-
+ // only tags are still defined as zero. that way we can use GAMETYPE_MATCHES
+ // checks in some cases without needing #ifdef _WIN32 and the optimiser can
+ // throw it out.
#define _GAMETYPE_ENUMBIT(x) _gametype_tagbit_##x,
-GAMETYPE_BASETAGS(_GAMETYPE_ENUMBIT)
-#undef _GAMETYPE_ENUMBIT
#define _GAMETYPE_ENUMVAL(x) _gametype_tag_##x = 1 << _gametype_tagbit_##x,
-GAMETYPE_BASETAGS(_GAMETYPE_ENUMVAL)
+#define _GAMETYPE_DISCARD(x)
+#define _GAMETYPE_ZERO(x) _gametype_tag_##x = 0,
+#ifdef _WIN32
+GAMETYPE_BASETAGS(_GAMETYPE_ENUMBIT, _GAMETYPE_ENUMBIT)
+GAMETYPE_BASETAGS(_GAMETYPE_ENUMVAL, _GAMETYPE_ENUMVAL)
+#else
+GAMETYPE_BASETAGS(_GAMETYPE_ENUMBIT, _GAMETYPE_DISCARD)
+GAMETYPE_BASETAGS(_GAMETYPE_ENUMVAL, _GAMETYPE_DISCARD)
+GAMETYPE_BASETAGS(_GAMETYPE_DISCARD, _GAMETYPE_ZERO)
+#endif
+#define _GAMETYPE_ENUMVAL(x) _gametype_tag_##x = 1 << _gametype_tagbit_##x,
+#undef _GAMETYPE_ZERO
+#undef _GAMETYPE_DISCARD
#undef _GAMETYPE_ENUMVAL
+#undef _GAMETYPE_ENUMBIT
};
/* Matches for any of multiple possible tags */
diff --git a/src/hook.c b/src/hook.c
index 5f964ad..0355c18 100644
--- a/src/hook.c
+++ b/src/hook.c
@@ -17,12 +17,12 @@
#include <string.h>
+#include "chunklets/x86.h"
#include "hook.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
#include "os.h"
-#include "x86.h"
// 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,
diff --git a/src/hud.c b/src/hud.c
index cb360ca..0c7c142 100644
--- a/src/hud.c
+++ b/src/hud.c
@@ -15,12 +15,12 @@
* PERFORMANCE OF THIS SOFTWARE.
*/
+#include "chunklets/x86.h"
#include "engineapi.h"
#include "errmsg.h"
#include "event.h"
#include "feature.h"
#include "gamedata.h"
-#include "gametype.h"
#include "hook.h"
#include "hud.h"
#include "intdefs.h"
@@ -29,7 +29,6 @@
#include "os.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE()
@@ -48,6 +47,7 @@ REQUIRE_GAMEDATA(vtidx_DrawPrintText)
REQUIRE_GAMEDATA(vtidx_GetScreenSize)
REQUIRE_GAMEDATA(vtidx_GetFontTall)
REQUIRE_GAMEDATA(vtidx_GetCharacterWidth)
+REQUIRE_GAMEDATA(vtidx_GetTextSize)
// CEngineVGui
REQUIRE_GAMEDATA(vtidx_GetPanel)
// vgui::Panel
@@ -169,9 +169,9 @@ static bool find_toolspanel(struct CEngineVGui *enginevgui) {
}
INIT {
- matsurf = factory_engine("MatSystemSurface006", 0);
- if_cold (!matsurf) {
- errmsg_errorx("couldn't get MatSystemSurface006 interface");
+ if (!(matsurf = factory_engine("MatSystemSurface006", 0)) &&
+ !(matsurf = factory_engine("MatSystemSurface008", 0))) {
+ errmsg_errorx("couldn't get MatSystemSurface interface");
return FEAT_INCOMPAT;
}
struct ISchemeManager *schememgr = factory_engine("VGUI_Scheme010", 0);
diff --git a/src/inputhud.c b/src/inputhud.c
index fdefe6a..ef7f9ac 100644
--- a/src/inputhud.c
+++ b/src/inputhud.c
@@ -18,6 +18,7 @@
#include <math.h>
+#include "chunklets/x86.h"
#include "con_.h"
#include "engineapi.h"
#include "event.h"
@@ -32,7 +33,6 @@
#include "langext.h"
#include "mem.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("button input HUD")
@@ -150,16 +150,12 @@ static inline int bsf(uint 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"
+ __asm volatile (
+ "bsf %0, %1\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
@@ -432,14 +428,16 @@ INIT {
// HL2 sprint HUD, so move it up. This is a bit yucky, but at least we don't
// have to go through all the virtual setter crap twice...
if (GAMETYPE_MATCHES(L4D)) {
- sst_inputhud_y->defaultval = "0.82";
- sst_inputhud_y->fval = 0.82f;
- sst_inputhud_y->ival = 0;
+ struct con_var_common *c = con_getvarcommon(sst_inputhud_y);
+ c->defaultval = "0.82";
+ c->fval = 0.82f;
+ c->ival = 0;
}
else if (GAMETYPE_MATCHES(HL2series)) {
- sst_inputhud_y->defaultval = "0.75";
- sst_inputhud_y->fval = 0.75f;
- sst_inputhud_y->ival = 0;
+ struct con_var_common *c = con_getvarcommon(sst_inputhud_y);
+ c->defaultval = "0.75";
+ c->fval = 0.75f;
+ c->ival = 0;
}
return FEAT_OK;
diff --git a/src/kvsys.c b/src/kvsys.c
index 9c0f75c..25a6672 100644
--- a/src/kvsys.c
+++ b/src/kvsys.c
@@ -16,6 +16,7 @@
*/
#include "abi.h"
+#include "chunklets/x86.h"
#include "extmalloc.h"
#include "errmsg.h"
#include "feature.h"
@@ -25,7 +26,6 @@
#include "langext.h"
#include "os.h"
#include "vcall.h"
-#include "x86.h"
FEATURE()
diff --git a/src/l4d1democompat.c b/src/l4d1democompat.c
index 1cfe959..4280d53 100644
--- a/src/l4d1democompat.c
+++ b/src/l4d1democompat.c
@@ -17,15 +17,16 @@
*/
#include "accessor.h"
+#include "chunklets/x86.h"
#include "con_.h"
#include "errmsg.h"
#include "feature.h"
+#include "gametype.h"
#include "hook.h"
#include "intdefs.h"
#include "mem.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("Left 4 Dead 1 demo file backwards compatibility")
@@ -50,9 +51,8 @@ static GetHostVersion_func orig_GetHostVersion;
typedef void (*VCALLCONV ReadDemoHeader_func)(void *);
static ReadDemoHeader_func orig_ReadDemoHeader;
-static inline bool find_ReadDemoHeader(con_cmdcb listdemo_cb) {
+static inline bool find_ReadDemoHeader(const uchar *insns) {
// Find the call to ReadDemoHeader in the listdemo callback
- const uchar *insns = (const uchar *)listdemo_cb;
for (const uchar *p = insns; p - insns < 192;) {
if (p[0] == X86_LEA && p[1] == X86_MODRM(2, 1, 4) && p[2] == 0x24 &&
p[7] == X86_CALL && p[12] == X86_LEA &&
@@ -115,21 +115,14 @@ static void VCALLCONV hook_ReadDemoHeader(struct CDemoFile *this) {
orig_ReadDemoHeader(this);
}
-#if defined(__clang__)
-__attribute__((naked))
-#elif defined(_MSC_VER)
-#error Inadequate inline assembly syntax, use Clang instead.
-#else
-#error No way to do naked functions! We only support Clang at the moment.
-#endif
-static int hook_midpoint() {
- __asm__ volatile (
- "pushl %%eax\n"
- "movl %1, %%eax\n"
- "movl (%%eax), %%eax\n" // dereference this_protocol
- "movl %%eax, %0\n" // store in demoversion
- "popl %%eax\n"
- "jmpl *%2\n"
+static asm_only int hook_midpoint() {
+ __asm volatile (
+ "push eax\n"
+ "mov eax, %1\n"
+ "mov eax, [eax]\n" // dereference this_protocol
+ "mov %0, eax\n" // store in demoversion
+ "pop eax\n"
+ "jmp dword ptr %2\n"
: "=m" (demoversion)
: "m" (this_protocol), "m" (ReadDemoHeader_midpoint)
);
@@ -138,7 +131,7 @@ static int hook_midpoint() {
INIT {
struct con_cmd *cmd_listdemo = con_findcmd("listdemo");
if_cold (!cmd_listdemo) return FEAT_INCOMPAT; // should never happen!
- if_cold (!find_ReadDemoHeader(cmd_listdemo->cb)) {
+ if_cold (!find_ReadDemoHeader(cmd_listdemo->cb_insns)) {
errmsg_errorx("couldn't find ReadDemoHeader function");
return FEAT_INCOMPAT;
}
diff --git a/src/l4daddon.c b/src/l4daddon.c
index fd344ce..5336199 100644
--- a/src/l4daddon.c
+++ b/src/l4daddon.c
@@ -18,6 +18,7 @@
#include <string.h>
+#include "chunklets/x86.h"
#include "con_.h"
#include "engineapi.h"
#include "errmsg.h"
@@ -31,7 +32,6 @@
#include "ppmagic.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
FEATURE("Left 4 Dead 2 addon bugfixes")
@@ -156,9 +156,8 @@ static inline bool find_FS_MAFAS() {
return false;
}
-static inline bool find_addonvecsz(con_cmdcb show_addon_metadata_cb) {
+static inline bool find_addonvecsz(const uchar *insns) {
#ifdef _WIN32
- const uchar *insns = (const uchar*)show_addon_metadata_cb;
// show_addon_metadata immediately checks if s_vecAddonMetadata.m_Size is 0,
// so we can just grab it from the CMP instruction
for (const uchar *p = insns; p - insns < 32;) {
@@ -224,7 +223,7 @@ static inline void try_fix_broken_addon_check() {
INIT {
struct con_cmd *show_addon_metadata = con_findcmd("show_addon_metadata");
if_cold (!show_addon_metadata) return FEAT_INCOMPAT; // shouldn't happen!
- if_cold (!find_addonvecsz(show_addon_metadata->cb)) {
+ if_cold (!find_addonvecsz(show_addon_metadata->cb_insns)) {
errmsg_errorx("couldn't find pointer to addon list");
return FEAT_INCOMPAT;
}
diff --git a/src/l4dreset.c b/src/l4dreset.c
index 207e487..dd59c60 100644
--- a/src/l4dreset.c
+++ b/src/l4dreset.c
@@ -20,6 +20,7 @@
#include "abi.h"
#include "accessor.h"
+#include "chunklets/x86.h"
#include "con_.h"
#include "engineapi.h"
#include "ent.h"
@@ -32,12 +33,11 @@
#include "gametype.h"
#include "hook.h"
#include "intdefs.h"
-#include "langext.h"
#include "l4dmm.h"
+#include "langext.h"
#include "mem.h"
#include "sst.h"
#include "vcall.h"
-#include "x86.h"
#include "x86util.h"
#ifdef _WIN32
@@ -349,7 +349,7 @@ static int *FinaleEscapeState;
DEF_FEAT_CCMD_HERE(sst_l4d_quickreset,
"Reset (or switch) campaign and clear all vote cooldowns", 0) {
- if (cmd->argc > 2) {
+ if (argc > 2) {
con_warn("usage: sst_l4d_quickreset [campaignid]\n");
return;
}
@@ -358,9 +358,9 @@ DEF_FEAT_CCMD_HERE(sst_l4d_quickreset,
return;
}
const char *campaign = l4dmm_curcampaign();
- if (cmd->argc == 2 && (!campaign || strcasecmp(campaign, cmd->argv[1]))) {
- change(cmd->argv[1]);
- campaign = cmd->argv[1];
+ if (argc == 2 && (!campaign || strcasecmp(campaign, argv[1]))) {
+ change(argv[1]);
+ campaign = argv[1];
nextmapnum = gameserver_spawncount() + 1; // immediate next changelevel
}
else {
@@ -381,8 +381,7 @@ DEF_FEAT_CCMD_HERE(sst_l4d_quickreset,
}
// Note: this returns a pointer to subsequent bytes for find_voteissues() below
-static inline const uchar *find_votecontroller(con_cmdcbv1 listissues_cb) {
- const uchar *insns = (const uchar *)listissues_cb;
+static inline const uchar *find_votecontroller(const uchar *insns) {
#ifdef _WIN32
// The "listissues" command calls CVoteController::ListIssues, loading
// g_voteController into ECX
@@ -510,8 +509,7 @@ INIT {
errmsg_errorx("couldn't find \"listissues\" command");
return FEAT_INCOMPAT;
}
- con_cmdcbv1 listissues_cb = con_getcmdcbv1(cmd_listissues);
- const uchar *nextinsns = find_votecontroller(listissues_cb);
+ const uchar *nextinsns = find_votecontroller(cmd_listissues->cb_insns);
if_cold (!nextinsns) {
errmsg_errorx("couldn't find vote controller variable");
return FEAT_INCOMPAT;
diff --git a/src/l4dwarp.c b/src/l4dwarp.c
index 03edb9e..7ddb4cc 100644
--- a/src/l4dwarp.c
+++ b/src/l4dwarp.c
@@ -19,6 +19,7 @@
#include <math.h>
#include "accessor.h"
+#include "chunklets/x86.h"
#include "clientcon.h"
#include "con_.h"
#include "engineapi.h"
@@ -26,13 +27,11 @@
#include "ent.h"
#include "feature.h"
#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")
@@ -110,10 +109,10 @@ DEF_FEAT_CCMD_HERE(sst_l4d_testwarp, "Simulate a bot warping to you "
CON_SERVERSIDE | CON_CHEAT) {
bool staystuck = false;
// TODO(autocomplete): suggest this argument
- if (cmd->argc == 2 && !strcmp(cmd->argv[1], "staystuck")) {
+ if (argc == 2 && !strcmp(argv[1], "staystuck")) {
staystuck = true;
}
- else if (cmd->argc != 1) {
+ else if (argc != 1) {
clientcon_reply("usage: sst_l4d_testwarp [staystuck]\n");
return;
}
@@ -268,9 +267,8 @@ DEF_CCMD_HERE_UNREG(sst_l4d_previewwarp, "Visualise bot warp unstuck logic "
}
}
-static bool find_EntityPlacementTest(con_cmdcb z_add_cb) {
+static bool find_EntityPlacementTest(const uchar *insns) {
#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 &&
@@ -314,7 +312,7 @@ static bool init_filter() {
INIT {
struct con_cmd *z_add = con_findcmd("z_add");
- if (!z_add || !find_EntityPlacementTest(z_add->cb)) {
+ if (!z_add || !find_EntityPlacementTest(z_add->cb_insns)) {
errmsg_errorx("couldn't find EntityPlacementTest function");
return FEAT_INCOMPAT;
}
diff --git a/src/langext.h b/src/langext.h
index de96ef5..0624b71 100644
--- a/src/langext.h
+++ b/src/langext.h
@@ -16,7 +16,8 @@
#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__))
+#define cold __attribute((__cold__, __noinline__))
+#define asm_only __attribute((__naked__)) // N.B.: may not actually work in GCC?
#else
#define if_hot(x) if (x)
#define if_cold(x) if (x)
@@ -25,11 +26,13 @@
#define unreachable __assume(0)
#define assume(x) ((void)(__assume(x), 0))
#define cold __declspec(noinline)
+#define asm_only __declspec(naked)
#else
static inline _Noreturn void _invoke_ub() {}
#define unreachable (_invoke_ub())
#define assume(x) ((void)(!!(x) || (_invoke_ub(), 0)))
#define cold
+//#define asm_only // Can't use this without Clang/GCC/MSVC. Too bad.
#endif
#endif
@@ -57,12 +60,23 @@ static inline _Noreturn void _invoke_ub() {}
#define import
#ifdef __GNUC__
// N.B. we assume -fvisibility=hidden
-#define export __attribute__((visibility("default"))
+#define export __attribute((visibility("default"))
#else
#define export int exp[-!!"compiler needs a way to export symbols!"];
#endif
#endif
+#ifdef __clang__
+#define tailcall \
+ /* Clang forces us to use void return and THEN warns about it ._. */ \
+ _Pragma("clang diagnostic push") \
+ _Pragma("clang diagnostic ignored \"-Wpedantic\"") \
+ __attribute((musttail)) return \
+ _Pragma("clang diagnostic pop")
+#else
+//#define tailcall // Can't use this without Clang.
+#endif
+
#endif
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/mem.h b/src/mem.h
index 726018f..86d310e 100644
--- a/src/mem.h
+++ b/src/mem.h
@@ -49,11 +49,8 @@ static inline s64 mem_loads64(const void *p) {
/* Retrieves a pointer from an unaligned pointer-to-pointer. */
static inline void *mem_loadptr(const void *p) {
-#if defined(_WIN64) || defined(__x86_64__)
- return (void *)mem_loadu64(p);
-#else
+ if (sizeof(void *) == 8) return (void *)mem_loadu64(p);
return (void *)mem_loadu32(p);
-#endif
}
/* Retrieves a signed size/offset value from an unaligned pointer. */
diff --git a/src/nomute.c b/src/nomute.c
index 8cdb823..9b69dad 100644
--- a/src/nomute.c
+++ b/src/nomute.c
@@ -34,7 +34,7 @@ FEATURE("inactive window audio control")
DEF_CVAR_UNREG(snd_mute_losefocus,
"Keep playing audio while tabbed out (SST reimplementation)", 1,
- CON_ARCHIVE | CON_HIDDEN)
+ CON_ARCHIVE | CON_INIT_HIDDEN)
static IDirectSoundVtbl *ds_vt = 0;
static typeof(ds_vt->CreateSoundBuffer) orig_CreateSoundBuffer;
@@ -81,7 +81,7 @@ INIT {
orig_CreateSoundBuffer = ds_vt->CreateSoundBuffer;
ds_vt->CreateSoundBuffer = &hook_CreateSoundBuffer;
- snd_mute_losefocus->base.flags &= ~CON_HIDDEN;
+ con_unhide(&snd_mute_losefocus->base);
struct con_cmd *snd_restart = con_findcmd("snd_restart");
if_hot (snd_restart) {
snd_restart_cb = con_getcmdcbv1(snd_restart);
diff --git a/src/nosleep.c b/src/nosleep.c
index b440bf3..3ac4069 100644
--- a/src/nosleep.c
+++ b/src/nosleep.c
@@ -31,7 +31,7 @@ REQUIRE_GLOBAL(inputsystem)
DEF_CVAR_UNREG(engine_no_focus_sleep,
"Delay while tabbed out (SST reimplementation)", 50,
- CON_ARCHIVE | CON_HIDDEN)
+ CON_ARCHIVE | CON_INIT_HIDDEN)
static void **vtable;
@@ -56,7 +56,7 @@ INIT {
}
orig_SleepUntilInput = (SleepUntilInput_func)hook_vtable(vtable,
vtidx_SleepUntilInput, (void *)&hook_SleepUntilInput);
- engine_no_focus_sleep->base.flags &= ~CON_HIDDEN;
+ con_unhide(&engine_no_focus_sleep->base);
return FEAT_OK;
}
diff --git a/src/portalcolours.c b/src/portalcolours.c
index 24224c1..f04a9a8 100644
--- a/src/portalcolours.c
+++ b/src/portalcolours.c
@@ -26,7 +26,6 @@
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
-#include "os.h"
#include "ppmagic.h"
#include "sst.h"
#include "vcall.h"
@@ -83,13 +82,14 @@ static bool find_UTIL_Portal_Color(void *base) {
// 5135
orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)mem_offset(base, 0x1BF090);
if (!memcmp((void *)orig_UTIL_Portal_Color, x, sizeof(x))) return true;
+ // 4104
+ orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)mem_offset(base, 0x1ADC30);
+ if (!memcmp((void *)orig_UTIL_Portal_Color, x, sizeof(x))) return true;
// 3420
orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)mem_offset(base, 0x1AA810);
if (!memcmp((void *)orig_UTIL_Portal_Color, x, sizeof(x))) return true;
// SteamPipe (7197370) - almost sure to break in a later update!
// TODO(compat): this has indeed been broken for ages.
- // TODO(compat): we also still don't have 4104. really need to do this
- // properly some time soon, it seems.
static const uchar y[] = HEXBYTES(55, 8B, EC, 8B, 45, 0C, 83, E8, 00, 74,
24, 48, 74, 16, 48, 8B, 45, 08, 74, 08, C7, 00, FF, FF);
orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)mem_offset(base, 0x234C00);
diff --git a/src/portalisg.c b/src/portalisg.c
new file mode 100644
index 0000000..d7851eb
--- /dev/null
+++ b/src/portalisg.c
@@ -0,0 +1,152 @@
+/*
+ * Copyright © 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.
+ */
+
+#include "chunklets/x86.h"
+#include "con_.h"
+#include "engineapi.h"
+#include "errmsg.h"
+#include "feature.h"
+#include "gamedata.h"
+#include "intdefs.h"
+#include "langext.h"
+#include "mem.h"
+#include "x86util.h"
+
+FEATURE("Portal \"ISG\" state reset (experimental)")
+GAMESPECIFIC(Portal1)
+REQUIRE_GAMEDATA(vtidx_CreateEnvironment)
+REQUIRE_GAMEDATA(vtidx_CreatePolyObject)
+REQUIRE_GAMEDATA(vtidx_RecheckCollisionFilter)
+
+static bool *isg_flag;
+static con_cmdcbv2 disconnect_cb;
+
+DEF_FEAT_CCMD_HERE(sst_portal_resetisg,
+ "Remove \"ISG\" state and disconnect from the server", 0) {
+ // TODO(compat): OE? guess it might work by accident due to cdecl, find out
+ disconnect_cb(&(struct con_cmdargs){0});
+ *isg_flag = false;
+}
+
+static void **find_physenv_vtable(void *CreateEnvironment) {
+ const uchar *insns = (uchar *)CreateEnvironment;
+ for (const uchar *p = insns; p - insns < 16;) {
+ if (*p == X86_CALL) { p = insns = p + 5 + mem_loads32(p + 1); goto _1; }
+ NEXT_INSN(p, "call to CreateEnvironment");
+ }
+ return 0;
+_1: for (const uchar *p = insns; p - insns < 32;) {
+ // tail call to the constructor
+ if (*p == X86_JMPIW) { insns = p + 5 + mem_loads32(p + 1); goto _2; }
+ NEXT_INSN(p, "call to CPhysicsEnvironment constructor");
+ }
+ return 0;
+_2: for (const uchar *p = insns; p - insns < 16;) {
+ // the vtable is loaded pretty early on:
+ // mov dword ptr [reg], <vtable address>
+ if (*p == X86_MOVMIW && (p[1] & 0xF8) == 0) return mem_loadptr(p + 2);
+ NEXT_INSN(p, "CPhysicsEnvironment vtable");
+ }
+ return 0;
+}
+
+static void **find_physobj_vtable(void *CreatePolyObject) {
+ const uchar *insns = (uchar *)CreatePolyObject;
+ for (const uchar *p = insns; p - insns < 64;) {
+ // first thing in the method is a call (after pushing a million params)
+ if (*p == X86_CALL) {
+ insns = p + 5 + mem_loads32(p + 1);
+ goto _1;
+ }
+ NEXT_INSN(p, "call to CreatePhysicsObject");
+ }
+ return 0;
+_1: for (const uchar *p = insns; p - insns < 768;) {
+ // there's a call to "new CPhysicsObject" somewhere down the line.
+ // the (always inlined) constructor calls memset on the obj to init it.
+ // the obj's vtable being loaded in is interleaved with pushing args
+ // for memset and the order for all the instructions varies between
+ // versions. the consistent bit is that `push 72` always happens shortly
+ // before the vtable is loaded.
+ if (*p == X86_PUSHI8 && p[1] == 72) { insns = p + 2; goto _2; }
+ NEXT_INSN(p, "push before CPhysicsObject vtable load");
+ }
+ return 0;
+_2: for (const uchar *p = insns; p - insns < 16;) {
+ // mov dword ptr [reg], <vtable address>
+ if (*p == X86_MOVMIW && (p[1] & 0xF8) == 0) return mem_loadptr(p + 2);
+ NEXT_INSN(p, "CPhysicsObject vtable");
+ }
+ return 0;
+}
+
+static bool find_isg_flag(void *RecheckCollisionFilter) {
+ const uchar *insns = (uchar *)RecheckCollisionFilter, *p = insns;
+ while (p - insns < 32) {
+ // besides some flag handling, the only thing this function does is
+ // call m_pObject->recheck_collision_filter()
+ if (*p == X86_CALL) {
+ p = p + 5 + mem_loads32(p + 1);
+ goto _1;
+ }
+ NEXT_INSN(p, "call to RecheckCollisionFilter");
+ }
+ return false;
+_1: for (insns = p; p - insns < 32;) {
+ // recheck_collision_filter pretty much just calls a function
+ if (*p == X86_CALL) {
+ p = p + 5 + mem_loads32(p + 1);
+ goto _2;
+ }
+ NEXT_INSN(p, "call to recheck_ov_element");
+ }
+ return false;
+_2: for (insns = p; p - insns < 0x300;) {
+ // mov byte ptr [g_fDeferDeleteMindist]
+ if (*p == X86_MOVMI8 && p[1] == X86_MODRM(0, 0, 5) && p[6] == 1) {
+ isg_flag = mem_loadptr(p + 2);
+ return true;
+ }
+ NEXT_INSN(p, "g_fDeferDeleteMindist");
+ }
+ return false;
+}
+
+INIT {
+ disconnect_cb = con_getcmdcbv2(con_findcmd("disconnect"));
+ if_cold(!disconnect_cb) return FEAT_INCOMPAT;
+ void *phys = factory_engine("VPhysics031", 0);
+ if_cold (phys == 0) {
+ errmsg_errorx("couldn't get IPhysics interface");
+ return FEAT_INCOMPAT;
+ }
+ void **vtable = mem_loadptr(phys);
+ vtable = find_physenv_vtable(vtable[vtidx_CreateEnvironment]);
+ if_cold (!vtable) {
+ errmsg_errorx("couldn't find CPhysicsEnvironment vtable");
+ return FEAT_INCOMPAT;
+ }
+ vtable = find_physobj_vtable(vtable[vtidx_CreatePolyObject]);
+ if_cold (!vtable) {
+ errmsg_errorx("couldn't find CPhysicsObject vtable");
+ return FEAT_INCOMPAT;
+ }
+ if_cold (!find_isg_flag(vtable[vtidx_RecheckCollisionFilter])) {
+ errmsg_errorx("couldn't find ISG flag");
+ return FEAT_INCOMPAT;
+ }
+ return FEAT_OK;
+}
diff --git a/src/rinput.c b/src/rinput.c
index 1e957b0..a7ed1a4 100644
--- a/src/rinput.c
+++ b/src/rinput.c
@@ -61,10 +61,10 @@ static union { // space saving
#define vtable_insys U.vtable_insys
DEF_CVAR_UNREG(m_rawinput, "Use Raw Input for mouse input (SST reimplementation)",
- 0, CON_ARCHIVE | CON_HIDDEN)
+ 0, CON_ARCHIVE | CON_INIT_HIDDEN)
DEF_CVAR_MINMAX(sst_mouse_factor, "Number of hardware mouse counts per step",
- 1, 1, 100, /*CON_ARCHIVE |*/ CON_HIDDEN)
+ 1, 1, 100, /*CON_ARCHIVE |*/ CON_INIT_HIDDEN)
static ssize __stdcall inproc(void *wnd, uint msg, usize wp, ssize lp) {
switch (msg) {
@@ -205,8 +205,11 @@ INIT {
hook_inline_commit(h1.prologue, (void *)&hook_GetCursorPos);
hook_inline_commit(h2.prologue, (void *)&hook_SetCursorPos);
-ok: m_rawinput->base.flags &= ~CON_HIDDEN;
- sst_mouse_factor->base.flags &= ~CON_HIDDEN;
+ok: // XXX: this is a little tricky and a little clunky. we have registered
+ // m_rawinput above but sst_mouse_factor will get auto-registered after init
+ // returns, so the flags are different.
+ con_unhide(&m_rawinput->base);
+ sst_mouse_factor->base.flags &= ~CON_INIT_HIDDEN;
return FEAT_OK;
e1: DestroyWindow(inwin);
diff --git a/src/sst.c b/src/sst.c
index 7628007..3ed92ee 100644
--- a/src/sst.c
+++ b/src/sst.c
@@ -240,11 +240,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 != 11
+#if VERSION_MAJOR != 0 || VERSION_MINOR != 17
#error Need to change this manually, since gluegen requires it to be spelled \
out in DEF_CVAR - better yet, can we get rid of this yet?
#endif
-DEF_CVAR(__sst_0_11_beta, "", 0, CON_HIDDEN | CON_DEMO)
+DEF_CVAR(__sst_0_17_beta, "", 0, CON_INIT_HIDDEN | CON_DEMO)
// most plugin callbacks are unused - define dummy functions for each signature
static void VCALLCONV nop_v_v(void *this) {}
@@ -270,7 +270,7 @@ static bool already_loaded = false, skip_unload = false;
// auto-update message. see below in do_featureinit()
static const char *updatenotes = "\
-* Fixed buggy addon fix behaviour in some L4D2 versions\n\
+* Fix sst_mouse_factor being hidden, causing toggle binds to break\n\
";
enum { // used in generated code, must line up with featmsgs arrays below
@@ -322,16 +322,15 @@ static void do_featureinit() {
}
void *inputsystemlib = os_dlhandle(OS_LIT("bin/") OS_LIT("inputsystem")
OS_LIT(OS_DLSUFFIX));
- if_cold (!inputsystemlib) {
- errmsg_warndl("couldn't get the input system library");
- }
- else if_cold (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib,
- "CreateInterface"))) {
- errmsg_warndl("couldn't get input system's CreateInterface");
- }
- else if_cold (!(inputsystem = factory_inputsystem(
- "InputSystemVersion001", 0))) {
- errmsg_warnx("missing input system interface");
+ if (inputsystemlib) { // might not have this, e.g. in OE. no point warning
+ if_cold (!(factory_inputsystem = (ifacefactory)os_dlsym(inputsystemlib,
+ "CreateInterface"))) {
+ errmsg_warndl("couldn't get input system's CreateInterface");
+ }
+ else if_cold (!(inputsystem = factory_inputsystem(
+ "InputSystemVersion001", 0))) {
+ errmsg_warnx("missing input system interface");
+ }
}
// ... and now for the real magic! (n.b. this also registers feature cvars)
initfeatures();
@@ -344,7 +343,7 @@ if (GAMETYPE_MATCHES(x)) { \
con_colourmsg(&purple, "%s%s", first ? "" : ", ", #x); \
first = false; \
}
- GAMETYPE_BASETAGS(PRINTTAG)
+ GAMETYPE_BASETAGS(PRINTTAG, PRINTTAG)
#undef PRINTTAG
con_colourmsg(&purple, "\n"); // xkcd 2109-compliant whitespace
#endif
@@ -395,8 +394,19 @@ static bool deferinit() {
// Arbitrary check to infer whether we've been early- or late-loaded.
// We used to just see whether gameui.dll/libgameui.so was loaded, but
// Portal 2 does away with the separate gameui library, so now we just call
- // CEngineVGui::IsInitialized() which works everywhere.
- if (VGuiIsInitialized(vgui)) return false;
+ // CEngineVGui::IsInitialized() which works everywhere on NE. On OE (Windows
+ // only), we still do the GameUI check, because I was struggling to get the
+ // vgui check to work consistently (maybe I just had the wrong vtable index
+ // for IsInitialized?). TODO(opt): I guess it would be faster to do the
+ // virtual call if we can figure out how to make it work...
+ if_hot (has_vtidx_VGuiIsInitialized) {
+ if (VGuiIsInitialized(vgui)) return false;
+ }
+#ifdef _WIN32
+ else {
+ if (GetModuleHandleW(L"GameUI.dll")) return false;
+ }
+#endif
sst_earlyloaded = true; // let other code know
if_cold (!os_mprot(vgui->vtable + vtidx_VGuiConnect, ssizeof(void *),
PAGE_READWRITE)) {
@@ -418,71 +428,116 @@ DEF_EVENT(PluginLoaded)
DEF_EVENT(PluginUnloaded)
static struct con_cmd *cmd_plugin_load, *cmd_plugin_unload;
-static con_cmdcb orig_plugin_load_cb, orig_plugin_unload_cb;
static int ownidx; // XXX: super hacky way of getting this to do_unload()
-static bool ispluginv1(const struct CPlugin *plugin) {
- // basename string is set with strncpy(), so if there's null bytes with more
- // stuff after, we can't be looking at a v2 struct. and we expect null bytes
- // in ifacever, since it's a small int value
- return (plugin->v2.basename[0] == 0 || plugin->v2.basename[0] == 1) &&
- plugin->v1.theplugin && plugin->v1.ifacever < 256 &&
- plugin->v1.ifacever;
+static int detectpluginver(const struct CPlugin *plugin) {
+ // if the first byte of basename is not 0 or 1, it can't be a bool value for
+ // paused, so this must be v3. XXX: there's an edge case where a string
+ // that starts with a 1 byte could be miscategorised but that should never
+ // happen in practice. still if we can think of a cleverer way around that
+ // it might be nice for the sake of absolute robustness.
+ if ((uchar)plugin->basename[0] > 1) return 3;
+ // if ifacever is not a small nonzero integer, it's not a version. treat it
+ // as the (possibly null) plugin pointer - meaning this is v1.
+ if (!plugin->v2.ifacever || (uint)plugin->v2.ifacever > 255) return 1;
+ return 2; // otherwise we just assume it must be v2!
}
-static void hook_plugin_load_cb(const struct con_cmdargs *args) {
- if (args->argc == 1) return;
- if (!CHECK_AllowPluginLoading(true)) return;
- orig_plugin_load_cb(args);
- EMIT_PluginLoaded();
+DEF_CCMD_COMPAT_HOOK(plugin_load) {
+ if (argc > 1 && !CHECK_AllowPluginLoading(true)) return;
+ int prevnplugins = pluginhandler->plugins.sz;
+ orig_plugin_load_cb(argc, argv);
+ // note: if loading fails, we won't see an increase in the plugin count.
+ // we of course only want to raise the PluginLoaded event on success
+ if (pluginhandler->plugins.sz != prevnplugins) EMIT_PluginLoaded();
}
-static void hook_plugin_unload_cb(const struct con_cmdargs *args) {
- if (args->argc == 1) return;
- if (!CHECK_AllowPluginLoading(false)) return;
- if (!*args->argv[1]) {
- errmsg_errorx("plugin_unload expects a numeric index");
- return;
- }
- // catch the very common user error of plugin_unload <name> and try to hint
- // people in the right direction. otherwise strings get atoi()d silently
- // into zero, which is just confusing and unhelpful. don't worry about
- // numeric range/overflow, worst case scenario we get a sev 9.8 CVE for it.
- char *end;
- int idx = strtol(args->argv[1], &end, 10);
- if (end == args->argv[1]) {
- errmsg_errorx("plugin_unload takes a number, not a name");
- errmsg_note("use plugin_print to get a list of plugin indices");
- return;
+
+// plugin_unload hook is kind of special. We have to force a tail call when
+// unloading SST itself, so we can't use the regular hook wrappers. Instead we
+// need to specifically and directly hook the two callback interfaces.
+static union {
+ con_cmdcbv1 v1;
+ con_cmdcbv2 v2;
+} orig_plugin_unload_cb;
+
+enum unload_action {
+ UNLOAD_SKIP,
+ UNLOAD_SELF,
+ UNLOAD_OTHER
+};
+static int hook_plugin_unload_common(int argc, const char **argv) {
+ if (argc > 1) {
+ if (!CHECK_AllowPluginLoading(false)) return UNLOAD_SKIP;
+ if (!*argv[1]) {
+ errmsg_errorx("plugin_unload expects a number, got an empty string");
+ return UNLOAD_SKIP;
+ }
+ // catch the very common user error of plugin_unload <name> and try to
+ // hint people in the right direction. otherwise strings get atoi()d
+ // silently into zero, which is confusing and unhelpful. don't worry
+ // about numeric range/overflow, worst case scenario it's a sev 9.8 CVE.
+ char *end;
+ int idx = strtol(argv[1], &end, 10);
+ if (end == argv[1]) {
+ errmsg_errorx("plugin_unload takes a number, not a name");
+ errmsg_note("use plugin_print to get a list of plugin indices");
+ return UNLOAD_SKIP;
+ }
+ if (*end) {
+ errmsg_errorx("unexpected trailing characters "
+ "(plugin_unload takes a number)");
+ return UNLOAD_SKIP;
+ }
+ struct CPlugin **plugins = pluginhandler->plugins.m.mem;
+ if_hot (idx >= 0 && idx < pluginhandler->plugins.sz) {
+ const struct CPlugin *plugin = plugins[idx];
+ // XXX: *could* memoise the detect call somehow, but... meh. effort.
+ const void *theplugin;
+ switch_exhaust (detectpluginver(plugin)) {
+ case 1: theplugin = plugin->v1.theplugin; break;
+ case 2: theplugin = plugin->v2.theplugin; break;
+ case 3: theplugin = plugin->v3.theplugin;
+ }
+ if (theplugin == &plugin_obj) {
+ sst_userunloaded = true;
+ ownidx = idx;
+ return UNLOAD_SELF;
+ }
+ // if it's some other plugin being unloaded, we can keep doing stuff
+ // after, so we raise the event.
+ return UNLOAD_OTHER;
+ }
}
- if (*end) {
- errmsg_errorx("unexpected trailing characters "
- "(plugin_unload takes a number)");
- return;
+ // error case, pass through to original to log the appropriate message
+ return UNLOAD_OTHER;
+}
+
+static void hook_plugin_unload_cbv1() {
+ extern int *_con_argc;
+ extern const char **_con_argv;
+ int action = hook_plugin_unload_common(*_con_argc, _con_argv);
+ switch_exhaust_enum(unload_action, action) {
+ case UNLOAD_SKIP:
+ return;
+ case UNLOAD_SELF:
+ tailcall orig_plugin_unload_cb.v1();
+ case UNLOAD_OTHER:
+ orig_plugin_unload_cb.v1();
+ EMIT_PluginUnloaded();
}
- struct CPlugin **plugins = pluginhandler->plugins.m.mem;
- 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) ?
- &plugin->v1: &plugin->v2.common;
- if (common->theplugin == &plugin_obj) {
- sst_userunloaded = true;
- ownidx = idx;
- }
-#ifdef __clang__
- // thanks clang for forcing use of return here and THEN warning about it
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wpedantic"
- __attribute__((musttail)) return orig_plugin_unload_cb(args);
-#pragma clang diagnostic pop
-#else
-#error We are tied to clang without an assembly solution for this!
-#endif
+}
+static void hook_plugin_unload_cbv2(struct con_cmdargs *args) {
+ int action = hook_plugin_unload_common(args->argc, args->argv);
+ switch_exhaust_enum(unload_action, action) {
+ case UNLOAD_SKIP:
+ return;
+ case UNLOAD_SELF:
+ tailcall orig_plugin_unload_cb.v2(args);
+ case UNLOAD_OTHER:
+ orig_plugin_unload_cb.v2(args);
+ EMIT_PluginUnloaded();
}
- // if it's some other plugin being unloaded, we can keep doing stuff after
- orig_plugin_unload_cb(args);
- EMIT_PluginUnloaded();
}
static bool do_load(ifacefactory enginef, ifacefactory serverf) {
@@ -492,6 +547,7 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) {
}
factory_engine = enginef; factory_server = serverf;
if_cold (!engineapi_init(ifacever)) return false;
+ if (GAMETYPE_MATCHES(OE)) shuntvars(); // see also comment in con_detect()
const void **p = vtable_firstdiff;
if (GAMETYPE_MATCHES(Portal2)) *p++ = (void *)&nop_p_v; // ClientFullyConnect
*p++ = (void *)&nop_p_v; // ClientDisconnect
@@ -509,11 +565,15 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) {
if (!deferinit()) { do_featureinit(); fixes_apply(); }
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;
+ hook_plugin_load_cb(cmd_plugin_load);
cmd_plugin_unload = con_findcmd("plugin_unload");
- orig_plugin_unload_cb = cmd_plugin_unload->cb;
- cmd_plugin_unload->cb = &hook_plugin_unload_cb;
+ orig_plugin_unload_cb.v1 = cmd_plugin_unload->cb_v1; // n.b.: union!
+ if (GAMETYPE_MATCHES(OE)) {
+ cmd_plugin_unload->cb_v1 = &hook_plugin_unload_cbv1;
+ }
+ else {
+ cmd_plugin_unload->cb_v2 = &hook_plugin_unload_cbv2;
+ }
}
return true;
}
@@ -521,18 +581,23 @@ static bool do_load(ifacefactory enginef, ifacefactory serverf) {
static void do_unload() {
// 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;
+ unhook_plugin_load_cb(cmd_plugin_load);
+ cmd_plugin_unload->cb_v1 = orig_plugin_unload_cb.v1;
#ifdef _WIN32 // this bit is only relevant in builds that predate linux support
struct CPlugin **plugins = pluginhandler->plugins.m.mem;
// see comment in CPlugin struct. setting this to the real handle right
// before the engine tries to unload us allows it to actually do so. in
- // newer branches this is redundant but doesn't do any harm so it's just
- // unconditional (for v1). NOTE: old engines ALSO just leak the handle
- // and never call Unload() if Load() fails; can't really do anything
- // about that.
+ // some newer branches still on v2 this is redundant but doesn't do any
+ // harm so it's just unconditional for simplicity. in v3 it's totally
+ // unnecessary so we skip it.. NOTE: older engines ALSO just leak the
+ // handle and never call Unload() if Load() fails; can't really do
+ // anything about that though.
struct CPlugin *plugin = plugins[ownidx];
- if (ispluginv1(plugin)) plugins[ownidx]->v1.module = ownhandle();
+ switch_exhaust (detectpluginver(plugin)) {
+ case 1: plugin->v1.module = ownhandle(); break;
+ case 2: plugin->v2.module = ownhandle(); break;
+ case 3:;
+ }
#endif
}
endfeatures();
diff --git a/src/vcall.h b/src/vcall.h
index f3a1f02..f91a6bd 100644
--- a/src/vcall.h
+++ b/src/vcall.h
@@ -23,13 +23,8 @@
*/
#ifdef _WIN32
-#if defined(__GNUC__) || defined(__clang__)
#define VCALLCONV __thiscall
#else
-// XXX: could support MSVC via __fastcall and dummy param, but is there a point?
-#error C __thiscall support requires Clang or GCC
-#endif
-#else
#define VCALLCONV
#endif
@@ -104,7 +99,7 @@
// I thought static inline was supposed to prevent unused warnings???
#if defined(__GNUC__) || defined(__clang__)
-#define _VCALL_UNUSED __attribute__((unused))
+#define _VCALL_UNUSED __attribute((unused))
#else
#define _VCALL_UNUSED
#endif
diff --git a/src/version.h b/src/version.h
index 893acc1..fb1b48b 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 11
-#define VERSION "0.11"
+#define VERSION_MINOR 17
+#define VERSION "0.17"
diff --git a/src/wincrt.c b/src/wincrt.c
index ced1203..9a0326b 100644
--- a/src/wincrt.c
+++ b/src/wincrt.c
@@ -10,11 +10,15 @@
//
// Is it actually reasonable to have to do any of this? Of course not.
+// Note: these functions have ifdefs with non-asm fallbacks just in case this
+// file is ever useful somewhere else, but generally we assume this codebase
+// will be built with Clang.
+
int memcmp(const void *restrict x, const void *restrict y, unsigned int sz) {
#if defined(__GNUC__) || defined(__clang__)
int a, b;
- __asm__ volatile (
- "xor %%eax, %%eax\n"
+ __asm volatile (
+ "xor eax, eax\n"
"repz cmpsb\n"
: "+D" (x), "+S" (y), "+c" (sz), "=@cca"(a), "=@ccb"(b)
:
@@ -34,7 +38,7 @@ int memcmp(const void *restrict x, const void *restrict y, unsigned int sz) {
void *memcpy(void *restrict x, const void *restrict y, unsigned int sz) {
#if defined(__GNUC__) || defined(__clang__)
void *r = x;
- __asm__ volatile (
+ __asm volatile (
"rep movsb\n"
: "+D" (x), "+S" (y), "+c" (sz)
:
@@ -48,13 +52,30 @@ void *memcpy(void *restrict x, const void *restrict y, unsigned int sz) {
#endif
}
+void *memset(void *x, int c, unsigned int sz) {
+#if defined(__GNUC__) || defined(__clang__)
+ void *r = x;
+ __asm volatile (
+ "rep stosb\n"
+ : "+D" (x), "+c" (sz)
+ : "a"(c)
+ : "memory"
+ );
+ return r;
+#else
+ const unsigned char *xb = x;
+ for (unsigned int i = 0; i < len; ++i) xb[i] = (unsigned char)c;
+ return x;
+#endif
+}
+
int __stdcall _DllMainCRTStartup(void *inst, unsigned int reason,
void *reserved) {
return 1;
}
#ifdef __clang__
-__attribute__((used))
+__attribute((used))
#endif
int _fltused = 1;
diff --git a/src/x86util.h b/src/x86util.h
index 33a3a08..433fbbf 100644
--- a/src/x86util.h
+++ b/src/x86util.h
@@ -17,9 +17,9 @@
#ifndef INC_X86UTIL_H
#define INC_X86UTIL_H
+#include "chunklets/x86.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 (and has been for years!)
diff --git a/src/xhair.c b/src/xhair.c
index 9d1ee34..9754f1e 100644
--- a/src/xhair.c
+++ b/src/xhair.c
@@ -20,7 +20,6 @@
#include "gamedata.h"
#include "hexcolour.h"
#include "hud.h"
-#include "intdefs.h"
#include "vcall.h"
FEATURE("custom crosshair drawing")
diff --git a/test/hook.test.c b/test/hook.test.c
index 625fdbf..6a9a7b4 100644
--- a/test/hook.test.c
+++ b/test/hook.test.c
@@ -4,7 +4,7 @@
#ifdef _WIN32
-#include "../src/x86.c"
+#include "../src/chunklets/x86.c"
#include "../src/hook.c"
#include "../src/os.c"
diff --git a/test/x86.test.c b/test/x86.test.c
index bf6e6e8..370a697 100644
--- a/test/x86.test.c
+++ b/test/x86.test.c
@@ -2,7 +2,10 @@
{.desc = "x86 opcode parsing"};
-#include "../src/x86.c"
+// TODO: should chunklets tests be moved or something? guess if/when that stuff
+// gets its own repo it would just go in there, right?
+
+#include "../src/chunklets/x86.c"
#include "../src/intdefs.h"
#include "../src/ppmagic.h"
diff --git a/tools/mkbindist.bat b/tools/mkbindist.bat
index ce1e8b1..00d49c3 100644
--- a/tools/mkbindist.bat
+++ b/tools/mkbindist.bat
@@ -21,8 +21,8 @@ md TEMP-%name% || goto :end
copy sst.dll TEMP-%name%\sst.dll || goto :end
copy dist\LICENCE.windows TEMP-%name%\LICENCE || goto :end
:: using midnight on release day to make zip deterministic! change on next release!
-powershell (Get-Item TEMP-%name%\sst.dll).LastWriteTime = new-object DateTime 2025, 5, 11, 0, 0, 0
-powershell (Get-Item TEMP-%name%\LICENCE).LastWriteTime = new-object DateTime 2025, 5, 11, 0, 0, 0
+powershell (Get-Item TEMP-%name%\sst.dll).LastWriteTime = new-object DateTime 2025, 12, 1, 0, 0, 0
+powershell (Get-Item TEMP-%name%\LICENCE).LastWriteTime = new-object DateTime 2025, 12, 1, 0, 0, 0
pushd TEMP-%name%
"%SEVENZIP%" a -mtc=off %name%.zip sst.dll LICENCE || goto :end
move %name%.zip ..\release\%name%.zip
diff --git a/tools/steamfix.bat b/tools/steamfix.bat
new file mode 100644
index 0000000..d0a77c2
--- /dev/null
+++ b/tools/steamfix.bat
@@ -0,0 +1,27 @@
+:: This file is dedicated to the public domain.
+@echo off
+
+:: In several old L4D2 builds, we currently have some weird black magic we don't
+:: fully understand to do what looks like DRM circumvention or... something.
+:: Annoyingly, that black magic manages to break regular use of Steam after the
+:: game exits. This is fixed by setting a registry key back to Steam's PID.
+
+:: The scripts used to launch those builds already do this, of course, but if
+:: you're launching L4D2 under a debugger, you can use this script instead.
+
+:: By the way, if anyone wants to look into solving the root cause so that none
+:: of this is needed any more, that would be cool!
+
+set REG=%SYSTEMROOT%\SysWOW64\reg.exe
+if not exist "%REG%" set REG=%SYSTEMROOT%\System32\reg.exe
+set steampid=
+for /F "usebackq skip=1 delims=" %%I in (
+ `wmic process where "name='steam.exe'" get processid 2^>nul`
+) do ( set steampid=%%I & goto :ok)
+:ok
+if not %steampid%=="" (
+ %REG% add "HKCU\SOFTWARE\Valve\Steam\ActiveProcess" /f /t REG_DWORD ^
+/v pid /d %steampid%>nul
+)
+
+:: vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/tools/windbg/.gitignore b/tools/windbg/.gitignore
new file mode 100644
index 0000000..ae3c172
--- /dev/null
+++ b/tools/windbg/.gitignore
@@ -0,0 +1 @@
+/bin/
diff --git a/tools/windbg/initcmds b/tools/windbg/initcmds
new file mode 100644
index 0000000..4fa517e
--- /dev/null
+++ b/tools/windbg/initcmds
@@ -0,0 +1,9 @@
+.nvload tools\windbg\natvis.xml
+
+$$ Emulate Source Thread Fix for high-core-count systems by breaking on
+$$ GetSystemInfo, grabbing the struct pointer from the stack, then fiddling
+$$ with its contents upon returning.
+bp kernelbase!GetSystemInfo "dx @$t1 = *(void **)(@esp + 4); bp /1 @$ra \"dx @$t2 = ((_SYSTEM_INFO *)@$t1)->dwNumberOfProcessors; dx ((_SYSTEM_INFO *)@$t1)->dwNumberOfProcessors = @$t2 > 24 ? 24 : @$t2; g\"; g"
+
+$$ Initial breakpoint was used to run the above commands. Now we can go ahead.
+g
diff --git a/tools/windbg/install.ps1 b/tools/windbg/install.ps1
new file mode 100644
index 0000000..4e206e0
--- /dev/null
+++ b/tools/windbg/install.ps1
@@ -0,0 +1,44 @@
+# This script is dedicated to the public domain.
+
+Add-Type -Assembly System.IO.Compression.FileSystem
+
+$OutDir = "tools\windbg\bin"
+$Arch = "x64" # can also use x86, arm64
+
+if (!(Test-Path $OutDir)) { $null = mkdir $OutDir }
+[xml]$content = (New-Object System.Net.WebClient).DownloadString("https://aka.ms/windbg/download")
+$bundleurl = $content.AppInstaller.MainBundle.Uri
+# Using curl.exe here instead because it has an actual useful progress bar.
+# Modern PowerShell does too, but not the PS 5.1 that still ships with W10
+#Invoke-WebRequest $bundleurl -OutFile $OutDir\__bundle.zip
+curl.exe "-#o$OutDir\__bundle.zip" "$bundleurl"
+$filename = switch ($Arch) {
+ "x64" { "windbg_win-x64.msix" }
+ "x86" { "windbg_win-x86.msix" }
+ "arm64" { "windbg_win-arm64.msix" }
+}
+$zip = [IO.Compression.ZipFile]::OpenRead("$OutDir\__bundle.zip")
+try {
+ if ($found = $zip.Entries.Where({ $_.FullName -eq $filename }, "First") ) {
+ $dest = "$OutDir\__msix.zip"
+ [IO.Compression.ZipFileExtensions]::ExtractToFile($found[0], $dest, $false)
+ }
+ else {
+ Write-Error "File not found in ZIP: $filename"
+ exit 100
+ }
+}
+finally {
+ if ($zip) { $zip.Dispose() }
+}
+rm $OutDir\__bundle.zip
+Expand-Archive -DestinationPath "$OutDir" "$OutDir\__msix.zip"
+rm $OutDir\__msix.zip
+# misc cleanup
+rm -r $OutDir\AppxMetadata\
+rm $OutDir\'``[Content_Types``].xml' # wtf, microsoft, wtf.
+rm $OutDir\AppxBlockMap.xml
+rm $OutDir\AppxManifest.xml
+rm $OutDir\AppxSignature.p7x
+rm -r $OutDir\runtimes\unix\
+mv "$OutDir\Third%20Party%20Notices.txt" "$OutDir\Third Party Notices.txt"
diff --git a/tools/windbg/natvis.xml b/tools/windbg/natvis.xml
new file mode 100644
index 0000000..159b4d0
--- /dev/null
+++ b/tools/windbg/natvis.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0"?>
+<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
+ <!-- trivial example, but we can add to this later! -->
+ <Type Name="con_var"><DisplayString>ConVar: {strval}</DisplayString></Type>
+</AutoVisualizer>
diff --git a/tools/windbg/windbg.bat b/tools/windbg/windbg.bat
new file mode 100644
index 0000000..2d2e6d2
--- /dev/null
+++ b/tools/windbg/windbg.bat
@@ -0,0 +1,16 @@
+:: This file is dedicated to the public domain.
+@echo off
+setlocal
+
+if not "%WINDBG_BIN%"=="" goto :ok
+set WINDBG_BIN=tools\windbg\bin
+if exist tools\windbg\bin\DbgX.Shell.exe goto :ok
+powershell tools\windbg\install.ps1 || goto :end
+
+:ok
+%WINDBG_BIN%\DbgX.Shell.exe /c $^<tools\windbg\initcmds
+
+:end
+exit /b %errorlevel%
+
+:: vi: sw=4 ts=4 noet tw=80 cc=80